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
iop/_remote.py
DELETED
|
@@ -1,473 +0,0 @@
|
|
|
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 os
|
|
16
|
-
import signal
|
|
17
|
-
import time
|
|
18
|
-
from typing import Any, Dict, List, Optional
|
|
19
|
-
|
|
20
|
-
import requests
|
|
21
|
-
import urllib3
|
|
22
|
-
|
|
23
|
-
from ._director_protocol import DirectorProtocol as _DirectorProtocol # noqa: F401 --- IGNORE ---
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class _RemoteDirector(_DirectorProtocol):
|
|
27
|
-
"""Implements DirectorProtocol over the IOP REST API."""
|
|
28
|
-
|
|
29
|
-
def __init__(self, remote_settings: Dict[str, Any]) -> None:
|
|
30
|
-
self._url = remote_settings["url"].rstrip("/")
|
|
31
|
-
self._base = self._url + "/api/iop"
|
|
32
|
-
self._auth = (
|
|
33
|
-
remote_settings.get("username", ""),
|
|
34
|
-
remote_settings.get("password", ""),
|
|
35
|
-
)
|
|
36
|
-
self._namespace: str = remote_settings.get("namespace", "USER")
|
|
37
|
-
self._verify: bool = remote_settings.get("verify_ssl", True)
|
|
38
|
-
if not self._verify:
|
|
39
|
-
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
40
|
-
|
|
41
|
-
# ------------------------------------------------------------------
|
|
42
|
-
# Internal helpers
|
|
43
|
-
# ------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
@staticmethod
|
|
46
|
-
def _raise_for_status(resp: requests.Response) -> None:
|
|
47
|
-
"""Like resp.raise_for_status() but includes the response body error message."""
|
|
48
|
-
if not resp.ok:
|
|
49
|
-
try:
|
|
50
|
-
body = resp.json()
|
|
51
|
-
error = body.get("error") or body.get("message") or resp.text
|
|
52
|
-
except Exception:
|
|
53
|
-
error = resp.text or resp.reason
|
|
54
|
-
raise requests.exceptions.HTTPError(
|
|
55
|
-
f"{resp.status_code} {resp.reason}: {error}",
|
|
56
|
-
response=resp,
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
def _get(self, path: str, params: Optional[dict] = None) -> Any:
|
|
60
|
-
p = {"namespace": self._namespace, **(params or {})}
|
|
61
|
-
resp = requests.get(
|
|
62
|
-
f"{self._base}{path}",
|
|
63
|
-
params=p,
|
|
64
|
-
auth=self._auth,
|
|
65
|
-
verify=self._verify,
|
|
66
|
-
timeout=30,
|
|
67
|
-
)
|
|
68
|
-
self._raise_for_status(resp)
|
|
69
|
-
return resp.json()
|
|
70
|
-
|
|
71
|
-
def _post(self, path: str, body: Optional[dict] = None) -> Any:
|
|
72
|
-
resp = requests.post(
|
|
73
|
-
f"{self._base}{path}",
|
|
74
|
-
json=(body or {}),
|
|
75
|
-
params={"namespace": self._namespace},
|
|
76
|
-
auth=self._auth,
|
|
77
|
-
verify=self._verify,
|
|
78
|
-
timeout=30,
|
|
79
|
-
)
|
|
80
|
-
self._raise_for_status(resp)
|
|
81
|
-
return resp.json()
|
|
82
|
-
|
|
83
|
-
def _put(self, path: str, body: Optional[dict] = None) -> Any:
|
|
84
|
-
resp = requests.put(
|
|
85
|
-
f"{self._base}{path}",
|
|
86
|
-
json=(body or {}),
|
|
87
|
-
params={"namespace": self._namespace},
|
|
88
|
-
auth=self._auth,
|
|
89
|
-
verify=self._verify,
|
|
90
|
-
timeout=30,
|
|
91
|
-
)
|
|
92
|
-
self._raise_for_status(resp)
|
|
93
|
-
return resp.json()
|
|
94
|
-
|
|
95
|
-
def _check_error(self, data: Any) -> Any:
|
|
96
|
-
if isinstance(data, dict) and "error" in data:
|
|
97
|
-
raise RuntimeError(data["error"])
|
|
98
|
-
return data
|
|
99
|
-
|
|
100
|
-
# ------------------------------------------------------------------
|
|
101
|
-
# Production lifecycle
|
|
102
|
-
# ------------------------------------------------------------------
|
|
103
|
-
|
|
104
|
-
def get_default_production(self) -> str:
|
|
105
|
-
data = self._check_error(self._get("/default"))
|
|
106
|
-
return data.get("production") or "Not defined"
|
|
107
|
-
|
|
108
|
-
def set_default_production(self, production_name: str = "") -> None:
|
|
109
|
-
self._check_error(self._put("/default", {"production": production_name}))
|
|
110
|
-
|
|
111
|
-
def list_productions(self) -> dict:
|
|
112
|
-
return self._check_error(self._get("/list"))
|
|
113
|
-
|
|
114
|
-
def status_production(self) -> dict:
|
|
115
|
-
data = self._check_error(self._get("/status"))
|
|
116
|
-
if not data.get("production"):
|
|
117
|
-
data["production"] = self.get_default_production()
|
|
118
|
-
return data
|
|
119
|
-
|
|
120
|
-
def start_production(self, production_name: Optional[str] = None) -> None:
|
|
121
|
-
body: dict = {}
|
|
122
|
-
if production_name:
|
|
123
|
-
body["production"] = production_name
|
|
124
|
-
self._check_error(self._post("/start", body))
|
|
125
|
-
|
|
126
|
-
def start_production_with_log(self, production_name: Optional[str] = None) -> None:
|
|
127
|
-
"""Start remotely then stream the log until Ctrl-C (which also stops)."""
|
|
128
|
-
self.start_production(production_name)
|
|
129
|
-
prod = production_name or self.get_default_production()
|
|
130
|
-
print(f"Production '{prod}' started. Streaming log — Ctrl-C to stop.")
|
|
131
|
-
running = True
|
|
132
|
-
|
|
133
|
-
def _sigint(sig, frame): # pragma: no cover
|
|
134
|
-
nonlocal running
|
|
135
|
-
running = False
|
|
136
|
-
|
|
137
|
-
signal.signal(signal.SIGINT, _sigint)
|
|
138
|
-
|
|
139
|
-
last_id = 0
|
|
140
|
-
for entry in self._get_log_entries(top=10):
|
|
141
|
-
_print_log_entry(entry)
|
|
142
|
-
last_id = max(last_id, entry.get("id", 0))
|
|
143
|
-
|
|
144
|
-
while running:
|
|
145
|
-
time.sleep(1)
|
|
146
|
-
entries = self._get_log_entries(since_id=last_id)
|
|
147
|
-
for entry in entries:
|
|
148
|
-
_print_log_entry(entry)
|
|
149
|
-
last_id = max(last_id, entry.get("id", 0))
|
|
150
|
-
|
|
151
|
-
self.stop_production()
|
|
152
|
-
|
|
153
|
-
def stop_production(self) -> None:
|
|
154
|
-
self._check_error(self._post("/stop"))
|
|
155
|
-
|
|
156
|
-
def shutdown_production(self) -> None:
|
|
157
|
-
self._check_error(self._post("/kill"))
|
|
158
|
-
|
|
159
|
-
def restart_production(self) -> None:
|
|
160
|
-
self._check_error(self._post("/restart"))
|
|
161
|
-
|
|
162
|
-
def update_production(self) -> None:
|
|
163
|
-
self._check_error(self._post("/update"))
|
|
164
|
-
|
|
165
|
-
# ------------------------------------------------------------------
|
|
166
|
-
# Logging
|
|
167
|
-
# ------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
def _get_log_entries(
|
|
170
|
-
self,
|
|
171
|
-
top: int = 10,
|
|
172
|
-
since_id: Optional[int] = None,
|
|
173
|
-
) -> List[dict]:
|
|
174
|
-
params: dict = {}
|
|
175
|
-
if since_id is not None:
|
|
176
|
-
params["since_id"] = since_id
|
|
177
|
-
else:
|
|
178
|
-
params["top"] = top
|
|
179
|
-
data = self._check_error(self._get("/log", params))
|
|
180
|
-
return data if isinstance(data, list) else []
|
|
181
|
-
|
|
182
|
-
def log_production_top(self, top: int = 10) -> None:
|
|
183
|
-
entries = self._get_log_entries(top=top)
|
|
184
|
-
for entry in reversed(entries):
|
|
185
|
-
_print_log_entry(entry)
|
|
186
|
-
|
|
187
|
-
def log_production(self) -> None:
|
|
188
|
-
"""Stream log continuously until Ctrl-C."""
|
|
189
|
-
running = True
|
|
190
|
-
|
|
191
|
-
def _sigint(sig, frame): # pragma: no cover
|
|
192
|
-
nonlocal running
|
|
193
|
-
running = False
|
|
194
|
-
|
|
195
|
-
signal.signal(signal.SIGINT, _sigint)
|
|
196
|
-
|
|
197
|
-
last_id = 0
|
|
198
|
-
for entry in self._get_log_entries(top=10):
|
|
199
|
-
_print_log_entry(entry)
|
|
200
|
-
last_id = max(last_id, entry.get("id", 0))
|
|
201
|
-
|
|
202
|
-
while running:
|
|
203
|
-
time.sleep(1)
|
|
204
|
-
entries = self._get_log_entries(since_id=last_id)
|
|
205
|
-
for entry in entries:
|
|
206
|
-
_print_log_entry(entry)
|
|
207
|
-
last_id = max(last_id, entry.get("id", 0))
|
|
208
|
-
|
|
209
|
-
# ------------------------------------------------------------------
|
|
210
|
-
# Test
|
|
211
|
-
# ------------------------------------------------------------------
|
|
212
|
-
|
|
213
|
-
def test_component(
|
|
214
|
-
self,
|
|
215
|
-
target: Optional[str],
|
|
216
|
-
message=None, # ignored remotely — not serialisable over HTTP
|
|
217
|
-
classname: Optional[str] = None,
|
|
218
|
-
body: "str | dict | None" = None,
|
|
219
|
-
restart: bool = True,
|
|
220
|
-
) -> dict:
|
|
221
|
-
"""Returns a dict: {"classname": "...", "body": "...", "truncated": false}.
|
|
222
|
-
|
|
223
|
-
If *restart* is True the target component is stopped and restarted on
|
|
224
|
-
the server before the test message is dispatched.
|
|
225
|
-
"""
|
|
226
|
-
payload: dict = {"target": target or ""}
|
|
227
|
-
if classname:
|
|
228
|
-
payload["classname"] = classname
|
|
229
|
-
if body is not None:
|
|
230
|
-
payload["body"] = body
|
|
231
|
-
if restart:
|
|
232
|
-
payload["restart"] = True
|
|
233
|
-
try:
|
|
234
|
-
return self._check_error(self._post("/test", payload))
|
|
235
|
-
except requests.exceptions.HTTPError as exc:
|
|
236
|
-
try:
|
|
237
|
-
err_msg = exc.response.json().get("error", str(exc))
|
|
238
|
-
except Exception:
|
|
239
|
-
err_msg = str(exc)
|
|
240
|
-
raise RuntimeError(err_msg) from exc
|
|
241
|
-
|
|
242
|
-
# ------------------------------------------------------------------
|
|
243
|
-
# Export
|
|
244
|
-
# ------------------------------------------------------------------
|
|
245
|
-
|
|
246
|
-
def export_production(self, production_name: str) -> dict:
|
|
247
|
-
return self._check_error(self._get("/export", {"production": production_name}))
|
|
248
|
-
|
|
249
|
-
# ------------------------------------------------------------------
|
|
250
|
-
# Migrate
|
|
251
|
-
# ------------------------------------------------------------------
|
|
252
|
-
|
|
253
|
-
def migrate(self, path: str) -> None:
|
|
254
|
-
"""Upload .py and .cls files from *path*'s folder to remote IRIS via the IOP migrate API.
|
|
255
|
-
|
|
256
|
-
*path* must be an absolute path to a ``settings.py`` file whose directory
|
|
257
|
-
(and sub-directories) will be walked for ``.py`` / ``.cls`` files.
|
|
258
|
-
``REMOTE_SETTINGS`` fields (package, namespace, remote_folder) are read
|
|
259
|
-
from that same settings file when present; otherwise the director's own
|
|
260
|
-
namespace / defaults are used.
|
|
261
|
-
"""
|
|
262
|
-
import importlib.util
|
|
263
|
-
|
|
264
|
-
folder = os.path.dirname(path)
|
|
265
|
-
|
|
266
|
-
# Try to read optional keys from the settings file
|
|
267
|
-
package = "python"
|
|
268
|
-
remote_folder = ""
|
|
269
|
-
try:
|
|
270
|
-
spec = importlib.util.spec_from_file_location("_iop_migrate_settings", path)
|
|
271
|
-
mod = importlib.util.module_from_spec(spec) # type: ignore[arg-type]
|
|
272
|
-
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
|
273
|
-
rs = getattr(mod, "REMOTE_SETTINGS", {})
|
|
274
|
-
package = rs.get("package", package)
|
|
275
|
-
remote_folder = rs.get("remote_folder", remote_folder)
|
|
276
|
-
except Exception:
|
|
277
|
-
pass
|
|
278
|
-
|
|
279
|
-
body: List[dict] = []
|
|
280
|
-
for dirpath, _, filenames in os.walk(folder):
|
|
281
|
-
for fname in sorted(filenames):
|
|
282
|
-
if not (fname.endswith(".py") or fname.endswith(".cls")):
|
|
283
|
-
continue
|
|
284
|
-
full = os.path.join(dirpath, fname)
|
|
285
|
-
rel = os.path.relpath(full, folder).replace(os.sep, "/")
|
|
286
|
-
with open(full, encoding="utf-8") as fh:
|
|
287
|
-
body.append({"name": rel, "data": fh.read()})
|
|
288
|
-
|
|
289
|
-
payload = {
|
|
290
|
-
"namespace": self._namespace,
|
|
291
|
-
"package": package,
|
|
292
|
-
"remote_folder": remote_folder,
|
|
293
|
-
"body": body,
|
|
294
|
-
}
|
|
295
|
-
resp = requests.put(
|
|
296
|
-
f"{self._base}/migrate",
|
|
297
|
-
json=payload,
|
|
298
|
-
params={"namespace": self._namespace},
|
|
299
|
-
auth=self._auth,
|
|
300
|
-
verify=self._verify,
|
|
301
|
-
timeout=30,
|
|
302
|
-
)
|
|
303
|
-
self._raise_for_status(resp)
|
|
304
|
-
# Server returns $$$OK (the integer 1) on success — not meaningful to print
|
|
305
|
-
|
|
306
|
-
# ------------------------------------------------------------------
|
|
307
|
-
# Init / setup — uploads .cls files via the Atelier API
|
|
308
|
-
# ------------------------------------------------------------------
|
|
309
|
-
|
|
310
|
-
def setup(self, path: Optional[str] = None) -> None:
|
|
311
|
-
"""Upload and compile IOP .cls files to remote IRIS via the Atelier REST API.
|
|
312
|
-
|
|
313
|
-
When *path* is ``None`` the bundled ``iop/cls/`` directory (and the
|
|
314
|
-
optional ``grongier/cls/`` directory for retrocompatibility) is used.
|
|
315
|
-
Any explicit *path* must point to a directory containing ``.cls`` files.
|
|
316
|
-
"""
|
|
317
|
-
import importlib.resources
|
|
318
|
-
|
|
319
|
-
paths_to_upload: List[str] = []
|
|
320
|
-
if path is None:
|
|
321
|
-
try:
|
|
322
|
-
paths_to_upload.append(
|
|
323
|
-
str(importlib.resources.files("iop").joinpath("cls"))
|
|
324
|
-
)
|
|
325
|
-
except ModuleNotFoundError:
|
|
326
|
-
pass
|
|
327
|
-
try: # retrocompatibility with the grongier.pex package
|
|
328
|
-
paths_to_upload.append(
|
|
329
|
-
str(importlib.resources.files("grongier").joinpath("cls"))
|
|
330
|
-
)
|
|
331
|
-
except ModuleNotFoundError:
|
|
332
|
-
pass
|
|
333
|
-
else:
|
|
334
|
-
paths_to_upload.append(path)
|
|
335
|
-
|
|
336
|
-
atelier_base = f"{self._url}/api/atelier/v1"
|
|
337
|
-
doc_names: List[str] = []
|
|
338
|
-
|
|
339
|
-
for cls_root in paths_to_upload:
|
|
340
|
-
for dirpath, _, filenames in os.walk(cls_root):
|
|
341
|
-
for fname in sorted(filenames):
|
|
342
|
-
if not fname.endswith(".cls"):
|
|
343
|
-
continue
|
|
344
|
-
full_path = os.path.join(dirpath, fname)
|
|
345
|
-
doc_name = (
|
|
346
|
-
os.path.relpath(full_path, cls_root)
|
|
347
|
-
.replace(os.sep, ".")
|
|
348
|
-
.replace("/", ".")
|
|
349
|
-
)
|
|
350
|
-
with open(full_path, encoding="utf-8") as fh:
|
|
351
|
-
content = fh.read().splitlines()
|
|
352
|
-
resp = requests.put(
|
|
353
|
-
f"{atelier_base}/{self._namespace}/doc/{doc_name}",
|
|
354
|
-
json={"enc": False, "content": content},
|
|
355
|
-
params={"ignoreConflict": "1"},
|
|
356
|
-
auth=self._auth,
|
|
357
|
-
verify=self._verify,
|
|
358
|
-
timeout=30,
|
|
359
|
-
)
|
|
360
|
-
self._raise_for_status(resp)
|
|
361
|
-
doc_names.append(doc_name)
|
|
362
|
-
print(f"Uploaded: {doc_name}")
|
|
363
|
-
|
|
364
|
-
if not doc_names:
|
|
365
|
-
raise RuntimeError("No .cls files found to upload.")
|
|
366
|
-
|
|
367
|
-
# Compile all uploaded documents in one request
|
|
368
|
-
resp = requests.post(
|
|
369
|
-
f"{atelier_base}/{self._namespace}/action/compile",
|
|
370
|
-
json=doc_names,
|
|
371
|
-
params={"flags": "cuk"},
|
|
372
|
-
auth=self._auth,
|
|
373
|
-
verify=self._verify,
|
|
374
|
-
timeout=120,
|
|
375
|
-
)
|
|
376
|
-
self._raise_for_status(resp)
|
|
377
|
-
result = resp.json()
|
|
378
|
-
for line in result.get("console", []):
|
|
379
|
-
if line:
|
|
380
|
-
print(line)
|
|
381
|
-
errors = result.get("status", {}).get("errors", [])
|
|
382
|
-
if errors:
|
|
383
|
-
raise RuntimeError(f"Compilation errors: {errors}")
|
|
384
|
-
print(
|
|
385
|
-
"\n.cls files uploaded and compiled successfully."
|
|
386
|
-
"\nNext step: ensure the 'iop' Python package is installed on the IRIS server:"
|
|
387
|
-
"\n python3 -m pip install iris-pex-embedded-python"
|
|
388
|
-
"\nThis is required for full IOP Support; without it, only the migrate() and export_production() methods will work remotely."
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
# ------------------------------------------------------------------
|
|
392
|
-
# Metadata
|
|
393
|
-
# ------------------------------------------------------------------
|
|
394
|
-
|
|
395
|
-
@property
|
|
396
|
-
def namespace(self) -> str:
|
|
397
|
-
return self._namespace
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
# ------------------------------------------------------------------
|
|
401
|
-
# Shared helpers
|
|
402
|
-
# ------------------------------------------------------------------
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
def _print_log_entry(entry: dict) -> None:
|
|
406
|
-
print(
|
|
407
|
-
entry.get("time_logged", ""),
|
|
408
|
-
entry.get("type", ""),
|
|
409
|
-
entry.get("config_name", ""),
|
|
410
|
-
entry.get("job", ""),
|
|
411
|
-
entry.get("message_id", ""),
|
|
412
|
-
entry.get("session_id", ""),
|
|
413
|
-
entry.get("source_class", ""),
|
|
414
|
-
entry.get("source_method", ""),
|
|
415
|
-
entry.get("text", ""),
|
|
416
|
-
)
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
def _load_remote_settings_from_file(settings_path: str) -> Optional[Dict[str, Any]]:
|
|
420
|
-
"""Load a ``REMOTE_SETTINGS`` dict from an arbitrary settings.py file."""
|
|
421
|
-
try:
|
|
422
|
-
import importlib.util
|
|
423
|
-
|
|
424
|
-
spec = importlib.util.spec_from_file_location(
|
|
425
|
-
"_iop_settings_remote", settings_path
|
|
426
|
-
)
|
|
427
|
-
mod = importlib.util.module_from_spec(spec) # type: ignore[arg-type]
|
|
428
|
-
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
|
429
|
-
remote = getattr(mod, "REMOTE_SETTINGS", None)
|
|
430
|
-
if isinstance(remote, dict) and "url" in remote:
|
|
431
|
-
return remote
|
|
432
|
-
except Exception:
|
|
433
|
-
pass
|
|
434
|
-
return None
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
def get_remote_settings(
|
|
438
|
-
explicit_settings_path: Optional[str] = None,
|
|
439
|
-
fallback_settings_path: Optional[str] = None,
|
|
440
|
-
) -> Optional[Dict[str, Any]]:
|
|
441
|
-
"""Detect remote settings from the environment or an explicit file.
|
|
442
|
-
|
|
443
|
-
Priority:
|
|
444
|
-
1. ``IOP_URL`` env var (direct inline configuration).
|
|
445
|
-
2. ``explicit_settings_path`` — the file supplied via ``--remote-settings``.
|
|
446
|
-
3. ``IOP_SETTINGS`` env var pointing to a settings.py with ``REMOTE_SETTINGS``.
|
|
447
|
-
4. ``fallback_settings_path`` — e.g. the file passed via ``-m settings.py``;
|
|
448
|
-
its ``REMOTE_SETTINGS`` dict is used when present.
|
|
449
|
-
"""
|
|
450
|
-
url = os.environ.get("IOP_URL")
|
|
451
|
-
if url:
|
|
452
|
-
verify_raw = os.environ.get("IOP_VERIFY_SSL", "1")
|
|
453
|
-
return {
|
|
454
|
-
"url": url,
|
|
455
|
-
"username": os.environ.get("IOP_USERNAME", ""),
|
|
456
|
-
"password": os.environ.get("IOP_PASSWORD", ""),
|
|
457
|
-
"namespace": os.environ.get("IOP_NAMESPACE", "USER"),
|
|
458
|
-
"verify_ssl": verify_raw.lower() not in ("0", "false"),
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
for path in filter(
|
|
462
|
-
None,
|
|
463
|
-
[
|
|
464
|
-
explicit_settings_path,
|
|
465
|
-
os.environ.get("IOP_SETTINGS"),
|
|
466
|
-
fallback_settings_path,
|
|
467
|
-
],
|
|
468
|
-
):
|
|
469
|
-
result = _load_remote_settings_from_file(path)
|
|
470
|
-
if result:
|
|
471
|
-
return result
|
|
472
|
-
|
|
473
|
-
return None
|