iris-pex-embedded-python 3.7.2b2__py3-none-any.whl → 4.0.0__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.
- iop/__init__.py +276 -18
- iop/__main__.py +3 -3
- iop/cli/__init__.py +1 -0
- iop/cli/formatting.py +45 -0
- iop/{_cli.py → cli/main.py} +238 -216
- iop/cli/parser.py +165 -0
- iop/cli/types.py +73 -0
- iop/cls/IOP/Common.cls +11 -2
- iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
- iop/cls/IOP/Service/Remote/Rest/v1.cls +152 -5
- iop/cls/IOP/Utils.cls +814 -7
- iop/cls/IOP/Wrapper.cls +1 -1
- iop/components/__init__.py +16 -0
- iop/{_async_request.py → components/async_request.py} +7 -7
- iop/components/business_host.py +363 -0
- iop/components/business_operation.py +84 -0
- iop/{_business_process.py → components/business_process.py} +145 -125
- iop/components/business_service.py +106 -0
- iop/{_common.py → components/common.py} +160 -164
- iop/{_debugpy.py → components/debugpy.py} +11 -11
- iop/{_generator_request.py → components/generator_request.py} +4 -4
- iop/{_inbound_adapter.py → components/inbound_adapter.py} +10 -18
- iop/{_log_manager.py → components/log_manager.py} +1 -1
- iop/{_outbound_adapter.py → components/outbound_adapter.py} +1 -1
- iop/components/polling_business_service.py +48 -0
- iop/{_private_session_duplex.py → components/private_session_duplex.py} +7 -9
- iop/{_private_session_process.py → components/private_session_process.py} +2 -2
- iop/{_settings.py → components/settings.py} +23 -3
- iop/messages/__init__.py +7 -0
- iop/messages/base.py +61 -0
- iop/{_decorators.py → messages/decorators.py} +8 -2
- iop/messages/dispatch.py +351 -0
- iop/{_persistent_message.py → messages/persistent.py} +24 -19
- iop/{_serialization.py → messages/serialization.py} +34 -21
- iop/{_message_validator.py → messages/validation.py} +15 -13
- iop/migration/__init__.py +1 -0
- iop/migration/io.py +61 -0
- iop/migration/manifest.py +534 -0
- iop/migration/plans.py +80 -0
- iop/migration/utils.py +1126 -0
- iop/production/__init__.py +55 -0
- iop/production/actions.py +525 -0
- iop/production/common.py +103 -0
- iop/production/component.py +234 -0
- iop/production/declarations.py +190 -0
- iop/production/declarative.py +214 -0
- iop/production/diff.py +327 -0
- iop/production/import_.py +296 -0
- iop/production/inspection.py +92 -0
- iop/production/model.py +1143 -0
- iop/production/planning.py +678 -0
- iop/production/reconstruction.py +341 -0
- iop/production/rendering.py +610 -0
- iop/production/runtime.py +91 -0
- iop/production/source_inference.py +729 -0
- iop/production/types.py +546 -0
- iop/production/validation.py +452 -0
- iop/runtime/__init__.py +1 -0
- iop/runtime/director.py +415 -0
- iop/runtime/environment.py +59 -0
- iop/{_iris.py → runtime/iris.py} +2 -2
- iop/runtime/local.py +145 -0
- iop/{_director_protocol.py → runtime/protocol.py} +25 -8
- iop/runtime/remote/__init__.py +12 -0
- iop/runtime/remote/client.py +90 -0
- iop/runtime/remote/director.py +283 -0
- iop/runtime/remote/migration.py +57 -0
- iop/runtime/remote/settings.py +69 -0
- iop/runtime/remote/setup.py +72 -0
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA +11 -2
- iris_pex_embedded_python-4.0.0.dist-info/RECORD +99 -0
- iris_pex_embedded_python-4.0.0.dist-info/entry_points.txt +2 -0
- iris_pex_embedded_python-4.0.0.dist-info/top_level.txt +1 -0
- grongier/__init__.py +0 -0
- grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -8
- grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -13
- grongier/cls/Grongier/PEX/BusinessService.cls +0 -8
- grongier/cls/Grongier/PEX/Common.cls +0 -10
- grongier/cls/Grongier/PEX/Director.cls +0 -10
- grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -4
- grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -13
- grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -4
- grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/Message.cls +0 -13
- grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/PickleMessage.cls +0 -13
- grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -8
- grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -14
- grongier/cls/Grongier/PEX/Test.cls +0 -10
- grongier/cls/Grongier/PEX/Utils.cls +0 -10
- grongier/cls/Grongier/Service/WSGI.cls +0 -4
- grongier/pex/__init__.py +0 -75
- grongier/pex/__main__.py +0 -4
- grongier/pex/_business_host.py +0 -1
- grongier/pex/_cli.py +0 -4
- grongier/pex/_common.py +0 -1
- grongier/pex/_director.py +0 -1
- grongier/pex/_utils.py +0 -1
- grongier/pex/wsgi/handlers.py +0 -104
- iop/_business_host.py +0 -322
- iop/_business_operation.py +0 -77
- iop/_business_service.py +0 -67
- iop/_director.py +0 -333
- iop/_dispatch.py +0 -194
- iop/_local.py +0 -106
- iop/_message.py +0 -45
- iop/_polling_business_service.py +0 -6
- iop/_remote.py +0 -473
- iop/_utils.py +0 -912
- iop/cls/IOP/Service/WSGI.cls +0 -310
- iop/wsgi/handlers.py +0 -112
- iris_pex_embedded_python-3.7.2b2.dist-info/RECORD +0 -96
- iris_pex_embedded_python-3.7.2b2.dist-info/entry_points.txt +0 -2
- iris_pex_embedded_python-3.7.2b2.dist-info/top_level.txt +0 -2
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/WHEEL +0 -0
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .director import _print_log_entry, _RemoteDirector
|
|
4
|
+
from .settings import get_remote_settings
|
|
5
|
+
from .settings import load_remote_settings_from_file as _load_remote_settings_from_file
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"_RemoteDirector",
|
|
9
|
+
"_load_remote_settings_from_file",
|
|
10
|
+
"_print_log_entry",
|
|
11
|
+
"get_remote_settings",
|
|
12
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
import urllib3
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _RemoteClient:
|
|
10
|
+
"""Small HTTP client for the IOP REST API."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, remote_settings: dict[str, Any]) -> None:
|
|
13
|
+
self._url = remote_settings["url"].rstrip("/")
|
|
14
|
+
self._base = self._url + "/api/iop"
|
|
15
|
+
self._auth = (
|
|
16
|
+
remote_settings.get("username", ""),
|
|
17
|
+
remote_settings.get("password", ""),
|
|
18
|
+
)
|
|
19
|
+
self._namespace: str = remote_settings.get("namespace", "USER")
|
|
20
|
+
self._verify: bool = remote_settings.get("verify_ssl", True)
|
|
21
|
+
if not self._verify:
|
|
22
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def _raise_for_status(resp: requests.Response) -> None:
|
|
26
|
+
"""Like resp.raise_for_status() but includes the response body error message."""
|
|
27
|
+
if resp.ok:
|
|
28
|
+
return
|
|
29
|
+
try:
|
|
30
|
+
body = resp.json()
|
|
31
|
+
error = body.get("error") or body.get("message") or resp.text
|
|
32
|
+
except Exception:
|
|
33
|
+
error = resp.text or resp.reason
|
|
34
|
+
raise requests.exceptions.HTTPError(
|
|
35
|
+
f"{resp.status_code} {resp.reason}: {error}",
|
|
36
|
+
response=resp,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def _get(self, path: str, params: dict | None = None) -> Any:
|
|
40
|
+
p = {"namespace": self._namespace, **(params or {})}
|
|
41
|
+
resp = requests.get(
|
|
42
|
+
f"{self._base}{path}",
|
|
43
|
+
params=p,
|
|
44
|
+
auth=self._auth,
|
|
45
|
+
verify=self._verify,
|
|
46
|
+
timeout=30,
|
|
47
|
+
)
|
|
48
|
+
self._raise_for_status(resp)
|
|
49
|
+
return resp.json()
|
|
50
|
+
|
|
51
|
+
def _post(self, path: str, body: dict | None = None) -> Any:
|
|
52
|
+
resp = requests.post(
|
|
53
|
+
f"{self._base}{path}",
|
|
54
|
+
json=(body or {}),
|
|
55
|
+
params={"namespace": self._namespace},
|
|
56
|
+
auth=self._auth,
|
|
57
|
+
verify=self._verify,
|
|
58
|
+
timeout=30,
|
|
59
|
+
)
|
|
60
|
+
self._raise_for_status(resp)
|
|
61
|
+
return resp.json()
|
|
62
|
+
|
|
63
|
+
def _put(self, path: str, body: dict | None = None) -> Any:
|
|
64
|
+
resp = requests.put(
|
|
65
|
+
f"{self._base}{path}",
|
|
66
|
+
json=(body or {}),
|
|
67
|
+
params={"namespace": self._namespace},
|
|
68
|
+
auth=self._auth,
|
|
69
|
+
verify=self._verify,
|
|
70
|
+
timeout=30,
|
|
71
|
+
)
|
|
72
|
+
self._raise_for_status(resp)
|
|
73
|
+
return resp.json()
|
|
74
|
+
|
|
75
|
+
def _delete(self, path: str, params: dict | None = None) -> Any:
|
|
76
|
+
p = {"namespace": self._namespace, **(params or {})}
|
|
77
|
+
resp = requests.delete(
|
|
78
|
+
f"{self._base}{path}",
|
|
79
|
+
params=p,
|
|
80
|
+
auth=self._auth,
|
|
81
|
+
verify=self._verify,
|
|
82
|
+
timeout=30,
|
|
83
|
+
)
|
|
84
|
+
self._raise_for_status(resp)
|
|
85
|
+
return resp.json()
|
|
86
|
+
|
|
87
|
+
def _check_error(self, data: Any) -> Any:
|
|
88
|
+
if isinstance(data, dict) and "error" in data:
|
|
89
|
+
raise RuntimeError(data["error"])
|
|
90
|
+
return data
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Remote director: mirrors _Director's interface over the IOP REST API.
|
|
2
|
+
|
|
3
|
+
Configure via environment variables:
|
|
4
|
+
IOP_URL Required. e.g. http://localhost:8080
|
|
5
|
+
IOP_USERNAME Optional. Default: ""
|
|
6
|
+
IOP_PASSWORD Optional. Default: ""
|
|
7
|
+
IOP_NAMESPACE Optional. Default: "USER"
|
|
8
|
+
IOP_VERIFY_SSL Optional. "0"/"false" to disable TLS verification.
|
|
9
|
+
|
|
10
|
+
Or pass a RemoteSettings dict directly (same shape as REMOTE_SETTINGS in settings.py).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import signal
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
|
|
20
|
+
from ..protocol import (
|
|
21
|
+
DirectorProtocol as _DirectorProtocol, # noqa: F401 --- IGNORE ---
|
|
22
|
+
)
|
|
23
|
+
from .client import _RemoteClient
|
|
24
|
+
from .migration import upload_migration
|
|
25
|
+
from .settings import (
|
|
26
|
+
get_remote_settings,
|
|
27
|
+
)
|
|
28
|
+
from .settings import (
|
|
29
|
+
load_remote_settings_from_file as _load_remote_settings_from_file,
|
|
30
|
+
)
|
|
31
|
+
from .setup import setup_remote_classes
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"_RemoteDirector",
|
|
35
|
+
"_load_remote_settings_from_file",
|
|
36
|
+
"_print_log_entry",
|
|
37
|
+
"get_remote_settings",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _RemoteDirector(_RemoteClient, _DirectorProtocol):
|
|
42
|
+
"""Implements DirectorProtocol over the IOP REST API."""
|
|
43
|
+
|
|
44
|
+
# ------------------------------------------------------------------
|
|
45
|
+
# Production lifecycle
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def get_default_production(self) -> str:
|
|
49
|
+
data = self._check_error(self._get("/default"))
|
|
50
|
+
return data.get("production") or "Not defined"
|
|
51
|
+
|
|
52
|
+
def set_default_production(self, production_name: str = "") -> None:
|
|
53
|
+
self._check_error(self._put("/default", {"production": production_name}))
|
|
54
|
+
|
|
55
|
+
def list_productions(self) -> dict:
|
|
56
|
+
return self._check_error(self._get("/list"))
|
|
57
|
+
|
|
58
|
+
def status_production(self) -> dict:
|
|
59
|
+
data = self._check_error(self._get("/status"))
|
|
60
|
+
if not data.get("production"):
|
|
61
|
+
data["production"] = self.get_default_production()
|
|
62
|
+
return data
|
|
63
|
+
|
|
64
|
+
def start_production(self, production_name: str | None = None) -> None:
|
|
65
|
+
body: dict = {}
|
|
66
|
+
if production_name:
|
|
67
|
+
body["production"] = production_name
|
|
68
|
+
self._check_error(self._post("/start", body))
|
|
69
|
+
|
|
70
|
+
def start_production_with_log(self, production_name: str | None = None) -> None:
|
|
71
|
+
"""Start remotely then stream the log until Ctrl-C (which also stops)."""
|
|
72
|
+
self.start_production(production_name)
|
|
73
|
+
prod = production_name or self.get_default_production()
|
|
74
|
+
print(f"Production '{prod}' started. Streaming log — Ctrl-C to stop.")
|
|
75
|
+
running = True
|
|
76
|
+
|
|
77
|
+
def _sigint(sig, frame): # pragma: no cover
|
|
78
|
+
nonlocal running
|
|
79
|
+
running = False
|
|
80
|
+
|
|
81
|
+
signal.signal(signal.SIGINT, _sigint)
|
|
82
|
+
|
|
83
|
+
last_id = 0
|
|
84
|
+
for entry in self._get_log_entries(top=10):
|
|
85
|
+
_print_log_entry(entry)
|
|
86
|
+
last_id = max(last_id, entry.get("id", 0))
|
|
87
|
+
|
|
88
|
+
while running:
|
|
89
|
+
time.sleep(1)
|
|
90
|
+
entries = self._get_log_entries(since_id=last_id)
|
|
91
|
+
for entry in entries:
|
|
92
|
+
_print_log_entry(entry)
|
|
93
|
+
last_id = max(last_id, entry.get("id", 0))
|
|
94
|
+
|
|
95
|
+
self.stop_production()
|
|
96
|
+
|
|
97
|
+
def stop_production(self) -> None:
|
|
98
|
+
self._check_error(self._post("/stop"))
|
|
99
|
+
|
|
100
|
+
def shutdown_production(self) -> None:
|
|
101
|
+
self._check_error(self._post("/kill"))
|
|
102
|
+
|
|
103
|
+
def restart_production(self) -> None:
|
|
104
|
+
self._check_error(self._post("/restart"))
|
|
105
|
+
|
|
106
|
+
def update_production(self) -> None:
|
|
107
|
+
self._check_error(self._post("/update"))
|
|
108
|
+
|
|
109
|
+
def start_component(self, component_name: str) -> None:
|
|
110
|
+
self._check_error(
|
|
111
|
+
self._post("/component/start", {"component": component_name})
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def stop_component(self, component_name: str) -> None:
|
|
115
|
+
self._check_error(
|
|
116
|
+
self._post("/component/stop", {"component": component_name})
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def restart_component(self, component_name: str) -> None:
|
|
120
|
+
self._check_error(
|
|
121
|
+
self._post("/component/restart", {"component": component_name})
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def list_bindings(self, unused_only: bool = False) -> list[dict]:
|
|
125
|
+
params = {"unused": 1} if unused_only else {}
|
|
126
|
+
data = self._check_error(self._get("/bindings", params))
|
|
127
|
+
return data if isinstance(data, list) else []
|
|
128
|
+
|
|
129
|
+
def unbind_component(self, iris_classname: str) -> None:
|
|
130
|
+
self._check_error(self._delete("/binding", {"class": iris_classname}))
|
|
131
|
+
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
# Logging
|
|
134
|
+
# ------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def _get_log_entries(
|
|
137
|
+
self,
|
|
138
|
+
top: int = 10,
|
|
139
|
+
since_id: int | None = None,
|
|
140
|
+
) -> list[dict]:
|
|
141
|
+
params: dict = {}
|
|
142
|
+
if since_id is not None:
|
|
143
|
+
params["since_id"] = since_id
|
|
144
|
+
else:
|
|
145
|
+
params["top"] = top
|
|
146
|
+
data = self._check_error(self._get("/log", params))
|
|
147
|
+
return data if isinstance(data, list) else []
|
|
148
|
+
|
|
149
|
+
def log_production_top(self, top: int = 10) -> None:
|
|
150
|
+
entries = self._get_log_entries(top=top)
|
|
151
|
+
for entry in reversed(entries):
|
|
152
|
+
_print_log_entry(entry)
|
|
153
|
+
|
|
154
|
+
def log_production(self) -> None:
|
|
155
|
+
"""Stream log continuously until Ctrl-C."""
|
|
156
|
+
running = True
|
|
157
|
+
|
|
158
|
+
def _sigint(sig, frame): # pragma: no cover
|
|
159
|
+
nonlocal running
|
|
160
|
+
running = False
|
|
161
|
+
|
|
162
|
+
signal.signal(signal.SIGINT, _sigint)
|
|
163
|
+
|
|
164
|
+
last_id = 0
|
|
165
|
+
for entry in self._get_log_entries(top=10):
|
|
166
|
+
_print_log_entry(entry)
|
|
167
|
+
last_id = max(last_id, entry.get("id", 0))
|
|
168
|
+
|
|
169
|
+
while running:
|
|
170
|
+
time.sleep(1)
|
|
171
|
+
entries = self._get_log_entries(since_id=last_id)
|
|
172
|
+
for entry in entries:
|
|
173
|
+
_print_log_entry(entry)
|
|
174
|
+
last_id = max(last_id, entry.get("id", 0))
|
|
175
|
+
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
# Test
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def test_component(
|
|
181
|
+
self,
|
|
182
|
+
target: str | None,
|
|
183
|
+
message=None, # ignored remotely — not serialisable over HTTP
|
|
184
|
+
classname: str | None = None,
|
|
185
|
+
body: str | dict | None = None,
|
|
186
|
+
restart: bool = True,
|
|
187
|
+
) -> dict:
|
|
188
|
+
"""Returns a dict: {"classname": "...", "body": "...", "truncated": false}.
|
|
189
|
+
|
|
190
|
+
If *restart* is True the target component is stopped and restarted on
|
|
191
|
+
the server before the test message is dispatched.
|
|
192
|
+
"""
|
|
193
|
+
payload: dict = {"target": target or ""}
|
|
194
|
+
if classname:
|
|
195
|
+
payload["classname"] = classname
|
|
196
|
+
if body is not None:
|
|
197
|
+
payload["body"] = body
|
|
198
|
+
if restart:
|
|
199
|
+
payload["restart"] = True
|
|
200
|
+
try:
|
|
201
|
+
return self._check_error(self._post("/test", payload))
|
|
202
|
+
except requests.exceptions.HTTPError as exc:
|
|
203
|
+
try:
|
|
204
|
+
err_msg = exc.response.json().get("error", str(exc))
|
|
205
|
+
except Exception:
|
|
206
|
+
err_msg = str(exc)
|
|
207
|
+
raise RuntimeError(err_msg) from exc
|
|
208
|
+
except requests.exceptions.RequestException as exc:
|
|
209
|
+
raise RuntimeError(f"Remote test component error: {exc}") from exc
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
# Export
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
def export_production(self, production_name: str) -> dict:
|
|
216
|
+
return self._check_error(self._get("/export", {"production": production_name}))
|
|
217
|
+
|
|
218
|
+
def export_production_connections(self, production_name: str) -> dict:
|
|
219
|
+
return self._check_error(
|
|
220
|
+
self._get("/connections", {"production": production_name})
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def export_production_queue_info(self, production_name: str) -> dict:
|
|
224
|
+
return self._check_error(self._get("/queues", {"production": production_name}))
|
|
225
|
+
|
|
226
|
+
def apply_production_plan(
|
|
227
|
+
self,
|
|
228
|
+
plan: dict,
|
|
229
|
+
allow_destructive: bool = False,
|
|
230
|
+
) -> dict:
|
|
231
|
+
raise RuntimeError(
|
|
232
|
+
"Remote production plan apply is not supported in v1. "
|
|
233
|
+
"Run apply from a local IRIS environment."
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# ------------------------------------------------------------------
|
|
237
|
+
# Migrate
|
|
238
|
+
# ------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
def migrate(
|
|
241
|
+
self,
|
|
242
|
+
path: str,
|
|
243
|
+
strict_production_validation: bool = False,
|
|
244
|
+
) -> None:
|
|
245
|
+
upload_migration(
|
|
246
|
+
self,
|
|
247
|
+
path,
|
|
248
|
+
strict_production_validation=strict_production_validation,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# ------------------------------------------------------------------
|
|
252
|
+
# Init / setup — uploads .cls files via the Atelier API
|
|
253
|
+
# ------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
def setup(self, path: str | None = None) -> None:
|
|
256
|
+
setup_remote_classes(self, path)
|
|
257
|
+
|
|
258
|
+
# ------------------------------------------------------------------
|
|
259
|
+
# Metadata
|
|
260
|
+
# ------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def namespace(self) -> str:
|
|
264
|
+
return self._namespace
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ------------------------------------------------------------------
|
|
268
|
+
# Shared helpers
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _print_log_entry(entry: dict) -> None:
|
|
273
|
+
print(
|
|
274
|
+
entry.get("time_logged", ""),
|
|
275
|
+
entry.get("type", ""),
|
|
276
|
+
entry.get("config_name", ""),
|
|
277
|
+
entry.get("job", ""),
|
|
278
|
+
entry.get("message_id", ""),
|
|
279
|
+
entry.get("session_id", ""),
|
|
280
|
+
entry.get("source_class", ""),
|
|
281
|
+
entry.get("source_method", ""),
|
|
282
|
+
entry.get("text", ""),
|
|
283
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def upload_migration(
|
|
10
|
+
client,
|
|
11
|
+
path: str,
|
|
12
|
+
*,
|
|
13
|
+
strict_production_validation: bool = False,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Upload .py and .cls files from *path*'s folder to remote IRIS."""
|
|
16
|
+
folder = os.path.dirname(path)
|
|
17
|
+
|
|
18
|
+
package = "python"
|
|
19
|
+
remote_folder = ""
|
|
20
|
+
try:
|
|
21
|
+
spec = importlib.util.spec_from_file_location("_iop_migrate_settings", path)
|
|
22
|
+
mod = importlib.util.module_from_spec(spec) # type: ignore[arg-type]
|
|
23
|
+
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
|
24
|
+
remote_settings = getattr(mod, "REMOTE_SETTINGS", {})
|
|
25
|
+
package = remote_settings.get("package", package)
|
|
26
|
+
remote_folder = remote_settings.get("remote_folder", remote_folder)
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
body: list[dict] = []
|
|
31
|
+
settings_file = os.path.basename(path)
|
|
32
|
+
for dirpath, _, filenames in os.walk(folder):
|
|
33
|
+
for fname in sorted(filenames):
|
|
34
|
+
if not (fname.endswith(".py") or fname.endswith(".cls")):
|
|
35
|
+
continue
|
|
36
|
+
full = os.path.join(dirpath, fname)
|
|
37
|
+
rel = os.path.relpath(full, folder).replace(os.sep, "/")
|
|
38
|
+
with open(full, encoding="utf-8") as fh:
|
|
39
|
+
body.append({"name": rel, "data": fh.read()})
|
|
40
|
+
|
|
41
|
+
payload = {
|
|
42
|
+
"namespace": client._namespace,
|
|
43
|
+
"package": package,
|
|
44
|
+
"remote_folder": remote_folder,
|
|
45
|
+
"settings_file": settings_file,
|
|
46
|
+
"strict_production_validation": bool(strict_production_validation),
|
|
47
|
+
"body": body,
|
|
48
|
+
}
|
|
49
|
+
resp = requests.put(
|
|
50
|
+
f"{client._base}/migrate",
|
|
51
|
+
json=payload,
|
|
52
|
+
params={"namespace": client._namespace},
|
|
53
|
+
auth=client._auth,
|
|
54
|
+
verify=client._verify,
|
|
55
|
+
timeout=30,
|
|
56
|
+
)
|
|
57
|
+
client._raise_for_status(resp)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_remote_settings_from_file(settings_path: str) -> dict[str, Any] | None:
|
|
9
|
+
"""Load a literal ``REMOTE_SETTINGS`` dict without executing the file."""
|
|
10
|
+
try:
|
|
11
|
+
with open(settings_path, encoding="utf-8") as fh:
|
|
12
|
+
tree = ast.parse(fh.read(), filename=settings_path)
|
|
13
|
+
except (OSError, SyntaxError):
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
for node in tree.body:
|
|
18
|
+
value = None
|
|
19
|
+
targets = []
|
|
20
|
+
if isinstance(node, ast.Assign):
|
|
21
|
+
value = node.value
|
|
22
|
+
targets = node.targets
|
|
23
|
+
elif isinstance(node, ast.AnnAssign):
|
|
24
|
+
value = node.value
|
|
25
|
+
targets = [node.target]
|
|
26
|
+
if value is None:
|
|
27
|
+
continue
|
|
28
|
+
if any(
|
|
29
|
+
isinstance(target, ast.Name) and target.id == "REMOTE_SETTINGS"
|
|
30
|
+
for target in targets
|
|
31
|
+
):
|
|
32
|
+
remote = ast.literal_eval(value)
|
|
33
|
+
if isinstance(remote, dict) and "url" in remote:
|
|
34
|
+
return remote
|
|
35
|
+
return None
|
|
36
|
+
except (ValueError, TypeError):
|
|
37
|
+
return None
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_remote_settings(
|
|
42
|
+
explicit_settings_path: str | None = None,
|
|
43
|
+
fallback_settings_path: str | None = None,
|
|
44
|
+
) -> dict[str, Any] | None:
|
|
45
|
+
"""Detect remote settings from env vars or settings files."""
|
|
46
|
+
url = os.environ.get("IOP_URL")
|
|
47
|
+
if url:
|
|
48
|
+
verify_raw = os.environ.get("IOP_VERIFY_SSL", "1")
|
|
49
|
+
return {
|
|
50
|
+
"url": url,
|
|
51
|
+
"username": os.environ.get("IOP_USERNAME", ""),
|
|
52
|
+
"password": os.environ.get("IOP_PASSWORD", ""),
|
|
53
|
+
"namespace": os.environ.get("IOP_NAMESPACE", "USER"),
|
|
54
|
+
"verify_ssl": verify_raw.lower() not in ("0", "false"),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for path in filter(
|
|
58
|
+
None,
|
|
59
|
+
[
|
|
60
|
+
explicit_settings_path,
|
|
61
|
+
os.environ.get("IOP_SETTINGS"),
|
|
62
|
+
fallback_settings_path,
|
|
63
|
+
],
|
|
64
|
+
):
|
|
65
|
+
result = load_remote_settings_from_file(path)
|
|
66
|
+
if result:
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
return None
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.resources
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def setup_remote_classes(client, path: str | None = None) -> None:
|
|
10
|
+
"""Upload and compile IOP .cls files to remote IRIS via the Atelier REST API."""
|
|
11
|
+
paths_to_upload: list[str] = []
|
|
12
|
+
if path is None:
|
|
13
|
+
try:
|
|
14
|
+
paths_to_upload.append(str(importlib.resources.files("iop").joinpath("cls")))
|
|
15
|
+
except ModuleNotFoundError:
|
|
16
|
+
pass
|
|
17
|
+
else:
|
|
18
|
+
paths_to_upload.append(path)
|
|
19
|
+
|
|
20
|
+
atelier_base = f"{client._url}/api/atelier/v1"
|
|
21
|
+
doc_names: list[str] = []
|
|
22
|
+
|
|
23
|
+
for cls_root in paths_to_upload:
|
|
24
|
+
for dirpath, _, filenames in os.walk(cls_root):
|
|
25
|
+
for fname in sorted(filenames):
|
|
26
|
+
if not fname.endswith(".cls"):
|
|
27
|
+
continue
|
|
28
|
+
full_path = os.path.join(dirpath, fname)
|
|
29
|
+
doc_name = (
|
|
30
|
+
os.path.relpath(full_path, cls_root)
|
|
31
|
+
.replace(os.sep, ".")
|
|
32
|
+
.replace("/", ".")
|
|
33
|
+
)
|
|
34
|
+
with open(full_path, encoding="utf-8") as fh:
|
|
35
|
+
content = fh.read().splitlines()
|
|
36
|
+
resp = requests.put(
|
|
37
|
+
f"{atelier_base}/{client._namespace}/doc/{doc_name}",
|
|
38
|
+
json={"enc": False, "content": content},
|
|
39
|
+
params={"ignoreConflict": "1"},
|
|
40
|
+
auth=client._auth,
|
|
41
|
+
verify=client._verify,
|
|
42
|
+
timeout=30,
|
|
43
|
+
)
|
|
44
|
+
client._raise_for_status(resp)
|
|
45
|
+
doc_names.append(doc_name)
|
|
46
|
+
print(f"Uploaded: {doc_name}")
|
|
47
|
+
|
|
48
|
+
if not doc_names:
|
|
49
|
+
raise RuntimeError("No .cls files found to upload.")
|
|
50
|
+
|
|
51
|
+
resp = requests.post(
|
|
52
|
+
f"{atelier_base}/{client._namespace}/action/compile",
|
|
53
|
+
json=doc_names,
|
|
54
|
+
params={"flags": "cuk"},
|
|
55
|
+
auth=client._auth,
|
|
56
|
+
verify=client._verify,
|
|
57
|
+
timeout=120,
|
|
58
|
+
)
|
|
59
|
+
client._raise_for_status(resp)
|
|
60
|
+
result = resp.json()
|
|
61
|
+
for line in result.get("console", []):
|
|
62
|
+
if line:
|
|
63
|
+
print(line)
|
|
64
|
+
errors = result.get("status", {}).get("errors", [])
|
|
65
|
+
if errors:
|
|
66
|
+
raise RuntimeError(f"Compilation errors: {errors}")
|
|
67
|
+
print(
|
|
68
|
+
"\n.cls files uploaded and compiled successfully."
|
|
69
|
+
"\nNext step: ensure the 'iop' Python package is installed on the IRIS server:"
|
|
70
|
+
"\n python3 -m pip install iris-pex-embedded-python"
|
|
71
|
+
"\nThis is required for full IOP Support; without it, only the migrate() and export_production() methods will work remotely."
|
|
72
|
+
)
|
{iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iris_pex_embedded_python
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: Iris Interoperability based on Embedded Python
|
|
5
5
|
Author-email: grongier <guillaume.rongier@intersystems.com>
|
|
6
6
|
License: MIT License
|
|
@@ -37,6 +37,8 @@ Classifier: Operating System :: OS Independent
|
|
|
37
37
|
Classifier: Programming Language :: Python :: 3.10
|
|
38
38
|
Classifier: Programming Language :: Python :: 3.11
|
|
39
39
|
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
40
42
|
Classifier: Topic :: Utilities
|
|
41
43
|
Requires-Python: >=3.10
|
|
42
44
|
Description-Content-Type: text/markdown
|
|
@@ -61,6 +63,9 @@ Dynamic: license-file
|
|
|
61
63
|
Welcome to the **Interoperability On Python (IoP)** proof of concept! This project demonstrates how the **IRIS Interoperability Framework** can be utilized with a **Python-first approach**.
|
|
62
64
|
|
|
63
65
|
Documentation can be found [here](https://grongierisc.github.io/interoperability-embedded-python/).
|
|
66
|
+
For prompt-driven workflows, see [AI-assisted coding with IoP](https://grongierisc.github.io/interoperability-embedded-python/ai-coding/).
|
|
67
|
+
For task-oriented examples, see the [IoP cookbooks](https://grongierisc.github.io/interoperability-embedded-python/cookbooks/).
|
|
68
|
+
For application repositories, start from the [reusable AGENTS.md template](https://grongierisc.github.io/interoperability-embedded-python/agents-template/).
|
|
64
69
|
|
|
65
70
|
## Example
|
|
66
71
|
|
|
@@ -84,6 +89,10 @@ pip install iris-pex-embedded-python
|
|
|
84
89
|
|
|
85
90
|
## Getting Started
|
|
86
91
|
|
|
87
|
-
If you're new to this project, begin by reading the [installation guide](https://grongierisc.github.io/interoperability-embedded-python/getting-started/installation). Then, follow the [first steps](https://grongierisc.github.io/interoperability-embedded-python/getting-started/first-steps) to create your first
|
|
92
|
+
If you're new to this project, begin by reading the [installation guide](https://grongierisc.github.io/interoperability-embedded-python/getting-started/installation). Then, follow the [first steps](https://grongierisc.github.io/interoperability-embedded-python/getting-started/first-steps) to create your first Python-authored production.
|
|
93
|
+
|
|
94
|
+
If you are using an AI coding assistant, start with [AI-assisted coding with IoP](https://grongierisc.github.io/interoperability-embedded-python/ai-coding/).
|
|
95
|
+
For concrete workflows, use the [IoP cookbooks](https://grongierisc.github.io/interoperability-embedded-python/cookbooks/).
|
|
96
|
+
For healthcare productions, also see [Healthcare AI-assisted coding](https://grongierisc.github.io/interoperability-embedded-python/healthcare-ai-coding/).
|
|
88
97
|
|
|
89
98
|
Happy coding!
|