pysapmanager

PySAPManager - Python SAP GUI Automation Library.

PySAPManager is a professional Python library for automating SAP operations via SAP GUI Scripting. It provides an object-oriented API to automate SAP tasks from Python by leveraging the SAP GUI Scripting engine. The package enables environment preparation, opening target SAP connections, manipulating sessions, executing VBScript scripts directly recorded from SAP GUI, and orchestrating parallel executions while respecting COM and SAP GUI constraints.

Key Features

  • Automated startup and supervision of the SAP Logon process
  • Opening and management of target SAP connections
  • Creation and retrieval of SAP sessions
  • Synchronous SAP script execution
  • Asynchronous SAP script execution with parallel control
  • Support for COM initialization in multi-threaded environments
  • Simple integration into automation workflows
  • Type hints and comprehensive error handling
  • Built-in support for Excel file operations within scripts

Prerequisites

  • SAP GUI installed on the workstation
  • SAP GUI Scripting enabled on the client side
  • SAP GUI Scripting authorized on the server side
  • Windows environment with COM support

Quick Start Example

from pysapmanager import SAPManager, SAPConfig, ScriptExecutionRequest

config = SAPConfig(
    sap_logon_path=r"C:\Program Files (x86)\SAP\FrontEnd\SAPGUI\saplogon.exe",
    connection_description="description",
)

with SAPManager(config=config) as manager:
    application = manager.get_application()
    connection = application.open_connection()
    session = connection.first_session()

    output = session.execute_script(
        ScriptExecutionRequest(
            script_path=r"C:\path\to\script.vbs",
            creates_excel=True,
            created_file_path=r"C:\path\to\output.xlsx",
        )
    )

Disabling Logging

By default, PySAPManager components log status messages via a simple console logger (or a custom logger you provide). To silence all log output from the library process-wide:

from pysapmanager import set_logging_enabled

set_logging_enabled(False)  # silence PySAPManager everywhere, including custom loggers

SAPManager also accepts an enable_logging constructor argument as a shortcut for the same global switch:

with SAPManager(config=config, enable_logging=False) as manager:
    ...  # no PySAPManager log output during this block or afterwards

Multi-Threaded Example

from pysapmanager import SAPManager, SAPConfig, ScriptExecutionRequest, com_initialized

config = SAPConfig(
    sap_logon_path=r"C:\Program Files (x86)\SAP\FrontEnd\SAPGUI\saplogon.exe",
    connection_description="description",
)

with com_initialized():
    with SAPManager(config=config, max_parallel_scripts=2) as manager:
        application = manager.get_application()
        connection = application.open_connection()
        first_session = connection.first_session()
        other_session = connection.create_session()

        # Submit scripts asynchronously
        first_task = first_session.execute_script_async(
            ScriptExecutionRequest(
                script_path=r"C:\path\to\script1.vbs",
                creates_excel=True,
                created_file_path=r"C:\path\to\output1.xlsx",
            )
        )

        other_task = other_session.execute_script_async(
            ScriptExecutionRequest(
                script_path=r"C:\path\to\script2.vbs",
                creates_excel=True,
                created_file_path=r"C:\path\to\output2.xlsx",
            )
        )

        # Your code executes in parallel with the scripts

        # Wait for all scripts to complete
        manager.wait_all(first_task, other_task)

        # Your code executes after scripts complete

Public API

The package exposes the following main classes and functions:

Attributes:
  • __title__ (str): The official name of the package.
  • __version__ (str): The current version number of the package.
  • __author__ (str): The author and maintainer of the package.
  • __license__ (str): The license under which the package is distributed.
  • __all__ (list of str): The public API exposed by the package for from pysapmanager import * usage.
  1"""# PySAPManager - Python SAP GUI Automation Library.
  2
  3PySAPManager is a professional Python library for automating SAP operations via SAP GUI
  4Scripting. It provides an object-oriented API to automate SAP tasks from Python by
  5leveraging the SAP GUI Scripting engine. The package enables environment preparation,
  6opening target SAP connections, manipulating sessions, executing VBScript scripts directly
  7recorded from SAP GUI, and orchestrating parallel executions while respecting COM and SAP GUI constraints.
  8
  9## Key Features
 10
 11- Automated startup and supervision of the **SAP Logon** process
 12- Opening and management of target **SAP** connections
 13- Creation and retrieval of **SAP** sessions
 14- Synchronous **SAP** script execution
 15- Asynchronous **SAP** script execution with parallel control
 16- Support for **COM** initialization in multi-threaded environments
 17- Simple integration into automation workflows
 18- Type hints and comprehensive error handling
 19- Built-in support for Excel file operations within scripts
 20
 21## Prerequisites
 22
 23- **SAP GUI** installed on the workstation
 24- **SAP GUI Scripting** enabled on the client side
 25- **SAP GUI Scripting** authorized on the server side
 26- **Windows** environment with **COM** support
 27
 28## Quick Start Example
 29
 30```python
 31from pysapmanager import SAPManager, SAPConfig, ScriptExecutionRequest
 32
 33config = SAPConfig(
 34    sap_logon_path=r"C:\\Program Files (x86)\\SAP\\FrontEnd\\SAPGUI\\saplogon.exe",
 35    connection_description="description",
 36)
 37
 38with SAPManager(config=config) as manager:
 39    application = manager.get_application()
 40    connection = application.open_connection()
 41    session = connection.first_session()
 42
 43    output = session.execute_script(
 44        ScriptExecutionRequest(
 45            script_path=r"C:\\path\\to\\script.vbs",
 46            creates_excel=True,
 47            created_file_path=r"C:\\path\\to\\output.xlsx",
 48        )
 49    )
 50```
 51
 52## Disabling Logging
 53
 54By default, PySAPManager components log status messages via a simple console
 55logger (or a custom logger you provide). To silence all log output from the
 56library process-wide:
 57
 58```python
 59from pysapmanager import set_logging_enabled
 60
 61set_logging_enabled(False)  # silence PySAPManager everywhere, including custom loggers
 62```
 63
 64`SAPManager` also accepts an `enable_logging` constructor argument as a
 65shortcut for the same global switch:
 66
 67```python
 68with SAPManager(config=config, enable_logging=False) as manager:
 69    ...  # no PySAPManager log output during this block or afterwards
 70```
 71
 72## Multi-Threaded Example
 73
 74```python
 75from pysapmanager import SAPManager, SAPConfig, ScriptExecutionRequest, com_initialized
 76
 77config = SAPConfig(
 78    sap_logon_path=r"C:\\Program Files (x86)\\SAP\\FrontEnd\\SAPGUI\\saplogon.exe",
 79    connection_description="description",
 80)
 81
 82with com_initialized():
 83    with SAPManager(config=config, max_parallel_scripts=2) as manager:
 84        application = manager.get_application()
 85        connection = application.open_connection()
 86        first_session = connection.first_session()
 87        other_session = connection.create_session()
 88
 89        # Submit scripts asynchronously
 90        first_task = first_session.execute_script_async(
 91            ScriptExecutionRequest(
 92                script_path=r"C:\\path\\to\\script1.vbs",
 93                creates_excel=True,
 94                created_file_path=r"C:\\path\\to\\output1.xlsx",
 95            )
 96        )
 97
 98        other_task = other_session.execute_script_async(
 99            ScriptExecutionRequest(
100                script_path=r"C:\\path\\to\\script2.vbs",
101                creates_excel=True,
102                created_file_path=r"C:\\path\\to\\output2.xlsx",
103            )
104        )
105
106        # Your code executes in parallel with the scripts
107
108        # Wait for all scripts to complete
109        manager.wait_all(first_task, other_task)
110
111        # Your code executes after scripts complete
112```
113
114## Public API
115
116The package exposes the following main classes and functions:
117
118- `SAPManager`: Main facade for SAP automation
119- `SAPApplication`: Access to SAP GUI Scripting application
120- `SAPConnection`: Represents an open SAP connection
121- `SAPSession`: Represents a SAP session
122- `SAPConfig`: Configuration for SAP connections
123- `SAPCredentials`: Login credentials for SAP
124- `ScriptExecutionRequest`: Script execution parameters
125- `com_initialized()`: Context manager for COM initialization
126- `set_logging_enabled(enabled)`: Globally enable/disable all library logging
127- `is_logging_enabled()`: Check the current global logging state
128
129Attributes:
130    __title__ (str): The official name of the package.
131    __version__ (str): The current version number of the package.
132    __author__ (str): The author and maintainer of the package.
133    __license__ (str): The license under which the package is distributed.
134    __all__ (list of str): The public API exposed by the package for `from pysapmanager import *` usage.
135"""
136
137from .application import SAPApplication
138from .com import com_initialized
139from .connection import SAPConnection
140from .manager import SAPManager
141from .models import SAPConfig, SAPCredentials, ScriptExecutionRequest
142from .session import SAPSession
143from .utils import is_logging_enabled, set_logging_enabled
144
145__title__ = "PySAPManager"
146__version__ = "0.1.0"
147__author__ = "yoqzii"
148__license__ = "MIT License"
149
150__all__ = [
151    "SAPManager",
152    "SAPApplication",
153    "SAPConnection",
154    "SAPSession",
155    "SAPConfig",
156    "SAPCredentials",
157    "ScriptExecutionRequest",
158    "com_initialized",
159    "set_logging_enabled",
160    "is_logging_enabled",
161]
class SAPManager:
 40class SAPManager:
 41    """Main facade for SAP GUI automation.
 42
 43    This class coordinates the different internal components necessary for
 44    controlling SAP GUI:
 45
 46    - SAPProcessService: Manages the SAP Logon process
 47    - SAPApplication: Accesses the SAP GUI Scripting application
 48    - VBScriptRunner: Executes scripts in parallel
 49
 50    The manager prepares the SAP environment and exposes the SAP GUI Scripting
 51    application, but does not itself open the connection. Connection opening
 52    is the responsibility of SAPApplication, consistent with the COM hierarchy
 53    of SAP GUI Scripting.
 54
 55    When a connection is opened via the application attached to this manager,
 56    the manager remembers it to enable automatic closing when exiting the
 57    `with` context.
 58
 59    Note:
 60        A single `SAPManager` can track **at most one** open connection at a
 61        time. Calling `application.open_connection()` a second time on the
 62        same manager before closing the first (via `close_connection()`)
 63        raises `SAPConnectionException`. If you need several simultaneous
 64        connections, create multiple sessions on one connection instead
 65        (`connection.create_session()`), or use separate `SAPManager`
 66        instances.
 67
 68    Attributes:
 69        _config (SAPConfig): Configuration settings for SAP and automation.
 70        _logger (Callable[[str, bool], None]): The logging function.
 71        _process (SAPProcessService): Service for managing SAP Logon processes.
 72        _runner (VBScriptRunner): Service for asynchronous script execution.
 73        _application (SAPApplication | None): Cached SAP GUI Scripting application.
 74        _connection (SAPConnection | None): The currently active/managed connection.
 75
 76    Example:
 77        Typical usage with a context manager:
 78
 79        ```python
 80        with SAPManager(config) as manager:
 81            application = manager.get_application()
 82            connection = application.open_connection()
 83            session = connection.first_session()
 84        ```
 85    """
 86
 87    def __init__(
 88        self,
 89        config: SAPConfig,
 90        *,
 91        logger: Callable[[str, bool], None] | None = None,
 92        file_validator: Callable[[Path], None] | None = None,
 93        max_parallel_scripts: int = 4,
 94        enable_logging: bool | None = None,
 95    ) -> None:
 96        """Initializes the main SAP GUI manager.
 97
 98        This method constructs the different internal services necessary for
 99        SAP automation from the provided configuration.
100
101        Args:
102            config: Global SAP configuration used for launching SAP Logon and
103                preparing the target connection.
104            logger: Logging function. If None, :func:`~pysapmanager.utils.default_logger`
105                is used. Expected signature:
106                `logger(message: str, critical: bool = False) -> None`.
107            file_validator: File validator eventually used by the script runner.
108                Expected signature: `file_validator(path: Path) -> None`.
109            max_parallel_scripts: Maximum number of scripts executed in parallel
110                by VBScriptRunner. Defaults to 4.
111            enable_logging: Convenience flag equivalent to calling
112                `pysapmanager.set_logging_enabled(enable_logging)` before
113                constructing the manager. Since the underlying switch is
114                library-wide (see :func:`pysapmanager.set_logging_enabled`),
115                passing `enable_logging=False` here silences **all**
116                PySAPManager components for the lifetime of the process, not
117                just this manager instance - including ones created before
118                or after this call, and it stays disabled until something
119                re-enables it. Defaults to `None`, which leaves the current
120                global logging state untouched (so this constructor never
121                surprises you by silently re-enabling logging you disabled
122                elsewhere). Pass `True` explicitly to force logging back on.
123        """
124        if enable_logging is not None:
125            set_logging_enabled(enable_logging)
126
127        self._config = config
128        self._logger = resolve_logger(logger)
129
130        self._process = SAPProcessService(
131            config.sap_logon_path,
132            logger=self._logger,
133        )
134        self._runner = VBScriptRunner(
135            max_workers=max_parallel_scripts,
136            file_validator=file_validator,
137            logger=self._logger,
138        )
139        self._application: SAPApplication | None = None
140        self._connection: SAPConnection | None = None
141
142    @property
143    def process(self) -> SAPProcessService:
144        """SAPProcessService: The SAP Logon process management service."""
145        return self._process
146
147    @property
148    def connection(self) -> SAPConnection | None:
149        """SAPConnection | None: The connection currently registered in the manager, 
150        or None if no connection is registered.
151        """
152        return self._connection
153
154    def get_application(
155        self, retry_count: int = 20, retry_delay: float = 0.5
156    ) -> SAPApplication:
157        """Returns the SAP GUI Scripting application associated with the manager.
158
159        The application is instantiated on demand and then cached for subsequent
160        accesses. This method serves as the normal entry point for obtaining
161        the SAPApplication object from the manager.
162
163        Args:
164            retry_count: Maximum number of attempts to get the SAP GUI Scripting
165                application. Defaults to 20.
166            retry_delay: Delay in seconds between attempts to access the SAP
167                GUI Scripting application. Defaults to 0.5.
168
169        Returns:
170            SAPApplication: Instance of the SAP GUI Scripting application.
171        """
172        if self._application is None:
173            self._application = SAPApplication(
174                config=self._config,
175                script_runner=self._runner,
176                retry_count=retry_count,
177                retry_delay=retry_delay,
178                logger=self._logger,
179                on_connection_opened=self._register_connection,
180            )
181        return self._application
182
183    def _register_connection(self, connection: SAPConnection) -> None:
184        """Registers the connection opened in the manager's context.
185
186        This method is called by the application when it opens a connection
187        in the manager's context. The manager can remember only one connection
188        at a time.
189
190        Args:
191            connection: SAP connection opened via the manager's application.
192
193        Raises:
194            SAPConnectionException: If a connection is already registered in
195                this manager.
196        """
197        if self._connection is not None:
198            raise SAPConnectionException(
199                "A SAP connection is already registered in this manager.",
200                critical=True,
201            )
202        self._connection = connection
203
204    @staticmethod
205    def wait_until(
206        condition: Callable[[], bool],
207        timeout: float = 10,
208        interval: float = 0.2,
209    ) -> bool:
210        """Waits for a condition to become true within a maximum delay.
211
212        This method periodically evaluates a condition until it returns True or
213        until the maximum delay expires. Exceptions raised by the condition are
214        ignored to tolerate transient states, such as during process startup or
215        shutdown.
216
217        Args:
218            condition: Function with no arguments called repeatedly.
219            timeout: Maximum wait duration in seconds.
220            interval: Delay in seconds between condition evaluations.
221
222        Returns:
223            bool: True if the condition becomes true before timeout expiration,
224                False otherwise.
225
226        Example:
227            Wait for a process to stop running:
228
229            ```python
230            SAPManager.wait_until(
231                lambda: not process.is_running(),
232                timeout=10
233            )
234            ```
235        """
236        start = time.time()
237        while time.time() - start < timeout:
238            try:
239                if condition():
240                    return True
241            except Exception:
242                pass
243            time.sleep(interval)
244        return False
245
246    def _reset_sap_logon(self) -> None:
247        """Closes then relaunches SAP Logon.
248
249        This method is used when the target connection defined in the
250        configuration is already open in SAP. It forces a complete restart
251        of SAP Logon to start from a clean state before reopening the connection.
252
253        Raises:
254            TimeoutError: If SAP Logon does not close or relaunch within the
255                allotted time.
256        """
257        self._logger("Closing SAP Logon.", False)
258        self._process.kill()
259
260        if not self.wait_until(lambda: not self._process.is_running()):
261            raise TimeoutError("SAP Logon did not close within the allotted time.")
262
263        self._logger("Reopening SAP Logon.", False)
264        self._process.launch()
265
266        if not self.wait_until(lambda: self._process.is_running()):
267            raise TimeoutError("SAP Logon did not open within the allotted time.")
268
269        self._application = None
270        self._connection = None
271
272    def prepare_sap_environment(self) -> None:
273        """Prepares the SAP environment for the configured target connection.
274
275        Behavior is as follows:
276
277        1. If SAP Logon is not running, it is started.
278        2. If SAP Logon is already running, open connections are inspected.
279        3. If the target connection is already open, SAP Logon is closed and
280           relaunched to ensure a clean state.
281        4. If the target connection is not open, the environment is preserved.
282
283        Raises:
284            TimeoutError: If SAP Logon does not open, close, or relaunch within
285                the allotted time.
286        """
287        if not self._process.is_running():
288            self._logger("SAP Logon is not open. Opening.", False)
289            self._process.launch()
290
291            if not self.wait_until(lambda: self._process.is_running()):
292                raise TimeoutError(
293                    "SAP Logon did not open within the allotted time."
294                )
295
296            self._application = None
297            return
298
299        self._application = None
300        application = self.get_application()
301
302        existing = application.find_connection(self._config.connection_description)
303        if existing is not None:
304            self._logger(
305                f"Target connection '{self._config.connection_description}' "
306                "is already open. Restarting SAP Logon.",
307                False,
308            )
309            self._reset_sap_logon()
310            return
311
312        self._logger(
313            f"Target connection '{self._config.connection_description}' "
314            "is not open. Current environment can be used.",
315            False,
316        )
317
318    def close_connection(self) -> None:
319        """Closes the connection registered in the manager, if any.
320
321        Raises:
322            SAPConnectionException: If the connection closing fails.
323        """
324        if self._connection is None:
325            return
326
327        self._connection.close()
328        self._connection = None
329        self._logger("Manager connection closed.", False)
330
331    def wait_all(self, *futures: Future[Any]) -> None:
332        """Waits for all asynchronous tasks to complete and reraises any exceptions.
333
334        Args:
335            *futures: Set of asynchronous task Future objects to wait for.
336
337        Raises:
338            Exception: Any exception raised in one of the tasks is propagated
339                when calling `future.result()`.
340        """
341        done, _ = wait(futures)
342        for future in done:
343            future.result()
344
345    def shutdown(self) -> None:
346        """Releases the manager's internal resources.
347
348        Gracefully stops components requiring explicit termination, such as
349        the script runner's thread pool.
350        """
351        self._runner.shutdown()
352
353    def __enter__(self) -> SAPManager:
354        """Context manager entry.
355
356        Prepares the SAP environment and returns the manager instance.
357
358        Returns:
359            SAPManager: The initialized manager instance.
360        """
361        self.prepare_sap_environment()
362        return self
363
364    def __exit__(
365        self,
366        exc_type: type[BaseException] | None,
367        exc_value: BaseException | None,
368        traceback: TracebackType | None,
369    ) -> None:
370        """Context manager exit.
371
372        Closes the registered connection then releases internal resources,
373        ensuring cleanup regardless of errors within the `with` block.
374        """
375        try:
376            self.close_connection()
377        finally:
378            self.shutdown()

Main facade for SAP GUI automation.

This class coordinates the different internal components necessary for controlling SAP GUI:

  • SAPProcessService: Manages the SAP Logon process
  • SAPApplication: Accesses the SAP GUI Scripting application
  • VBScriptRunner: Executes scripts in parallel

The manager prepares the SAP environment and exposes the SAP GUI Scripting application, but does not itself open the connection. Connection opening is the responsibility of SAPApplication, consistent with the COM hierarchy of SAP GUI Scripting.

When a connection is opened via the application attached to this manager, the manager remembers it to enable automatic closing when exiting the with context.

Note:

A single SAPManager can track at most one open connection at a time. Calling application.open_connection() a second time on the same manager before closing the first (via close_connection()) raises SAPConnectionException. If you need several simultaneous connections, create multiple sessions on one connection instead (connection.create_session()), or use separate SAPManager instances.

Attributes:
  • _config (SAPConfig): Configuration settings for SAP and automation.
  • _logger (Callable[[str, bool], None]): The logging function.
  • _process (SAPProcessService): Service for managing SAP Logon processes.
  • _runner (VBScriptRunner): Service for asynchronous script execution.
  • _application (SAPApplication | None): Cached SAP GUI Scripting application.
  • _connection (SAPConnection | None): The currently active/managed connection.
Example:

Typical usage with a context manager:

with SAPManager(config) as manager:
    application = manager.get_application()
    connection = application.open_connection()
    session = connection.first_session()
SAPManager( config: SAPConfig, *, logger: Callable[[str, bool], None] | None = None, file_validator: Callable[[pathlib._local.Path], None] | None = None, max_parallel_scripts: int = 4, enable_logging: bool | None = None)
 87    def __init__(
 88        self,
 89        config: SAPConfig,
 90        *,
 91        logger: Callable[[str, bool], None] | None = None,
 92        file_validator: Callable[[Path], None] | None = None,
 93        max_parallel_scripts: int = 4,
 94        enable_logging: bool | None = None,
 95    ) -> None:
 96        """Initializes the main SAP GUI manager.
 97
 98        This method constructs the different internal services necessary for
 99        SAP automation from the provided configuration.
100
101        Args:
102            config: Global SAP configuration used for launching SAP Logon and
103                preparing the target connection.
104            logger: Logging function. If None, :func:`~pysapmanager.utils.default_logger`
105                is used. Expected signature:
106                `logger(message: str, critical: bool = False) -> None`.
107            file_validator: File validator eventually used by the script runner.
108                Expected signature: `file_validator(path: Path) -> None`.
109            max_parallel_scripts: Maximum number of scripts executed in parallel
110                by VBScriptRunner. Defaults to 4.
111            enable_logging: Convenience flag equivalent to calling
112                `pysapmanager.set_logging_enabled(enable_logging)` before
113                constructing the manager. Since the underlying switch is
114                library-wide (see :func:`pysapmanager.set_logging_enabled`),
115                passing `enable_logging=False` here silences **all**
116                PySAPManager components for the lifetime of the process, not
117                just this manager instance - including ones created before
118                or after this call, and it stays disabled until something
119                re-enables it. Defaults to `None`, which leaves the current
120                global logging state untouched (so this constructor never
121                surprises you by silently re-enabling logging you disabled
122                elsewhere). Pass `True` explicitly to force logging back on.
123        """
124        if enable_logging is not None:
125            set_logging_enabled(enable_logging)
126
127        self._config = config
128        self._logger = resolve_logger(logger)
129
130        self._process = SAPProcessService(
131            config.sap_logon_path,
132            logger=self._logger,
133        )
134        self._runner = VBScriptRunner(
135            max_workers=max_parallel_scripts,
136            file_validator=file_validator,
137            logger=self._logger,
138        )
139        self._application: SAPApplication | None = None
140        self._connection: SAPConnection | None = None

Initializes the main SAP GUI manager.

This method constructs the different internal services necessary for SAP automation from the provided configuration.

Arguments:
  • config: Global SAP configuration used for launching SAP Logon and preparing the target connection.
  • logger: Logging function. If None, ~pysapmanager.utils.default_logger() is used. Expected signature: logger(message: str, critical: bool = False) -> None.
  • file_validator: File validator eventually used by the script runner. Expected signature: file_validator(path: Path) -> None.
  • max_parallel_scripts: Maximum number of scripts executed in parallel by VBScriptRunner. Defaults to 4.
  • enable_logging: Convenience flag equivalent to calling pysapmanager.set_logging_enabled(enable_logging) before constructing the manager. Since the underlying switch is library-wide (see pysapmanager.set_logging_enabled()), passing enable_logging=False here silences all PySAPManager components for the lifetime of the process, not just this manager instance - including ones created before or after this call, and it stays disabled until something re-enables it. Defaults to None, which leaves the current global logging state untouched (so this constructor never surprises you by silently re-enabling logging you disabled elsewhere). Pass True explicitly to force logging back on.
process: pysapmanager.process.SAPProcessService
142    @property
143    def process(self) -> SAPProcessService:
144        """SAPProcessService: The SAP Logon process management service."""
145        return self._process

SAPProcessService: The SAP Logon process management service.

connection: SAPConnection | None
147    @property
148    def connection(self) -> SAPConnection | None:
149        """SAPConnection | None: The connection currently registered in the manager, 
150        or None if no connection is registered.
151        """
152        return self._connection

SAPConnection | None: The connection currently registered in the manager, or None if no connection is registered.

def get_application( self, retry_count: int = 20, retry_delay: float = 0.5) -> SAPApplication:
154    def get_application(
155        self, retry_count: int = 20, retry_delay: float = 0.5
156    ) -> SAPApplication:
157        """Returns the SAP GUI Scripting application associated with the manager.
158
159        The application is instantiated on demand and then cached for subsequent
160        accesses. This method serves as the normal entry point for obtaining
161        the SAPApplication object from the manager.
162
163        Args:
164            retry_count: Maximum number of attempts to get the SAP GUI Scripting
165                application. Defaults to 20.
166            retry_delay: Delay in seconds between attempts to access the SAP
167                GUI Scripting application. Defaults to 0.5.
168
169        Returns:
170            SAPApplication: Instance of the SAP GUI Scripting application.
171        """
172        if self._application is None:
173            self._application = SAPApplication(
174                config=self._config,
175                script_runner=self._runner,
176                retry_count=retry_count,
177                retry_delay=retry_delay,
178                logger=self._logger,
179                on_connection_opened=self._register_connection,
180            )
181        return self._application

Returns the SAP GUI Scripting application associated with the manager.

The application is instantiated on demand and then cached for subsequent accesses. This method serves as the normal entry point for obtaining the SAPApplication object from the manager.

Arguments:
  • retry_count: Maximum number of attempts to get the SAP GUI Scripting application. Defaults to 20.
  • retry_delay: Delay in seconds between attempts to access the SAP GUI Scripting application. Defaults to 0.5.
Returns:

SAPApplication: Instance of the SAP GUI Scripting application.

@staticmethod
def wait_until( condition: Callable[[], bool], timeout: float = 10, interval: float = 0.2) -> bool:
204    @staticmethod
205    def wait_until(
206        condition: Callable[[], bool],
207        timeout: float = 10,
208        interval: float = 0.2,
209    ) -> bool:
210        """Waits for a condition to become true within a maximum delay.
211
212        This method periodically evaluates a condition until it returns True or
213        until the maximum delay expires. Exceptions raised by the condition are
214        ignored to tolerate transient states, such as during process startup or
215        shutdown.
216
217        Args:
218            condition: Function with no arguments called repeatedly.
219            timeout: Maximum wait duration in seconds.
220            interval: Delay in seconds between condition evaluations.
221
222        Returns:
223            bool: True if the condition becomes true before timeout expiration,
224                False otherwise.
225
226        Example:
227            Wait for a process to stop running:
228
229            ```python
230            SAPManager.wait_until(
231                lambda: not process.is_running(),
232                timeout=10
233            )
234            ```
235        """
236        start = time.time()
237        while time.time() - start < timeout:
238            try:
239                if condition():
240                    return True
241            except Exception:
242                pass
243            time.sleep(interval)
244        return False

Waits for a condition to become true within a maximum delay.

This method periodically evaluates a condition until it returns True or until the maximum delay expires. Exceptions raised by the condition are ignored to tolerate transient states, such as during process startup or shutdown.

Arguments:
  • condition: Function with no arguments called repeatedly.
  • timeout: Maximum wait duration in seconds.
  • interval: Delay in seconds between condition evaluations.
Returns:

bool: True if the condition becomes true before timeout expiration, False otherwise.

Example:

Wait for a process to stop running:

SAPManager.wait_until(
    lambda: not process.is_running(),
    timeout=10
)
def prepare_sap_environment(self) -> None:
272    def prepare_sap_environment(self) -> None:
273        """Prepares the SAP environment for the configured target connection.
274
275        Behavior is as follows:
276
277        1. If SAP Logon is not running, it is started.
278        2. If SAP Logon is already running, open connections are inspected.
279        3. If the target connection is already open, SAP Logon is closed and
280           relaunched to ensure a clean state.
281        4. If the target connection is not open, the environment is preserved.
282
283        Raises:
284            TimeoutError: If SAP Logon does not open, close, or relaunch within
285                the allotted time.
286        """
287        if not self._process.is_running():
288            self._logger("SAP Logon is not open. Opening.", False)
289            self._process.launch()
290
291            if not self.wait_until(lambda: self._process.is_running()):
292                raise TimeoutError(
293                    "SAP Logon did not open within the allotted time."
294                )
295
296            self._application = None
297            return
298
299        self._application = None
300        application = self.get_application()
301
302        existing = application.find_connection(self._config.connection_description)
303        if existing is not None:
304            self._logger(
305                f"Target connection '{self._config.connection_description}' "
306                "is already open. Restarting SAP Logon.",
307                False,
308            )
309            self._reset_sap_logon()
310            return
311
312        self._logger(
313            f"Target connection '{self._config.connection_description}' "
314            "is not open. Current environment can be used.",
315            False,
316        )

Prepares the SAP environment for the configured target connection.

Behavior is as follows:

  1. If SAP Logon is not running, it is started.
  2. If SAP Logon is already running, open connections are inspected.
  3. If the target connection is already open, SAP Logon is closed and relaunched to ensure a clean state.
  4. If the target connection is not open, the environment is preserved.
Raises:
  • TimeoutError: If SAP Logon does not open, close, or relaunch within the allotted time.
def close_connection(self) -> None:
318    def close_connection(self) -> None:
319        """Closes the connection registered in the manager, if any.
320
321        Raises:
322            SAPConnectionException: If the connection closing fails.
323        """
324        if self._connection is None:
325            return
326
327        self._connection.close()
328        self._connection = None
329        self._logger("Manager connection closed.", False)

Closes the connection registered in the manager, if any.

Raises:
  • SAPConnectionException: If the connection closing fails.
def wait_all(self, *futures: concurrent.futures._base.Future[typing.Any]) -> None:
331    def wait_all(self, *futures: Future[Any]) -> None:
332        """Waits for all asynchronous tasks to complete and reraises any exceptions.
333
334        Args:
335            *futures: Set of asynchronous task Future objects to wait for.
336
337        Raises:
338            Exception: Any exception raised in one of the tasks is propagated
339                when calling `future.result()`.
340        """
341        done, _ = wait(futures)
342        for future in done:
343            future.result()

Waits for all asynchronous tasks to complete and reraises any exceptions.

Arguments:
  • *futures: Set of asynchronous task Future objects to wait for.
Raises:
  • Exception: Any exception raised in one of the tasks is propagated when calling future.result().
def shutdown(self) -> None:
345    def shutdown(self) -> None:
346        """Releases the manager's internal resources.
347
348        Gracefully stops components requiring explicit termination, such as
349        the script runner's thread pool.
350        """
351        self._runner.shutdown()

Releases the manager's internal resources.

Gracefully stops components requiring explicit termination, such as the script runner's thread pool.

class SAPApplication:
 31class SAPApplication:
 32    """Represents the SAP GUI Scripting application (GuiApplication).
 33
 34    This class encapsulates access to the SAP GUI Scripting application
 35    exposed by COM and provides the main operations related to open or
 36    unopened SAP connections.
 37
 38    The instance automatically retrieves the COM GuiApplication object upon
 39    creation, with a configurable retry mechanism to tolerate availability
 40    delays in SAP GUI immediately after SAP Logon startup.
 41
 42    Attributes:
 43        _config (SAPConfig): Global configuration for SAP connection parameters.
 44        _script_runner (VBScriptRunner): Executor for VBScript files.
 45        _retry_count (int): Maximum attempts to retrieve COM interface.
 46        _retry_delay (float): Seconds to wait between retry attempts.
 47        _logger (Callable[[str, bool], None]): Logging function for reporting status.
 48        _on_connection_opened (Callable[[SAPConnection], None] | None): Callback for new connections.
 49        _com_application (Any): The underlying COM GuiApplication object.
 50    """
 51
 52    def __init__(
 53        self,
 54        *,
 55        config: SAPConfig,
 56        script_runner: VBScriptRunner,
 57        retry_count: int = 20,
 58        retry_delay: float = 0.5,
 59        logger: Callable[[str, bool], None] | None = None,
 60        on_connection_opened: Callable[[SAPConnection], None] | None = None,
 61    ) -> None:
 62        """Initializes the SAP GUI Scripting application.
 63
 64        Args:
 65            config: Global SAP configuration used particularly for opening the
 66                target connection.
 67            script_runner: VBScript executor used by connections and sessions
 68                created from this application.
 69            retry_count: Maximum number of attempts to retrieve the SAP GUI Scripting
 70                application. Defaults to 20.
 71            retry_delay: Delay in seconds between successive retry attempts.
 72                Defaults to 0.5.
 73            logger: Logging function. If None, :func:`~pysapmanager.utils.default_logger`
 74                is used. Regardless of which logger is used, all output is
 75                subject to the library-wide switch controlled by
 76                :func:`pysapmanager.set_logging_enabled`.
 77            on_connection_opened: Callback invoked when a connection is opened
 78                successfully. Allows a calling component like the manager to
 79                register the opened connection.
 80
 81        Raises:
 82            SAPApplicationException: If the SAP GUI Scripting application cannot
 83                be retrieved after the specified retry attempts.
 84        """
 85        self._config = config
 86        self._script_runner = script_runner
 87        self._retry_count = retry_count
 88        self._retry_delay = retry_delay
 89        self._logger = resolve_logger(logger)
 90        self._on_connection_opened = on_connection_opened
 91        self._com_application = self._acquire_com_application()
 92
 93    def _acquire_com_application(self) -> Any:
 94        """Retrieves the COM GuiApplication object from SAP GUI Scripting.
 95
 96        This method attempts to retrieve the SAPGUI COM object via
 97        win32com.client.GetObject, then accesses its GetScriptingEngine
 98        property to obtain the GuiApplication object.
 99
100        On failure, multiple attempts are made according to the configuration,
101        with a delay between each attempt.
102
103        Returns:
104            The COM GuiApplication object returned by SAP GUI Scripting.
105
106        Raises:
107            SAPApplicationException: If the SAPGUI object is not found, if the
108                scripting engine is unavailable, or if all retry attempts fail.
109        """
110        last_error: Exception | None = None
111
112        for attempt in range(1, self._retry_count + 1):
113            try:
114                sap_gui = win32.GetObject("SAPGUI")
115
116                application = sap_gui.GetScriptingEngine
117                if application is None:
118                    raise SAPApplicationException(
119                        "SAP GUI Scripting engine is unavailable."
120                    )
121
122                self._logger("SAP GUI Scripting application retrieved successfully.", False)
123                return application
124
125            except Exception as exc:
126                last_error = exc
127                self._logger(
128                    f"Attempt {attempt}/{self._retry_count} to access SAP GUI Scripting failed: {exc}",
129                    True,
130                )
131                if attempt < self._retry_count:
132                    time.sleep(self._retry_delay)
133
134        raise SAPApplicationException(
135            f"Unable to access SAP GUI Scripting engine. Ensure SAP GUI Scripting is enabled. "
136            f"Detailed error: {last_error}",
137            critical=True,
138        ) from last_error
139
140    @property
141    def com_object(self) -> Any:
142        """Returns the raw COM GuiApplication object.
143
144        This property exposes the underlying COM object directly to enable
145        advanced use cases not covered by the library's business API.
146
147        Returns:
148            The raw COM object representing the SAP GUI Scripting application.
149        """
150        return self._com_application
151
152    def iter_connections(self) -> Iterator[SAPConnection]:
153        """Iterates over open SAP connections via native COM enumeration.
154
155        This method relies on the COM collection Application.Children as
156        exposed by SAP GUI Scripting. Each COM connection retrieved is wrapped
157        in a SAPConnection instance.
158
159        Yields:
160            SAPConnection: Each open connection currently managed by the application.
161
162        Raises:
163            SAPApplicationException: If iteration over open connections fails due to
164                COM communication errors.
165        """
166        try:
167            for com_connection in self._com_application.Children:
168                yield SAPConnection(
169                    application=self,
170                    com_connection=com_connection,
171                    config=self._config,
172                    script_runner=self._script_runner,
173                    logger=self._logger,
174                )
175        except Exception as exc:
176            raise SAPApplicationException(
177                f"Failed to iterate over open SAP connections: {exc}",
178                critical=True,
179            ) from exc
180
181    def find_connection(self, description: str) -> SAPConnection | None:
182        """Searches for an open connection by its description.
183
184        The comparison is performed on the Description property of each open
185        SAP connection. The description matching is case-sensitive after
186        stripping whitespace.
187
188        Args:
189            description: The exact description of the connection to search for.
190
191        Returns:
192            The matching SAPConnection instance if found, otherwise None.
193        """
194        expected = description.strip()
195
196        for connection in self.iter_connections():
197            try:
198                current = connection.description.strip()
199                if current == expected:
200                    return connection
201            except Exception:
202                continue
203
204        return None
205
206    def open_connection(self, sync: bool = True) -> SAPConnection:
207        """Opens the target SAP connection defined in the configuration.
208
209        This method calls OpenConnection() on the COM application using the
210        connection description defined in SAPConfig. Once the COM connection
211        is obtained, it is wrapped in a SAPConnection instance.
212
213        If the configuration requests it, SAP credentials are automatically
214        injected. If a registration callback was provided, the opened connection
215        is notified to the calling component so it can be remembered.
216
217        Args:
218            sync: Whether the connection opening should be synchronous (waits
219                for the window to fully open). Defaults to True.
220
221        Returns:
222            The open SAP connection wrapped in a business-logic object.
223
224        Raises:
225            SAPConnectionException: If OpenConnection returns None, if the
226                connection description is invalid, or if an underlying COM error
227                occurs during the connection process.
228        """
229        try:
230            com_connection = self._com_application.OpenConnection(
231                self._config.connection_description,
232                sync,
233            )
234
235            if com_connection is None:
236                raise SAPConnectionException(
237                    "OpenConnection returned None (connection failed).",
238                    critical=True,
239                )
240
241            connection = SAPConnection(
242                application=self,
243                com_connection=com_connection,
244                config=self._config,
245                script_runner=self._script_runner,
246                logger=self._logger,
247            )
248            connection.fill_credentials_if_needed()
249
250            if self._on_connection_opened is not None:
251                self._on_connection_opened(connection)
252
253            self._logger(f"SAP connection opened: {connection.description}", False)
254            return connection
255
256        except SAPConnectionException:
257            raise
258        except Exception as exc:
259            raise SAPConnectionException(
260                f"Failed to open SAP connection '{self._config.connection_description}': {exc}",
261                critical=True,
262            ) from exc

Represents the SAP GUI Scripting application (GuiApplication).

This class encapsulates access to the SAP GUI Scripting application exposed by COM and provides the main operations related to open or unopened SAP connections.

The instance automatically retrieves the COM GuiApplication object upon creation, with a configurable retry mechanism to tolerate availability delays in SAP GUI immediately after SAP Logon startup.

Attributes:
  • _config (SAPConfig): Global configuration for SAP connection parameters.
  • _script_runner (VBScriptRunner): Executor for VBScript files.
  • _retry_count (int): Maximum attempts to retrieve COM interface.
  • _retry_delay (float): Seconds to wait between retry attempts.
  • _logger (Callable[[str, bool], None]): Logging function for reporting status.
  • _on_connection_opened (Callable[[SAPConnection], None] | None): Callback for new connections.
  • _com_application (Any): The underlying COM GuiApplication object.
SAPApplication( *, config: SAPConfig, script_runner: pysapmanager.script_runner.VBScriptRunner, retry_count: int = 20, retry_delay: float = 0.5, logger: Callable[[str, bool], None] | None = None, on_connection_opened: Callable[[SAPConnection], None] | None = None)
52    def __init__(
53        self,
54        *,
55        config: SAPConfig,
56        script_runner: VBScriptRunner,
57        retry_count: int = 20,
58        retry_delay: float = 0.5,
59        logger: Callable[[str, bool], None] | None = None,
60        on_connection_opened: Callable[[SAPConnection], None] | None = None,
61    ) -> None:
62        """Initializes the SAP GUI Scripting application.
63
64        Args:
65            config: Global SAP configuration used particularly for opening the
66                target connection.
67            script_runner: VBScript executor used by connections and sessions
68                created from this application.
69            retry_count: Maximum number of attempts to retrieve the SAP GUI Scripting
70                application. Defaults to 20.
71            retry_delay: Delay in seconds between successive retry attempts.
72                Defaults to 0.5.
73            logger: Logging function. If None, :func:`~pysapmanager.utils.default_logger`
74                is used. Regardless of which logger is used, all output is
75                subject to the library-wide switch controlled by
76                :func:`pysapmanager.set_logging_enabled`.
77            on_connection_opened: Callback invoked when a connection is opened
78                successfully. Allows a calling component like the manager to
79                register the opened connection.
80
81        Raises:
82            SAPApplicationException: If the SAP GUI Scripting application cannot
83                be retrieved after the specified retry attempts.
84        """
85        self._config = config
86        self._script_runner = script_runner
87        self._retry_count = retry_count
88        self._retry_delay = retry_delay
89        self._logger = resolve_logger(logger)
90        self._on_connection_opened = on_connection_opened
91        self._com_application = self._acquire_com_application()

Initializes the SAP GUI Scripting application.

Arguments:
  • config: Global SAP configuration used particularly for opening the target connection.
  • script_runner: VBScript executor used by connections and sessions created from this application.
  • retry_count: Maximum number of attempts to retrieve the SAP GUI Scripting application. Defaults to 20.
  • retry_delay: Delay in seconds between successive retry attempts. Defaults to 0.5.
  • logger: Logging function. If None, ~pysapmanager.utils.default_logger() is used. Regardless of which logger is used, all output is subject to the library-wide switch controlled by pysapmanager.set_logging_enabled().
  • on_connection_opened: Callback invoked when a connection is opened successfully. Allows a calling component like the manager to register the opened connection.
Raises:
  • SAPApplicationException: If the SAP GUI Scripting application cannot be retrieved after the specified retry attempts.
com_object: Any
140    @property
141    def com_object(self) -> Any:
142        """Returns the raw COM GuiApplication object.
143
144        This property exposes the underlying COM object directly to enable
145        advanced use cases not covered by the library's business API.
146
147        Returns:
148            The raw COM object representing the SAP GUI Scripting application.
149        """
150        return self._com_application

Returns the raw COM GuiApplication object.

This property exposes the underlying COM object directly to enable advanced use cases not covered by the library's business API.

Returns:

The raw COM object representing the SAP GUI Scripting application.

def iter_connections(self) -> Iterator[SAPConnection]:
152    def iter_connections(self) -> Iterator[SAPConnection]:
153        """Iterates over open SAP connections via native COM enumeration.
154
155        This method relies on the COM collection Application.Children as
156        exposed by SAP GUI Scripting. Each COM connection retrieved is wrapped
157        in a SAPConnection instance.
158
159        Yields:
160            SAPConnection: Each open connection currently managed by the application.
161
162        Raises:
163            SAPApplicationException: If iteration over open connections fails due to
164                COM communication errors.
165        """
166        try:
167            for com_connection in self._com_application.Children:
168                yield SAPConnection(
169                    application=self,
170                    com_connection=com_connection,
171                    config=self._config,
172                    script_runner=self._script_runner,
173                    logger=self._logger,
174                )
175        except Exception as exc:
176            raise SAPApplicationException(
177                f"Failed to iterate over open SAP connections: {exc}",
178                critical=True,
179            ) from exc

Iterates over open SAP connections via native COM enumeration.

This method relies on the COM collection Application.Children as exposed by SAP GUI Scripting. Each COM connection retrieved is wrapped in a SAPConnection instance.

Yields:

SAPConnection: Each open connection currently managed by the application.

Raises:
  • SAPApplicationException: If iteration over open connections fails due to COM communication errors.
def find_connection(self, description: str) -> SAPConnection | None:
181    def find_connection(self, description: str) -> SAPConnection | None:
182        """Searches for an open connection by its description.
183
184        The comparison is performed on the Description property of each open
185        SAP connection. The description matching is case-sensitive after
186        stripping whitespace.
187
188        Args:
189            description: The exact description of the connection to search for.
190
191        Returns:
192            The matching SAPConnection instance if found, otherwise None.
193        """
194        expected = description.strip()
195
196        for connection in self.iter_connections():
197            try:
198                current = connection.description.strip()
199                if current == expected:
200                    return connection
201            except Exception:
202                continue
203
204        return None

Searches for an open connection by its description.

The comparison is performed on the Description property of each open SAP connection. The description matching is case-sensitive after stripping whitespace.

Arguments:
  • description: The exact description of the connection to search for.
Returns:

The matching SAPConnection instance if found, otherwise None.

def open_connection(self, sync: bool = True) -> SAPConnection:
206    def open_connection(self, sync: bool = True) -> SAPConnection:
207        """Opens the target SAP connection defined in the configuration.
208
209        This method calls OpenConnection() on the COM application using the
210        connection description defined in SAPConfig. Once the COM connection
211        is obtained, it is wrapped in a SAPConnection instance.
212
213        If the configuration requests it, SAP credentials are automatically
214        injected. If a registration callback was provided, the opened connection
215        is notified to the calling component so it can be remembered.
216
217        Args:
218            sync: Whether the connection opening should be synchronous (waits
219                for the window to fully open). Defaults to True.
220
221        Returns:
222            The open SAP connection wrapped in a business-logic object.
223
224        Raises:
225            SAPConnectionException: If OpenConnection returns None, if the
226                connection description is invalid, or if an underlying COM error
227                occurs during the connection process.
228        """
229        try:
230            com_connection = self._com_application.OpenConnection(
231                self._config.connection_description,
232                sync,
233            )
234
235            if com_connection is None:
236                raise SAPConnectionException(
237                    "OpenConnection returned None (connection failed).",
238                    critical=True,
239                )
240
241            connection = SAPConnection(
242                application=self,
243                com_connection=com_connection,
244                config=self._config,
245                script_runner=self._script_runner,
246                logger=self._logger,
247            )
248            connection.fill_credentials_if_needed()
249
250            if self._on_connection_opened is not None:
251                self._on_connection_opened(connection)
252
253            self._logger(f"SAP connection opened: {connection.description}", False)
254            return connection
255
256        except SAPConnectionException:
257            raise
258        except Exception as exc:
259            raise SAPConnectionException(
260                f"Failed to open SAP connection '{self._config.connection_description}': {exc}",
261                critical=True,
262            ) from exc

Opens the target SAP connection defined in the configuration.

This method calls OpenConnection() on the COM application using the connection description defined in SAPConfig. Once the COM connection is obtained, it is wrapped in a SAPConnection instance.

If the configuration requests it, SAP credentials are automatically injected. If a registration callback was provided, the opened connection is notified to the calling component so it can be remembered.

Arguments:
  • sync: Whether the connection opening should be synchronous (waits for the window to fully open). Defaults to True.
Returns:

The open SAP connection wrapped in a business-logic object.

Raises:
  • SAPConnectionException: If OpenConnection returns None, if the connection description is invalid, or if an underlying COM error occurs during the connection process.
class SAPConnection:
 33class SAPConnection:
 34    """Represents an open SAP GUI connection.
 35
 36    This class encapsulates a SAP connection exposed via COM and provides a
 37    simplified business API for manipulating its sessions and performing
 38    standard operations like closing the connection or logging in.
 39
 40    It relies on a combination of raw COM objects, custom session wrappers,
 41    and business configurations.
 42
 43    Attributes:
 44        MAX_SESSIONS (int): The maximum number of sessions allowed for a single 
 45            SAP connection (SAP GUI limit is 6).
 46        _application (SAPApplication): The parent SAP application instance.
 47        _com_connection (Any): The underlying COM GuiConnection object.
 48        _config (SAPConfig): The configuration settings for this connection.
 49        _script_runner (VBScriptRunner): Executor for VBScript files.
 50        _logger (Callable[[str, bool], None]): The logging function.
 51
 52    Example:
 53        >>> connection = application.open_connection()
 54        >>> session = connection.first_session()
 55        >>> new_session = connection.create_session()
 56        >>> connection.close()
 57    """
 58
 59    MAX_SESSIONS = 6
 60    """The maximum number of sessions a user can have open simultaneously on a single connection."""
 61
 62    def __init__(
 63        self,
 64        *,
 65        application: SAPApplication,
 66        com_connection: Any,
 67        config: SAPConfig,
 68        script_runner: VBScriptRunner,
 69        logger: Callable[[str, bool], None] | None = None,
 70    ) -> None:
 71        """Initializes a SAP connection wrapper.
 72
 73        Args:
 74            application: Parent SAP application instance that spawned this connection.
 75            com_connection: COM object representing an existing active SAP connection.
 76            config: SAP configuration associated with this specific connection.
 77            script_runner: Component responsible for executing VBScripts.
 78            logger: Logging function. If None, :func:`~pysapmanager.utils.default_logger`
 79                is used. All output is subject to the library-wide switch
 80                controlled by :func:`pysapmanager.set_logging_enabled`.
 81        """
 82        self._application = application
 83        self._com_connection = com_connection
 84        self._config = config
 85        self._script_runner = script_runner
 86        self._logger = resolve_logger(logger)
 87
 88    @property
 89    def application(self) -> SAPApplication:
 90        """SAPApplication: The parent SAP GUI Scripting application."""
 91        return self._application
 92
 93    @property
 94    def com_object(self) -> Any:
 95        """Any: The raw COM GuiConnection object."""
 96        return self._com_connection
 97
 98    @property
 99    def name(self) -> str:
100        """str: The technical name of the SAP connection."""
101        return self._com_connection.Name
102
103    @property
104    def description(self) -> str:
105        """str: The business description of the SAP connection."""
106        return self._com_connection.Description
107
108    @property
109    def session_count(self) -> int:
110        """int: Number of sessions currently open in the connection."""
111        return self._com_connection.Children.Count
112
113    def sessions(self) -> list[SAPSession]:
114        """Returns all sessions currently open in the connection.
115
116        This method materializes the collection of SAP sessions as a Python list.
117        It is appropriate for the SAP GUI context, where a connection can contain
118        a maximum of six sessions.
119
120        Returns:
121            list[SAPSession]: A list of all active SAP sessions.
122
123        Raises:
124            SAPSessionException: If a session cannot be retrieved or wrapped 
125                correctly due to a COM communication issue.
126        """
127        return [self.session(i) for i in range(self.session_count)]
128
129    def session(self, index: int) -> SAPSession:
130        """Returns the session at the specified index.
131
132        Args:
133            index: The 0-based index of the session to retrieve.
134
135        Returns:
136            SAPSession: The SAP session at the given index.
137
138        Raises:
139            SAPSessionException: If the session cannot be retrieved or if 
140                the index is out of bounds.
141        """
142        try:
143            com_session = self._com_connection.Children(index)
144            return SAPSession(
145                connection=self,
146                com_session=com_session,
147                script_runner=self._script_runner,
148            )
149        except Exception as exc:
150            raise SAPSessionException(
151                f"Failed to retrieve SAP session at index {index}: {exc}",
152                critical=True,
153            ) from exc
154
155    def first_session(self) -> SAPSession:
156        """Returns the first session of the connection.
157
158        Returns:
159            SAPSession: The first active session (index 0).
160
161        Raises:
162            SAPSessionException: If the first session cannot be accessed.
163        """
164        return self.session(0)
165
166    def create_session(
167        self,
168        *,
169        timeout: float = 10.0,
170        poll_delay: float = 0.2,
171    ) -> SAPSession:
172        """Creates and returns a new SAP session.
173
174        Session creation is triggered from the first session of the connection.
175        The method then performs a polling loop to wait for the new session to 
176        appear in the connection's children collection.
177
178        Args:
179            timeout: Maximum time in seconds allowed for the new session to appear.
180                Defaults to 10.0.
181            poll_delay: Interval in seconds between checks for session existence.
182                Defaults to 0.2.
183
184        Returns:
185            SAPSession: The newly created SAP session.
186
187        Raises:
188            SAPSessionException: If the maximum session limit (MAX_SESSIONS) is 
189                reached, if the operation times out, or if a COM error occurs.
190        """
191        try:
192            initial_count = self.session_count
193
194            if initial_count >= self.MAX_SESSIONS:
195                raise SAPSessionException(
196                    f"Maximum number of sessions ({self.MAX_SESSIONS}) reached.",
197                    critical=True,
198                )
199
200            base_session = self._com_connection.Children(0)
201            base_session.CreateSession()
202
203            deadline = time.time() + timeout
204            while time.time() < deadline:
205                current_count = self.session_count
206                if current_count > initial_count:
207                    new_session = self.session(initial_count)
208                    self._logger(
209                        f"New SAP session created successfully: {new_session.name}.",
210                        False
211                    )
212                    return new_session
213                time.sleep(poll_delay)
214
215            raise SAPSessionException(
216                "Timeout during new SAP session creation.",
217                critical=True,
218            )
219
220        except SAPSessionException:
221            raise
222        except Exception as exc:
223            raise SAPSessionException(
224                f"Failed to create a new SAP session: {exc}",
225                critical=True,
226            ) from exc
227
228    def close(self) -> None:
229        """Closes the current SAP connection.
230
231        The method first attempts to close the connection via the native 
232        COM API `CloseConnection()`. If that fails, it attempts a fallback 
233        method by entering the '/nex' command into the SAP command field 
234        of the first session.
235
236        Raises:
237            SAPConnectionException: If all attempts to close the connection fail.
238        """
239        try:
240            self._com_connection.CloseConnection()
241            self._logger("SAP connection closed via CloseConnection().", False)
242            return
243
244        except Exception as close_exc:
245            self._logger(
246                f"CloseConnection() failed: {close_exc}. Attempting via '/nex'.", True
247            )
248
249            try:
250                com_session = self.first_session().com_object
251                com_session.findById("wnd[0]/tbar[0]/okcd").text = "/nex"
252                com_session.findById("wnd[0]").sendVKey(0)
253                self._logger("SAP connection closed via '/nex' command.", False)
254
255            except Exception as nex_exc:
256                raise SAPConnectionException(
257                    "Failed to close SAP connection. "
258                    f"CloseConnection() failed ({close_exc}) and "
259                    f"'/nex' failed ({nex_exc}).",
260                    critical=True,
261                ) from nex_exc
262
263    def fill_credentials_if_needed(self) -> None:
264        """Injects SAP credentials if the configuration requires it.
265
266        This method checks if the configuration mandates a manual login. If 
267        so, it locates the standard SAP fields (Client, User, Password, Language) 
268        in the first session and populates them.
269
270        Raises:
271            SAPConnectionException: If the credential fields cannot be found 
272                on the active screen or if an injection error occurs.
273        """
274        if not self._config.manual_login_required:
275            return
276
277        credentials = self._config.credentials
278        if credentials is None:
279            return
280
281        try:
282            com_session: Any = self.first_session().com_object
283
284            com_session.findById("wnd[0]/usr/txtRSYST-MANDT").text = credentials.client
285            com_session.findById("wnd[0]/usr/txtRSYST-BNAME").text = credentials.username
286            com_session.findById("wnd[0]/usr/pwdRSYST-BCODE").text = credentials.password
287            com_session.findById("wnd[0]/usr/txtRSYST-LANGU").text = credentials.language
288
289            self._logger("SAP credentials injected successfully.", False)
290        except Exception as exc:
291            raise SAPConnectionException(
292                f"Failed to inject SAP credentials: {exc}",
293                critical=True,
294            ) from exc

Represents an open SAP GUI connection.

This class encapsulates a SAP connection exposed via COM and provides a simplified business API for manipulating its sessions and performing standard operations like closing the connection or logging in.

It relies on a combination of raw COM objects, custom session wrappers, and business configurations.

Attributes:
  • MAX_SESSIONS (int): The maximum number of sessions allowed for a single SAP connection (SAP GUI limit is 6).
  • _application (SAPApplication): The parent SAP application instance.
  • _com_connection (Any): The underlying COM GuiConnection object.
  • _config (SAPConfig): The configuration settings for this connection.
  • _script_runner (VBScriptRunner): Executor for VBScript files.
  • _logger (Callable[[str, bool], None]): The logging function.
Example:
>>> connection = application.open_connection()
>>> session = connection.first_session()
>>> new_session = connection.create_session()
>>> connection.close()
SAPConnection( *, application: SAPApplication, com_connection: Any, config: SAPConfig, script_runner: pysapmanager.script_runner.VBScriptRunner, logger: Callable[[str, bool], None] | None = None)
62    def __init__(
63        self,
64        *,
65        application: SAPApplication,
66        com_connection: Any,
67        config: SAPConfig,
68        script_runner: VBScriptRunner,
69        logger: Callable[[str, bool], None] | None = None,
70    ) -> None:
71        """Initializes a SAP connection wrapper.
72
73        Args:
74            application: Parent SAP application instance that spawned this connection.
75            com_connection: COM object representing an existing active SAP connection.
76            config: SAP configuration associated with this specific connection.
77            script_runner: Component responsible for executing VBScripts.
78            logger: Logging function. If None, :func:`~pysapmanager.utils.default_logger`
79                is used. All output is subject to the library-wide switch
80                controlled by :func:`pysapmanager.set_logging_enabled`.
81        """
82        self._application = application
83        self._com_connection = com_connection
84        self._config = config
85        self._script_runner = script_runner
86        self._logger = resolve_logger(logger)

Initializes a SAP connection wrapper.

Arguments:
  • application: Parent SAP application instance that spawned this connection.
  • com_connection: COM object representing an existing active SAP connection.
  • config: SAP configuration associated with this specific connection.
  • script_runner: Component responsible for executing VBScripts.
  • logger: Logging function. If None, ~pysapmanager.utils.default_logger() is used. All output is subject to the library-wide switch controlled by pysapmanager.set_logging_enabled().
MAX_SESSIONS = 6

The maximum number of sessions a user can have open simultaneously on a single connection.

application: SAPApplication
88    @property
89    def application(self) -> SAPApplication:
90        """SAPApplication: The parent SAP GUI Scripting application."""
91        return self._application

SAPApplication: The parent SAP GUI Scripting application.

com_object: Any
93    @property
94    def com_object(self) -> Any:
95        """Any: The raw COM GuiConnection object."""
96        return self._com_connection

Any: The raw COM GuiConnection object.

name: str
 98    @property
 99    def name(self) -> str:
100        """str: The technical name of the SAP connection."""
101        return self._com_connection.Name

str: The technical name of the SAP connection.

description: str
103    @property
104    def description(self) -> str:
105        """str: The business description of the SAP connection."""
106        return self._com_connection.Description

str: The business description of the SAP connection.

session_count: int
108    @property
109    def session_count(self) -> int:
110        """int: Number of sessions currently open in the connection."""
111        return self._com_connection.Children.Count

int: Number of sessions currently open in the connection.

def sessions(self) -> list[SAPSession]:
113    def sessions(self) -> list[SAPSession]:
114        """Returns all sessions currently open in the connection.
115
116        This method materializes the collection of SAP sessions as a Python list.
117        It is appropriate for the SAP GUI context, where a connection can contain
118        a maximum of six sessions.
119
120        Returns:
121            list[SAPSession]: A list of all active SAP sessions.
122
123        Raises:
124            SAPSessionException: If a session cannot be retrieved or wrapped 
125                correctly due to a COM communication issue.
126        """
127        return [self.session(i) for i in range(self.session_count)]

Returns all sessions currently open in the connection.

This method materializes the collection of SAP sessions as a Python list. It is appropriate for the SAP GUI context, where a connection can contain a maximum of six sessions.

Returns:

list[SAPSession]: A list of all active SAP sessions.

Raises:
  • SAPSessionException: If a session cannot be retrieved or wrapped correctly due to a COM communication issue.
def session(self, index: int) -> SAPSession:
129    def session(self, index: int) -> SAPSession:
130        """Returns the session at the specified index.
131
132        Args:
133            index: The 0-based index of the session to retrieve.
134
135        Returns:
136            SAPSession: The SAP session at the given index.
137
138        Raises:
139            SAPSessionException: If the session cannot be retrieved or if 
140                the index is out of bounds.
141        """
142        try:
143            com_session = self._com_connection.Children(index)
144            return SAPSession(
145                connection=self,
146                com_session=com_session,
147                script_runner=self._script_runner,
148            )
149        except Exception as exc:
150            raise SAPSessionException(
151                f"Failed to retrieve SAP session at index {index}: {exc}",
152                critical=True,
153            ) from exc

Returns the session at the specified index.

Arguments:
  • index: The 0-based index of the session to retrieve.
Returns:

SAPSession: The SAP session at the given index.

Raises:
  • SAPSessionException: If the session cannot be retrieved or if the index is out of bounds.
def first_session(self) -> SAPSession:
155    def first_session(self) -> SAPSession:
156        """Returns the first session of the connection.
157
158        Returns:
159            SAPSession: The first active session (index 0).
160
161        Raises:
162            SAPSessionException: If the first session cannot be accessed.
163        """
164        return self.session(0)

Returns the first session of the connection.

Returns:

SAPSession: The first active session (index 0).

Raises:
  • SAPSessionException: If the first session cannot be accessed.
def create_session( self, *, timeout: float = 10.0, poll_delay: float = 0.2) -> SAPSession:
166    def create_session(
167        self,
168        *,
169        timeout: float = 10.0,
170        poll_delay: float = 0.2,
171    ) -> SAPSession:
172        """Creates and returns a new SAP session.
173
174        Session creation is triggered from the first session of the connection.
175        The method then performs a polling loop to wait for the new session to 
176        appear in the connection's children collection.
177
178        Args:
179            timeout: Maximum time in seconds allowed for the new session to appear.
180                Defaults to 10.0.
181            poll_delay: Interval in seconds between checks for session existence.
182                Defaults to 0.2.
183
184        Returns:
185            SAPSession: The newly created SAP session.
186
187        Raises:
188            SAPSessionException: If the maximum session limit (MAX_SESSIONS) is 
189                reached, if the operation times out, or if a COM error occurs.
190        """
191        try:
192            initial_count = self.session_count
193
194            if initial_count >= self.MAX_SESSIONS:
195                raise SAPSessionException(
196                    f"Maximum number of sessions ({self.MAX_SESSIONS}) reached.",
197                    critical=True,
198                )
199
200            base_session = self._com_connection.Children(0)
201            base_session.CreateSession()
202
203            deadline = time.time() + timeout
204            while time.time() < deadline:
205                current_count = self.session_count
206                if current_count > initial_count:
207                    new_session = self.session(initial_count)
208                    self._logger(
209                        f"New SAP session created successfully: {new_session.name}.",
210                        False
211                    )
212                    return new_session
213                time.sleep(poll_delay)
214
215            raise SAPSessionException(
216                "Timeout during new SAP session creation.",
217                critical=True,
218            )
219
220        except SAPSessionException:
221            raise
222        except Exception as exc:
223            raise SAPSessionException(
224                f"Failed to create a new SAP session: {exc}",
225                critical=True,
226            ) from exc

Creates and returns a new SAP session.

Session creation is triggered from the first session of the connection. The method then performs a polling loop to wait for the new session to appear in the connection's children collection.

Arguments:
  • timeout: Maximum time in seconds allowed for the new session to appear. Defaults to 10.0.
  • poll_delay: Interval in seconds between checks for session existence. Defaults to 0.2.
Returns:

SAPSession: The newly created SAP session.

Raises:
  • SAPSessionException: If the maximum session limit (MAX_SESSIONS) is reached, if the operation times out, or if a COM error occurs.
def close(self) -> None:
228    def close(self) -> None:
229        """Closes the current SAP connection.
230
231        The method first attempts to close the connection via the native 
232        COM API `CloseConnection()`. If that fails, it attempts a fallback 
233        method by entering the '/nex' command into the SAP command field 
234        of the first session.
235
236        Raises:
237            SAPConnectionException: If all attempts to close the connection fail.
238        """
239        try:
240            self._com_connection.CloseConnection()
241            self._logger("SAP connection closed via CloseConnection().", False)
242            return
243
244        except Exception as close_exc:
245            self._logger(
246                f"CloseConnection() failed: {close_exc}. Attempting via '/nex'.", True
247            )
248
249            try:
250                com_session = self.first_session().com_object
251                com_session.findById("wnd[0]/tbar[0]/okcd").text = "/nex"
252                com_session.findById("wnd[0]").sendVKey(0)
253                self._logger("SAP connection closed via '/nex' command.", False)
254
255            except Exception as nex_exc:
256                raise SAPConnectionException(
257                    "Failed to close SAP connection. "
258                    f"CloseConnection() failed ({close_exc}) and "
259                    f"'/nex' failed ({nex_exc}).",
260                    critical=True,
261                ) from nex_exc

Closes the current SAP connection.

The method first attempts to close the connection via the native COM API CloseConnection(). If that fails, it attempts a fallback method by entering the '/nex' command into the SAP command field of the first session.

Raises:
  • SAPConnectionException: If all attempts to close the connection fail.
def fill_credentials_if_needed(self) -> None:
263    def fill_credentials_if_needed(self) -> None:
264        """Injects SAP credentials if the configuration requires it.
265
266        This method checks if the configuration mandates a manual login. If 
267        so, it locates the standard SAP fields (Client, User, Password, Language) 
268        in the first session and populates them.
269
270        Raises:
271            SAPConnectionException: If the credential fields cannot be found 
272                on the active screen or if an injection error occurs.
273        """
274        if not self._config.manual_login_required:
275            return
276
277        credentials = self._config.credentials
278        if credentials is None:
279            return
280
281        try:
282            com_session: Any = self.first_session().com_object
283
284            com_session.findById("wnd[0]/usr/txtRSYST-MANDT").text = credentials.client
285            com_session.findById("wnd[0]/usr/txtRSYST-BNAME").text = credentials.username
286            com_session.findById("wnd[0]/usr/pwdRSYST-BCODE").text = credentials.password
287            com_session.findById("wnd[0]/usr/txtRSYST-LANGU").text = credentials.language
288
289            self._logger("SAP credentials injected successfully.", False)
290        except Exception as exc:
291            raise SAPConnectionException(
292                f"Failed to inject SAP credentials: {exc}",
293                critical=True,
294            ) from exc

Injects SAP credentials if the configuration requires it.

This method checks if the configuration mandates a manual login. If so, it locates the standard SAP fields (Client, User, Password, Language) in the first session and populates them.

Raises:
  • SAPConnectionException: If the credential fields cannot be found on the active screen or if an injection error occurs.
class SAPSession:
 29class SAPSession:
 30    """Represents a single SAP GUI session.
 31
 32    This class encapsulates an individual SAP session exposed by COM and
 33    stores the information necessary for executing scripts targeted at
 34    this session.
 35
 36    A SAPSession instance is typically created from a SAP connection via
 37    `SAPConnection`.
 38
 39    Attributes:
 40        _connection (SAPConnection): Parent SAP connection.
 41        _com_session (Any): Raw COM object representing the SAP session.
 42        _script_runner (VBScriptRunner): Script executor.
 43
 44    Example:
 45        ```python
 46        session = connection.first_session()
 47        output = session.execute_script(request)
 48        future = session.execute_script_async(request)
 49        ```
 50    """
 51
 52    def __init__(
 53        self,
 54        *,
 55        connection: SAPConnection,
 56        com_session: Any,
 57        script_runner: VBScriptRunner,
 58    ) -> None:
 59        """Initializes a SAP session.
 60
 61        Args:
 62            connection: Parent SAP connection.
 63            com_session: COM object representing the SAP session.
 64            script_runner: Script executor used to run VBScripts.
 65        """
 66        self._connection = connection
 67        self._com_session = com_session
 68        self._script_runner = script_runner
 69
 70    @property
 71    def connection(self) -> SAPConnection:
 72        """Parent SAP connection."""
 73        return self._connection
 74
 75    @property
 76    def com_object(self) -> Any:
 77        """Raw COM `GuiSession` object.
 78
 79        This property exposes the underlying COM object directly to enable
 80        advanced interactions not covered by the SAPSession business API.
 81
 82        Returns:
 83            Any: COM object representing the SAP session.
 84        """
 85        return self._com_session
 86
 87    @property
 88    def name(self) -> str:
 89        """SAP session name."""
 90        return str(self._com_session.Name)
 91
 92    @property
 93    def is_busy(self) -> bool:
 94        """Whether the SAP session is busy.
 95
 96        Returns:
 97            bool: True if the session is busy, False otherwise.
 98        """
 99        return bool(self._com_session.Busy)
100
101    @property
102    def session_index(self) -> int:
103        """Index of the session in its parent connection.
104
105        This index is used to pass the correct session context to
106        VBScripts executed via `cscript.exe`.
107
108        Returns:
109            int: Index of the session in the parent connection's session collection.
110
111        Raises:
112            ValueError: If the session index cannot be determined.
113        """
114        parent = self._connection.com_object
115        session_id = self._com_session.Id
116
117        for i in range(parent.Children.Count):
118            if parent.Children(i).Id == session_id:
119                return i
120
121        raise ValueError("Failed to determine SAP session index.")
122
123    @property
124    def connection_index(self) -> int:
125        """Index of the parent connection in the SAP application.
126
127        This index is used to pass the correct connection context to
128        VBScripts executed via `cscript.exe`.
129
130        Returns:
131            int: Index of the parent connection in the SAP application's connection
132                collection.
133
134        Raises:
135            ValueError: If the connection index cannot be determined.
136        """
137        app = self._connection.application.com_object
138        connection_id = self._connection.com_object.Id
139
140        for i in range(app.Children.Count):
141            if app.Children(i).Id == connection_id:
142                return i
143
144        raise ValueError("Failed to determine SAP connection index.")
145
146    def execute_script(self, request: ScriptExecutionRequest) -> str:
147        """Executes a VBScript on this session.
148
149        This method delegates execution to `VBScriptRunner`, passing the context
150        of the current session and connection along with parameters from the request.
151
152        Args:
153            request: Script execution request parameters.
154
155        Returns:
156            str: Standard output returned by the VBScript.
157
158        Raises:
159            SAPScriptExecutionException: If script execution fails, if the
160                script cannot be decoded with the resolved encoding, or if
161                `request.script_path` is already being executed concurrently
162                by another in-flight call (see `VBScriptRunner`'s concurrency
163                Warning).
164        """
165        return self._script_runner.execute(
166            script_path=request.script_path,
167            script_args=request.script_args,
168            session_index=self.session_index,
169            connection_index=self.connection_index,
170            timeout=request.timeout,
171            creates_excel=request.creates_excel,
172            created_file_path=request.created_file_path,
173            encoding=request.encoding,
174        )
175
176    def execute_script_async(self, request: ScriptExecutionRequest) -> Future[str]:
177        """Executes a VBScript on this session asynchronously.
178
179        Note:
180            If `request.script_path` is already in use by another in-flight
181            execution (on this session or another), the concurrency guard in
182            `VBScriptRunner.execute` raises `SAPScriptExecutionException` -
183            but since this happens on the worker thread, that error surfaces
184            when you call `future.result()`, not from this method directly.
185
186        Args:
187            request: Script execution request parameters.
188
189        Returns:
190            Future[str]: Future object representing the asynchronous task.
191        """
192        return self._script_runner.execute_async(
193            script_path=request.script_path,
194            script_args=request.script_args,
195            session_index=self.session_index,
196            connection_index=self.connection_index,
197            timeout=request.timeout,
198            creates_excel=request.creates_excel,
199            created_file_path=request.created_file_path,
200            encoding=request.encoding,
201        )

Represents a single SAP GUI session.

This class encapsulates an individual SAP session exposed by COM and stores the information necessary for executing scripts targeted at this session.

A SAPSession instance is typically created from a SAP connection via SAPConnection.

Attributes:
  • _connection (SAPConnection): Parent SAP connection.
  • _com_session (Any): Raw COM object representing the SAP session.
  • _script_runner (VBScriptRunner): Script executor.
Example:
session = connection.first_session()
output = session.execute_script(request)
future = session.execute_script_async(request)
SAPSession( *, connection: SAPConnection, com_session: Any, script_runner: pysapmanager.script_runner.VBScriptRunner)
52    def __init__(
53        self,
54        *,
55        connection: SAPConnection,
56        com_session: Any,
57        script_runner: VBScriptRunner,
58    ) -> None:
59        """Initializes a SAP session.
60
61        Args:
62            connection: Parent SAP connection.
63            com_session: COM object representing the SAP session.
64            script_runner: Script executor used to run VBScripts.
65        """
66        self._connection = connection
67        self._com_session = com_session
68        self._script_runner = script_runner

Initializes a SAP session.

Arguments:
  • connection: Parent SAP connection.
  • com_session: COM object representing the SAP session.
  • script_runner: Script executor used to run VBScripts.
connection: SAPConnection
70    @property
71    def connection(self) -> SAPConnection:
72        """Parent SAP connection."""
73        return self._connection

Parent SAP connection.

com_object: Any
75    @property
76    def com_object(self) -> Any:
77        """Raw COM `GuiSession` object.
78
79        This property exposes the underlying COM object directly to enable
80        advanced interactions not covered by the SAPSession business API.
81
82        Returns:
83            Any: COM object representing the SAP session.
84        """
85        return self._com_session

Raw COM GuiSession object.

This property exposes the underlying COM object directly to enable advanced interactions not covered by the SAPSession business API.

Returns:

Any: COM object representing the SAP session.

name: str
87    @property
88    def name(self) -> str:
89        """SAP session name."""
90        return str(self._com_session.Name)

SAP session name.

is_busy: bool
92    @property
93    def is_busy(self) -> bool:
94        """Whether the SAP session is busy.
95
96        Returns:
97            bool: True if the session is busy, False otherwise.
98        """
99        return bool(self._com_session.Busy)

Whether the SAP session is busy.

Returns:

bool: True if the session is busy, False otherwise.

session_index: int
101    @property
102    def session_index(self) -> int:
103        """Index of the session in its parent connection.
104
105        This index is used to pass the correct session context to
106        VBScripts executed via `cscript.exe`.
107
108        Returns:
109            int: Index of the session in the parent connection's session collection.
110
111        Raises:
112            ValueError: If the session index cannot be determined.
113        """
114        parent = self._connection.com_object
115        session_id = self._com_session.Id
116
117        for i in range(parent.Children.Count):
118            if parent.Children(i).Id == session_id:
119                return i
120
121        raise ValueError("Failed to determine SAP session index.")

Index of the session in its parent connection.

This index is used to pass the correct session context to VBScripts executed via cscript.exe.

Returns:

int: Index of the session in the parent connection's session collection.

Raises:
  • ValueError: If the session index cannot be determined.
connection_index: int
123    @property
124    def connection_index(self) -> int:
125        """Index of the parent connection in the SAP application.
126
127        This index is used to pass the correct connection context to
128        VBScripts executed via `cscript.exe`.
129
130        Returns:
131            int: Index of the parent connection in the SAP application's connection
132                collection.
133
134        Raises:
135            ValueError: If the connection index cannot be determined.
136        """
137        app = self._connection.application.com_object
138        connection_id = self._connection.com_object.Id
139
140        for i in range(app.Children.Count):
141            if app.Children(i).Id == connection_id:
142                return i
143
144        raise ValueError("Failed to determine SAP connection index.")

Index of the parent connection in the SAP application.

This index is used to pass the correct connection context to VBScripts executed via cscript.exe.

Returns:

int: Index of the parent connection in the SAP application's connection collection.

Raises:
  • ValueError: If the connection index cannot be determined.
def execute_script(self, request: ScriptExecutionRequest) -> str:
146    def execute_script(self, request: ScriptExecutionRequest) -> str:
147        """Executes a VBScript on this session.
148
149        This method delegates execution to `VBScriptRunner`, passing the context
150        of the current session and connection along with parameters from the request.
151
152        Args:
153            request: Script execution request parameters.
154
155        Returns:
156            str: Standard output returned by the VBScript.
157
158        Raises:
159            SAPScriptExecutionException: If script execution fails, if the
160                script cannot be decoded with the resolved encoding, or if
161                `request.script_path` is already being executed concurrently
162                by another in-flight call (see `VBScriptRunner`'s concurrency
163                Warning).
164        """
165        return self._script_runner.execute(
166            script_path=request.script_path,
167            script_args=request.script_args,
168            session_index=self.session_index,
169            connection_index=self.connection_index,
170            timeout=request.timeout,
171            creates_excel=request.creates_excel,
172            created_file_path=request.created_file_path,
173            encoding=request.encoding,
174        )

Executes a VBScript on this session.

This method delegates execution to VBScriptRunner, passing the context of the current session and connection along with parameters from the request.

Arguments:
  • request: Script execution request parameters.
Returns:

str: Standard output returned by the VBScript.

Raises:
  • SAPScriptExecutionException: If script execution fails, if the script cannot be decoded with the resolved encoding, or if request.script_path is already being executed concurrently by another in-flight call (see VBScriptRunner's concurrency Warning).
def execute_script_async( self, request: ScriptExecutionRequest) -> concurrent.futures._base.Future[str]:
176    def execute_script_async(self, request: ScriptExecutionRequest) -> Future[str]:
177        """Executes a VBScript on this session asynchronously.
178
179        Note:
180            If `request.script_path` is already in use by another in-flight
181            execution (on this session or another), the concurrency guard in
182            `VBScriptRunner.execute` raises `SAPScriptExecutionException` -
183            but since this happens on the worker thread, that error surfaces
184            when you call `future.result()`, not from this method directly.
185
186        Args:
187            request: Script execution request parameters.
188
189        Returns:
190            Future[str]: Future object representing the asynchronous task.
191        """
192        return self._script_runner.execute_async(
193            script_path=request.script_path,
194            script_args=request.script_args,
195            session_index=self.session_index,
196            connection_index=self.connection_index,
197            timeout=request.timeout,
198            creates_excel=request.creates_excel,
199            created_file_path=request.created_file_path,
200            encoding=request.encoding,
201        )

Executes a VBScript on this session asynchronously.

Note:

If request.script_path is already in use by another in-flight execution (on this session or another), the concurrency guard in VBScriptRunner.execute raises SAPScriptExecutionException - but since this happens on the worker thread, that error surfaces when you call future.result(), not from this method directly.

Arguments:
  • request: Script execution request parameters.
Returns:

Future[str]: Future object representing the asynchronous task.

@dataclass(frozen=True)
class SAPConfig:
 74@dataclass(frozen=True)
 75class SAPConfig:
 76    """Main SAP GUI configuration.
 77
 78    This immutable structure groups the parameters necessary to control SAP GUI
 79    from the application, including the path to the SAP Logon executable, the
 80    description of the target connection, and optionally the credentials to inject.
 81
 82    Attributes:
 83        sap_logon_path: Path to the SAP Logon executable.
 84        connection_description: Description of the SAP connection as it appears
 85            in SAP Logon Pad.
 86        credentials: SAP credentials to use for automatic login screen filling.
 87            If None, no automatic credential injection is performed.
 88
 89    Raises:
 90        ValueError: If SAP Logon path or connection description are empty.
 91    """
 92
 93    sap_logon_path: str | Path
 94    connection_description: str
 95    credentials: SAPCredentials | None = None
 96
 97    def __post_init__(self) -> None:
 98        """Validates and normalizes the SAP configuration.
 99
100        Raises:
101            ValueError: If SAP Logon path or connection description are empty.
102        """
103        if not str(self.sap_logon_path).strip():
104            raise ValueError("sap_logon_path cannot be empty.")
105
106        if not self.connection_description or not self.connection_description.strip():
107            raise ValueError("connection_description cannot be empty.")
108
109    @property
110    def manual_login_required(self) -> bool:
111        """Whether credentials need to be injected manually.
112
113        Returns:
114            bool: True if credentials are provided, False otherwise.
115        """
116        return self.credentials is not None

Main SAP GUI configuration.

This immutable structure groups the parameters necessary to control SAP GUI from the application, including the path to the SAP Logon executable, the description of the target connection, and optionally the credentials to inject.

Attributes:
  • sap_logon_path: Path to the SAP Logon executable.
  • connection_description: Description of the SAP connection as it appears in SAP Logon Pad.
  • credentials: SAP credentials to use for automatic login screen filling. If None, no automatic credential injection is performed.
Raises:
  • ValueError: If SAP Logon path or connection description are empty.
SAPConfig( sap_logon_path: str | pathlib._local.Path, connection_description: str, credentials: SAPCredentials | None = None)
sap_logon_path: str | pathlib._local.Path
connection_description: str
credentials: SAPCredentials | None = None
manual_login_required: bool
109    @property
110    def manual_login_required(self) -> bool:
111        """Whether credentials need to be injected manually.
112
113        Returns:
114            bool: True if credentials are provided, False otherwise.
115        """
116        return self.credentials is not None

Whether credentials need to be injected manually.

Returns:

bool: True if credentials are provided, False otherwise.

@dataclass(frozen=True)
class SAPCredentials:
23@dataclass(frozen=True)
24class SAPCredentials:
25    """SAP login credentials for manual authentication.
26
27    This immutable structure contains the information necessary to populate
28    the SAP login screen when automatic credential injection is required.
29
30    Security:
31        `password` is excluded from this class's auto-generated `repr()`
32        (it prints as `password=<hidden>` instead of the plaintext value).
33        This matters because `SAPCredentials` instances are commonly embedded
34        in a `SAPConfig` and end up inside exception messages, tracebacks, or
35        ad-hoc `print()`/logging calls for debugging - without this,
36        the plaintext password would be trivially leaked into logs.
37        `str(credentials)` uses the same repr and is therefore also safe to
38        log by accident, but avoid printing `credentials.password` directly.
39
40    Attributes:
41        username: SAP username.
42        password: Password associated with the username. Hidden from `repr()`.
43        client: SAP client number.
44        language: SAP login language.
45
46    Raises:
47        ValueError: If any required value is empty or invalid.
48    """
49
50    username: str
51    password: str = field(repr=False)
52    client: str
53    language: str
54
55    def __post_init__(self) -> None:
56        """Validates SAP credentials data.
57
58        Raises:
59            ValueError: If username, password, client, or language are empty.
60        """
61        if not self.username or not self.username.strip():
62            raise ValueError("username cannot be empty.")
63
64        if not self.password or not self.password.strip():
65            raise ValueError("password cannot be empty.")
66
67        if not self.client or not self.client.strip():
68            raise ValueError("client cannot be empty.")
69
70        if not self.language or not self.language.strip():
71            raise ValueError("language cannot be empty.")

SAP login credentials for manual authentication.

This immutable structure contains the information necessary to populate the SAP login screen when automatic credential injection is required.

Security:

password is excluded from this class's auto-generated repr() (it prints as password=<hidden> instead of the plaintext value). This matters because SAPCredentials instances are commonly embedded in a SAPConfig and end up inside exception messages, tracebacks, or ad-hoc print()/logging calls for debugging - without this, the plaintext password would be trivially leaked into logs. str(credentials) uses the same repr and is therefore also safe to log by accident, but avoid printing credentials.password directly.

Attributes:
  • username: SAP username.
  • password: Password associated with the username. Hidden from repr().
  • client: SAP client number.
  • language: SAP login language.
Raises:
  • ValueError: If any required value is empty or invalid.
SAPCredentials(username: str, password: str, client: str, language: str)
username: str
password: str
client: str
language: str
@dataclass(frozen=True)
class ScriptExecutionRequest:
119@dataclass(frozen=True)
120class ScriptExecutionRequest:
121    """Request to execute a VBScript on a SAP session.
122
123    This immutable structure encapsulates the parameters necessary to launch
124    a VBScript to control a specific SAP session during automation. It describes
125    the script to execute, its maximum execution time, and any expected effects
126    on the file system.
127
128    Attributes:
129        script_path: Path to the VBScript file to execute.
130        script_args: Additional arguments to pass to the script. Defaults to empty tuple.
131        timeout: Maximum execution duration in seconds. Defaults to 120.
132        creates_excel: Whether the script is expected to create or manipulate
133            an Excel file. Defaults to False.
134        created_file_path: Path to the file expected after execution, if applicable.
135            Defaults to None.
136        encoding: Text encoding to use when reading/patching this script's
137            `.vbs` file (e.g. `"utf-16"`, `"utf-8"`, `"utf-8-sig"`, `"cp1252"`).
138            Set this explicitly if a particular script is known to be saved
139            with a non-default encoding, to avoid silently mis-decoded content.
140
141    Raises:
142        ValueError: If request parameters are inconsistent.
143    """
144
145    script_path: str | Path
146    script_args: tuple[str, ...] = ()
147    timeout: int = 120
148    creates_excel: bool = False
149    created_file_path: str | Path | None = None
150    encoding: str = "utf-16"
151
152    def __post_init__(self) -> None:
153        """Validates and normalizes the script execution request.
154
155        Raises:
156            ValueError: If script path is empty, timeout is invalid,
157                creates_excel is True without created_file_path, or encoding
158                is provided but is not a valid/known text codec name.
159        """
160        if not str(self.script_path).strip():
161            raise ValueError("script_path cannot be empty.")
162
163        if self.timeout <= 0:
164            raise ValueError("timeout must be greater than 0.")
165
166        if self.creates_excel and self.created_file_path is None:
167            raise ValueError("created_file_path is required when creates_excel=True.")
168
169        if not self.encoding.strip():
170            raise ValueError("encoding cannot be empty when provided.")
171        try:
172            codecs.lookup(self.encoding)
173        except LookupError as exc:
174            raise ValueError(f"Unknown text encoding: '{self.encoding}'.") from exc
175
176        if self.created_file_path is not None:
177            # Bypass frozen=True to set normalized Path object
178            object.__setattr__(
179                self,
180                "created_file_path",
181                Path(self.created_file_path),
182            )
183
184            if not str(self.created_file_path).strip():
185                raise ValueError("created_file_path cannot be empty.")

Request to execute a VBScript on a SAP session.

This immutable structure encapsulates the parameters necessary to launch a VBScript to control a specific SAP session during automation. It describes the script to execute, its maximum execution time, and any expected effects on the file system.

Attributes:
  • script_path: Path to the VBScript file to execute.
  • script_args: Additional arguments to pass to the script. Defaults to empty tuple.
  • timeout: Maximum execution duration in seconds. Defaults to 120.
  • creates_excel: Whether the script is expected to create or manipulate an Excel file. Defaults to False.
  • created_file_path: Path to the file expected after execution, if applicable. Defaults to None.
  • encoding: Text encoding to use when reading/patching this script's .vbs file (e.g. "utf-16", "utf-8", "utf-8-sig", "cp1252"). Set this explicitly if a particular script is known to be saved with a non-default encoding, to avoid silently mis-decoded content.
Raises:
  • ValueError: If request parameters are inconsistent.
ScriptExecutionRequest( script_path: str | pathlib._local.Path, script_args: tuple[str, ...] = (), timeout: int = 120, creates_excel: bool = False, created_file_path: str | pathlib._local.Path | None = None, encoding: str = 'utf-16')
script_path: str | pathlib._local.Path
script_args: tuple[str, ...] = ()
timeout: int = 120
creates_excel: bool = False
created_file_path: str | pathlib._local.Path | None = None
encoding: str = 'utf-16'
@contextmanager
def com_initialized() -> Iterator[None]:
21@contextmanager
22def com_initialized() -> Iterator[None]:
23    """Context manager to initialize COM in the current thread.
24
25    This context manager facilitates safe interaction with COM objects by
26    ensuring that the calling thread is properly initialized. It calls
27    `pythoncom.CoInitialize()` upon entering the context and guarantees that
28    `pythoncom.CoUninitialize()` is invoked upon exit, ensuring cleanup
29    regardless of whether the code block completes successfully or raises
30    an exception.
31
32    This is mandatory for multi-threaded applications, as each thread that
33    interacts with COM objects must perform its own initialization.
34
35    Yields:
36        None: The context manager does not return a value; it strictly
37            manages the lifecycle of the thread's COM state.
38
39    Raises:
40        pythoncom.com_error: If the underlying COM initialization fails or
41            if there is a conflict in the apartment threading model.
42
43    Examples:
44        **Standard Synchronous Usage:**
45
46        ```python
47        with com_initialized():
48            # Perform COM calls here
49            import win32com.client as win32
50            app = win32.GetObject("SAPGUI")
51        ```
52
53        **Multi-threaded Usage:**
54
55        ```python
56        import threading
57        import win32com.client as win32
58
59        def worker_thread():
60            with com_initialized():
61                # COM calls are now safe in this background thread
62                app = win32.GetObject("SAPGUI")
63
64        thread = threading.Thread(target=worker_thread)
65        thread.start()
66        thread.join()
67        ```
68    """
69    pythoncom.CoInitialize()
70    try:
71        yield
72    finally:
73        pythoncom.CoUninitialize()

Context manager to initialize COM in the current thread.

This context manager facilitates safe interaction with COM objects by ensuring that the calling thread is properly initialized. It calls pythoncom.CoInitialize() upon entering the context and guarantees that pythoncom.CoUninitialize() is invoked upon exit, ensuring cleanup regardless of whether the code block completes successfully or raises an exception.

This is mandatory for multi-threaded applications, as each thread that interacts with COM objects must perform its own initialization.

Yields:

None: The context manager does not return a value; it strictly manages the lifecycle of the thread's COM state.

Raises:
  • pythoncom.com_error: If the underlying COM initialization fails or if there is a conflict in the apartment threading model.
Examples:

Standard Synchronous Usage:

with com_initialized():
    # Perform COM calls here
    import win32com.client as win32
    app = win32.GetObject("SAPGUI")

Multi-threaded Usage:

import threading
import win32com.client as win32

def worker_thread():
    with com_initialized():
        # COM calls are now safe in this background thread
        app = win32.GetObject("SAPGUI")

thread = threading.Thread(target=worker_thread)
thread.start()
thread.join()
def set_logging_enabled(enabled: bool) -> None:
57def set_logging_enabled(enabled: bool) -> None:
58    """Globally enables or disables all logging produced by PySAPManager.
59
60    This affects every component in the library, including instances that
61    were constructed with a custom ``logger`` callable: when disabled, no
62    logger (default or custom) will be invoked by any PySAPManager component
63    until logging is re-enabled.
64
65    This is a process-wide (module-level) setting. It affects all
66    ``SAPManager``, ``SAPApplication``, ``SAPConnection``, ``VBScriptRunner``,
67    and ``SAPProcessService`` instances currently alive or created after this
68    call, since each of them resolves the flag dynamically on every log call
69    rather than caching it at construction time.
70
71    Args:
72        enabled: ``True`` to allow log messages to be emitted (default
73            library behavior). ``False`` to suppress all PySAPManager log output.
74
75    Example:
76        ```python
77        from pysapmanager import SAPManager, SAPConfig, set_logging_enabled
78
79        set_logging_enabled(False)  # silence the library
80
81        with SAPManager(config=SAPConfig(...)) as manager:
82            ...  # no log output will be produced
83
84        set_logging_enabled(True)  # restore normal logging
85        ```
86    """
87    global _logging_enabled
88    _logging_enabled = bool(enabled)

Globally enables or disables all logging produced by PySAPManager.

This affects every component in the library, including instances that were constructed with a custom logger callable: when disabled, no logger (default or custom) will be invoked by any PySAPManager component until logging is re-enabled.

This is a process-wide (module-level) setting. It affects all SAPManager, SAPApplication, SAPConnection, VBScriptRunner, and SAPProcessService instances currently alive or created after this call, since each of them resolves the flag dynamically on every log call rather than caching it at construction time.

Arguments:
  • enabled: True to allow log messages to be emitted (default library behavior). False to suppress all PySAPManager log output.
Example:
from pysapmanager import SAPManager, SAPConfig, set_logging_enabled

set_logging_enabled(False)  # silence the library

with SAPManager(config=SAPConfig(...)) as manager:
    ...  # no log output will be produced

set_logging_enabled(True)  # restore normal logging
def is_logging_enabled() -> bool:
91def is_logging_enabled() -> bool:
92    """Returns whether PySAPManager logging is currently enabled.
93
94    Returns:
95        bool: ``True`` if logging is currently enabled (the default),
96            ``False`` if it has been disabled via :func:`set_logging_enabled`.
97    """
98    return _logging_enabled

Returns whether PySAPManager logging is currently enabled.

Returns:

bool: True if logging is currently enabled (the default), False if it has been disabled via set_logging_enabled().