iris-pex-embedded-python 3.6.1b1__py3-none-any.whl → 3.7.1b1__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.
grongier/pex/__init__.py CHANGED
@@ -5,10 +5,11 @@ from iop._private_session_process import _PrivateSessionProcess
5
5
  from iop._business_operation import _BusinessOperation
6
6
  from iop._inbound_adapter import _InboundAdapter
7
7
  from iop._outbound_adapter import _OutboundAdapter
8
- from iop._message import _Message
9
- from iop._message import _PickleMessage
10
- from iop._director import _Director
11
- from iop._utils import _Utils
8
+ from iop._message import _Message
9
+ from iop._message import _PickleMessage
10
+ from iop._persistent_message import Field, Model, _PersistentMessage
11
+ from iop._director import _Director
12
+ from iop._utils import _Utils
12
13
 
13
14
  class Utils(_Utils): pass
14
15
  class InboundAdapter(_InboundAdapter): pass
@@ -19,6 +20,8 @@ class BusinessProcess(_BusinessProcess): pass
19
20
  class DuplexService(_PrivateSessionDuplex): pass
20
21
  class DuplexOperation(_PrivateSessionDuplex): pass
21
22
  class DuplexProcess(_PrivateSessionProcess): pass
22
- class Message(_Message): pass
23
- class PickleMessage(_PickleMessage): pass
24
- class Director(_Director): pass
23
+ class Message(_Message): pass
24
+ class PickleMessage(_PickleMessage): pass
25
+ class PersistentMessage(_PersistentMessage):
26
+ _iop_persistent_message_abstract = True
27
+ class Director(_Director): pass
iop/__init__.py CHANGED
@@ -2,12 +2,13 @@ from iop._business_operation import _BusinessOperation
2
2
  from iop._business_process import _BusinessProcess
3
3
  from iop._business_service import _BusinessService
4
4
  from iop._director import _Director
5
- from iop._inbound_adapter import _InboundAdapter
6
- from iop._message import _Message, _PickleMessage, _PydanticMessage, _PydanticPickleMessage
7
- from iop._outbound_adapter import _OutboundAdapter
8
- from iop._private_session_duplex import _PrivateSessionDuplex
9
- from iop._private_session_process import _PrivateSessionProcess
10
- from iop._utils import _Utils
5
+ from iop._inbound_adapter import _InboundAdapter
6
+ from iop._message import _Message, _PickleMessage, _PydanticMessage, _PydanticPickleMessage
7
+ from iop._outbound_adapter import _OutboundAdapter
8
+ from iop._persistent_message import Field, Model, _PersistentMessage
9
+ from iop._private_session_duplex import _PrivateSessionDuplex
10
+ from iop._private_session_process import _PrivateSessionProcess
11
+ from iop._utils import _Utils
11
12
 
12
13
  class Utils(_Utils): pass
13
14
  class InboundAdapter(_InboundAdapter): pass
@@ -19,7 +20,9 @@ class DuplexService(_PrivateSessionDuplex): pass
19
20
  class DuplexOperation(_PrivateSessionDuplex): pass
20
21
  class DuplexProcess(_PrivateSessionProcess): pass
21
22
  class Message(_Message): pass
22
- class PickleMessage(_PickleMessage): pass
23
- class PydanticMessage(_PydanticMessage): pass
24
- class PydanticPickleMessage(_PydanticPickleMessage): pass
25
- class Director(_Director): pass
23
+ class PickleMessage(_PickleMessage): pass
24
+ class PydanticMessage(_PydanticMessage): pass
25
+ class PydanticPickleMessage(_PydanticPickleMessage): pass
26
+ class PersistentMessage(_PersistentMessage):
27
+ _iop_persistent_message_abstract = True
28
+ class Director(_Director): pass
iop/_dispatch.py CHANGED
@@ -3,6 +3,11 @@ from typing import Any, List, Tuple, Callable
3
3
 
4
4
  from ._serialization import serialize_message, serialize_pickle_message, deserialize_message, deserialize_pickle_message, serialize_message_generator, serialize_pickle_message_generator
5
5
  from ._message_validator import is_message_instance, is_pickle_message_instance, is_iris_object_instance
6
+ from ._persistent_message import (
7
+ deserialize_persistent_message,
8
+ is_persistent_message_instance,
9
+ serialize_persistent_message,
10
+ )
6
11
 
7
12
  def dispatch_serializer(message: Any, is_generator: bool = False) -> Any:
8
13
  """Serializes the message based on its type.
@@ -17,7 +22,9 @@ def dispatch_serializer(message: Any, is_generator: bool = False) -> Any:
17
22
  TypeError: If message is invalid type
18
23
  """
19
24
  if message is not None:
20
- if is_message_instance(message):
25
+ if is_persistent_message_instance(message):
26
+ return serialize_persistent_message(message, is_generator=is_generator)
27
+ elif is_message_instance(message):
21
28
  if is_generator:
22
29
  return serialize_message_generator(message)
23
30
  return serialize_message(message)
@@ -63,6 +70,8 @@ def dispatch_deserializer(serial: Any) -> Any:
63
70
  )
64
71
  ):
65
72
  return deserialize_pickle_message(serial)
73
+ elif is_iris_object_instance(serial):
74
+ return deserialize_persistent_message(serial)
66
75
  else:
67
76
  return serial
68
77
 
@@ -133,4 +142,4 @@ def get_handler_info(host: Any, method_name: str) -> Tuple[str, str] | None:
133
142
  return f"{annotation.__module__}.{annotation.__name__}", method_name
134
143
 
135
144
  except ValueError:
136
- return None
145
+ return None
@@ -0,0 +1,513 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import inspect
5
+ import os
6
+ import sys
7
+ from typing import Any, Optional
8
+
9
+ from iris_persistence import Field, Model
10
+ from iris_persistence.models import ModelMeta
11
+ from iris_persistence.query import _build_model_from_iris_obj, _materialize_related_value
12
+ from iris_persistence.runtime import get_runtime
13
+
14
+
15
+ DEFAULT_SUPERCLASS = "Ens.MessageBody"
16
+ DEFAULT_SYNC_MODE = "extend"
17
+ MESSAGE_KIND_PARAMETER = "IOP_MESSAGE_KIND"
18
+ MESSAGE_KIND_VALUE = "PersistentMessage"
19
+ PYTHON_CLASS_PARAMETER = "IOP_PYTHON_CLASS"
20
+ PYTHON_CLASSPATH_PARAMETER = "IOP_PYTHON_CLASSPATH"
21
+
22
+ _PYTHON_TO_IRIS_CACHE: dict[str, str] = {}
23
+ _IRIS_TO_PYTHON_CACHE: dict[str, str] = {}
24
+ _IRIS_TO_PYTHON_CLASSPATH_CACHE: dict[str, str] = {}
25
+ _IRIS_TO_PYTHON_STRICT_CACHE: dict[str, bool] = {}
26
+ _IRIS_PARAMETER_CACHE: dict[tuple[str, str], Optional[str]] = {}
27
+ _AUTO_SYNCED: set[tuple[type, str]] = set()
28
+
29
+
30
+ class PersistentMessageError(Exception):
31
+ """Raised when a native persistent message cannot be materialized."""
32
+
33
+
34
+ class _PersistentMessageMeta(ModelMeta):
35
+ def __init__(cls, name: str, bases: tuple, namespace: dict, **kwargs: Any):
36
+ super().__init__(name, bases, namespace, **kwargs)
37
+
38
+ if namespace.get("_iop_persistent_message_abstract", False):
39
+ cls._iop_persistent_message_base = True
40
+ cls._iop_persistent_message_abstract = True
41
+ return
42
+
43
+ if not any(getattr(base, "_iop_persistent_message_base", False) for base in bases):
44
+ cls._iop_persistent_message_base = True
45
+ cls._iop_persistent_message_abstract = True
46
+ return
47
+
48
+ _apply_persistent_message_defaults(cls, namespace, kwargs)
49
+ cls._iop_persistent_message_base = True
50
+ cls._iop_persistent_message_abstract = False
51
+
52
+
53
+ def _explicit_meta_classname(cls: type) -> Optional[str]:
54
+ meta = cls.__dict__.get("Meta")
55
+ if meta is None or not hasattr(meta, "classname"):
56
+ return None
57
+ value = getattr(meta, "classname")
58
+ return str(value) if value else None
59
+
60
+
61
+ def _apply_persistent_message_defaults(
62
+ cls: type,
63
+ namespace: Optional[dict] = None,
64
+ kwargs: Optional[dict] = None,
65
+ ) -> None:
66
+ namespace = namespace or {}
67
+ kwargs = kwargs or {}
68
+ meta = namespace.get("Meta") or cls.__dict__.get("Meta")
69
+
70
+ has_superclasses = "superclasses" in kwargs or (
71
+ meta is not None and hasattr(meta, "superclasses")
72
+ )
73
+ has_classname = meta is not None and hasattr(meta, "classname")
74
+ has_mode = meta is not None and hasattr(meta, "mode")
75
+ has_auto_sync = meta is not None and hasattr(meta, "auto_sync")
76
+
77
+ if not has_superclasses:
78
+ cls._superclasses = DEFAULT_SUPERCLASS
79
+ if not has_classname and not getattr(cls, "_iop_registered_classname", None):
80
+ cls._classname = python_classname_to_iris_classname(get_python_classname(cls))
81
+ if not has_mode:
82
+ cls._sync_mode = DEFAULT_SYNC_MODE
83
+ if not has_auto_sync:
84
+ cls._auto_sync = True
85
+ _set_message_parameters(cls)
86
+
87
+
88
+ class _PersistentMessage(Model, metaclass=_PersistentMessageMeta):
89
+ _iop_persistent_message_abstract = True
90
+
91
+ def get_iris_id(self) -> Optional[str]:
92
+ if self.pk:
93
+ return self.pk
94
+
95
+ iris_obj = self.__dict__.get("_iris_obj")
96
+ if iris_obj is None:
97
+ return None
98
+
99
+ try:
100
+ obj_id = get_runtime().get_object_id(iris_obj)
101
+ if obj_id:
102
+ return str(obj_id)
103
+ except Exception:
104
+ pass
105
+
106
+ for method_name in ("_Id", "Id", "%Id"):
107
+ try:
108
+ method = getattr(iris_obj, method_name)
109
+ obj_id = method()
110
+ if obj_id:
111
+ return str(obj_id)
112
+ except Exception:
113
+ pass
114
+ return None
115
+
116
+
117
+ def is_persistent_message_class(klass: Any) -> bool:
118
+ try:
119
+ return (
120
+ isinstance(klass, type)
121
+ and issubclass(klass, _PersistentMessage)
122
+ and not getattr(klass, "_iop_persistent_message_abstract", False)
123
+ )
124
+ except TypeError:
125
+ return False
126
+
127
+
128
+ def is_persistent_message_instance(obj: Any) -> bool:
129
+ return is_persistent_message_class(type(obj))
130
+
131
+
132
+ def get_python_classname(klass: type) -> str:
133
+ return f"{klass.__module__}.{klass.__name__}"
134
+
135
+
136
+ def python_classname_to_iris_classname(python_classname: str) -> str:
137
+ """Encode a Python FQCN as an IRIS-safe classname."""
138
+ if not python_classname:
139
+ raise ValueError("Python classname cannot be empty")
140
+ return ".".join(_encode_iris_identifier(part) for part in python_classname.split("."))
141
+
142
+
143
+ def iris_classname_to_python_classname(iris_classname: str) -> str:
144
+ """Decode an IRIS classname produced by python_classname_to_iris_classname."""
145
+ if not iris_classname:
146
+ raise ValueError("IRIS classname cannot be empty")
147
+ return ".".join(_decode_iris_identifier(part) for part in iris_classname.split("."))
148
+
149
+
150
+ def register_persistent_message_class(
151
+ msg_cls: type,
152
+ iris_classname: str,
153
+ *,
154
+ sync_schema: bool = True,
155
+ ) -> None:
156
+ if not is_persistent_message_class(msg_cls):
157
+ raise TypeError("The class must be a subclass of PersistentMessage")
158
+ if not iris_classname:
159
+ raise ValueError("PersistentMessage IRIS classname cannot be empty")
160
+
161
+ explicit_classname = _explicit_meta_classname(msg_cls)
162
+ if explicit_classname and explicit_classname != iris_classname:
163
+ raise ValueError(
164
+ f"{get_python_classname(msg_cls)} declares Meta.classname={explicit_classname!r}, "
165
+ f"but CLASSES registers it as {iris_classname!r}"
166
+ )
167
+
168
+ _prepare_message_class(msg_cls, iris_classname, registered=True)
169
+
170
+ if sync_schema:
171
+ msg_cls.sync_schema()
172
+
173
+
174
+ def serialize_persistent_message(message: _PersistentMessage, is_generator: bool = False) -> Any:
175
+ if is_generator:
176
+ raise TypeError("PersistentMessage cannot be used as a generator start message.")
177
+
178
+ msg_cls = type(message)
179
+ iris_classname = resolve_iris_classname(msg_cls)
180
+ _ensure_schema(msg_cls, iris_classname)
181
+
182
+ runtime = get_runtime()
183
+ iris_obj = message.__dict__.get("_iris_obj")
184
+ if iris_obj is None:
185
+ iris_obj = runtime.create_object(iris_classname)
186
+
187
+ for field_name, model_field in msg_cls.__model_fields__.items():
188
+ if field_name not in message.__dict__:
189
+ continue
190
+ value = getattr(message, field_name)
191
+ materialized = _materialize_related_value(runtime, model_field.declared_type, value)
192
+ runtime.inject_iris_value(
193
+ iris_obj,
194
+ field_name,
195
+ materialized,
196
+ field_meta=model_field.field_info,
197
+ )
198
+
199
+ message._iris_obj = iris_obj
200
+ try:
201
+ obj_id = runtime.get_object_id(iris_obj)
202
+ if obj_id:
203
+ message._pk = str(obj_id)
204
+ except Exception:
205
+ pass
206
+ return iris_obj
207
+
208
+
209
+ def deserialize_persistent_message(serial: Any) -> Any:
210
+ iris_classname = get_iris_object_classname(serial)
211
+ if not iris_classname:
212
+ return serial
213
+
214
+ python_classname, python_classpath, strict = _resolve_python_message_metadata(
215
+ iris_classname
216
+ )
217
+ if not python_classname:
218
+ return serial
219
+
220
+ try:
221
+ msg_cls = load_python_class(python_classname, python_classpath)
222
+ except (AttributeError, ModuleNotFoundError, PersistentMessageError) as exc:
223
+ if strict:
224
+ raise PersistentMessageError(
225
+ f"IRIS class {iris_classname!r} is marked as a PersistentMessage for "
226
+ f"Python class {python_classname!r}, but that Python class could not "
227
+ "be imported. Ensure the message class is importable, or register it "
228
+ "through CLASSES so migration writes IOP_PYTHON_CLASSPATH."
229
+ ) from exc
230
+ return serial
231
+
232
+ if not is_persistent_message_class(msg_cls):
233
+ if strict:
234
+ raise PersistentMessageError(
235
+ f"IRIS class {iris_classname!r} maps to {python_classname!r}, "
236
+ "but that class is not a PersistentMessage subclass."
237
+ )
238
+ return serial
239
+
240
+ _prepare_message_class(msg_cls, iris_classname, registered=False)
241
+
242
+ known_pk = _safe_get_object_id(serial)
243
+ return _build_model_from_iris_obj(msg_cls, serial, known_pk=known_pk or "")
244
+
245
+
246
+ def resolve_iris_classname(msg_cls: type) -> str:
247
+ python_classname = get_python_classname(msg_cls)
248
+
249
+ cached = _PYTHON_TO_IRIS_CACHE.get(python_classname)
250
+ if cached:
251
+ return cached
252
+
253
+ registered = getattr(msg_cls, "_iop_registered_classname", None)
254
+ if registered:
255
+ _cache_mapping(python_classname, registered)
256
+ return registered
257
+
258
+ explicit_classname = _explicit_meta_classname(msg_cls)
259
+ if explicit_classname:
260
+ _prepare_message_class(msg_cls, explicit_classname, registered=False)
261
+ return explicit_classname
262
+
263
+ iris_classname = python_classname_to_iris_classname(python_classname)
264
+ _prepare_message_class(msg_cls, iris_classname, registered=False)
265
+ return iris_classname
266
+
267
+
268
+ def resolve_python_classname(iris_classname: str) -> Optional[str]:
269
+ python_classname, _python_classpath, _strict = _resolve_python_message_metadata(
270
+ iris_classname
271
+ )
272
+ return python_classname
273
+
274
+
275
+ def load_python_class(python_classname: str, python_classpath: Optional[str] = None) -> type:
276
+ module_name, _, class_name = python_classname.rpartition(".")
277
+ if not module_name:
278
+ raise PersistentMessageError(
279
+ f"PersistentMessage Python classname {python_classname!r} is not fully qualified"
280
+ )
281
+ try:
282
+ module = importlib.import_module(module_name)
283
+ except ModuleNotFoundError as exc:
284
+ if not python_classpath or not _is_missing_target_module(exc, module_name):
285
+ raise
286
+ _prepend_sys_path(python_classpath)
287
+ module = importlib.import_module(module_name)
288
+ return getattr(module, class_name)
289
+
290
+
291
+ def get_python_classpath(klass: type) -> str:
292
+ try:
293
+ class_file = inspect.getfile(klass)
294
+ except TypeError:
295
+ return ""
296
+
297
+ if not class_file:
298
+ return ""
299
+
300
+ classpath = os.path.abspath(os.path.dirname(class_file))
301
+ module_parts = klass.__module__.split(".")
302
+ for _ in module_parts[:-1]:
303
+ classpath = os.path.dirname(classpath)
304
+ return classpath
305
+
306
+
307
+ def get_iris_object_classname(obj: Any) -> Optional[str]:
308
+ for method_name in ("%ClassName", "_ClassName"):
309
+ try:
310
+ method = getattr(obj, method_name)
311
+ value = method(1)
312
+ if value:
313
+ return str(value)
314
+ except Exception:
315
+ pass
316
+ try:
317
+ value = obj.__class__.__name__
318
+ return str(value) if value else None
319
+ except Exception:
320
+ return None
321
+
322
+
323
+ def _safe_get_object_id(obj: Any) -> Optional[str]:
324
+ for method_name in ("_Id", "Id", "%Id"):
325
+ try:
326
+ method = getattr(obj, method_name)
327
+ obj_id = method()
328
+ if obj_id:
329
+ return str(obj_id)
330
+ except Exception:
331
+ pass
332
+ return None
333
+
334
+
335
+ def _ensure_schema(msg_cls: type, iris_classname: str) -> None:
336
+ if not getattr(msg_cls, "_auto_sync", False):
337
+ return
338
+ if getattr(msg_cls, "_sync_mode", DEFAULT_SYNC_MODE) != DEFAULT_SYNC_MODE:
339
+ raise PersistentMessageError(
340
+ f"{get_python_classname(msg_cls)} has auto_sync=True but mode="
341
+ f"{getattr(msg_cls, '_sync_mode', None)!r}. Runtime auto-sync is only allowed "
342
+ "with mode='extend'."
343
+ )
344
+
345
+ key = (msg_cls, iris_classname)
346
+ if key in _AUTO_SYNCED:
347
+ return
348
+
349
+ _prepare_message_class(msg_cls, iris_classname, registered=False)
350
+ msg_cls.sync_schema()
351
+ _AUTO_SYNCED.add(key)
352
+
353
+
354
+ def _prepare_message_class(
355
+ msg_cls: type,
356
+ iris_classname: str,
357
+ *,
358
+ registered: bool,
359
+ ) -> None:
360
+ _apply_persistent_message_defaults(msg_cls)
361
+ msg_cls._classname = iris_classname
362
+ if registered:
363
+ msg_cls._iop_registered_classname = iris_classname
364
+ _set_message_parameters(msg_cls)
365
+ _cache_mapping(get_python_classname(msg_cls), iris_classname)
366
+
367
+
368
+ def _set_message_parameters(msg_cls: type) -> None:
369
+ parameters = dict(getattr(msg_cls, "_parameters", {}) or {})
370
+ parameters.update(
371
+ {
372
+ MESSAGE_KIND_PARAMETER: MESSAGE_KIND_VALUE,
373
+ PYTHON_CLASS_PARAMETER: get_python_classname(msg_cls),
374
+ PYTHON_CLASSPATH_PARAMETER: get_python_classpath(msg_cls),
375
+ }
376
+ )
377
+ msg_cls._parameters = parameters
378
+
379
+
380
+ def _cache_mapping(python_classname: str, iris_classname: str) -> None:
381
+ _PYTHON_TO_IRIS_CACHE[python_classname] = iris_classname
382
+ _IRIS_TO_PYTHON_CACHE[iris_classname] = python_classname
383
+ _IRIS_TO_PYTHON_STRICT_CACHE[iris_classname] = True
384
+
385
+
386
+ def _resolve_python_message_metadata(
387
+ iris_classname: str,
388
+ ) -> tuple[Optional[str], Optional[str], bool]:
389
+ cached = _IRIS_TO_PYTHON_CACHE.get(iris_classname)
390
+ if cached:
391
+ return (
392
+ cached,
393
+ _IRIS_TO_PYTHON_CLASSPATH_CACHE.get(iris_classname),
394
+ _IRIS_TO_PYTHON_STRICT_CACHE.get(iris_classname, True),
395
+ )
396
+
397
+ kind = get_iris_class_parameter(iris_classname, MESSAGE_KIND_PARAMETER)
398
+ python_classname = get_iris_class_parameter(iris_classname, PYTHON_CLASS_PARAMETER)
399
+ python_classpath = get_iris_class_parameter(iris_classname, PYTHON_CLASSPATH_PARAMETER)
400
+ strict = kind == MESSAGE_KIND_VALUE or bool(python_classname)
401
+
402
+ if not python_classname:
403
+ python_classname = iris_classname_to_python_classname(iris_classname)
404
+
405
+ if python_classname and strict:
406
+ _cache_mapping(python_classname, iris_classname)
407
+ if python_classpath:
408
+ _IRIS_TO_PYTHON_CLASSPATH_CACHE[iris_classname] = python_classpath
409
+
410
+ return python_classname, python_classpath, strict
411
+
412
+
413
+ def get_iris_class_parameter(
414
+ iris_classname: str,
415
+ parameter_name: str,
416
+ ) -> Optional[str]:
417
+ key = (iris_classname, parameter_name)
418
+ if key in _IRIS_PARAMETER_CACHE:
419
+ return _IRIS_PARAMETER_CACHE[key]
420
+
421
+ value = None
422
+ runtime = get_runtime()
423
+ for method_name in ("_GetParameter", "%GetParameter"):
424
+ try:
425
+ raw_value = runtime.call_classmethod(
426
+ iris_classname,
427
+ method_name,
428
+ parameter_name,
429
+ )
430
+ except Exception:
431
+ continue
432
+ if raw_value:
433
+ value = str(raw_value)
434
+ break
435
+
436
+ if value is not None:
437
+ _IRIS_PARAMETER_CACHE[key] = value
438
+ return value
439
+
440
+
441
+ def _is_missing_target_module(exc: ModuleNotFoundError, module_name: str) -> bool:
442
+ missing_name = getattr(exc, "name", None)
443
+ return bool(
444
+ missing_name
445
+ and (module_name == missing_name or module_name.startswith(f"{missing_name}."))
446
+ )
447
+
448
+
449
+ def _encode_iris_identifier(value: str) -> str:
450
+ if not value:
451
+ raise ValueError("IRIS classname parts cannot be empty")
452
+
453
+ encoded: list[str] = []
454
+ for char in value:
455
+ if char == "z":
456
+ encoded.append("zz")
457
+ elif char == "_":
458
+ encoded.append("zU")
459
+ elif char.isascii() and char.isalnum():
460
+ encoded.append(char)
461
+ else:
462
+ encoded.append(f"zX{ord(char):X}z")
463
+
464
+ return "".join(encoded)
465
+
466
+
467
+ def _decode_iris_identifier(value: str) -> str:
468
+ decoded: list[str] = []
469
+ index = 0
470
+ while index < len(value):
471
+ char = value[index]
472
+ if char != "z":
473
+ decoded.append(char)
474
+ index += 1
475
+ continue
476
+
477
+ if index + 1 >= len(value):
478
+ decoded.append(char)
479
+ index += 1
480
+ continue
481
+
482
+ marker = value[index + 1]
483
+ if marker == "z":
484
+ decoded.append("z")
485
+ index += 2
486
+ elif marker == "U":
487
+ decoded.append("_")
488
+ index += 2
489
+ elif marker == "X":
490
+ end = value.find("z", index + 2)
491
+ if end == -1:
492
+ decoded.append(char)
493
+ index += 1
494
+ continue
495
+ try:
496
+ decoded.append(chr(int(value[index + 2 : end], 16)))
497
+ except ValueError:
498
+ decoded.append(char)
499
+ index += 1
500
+ continue
501
+ index = end + 1
502
+ else:
503
+ decoded.append(char)
504
+ index += 1
505
+
506
+ return "".join(decoded)
507
+
508
+
509
+ def _prepend_sys_path(path: str) -> None:
510
+ normalized_path = os.path.abspath(os.path.normpath(path))
511
+ while normalized_path in sys.path:
512
+ sys.path.remove(normalized_path)
513
+ sys.path.insert(0, normalized_path)
iop/_utils.py CHANGED
@@ -13,6 +13,10 @@ from pydantic import TypeAdapter
13
13
 
14
14
  from . import _iris
15
15
  from ._message import _Message, _PydanticMessage
16
+ from ._persistent_message import (
17
+ is_persistent_message_class,
18
+ register_persistent_message_class,
19
+ )
16
20
 
17
21
  class _Utils():
18
22
  @staticmethod
@@ -84,6 +88,17 @@ class _Utils():
84
88
  """
85
89
  _Utils.raise_on_error(_iris.get_iris().cls('IOP.Message.JSONSchema').Import(schema_str,categories,schema_name))
86
90
 
91
+ @staticmethod
92
+ def register_persistent_message(msg_cls: type, iris_classname: str, sync_schema: bool = True):
93
+ """
94
+ Register a PersistentMessage as a native IRIS message body class.
95
+
96
+ :param msg_cls: PersistentMessage subclass to register
97
+ :param iris_classname: IRIS class name to generate, usually the CLASSES key
98
+ :param sync_schema: when True, sync the IRIS schema immediately
99
+ """
100
+ register_persistent_message_class(msg_cls, iris_classname, sync_schema=sync_schema)
101
+
87
102
  @staticmethod
88
103
  def get_python_settings() -> Tuple[str,str,str]:
89
104
  import iris_utils._cli
@@ -384,6 +399,9 @@ class _Utils():
384
399
  """
385
400
  for key, value in class_items.items():
386
401
  if inspect.isclass(value):
402
+ if is_persistent_message_class(value):
403
+ _Utils.register_persistent_message(value, key)
404
+ continue
387
405
  path = None
388
406
  if root_path:
389
407
  path = root_path
@@ -401,6 +419,10 @@ class _Utils():
401
419
  elif isinstance(value,dict):
402
420
  # if the dict has a key 'path' and a key 'module' and a key 'class'
403
421
  if 'path' in value and 'module' in value and 'class' in value:
422
+ msg_cls = _Utils._try_import_class(value['module'], value['class'], value['path'])
423
+ if msg_cls is not None and is_persistent_message_class(msg_cls):
424
+ _Utils.register_persistent_message(msg_cls, key)
425
+ continue
404
426
  # register the component
405
427
  _Utils.register_component(value['module'],value['class'],value['path'],1,key)
406
428
  # if the dict has a key 'path' and a key 'package'
@@ -418,6 +440,31 @@ class _Utils():
418
440
  else:
419
441
  raise ValueError(f"Invalid value for {key}.")
420
442
 
443
+ @staticmethod
444
+ def _try_import_class(module_name: str, class_name: str, path: str):
445
+ remove_path = False
446
+ try:
447
+ path = os.path.abspath(os.path.normpath(path))
448
+ fullpath = _Utils.guess_path(module_name, path)
449
+ if path not in sys.path:
450
+ sys.path.insert(0, path)
451
+ remove_path = True
452
+ else:
453
+ remove_path = False
454
+ try:
455
+ module = _Utils.import_module_from_path(module_name, fullpath)
456
+ except Exception:
457
+ module = importlib.import_module(module_name)
458
+ return getattr(module, class_name)
459
+ except Exception:
460
+ return None
461
+ finally:
462
+ try:
463
+ if remove_path:
464
+ sys.path.remove(path)
465
+ except Exception:
466
+ pass
467
+
421
468
  @staticmethod
422
469
  def set_productions_settings(production_list,root_path=None):
423
470
  """
@@ -582,4 +629,4 @@ class _Utils():
582
629
  module_path = module.replace(".", os.sep)
583
630
  else:
584
631
  module_path = module.replace(".", os.sep) + ".py"
585
- return os.path.join(path, module_path)
632
+ return os.path.join(path, module_path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris_pex_embedded_python
3
- Version: 3.6.1b1
3
+ Version: 3.7.1b1
4
4
  Summary: Iris Interoperability based on Embedded Python
5
5
  Author-email: grongier <guillaume.rongier@intersystems.com>
6
6
  License: MIT License
@@ -34,19 +34,17 @@ Classifier: Development Status :: 5 - Production/Stable
34
34
  Classifier: Intended Audience :: Developers
35
35
  Classifier: License :: OSI Approved :: MIT License
36
36
  Classifier: Operating System :: OS Independent
37
- Classifier: Programming Language :: Python :: 3.6
38
- Classifier: Programming Language :: Python :: 3.7
39
- Classifier: Programming Language :: Python :: 3.8
40
- Classifier: Programming Language :: Python :: 3.9
41
37
  Classifier: Programming Language :: Python :: 3.10
42
38
  Classifier: Programming Language :: Python :: 3.11
43
39
  Classifier: Programming Language :: Python :: 3.12
44
40
  Classifier: Topic :: Utilities
41
+ Requires-Python: >=3.10
45
42
  Description-Content-Type: text/markdown
46
43
  License-File: LICENSE
47
44
  Requires-Dist: pydantic>=2.0.0
48
45
  Requires-Dist: xmltodict>=0.12.0
49
46
  Requires-Dist: iris-embedded-python-wrapper>=0.0.6
47
+ Requires-Dist: iris-persistence>=0.1.0
50
48
  Requires-Dist: jsonpath-ng>=1.7.0
51
49
  Requires-Dist: debugpy>=1.8.0
52
50
  Requires-Dist: requests>=2.24.0
@@ -19,7 +19,7 @@ grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls,sha256=pcUgHgxX1pMH-wQ
19
19
  grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls,sha256=T3jNoR8RjKr1InQ6SgqBYTgFwpSB0Q60WholjbvForg,433
20
20
  grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls,sha256=zy30ZXXN4XcovPij-kOF3PuH1SkP1EUvlEJQRx2S9RU,431
21
21
  grongier/cls/Grongier/Service/WSGI.cls,sha256=7u2SsFmnsubMfdazvaDchKCM3yesPRMfKBzMIkwQ9xc,77
22
- grongier/pex/__init__.py,sha256=CPLDFa4dusvGX9VZYTUk-M0Xa_yR4e4Gqku1rIT75qo,1060
22
+ grongier/pex/__init__.py,sha256=Gs3S8F1xAXAGxDUFZHh9giJIKJF66Obsuc-jfhU3Rsg,1211
23
23
  grongier/pex/__main__.py,sha256=pQzVtkDhAeI6dpNRC632dVk2SGZZIEDwDufdgZe8VWs,98
24
24
  grongier/pex/_business_host.py,sha256=dlV8CWJad8Pr2TNfD9OjcVKaq5gEYQACZla1FK6-bDM,44
25
25
  grongier/pex/_cli.py,sha256=hOHz3n-aHtULuhdCkqZ_SSb3sv7M6j2WhRxgCTvgR9I,64
@@ -27,7 +27,7 @@ grongier/pex/_common.py,sha256=HZwG2C2-yB8yNN8kXhI6vxg8h-rROuEx38YOVFWIk1s,31
27
27
  grongier/pex/_director.py,sha256=pCmoiJ-sxe24yQaDz6ZFBsAnqU6fh57_dlew98B7rtE,35
28
28
  grongier/pex/_utils.py,sha256=gvsdr8WhWrE6smlsCxhoF14VUZfitrwqr5J57HkJhi4,29
29
29
  grongier/pex/wsgi/handlers.py,sha256=NrFLo_YbAh-x_PlWhAiWkQnUUN2Ss9HoEm63dDWCBpQ,2947
30
- iop/__init__.py,sha256=1C589HojSVK0yJf1KuTPA39ZjrOYO0QFLv45rqbZpA4,1183
30
+ iop/__init__.py,sha256=XA_tU3-KsFgMaweY2QSVOTWgj7NhRYmW7Mp-nobe-k0,1331
31
31
  iop/__main__.py,sha256=pQzVtkDhAeI6dpNRC632dVk2SGZZIEDwDufdgZe8VWs,98
32
32
  iop/_async_request.py,sha256=Umx79MHjIE5JV5exxCzKT9ZuJ3YhMHYwjeFyB4gIlU4,2324
33
33
  iop/_business_host.py,sha256=asX2z9Jfbwrs-B0TI2-JeXvSsYUMKUUlnJ4-kohZg8U,11280
@@ -40,7 +40,7 @@ iop/_debugpy.py,sha256=EJ3XK29cDQ1mBklEgUwG6HyHYJAmhVa3-8m_FvtbVOs,6008
40
40
  iop/_decorators.py,sha256=LpK0AK4GIzXbPFSIw_x6zzM3FwsHvFjgQUh-MnqtTE8,2322
41
41
  iop/_director.py,sha256=MVTCLIhxp1bYbDW7K-LEJ7A9fhAAnOZtDNE9pWK3tgE,11462
42
42
  iop/_director_protocol.py,sha256=Zn4vqN_N0SBSmo5S2LdI5BO0Fk1ZkZb5YVlNfi-i4wc,2783
43
- iop/_dispatch.py,sha256=eXElnLGLdc6ondZhTQtKfa7URMkT-QnmqOTwXBSXAsY,4650
43
+ iop/_dispatch.py,sha256=tnkbLvAcK2cH3wE3YvE1VpuiyMmeOhIxZ3Mipjlfklc,5028
44
44
  iop/_generator_request.py,sha256=I67JOjfsznN9JlS0bg_D05phcfSXqp6GJlCULJPXKvw,1284
45
45
  iop/_inbound_adapter.py,sha256=yG33VfJ2KxSDVxBTQTjFXqdX1fMEic1zxSAOhP5DqTk,1649
46
46
  iop/_iris.py,sha256=cw1mIKchXNlUJXSxwMhXYQr8DntJEO1hSPnLyJab10w,204
@@ -49,11 +49,12 @@ iop/_log_manager.py,sha256=SHY2AUBSjh-qZbEHfe0j4c_S8PuU6JFjjmjEX6qnoC4,3407
49
49
  iop/_message.py,sha256=iM7LXdhYRGOBEAJu-VH1m9iKyMYtrScWcALc3khmCFY,1465
50
50
  iop/_message_validator.py,sha256=ooDFWp8XvqJWP91RDbkFgpA5V3LbNrQO6IMx2vSjoF8,1516
51
51
  iop/_outbound_adapter.py,sha256=cN7dkZyx9ED89yUGePsUYsUhlR3ze3w1JorCG8HvDCw,723
52
+ iop/_persistent_message.py,sha256=hAlvzqqnYHaoDxoTu_F401TaE0Kus78q5Eae90XEuvA,16559
52
53
  iop/_private_session_duplex.py,sha256=c6Q0k-qnZi_JcIOdpUx1Edu44zVbUE2Kf2aCHM8Eq80,5202
53
54
  iop/_private_session_process.py,sha256=rvZFO6nWVwZtaEWJkSHyLTV-vhzDqQhsVi7INQLLwWI,1685
54
55
  iop/_remote.py,sha256=7Iz2WxmKBmzEP-Dfkxa7YVq2nrwwmEVquLhCuPeNbjg,17169
55
56
  iop/_serialization.py,sha256=mnLnEvkigWBAU2xIoAWW5laSn3W2xH_s7XemBZB0400,8560
56
- iop/_utils.py,sha256=qdj4pWOgNa2PjTyXShGrGrclbx88J_WJR8zNC-PNIFM,23679
57
+ iop/_utils.py,sha256=tqdlJnc05zw88n2usLgTJUQiamhHIB3CRvFoz3i-F2g,25625
57
58
  iop/cls/IOP/BusinessOperation.cls,sha256=NlvbNtm1ZFZmHaMX_9FMKoptY-hQMq5jYN1nLQwvYJw,936
58
59
  iop/cls/IOP/BusinessProcess.cls,sha256=XJxzbiV0xokzRm-iI2Be5UIJLE3MlXr7W3WS_LkOCYs,3363
59
60
  iop/cls/IOP/BusinessService.cls,sha256=fplKrbQgA7cQgjKIqDR2IK2iD1iNHmT-QvWrozhE4n4,1189
@@ -85,9 +86,9 @@ iop/cls/IOP/Service/WSGI.cls,sha256=VLNCXEwmHW9dBnE51uGE1nvGX6T4HjhqePT3LVhsjAE,
85
86
  iop/cls/IOP/Service/Remote/Handler.cls,sha256=JfsXse2jvoVvQfW8_rVEt2DCQJ9SVqReCcOUngOkpzE,938
86
87
  iop/cls/IOP/Service/Remote/Rest/v1.cls,sha256=DRaYNsbFmczdVltb-HMZvviy5EcjcJx7__zmnKANNm8,14429
87
88
  iop/wsgi/handlers.py,sha256=NrFLo_YbAh-x_PlWhAiWkQnUUN2Ss9HoEm63dDWCBpQ,2947
88
- iris_pex_embedded_python-3.6.1b1.dist-info/licenses/LICENSE,sha256=rZSiBFId_sfbJ6RL0GjjPX-InNLkNS9ou7eQsikciI8,1089
89
- iris_pex_embedded_python-3.6.1b1.dist-info/METADATA,sha256=CMA3iqi8GJIXimiVM13aXKYHXpkJqr0mFuKOpj_HjK0,4447
90
- iris_pex_embedded_python-3.6.1b1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
91
- iris_pex_embedded_python-3.6.1b1.dist-info/entry_points.txt,sha256=pj-i4LSDyiSP6xpHlVjMCbg1Pik7dC3_sdGY3Yp9Vhk,38
92
- iris_pex_embedded_python-3.6.1b1.dist-info/top_level.txt,sha256=4p0q6hCATmYIVMVi3I8hOUcJE1kwzyBeHygWv_rGvrU,13
93
- iris_pex_embedded_python-3.6.1b1.dist-info/RECORD,,
89
+ iris_pex_embedded_python-3.7.1b1.dist-info/licenses/LICENSE,sha256=rZSiBFId_sfbJ6RL0GjjPX-InNLkNS9ou7eQsikciI8,1089
90
+ iris_pex_embedded_python-3.7.1b1.dist-info/METADATA,sha256=zoK-1dOfnusytNLr4ijEbP_16LiUKqCMs3xB9mdPi14,4310
91
+ iris_pex_embedded_python-3.7.1b1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
92
+ iris_pex_embedded_python-3.7.1b1.dist-info/entry_points.txt,sha256=pj-i4LSDyiSP6xpHlVjMCbg1Pik7dC3_sdGY3Yp9Vhk,38
93
+ iris_pex_embedded_python-3.7.1b1.dist-info/top_level.txt,sha256=4p0q6hCATmYIVMVi3I8hOUcJE1kwzyBeHygWv_rGvrU,13
94
+ iris_pex_embedded_python-3.7.1b1.dist-info/RECORD,,