iris-pex-embedded-python 4.0.2b1__py3-none-any.whl → 4.1.0b1__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/cli/formatting.py +2 -2
- iop/cli/main.py +2 -0
- iop/components/async_request.py +36 -25
- iop/components/debugpy.py +8 -2
- iop/messages/persistent.py +146 -44
- iop/messages/serialization.py +3 -0
- iop/migration/_conversion.py +58 -0
- iop/migration/_production_io.py +82 -0
- iop/migration/_registration.py +78 -0
- iop/migration/_settings.py +47 -0
- iop/migration/io.py +6 -60
- iop/migration/manifest.py +2 -2
- iop/migration/utils.py +40 -174
- iop/production/_authoring_methods.py +182 -0
- iop/production/_runtime_methods.py +159 -0
- iop/production/actions.py +1 -0
- iop/production/model.py +12 -313
- iop/production/planning.py +4 -4
- iop/runtime/director.py +2 -1
- iop/runtime/remote/director.py +6 -1
- {iris_pex_embedded_python-4.0.2b1.dist-info → iris_pex_embedded_python-4.1.0b1.dist-info}/METADATA +41 -5
- {iris_pex_embedded_python-4.0.2b1.dist-info → iris_pex_embedded_python-4.1.0b1.dist-info}/RECORD +26 -20
- {iris_pex_embedded_python-4.0.2b1.dist-info → iris_pex_embedded_python-4.1.0b1.dist-info}/WHEEL +1 -1
- {iris_pex_embedded_python-4.0.2b1.dist-info → iris_pex_embedded_python-4.1.0b1.dist-info}/entry_points.txt +0 -0
- {iris_pex_embedded_python-4.0.2b1.dist-info → iris_pex_embedded_python-4.1.0b1.dist-info}/licenses/LICENSE +0 -0
- {iris_pex_embedded_python-4.0.2b1.dist-info → iris_pex_embedded_python-4.1.0b1.dist-info}/top_level.txt +0 -0
iop/cli/formatting.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
import json
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any, cast
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def format_test_response(response: Any) -> str:
|
|
@@ -41,5 +41,5 @@ def format_test_response(response: Any) -> str:
|
|
|
41
41
|
return response
|
|
42
42
|
|
|
43
43
|
if dataclasses.is_dataclass(response):
|
|
44
|
-
return json.dumps(dataclasses.asdict(response), indent=4)
|
|
44
|
+
return json.dumps(dataclasses.asdict(cast(Any, response)), indent=4)
|
|
45
45
|
return str(response)
|
iop/cli/main.py
CHANGED
|
@@ -241,6 +241,8 @@ class Command:
|
|
|
241
241
|
if self.args.export == "not_set"
|
|
242
242
|
else self.args.export
|
|
243
243
|
)
|
|
244
|
+
if not export_name:
|
|
245
|
+
raise ValueError("No production name was provided or configured.")
|
|
244
246
|
export_format = self.args.export_format or "json"
|
|
245
247
|
if export_format == "json":
|
|
246
248
|
print(json.dumps(self.director.export_production(export_name), indent=4))
|
iop/components/async_request.py
CHANGED
|
@@ -30,37 +30,48 @@ class AsyncRequest(asyncio.Future):
|
|
|
30
30
|
if host is None:
|
|
31
31
|
raise ValueError("host parameter cannot be None")
|
|
32
32
|
self._iris_handle = host.iris_handle
|
|
33
|
-
|
|
33
|
+
self._message_header_id = 0
|
|
34
|
+
self._queue_name = ""
|
|
35
|
+
self._end_time = 0
|
|
36
|
+
self._response = None
|
|
37
|
+
self._done = False
|
|
38
|
+
self._send_task = asyncio.create_task(self.send())
|
|
34
39
|
|
|
35
40
|
async def send(self) -> None:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
try:
|
|
42
|
+
iris = _iris.get_iris()
|
|
43
|
+
message_header_id = iris.ref()
|
|
44
|
+
queue_name = iris.ref()
|
|
45
|
+
end_time = iris.ref()
|
|
46
|
+
request = dispatch_serializer(self.request)
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
48
|
+
self._iris_handle.dispatchSendRequestAsyncNG(
|
|
49
|
+
self.target,
|
|
50
|
+
request,
|
|
51
|
+
self.timeout,
|
|
52
|
+
self.description,
|
|
53
|
+
message_header_id,
|
|
54
|
+
queue_name,
|
|
55
|
+
end_time,
|
|
56
|
+
)
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
self._end_time = end_time.value
|
|
58
|
+
self._message_header_id = message_header_id.value
|
|
59
|
+
self._queue_name = queue_name.value
|
|
60
|
+
self._end_time = end_time.value
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
while not self._done:
|
|
63
|
+
await asyncio.sleep(0.1)
|
|
64
|
+
self.is_done()
|
|
62
65
|
|
|
63
|
-
|
|
66
|
+
if not self.done():
|
|
67
|
+
self.set_result(self._response)
|
|
68
|
+
except asyncio.CancelledError:
|
|
69
|
+
if not self.done():
|
|
70
|
+
self.cancel()
|
|
71
|
+
raise
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
if not self.done():
|
|
74
|
+
self.set_exception(exc)
|
|
64
75
|
|
|
65
76
|
def is_done(self) -> None:
|
|
66
77
|
iris = _iris.get_iris()
|
iop/components/debugpy.py
CHANGED
|
@@ -127,7 +127,10 @@ def debugpython(self, host_object: Any) -> None:
|
|
|
127
127
|
return
|
|
128
128
|
|
|
129
129
|
if not is_debugpy_installed():
|
|
130
|
-
self.log_alert(
|
|
130
|
+
self.log_alert(
|
|
131
|
+
"Debugpy is not installed. Install iris-pex-embedded-python[debug] "
|
|
132
|
+
"to enable remote debugging."
|
|
133
|
+
)
|
|
131
134
|
return
|
|
132
135
|
|
|
133
136
|
# Configure Python interpreter
|
|
@@ -167,7 +170,10 @@ def debugpy_in_iris(iris_dir, port) -> bool:
|
|
|
167
170
|
os.__file__ = __file__
|
|
168
171
|
|
|
169
172
|
if not is_debugpy_installed():
|
|
170
|
-
print(
|
|
173
|
+
print(
|
|
174
|
+
"debugpy is not installed. Install iris-pex-embedded-python[debug] "
|
|
175
|
+
"to enable remote debugging."
|
|
176
|
+
)
|
|
171
177
|
return False
|
|
172
178
|
if not iris_dir:
|
|
173
179
|
print("IRIS directory is not specified.")
|
iop/messages/persistent.py
CHANGED
|
@@ -4,18 +4,19 @@ import importlib
|
|
|
4
4
|
import inspect
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import Enum
|
|
7
9
|
from typing import Any
|
|
8
10
|
|
|
9
|
-
import iris_persistence
|
|
10
11
|
from iris_persistence import Field as Field
|
|
11
|
-
from iris_persistence import Model
|
|
12
|
+
from iris_persistence import Model as _PersistenceModel
|
|
12
13
|
from iris_persistence.models import ModelMeta
|
|
13
14
|
from iris_persistence.runtime import get_runtime
|
|
14
15
|
|
|
15
16
|
from ..runtime.environment import prepend_sys_path
|
|
16
17
|
|
|
17
18
|
DEFAULT_SUPERCLASS = "Ens.MessageBody"
|
|
18
|
-
DEFAULT_SYNC_MODE = "
|
|
19
|
+
DEFAULT_SYNC_MODE = "managed"
|
|
19
20
|
MESSAGE_KIND_PARAMETER = "IOP_MESSAGE_KIND"
|
|
20
21
|
MESSAGE_KIND_VALUE = "PersistentMessage"
|
|
21
22
|
PYTHON_CLASS_PARAMETER = "IOP_PYTHON_CLASS"
|
|
@@ -28,13 +29,46 @@ _IRIS_TO_PYTHON_STRICT_CACHE: dict[str, bool] = {}
|
|
|
28
29
|
_IRIS_TO_MESSAGE_CLASS_CACHE: dict[str, type] = {}
|
|
29
30
|
_IRIS_PARAMETER_CACHE: dict[tuple[str, str], str | None] = {}
|
|
30
31
|
_AUTO_SYNCED: set[tuple[type, str]] = set()
|
|
32
|
+
_CACHE_VALUE_UNSET = object()
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
class PersistentMessageError(Exception):
|
|
34
36
|
"""Raised when a native persistent message cannot be materialized."""
|
|
35
37
|
|
|
36
38
|
|
|
37
|
-
class
|
|
39
|
+
class _PersistentMessageResolutionState(Enum):
|
|
40
|
+
UNMAPPED = "unmapped"
|
|
41
|
+
RESOLVED = "resolved"
|
|
42
|
+
INVALID = "invalid"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class _PersistentMessageResolution:
|
|
47
|
+
state: _PersistentMessageResolutionState
|
|
48
|
+
message_class: type | None = None
|
|
49
|
+
error: PersistentMessageError | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _materialize_deferred_annotations(namespace: dict) -> None:
|
|
53
|
+
"""Expose Python 3.14 deferred annotations to iris-persistence."""
|
|
54
|
+
if "__annotations__" in namespace:
|
|
55
|
+
return
|
|
56
|
+
annotate = namespace.get("__annotate_func__")
|
|
57
|
+
if annotate is not None:
|
|
58
|
+
namespace["__annotations__"] = annotate(1)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _ModelMeta(ModelMeta):
|
|
62
|
+
def __new__(mcs, name: str, bases: tuple, namespace: dict, **kwargs: Any):
|
|
63
|
+
_materialize_deferred_annotations(namespace)
|
|
64
|
+
return super().__new__(mcs, name, bases, namespace, **kwargs)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Model(_PersistenceModel, metaclass=_ModelMeta):
|
|
68
|
+
"""Python 3.10–3.14 compatible public persistence model base."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _PersistentMessageMeta(_ModelMeta):
|
|
38
72
|
def __init__(cls, name: str, bases: tuple, namespace: dict, **kwargs: Any):
|
|
39
73
|
super().__init__(name, bases, namespace, **kwargs)
|
|
40
74
|
|
|
@@ -199,8 +233,7 @@ def serialize_persistent_message(
|
|
|
199
233
|
iris_classname = resolve_iris_classname(msg_cls)
|
|
200
234
|
_ensure_schema(msg_cls, iris_classname)
|
|
201
235
|
|
|
202
|
-
iris_obj =
|
|
203
|
-
message,
|
|
236
|
+
iris_obj = message.to_iris(
|
|
204
237
|
auto_sync=False,
|
|
205
238
|
validate=False,
|
|
206
239
|
)
|
|
@@ -215,36 +248,14 @@ def deserialize_persistent_message(
|
|
|
215
248
|
if not iris_classname:
|
|
216
249
|
return serial
|
|
217
250
|
|
|
218
|
-
|
|
219
|
-
if
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
try:
|
|
227
|
-
msg_cls = load_python_class(python_classname, python_classpath)
|
|
228
|
-
except (AttributeError, ModuleNotFoundError, PersistentMessageError) as exc:
|
|
229
|
-
if strict:
|
|
230
|
-
raise PersistentMessageError(
|
|
231
|
-
f"IRIS class {iris_classname!r} is marked as a PersistentMessage for "
|
|
232
|
-
f"Python class {python_classname!r}, but that Python class could not "
|
|
233
|
-
"be imported. Ensure the message class is importable, or register it "
|
|
234
|
-
"through CLASSES so migration writes IOP_PYTHON_CLASSPATH."
|
|
235
|
-
) from exc
|
|
236
|
-
return serial
|
|
237
|
-
|
|
238
|
-
if not is_persistent_message_class(msg_cls):
|
|
239
|
-
if strict:
|
|
240
|
-
raise PersistentMessageError(
|
|
241
|
-
f"IRIS class {iris_classname!r} maps to {python_classname!r}, "
|
|
242
|
-
"but that class is not a PersistentMessage subclass."
|
|
243
|
-
)
|
|
244
|
-
return serial
|
|
245
|
-
|
|
246
|
-
_prepare_message_class(msg_cls, iris_classname, registered=False)
|
|
247
|
-
_IRIS_TO_MESSAGE_CLASS_CACHE[iris_classname] = msg_cls
|
|
251
|
+
resolution = _resolve_persistent_message_class(iris_classname)
|
|
252
|
+
if resolution.state is _PersistentMessageResolutionState.UNMAPPED:
|
|
253
|
+
return serial
|
|
254
|
+
if resolution.state is _PersistentMessageResolutionState.INVALID:
|
|
255
|
+
assert resolution.error is not None
|
|
256
|
+
raise resolution.error
|
|
257
|
+
msg_cls = resolution.message_class
|
|
258
|
+
assert msg_cls is not None
|
|
248
259
|
|
|
249
260
|
known_pk = _safe_get_object_id(serial)
|
|
250
261
|
return msg_cls.from_iris(serial, known_pk=known_pk or "")
|
|
@@ -353,7 +364,7 @@ def _ensure_schema(msg_cls: type, iris_classname: str) -> None:
|
|
|
353
364
|
raise PersistentMessageError(
|
|
354
365
|
f"{get_python_classname(msg_cls)} has auto_sync=True but mode="
|
|
355
366
|
f"{getattr(msg_cls, '_sync_mode', None)!r}. Runtime auto-sync is only allowed "
|
|
356
|
-
"with mode='
|
|
367
|
+
"with mode='managed'."
|
|
357
368
|
)
|
|
358
369
|
|
|
359
370
|
_prepare_message_class(msg_cls, iris_classname, registered=False)
|
|
@@ -372,8 +383,12 @@ def _prepare_message_class(
|
|
|
372
383
|
if registered:
|
|
373
384
|
msg_cls._iop_registered_classname = iris_classname
|
|
374
385
|
_set_message_parameters(msg_cls)
|
|
375
|
-
_cache_mapping(
|
|
376
|
-
|
|
386
|
+
_cache_mapping(
|
|
387
|
+
get_python_classname(msg_cls),
|
|
388
|
+
iris_classname,
|
|
389
|
+
python_classpath=get_python_classpath(msg_cls),
|
|
390
|
+
message_class=msg_cls,
|
|
391
|
+
)
|
|
377
392
|
|
|
378
393
|
|
|
379
394
|
def _set_message_parameters(msg_cls: type) -> None:
|
|
@@ -388,10 +403,37 @@ def _set_message_parameters(msg_cls: type) -> None:
|
|
|
388
403
|
msg_cls._parameters = parameters
|
|
389
404
|
|
|
390
405
|
|
|
391
|
-
def _cache_mapping(
|
|
406
|
+
def _cache_mapping(
|
|
407
|
+
python_classname: str,
|
|
408
|
+
iris_classname: str,
|
|
409
|
+
*,
|
|
410
|
+
python_classpath: str | None | object = _CACHE_VALUE_UNSET,
|
|
411
|
+
strict: bool = True,
|
|
412
|
+
message_class: type | None = None,
|
|
413
|
+
) -> None:
|
|
414
|
+
previous_iris = _PYTHON_TO_IRIS_CACHE.get(python_classname)
|
|
415
|
+
if previous_iris and previous_iris != iris_classname:
|
|
416
|
+
_IRIS_TO_PYTHON_CACHE.pop(previous_iris, None)
|
|
417
|
+
_IRIS_TO_PYTHON_CLASSPATH_CACHE.pop(previous_iris, None)
|
|
418
|
+
_IRIS_TO_PYTHON_STRICT_CACHE.pop(previous_iris, None)
|
|
419
|
+
_IRIS_TO_MESSAGE_CLASS_CACHE.pop(previous_iris, None)
|
|
420
|
+
|
|
421
|
+
previous_python = _IRIS_TO_PYTHON_CACHE.get(iris_classname)
|
|
422
|
+
if previous_python and previous_python != python_classname:
|
|
423
|
+
if _PYTHON_TO_IRIS_CACHE.get(previous_python) == iris_classname:
|
|
424
|
+
_PYTHON_TO_IRIS_CACHE.pop(previous_python, None)
|
|
425
|
+
_IRIS_TO_MESSAGE_CLASS_CACHE.pop(iris_classname, None)
|
|
426
|
+
|
|
392
427
|
_PYTHON_TO_IRIS_CACHE[python_classname] = iris_classname
|
|
393
428
|
_IRIS_TO_PYTHON_CACHE[iris_classname] = python_classname
|
|
394
|
-
_IRIS_TO_PYTHON_STRICT_CACHE[iris_classname] =
|
|
429
|
+
_IRIS_TO_PYTHON_STRICT_CACHE[iris_classname] = strict
|
|
430
|
+
if python_classpath is not _CACHE_VALUE_UNSET:
|
|
431
|
+
if python_classpath:
|
|
432
|
+
_IRIS_TO_PYTHON_CLASSPATH_CACHE[iris_classname] = str(python_classpath)
|
|
433
|
+
else:
|
|
434
|
+
_IRIS_TO_PYTHON_CLASSPATH_CACHE.pop(iris_classname, None)
|
|
435
|
+
if message_class is not None:
|
|
436
|
+
_IRIS_TO_MESSAGE_CLASS_CACHE[iris_classname] = message_class
|
|
395
437
|
|
|
396
438
|
|
|
397
439
|
def _resolve_python_message_metadata(
|
|
@@ -416,13 +458,73 @@ def _resolve_python_message_metadata(
|
|
|
416
458
|
python_classname = iris_classname_to_python_classname(iris_classname)
|
|
417
459
|
|
|
418
460
|
if python_classname and strict:
|
|
419
|
-
_cache_mapping(
|
|
420
|
-
|
|
421
|
-
|
|
461
|
+
_cache_mapping(
|
|
462
|
+
python_classname,
|
|
463
|
+
iris_classname,
|
|
464
|
+
python_classpath=python_classpath,
|
|
465
|
+
strict=strict,
|
|
466
|
+
)
|
|
422
467
|
|
|
423
468
|
return python_classname, python_classpath, strict
|
|
424
469
|
|
|
425
470
|
|
|
471
|
+
def _resolve_persistent_message_class(
|
|
472
|
+
iris_classname: str,
|
|
473
|
+
) -> _PersistentMessageResolution:
|
|
474
|
+
cached = _IRIS_TO_MESSAGE_CLASS_CACHE.get(iris_classname)
|
|
475
|
+
if cached is not None:
|
|
476
|
+
return _PersistentMessageResolution(
|
|
477
|
+
_PersistentMessageResolutionState.RESOLVED,
|
|
478
|
+
message_class=cached,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
python_classname, python_classpath, strict = _resolve_python_message_metadata(
|
|
482
|
+
iris_classname
|
|
483
|
+
)
|
|
484
|
+
if not python_classname:
|
|
485
|
+
return _PersistentMessageResolution(
|
|
486
|
+
_PersistentMessageResolutionState.UNMAPPED
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
msg_cls = load_python_class(python_classname, python_classpath)
|
|
491
|
+
except (AttributeError, ModuleNotFoundError, PersistentMessageError) as exc:
|
|
492
|
+
if not strict:
|
|
493
|
+
return _PersistentMessageResolution(
|
|
494
|
+
_PersistentMessageResolutionState.UNMAPPED
|
|
495
|
+
)
|
|
496
|
+
error = PersistentMessageError(
|
|
497
|
+
f"IRIS class {iris_classname!r} is marked as a PersistentMessage for "
|
|
498
|
+
f"Python class {python_classname!r}, but that Python class could not "
|
|
499
|
+
"be imported. Ensure the message class is importable, or register it "
|
|
500
|
+
"through CLASSES so migration writes IOP_PYTHON_CLASSPATH."
|
|
501
|
+
)
|
|
502
|
+
error.__cause__ = exc
|
|
503
|
+
return _PersistentMessageResolution(
|
|
504
|
+
_PersistentMessageResolutionState.INVALID,
|
|
505
|
+
error=error,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if not is_persistent_message_class(msg_cls):
|
|
509
|
+
if not strict:
|
|
510
|
+
return _PersistentMessageResolution(
|
|
511
|
+
_PersistentMessageResolutionState.UNMAPPED
|
|
512
|
+
)
|
|
513
|
+
return _PersistentMessageResolution(
|
|
514
|
+
_PersistentMessageResolutionState.INVALID,
|
|
515
|
+
error=PersistentMessageError(
|
|
516
|
+
f"IRIS class {iris_classname!r} maps to {python_classname!r}, "
|
|
517
|
+
"but that class is not a PersistentMessage subclass."
|
|
518
|
+
),
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
_prepare_message_class(msg_cls, iris_classname, registered=False)
|
|
522
|
+
return _PersistentMessageResolution(
|
|
523
|
+
_PersistentMessageResolutionState.RESOLVED,
|
|
524
|
+
message_class=msg_cls,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
426
528
|
def get_iris_class_parameter(
|
|
427
529
|
iris_classname: str,
|
|
428
530
|
parameter_name: str,
|
iop/messages/serialization.py
CHANGED
|
@@ -104,6 +104,9 @@ class MessageSerializer:
|
|
|
104
104
|
f"Failed to load class {serial.classname}: {str(e)}"
|
|
105
105
|
) from e
|
|
106
106
|
|
|
107
|
+
if not isinstance(msg_class, type):
|
|
108
|
+
raise SerializationError(f"Class {msg_class} must be a class")
|
|
109
|
+
|
|
107
110
|
if is_dataclass(msg_class) and issubclass(msg_class, BaseModel):
|
|
108
111
|
raise SerializationError(
|
|
109
112
|
f"Class '{msg_class.__name__}' combines @dataclass with PydanticMessage, which are incompatible. "
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import xmltodict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def dict_to_xml(data: dict[str, Any]) -> str:
|
|
11
|
+
xml = xmltodict.unparse(data, pretty=True)
|
|
12
|
+
xml = xml.replace('<?xml version="1.0" encoding="utf-8"?>', "")
|
|
13
|
+
return xml[1:]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def xml_to_json(xml_string: str) -> str:
|
|
17
|
+
def postprocessor(path, key, value):
|
|
18
|
+
return key, "" if value is None else value
|
|
19
|
+
|
|
20
|
+
data = xmltodict.parse(xml_string, postprocessor=postprocessor)
|
|
21
|
+
if "Production" in data:
|
|
22
|
+
production_obj = data["Production"]
|
|
23
|
+
production_name = production_obj.get("@Name", "Production")
|
|
24
|
+
data = {production_name: production_obj}
|
|
25
|
+
return json.dumps(data)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def stream_to_string(stream, buffer=1000000) -> str:
|
|
29
|
+
string = ""
|
|
30
|
+
stream.Rewind()
|
|
31
|
+
while not stream.AtEnd:
|
|
32
|
+
string += stream.Read(buffer)
|
|
33
|
+
return string
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def string_to_stream(iris, string: str, buffer=1000000):
|
|
37
|
+
stream = iris.cls("%Stream.GlobalCharacter")._New()
|
|
38
|
+
chunks = [string[i : i + buffer] for i in range(0, len(string), buffer)]
|
|
39
|
+
for chunk in chunks:
|
|
40
|
+
stream.Write(chunk)
|
|
41
|
+
return stream
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def guess_path(module: str, path: str) -> str:
|
|
45
|
+
if not module:
|
|
46
|
+
raise ValueError("Module name cannot be empty")
|
|
47
|
+
|
|
48
|
+
if module.startswith("."):
|
|
49
|
+
dot_count = len(module) - len(module.lstrip("."))
|
|
50
|
+
module = module[dot_count:]
|
|
51
|
+
for _ in range(dot_count - 1):
|
|
52
|
+
path = os.path.dirname(path)
|
|
53
|
+
|
|
54
|
+
if module.endswith(".py"):
|
|
55
|
+
module_path = module.replace(".", os.sep)
|
|
56
|
+
else:
|
|
57
|
+
module_path = module.replace(".", os.sep) + ".py"
|
|
58
|
+
return os.path.join(path, module_path)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..runtime import iris as _iris
|
|
8
|
+
from .io import dict_to_xml, stream_to_string, string_to_stream, xml_to_json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _raise_on_error(status: Any) -> None:
|
|
12
|
+
iris = _iris.get_iris()
|
|
13
|
+
if iris.system.Status.IsError(status):
|
|
14
|
+
raise RuntimeError(iris.system.Status.GetOneStatusText(status))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_production(production_name: str, xml: str) -> None:
|
|
18
|
+
package, _, short_name = production_name.rpartition(".")
|
|
19
|
+
stream = string_to_stream(_iris.get_iris(), xml)
|
|
20
|
+
_raise_on_error(
|
|
21
|
+
_iris.get_iris().cls("IOP.Utils").CreateProduction(package, short_name, stream)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def register_production_definition(
|
|
26
|
+
production_name: str,
|
|
27
|
+
production: dict,
|
|
28
|
+
*,
|
|
29
|
+
xml_fallback: Callable[[str, str], None] = register_production,
|
|
30
|
+
) -> None:
|
|
31
|
+
try:
|
|
32
|
+
_raise_on_error(
|
|
33
|
+
_iris.get_iris()
|
|
34
|
+
.cls("IOP.Utils")
|
|
35
|
+
.CreateProductionFromJSON(production_name, json.dumps(production))
|
|
36
|
+
)
|
|
37
|
+
except RuntimeError as exc:
|
|
38
|
+
if not is_missing_production_class_error(exc, production_name):
|
|
39
|
+
raise
|
|
40
|
+
xml_fallback(production_name, dict_to_xml(production))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_missing_production_class_error(
|
|
44
|
+
exc: RuntimeError, production_name: str
|
|
45
|
+
) -> bool:
|
|
46
|
+
message = str(exc)
|
|
47
|
+
return "CLASS DOES NOT EXIST" in message and production_name in message
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def export_production(production_name: str):
|
|
51
|
+
data = _iris.get_iris().cls("IOP.Utils").ExportProduction(production_name)
|
|
52
|
+
return xml_to_json(stream_to_string(data))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def export_production_connections(production_name: str) -> dict:
|
|
56
|
+
data = _iris.get_iris().cls("IOP.Utils").ExportProductionConnections(
|
|
57
|
+
production_name
|
|
58
|
+
)
|
|
59
|
+
return data if isinstance(data, dict) else json.loads(data)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def export_production_queue_info(production_name: str) -> dict:
|
|
63
|
+
data = _iris.get_iris().cls("IOP.Utils").ExportProductionQueueInfo(production_name)
|
|
64
|
+
return data if isinstance(data, dict) else json.loads(data)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def apply_production_plan(
|
|
68
|
+
plan: dict, allow_destructive: bool = False
|
|
69
|
+
) -> dict:
|
|
70
|
+
production_name = plan.get("production") or plan.get("production_name")
|
|
71
|
+
if not production_name:
|
|
72
|
+
raise ValueError("Production plan is missing the production name.")
|
|
73
|
+
data = (
|
|
74
|
+
_iris.get_iris()
|
|
75
|
+
.cls("IOP.Utils")
|
|
76
|
+
.ApplyProductionPlan(
|
|
77
|
+
production_name,
|
|
78
|
+
json.dumps(plan),
|
|
79
|
+
1 if allow_destructive else 0,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
return data if isinstance(data, dict) else json.loads(data)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .manifest import ClassRegistration, MigrationManifest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def execute_class_registration(
|
|
10
|
+
registration: ClassRegistration,
|
|
11
|
+
*,
|
|
12
|
+
persistent_registry: dict[str, tuple[type, bool]] | None,
|
|
13
|
+
register_persistent: Callable[..., None],
|
|
14
|
+
register_component: Callable[..., None],
|
|
15
|
+
register_file: Callable[..., None],
|
|
16
|
+
register_package: Callable[..., None],
|
|
17
|
+
register_folder: Callable[..., None],
|
|
18
|
+
) -> None:
|
|
19
|
+
action = registration.action
|
|
20
|
+
if action == "persistent_message":
|
|
21
|
+
assert registration.cls is not None
|
|
22
|
+
register_persistent(
|
|
23
|
+
registration.cls,
|
|
24
|
+
registration.iris_classname,
|
|
25
|
+
sync_schema=registration.sync_schema,
|
|
26
|
+
persistent_registry=persistent_registry,
|
|
27
|
+
)
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
assert registration.path is not None
|
|
31
|
+
if action in {"component_class", "component_descriptor"}:
|
|
32
|
+
register_component(
|
|
33
|
+
registration.module,
|
|
34
|
+
registration.classname,
|
|
35
|
+
registration.path,
|
|
36
|
+
1,
|
|
37
|
+
registration.iris_classname,
|
|
38
|
+
)
|
|
39
|
+
elif action in {"module_file", "file"}:
|
|
40
|
+
register_file(
|
|
41
|
+
registration.file,
|
|
42
|
+
registration.path,
|
|
43
|
+
1,
|
|
44
|
+
registration.iris_classname,
|
|
45
|
+
)
|
|
46
|
+
elif action == "package":
|
|
47
|
+
register_package(
|
|
48
|
+
registration.package,
|
|
49
|
+
registration.path,
|
|
50
|
+
1,
|
|
51
|
+
registration.iris_classname,
|
|
52
|
+
)
|
|
53
|
+
elif action == "folder":
|
|
54
|
+
register_folder(registration.path, 1, registration.iris_classname)
|
|
55
|
+
else:
|
|
56
|
+
raise ValueError(f"Invalid migration class action: {action}.")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def execute_manifest(
|
|
60
|
+
manifest: MigrationManifest,
|
|
61
|
+
*,
|
|
62
|
+
persistent_registry: dict[str, tuple[type, bool]] | None,
|
|
63
|
+
execute_class: Callable[..., None],
|
|
64
|
+
validate_production: Callable[[Any], Any],
|
|
65
|
+
register_production: Callable[[str, dict], None],
|
|
66
|
+
register_schema: Callable[[type], None],
|
|
67
|
+
) -> None:
|
|
68
|
+
for registration in manifest.class_registrations:
|
|
69
|
+
execute_class(registration, persistent_registry=persistent_registry)
|
|
70
|
+
|
|
71
|
+
for production in manifest.production_registrations:
|
|
72
|
+
for registration in production.class_registrations:
|
|
73
|
+
execute_class(registration, persistent_registry=persistent_registry)
|
|
74
|
+
validate_production(production.validation_subject)
|
|
75
|
+
register_production(production.name, production.definition)
|
|
76
|
+
|
|
77
|
+
for registration in manifest.schema_registrations:
|
|
78
|
+
register_schema(registration.schema_class)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from types import ModuleType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def module_name_from_file(filename: str) -> str:
|
|
10
|
+
return os.path.splitext(os.path.basename(filename))[0]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def import_module_from_path(module_name: str, file_path: str) -> ModuleType:
|
|
14
|
+
if not os.path.isabs(file_path):
|
|
15
|
+
raise ValueError("The file path must be absolute")
|
|
16
|
+
|
|
17
|
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
18
|
+
if spec is None or spec.loader is None:
|
|
19
|
+
raise ImportError(f"Cannot find module named {module_name} at {file_path}")
|
|
20
|
+
|
|
21
|
+
module = importlib.util.module_from_spec(spec)
|
|
22
|
+
sys.modules[module_name] = module
|
|
23
|
+
try:
|
|
24
|
+
spec.loader.exec_module(module)
|
|
25
|
+
except Exception:
|
|
26
|
+
sys.modules.pop(module_name, None)
|
|
27
|
+
raise
|
|
28
|
+
return module
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_settings(filename: str | None = None) -> tuple[ModuleType, str | None]:
|
|
32
|
+
path_added = None
|
|
33
|
+
if filename:
|
|
34
|
+
if not os.path.isabs(filename):
|
|
35
|
+
raise ValueError("The filename must be absolute")
|
|
36
|
+
path_added = os.path.normpath(os.path.dirname(filename))
|
|
37
|
+
sys.path.insert(0, path_added)
|
|
38
|
+
settings = import_module_from_path(module_name_from_file(filename), filename)
|
|
39
|
+
else:
|
|
40
|
+
import settings # type: ignore
|
|
41
|
+
|
|
42
|
+
return settings, path_added
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def folder_path(filename: str | None, path_added_to_sys: str | None) -> str:
|
|
46
|
+
del path_added_to_sys
|
|
47
|
+
return os.path.dirname(filename) if filename else os.getcwd()
|