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.
Files changed (119) hide show
  1. iop/__init__.py +276 -18
  2. iop/__main__.py +3 -3
  3. iop/cli/__init__.py +1 -0
  4. iop/cli/formatting.py +45 -0
  5. iop/{_cli.py → cli/main.py} +238 -216
  6. iop/cli/parser.py +165 -0
  7. iop/cli/types.py +73 -0
  8. iop/cls/IOP/Common.cls +11 -2
  9. iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
  10. iop/cls/IOP/Service/Remote/Rest/v1.cls +152 -5
  11. iop/cls/IOP/Utils.cls +814 -7
  12. iop/cls/IOP/Wrapper.cls +1 -1
  13. iop/components/__init__.py +16 -0
  14. iop/{_async_request.py → components/async_request.py} +7 -7
  15. iop/components/business_host.py +363 -0
  16. iop/components/business_operation.py +84 -0
  17. iop/{_business_process.py → components/business_process.py} +145 -125
  18. iop/components/business_service.py +106 -0
  19. iop/{_common.py → components/common.py} +160 -164
  20. iop/{_debugpy.py → components/debugpy.py} +11 -11
  21. iop/{_generator_request.py → components/generator_request.py} +4 -4
  22. iop/{_inbound_adapter.py → components/inbound_adapter.py} +10 -18
  23. iop/{_log_manager.py → components/log_manager.py} +1 -1
  24. iop/{_outbound_adapter.py → components/outbound_adapter.py} +1 -1
  25. iop/components/polling_business_service.py +48 -0
  26. iop/{_private_session_duplex.py → components/private_session_duplex.py} +7 -9
  27. iop/{_private_session_process.py → components/private_session_process.py} +2 -2
  28. iop/{_settings.py → components/settings.py} +23 -3
  29. iop/messages/__init__.py +7 -0
  30. iop/messages/base.py +61 -0
  31. iop/{_decorators.py → messages/decorators.py} +8 -2
  32. iop/messages/dispatch.py +351 -0
  33. iop/{_persistent_message.py → messages/persistent.py} +24 -19
  34. iop/{_serialization.py → messages/serialization.py} +34 -21
  35. iop/{_message_validator.py → messages/validation.py} +15 -13
  36. iop/migration/__init__.py +1 -0
  37. iop/migration/io.py +61 -0
  38. iop/migration/manifest.py +534 -0
  39. iop/migration/plans.py +80 -0
  40. iop/migration/utils.py +1126 -0
  41. iop/production/__init__.py +55 -0
  42. iop/production/actions.py +525 -0
  43. iop/production/common.py +103 -0
  44. iop/production/component.py +234 -0
  45. iop/production/declarations.py +190 -0
  46. iop/production/declarative.py +214 -0
  47. iop/production/diff.py +327 -0
  48. iop/production/import_.py +296 -0
  49. iop/production/inspection.py +92 -0
  50. iop/production/model.py +1143 -0
  51. iop/production/planning.py +678 -0
  52. iop/production/reconstruction.py +341 -0
  53. iop/production/rendering.py +610 -0
  54. iop/production/runtime.py +91 -0
  55. iop/production/source_inference.py +729 -0
  56. iop/production/types.py +546 -0
  57. iop/production/validation.py +452 -0
  58. iop/runtime/__init__.py +1 -0
  59. iop/runtime/director.py +415 -0
  60. iop/runtime/environment.py +59 -0
  61. iop/{_iris.py → runtime/iris.py} +2 -2
  62. iop/runtime/local.py +145 -0
  63. iop/{_director_protocol.py → runtime/protocol.py} +25 -8
  64. iop/runtime/remote/__init__.py +12 -0
  65. iop/runtime/remote/client.py +90 -0
  66. iop/runtime/remote/director.py +283 -0
  67. iop/runtime/remote/migration.py +57 -0
  68. iop/runtime/remote/settings.py +69 -0
  69. iop/runtime/remote/setup.py +72 -0
  70. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA +11 -2
  71. iris_pex_embedded_python-4.0.0.dist-info/RECORD +99 -0
  72. iris_pex_embedded_python-4.0.0.dist-info/entry_points.txt +2 -0
  73. iris_pex_embedded_python-4.0.0.dist-info/top_level.txt +1 -0
  74. grongier/__init__.py +0 -0
  75. grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -8
  76. grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -13
  77. grongier/cls/Grongier/PEX/BusinessService.cls +0 -8
  78. grongier/cls/Grongier/PEX/Common.cls +0 -10
  79. grongier/cls/Grongier/PEX/Director.cls +0 -10
  80. grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -4
  81. grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -13
  82. grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -4
  83. grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -8
  84. grongier/cls/Grongier/PEX/Message.cls +0 -13
  85. grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -8
  86. grongier/cls/Grongier/PEX/PickleMessage.cls +0 -13
  87. grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -8
  88. grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -14
  89. grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -14
  90. grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -14
  91. grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -14
  92. grongier/cls/Grongier/PEX/Test.cls +0 -10
  93. grongier/cls/Grongier/PEX/Utils.cls +0 -10
  94. grongier/cls/Grongier/Service/WSGI.cls +0 -4
  95. grongier/pex/__init__.py +0 -75
  96. grongier/pex/__main__.py +0 -4
  97. grongier/pex/_business_host.py +0 -1
  98. grongier/pex/_cli.py +0 -4
  99. grongier/pex/_common.py +0 -1
  100. grongier/pex/_director.py +0 -1
  101. grongier/pex/_utils.py +0 -1
  102. grongier/pex/wsgi/handlers.py +0 -104
  103. iop/_business_host.py +0 -322
  104. iop/_business_operation.py +0 -77
  105. iop/_business_service.py +0 -67
  106. iop/_director.py +0 -333
  107. iop/_dispatch.py +0 -194
  108. iop/_local.py +0 -106
  109. iop/_message.py +0 -45
  110. iop/_polling_business_service.py +0 -6
  111. iop/_remote.py +0 -473
  112. iop/_utils.py +0 -912
  113. iop/cls/IOP/Service/WSGI.cls +0 -310
  114. iop/wsgi/handlers.py +0 -112
  115. iris_pex_embedded_python-3.7.2b2.dist-info/RECORD +0 -96
  116. iris_pex_embedded_python-3.7.2b2.dist-info/entry_points.txt +0 -2
  117. iris_pex_embedded_python-3.7.2b2.dist-info/top_level.txt +0 -2
  118. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/WHEEL +0 -0
  119. {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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris_pex_embedded_python
3
- Version: 3.7.2b2
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 Business Operation.
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!