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 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,5 @@
1
+ """SAP automation backends (Java gateway and Windows COM)."""
2
+
3
+ from SapLibrary.backends.base import SapAutomationBackend, resolve_client
4
+
5
+ __all__ = ["SapAutomationBackend", "resolve_client"]
@@ -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