iris-pex-embedded-python 3.7.2b2__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iop/__init__.py +276 -18
- iop/__main__.py +3 -3
- iop/cli/__init__.py +1 -0
- iop/cli/formatting.py +45 -0
- iop/{_cli.py → cli/main.py} +238 -216
- iop/cli/parser.py +165 -0
- iop/cli/types.py +73 -0
- iop/cls/IOP/Common.cls +11 -2
- iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
- iop/cls/IOP/Service/Remote/Rest/v1.cls +152 -5
- iop/cls/IOP/Utils.cls +814 -7
- iop/cls/IOP/Wrapper.cls +1 -1
- iop/components/__init__.py +16 -0
- iop/{_async_request.py → components/async_request.py} +7 -7
- iop/components/business_host.py +363 -0
- iop/components/business_operation.py +84 -0
- iop/{_business_process.py → components/business_process.py} +145 -125
- iop/components/business_service.py +106 -0
- iop/{_common.py → components/common.py} +160 -164
- iop/{_debugpy.py → components/debugpy.py} +11 -11
- iop/{_generator_request.py → components/generator_request.py} +4 -4
- iop/{_inbound_adapter.py → components/inbound_adapter.py} +10 -18
- iop/{_log_manager.py → components/log_manager.py} +1 -1
- iop/{_outbound_adapter.py → components/outbound_adapter.py} +1 -1
- iop/components/polling_business_service.py +48 -0
- iop/{_private_session_duplex.py → components/private_session_duplex.py} +7 -9
- iop/{_private_session_process.py → components/private_session_process.py} +2 -2
- iop/{_settings.py → components/settings.py} +23 -3
- iop/messages/__init__.py +7 -0
- iop/messages/base.py +61 -0
- iop/{_decorators.py → messages/decorators.py} +8 -2
- iop/messages/dispatch.py +351 -0
- iop/{_persistent_message.py → messages/persistent.py} +24 -19
- iop/{_serialization.py → messages/serialization.py} +34 -21
- iop/{_message_validator.py → messages/validation.py} +15 -13
- iop/migration/__init__.py +1 -0
- iop/migration/io.py +61 -0
- iop/migration/manifest.py +534 -0
- iop/migration/plans.py +80 -0
- iop/migration/utils.py +1126 -0
- iop/production/__init__.py +55 -0
- iop/production/actions.py +525 -0
- iop/production/common.py +103 -0
- iop/production/component.py +234 -0
- iop/production/declarations.py +190 -0
- iop/production/declarative.py +214 -0
- iop/production/diff.py +327 -0
- iop/production/import_.py +296 -0
- iop/production/inspection.py +92 -0
- iop/production/model.py +1143 -0
- iop/production/planning.py +678 -0
- iop/production/reconstruction.py +341 -0
- iop/production/rendering.py +610 -0
- iop/production/runtime.py +91 -0
- iop/production/source_inference.py +729 -0
- iop/production/types.py +546 -0
- iop/production/validation.py +452 -0
- iop/runtime/__init__.py +1 -0
- iop/runtime/director.py +415 -0
- iop/runtime/environment.py +59 -0
- iop/{_iris.py → runtime/iris.py} +2 -2
- iop/runtime/local.py +145 -0
- iop/{_director_protocol.py → runtime/protocol.py} +25 -8
- iop/runtime/remote/__init__.py +12 -0
- iop/runtime/remote/client.py +90 -0
- iop/runtime/remote/director.py +283 -0
- iop/runtime/remote/migration.py +57 -0
- iop/runtime/remote/settings.py +69 -0
- iop/runtime/remote/setup.py +72 -0
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA +11 -2
- iris_pex_embedded_python-4.0.0.dist-info/RECORD +99 -0
- iris_pex_embedded_python-4.0.0.dist-info/entry_points.txt +2 -0
- iris_pex_embedded_python-4.0.0.dist-info/top_level.txt +1 -0
- grongier/__init__.py +0 -0
- grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -8
- grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -13
- grongier/cls/Grongier/PEX/BusinessService.cls +0 -8
- grongier/cls/Grongier/PEX/Common.cls +0 -10
- grongier/cls/Grongier/PEX/Director.cls +0 -10
- grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -4
- grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -13
- grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -4
- grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/Message.cls +0 -13
- grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/PickleMessage.cls +0 -13
- grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -8
- grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -14
- grongier/cls/Grongier/PEX/Test.cls +0 -10
- grongier/cls/Grongier/PEX/Utils.cls +0 -10
- grongier/cls/Grongier/Service/WSGI.cls +0 -4
- grongier/pex/__init__.py +0 -75
- grongier/pex/__main__.py +0 -4
- grongier/pex/_business_host.py +0 -1
- grongier/pex/_cli.py +0 -4
- grongier/pex/_common.py +0 -1
- grongier/pex/_director.py +0 -1
- grongier/pex/_utils.py +0 -1
- grongier/pex/wsgi/handlers.py +0 -104
- iop/_business_host.py +0 -322
- iop/_business_operation.py +0 -77
- iop/_business_service.py +0 -67
- iop/_director.py +0 -333
- iop/_dispatch.py +0 -194
- iop/_local.py +0 -106
- iop/_message.py +0 -45
- iop/_polling_business_service.py +0 -6
- iop/_remote.py +0 -473
- iop/_utils.py +0 -912
- iop/cls/IOP/Service/WSGI.cls +0 -310
- iop/wsgi/handlers.py +0 -112
- iris_pex_embedded_python-3.7.2b2.dist-info/RECORD +0 -96
- iris_pex_embedded_python-3.7.2b2.dist-info/entry_points.txt +0 -2
- iris_pex_embedded_python-3.7.2b2.dist-info/top_level.txt +0 -2
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/WHEEL +0 -0
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/licenses/LICENSE +0 -0
iop/_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)
|