sapwright 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sapwright/__init__.py +213 -0
- sapwright/_utils.py +140 -0
- sapwright/connection_manager.py +125 -0
- sapwright/exceptions.py +46 -0
- sapwright/models.py +41 -0
- sapwright/objects/__init__.py +16 -0
- sapwright/objects/component.py +420 -0
- sapwright/objects/scrollbar.py +37 -0
- sapwright/objects/session.py +436 -0
- sapwright/objects/shell.py +45 -0
- sapwright/objects/table_control.py +324 -0
- sapwright/py.typed +0 -0
- sapwright/types.py +125 -0
- sapwright-0.0.1.dist-info/METADATA +909 -0
- sapwright-0.0.1.dist-info/RECORD +16 -0
- sapwright-0.0.1.dist-info/WHEEL +4 -0
sapwright/__init__.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import final
|
|
7
|
+
|
|
8
|
+
from sapwright._utils import default_password_generator, launch_saplogon
|
|
9
|
+
from sapwright.connection_manager import ConnectionManager
|
|
10
|
+
from sapwright.exceptions import SAPLoginError
|
|
11
|
+
from sapwright.objects import GuiSession
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class SAPLoginScreenElements:
|
|
18
|
+
client = "wnd[0]/usr/txtRSYST-MANDT"
|
|
19
|
+
username = "wnd[0]/usr/txtRSYST-BNAME"
|
|
20
|
+
password = "wnd[0]/usr/pwdRSYST-BCODE"
|
|
21
|
+
language = "wnd[0]/usr/txtRSYST-LANGU"
|
|
22
|
+
terminate_other_sessions_radio = "wnd[1]/usr/radMULTI_LOGON_OPT1"
|
|
23
|
+
reset_password = r"wnd[1]/usr/lblRSYST-NCODE_TEXT"
|
|
24
|
+
new_password = "wnd[1]/usr/pwdRSYST-NCODE"
|
|
25
|
+
repeat_password = "wnd[1]/usr/pwdRSYST-NCOD2"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@final
|
|
29
|
+
class Sapwright:
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
username: str,
|
|
33
|
+
password: str,
|
|
34
|
+
connection_string: str | None = None,
|
|
35
|
+
connection_name: str | None = None,
|
|
36
|
+
client: int | None = None,
|
|
37
|
+
language: str | None = None,
|
|
38
|
+
terminate_other_sessions: bool = True,
|
|
39
|
+
exe_path: str | Path | None = None,
|
|
40
|
+
password_generator: Callable[[], str] | None = None,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
First preference is given to the connection_string, then connection_name.
|
|
44
|
+
|
|
45
|
+
password_generator is called to produce a new password when SAP asks
|
|
46
|
+
for a password change during login. Defaults to month and year of
|
|
47
|
+
today's date, e.g. "Jan@2026".
|
|
48
|
+
"""
|
|
49
|
+
if not connection_string and not connection_name:
|
|
50
|
+
raise ValueError("connection_string or connection_name must be provided.")
|
|
51
|
+
|
|
52
|
+
self._username = username
|
|
53
|
+
self._password = password
|
|
54
|
+
self._connection_string = connection_string
|
|
55
|
+
self._connection_name = connection_name
|
|
56
|
+
self._client = client
|
|
57
|
+
self._language = language
|
|
58
|
+
self._terminate_other_sessions = terminate_other_sessions
|
|
59
|
+
self._exe_path = exe_path
|
|
60
|
+
self._password_generator = password_generator or default_password_generator
|
|
61
|
+
self._connection_mgr = ConnectionManager()
|
|
62
|
+
self._session: GuiSession | None = None
|
|
63
|
+
|
|
64
|
+
def __enter__(self):
|
|
65
|
+
return self.connect()
|
|
66
|
+
|
|
67
|
+
def __exit__(
|
|
68
|
+
self,
|
|
69
|
+
exc_type: type[BaseException] | None,
|
|
70
|
+
exc_value: BaseException | None,
|
|
71
|
+
traceback: TracebackType | None,
|
|
72
|
+
) -> None:
|
|
73
|
+
if self._session:
|
|
74
|
+
self._session.close()
|
|
75
|
+
|
|
76
|
+
def connect(self) -> GuiSession:
|
|
77
|
+
"""Attach to the user's existing connection, or open a new one if none is found.
|
|
78
|
+
|
|
79
|
+
When opening, connection_string takes precedence over connection_name.
|
|
80
|
+
"""
|
|
81
|
+
session = self._attach_existing() or self._open_new()
|
|
82
|
+
self._login(session)
|
|
83
|
+
self._session = session
|
|
84
|
+
return session
|
|
85
|
+
|
|
86
|
+
def close_connection(self):
|
|
87
|
+
"""Closes the SAP connection including all its sessions.
|
|
88
|
+
|
|
89
|
+
Logs off by sending "/nex" to the session, falling back to
|
|
90
|
+
connection.CloseConnection() if the command fails.
|
|
91
|
+
"""
|
|
92
|
+
session, self._session = self._session, None
|
|
93
|
+
if session is None:
|
|
94
|
+
self._connection_mgr.close_connection()
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
session.send_command("/nex")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.debug(f"'/nex' failed, closing connection directly: {e}")
|
|
101
|
+
self._connection_mgr.close_connection()
|
|
102
|
+
|
|
103
|
+
def _attach_existing(self) -> GuiSession | None:
|
|
104
|
+
# find existing connections of the user
|
|
105
|
+
connection = self._connection_mgr.find_connection_by_user(
|
|
106
|
+
self._username,
|
|
107
|
+
connection_string=self._connection_string,
|
|
108
|
+
connection_name=self._connection_name,
|
|
109
|
+
)
|
|
110
|
+
if not connection:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
logger.debug(
|
|
114
|
+
f"Attaching to existing connection '{connection.Description}' for user: {self._username}"
|
|
115
|
+
)
|
|
116
|
+
return GuiSession(connection.Children(0))
|
|
117
|
+
|
|
118
|
+
def _open_new(self) -> GuiSession:
|
|
119
|
+
logger.info(
|
|
120
|
+
f"No existing connection with given connection_string or connection_name found for user: {self._username}, opening a new one"
|
|
121
|
+
)
|
|
122
|
+
launch_saplogon(self._exe_path)
|
|
123
|
+
return self._connection_mgr.open_connection(
|
|
124
|
+
connection_string=self._connection_string,
|
|
125
|
+
connection_name=self._connection_name,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _login(self, session: GuiSession):
|
|
129
|
+
"""Performs SAP login with provided credentials
|
|
130
|
+
|
|
131
|
+
This method performs SAP login with the provided credentials, handling
|
|
132
|
+
multiple login attempts, incorrect password attempts, terminating other
|
|
133
|
+
sessions logged in other computers, and dismissing any additional popups
|
|
134
|
+
that may appear after login.
|
|
135
|
+
|
|
136
|
+
Raises
|
|
137
|
+
------
|
|
138
|
+
SAPLoginError
|
|
139
|
+
If the login process fails
|
|
140
|
+
"""
|
|
141
|
+
user_field = session.find_by_id(
|
|
142
|
+
SAPLoginScreenElements.username, raise_error=False
|
|
143
|
+
)
|
|
144
|
+
if not user_field:
|
|
145
|
+
logger.debug("Login screen not found, assuming user is already logged in")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
logger.debug(f"Logging in as: {self._username}")
|
|
149
|
+
user_field.text = self._username
|
|
150
|
+
session.find_by_id(SAPLoginScreenElements.password).text = self._password
|
|
151
|
+
|
|
152
|
+
if self._client:
|
|
153
|
+
session.find_by_id(SAPLoginScreenElements.client).text = self._client
|
|
154
|
+
|
|
155
|
+
if self._language:
|
|
156
|
+
session.find_by_id(SAPLoginScreenElements.language).text = self._language
|
|
157
|
+
|
|
158
|
+
session.press_enter()
|
|
159
|
+
|
|
160
|
+
# Check for immediate login errors (e.g invalid credentials)
|
|
161
|
+
status = session.raise_for_status(
|
|
162
|
+
message="Login failed", exception=SAPLoginError
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Handle multi-logon
|
|
166
|
+
if status.text and "already logged on" in status.text.lower():
|
|
167
|
+
self._handle_multi_logon(session)
|
|
168
|
+
|
|
169
|
+
# Handle new/reset password dialog
|
|
170
|
+
if session.find_by_id(SAPLoginScreenElements.reset_password, False):
|
|
171
|
+
self._change_password(session)
|
|
172
|
+
|
|
173
|
+
# Dismiss any other popups
|
|
174
|
+
session.dismiss_popups(limit=10)
|
|
175
|
+
|
|
176
|
+
def _change_password(self, session: GuiSession):
|
|
177
|
+
logger.debug("Password change requested")
|
|
178
|
+
new_password = self._password_generator()
|
|
179
|
+
if not new_password:
|
|
180
|
+
raise SAPLoginError("password_generator must return a non-empty string.")
|
|
181
|
+
|
|
182
|
+
session.find_by_id(SAPLoginScreenElements.new_password).text = new_password
|
|
183
|
+
session.find_by_id(SAPLoginScreenElements.repeat_password).text = new_password
|
|
184
|
+
session.press_enter(1)
|
|
185
|
+
|
|
186
|
+
session.raise_for_status(
|
|
187
|
+
message="Password change failed", exception=SAPLoginError
|
|
188
|
+
)
|
|
189
|
+
if session.find_by_id(SAPLoginScreenElements.reset_password, False):
|
|
190
|
+
raise SAPLoginError("Password change failed: new password was rejected.")
|
|
191
|
+
|
|
192
|
+
self._password = new_password
|
|
193
|
+
logger.info(f"Password changed for user: {self._username}")
|
|
194
|
+
|
|
195
|
+
def _handle_multi_logon(self, session: GuiSession):
|
|
196
|
+
# TODO: If server supports multi-logon, allow user to multi-logon
|
|
197
|
+
|
|
198
|
+
logger.debug("Multi-logon detected")
|
|
199
|
+
if not self._terminate_other_sessions:
|
|
200
|
+
raise SAPLoginError("User already logged on in some other session.")
|
|
201
|
+
|
|
202
|
+
logger.debug("Terminating other sessions")
|
|
203
|
+
try:
|
|
204
|
+
rb = session.find_by_id(
|
|
205
|
+
SAPLoginScreenElements.terminate_other_sessions_radio, raise_error=False
|
|
206
|
+
)
|
|
207
|
+
if rb:
|
|
208
|
+
rb.select()
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.warning(
|
|
211
|
+
f"Could not find multi-logon terminate radio button, attempting to press enter key: {e}"
|
|
212
|
+
)
|
|
213
|
+
session.press_enter(1)
|
sapwright/_utils.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import winreg
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import psutil
|
|
9
|
+
import pywintypes
|
|
10
|
+
import win32com.client as win32
|
|
11
|
+
from pywinauto import Application
|
|
12
|
+
from pywinauto.timings import TimeoutError as PywinautoTimeoutError
|
|
13
|
+
|
|
14
|
+
from sapwright.exceptions import SAPLogonError
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
SAPLOGON_EXE = "saplogon.exe"
|
|
19
|
+
DEFAULT_TIMEOUT = 60
|
|
20
|
+
REGISTRY_KEYS = (
|
|
21
|
+
r"SOFTWARE\SAP\SAP Shared",
|
|
22
|
+
r"SOFTWARE\WOW6432Node\SAP\SAP Shared",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def launch_saplogon(exe_path: str | Path | None = None, timeout: int = 60) -> None:
|
|
27
|
+
"""Launches the SAP Logon Pad executable and waits for it to become ready.
|
|
28
|
+
If no exe_path is provided, saplogon.exe is resolved from windows registry.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
exe_path : str | Path | None, optional
|
|
33
|
+
Path to the saplogon executable, by default None
|
|
34
|
+
timeout : int, optional
|
|
35
|
+
Maximum time to wait for the application to become ready, by default 30
|
|
36
|
+
|
|
37
|
+
Raises
|
|
38
|
+
------
|
|
39
|
+
NotImplementedError
|
|
40
|
+
If called on non-windows platform
|
|
41
|
+
FileNotFoundError
|
|
42
|
+
If the saplogon executable cannot be found
|
|
43
|
+
TimeoutError
|
|
44
|
+
If timeout waiting for the main window after launching.
|
|
45
|
+
SAPLogonError
|
|
46
|
+
If the SAP Logon fails to launch
|
|
47
|
+
"""
|
|
48
|
+
if sys.platform != "win32":
|
|
49
|
+
raise NotImplementedError("Launch SAP Logon only supported on Windows")
|
|
50
|
+
|
|
51
|
+
if is_process_running(SAPLOGON_EXE):
|
|
52
|
+
logger.debug("SAP Logon is already running")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
path = Path(exe_path) if exe_path else get_saplogon_path()
|
|
56
|
+
if path is None or not path.is_file():
|
|
57
|
+
raise FileNotFoundError(
|
|
58
|
+
f"SAP Logon executable not found at {path or 'registry'}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
logger.debug(f"Launching SAP Logon from '{path}'")
|
|
62
|
+
try:
|
|
63
|
+
process = psutil.Popen([path])
|
|
64
|
+
app = Application().connect(process=process.pid, timeout=timeout)
|
|
65
|
+
app.top_window().wait("ready", timeout=timeout)
|
|
66
|
+
except PywinautoTimeoutError as e:
|
|
67
|
+
raise TimeoutError(f"Timeout waiting for SAP Logon to become ready: {e}") from e
|
|
68
|
+
except Exception as e:
|
|
69
|
+
msg = f"Failed to connect to SAP Logon: {e}"
|
|
70
|
+
logger.error(msg)
|
|
71
|
+
raise SAPLogonError(msg) from e
|
|
72
|
+
|
|
73
|
+
logger.debug("SAP Logon launched and ready")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_process_running(process_name: str) -> bool:
|
|
77
|
+
"""Checks if a process with the given name is running.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
process_name : str
|
|
82
|
+
Name of the process
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
bool
|
|
87
|
+
True if the process is running
|
|
88
|
+
|
|
89
|
+
Example
|
|
90
|
+
-------
|
|
91
|
+
>>> is_process_running("saplogon.exe")
|
|
92
|
+
True
|
|
93
|
+
"""
|
|
94
|
+
target = process_name.lower()
|
|
95
|
+
for proc in psutil.process_iter(["name"]):
|
|
96
|
+
if (proc.info["name"] or "").lower() == target:
|
|
97
|
+
return True
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_saplogon_path() -> Path | None:
|
|
102
|
+
"""Finds the saplogon executable path from the windows registry, or None if not found.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
Path | None
|
|
107
|
+
Path to the saplogon.exe or None if not found
|
|
108
|
+
"""
|
|
109
|
+
for key_path in REGISTRY_KEYS:
|
|
110
|
+
try:
|
|
111
|
+
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path) as key:
|
|
112
|
+
install_dir, _ = winreg.QueryValueEx(key, "SAPsysdir")
|
|
113
|
+
except FileNotFoundError:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
path = Path(install_dir) / SAPLOGON_EXE
|
|
117
|
+
if path.is_file():
|
|
118
|
+
return path
|
|
119
|
+
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def default_password_generator() -> str:
|
|
124
|
+
"""Generates a password from the current month abbreviation and year.
|
|
125
|
+
|
|
126
|
+
Example
|
|
127
|
+
-------
|
|
128
|
+
>>> default_password_generator() # on 2026-01-15
|
|
129
|
+
'Jan@2026'
|
|
130
|
+
"""
|
|
131
|
+
return datetime.date.today().strftime("%b@%Y") # noqa: DTZ011
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_scripting_engine() -> Any | None:
|
|
135
|
+
"""Returns the SAP GUI scripting engine, or None if SAP Logon is not running."""
|
|
136
|
+
try:
|
|
137
|
+
rot_entry = win32.GetObject("SAPGUI")
|
|
138
|
+
except pywintypes.com_error:
|
|
139
|
+
return None
|
|
140
|
+
return rot_entry.GetScriptingEngine
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from sapwright._utils import get_scripting_engine
|
|
5
|
+
from sapwright.exceptions import SAPConnectionError, SAPLogonError, SAPScriptingDisabled
|
|
6
|
+
from sapwright.objects import GuiSession
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _normalize(value: object) -> str:
|
|
12
|
+
return str(value).strip().lower()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _matches(
|
|
16
|
+
connection: Any, connection_string: str | None, connection_name: str | None
|
|
17
|
+
) -> bool:
|
|
18
|
+
"""Match by connection string (substring) if given, otherwise by description."""
|
|
19
|
+
if connection_string:
|
|
20
|
+
# connection_string returned by SAP logon contains a lot more text and other details
|
|
21
|
+
# we would only want to check for a substring match
|
|
22
|
+
return connection_string.lower() in str(connection.ConnectionString).lower()
|
|
23
|
+
|
|
24
|
+
# Case-insensitive connection name matching
|
|
25
|
+
return _normalize(connection.Description) == _normalize(connection_name)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConnectionManager:
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self._connection: Any = None
|
|
31
|
+
|
|
32
|
+
def find_connection_by_user(
|
|
33
|
+
self,
|
|
34
|
+
username: str,
|
|
35
|
+
connection_string: str | None = None,
|
|
36
|
+
connection_name: str | None = None,
|
|
37
|
+
) -> Any | None:
|
|
38
|
+
"""Check if a connection for the specified user is already open.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
object or None
|
|
43
|
+
The underlying native GuiConnection COM object if a matching connection
|
|
44
|
+
has a session logged in as the user, otherwise None
|
|
45
|
+
|
|
46
|
+
Raises
|
|
47
|
+
------
|
|
48
|
+
ValueError
|
|
49
|
+
If neither connection_string nor connection_name is provided
|
|
50
|
+
SAPScriptingDisabled
|
|
51
|
+
If SAP GUI Scripting is disabled by the server
|
|
52
|
+
"""
|
|
53
|
+
if not connection_string and not connection_name:
|
|
54
|
+
raise ValueError("connection_string or connection_name must be provided.")
|
|
55
|
+
|
|
56
|
+
application = get_scripting_engine()
|
|
57
|
+
if application is None:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
user = _normalize(username)
|
|
61
|
+
for i in range(application.Children.Count):
|
|
62
|
+
connection = application.Children(i)
|
|
63
|
+
if connection.DisabledByServer:
|
|
64
|
+
raise SAPScriptingDisabled(
|
|
65
|
+
"SAP GUI Scripting is disabled by server. Contact your SAP basis team to enable it."
|
|
66
|
+
)
|
|
67
|
+
if not _matches(connection, connection_string, connection_name):
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
sessions = connection.Children
|
|
71
|
+
for j in range(sessions.Count):
|
|
72
|
+
if _normalize(sessions(j).Info.User) == user:
|
|
73
|
+
self._connection = connection
|
|
74
|
+
return connection
|
|
75
|
+
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def open_connection(
|
|
79
|
+
self,
|
|
80
|
+
connection_string: str | None = None,
|
|
81
|
+
connection_name: str | None = None,
|
|
82
|
+
) -> GuiSession:
|
|
83
|
+
"""Open a new connection and return its first session.
|
|
84
|
+
|
|
85
|
+
connection_string takes precedence over connection_name.
|
|
86
|
+
|
|
87
|
+
Raises
|
|
88
|
+
------
|
|
89
|
+
SAPLogonError
|
|
90
|
+
If SAP Logon is not running
|
|
91
|
+
SAPConnectionError
|
|
92
|
+
If the connection could not be opened
|
|
93
|
+
"""
|
|
94
|
+
application = get_scripting_engine()
|
|
95
|
+
if application is None:
|
|
96
|
+
raise SAPLogonError("SAP Logon is not running")
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
if connection_string:
|
|
100
|
+
connection = application.OpenConnectionByConnectionString(
|
|
101
|
+
connection_string, True, True
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
connection = application.OpenConnection(connection_name, True, True)
|
|
105
|
+
|
|
106
|
+
self._connection = connection
|
|
107
|
+
return GuiSession(connection.Children(0))
|
|
108
|
+
except Exception as e:
|
|
109
|
+
msg = f"Failed to open SAP connection: {e}"
|
|
110
|
+
logger.error(msg)
|
|
111
|
+
raise SAPConnectionError(msg) from e
|
|
112
|
+
|
|
113
|
+
def close_connection(self):
|
|
114
|
+
"""
|
|
115
|
+
Closes the SAP connection including all sessions.
|
|
116
|
+
This method closes the SAP connection and all associated sessions,
|
|
117
|
+
and cleans up the internal references to the SAP GUI objects.
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
self._connection.CloseConnection()
|
|
121
|
+
logger.debug("SAP connection closed")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.error(f"Error while closing connection: {e}")
|
|
124
|
+
|
|
125
|
+
self._connection = None
|
sapwright/exceptions.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
class SAPGuiError(Exception):
|
|
2
|
+
"""Base class for all SAP GUI related errors"""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SAPLogonError(SAPGuiError):
|
|
6
|
+
"""Raised when SAP Logon fails to launch"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SAPConnectionError(SAPGuiError):
|
|
10
|
+
"""Raised when a connection to SAP Logon fails"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SAPScriptingDisabled(SAPGuiError):
|
|
14
|
+
"""Raised when SAP GUI Scripting is disabled by server"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SAPElementNotFound(SAPGuiError):
|
|
18
|
+
"""Raised when a SAP GUI element is not found"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SAPElementNotChangeable(SAPGuiError):
|
|
22
|
+
"""Raised when attempting to modify a read-only element."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SAPComboBoxOptionNotFound(SAPGuiError):
|
|
26
|
+
"""Raised when a specified option is not available in a ComboBox."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SAPStatusBarError(SAPGuiError):
|
|
30
|
+
"""Raised when there is an error in the status bar."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SAPTransactionError(SAPGuiError):
|
|
34
|
+
"""Raised when a transaction fails to start or encounters a critical error."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SAPLoginError(SAPGuiError):
|
|
38
|
+
"""Raised when the login process fails."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SAPElementTypeMismatch(SAPGuiError):
|
|
42
|
+
"""Raised when the element type does not match the expected type."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SAPTableConfigurationError(SAPGuiError):
|
|
46
|
+
"""Raised when there is a mismatch or error in table configuration/population."""
|
sapwright/models.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StatusBarMsg(BaseModel):
|
|
5
|
+
"""The status bar message"""
|
|
6
|
+
|
|
7
|
+
id: str
|
|
8
|
+
type: str
|
|
9
|
+
text: str
|
|
10
|
+
number: str
|
|
11
|
+
has_longtext: int
|
|
12
|
+
is_popup: bool | None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SessionInfo(BaseModel):
|
|
16
|
+
"""Session information"""
|
|
17
|
+
|
|
18
|
+
application_server: str = ""
|
|
19
|
+
client: str = ""
|
|
20
|
+
codepage: int | None = None
|
|
21
|
+
flushes: int | None = None
|
|
22
|
+
group: str = ""
|
|
23
|
+
gui_codepage: int | None = None
|
|
24
|
+
i18n_mode: bool | None = None
|
|
25
|
+
interpretation_time: int | None = None
|
|
26
|
+
is_low_speed_connection: bool | None = None
|
|
27
|
+
language: str = ""
|
|
28
|
+
message_server: str = ""
|
|
29
|
+
program: str = ""
|
|
30
|
+
response_time: int | None = None
|
|
31
|
+
round_trips: int | None = None
|
|
32
|
+
screen_number: int | None = None
|
|
33
|
+
scripting_mode_force_notification: bool | None = None
|
|
34
|
+
scripting_mode_read_only: bool | None = None
|
|
35
|
+
scripting_mode_recording_disabled: bool | None = None
|
|
36
|
+
system_name: str = ""
|
|
37
|
+
session_number: int | None = None
|
|
38
|
+
system_session_id: str = ""
|
|
39
|
+
transaction: str = ""
|
|
40
|
+
ui_guideline: int | None = None
|
|
41
|
+
user: str = ""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from sapwright.objects.component import GuiComponent, GuiComponentCollection
|
|
2
|
+
from sapwright.objects.scrollbar import GuiScrollbar
|
|
3
|
+
from sapwright.objects.session import GuiSession
|
|
4
|
+
from sapwright.objects.shell import GuiShell
|
|
5
|
+
from sapwright.objects.table_control import GuiTableColumn, GuiTableControl, GuiTableRow
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"GuiComponent",
|
|
9
|
+
"GuiComponentCollection",
|
|
10
|
+
"GuiScrollbar",
|
|
11
|
+
"GuiSession",
|
|
12
|
+
"GuiShell",
|
|
13
|
+
"GuiTableColumn",
|
|
14
|
+
"GuiTableControl",
|
|
15
|
+
"GuiTableRow",
|
|
16
|
+
]
|