robotframework-velo-sapgui 0.2.2__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.
- SapLibrary/__init__.py +27 -0
- SapLibrary/backends/__init__.py +5 -0
- SapLibrary/backends/base.py +70 -0
- SapLibrary/backends/event_capture.py +78 -0
- SapLibrary/backends/java_gateway.py +126 -0
- SapLibrary/backends/windows_scripting.py +477 -0
- SapLibrary/exceptions.py +30 -0
- SapLibrary/keywords.py +340 -0
- SapLibrary/library.py +120 -0
- SapLibrary/py.typed +0 -0
- robotframework_velo_sapgui-0.2.2.dist-info/METADATA +181 -0
- robotframework_velo_sapgui-0.2.2.dist-info/RECORD +15 -0
- robotframework_velo_sapgui-0.2.2.dist-info/WHEEL +5 -0
- robotframework_velo_sapgui-0.2.2.dist-info/licenses/LICENSE +49 -0
- robotframework_velo_sapgui-0.2.2.dist-info/top_level.txt +1 -0
SapLibrary/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from SapLibrary.exceptions import SapConnectionError, SapKeywordError, SapLibraryError
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = version("robotframework-velo-sapgui")
|
|
9
|
+
except PackageNotFoundError:
|
|
10
|
+
__version__ = "unknown"
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"SapLibrary",
|
|
14
|
+
"SapLibraryError",
|
|
15
|
+
"SapConnectionError",
|
|
16
|
+
"SapKeywordError",
|
|
17
|
+
"__version__",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def __getattr__(name: str) -> object:
|
|
22
|
+
if name == "SapLibrary":
|
|
23
|
+
from SapLibrary.library import SapLibrary
|
|
24
|
+
|
|
25
|
+
globals()["SapLibrary"] = SapLibrary
|
|
26
|
+
return SapLibrary
|
|
27
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Backend interface for SAP GUI automation (Java gateway or Windows COM)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def resolve_client(client: str | None = None) -> str:
|
|
12
|
+
"""Resolve SAP client type: ``auto``, ``java``, or ``windows``."""
|
|
13
|
+
raw = (client or os.getenv("VELO_SAP_CLIENT") or "auto").strip().lower()
|
|
14
|
+
if raw not in ("auto", "java", "windows"):
|
|
15
|
+
raise ValueError(f"Invalid SAP client '{client}'. Expected auto, java, or windows.")
|
|
16
|
+
if raw == "auto":
|
|
17
|
+
return "windows" if sys.platform == "win32" else "java"
|
|
18
|
+
if raw == "windows" and sys.platform != "win32":
|
|
19
|
+
raise RuntimeError(
|
|
20
|
+
"SAP GUI for Windows backend requires a Windows host. "
|
|
21
|
+
"Use client=java or VELO_SAP_CLIENT=java for Docker/Java execution."
|
|
22
|
+
)
|
|
23
|
+
return raw
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BackendResponse:
|
|
27
|
+
"""Minimal response object compatible with Java StepResponse accessors."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, success: bool, message: str = "", result: Any = None) -> None:
|
|
30
|
+
self._success = success
|
|
31
|
+
self._message = message
|
|
32
|
+
self._result = result
|
|
33
|
+
|
|
34
|
+
def isSuccessful(self) -> bool:
|
|
35
|
+
return self._success
|
|
36
|
+
|
|
37
|
+
def getMessage(self) -> str:
|
|
38
|
+
return self._message
|
|
39
|
+
|
|
40
|
+
def getResult(self) -> Any:
|
|
41
|
+
return self._result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SapAutomationBackend(ABC):
|
|
45
|
+
"""Abstract SAP automation backend."""
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def ensure_connected(self) -> None:
|
|
49
|
+
"""Open backend resources if needed."""
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def call(self, method: str, *args: Any) -> Any:
|
|
53
|
+
"""Invoke a facade method by name."""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def start_event_capture(self) -> None:
|
|
57
|
+
"""Begin scripting event capture."""
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def stop_event_capture(self) -> str | None:
|
|
61
|
+
"""Stop event capture and return output path."""
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def cleanup(self, close_app: bool = True, stop_recording: bool = True) -> str:
|
|
65
|
+
"""Release backend resources."""
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def client_name(self) -> str:
|
|
70
|
+
"""Backend identifier (``java`` or ``windows``)."""
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Normalized scripting event log (events.jsonl) shared by Java and Windows backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from threading import Lock
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EventCaptureWriter:
|
|
14
|
+
"""Append-only JSONL writer for SAP GUI scripting interaction events."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, output_path: Path | None = None) -> None:
|
|
17
|
+
results_dir = os.environ.get("RESULTS_DIR", "/results")
|
|
18
|
+
default_path = Path(results_dir) / "events.jsonl"
|
|
19
|
+
env_path = os.environ.get("VELO_EVENTS_PATH")
|
|
20
|
+
self._path = Path(output_path or env_path or default_path)
|
|
21
|
+
self._active = False
|
|
22
|
+
self._lock = Lock()
|
|
23
|
+
self._events: list[dict[str, Any]] = []
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def is_active(self) -> bool:
|
|
27
|
+
return self._active
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def path(self) -> Path:
|
|
31
|
+
return self._path
|
|
32
|
+
|
|
33
|
+
def start(self) -> None:
|
|
34
|
+
with self._lock:
|
|
35
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
self._events = []
|
|
37
|
+
self._active = True
|
|
38
|
+
|
|
39
|
+
def record(
|
|
40
|
+
self,
|
|
41
|
+
action: str,
|
|
42
|
+
*,
|
|
43
|
+
element_id: str = "",
|
|
44
|
+
name: str = "",
|
|
45
|
+
element_type: str = "",
|
|
46
|
+
value: str = "",
|
|
47
|
+
screen_context: str = "",
|
|
48
|
+
source: str = "",
|
|
49
|
+
) -> None:
|
|
50
|
+
if not self._active:
|
|
51
|
+
return
|
|
52
|
+
event = {
|
|
53
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
54
|
+
"action": action,
|
|
55
|
+
"element_id": element_id,
|
|
56
|
+
"name": name,
|
|
57
|
+
"type": element_type,
|
|
58
|
+
"value": value,
|
|
59
|
+
"screen_context": screen_context,
|
|
60
|
+
"source": source,
|
|
61
|
+
}
|
|
62
|
+
with self._lock:
|
|
63
|
+
self._events.append(event)
|
|
64
|
+
|
|
65
|
+
def stop(self) -> str:
|
|
66
|
+
with self._lock:
|
|
67
|
+
if not self._active:
|
|
68
|
+
return str(self._path)
|
|
69
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
with self._path.open("w", encoding="utf-8") as handle:
|
|
71
|
+
for event in self._events:
|
|
72
|
+
handle.write(json.dumps(event, ensure_ascii=False) + "\n")
|
|
73
|
+
self._active = False
|
|
74
|
+
return str(self._path)
|
|
75
|
+
|
|
76
|
+
def get_events(self) -> list[dict[str, Any]]:
|
|
77
|
+
with self._lock:
|
|
78
|
+
return list(self._events)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Java py4j gateway backend (SAP GUI for Java)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from py4j.java_gateway import GatewayParameters, JavaGateway
|
|
9
|
+
from py4j.protocol import Py4JError, Py4JJavaError
|
|
10
|
+
|
|
11
|
+
from SapLibrary.backends.base import SapAutomationBackend
|
|
12
|
+
from SapLibrary.exceptions import SapConnectionError, SapKeywordError
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class JavaGatewayBackend(SapAutomationBackend):
|
|
18
|
+
"""Drive SAP GUI for Java via the sapgui-engine py4j gateway."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, port: int = 8081) -> None:
|
|
21
|
+
self._port = int(port)
|
|
22
|
+
self._gateway: JavaGateway | None = None
|
|
23
|
+
self._automation: Any = None
|
|
24
|
+
self._event_capture_active = False
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def client_name(self) -> str:
|
|
28
|
+
return "java"
|
|
29
|
+
|
|
30
|
+
def ensure_connected(self) -> None:
|
|
31
|
+
if self._gateway is not None:
|
|
32
|
+
return
|
|
33
|
+
try:
|
|
34
|
+
self._gateway = JavaGateway(
|
|
35
|
+
gateway_parameters=GatewayParameters(
|
|
36
|
+
port=self._port,
|
|
37
|
+
auto_field=True,
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
self._automation = self._gateway.entry_point.getAutomationFacade()
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
self._gateway = None
|
|
43
|
+
self._automation = None
|
|
44
|
+
raise SapConnectionError(
|
|
45
|
+
f"Cannot connect to the Java automation gateway on port {self._port}. "
|
|
46
|
+
"Ensure the Java process is running before calling any keyword."
|
|
47
|
+
) from exc
|
|
48
|
+
|
|
49
|
+
def call(self, method: str, *args: Any) -> Any:
|
|
50
|
+
self.ensure_connected()
|
|
51
|
+
try:
|
|
52
|
+
response = getattr(self._automation, method)(*args)
|
|
53
|
+
except (Py4JJavaError, Py4JError) as exc:
|
|
54
|
+
raise SapKeywordError(method, exc) from exc
|
|
55
|
+
except AttributeError as exc:
|
|
56
|
+
raise SapKeywordError(method, exc) from exc
|
|
57
|
+
|
|
58
|
+
if hasattr(response, "isSuccessful") and not response.isSuccessful():
|
|
59
|
+
msg = response.getMessage() if hasattr(response, "getMessage") else "Action failed"
|
|
60
|
+
raise SapKeywordError(method, msg)
|
|
61
|
+
|
|
62
|
+
return response
|
|
63
|
+
|
|
64
|
+
def start_event_capture(self) -> None:
|
|
65
|
+
self.ensure_connected()
|
|
66
|
+
response = self._automation.startEventCapture()
|
|
67
|
+
if hasattr(response, "isSuccessful") and not response.isSuccessful():
|
|
68
|
+
msg = (
|
|
69
|
+
response.getMessage()
|
|
70
|
+
if hasattr(response, "getMessage")
|
|
71
|
+
else "startEventCapture failed"
|
|
72
|
+
)
|
|
73
|
+
raise SapConnectionError(msg)
|
|
74
|
+
self._event_capture_active = True
|
|
75
|
+
|
|
76
|
+
def stop_event_capture(self) -> str | None:
|
|
77
|
+
if not self._event_capture_active or self._automation is None:
|
|
78
|
+
return None
|
|
79
|
+
try:
|
|
80
|
+
result = self._automation.stopEventCapture()
|
|
81
|
+
self._event_capture_active = False
|
|
82
|
+
return str(result) if result is not None else None
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
logger.warning("JavaGatewayBackend: stopEventCapture() failed: %s", exc)
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def cleanup(self, close_app: bool = True, stop_recording: bool = True) -> str:
|
|
88
|
+
if self._gateway is None:
|
|
89
|
+
return "Cleanup: gateway was already closed."
|
|
90
|
+
|
|
91
|
+
events_path: str | None = None
|
|
92
|
+
if stop_recording:
|
|
93
|
+
events_path = self.stop_event_capture()
|
|
94
|
+
|
|
95
|
+
if close_app and self._automation is not None:
|
|
96
|
+
try:
|
|
97
|
+
self._automation.closeApplication()
|
|
98
|
+
except Exception:
|
|
99
|
+
logger.warning("JavaGatewayBackend: closeApplication() failed during cleanup.")
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
self._gateway.close()
|
|
103
|
+
except Exception:
|
|
104
|
+
logger.warning("JavaGatewayBackend: gateway.close() failed.")
|
|
105
|
+
finally:
|
|
106
|
+
self._gateway = None
|
|
107
|
+
self._automation = None
|
|
108
|
+
self._event_capture_active = False
|
|
109
|
+
|
|
110
|
+
return events_path or "Cleanup complete."
|
|
111
|
+
|
|
112
|
+
def get_captured_events(self) -> list[Any]:
|
|
113
|
+
self.ensure_connected()
|
|
114
|
+
return list(self._automation.getCapturedEvents())
|
|
115
|
+
|
|
116
|
+
def close_gateway_only(self) -> None:
|
|
117
|
+
"""Close py4j socket without closing the SAP application."""
|
|
118
|
+
if self._gateway is None:
|
|
119
|
+
return
|
|
120
|
+
try:
|
|
121
|
+
self._gateway.close()
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
finally:
|
|
125
|
+
self._gateway = None
|
|
126
|
+
self._automation = None
|