iris-pex-embedded-python 3.5.5b4__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 (91) hide show
  1. grongier/__init__.py +0 -0
  2. grongier/cls/Grongier/PEX/BusinessOperation.cls +8 -0
  3. grongier/cls/Grongier/PEX/BusinessProcess.cls +13 -0
  4. grongier/cls/Grongier/PEX/BusinessService.cls +8 -0
  5. grongier/cls/Grongier/PEX/Common.cls +10 -0
  6. grongier/cls/Grongier/PEX/Director.cls +10 -0
  7. grongier/cls/Grongier/PEX/Duplex/Operation.cls +4 -0
  8. grongier/cls/Grongier/PEX/Duplex/Process.cls +13 -0
  9. grongier/cls/Grongier/PEX/Duplex/Service.cls +4 -0
  10. grongier/cls/Grongier/PEX/InboundAdapter.cls +8 -0
  11. grongier/cls/Grongier/PEX/Message.cls +13 -0
  12. grongier/cls/Grongier/PEX/OutboundAdapter.cls +8 -0
  13. grongier/cls/Grongier/PEX/PickleMessage.cls +13 -0
  14. grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +8 -0
  15. grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +14 -0
  16. grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +14 -0
  17. grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +14 -0
  18. grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +14 -0
  19. grongier/cls/Grongier/PEX/Test.cls +10 -0
  20. grongier/cls/Grongier/PEX/Utils.cls +10 -0
  21. grongier/cls/Grongier/Service/WSGI.cls +4 -0
  22. grongier/pex/__init__.py +24 -0
  23. grongier/pex/__main__.py +4 -0
  24. grongier/pex/_business_host.py +1 -0
  25. grongier/pex/_cli.py +4 -0
  26. grongier/pex/_common.py +1 -0
  27. grongier/pex/_director.py +1 -0
  28. grongier/pex/_utils.py +1 -0
  29. grongier/pex/wsgi/handlers.py +104 -0
  30. iop/__init__.py +25 -0
  31. iop/__main__.py +4 -0
  32. iop/_async_request.py +67 -0
  33. iop/_business_host.py +256 -0
  34. iop/_business_operation.py +75 -0
  35. iop/_business_process.py +224 -0
  36. iop/_business_service.py +63 -0
  37. iop/_cli.py +247 -0
  38. iop/_common.py +334 -0
  39. iop/_debugpy.py +187 -0
  40. iop/_decorators.py +49 -0
  41. iop/_director.py +301 -0
  42. iop/_dispatch.py +136 -0
  43. iop/_generator_request.py +30 -0
  44. iop/_inbound_adapter.py +34 -0
  45. iop/_iris.py +8 -0
  46. iop/_log_manager.py +100 -0
  47. iop/_message.py +40 -0
  48. iop/_message_validator.py +49 -0
  49. iop/_outbound_adapter.py +23 -0
  50. iop/_private_session_duplex.py +103 -0
  51. iop/_private_session_process.py +41 -0
  52. iop/_remote.py +91 -0
  53. iop/_serialization.py +199 -0
  54. iop/_utils.py +671 -0
  55. iop/cls/IOP/BusinessOperation.cls +35 -0
  56. iop/cls/IOP/BusinessProcess.cls +156 -0
  57. iop/cls/IOP/BusinessService.cls +40 -0
  58. iop/cls/IOP/Common.cls +569 -0
  59. iop/cls/IOP/Director.cls +70 -0
  60. iop/cls/IOP/Duplex/Operation.cls +29 -0
  61. iop/cls/IOP/Duplex/Process.cls +229 -0
  62. iop/cls/IOP/Duplex/Service.cls +9 -0
  63. iop/cls/IOP/Generator/Message/Ack.cls +31 -0
  64. iop/cls/IOP/Generator/Message/Poll.cls +31 -0
  65. iop/cls/IOP/Generator/Message/Start.cls +15 -0
  66. iop/cls/IOP/Generator/Message/StartPickle.cls +15 -0
  67. iop/cls/IOP/Generator/Message/Stop.cls +32 -0
  68. iop/cls/IOP/InboundAdapter.cls +22 -0
  69. iop/cls/IOP/Message/JSONSchema.cls +125 -0
  70. iop/cls/IOP/Message.cls +754 -0
  71. iop/cls/IOP/OutboundAdapter.cls +36 -0
  72. iop/cls/IOP/PickleMessage.cls +58 -0
  73. iop/cls/IOP/PrivateSession/Duplex.cls +260 -0
  74. iop/cls/IOP/PrivateSession/Message/Ack.cls +32 -0
  75. iop/cls/IOP/PrivateSession/Message/Poll.cls +32 -0
  76. iop/cls/IOP/PrivateSession/Message/Start.cls +31 -0
  77. iop/cls/IOP/PrivateSession/Message/Stop.cls +48 -0
  78. iop/cls/IOP/Projection.cls +49 -0
  79. iop/cls/IOP/Service/Remote/Handler.cls +30 -0
  80. iop/cls/IOP/Service/Remote/Rest/v1.cls +97 -0
  81. iop/cls/IOP/Service/WSGI.cls +310 -0
  82. iop/cls/IOP/Test.cls +85 -0
  83. iop/cls/IOP/Utils.cls +503 -0
  84. iop/cls/IOP/Wrapper.cls +58 -0
  85. iop/wsgi/handlers.py +104 -0
  86. iris_pex_embedded_python-3.5.5b4.dist-info/METADATA +91 -0
  87. iris_pex_embedded_python-3.5.5b4.dist-info/RECORD +91 -0
  88. iris_pex_embedded_python-3.5.5b4.dist-info/WHEEL +5 -0
  89. iris_pex_embedded_python-3.5.5b4.dist-info/entry_points.txt +2 -0
  90. iris_pex_embedded_python-3.5.5b4.dist-info/licenses/LICENSE +21 -0
  91. iris_pex_embedded_python-3.5.5b4.dist-info/top_level.txt +2 -0
iop/_utils.py ADDED
@@ -0,0 +1,671 @@
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, Dict, Optional, Union, Tuple, TypedDict
10
+
11
+ import xmltodict
12
+ import requests
13
+ from pydantic import TypeAdapter
14
+
15
+ from . import _iris
16
+ from ._message import _Message, _PydanticMessage
17
+
18
+ class RemoteSettings(TypedDict, total=False):
19
+ """Typed dictionary for remote migration settings."""
20
+ url: str # Required: the host url to connect to
21
+ namespace: str # Optional: the namespace to use (default: 'USER')
22
+ package: str # Optional: the package to use (default: 'python')
23
+ remote_folder: str # Optional: the folder to use (default: '')
24
+ username: str # Optional: the username to use to connect (default: '')
25
+ password: str # Optional: the password to use to connect (default: '')
26
+ verify_ssl: bool # Optional: verify SSL certificates (default: True, set to False for self-signed certs)
27
+
28
+ class _Utils():
29
+ @staticmethod
30
+ def raise_on_error(sc):
31
+ """
32
+ If the status code is an error, raise an exception
33
+
34
+ :param sc: The status code returned by the Iris API
35
+ """
36
+ if _iris.get_iris().system.Status.IsError(sc):
37
+ raise RuntimeError(_iris.get_iris().system.Status.GetOneStatusText(sc))
38
+
39
+ @staticmethod
40
+ def setup(path:Optional[str] = None):
41
+
42
+ if path is None:
43
+ # get the path of the data folder with importlib.resources
44
+ try:
45
+ path = str(importlib.resources.files('iop').joinpath('cls'))
46
+ except ModuleNotFoundError:
47
+ path = None
48
+
49
+ if path:
50
+ _Utils.raise_on_error(_iris.get_iris().cls('%SYSTEM.OBJ').LoadDir(path,'cubk',"*.cls",1))
51
+
52
+ # for retrocompatibility load grongier.pex
53
+ try:
54
+ path = str(importlib.resources.files('grongier').joinpath('cls'))
55
+ except ModuleNotFoundError:
56
+ path = None
57
+
58
+ if path:
59
+ _Utils.raise_on_error(_iris.get_iris().cls('%SYSTEM.OBJ').LoadDir(path,'cubk',"*.cls",1))
60
+
61
+ @staticmethod
62
+ def register_message_schema(msg_cls: type):
63
+ """
64
+ It takes a class and registers the schema
65
+
66
+ :param cls: The class to register
67
+ """
68
+ if issubclass(msg_cls,_PydanticMessage):
69
+ schema = msg_cls.model_json_schema()
70
+ elif issubclass(msg_cls,_Message):
71
+ type_adapter = TypeAdapter(msg_cls)
72
+ schema = type_adapter.json_schema()
73
+ else:
74
+ raise ValueError("The class must be a subclass of _Message or _PydanticMessage")
75
+ schema_name = msg_cls.__module__ + '.' + msg_cls.__name__
76
+ schema_str = json.dumps(schema)
77
+ categories = schema_name
78
+ _Utils.register_schema(schema_name,schema_str,categories)
79
+
80
+ @staticmethod
81
+ def register_schema(schema_name:str, schema_str:str,categories:str):
82
+ """
83
+ It takes a schema name, a schema string, and a category string, and registers the schema
84
+
85
+ :param schema_name: The name of the schema
86
+ :type schema_name: str
87
+ :param schema_str: The schema as a string
88
+ :type schema_str: str
89
+ :param categories: The categories of the schema
90
+ :type categories: str
91
+ """
92
+ _Utils.raise_on_error(_iris.get_iris().cls('IOP.Message.JSONSchema').Import(schema_str,categories,schema_name))
93
+
94
+ @staticmethod
95
+ def get_python_settings() -> Tuple[str,str,str]:
96
+ import iris_utils._cli
97
+
98
+ pythonlib = iris_utils._cli.find_libpython()
99
+ pythonpath = _Utils._get_python_path()
100
+ pythonversion = sys.version[:4]
101
+
102
+ if not pythonlib:
103
+ pythonlib = ""
104
+
105
+ return pythonlib, pythonpath, pythonversion
106
+
107
+ @staticmethod
108
+ def _get_python_path() -> str:
109
+
110
+ if "VIRTUAL_ENV" in os.environ:
111
+ return os.path.join(
112
+ os.environ["VIRTUAL_ENV"],
113
+ "lib",
114
+ f"python{sys.version[:4]}",
115
+ "site-packages"
116
+ )
117
+ return ""
118
+
119
+ @staticmethod
120
+ def register_component(module:str,classname:str,path:str,overwrite:int=1,iris_classname:str='Python'):
121
+ """
122
+ It registers a component in the Iris database.
123
+
124
+ :param module: The name of the module that contains the class
125
+ :type module: str
126
+ :param classname: The name of the class you want to register
127
+ :type classname: str
128
+ :param path: The path to the component
129
+ :type path: str
130
+ :param overwrite: 0 = no, 1 = yes
131
+ :type overwrite: int
132
+ :param iris_classname: The name of the class in the Iris class hierarchy
133
+ :type iris_classname: str
134
+ :return: The return value is a string.
135
+ """
136
+ path = os.path.abspath(os.path.normpath(path))
137
+ fullpath = _Utils.guess_path(module,path)
138
+ pythonlib, pythonpath, pythonversion = _Utils.get_python_settings()
139
+ try:
140
+ _iris.get_iris().cls('IOP.Utils').dispatchRegisterComponent(module,classname,path,fullpath,overwrite,iris_classname,pythonlib,pythonpath,pythonversion)
141
+ except RuntimeError as e:
142
+ # New message error : Make sure the iop package is installed in iris
143
+ raise RuntimeError("Iris class : IOP.Utils not found. Make sure the iop package is installed in iris eg: iop --init.") from e
144
+
145
+ @staticmethod
146
+ def register_folder(path:str,overwrite:int=1,iris_package_name:str='Python'):
147
+ """
148
+ > This function takes a path to a folder, and registers all the Python files in that folder as IRIS
149
+ classes
150
+
151
+ :param path: the path to the folder containing the files you want to register
152
+ :type path: str
153
+ :param overwrite:
154
+ :type overwrite: int
155
+ :param iris_package_name: The name of the iris package you want to register the file to
156
+ :type iris_package_name: str
157
+ """
158
+ path = os.path.normpath(path)
159
+ # get the absolute path of the folder
160
+ path = os.path.abspath(path)
161
+ for filename in os.listdir(path):
162
+ if filename.endswith(".py"):
163
+ _Utils._register_file(filename, path, overwrite, iris_package_name)
164
+ else:
165
+ continue
166
+
167
+ @staticmethod
168
+ def register_file(file:str,overwrite:int=1,iris_package_name:str='Python'):
169
+ """
170
+ It takes a file name, a boolean to overwrite existing components, and the name of the Iris
171
+ package that the file is in. It then opens the file, parses it, and looks for classes that extend
172
+ BusinessOperation, BusinessProcess, or BusinessService. If it finds one, it calls register_component
173
+ with the module name, class name, path, overwrite boolean, and the full Iris package name
174
+
175
+ :param file: the name of the file containing the component
176
+ :type file: str
177
+ :param overwrite: if the component already exists, overwrite it
178
+ :type overwrite: int
179
+ :param iris_package_name: the name of the iris package that you want to register the components to
180
+ :type iris_package_name: str
181
+ """
182
+ head_tail = os.path.split(file)
183
+ return _Utils._register_file(head_tail[1],head_tail[0],overwrite,iris_package_name)
184
+
185
+ @staticmethod
186
+ def _register_file(filename:str,path:str,overwrite:int=1,iris_package_name:str='Python'):
187
+ """
188
+ It takes a file name, a path, a boolean to overwrite existing components, and the name of the Iris
189
+ package that the file is in. It then opens the file, parses it, and looks for classes that extend
190
+ BusinessOperation, BusinessProcess, or BusinessService. If it finds one, it calls register_component
191
+ with the module name, class name, path, overwrite boolean, and the full Iris package name
192
+
193
+ :param filename: the name of the file containing the component
194
+ :type filename: str
195
+ :param path: the path to the directory containing the files to be registered
196
+ :type path: str
197
+ :param overwrite: if the component already exists, overwrite it
198
+ :type overwrite: int
199
+ :param iris_package_name: the name of the iris package that you want to register the components to
200
+ :type iris_package_name: str
201
+ """
202
+ #pour chaque classe dans le module, appeler register_component
203
+ f = os.path.join(path,filename)
204
+ with open(f) as file:
205
+ node = ast.parse(file.read())
206
+ #list of class in the file
207
+ classes = [n for n in node.body if isinstance(n, ast.ClassDef)]
208
+ for klass in classes:
209
+ extend = ''
210
+ if len(klass.bases) == 1:
211
+ base = klass.bases[0]
212
+ if isinstance(base, ast.Name):
213
+ extend = base.id
214
+ elif isinstance(base, ast.Attribute):
215
+ extend = base.attr
216
+ if extend in ('BusinessOperation','BusinessProcess','BusinessService','DuplexService','DuplexProcess','DuplexOperation','InboundAdapter','OutboundAdapter'):
217
+ module = _Utils.filename_to_module(filename)
218
+ iris_class_name = f"{iris_package_name}.{module}.{klass.name}"
219
+ # strip "_" for iris class name
220
+ iris_class_name = iris_class_name.replace('_','')
221
+ _Utils.register_component(module, klass.name, path, overwrite, iris_class_name)
222
+ @staticmethod
223
+ def register_package(package:str,path:str,overwrite:int=1,iris_package_name:str='Python'):
224
+ """
225
+ It takes a package name, a path to the package, a flag to overwrite existing files, and the name of
226
+ the iris package to register the files to. It then loops through all the files in the package and
227
+ registers them to the iris package
228
+
229
+ :param package: the name of the package you want to register
230
+ :type package: str
231
+ :param path: the path to the directory containing the package
232
+ :type path: str
233
+ :param overwrite: 0 = don't overwrite, 1 = overwrite
234
+ :type overwrite: int
235
+ :param iris_package_name: The name of the package in the Iris package manager
236
+ :type iris_package_name: str
237
+ """
238
+ for filename in os.listdir(os.path.join(path,package)):
239
+ if filename.endswith(".py"):
240
+ _Utils._register_file(filename, os.path.join(path,package), overwrite, iris_package_name)
241
+ else:
242
+ continue
243
+
244
+ @staticmethod
245
+ def filename_to_module(filename) -> str:
246
+ """
247
+ It takes a filename and returns the module name
248
+
249
+ :param filename: The name of the file to be imported
250
+ :return: The module name
251
+ """
252
+ module = ''
253
+
254
+ path,file = os.path.split(filename)
255
+ mod = file.split('.')[0]
256
+ packages = path.replace(os.sep, ('.'))
257
+ if len(packages) >1:
258
+ module = packages+'.'+mod
259
+ else:
260
+ module = mod
261
+
262
+ return module
263
+
264
+ @staticmethod
265
+ def migrate_remote(filename=None, force_local=False):
266
+ """
267
+ Read a settings file from the filename
268
+ If the settings.py file has a key 'REMOTE_SETTINGS' then it will use the value of that key
269
+ as the remote host to connect to.
270
+ the REMOTE_SETTINGS is a RemoteSettings dictionary with the following keys:
271
+ * 'url': the host url to connect to (mandatory)
272
+ * 'namespace': the namespace to use (optional, default is 'USER')
273
+ * 'package': the package to use (optional, default is 'python')
274
+ * 'remote_folder': the folder to use (optional, default is '')
275
+ * 'username': the username to use to connect (optional, default is '')
276
+ * 'password': the password to use to connect (optional, default is '')
277
+ * 'verify_ssl': verify SSL certificates (optional, default is True)
278
+
279
+ The remote host is a rest API that will be used to register the components
280
+ The payload will be a json object with the following keys:
281
+ * 'namespace': the namespace to use
282
+ * 'package': the package to use
283
+ * 'body': the body of the request, it will be a json object with the following keys:
284
+ * 'name': name of the file
285
+ * 'data': the data of the file, it will be an UTF-8 encoded string
286
+
287
+ 'body' will be constructed with all the files in the folder if the folder is not empty else use root folder of settings.py
288
+
289
+ Args:
290
+ filename: Path to the settings file
291
+ force_local: If True, skip remote migration even if REMOTE_SETTINGS is present
292
+ """
293
+ settings, path = _Utils._load_settings(filename)
294
+ remote_settings: Optional[RemoteSettings] = getattr(settings, 'REMOTE_SETTINGS', None) if settings else None
295
+
296
+ if not remote_settings or force_local:
297
+ _Utils.migrate(filename)
298
+ return
299
+
300
+ # Validate required fields
301
+ if 'url' not in remote_settings:
302
+ raise ValueError("REMOTE_SETTINGS must contain 'url' field")
303
+
304
+ # prepare the payload with defaults
305
+ payload = {
306
+ 'namespace': remote_settings.get('namespace', 'USER'),
307
+ 'package': remote_settings.get('package', 'python'),
308
+ 'remote_folder': remote_settings.get('remote_folder', ''),
309
+ 'body': []
310
+ }
311
+
312
+ # get the folder to register
313
+ folder = _Utils._get_folder_path(filename, path)
314
+
315
+ # iterate over all files in the folder
316
+ for root, _, files in os.walk(folder):
317
+ for file in files:
318
+ if file.endswith('.py') or file.endswith('.cls'):
319
+ file_path = os.path.join(root, file)
320
+ relative_path = os.path.relpath(file_path, folder)
321
+ # Normalize path separators for cross-platform compatibility
322
+ relative_path = relative_path.replace(os.sep, '/')
323
+ with open(file_path, 'r', encoding='utf-8') as f:
324
+ data = f.read()
325
+ payload['body'].append({
326
+ 'name': relative_path,
327
+ 'data': data
328
+ })
329
+
330
+ # Get SSL verification setting (default to True for security)
331
+ verify_ssl = remote_settings.get('verify_ssl', True)
332
+
333
+ # Disable SSL warnings if verify_ssl is False
334
+ if not verify_ssl:
335
+ import urllib3
336
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
337
+
338
+ # send the request to the remote settings
339
+ try:
340
+ response = requests.put(
341
+ url=f"{remote_settings['url']}/api/iop/migrate",
342
+ json=payload,
343
+ headers={
344
+ 'Content-Type': 'application/json',
345
+ 'Accept': 'application/json'
346
+ },
347
+ auth=(remote_settings.get('username', ''), remote_settings.get('password', '')),
348
+ timeout=10,
349
+ verify=verify_ssl
350
+ )
351
+
352
+ print(f"Response from remote migration:\n{response.text}")
353
+
354
+ response.raise_for_status() # Raise an error for bad responses
355
+ except requests.exceptions.SSLError as e:
356
+ print(f"SSL Error: {e}")
357
+ print("If you're using a self-signed certificate, set 'verify_ssl': False in REMOTE_SETTINGS")
358
+ raise
359
+
360
+ @staticmethod
361
+ def migrate(filename=None):
362
+ """
363
+ Read the settings.py file and register all the components
364
+ settings.py file has two dictionaries:
365
+ * CLASSES
366
+ * key: the name of the class
367
+ * value: an instance of the class
368
+ * PRODUCTIONS
369
+ list of dictionaries:
370
+ * key: the name of the production
371
+ * value: a dictionary containing the settings for the production
372
+ * SCHEMAS
373
+ List of classes
374
+ """
375
+ settings, path = _Utils._load_settings(filename)
376
+
377
+ _Utils._register_settings_components(settings, path)
378
+
379
+ _Utils._cleanup_sys_path(path)
380
+
381
+ @staticmethod
382
+ def _load_settings(filename):
383
+ """Load settings module from file or default location.
384
+
385
+ Returns:
386
+ tuple: (settings_module, path_added_to_sys)
387
+ """
388
+ path_added = None
389
+
390
+ if filename:
391
+ # check if the filename is absolute or relative
392
+ if not os.path.isabs(filename):
393
+ raise ValueError("The filename must be absolute")
394
+
395
+ # add the path to the system path to the beginning
396
+ path_added = os.path.normpath(os.path.dirname(filename))
397
+ sys.path.insert(0, path_added)
398
+ # import settings from the specified file
399
+ settings = _Utils.import_module_from_path('settings', filename)
400
+ else:
401
+ # import settings from the settings module
402
+ import settings # type: ignore
403
+
404
+ return settings, path_added
405
+
406
+ @staticmethod
407
+ def _get_folder_path(filename, path_added_to_sys):
408
+ """Get the folder path for migration operations.
409
+
410
+ Args:
411
+ filename: Original filename parameter
412
+ path_added_to_sys: Path that was added to sys.path
413
+
414
+ Returns:
415
+ str: Folder path to use for migration
416
+ """
417
+ if filename:
418
+ return os.path.dirname(filename)
419
+ else:
420
+ return os.getcwd()
421
+
422
+ @staticmethod
423
+ def _register_settings_components(settings, path):
424
+ """Register all components from settings (classes, productions, schemas).
425
+
426
+ Args:
427
+ settings: Settings module containing CLASSES, PRODUCTIONS, SCHEMAS
428
+ path: Base path for component registration
429
+ """
430
+ # Use settings file location if path not provided
431
+ if not path:
432
+ path = os.path.dirname(inspect.getfile(settings))
433
+
434
+ try:
435
+ # set the classes settings
436
+ _Utils.set_classes_settings(settings.CLASSES, path)
437
+ except AttributeError:
438
+ print("No classes to register")
439
+
440
+ try:
441
+ # set the productions settings
442
+ _Utils.set_productions_settings(settings.PRODUCTIONS, path)
443
+ except AttributeError:
444
+ print("No productions to register")
445
+
446
+ try:
447
+ # set the schemas
448
+ for cls in settings.SCHEMAS:
449
+ _Utils.register_message_schema(cls)
450
+ except AttributeError:
451
+ print("No schemas to register")
452
+
453
+ @staticmethod
454
+ def _cleanup_sys_path(path):
455
+ """Remove path from sys.path if it was added.
456
+
457
+ Args:
458
+ path: Path to remove from sys.path
459
+ """
460
+ if path:
461
+ try:
462
+ sys.path.remove(os.path.normpath(path))
463
+ except ValueError:
464
+ pass
465
+
466
+ @staticmethod
467
+ def import_module_from_path(module_name, file_path):
468
+ if not os.path.isabs(file_path):
469
+ raise ValueError("The file path must be absolute")
470
+
471
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
472
+ if spec is None or spec.loader is None:
473
+ raise ImportError(f"Cannot find module named {module_name} at {file_path}")
474
+
475
+ module = importlib.util.module_from_spec(spec)
476
+ sys.modules[module_name] = module
477
+ spec.loader.exec_module(module)
478
+ return module
479
+
480
+ @staticmethod
481
+ def set_classes_settings(class_items,root_path=None):
482
+ """
483
+ It takes a dictionary of classes and returns a dictionary of settings for each class
484
+
485
+ :param class_items: a dictionary of classes
486
+ :return: a dictionary of settings for each class
487
+ """
488
+ for key, value in class_items.items():
489
+ if inspect.isclass(value):
490
+ path = None
491
+ if root_path:
492
+ path = root_path
493
+ else:
494
+ path = os.path.dirname(inspect.getfile(value))
495
+ _Utils.register_component(value.__module__,value.__name__,path,1,key)
496
+ elif inspect.ismodule(value):
497
+ path = None
498
+ if root_path:
499
+ path = root_path
500
+ else:
501
+ path = os.path.dirname(inspect.getfile(value))
502
+ _Utils._register_file(value.__name__+'.py',path,1,key)
503
+ # if the value is a dict
504
+ elif isinstance(value,dict):
505
+ # if the dict has a key 'path' and a key 'module' and a key 'class'
506
+ if 'path' in value and 'module' in value and 'class' in value:
507
+ # register the component
508
+ _Utils.register_component(value['module'],value['class'],value['path'],1,key)
509
+ # if the dict has a key 'path' and a key 'package'
510
+ elif 'path' in value and 'package' in value:
511
+ # register the package
512
+ _Utils.register_package(value['package'],value['path'],1,key)
513
+ # if the dict has a key 'path' and a key 'file'
514
+ elif 'path' in value and 'file' in value:
515
+ # register the file
516
+ _Utils._register_file(value['file'],value['path'],1,key)
517
+ # if the dict has a key 'path'
518
+ elif 'path' in value:
519
+ # register folder
520
+ _Utils.register_folder(value['path'],1,key)
521
+ else:
522
+ raise ValueError(f"Invalid value for {key}.")
523
+
524
+ @staticmethod
525
+ def set_productions_settings(production_list,root_path=None):
526
+ """
527
+ It takes a list of dictionaries and registers the productions
528
+ """
529
+ # for each production in the list
530
+ for production in production_list:
531
+ # get the production name (first key in the dictionary)
532
+ production_name = list(production.keys())[0]
533
+ # set the first key to 'production'
534
+ production['Production'] = production.pop(production_name)
535
+ # handle Items
536
+ production = _Utils.handle_items(production,root_path)
537
+ # transform the json as an xml
538
+ xml = _Utils.dict_to_xml(production)
539
+ # register the production
540
+ _Utils.register_production(production_name,xml)
541
+
542
+ @staticmethod
543
+ def handle_items(production,root_path=None):
544
+ # if an item is a class, register it and replace it with the name of the class
545
+ if 'Item' in production['Production']:
546
+ # for each item in the list
547
+ for i,item in enumerate(production['Production']['Item']):
548
+ # if the attribute "@ClassName" is a class, register it and replace it with the name of the class
549
+ if '@ClassName' in item:
550
+ if inspect.isclass(item['@ClassName']):
551
+ path = None
552
+ if root_path:
553
+ path = root_path
554
+ else:
555
+ path = os.path.dirname(inspect.getfile(item['@ClassName']))
556
+ _Utils.register_component(item['@ClassName'].__module__,item['@ClassName'].__name__,path,1,item['@Name'])
557
+ # replace the class with the name of the class
558
+ production['Production']['Item'][i]['@ClassName'] = item['@Name']
559
+ # if the attribute "@ClassName" is a dict
560
+ elif isinstance(item['@ClassName'],dict):
561
+ # create a new dict where the key is the name of the class and the value is the dict
562
+ class_dict = {item['@Name']:item['@ClassName']}
563
+ # pass the new dict to set_classes_settings
564
+ _Utils.set_classes_settings(class_dict)
565
+ # replace the class with the name of the class
566
+ production['Production']['Item'][i]['@ClassName'] = item['@Name']
567
+ else:
568
+ raise ValueError(f"Invalid value for {item['@Name']}.")
569
+
570
+ return production
571
+
572
+ @staticmethod
573
+ def dict_to_xml(json):
574
+ """
575
+ It takes a json and returns an xml
576
+
577
+ :param json: a json
578
+ :return: an xml
579
+ """
580
+ xml = xmltodict.unparse(json,pretty=True)
581
+ # remove the xml version tag
582
+ xml = xml.replace('<?xml version="1.0" encoding="utf-8"?>','')
583
+ # remove the new line at the beginning of the xml
584
+ xml = xml[1:]
585
+ return xml
586
+
587
+ @staticmethod
588
+ def register_production(production_name,xml):
589
+ """
590
+ It takes a production name and an xml and registers the production
591
+
592
+ :param production_name: the name of the production
593
+ :type production_name: str
594
+ :param xml: the xml of the production
595
+ :type xml: str
596
+ """
597
+ # split the production name in the package name and the production name
598
+ # the production name is the last part of the string
599
+ package = '.'.join(production_name.split('.')[:-1])
600
+ production_name = production_name.split('.')[-1]
601
+ stream = _Utils.string_to_stream(xml)
602
+ # register the production
603
+ _Utils.raise_on_error(_iris.get_iris().cls('IOP.Utils').CreateProduction(package,production_name,stream))
604
+
605
+ @staticmethod
606
+ def export_production(production_name):
607
+ """
608
+ It takes a production name and exports the production
609
+
610
+ :param production_name: the name of the production
611
+ :type production_name: str
612
+ """
613
+ def postprocessor(path, key, value):
614
+ if value is None:
615
+ return key, ''
616
+ return key, value
617
+ # export the production
618
+ xdata = _iris.get_iris().cls('IOP.Utils').ExportProduction(production_name)
619
+ # for each chunk of 1024 characters
620
+ string = _Utils.stream_to_string(xdata)
621
+ # convert the xml to a dictionary
622
+ data = xmltodict.parse(string,postprocessor=postprocessor)
623
+ # return the dictionary
624
+ return data
625
+
626
+ @staticmethod
627
+ def stream_to_string(stream,buffer=1000000)-> str:
628
+ string = ""
629
+ stream.Rewind()
630
+ while not stream.AtEnd:
631
+ string += stream.Read(buffer)
632
+ return string
633
+
634
+ @staticmethod
635
+ def string_to_stream(string:str,buffer=1000000):
636
+ stream = _iris.get_iris().cls('%Stream.GlobalCharacter')._New()
637
+ n = buffer
638
+ chunks = [string[i:i+n] for i in range(0, len(string), n)]
639
+ for chunk in chunks:
640
+ stream.Write(chunk)
641
+ return stream
642
+
643
+ @staticmethod
644
+ def guess_path(module: str, path: str) -> str:
645
+ """Determines the full file path for a given module.
646
+
647
+ Args:
648
+ module: Module name/path (e.g. 'foo.bar' or '.foo.bar')
649
+ path: Base directory path
650
+
651
+ Returns:
652
+ Full path to the module's .py file
653
+ """
654
+ if not module:
655
+ raise ValueError("Module name cannot be empty")
656
+
657
+ if module.startswith("."):
658
+ # Handle relative imports
659
+ dot_count = len(module) - len(module.lstrip("."))
660
+ module = module[dot_count:]
661
+
662
+ # Go up directory tree based on dot count
663
+ for _ in range(dot_count - 1):
664
+ path = os.path.dirname(path)
665
+
666
+ # Convert module path to file path
667
+ if module.endswith(".py"):
668
+ module_path = module.replace(".", os.sep)
669
+ else:
670
+ module_path = module.replace(".", os.sep) + ".py"
671
+ return os.path.join(path, module_path)