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:
SAPManager: Main facade for SAP automationSAPApplication: Access to SAP GUI Scripting applicationSAPConnection: Represents an open SAP connectionSAPSession: Represents a SAP sessionSAPConfig: Configuration for SAP connectionsSAPCredentials: Login credentials for SAPScriptExecutionRequest: Script execution parameterscom_initialized(): Context manager for COM initializationset_logging_enabled(enabled): Globally enable/disable all library loggingis_logging_enabled(): Check the current global logging state
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]
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
SAPManagercan track at most one open connection at a time. Callingapplication.open_connection()a second time on the same manager before closing the first (viaclose_connection()) raisesSAPConnectionException. If you need several simultaneous connections, create multiple sessions on one connection instead (connection.create_session()), or use separateSAPManagerinstances.
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()
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 (seepysapmanager.set_logging_enabled()), passingenable_logging=Falsehere 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 toNone, which leaves the current global logging state untouched (so this constructor never surprises you by silently re-enabling logging you disabled elsewhere). PassTrueexplicitly to force logging back on.
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.
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.
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.
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 )
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:
- If SAP Logon is not running, it is started.
- If SAP Logon is already running, open connections are inspected.
- If the target connection is already open, SAP Logon is closed and relaunched to ensure a clean state.
- 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.
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.
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().
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.
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.
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 bypysapmanager.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.
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.
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.
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.
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.
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()
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 bypysapmanager.set_logging_enabled().
The maximum number of sessions a user can have open simultaneously on a single connection.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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)
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.
70 @property 71 def connection(self) -> SAPConnection: 72 """Parent SAP connection.""" 73 return self._connection
Parent SAP connection.
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.
87 @property 88 def name(self) -> str: 89 """SAP session name.""" 90 return str(self._com_session.Name)
SAP session name.
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.
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.
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.
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_pathis already being executed concurrently by another in-flight call (seeVBScriptRunner's concurrency Warning).
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_pathis already in use by another in-flight execution (on this session or another), the concurrency guard inVBScriptRunner.executeraisesSAPScriptExecutionException- but since this happens on the worker thread, that error surfaces when you callfuture.result(), not from this method directly.
Arguments:
- request: Script execution request parameters.
Returns:
Future[str]: Future object representing the asynchronous task.
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.
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.
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:
passwordis excluded from this class's auto-generatedrepr()(it prints aspassword=<hidden>instead of the plaintext value). This matters becauseSAPCredentialsinstances are commonly embedded in aSAPConfigand end up inside exception messages, tracebacks, or ad-hocprint()/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 printingcredentials.passworddirectly.
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.
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
.vbsfile (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.
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()
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:
Trueto allow log messages to be emitted (default library behavior).Falseto 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
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:
Trueif logging is currently enabled (the default),Falseif it has been disabled viaset_logging_enabled().