iris-pex-embedded-python 4.0.2b1__py3-none-any.whl → 4.1.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/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))
@@ -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
- asyncio.create_task(self.send())
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
- # init parameters
37
- iris = _iris.get_iris()
38
- message_header_id = iris.ref()
39
- queue_name = iris.ref()
40
- end_time = iris.ref()
41
- request = dispatch_serializer(self.request)
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
- # send request
44
- self._iris_handle.dispatchSendRequestAsyncNG(
45
- self.target,
46
- request,
47
- self.timeout,
48
- self.description,
49
- message_header_id,
50
- queue_name,
51
- end_time,
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
- # get byref values
55
- self._message_header_id = message_header_id.value
56
- self._queue_name = queue_name.value
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
- while not self._done:
60
- await asyncio.sleep(0.1)
61
- self.is_done()
62
+ while not self._done:
63
+ await asyncio.sleep(0.1)
64
+ self.is_done()
62
65
 
63
- self.set_result(self._response)
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("Debugpy is not installed.")
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("debugpy is not installed.")
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.")
@@ -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 = "extend"
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 _PersistentMessageMeta(ModelMeta):
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 = iris_persistence.materialize(
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
- msg_cls = _IRIS_TO_MESSAGE_CLASS_CACHE.get(iris_classname)
219
- if msg_cls is None:
220
- python_classname, python_classpath, strict = _resolve_python_message_metadata(
221
- iris_classname
222
- )
223
- if not python_classname:
224
- return serial
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='extend'."
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(get_python_classname(msg_cls), iris_classname)
376
- _IRIS_TO_MESSAGE_CLASS_CACHE[iris_classname] = msg_cls
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(python_classname: str, iris_classname: str) -> None:
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] = True
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(python_classname, iris_classname)
420
- if python_classpath:
421
- _IRIS_TO_PYTHON_CLASSPATH_CACHE[iris_classname] = python_classpath
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,
@@ -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()