iris-pex-embedded-python 3.7.2b2__py3-none-any.whl → 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. iop/__init__.py +276 -18
  2. iop/__main__.py +3 -3
  3. iop/cli/__init__.py +1 -0
  4. iop/cli/formatting.py +45 -0
  5. iop/{_cli.py → cli/main.py} +238 -216
  6. iop/cli/parser.py +165 -0
  7. iop/cli/types.py +73 -0
  8. iop/cls/IOP/Common.cls +11 -2
  9. iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
  10. iop/cls/IOP/Service/Remote/Rest/v1.cls +152 -5
  11. iop/cls/IOP/Utils.cls +814 -7
  12. iop/cls/IOP/Wrapper.cls +1 -1
  13. iop/components/__init__.py +16 -0
  14. iop/{_async_request.py → components/async_request.py} +7 -7
  15. iop/components/business_host.py +363 -0
  16. iop/components/business_operation.py +84 -0
  17. iop/{_business_process.py → components/business_process.py} +145 -125
  18. iop/components/business_service.py +106 -0
  19. iop/{_common.py → components/common.py} +160 -164
  20. iop/{_debugpy.py → components/debugpy.py} +11 -11
  21. iop/{_generator_request.py → components/generator_request.py} +4 -4
  22. iop/{_inbound_adapter.py → components/inbound_adapter.py} +10 -18
  23. iop/{_log_manager.py → components/log_manager.py} +1 -1
  24. iop/{_outbound_adapter.py → components/outbound_adapter.py} +1 -1
  25. iop/components/polling_business_service.py +48 -0
  26. iop/{_private_session_duplex.py → components/private_session_duplex.py} +7 -9
  27. iop/{_private_session_process.py → components/private_session_process.py} +2 -2
  28. iop/{_settings.py → components/settings.py} +23 -3
  29. iop/messages/__init__.py +7 -0
  30. iop/messages/base.py +61 -0
  31. iop/{_decorators.py → messages/decorators.py} +8 -2
  32. iop/messages/dispatch.py +351 -0
  33. iop/{_persistent_message.py → messages/persistent.py} +24 -19
  34. iop/{_serialization.py → messages/serialization.py} +34 -21
  35. iop/{_message_validator.py → messages/validation.py} +15 -13
  36. iop/migration/__init__.py +1 -0
  37. iop/migration/io.py +61 -0
  38. iop/migration/manifest.py +534 -0
  39. iop/migration/plans.py +80 -0
  40. iop/migration/utils.py +1126 -0
  41. iop/production/__init__.py +55 -0
  42. iop/production/actions.py +525 -0
  43. iop/production/common.py +103 -0
  44. iop/production/component.py +234 -0
  45. iop/production/declarations.py +190 -0
  46. iop/production/declarative.py +214 -0
  47. iop/production/diff.py +327 -0
  48. iop/production/import_.py +296 -0
  49. iop/production/inspection.py +92 -0
  50. iop/production/model.py +1143 -0
  51. iop/production/planning.py +678 -0
  52. iop/production/reconstruction.py +341 -0
  53. iop/production/rendering.py +610 -0
  54. iop/production/runtime.py +91 -0
  55. iop/production/source_inference.py +729 -0
  56. iop/production/types.py +546 -0
  57. iop/production/validation.py +452 -0
  58. iop/runtime/__init__.py +1 -0
  59. iop/runtime/director.py +415 -0
  60. iop/runtime/environment.py +59 -0
  61. iop/{_iris.py → runtime/iris.py} +2 -2
  62. iop/runtime/local.py +145 -0
  63. iop/{_director_protocol.py → runtime/protocol.py} +25 -8
  64. iop/runtime/remote/__init__.py +12 -0
  65. iop/runtime/remote/client.py +90 -0
  66. iop/runtime/remote/director.py +283 -0
  67. iop/runtime/remote/migration.py +57 -0
  68. iop/runtime/remote/settings.py +69 -0
  69. iop/runtime/remote/setup.py +72 -0
  70. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA +11 -2
  71. iris_pex_embedded_python-4.0.0.dist-info/RECORD +99 -0
  72. iris_pex_embedded_python-4.0.0.dist-info/entry_points.txt +2 -0
  73. iris_pex_embedded_python-4.0.0.dist-info/top_level.txt +1 -0
  74. grongier/__init__.py +0 -0
  75. grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -8
  76. grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -13
  77. grongier/cls/Grongier/PEX/BusinessService.cls +0 -8
  78. grongier/cls/Grongier/PEX/Common.cls +0 -10
  79. grongier/cls/Grongier/PEX/Director.cls +0 -10
  80. grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -4
  81. grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -13
  82. grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -4
  83. grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -8
  84. grongier/cls/Grongier/PEX/Message.cls +0 -13
  85. grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -8
  86. grongier/cls/Grongier/PEX/PickleMessage.cls +0 -13
  87. grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -8
  88. grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -14
  89. grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -14
  90. grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -14
  91. grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -14
  92. grongier/cls/Grongier/PEX/Test.cls +0 -10
  93. grongier/cls/Grongier/PEX/Utils.cls +0 -10
  94. grongier/cls/Grongier/Service/WSGI.cls +0 -4
  95. grongier/pex/__init__.py +0 -75
  96. grongier/pex/__main__.py +0 -4
  97. grongier/pex/_business_host.py +0 -1
  98. grongier/pex/_cli.py +0 -4
  99. grongier/pex/_common.py +0 -1
  100. grongier/pex/_director.py +0 -1
  101. grongier/pex/_utils.py +0 -1
  102. grongier/pex/wsgi/handlers.py +0 -104
  103. iop/_business_host.py +0 -322
  104. iop/_business_operation.py +0 -77
  105. iop/_business_service.py +0 -67
  106. iop/_director.py +0 -333
  107. iop/_dispatch.py +0 -194
  108. iop/_local.py +0 -106
  109. iop/_message.py +0 -45
  110. iop/_polling_business_service.py +0 -6
  111. iop/_remote.py +0 -473
  112. iop/_utils.py +0 -912
  113. iop/cls/IOP/Service/WSGI.cls +0 -310
  114. iop/wsgi/handlers.py +0 -112
  115. iris_pex_embedded_python-3.7.2b2.dist-info/RECORD +0 -96
  116. iris_pex_embedded_python-3.7.2b2.dist-info/entry_points.txt +0 -2
  117. iris_pex_embedded_python-3.7.2b2.dist-info/top_level.txt +0 -2
  118. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/WHEEL +0 -0
  119. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/licenses/LICENSE +0 -0
iop/_utils.py DELETED
@@ -1,912 +0,0 @@
1
- import os
2
- import sys
3
- import importlib
4
- import importlib.util
5
- import importlib.resources
6
- import json
7
- import inspect
8
- import ast
9
- from typing import Any, Optional, Tuple
10
-
11
- import xmltodict
12
- from pydantic import TypeAdapter
13
-
14
- from . import _iris
15
- from ._message import _Message, _PydanticMessage
16
- from ._persistent_message import (
17
- is_persistent_message_class,
18
- register_persistent_message_class,
19
- )
20
-
21
-
22
- class _Utils:
23
- @staticmethod
24
- def raise_on_error(sc):
25
- """
26
- If the status code is an error, raise an exception
27
-
28
- :param sc: The status code returned by the Iris API
29
- """
30
- if _iris.get_iris().system.Status.IsError(sc):
31
- raise RuntimeError(_iris.get_iris().system.Status.GetOneStatusText(sc))
32
-
33
- @staticmethod
34
- def setup(path: Optional[str] = None):
35
-
36
- if path is None:
37
- # get the path of the data folder with importlib.resources
38
- try:
39
- path = str(importlib.resources.files("iop").joinpath("cls"))
40
- except ModuleNotFoundError:
41
- path = None
42
-
43
- if path:
44
- _Utils.raise_on_error(
45
- _iris.get_iris().cls("%SYSTEM.OBJ").LoadDir(path, "uk", "*.cls", 1)
46
- )
47
-
48
- # for retrocompatibility load grongier.pex
49
- try:
50
- path = str(importlib.resources.files("grongier").joinpath("cls"))
51
- except ModuleNotFoundError:
52
- path = None
53
-
54
- if path:
55
- _Utils.raise_on_error(
56
- _iris.get_iris().cls("%SYSTEM.OBJ").LoadDir(path, "uk", "*.cls", 1)
57
- )
58
-
59
- # compile loaded classes by package (avoids parallel worker crashes in IRIS 2026.1+)
60
- _Utils.raise_on_error(
61
- _iris.get_iris().cls("%SYSTEM.OBJ").Compile("IOP.*", "cb")
62
- )
63
- _Utils.raise_on_error(
64
- _iris.get_iris().cls("%SYSTEM.OBJ").Compile("Grongier.*", "cb")
65
- )
66
-
67
- @staticmethod
68
- def register_message_schema(msg_cls: type):
69
- """
70
- It takes a class and registers the schema
71
-
72
- :param cls: The class to register
73
- """
74
- if is_persistent_message_class(msg_cls):
75
- raise ValueError(
76
- f"{_Utils._python_classname(msg_cls)} is a PersistentMessage. "
77
- "Register it in CLASSES, not SCHEMAS."
78
- )
79
- if not inspect.isclass(msg_cls):
80
- raise ValueError("SCHEMAS entries must be message classes.")
81
- if issubclass(msg_cls, _PydanticMessage):
82
- schema = msg_cls.model_json_schema()
83
- elif issubclass(msg_cls, _Message):
84
- type_adapter = TypeAdapter(msg_cls)
85
- schema = type_adapter.json_schema()
86
- else:
87
- raise ValueError(
88
- f"{_Utils._python_classname(msg_cls)} cannot be registered as a "
89
- "DTL schema. Use a Message or PydanticMessage subclass."
90
- )
91
- schema_name = msg_cls.__module__ + "." + msg_cls.__name__
92
- schema_str = json.dumps(schema)
93
- categories = schema_name
94
- _Utils.register_schema(schema_name, schema_str, categories)
95
-
96
- @staticmethod
97
- def register_schema(schema_name: str, schema_str: str, categories: str):
98
- """
99
- It takes a schema name, a schema string, and a category string, and registers the schema
100
-
101
- :param schema_name: The name of the schema
102
- :type schema_name: str
103
- :param schema_str: The schema as a string
104
- :type schema_str: str
105
- :param categories: The categories of the schema
106
- :type categories: str
107
- """
108
- _Utils.raise_on_error(
109
- _iris.get_iris()
110
- .cls("IOP.Message.JSONSchema")
111
- .Import(schema_str, categories, schema_name)
112
- )
113
-
114
- @staticmethod
115
- def register_persistent_message(
116
- msg_cls: type, iris_classname: str, sync_schema: bool = True
117
- ):
118
- """
119
- Register a PersistentMessage as a native IRIS message body class.
120
-
121
- :param msg_cls: PersistentMessage subclass to register
122
- :param iris_classname: IRIS class name to generate, usually the CLASSES key
123
- :param sync_schema: when True, sync the IRIS schema immediately
124
- """
125
- register_persistent_message_class(
126
- msg_cls, iris_classname, sync_schema=sync_schema
127
- )
128
-
129
- @staticmethod
130
- def _python_classname(value: Any) -> str:
131
- module = getattr(value, "__module__", "")
132
- name = getattr(value, "__name__", repr(value))
133
- return f"{module}.{name}" if module else str(name)
134
-
135
- @staticmethod
136
- def _is_message_schema_class(value: Any) -> bool:
137
- try:
138
- return (
139
- inspect.isclass(value)
140
- and (issubclass(value, _Message) or issubclass(value, _PydanticMessage))
141
- and not is_persistent_message_class(value)
142
- )
143
- except TypeError:
144
- return False
145
-
146
- @staticmethod
147
- def get_python_settings() -> Tuple[str, str, str]:
148
- import iris_utils._cli
149
-
150
- pythonlib = iris_utils._cli.find_libpython()
151
- pythonpath = _Utils._get_python_path()
152
- pythonversion = sys.version[:4]
153
-
154
- if not pythonlib:
155
- pythonlib = ""
156
-
157
- return pythonlib, pythonpath, pythonversion
158
-
159
- @staticmethod
160
- def _get_python_path() -> str:
161
-
162
- if "VIRTUAL_ENV" in os.environ:
163
- return os.path.join(
164
- os.environ["VIRTUAL_ENV"],
165
- "lib",
166
- f"python{sys.version[:4]}",
167
- "site-packages",
168
- )
169
- return ""
170
-
171
- @staticmethod
172
- def register_component(
173
- module: str,
174
- classname: str,
175
- path: str,
176
- overwrite: int = 1,
177
- iris_classname: str = "Python",
178
- ):
179
- """
180
- It registers a component in the Iris database.
181
-
182
- :param module: The name of the module that contains the class
183
- :type module: str
184
- :param classname: The name of the class you want to register
185
- :type classname: str
186
- :param path: The path to the component
187
- :type path: str
188
- :param overwrite: 0 = no, 1 = yes
189
- :type overwrite: int
190
- :param iris_classname: The name of the class in the Iris class hierarchy
191
- :type iris_classname: str
192
- :return: The return value is a string.
193
- """
194
- path = os.path.abspath(os.path.normpath(path))
195
- fullpath = _Utils.guess_path(module, path)
196
- pythonlib, pythonpath, pythonversion = _Utils.get_python_settings()
197
- try:
198
- _iris.get_iris().cls("IOP.Utils").dispatchRegisterComponent(
199
- module,
200
- classname,
201
- path,
202
- fullpath,
203
- overwrite,
204
- iris_classname,
205
- pythonlib,
206
- pythonpath,
207
- pythonversion,
208
- )
209
- except RuntimeError as e:
210
- # New message error : Make sure the iop package is installed in iris
211
- raise RuntimeError(
212
- "Iris class : IOP.Utils not found. Make sure the iop package is installed in iris eg: iop --init."
213
- ) from e
214
-
215
- @staticmethod
216
- def register_folder(
217
- path: str, overwrite: int = 1, iris_package_name: str = "Python"
218
- ):
219
- """
220
- > This function takes a path to a folder, and registers all the Python files in that folder as IRIS
221
- classes
222
-
223
- :param path: the path to the folder containing the files you want to register
224
- :type path: str
225
- :param overwrite:
226
- :type overwrite: int
227
- :param iris_package_name: The name of the iris package you want to register the file to
228
- :type iris_package_name: str
229
- """
230
- path = os.path.normpath(path)
231
- # get the absolute path of the folder
232
- path = os.path.abspath(path)
233
- for filename in os.listdir(path):
234
- if filename.endswith(".py"):
235
- _Utils._register_file(filename, path, overwrite, iris_package_name)
236
- else:
237
- continue
238
-
239
- @staticmethod
240
- def register_file(file: str, overwrite: int = 1, iris_package_name: str = "Python"):
241
- """
242
- It takes a file name, a boolean to overwrite existing components, and the name of the Iris
243
- package that the file is in. It then opens the file, parses it, and looks for classes that extend
244
- BusinessOperation, BusinessProcess, or BusinessService. If it finds one, it calls register_component
245
- with the module name, class name, path, overwrite boolean, and the full Iris package name
246
-
247
- :param file: the name of the file containing the component
248
- :type file: str
249
- :param overwrite: if the component already exists, overwrite it
250
- :type overwrite: int
251
- :param iris_package_name: the name of the iris package that you want to register the components to
252
- :type iris_package_name: str
253
- """
254
- head_tail = os.path.split(file)
255
- return _Utils._register_file(
256
- head_tail[1], head_tail[0], overwrite, iris_package_name
257
- )
258
-
259
- @staticmethod
260
- def _register_file(
261
- filename: str, path: str, overwrite: int = 1, iris_package_name: str = "Python"
262
- ):
263
- """
264
- It takes a file name, a path, a boolean to overwrite existing components, and the name of the Iris
265
- package that the file is in. It then opens the file, parses it, and looks for classes that extend
266
- BusinessOperation, BusinessProcess, or BusinessService. If it finds one, it calls register_component
267
- with the module name, class name, path, overwrite boolean, and the full Iris package name
268
-
269
- :param filename: the name of the file containing the component
270
- :type filename: str
271
- :param path: the path to the directory containing the files to be registered
272
- :type path: str
273
- :param overwrite: if the component already exists, overwrite it
274
- :type overwrite: int
275
- :param iris_package_name: the name of the iris package that you want to register the components to
276
- :type iris_package_name: str
277
- """
278
- # pour chaque classe dans le module, appeler register_component
279
- f = os.path.join(path, filename)
280
- with open(f) as file:
281
- node = ast.parse(file.read())
282
- # list of class in the file
283
- classes = [n for n in node.body if isinstance(n, ast.ClassDef)]
284
- for klass in classes:
285
- extend = ""
286
- if len(klass.bases) == 1:
287
- base = klass.bases[0]
288
- if isinstance(base, ast.Name):
289
- extend = base.id
290
- elif isinstance(base, ast.Attribute):
291
- extend = base.attr
292
- if extend in (
293
- "BusinessOperation",
294
- "BusinessProcess",
295
- "BusinessService",
296
- "PollingBusinessService",
297
- "DuplexService",
298
- "DuplexProcess",
299
- "DuplexOperation",
300
- "InboundAdapter",
301
- "OutboundAdapter",
302
- ):
303
- module = _Utils.filename_to_module(filename)
304
- iris_class_name = f"{iris_package_name}.{module}.{klass.name}"
305
- # strip "_" for iris class name
306
- iris_class_name = iris_class_name.replace("_", "")
307
- _Utils.register_component(
308
- module, klass.name, path, overwrite, iris_class_name
309
- )
310
-
311
- @staticmethod
312
- def register_package(
313
- package: str, path: str, overwrite: int = 1, iris_package_name: str = "Python"
314
- ):
315
- """
316
- It takes a package name, a path to the package, a flag to overwrite existing files, and the name of
317
- the iris package to register the files to. It then loops through all the files in the package and
318
- registers them to the iris package
319
-
320
- :param package: the name of the package you want to register
321
- :type package: str
322
- :param path: the path to the directory containing the package
323
- :type path: str
324
- :param overwrite: 0 = don't overwrite, 1 = overwrite
325
- :type overwrite: int
326
- :param iris_package_name: The name of the package in the Iris package manager
327
- :type iris_package_name: str
328
- """
329
- for filename in os.listdir(os.path.join(path, package)):
330
- if filename.endswith(".py"):
331
- _Utils._register_file(
332
- filename, os.path.join(path, package), overwrite, iris_package_name
333
- )
334
- else:
335
- continue
336
-
337
- @staticmethod
338
- def filename_to_module(filename) -> str:
339
- """
340
- It takes a filename and returns the module name
341
-
342
- :param filename: The name of the file to be imported
343
- :return: The module name
344
- """
345
- module = ""
346
-
347
- path, file = os.path.split(filename)
348
- mod = file.split(".")[0]
349
- packages = path.replace(os.sep, ("."))
350
- if len(packages) > 1:
351
- module = packages + "." + mod
352
- else:
353
- module = mod
354
-
355
- return module
356
-
357
- @staticmethod
358
- def migrate(
359
- filename=None, mode: Optional[str] = None, namespace: Optional[str] = None
360
- ):
361
- """
362
- Read the settings.py file and register all the components
363
- settings.py file has two dictionaries:
364
- * CLASSES
365
- * key: the name of the class
366
- * value: an instance of the class
367
- * PRODUCTIONS
368
- list of dictionaries:
369
- * key: the name of the production
370
- * value: a dictionary containing the settings for the production
371
- * SCHEMAS
372
- List of classes
373
- """
374
- settings, path = _Utils._load_settings(filename)
375
-
376
- try:
377
- plan = _Utils._build_migration_plan(
378
- settings, path, filename, mode=mode, namespace=namespace
379
- )
380
- print(_Utils.format_migration_plan(plan))
381
- _Utils._register_settings_components(settings, path)
382
- print(
383
- _Utils.format_migration_success(
384
- filename or inspect.getfile(settings), namespace=namespace
385
- )
386
- )
387
- finally:
388
- _Utils._cleanup_sys_path(path)
389
-
390
- @staticmethod
391
- def explain_migration(
392
- filename=None, mode: Optional[str] = None, namespace: Optional[str] = None
393
- ):
394
- """Return a human-readable migration plan without writing to IRIS."""
395
- settings, path = _Utils._load_settings(filename)
396
- try:
397
- plan = _Utils._build_migration_plan(
398
- settings, path, filename, mode=mode, namespace=namespace
399
- )
400
- return _Utils.format_migration_plan(plan)
401
- finally:
402
- _Utils._cleanup_sys_path(path)
403
-
404
- @staticmethod
405
- def format_migration_success(filename, namespace: Optional[str] = None):
406
- suffix = f" in namespace {namespace}" if namespace else ""
407
- return f"Migration succeeded{suffix}: {filename}"
408
-
409
- @staticmethod
410
- def format_migration_plan(plan):
411
- """Format a migration plan for CLI and migration output."""
412
- lines = [f"Migration plan: {plan['settings']}"]
413
- if plan.get("mode"):
414
- lines.append(f"Mode: {plan['mode']}")
415
- if plan.get("namespace"):
416
- lines.append(f"Namespace: {plan['namespace']}")
417
- lines.append("")
418
- lines.extend(_Utils._format_plan_section("CLASSES", plan["classes"]))
419
- lines.extend(_Utils._format_plan_section("SCHEMAS", plan["schemas"]))
420
- lines.extend(_Utils._format_plan_section("PRODUCTIONS", plan["productions"]))
421
- return "\n".join(lines)
422
-
423
- @staticmethod
424
- def _format_plan_section(title, entries):
425
- lines = [f"{title}:"]
426
- if entries:
427
- lines.extend(f" {entry}" for entry in entries)
428
- else:
429
- lines.append(" none")
430
- lines.append("")
431
- return lines
432
-
433
- @staticmethod
434
- def _build_migration_plan(
435
- settings,
436
- path,
437
- filename=None,
438
- mode: Optional[str] = None,
439
- namespace: Optional[str] = None,
440
- ):
441
- """Build and validate a migration plan from a settings module."""
442
- if not path:
443
- path = os.path.dirname(inspect.getfile(settings))
444
-
445
- plan = {
446
- "settings": filename or inspect.getfile(settings),
447
- "mode": mode,
448
- "namespace": namespace,
449
- "classes": [],
450
- "schemas": [],
451
- "productions": [],
452
- }
453
-
454
- classes = getattr(settings, "CLASSES", {})
455
- if not isinstance(classes, dict):
456
- raise ValueError("CLASSES must be a dictionary.")
457
- for key, value in classes.items():
458
- kind, target = _Utils._classify_class_setting(value, path)
459
- if kind == "message_schema":
460
- schema_hint = value.__name__ if inspect.isclass(value) else target
461
- raise ValueError(
462
- f"{target} is a Message/PydanticMessage and cannot be registered "
463
- f"in CLASSES. Use SCHEMAS = [{schema_hint}] if you "
464
- "need DTL support. Otherwise, no migration is required for this "
465
- "message."
466
- )
467
- if kind == "persistent_message":
468
- plan["classes"].append(f"{key} -> {target} (PersistentMessage)")
469
- else:
470
- plan["classes"].append(f"{key} -> {target} (component)")
471
-
472
- schemas = getattr(settings, "SCHEMAS", None)
473
- if schemas is not None:
474
- if not isinstance(schemas, list):
475
- raise ValueError("SCHEMAS must be a list of message classes.")
476
- for cls in schemas:
477
- _Utils._validate_dtl_schema_class(cls, "SCHEMAS")
478
- plan["schemas"].append(_Utils._python_classname(cls))
479
-
480
- productions = getattr(settings, "PRODUCTIONS", None)
481
- if productions is not None:
482
- if not isinstance(productions, list):
483
- raise ValueError("PRODUCTIONS must be a list.")
484
- for production in productions:
485
- if not isinstance(production, dict) or not production:
486
- raise ValueError("Each PRODUCTION entry must be a non-empty dict.")
487
- plan["productions"].append(next(iter(production.keys())))
488
-
489
- return plan
490
-
491
- @staticmethod
492
- def _load_settings(filename):
493
- """Load settings module from file or default location.
494
-
495
- Returns:
496
- tuple: (settings_module, path_added_to_sys)
497
- """
498
- path_added = None
499
-
500
- if filename:
501
- # check if the filename is absolute or relative
502
- if not os.path.isabs(filename):
503
- raise ValueError("The filename must be absolute")
504
-
505
- # add the path to the system path to the beginning
506
- path_added = os.path.normpath(os.path.dirname(filename))
507
- sys.path.insert(0, path_added)
508
- # import settings from the specified file
509
- settings = _Utils.import_module_from_path("settings", filename)
510
- else:
511
- # import settings from the settings module
512
- import settings # type: ignore
513
-
514
- return settings, path_added
515
-
516
- @staticmethod
517
- def _get_folder_path(filename, path_added_to_sys):
518
- """Get the folder path for migration operations.
519
-
520
- Args:
521
- filename: Original filename parameter
522
- path_added_to_sys: Path that was added to sys.path
523
-
524
- Returns:
525
- str: Folder path to use for migration
526
- """
527
- if filename:
528
- return os.path.dirname(filename)
529
- else:
530
- return os.getcwd()
531
-
532
- @staticmethod
533
- def _classify_class_setting(value, root_path=None):
534
- if inspect.isclass(value):
535
- if is_persistent_message_class(value):
536
- return "persistent_message", _Utils._python_classname(value)
537
- if _Utils._is_message_schema_class(value):
538
- return "message_schema", _Utils._python_classname(value)
539
- return "component", _Utils._python_classname(value)
540
-
541
- if inspect.ismodule(value):
542
- return "component", f"{value.__name__}.*"
543
-
544
- if isinstance(value, dict):
545
- if "path" in value and "module" in value and "class" in value:
546
- cls = _Utils._try_import_class(
547
- value["module"], value["class"], value["path"]
548
- )
549
- target = f"{value['module']}.{value['class']}"
550
- if cls is not None:
551
- if is_persistent_message_class(cls):
552
- return "persistent_message", target
553
- if _Utils._is_message_schema_class(cls):
554
- return "message_schema", target
555
- return "component", target
556
- if "path" in value and "package" in value:
557
- return "component", f"{value['package']} package"
558
- if "path" in value and "file" in value:
559
- return "component", value["file"]
560
- if "path" in value:
561
- return "component", value["path"]
562
-
563
- raise ValueError(f"Invalid migration class entry: {value!r}.")
564
-
565
- @staticmethod
566
- def _validate_dtl_schema_class(cls, setting_name):
567
- if is_persistent_message_class(cls):
568
- raise ValueError(
569
- f"{_Utils._python_classname(cls)} is a PersistentMessage. Register it "
570
- "in CLASSES, not SCHEMAS."
571
- )
572
- if not _Utils._is_message_schema_class(cls):
573
- raise ValueError(
574
- f"{_Utils._python_classname(cls)} cannot be registered in "
575
- f"{setting_name}. Use a Message or PydanticMessage subclass."
576
- )
577
-
578
- @staticmethod
579
- def _register_settings_components(settings, path):
580
- """Register all components from settings (classes, productions, schemas).
581
-
582
- Args:
583
- settings: Settings module containing CLASSES, PRODUCTIONS, SCHEMAS
584
- path: Base path for component registration
585
- """
586
- # Use settings file location if path not provided
587
- if not path:
588
- path = os.path.dirname(inspect.getfile(settings))
589
-
590
- class_items = getattr(settings, "CLASSES", None)
591
- if class_items is not None:
592
- _Utils.set_classes_settings(class_items, path)
593
-
594
- try:
595
- # set the productions settings
596
- _Utils.set_productions_settings(settings.PRODUCTIONS, path)
597
- except AttributeError:
598
- pass
599
-
600
- schemas = getattr(settings, "SCHEMAS", None)
601
- if schemas is not None:
602
- if not isinstance(schemas, list):
603
- raise ValueError("SCHEMAS must be a list of message classes.")
604
- for cls in schemas:
605
- _Utils._validate_dtl_schema_class(cls, "SCHEMAS")
606
- _Utils.register_message_schema(cls)
607
-
608
- @staticmethod
609
- def _cleanup_sys_path(path):
610
- """Remove path from sys.path if it was added.
611
-
612
- Args:
613
- path: Path to remove from sys.path
614
- """
615
- if path:
616
- try:
617
- sys.path.remove(os.path.normpath(path))
618
- except ValueError:
619
- pass
620
-
621
- @staticmethod
622
- def import_module_from_path(module_name, file_path):
623
- if not os.path.isabs(file_path):
624
- raise ValueError("The file path must be absolute")
625
-
626
- spec = importlib.util.spec_from_file_location(module_name, file_path)
627
- if spec is None or spec.loader is None:
628
- raise ImportError(f"Cannot find module named {module_name} at {file_path}")
629
-
630
- module = importlib.util.module_from_spec(spec)
631
- sys.modules[module_name] = module
632
- spec.loader.exec_module(module)
633
- return module
634
-
635
- @staticmethod
636
- def set_classes_settings(class_items, root_path=None):
637
- """
638
- It takes a dictionary of classes and returns a dictionary of settings for each class
639
-
640
- :param class_items: a dictionary of classes
641
- :return: a dictionary of settings for each class
642
- """
643
- if not isinstance(class_items, dict):
644
- raise ValueError("CLASSES must be a dictionary.")
645
- for key, value in class_items.items():
646
- if inspect.isclass(value):
647
- if is_persistent_message_class(value):
648
- _Utils.register_persistent_message(value, key)
649
- continue
650
- if _Utils._is_message_schema_class(value):
651
- raise ValueError(
652
- f"{_Utils._python_classname(value)} is a Message/"
653
- "PydanticMessage and cannot be registered in CLASSES. "
654
- f"Use SCHEMAS = [{value.__name__}] if you need DTL "
655
- "support. Otherwise, no migration is required for this "
656
- "message."
657
- )
658
- path = None
659
- if root_path:
660
- path = root_path
661
- else:
662
- path = os.path.dirname(inspect.getfile(value))
663
- _Utils.register_component(
664
- value.__module__, value.__name__, path, 1, key
665
- )
666
- elif inspect.ismodule(value):
667
- path = None
668
- if root_path:
669
- path = root_path
670
- else:
671
- path = os.path.dirname(inspect.getfile(value))
672
- _Utils._register_file(value.__name__ + ".py", path, 1, key)
673
- # if the value is a dict
674
- elif isinstance(value, dict):
675
- # if the dict has a key 'path' and a key 'module' and a key 'class'
676
- if "path" in value and "module" in value and "class" in value:
677
- msg_cls = _Utils._try_import_class(
678
- value["module"], value["class"], value["path"]
679
- )
680
- if msg_cls is not None and is_persistent_message_class(msg_cls):
681
- _Utils.register_persistent_message(msg_cls, key)
682
- continue
683
- if msg_cls is not None and _Utils._is_message_schema_class(msg_cls):
684
- raise ValueError(
685
- f"{value['module']}.{value['class']} is a Message/"
686
- "PydanticMessage and cannot be registered in CLASSES. "
687
- "Use SCHEMAS if you need DTL "
688
- "support. Otherwise, no migration is required for this "
689
- "message."
690
- )
691
- # register the component
692
- _Utils.register_component(
693
- value["module"], value["class"], value["path"], 1, key
694
- )
695
- # if the dict has a key 'path' and a key 'package'
696
- elif "path" in value and "package" in value:
697
- # register the package
698
- _Utils.register_package(value["package"], value["path"], 1, key)
699
- # if the dict has a key 'path' and a key 'file'
700
- elif "path" in value and "file" in value:
701
- # register the file
702
- _Utils._register_file(value["file"], value["path"], 1, key)
703
- # if the dict has a key 'path'
704
- elif "path" in value:
705
- # register folder
706
- _Utils.register_folder(value["path"], 1, key)
707
- else:
708
- raise ValueError(f"Invalid value for {key}.")
709
-
710
- @staticmethod
711
- def _try_import_class(module_name: str, class_name: str, path: str):
712
- remove_path = False
713
- try:
714
- path = os.path.abspath(os.path.normpath(path))
715
- fullpath = _Utils.guess_path(module_name, path)
716
- if path not in sys.path:
717
- sys.path.insert(0, path)
718
- remove_path = True
719
- else:
720
- remove_path = False
721
- try:
722
- module = _Utils.import_module_from_path(module_name, fullpath)
723
- except Exception:
724
- module = importlib.import_module(module_name)
725
- return getattr(module, class_name)
726
- except Exception:
727
- return None
728
- finally:
729
- try:
730
- if remove_path:
731
- sys.path.remove(path)
732
- except Exception:
733
- pass
734
-
735
- @staticmethod
736
- def set_productions_settings(production_list, root_path=None):
737
- """
738
- It takes a list of dictionaries and registers the productions
739
- """
740
- # for each production in the list
741
- for production in production_list:
742
- # get the production name (first key in the dictionary)
743
- production_name = list(production.keys())[0]
744
- # set the first key to 'production'
745
- production["Production"] = production.pop(production_name)
746
- # handle Items
747
- production = _Utils.handle_items(production, root_path)
748
- # transform the json as an xml
749
- xml = _Utils.dict_to_xml(production)
750
- # register the production
751
- _Utils.register_production(production_name, xml)
752
-
753
- @staticmethod
754
- def handle_items(production, root_path=None):
755
- # if an item is a class, register it and replace it with the name of the class
756
- if "Item" in production["Production"]:
757
- # for each item in the list
758
- for i, item in enumerate(production["Production"]["Item"]):
759
- # if the attribute "@ClassName" is a class, register it and replace it with the name of the class
760
- if "@ClassName" in item:
761
- if inspect.isclass(item["@ClassName"]):
762
- path = None
763
- if root_path:
764
- path = root_path
765
- else:
766
- path = os.path.dirname(inspect.getfile(item["@ClassName"]))
767
- _Utils.register_component(
768
- item["@ClassName"].__module__,
769
- item["@ClassName"].__name__,
770
- path,
771
- 1,
772
- item["@Name"],
773
- )
774
- # replace the class with the name of the class
775
- production["Production"]["Item"][i]["@ClassName"] = item[
776
- "@Name"
777
- ]
778
- # if the attribute "@ClassName" is a dict
779
- elif isinstance(item["@ClassName"], dict):
780
- # create a new dict where the key is the name of the class and the value is the dict
781
- class_dict = {item["@Name"]: item["@ClassName"]}
782
- # pass the new dict to set_classes_settings
783
- _Utils.set_classes_settings(class_dict)
784
- # replace the class with the name of the class
785
- production["Production"]["Item"][i]["@ClassName"] = item["@Name"]
786
- else:
787
- raise ValueError(f"Invalid value for {item['@Name']}.")
788
-
789
- return production
790
-
791
- @staticmethod
792
- def dict_to_xml(json):
793
- """
794
- It takes a json and returns an xml
795
-
796
- :param json: a json
797
- :return: an xml
798
- """
799
- xml = xmltodict.unparse(json, pretty=True)
800
- # remove the xml version tag
801
- xml = xml.replace('<?xml version="1.0" encoding="utf-8"?>', "")
802
- # remove the new line at the beginning of the xml
803
- xml = xml[1:]
804
- return xml
805
-
806
- @staticmethod
807
- def register_production(production_name, xml):
808
- """
809
- It takes a production name and an xml and registers the production
810
-
811
- :param production_name: the name of the production
812
- :type production_name: str
813
- :param xml: the xml of the production
814
- :type xml: str
815
- """
816
- # split the production name in the package name and the production name
817
- # the production name is the last part of the string
818
- package = ".".join(production_name.split(".")[:-1])
819
- production_name = production_name.split(".")[-1]
820
- stream = _Utils.string_to_stream(xml)
821
- # register the production
822
- _Utils.raise_on_error(
823
- _iris.get_iris()
824
- .cls("IOP.Utils")
825
- .CreateProduction(package, production_name, stream)
826
- )
827
-
828
- @staticmethod
829
- def export_production(production_name):
830
- """
831
- It takes a production name and exports the production
832
-
833
- :param production_name: the name of the production
834
- :type production_name: str
835
- """
836
- # export the production
837
- xdata = _iris.get_iris().cls("IOP.Utils").ExportProduction(production_name)
838
- # for each chunk of 1024 characters
839
- string = _Utils.stream_to_string(xdata)
840
- # convert the xml to a dictionary
841
- return _Utils.xml_to_json(string)
842
-
843
- @staticmethod
844
- def xml_to_json(xml_string: str) -> str:
845
- """Convert an XML string to a JSON string using xmltodict.
846
-
847
- The top-level key is replaced with the production's ``Name`` attribute
848
- so callers get ``{"MyApp.Production": {...}}`` instead of
849
- ``{"Production": {...}}``.
850
-
851
- :param xml_string: Raw XML to convert
852
- :type xml_string: str
853
- :return: JSON string
854
- :rtype: str
855
- """
856
-
857
- def postprocessor(path, key, value):
858
- return key, "" if value is None else value
859
-
860
- data = xmltodict.parse(xml_string, postprocessor=postprocessor)
861
- if "Production" in data:
862
- production_obj = data["Production"]
863
- production_name = production_obj.get("@Name", "Production")
864
- data = {production_name: production_obj}
865
- return json.dumps(data)
866
-
867
- @staticmethod
868
- def stream_to_string(stream, buffer=1000000) -> str:
869
- string = ""
870
- stream.Rewind()
871
- while not stream.AtEnd:
872
- string += stream.Read(buffer)
873
- return string
874
-
875
- @staticmethod
876
- def string_to_stream(string: str, buffer=1000000):
877
- stream = _iris.get_iris().cls("%Stream.GlobalCharacter")._New()
878
- n = buffer
879
- chunks = [string[i : i + n] for i in range(0, len(string), n)]
880
- for chunk in chunks:
881
- stream.Write(chunk)
882
- return stream
883
-
884
- @staticmethod
885
- def guess_path(module: str, path: str) -> str:
886
- """Determines the full file path for a given module.
887
-
888
- Args:
889
- module: Module name/path (e.g. 'foo.bar' or '.foo.bar')
890
- path: Base directory path
891
-
892
- Returns:
893
- Full path to the module's .py file
894
- """
895
- if not module:
896
- raise ValueError("Module name cannot be empty")
897
-
898
- if module.startswith("."):
899
- # Handle relative imports
900
- dot_count = len(module) - len(module.lstrip("."))
901
- module = module[dot_count:]
902
-
903
- # Go up directory tree based on dot count
904
- for _ in range(dot_count - 1):
905
- path = os.path.dirname(path)
906
-
907
- # Convert module path to file path
908
- if module.endswith(".py"):
909
- module_path = module.replace(".", os.sep)
910
- else:
911
- module_path = module.replace(".", os.sep) + ".py"
912
- return os.path.join(path, module_path)