iris-pex-embedded-python 2.3.27b2__py3-none-any.whl → 3.2.1b3__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.

Potentially problematic release.


This version of iris-pex-embedded-python might be problematic. Click here for more details.

Files changed (85) hide show
  1. grongier/cls/Grongier/PEX/BusinessOperation.cls +1 -28
  2. grongier/cls/Grongier/PEX/BusinessProcess.cls +1 -100
  3. grongier/cls/Grongier/PEX/BusinessService.cls +1 -28
  4. grongier/cls/Grongier/PEX/Common.cls +1 -194
  5. grongier/cls/Grongier/PEX/Director.cls +1 -48
  6. grongier/cls/Grongier/PEX/Duplex/Operation.cls +1 -26
  7. grongier/cls/Grongier/PEX/Duplex/Process.cls +1 -217
  8. grongier/cls/Grongier/PEX/Duplex/Service.cls +1 -6
  9. grongier/cls/Grongier/PEX/InboundAdapter.cls +1 -15
  10. grongier/cls/Grongier/PEX/Message.cls +1 -116
  11. grongier/cls/Grongier/PEX/OutboundAdapter.cls +1 -29
  12. grongier/cls/Grongier/PEX/PickleMessage.cls +1 -46
  13. grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +1 -253
  14. grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +1 -19
  15. grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +1 -19
  16. grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +1 -19
  17. grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +1 -35
  18. grongier/cls/Grongier/PEX/Test.cls +1 -53
  19. grongier/cls/Grongier/PEX/Utils.cls +1 -365
  20. grongier/cls/Grongier/Service/WSGI.cls +1 -307
  21. grongier/pex/__init__.py +11 -11
  22. grongier/pex/__main__.py +1 -1
  23. grongier/pex/_business_host.py +1 -511
  24. grongier/pex/_cli.py +2 -150
  25. grongier/pex/_common.py +1 -347
  26. grongier/pex/_director.py +1 -286
  27. grongier/pex/_utils.py +1 -369
  28. intersystems_iris/_ConnectionInformation.py +22 -20
  29. intersystems_iris/dbapi/_DBAPI.py +6 -1
  30. intersystems_iris/dbapi/_ResultSetRow.py +26 -15
  31. intersystems_iris/dbapi/preparser/_PreParser.py +4 -1
  32. iop/__init__.py +24 -0
  33. iop/__main__.py +4 -0
  34. iop/_business_host.py +675 -0
  35. iop/_business_operation.py +71 -0
  36. iop/_business_process.py +220 -0
  37. {grongier/pex → iop}/_business_service.py +2 -2
  38. iop/_cli.py +196 -0
  39. iop/_common.py +352 -0
  40. iop/_director.py +301 -0
  41. {grongier/pex → iop}/_inbound_adapter.py +1 -1
  42. iop/_log_manager.py +81 -0
  43. {grongier/pex → iop}/_message.py +1 -1
  44. {grongier/pex → iop}/_outbound_adapter.py +1 -1
  45. {grongier/pex → iop}/_private_session_duplex.py +3 -2
  46. {grongier/pex → iop}/_private_session_process.py +2 -2
  47. iop/_utils.py +458 -0
  48. iop/cls/IOP/BusinessOperation.cls +35 -0
  49. iop/cls/IOP/BusinessProcess.cls +124 -0
  50. iop/cls/IOP/BusinessService.cls +35 -0
  51. iop/cls/IOP/Common.cls +344 -0
  52. iop/cls/IOP/Director.cls +62 -0
  53. iop/cls/IOP/Duplex/Operation.cls +29 -0
  54. iop/cls/IOP/Duplex/Process.cls +229 -0
  55. iop/cls/IOP/Duplex/Service.cls +9 -0
  56. iop/cls/IOP/InboundAdapter.cls +22 -0
  57. iop/cls/IOP/Message/JSONSchema.cls +125 -0
  58. iop/cls/IOP/Message.cls +729 -0
  59. iop/cls/IOP/OutboundAdapter.cls +36 -0
  60. iop/cls/IOP/PickleMessage.cls +58 -0
  61. iop/cls/IOP/PrivateSession/Duplex.cls +260 -0
  62. iop/cls/IOP/PrivateSession/Message/Ack.cls +32 -0
  63. iop/cls/IOP/PrivateSession/Message/Poll.cls +32 -0
  64. iop/cls/IOP/PrivateSession/Message/Start.cls +32 -0
  65. iop/cls/IOP/PrivateSession/Message/Stop.cls +48 -0
  66. iop/cls/IOP/Service/WSGI.cls +310 -0
  67. iop/cls/IOP/Test.cls +85 -0
  68. iop/cls/IOP/Utils.cls +378 -0
  69. iop/wsgi/handlers.py +104 -0
  70. iris_pex_embedded_python-3.2.1b3.dist-info/METADATA +90 -0
  71. iris_pex_embedded_python-3.2.1b3.dist-info/RECORD +139 -0
  72. {iris_pex_embedded_python-2.3.27b2.dist-info → iris_pex_embedded_python-3.2.1b3.dist-info}/WHEEL +1 -1
  73. iris_pex_embedded_python-3.2.1b3.dist-info/entry_points.txt +2 -0
  74. {iris_pex_embedded_python-2.3.27b2.dist-info → iris_pex_embedded_python-3.2.1b3.dist-info}/top_level.txt +1 -1
  75. grongier/pex/_business_operation.py +0 -70
  76. grongier/pex/_business_process.py +0 -215
  77. iris/__init__.py +0 -60
  78. iris/__init__.pyi +0 -236
  79. iris/iris_ipm.py +0 -40
  80. iris/iris_ipm.pyi +0 -17
  81. iris_pex_embedded_python-2.3.27b2.dist-info/METADATA +0 -1384
  82. iris_pex_embedded_python-2.3.27b2.dist-info/RECORD +0 -113
  83. iris_pex_embedded_python-2.3.27b2.dist-info/entry_points.txt +0 -2
  84. {grongier/pex → iop}/_pickle_message.py +0 -0
  85. {iris_pex_embedded_python-2.3.27b2.dist-info → iris_pex_embedded_python-3.2.1b3.dist-info}/LICENSE +0 -0
iop/_log_manager.py ADDED
@@ -0,0 +1,81 @@
1
+ import iris
2
+ import logging
3
+ from typing import Optional, Tuple
4
+
5
+ class LogManager:
6
+ """Manages logging integration between Python's logging module and IRIS."""
7
+
8
+ @staticmethod
9
+ def get_logger(class_name: str, method_name: Optional[str] = None, to_console: bool = False) -> logging.Logger:
10
+ """Get a logger instance configured for IRIS integration.
11
+
12
+ Args:
13
+ class_name: Name of the class logging the message
14
+ method_name: Optional name of the method logging the message
15
+ to_console: If True, log to the console instead of IRIS
16
+
17
+ Returns:
18
+ Logger instance configured for IRIS integration
19
+ """
20
+ logger = logging.getLogger(f"{class_name}.{method_name}" if method_name else class_name)
21
+
22
+ # Only add handler if none exists
23
+ if not logger.handlers:
24
+ handler = IRISLogHandler(class_name, method_name, to_console)
25
+ formatter = logging.Formatter('%(message)s')
26
+ handler.setFormatter(formatter)
27
+ logger.addHandler(handler)
28
+ # Set the log level to the lowest level to ensure all messages are sent to IRIS
29
+ logger.setLevel(logging.DEBUG)
30
+
31
+ return logger
32
+
33
+ class IRISLogHandler(logging.Handler):
34
+ """Custom logging handler that routes Python logs to IRIS logging system."""
35
+
36
+ def __init__(self, class_name: str, method_name: Optional[str] = None, to_console: bool = False):
37
+ """Initialize the handler with context information.
38
+
39
+ Args:
40
+ class_name: Name of the class logging the message
41
+ method_name: Optional name of the method logging the message
42
+ console: If True, log to the console instead of IRIS
43
+ """
44
+ super().__init__()
45
+ self.class_name = class_name
46
+ self.method_name = method_name
47
+ self.to_console = to_console
48
+
49
+ def format(self, record: logging.LogRecord) -> str:
50
+ """Format the log record into a string.
51
+
52
+ Args:
53
+ record: The logging record to format
54
+
55
+ Returns:
56
+ Formatted log message
57
+ """
58
+ if self.to_console:
59
+ return f"{record}"
60
+ return record.getMessage()
61
+
62
+ def emit(self, record: logging.LogRecord) -> None:
63
+ """Route the log record to appropriate IRIS logging method.
64
+
65
+ Args:
66
+ record: The logging record to emit
67
+ """
68
+ # Map Python logging levels to IRIS logging methods
69
+ level_map = {
70
+ logging.DEBUG: iris.cls("Ens.Util.Log").LogTrace,
71
+ logging.INFO: iris.cls("Ens.Util.Log").LogInfo,
72
+ logging.WARNING: iris.cls("Ens.Util.Log").LogWarning,
73
+ logging.ERROR: iris.cls("Ens.Util.Log").LogError,
74
+ logging.CRITICAL: iris.cls("Ens.Util.Log").LogAlert,
75
+ }
76
+
77
+ log_func = level_map.get(record.levelno, iris.cls("Ens.Util.Log").LogInfo)
78
+ if self.to_console or (hasattr(record, "to_console") and record.to_console):
79
+ iris.cls("%SYS.System").WriteToConsoleLog(self.format(record),0,0,"IoP.Log")
80
+ else:
81
+ log_func(self.class_name, self.method_name, self.format(record))
@@ -1,6 +1,6 @@
1
1
  class _Message:
2
2
  """ The abstract class that is the superclass for persistent messages sent from one component to another.
3
3
  This class has no properties or methods. Users subclass Message and add properties.
4
- The PEX framework provides the persistence to objects derived from the Message class.
4
+ The IOP framework provides the persistence to objects derived from the Message class.
5
5
  """
6
6
  pass
@@ -1,4 +1,4 @@
1
- from grongier.pex._common import _Common
1
+ from iop._common import _Common
2
2
 
3
3
  class _OutboundAdapter(_Common):
4
4
  """ Responsible for sending the data to the external system."""
@@ -1,5 +1,5 @@
1
1
  import importlib
2
- from grongier.pex._business_host import _BusinessHost
2
+ from iop._business_host import _BusinessHost
3
3
 
4
4
  class _PrivateSessionDuplex(_BusinessHost):
5
5
 
@@ -31,7 +31,8 @@ class _PrivateSessionDuplex(_BusinessHost):
31
31
  """ For internal use only. """
32
32
  self.iris_handle = handle_current
33
33
  if type(handle_partner).__module__.find('iris') == 0:
34
- if handle_partner._IsA("Grongier.PEX.InboundAdapter") or handle_partner._IsA("Grongier.PEX.OutboundAdapter"):
34
+ if (handle_partner._IsA("Grongier.PEX.InboundAdapter") or handle_partner._IsA("Grongier.PEX.OutboundAdapter")
35
+ or handle_partner._IsA("IOP.InboundAdapter") or handle_partner._IsA("IOP.OutboundAdapter")):
35
36
  module = importlib.import_module(handle_partner.GetModule())
36
37
  handle_partner = getattr(module, handle_partner.GetClassname())()
37
38
  self.Adapter = self.adapter = handle_partner
@@ -1,5 +1,5 @@
1
- from grongier.pex._business_process import _BusinessProcess
2
- from grongier.pex._business_host import _BusinessHost
1
+ from iop._business_process import _BusinessProcess
2
+ from iop._business_host import _BusinessHost
3
3
 
4
4
  class _PrivateSessionProcess(_BusinessProcess):
5
5
 
iop/_utils.py ADDED
@@ -0,0 +1,458 @@
1
+ import os
2
+ import sys
3
+ import ast
4
+ import iris
5
+ import inspect
6
+ import xmltodict
7
+ import pkg_resources
8
+ import importlib
9
+ import json
10
+ from dc_schema import get_schema
11
+
12
+ class _Utils():
13
+ @staticmethod
14
+ def raise_on_error(sc):
15
+ """
16
+ If the status code is an error, raise an exception
17
+
18
+ :param sc: The status code returned by the Iris API
19
+ """
20
+ if iris.system.Status.IsError(sc):
21
+ raise RuntimeError(iris.system.Status.GetOneStatusText(sc))
22
+
23
+ @staticmethod
24
+ def setup(path:str = None):
25
+
26
+ if path is None:
27
+ # get the path of the data folder with pkg_resources
28
+ path = pkg_resources.resource_filename('iop', 'cls')
29
+
30
+ _Utils.raise_on_error(iris.cls('%SYSTEM.OBJ').LoadDir(path,'cubk',"*.cls",1))
31
+
32
+ # for retrocompatibility load grongier.pex
33
+ path = pkg_resources.resource_filename('grongier', 'cls')
34
+
35
+ if path:
36
+ _Utils.raise_on_error(iris.cls('%SYSTEM.OBJ').LoadDir(path,'cubk',"*.cls",1))
37
+
38
+ @staticmethod
39
+ def register_message_schema(cls):
40
+ """
41
+ It takes a class and registers the schema
42
+
43
+ :param cls: The class to register
44
+ """
45
+ schema = get_schema(cls)
46
+ schema_name = cls.__module__ + '.' + cls.__name__
47
+ schema_str = json.dumps(schema)
48
+ categories = schema_name
49
+ _Utils.register_schema(schema_name,schema_str,categories)
50
+
51
+ @staticmethod
52
+ def register_schema(schema_name:str, schema_str:str,categories:str):
53
+ """
54
+ It takes a schema name, a schema string, and a category string, and registers the schema
55
+
56
+ :param schema_name: The name of the schema
57
+ :type schema_name: str
58
+ :param schema_str: The schema as a string
59
+ :type schema_str: str
60
+ :param categories: The categories of the schema
61
+ :type categories: str
62
+ """
63
+ _Utils.raise_on_error(iris.cls('IOP.Message.JSONSchema').Import(schema_str,categories,schema_name))
64
+
65
+ @staticmethod
66
+ def register_component(module:str,classname:str,path:str,overwrite:int=1,iris_classname:str='Python'):
67
+ """
68
+ It registers a component in the Iris database.
69
+
70
+ :param module: The name of the module that contains the class
71
+ :type module: str
72
+ :param classname: The name of the class you want to register
73
+ :type classname: str
74
+ :param path: The path to the component
75
+ :type path: str
76
+ :param overwrite: 0 = no, 1 = yes
77
+ :type overwrite: int
78
+ :param iris_classname: The name of the class in the Iris class hierarchy
79
+ :type iris_classname: str
80
+ :return: The return value is a string.
81
+ """
82
+ path = os.path.abspath(os.path.normpath(path))
83
+ fullpath = _Utils.guess_path(module,path)
84
+ try:
85
+ iris.cls('IOP.Utils').dispatchRegisterComponent(module,classname,path,fullpath,overwrite,iris_classname)
86
+ except RuntimeError as e:
87
+ # New message error : Make sure the iop package is installed in iris
88
+ raise RuntimeError("Iris class : IOP.Utils not found. Make sure the iop package is installed in iris eg: iop --init.") from e
89
+
90
+ @staticmethod
91
+ def register_folder(path:str,overwrite:int=1,iris_package_name:str='Python'):
92
+ """
93
+ > This function takes a path to a folder, and registers all the Python files in that folder as IRIS
94
+ classes
95
+
96
+ :param path: the path to the folder containing the files you want to register
97
+ :type path: str
98
+ :param overwrite:
99
+ :type overwrite: int
100
+ :param iris_package_name: The name of the iris package you want to register the file to
101
+ :type iris_package_name: str
102
+ """
103
+ path = os.path.normpath(path)
104
+ # get the absolute path of the folder
105
+ path = os.path.abspath(path)
106
+ for filename in os.listdir(path):
107
+ if filename.endswith(".py"):
108
+ _Utils._register_file(filename, path, overwrite, iris_package_name)
109
+ else:
110
+ continue
111
+
112
+ @staticmethod
113
+ def register_file(file:str,overwrite:int=1,iris_package_name:str='Python'):
114
+ """
115
+ It takes a file name, a boolean to overwrite existing components, and the name of the Iris
116
+ package that the file is in. It then opens the file, parses it, and looks for classes that extend
117
+ BusinessOperation, BusinessProcess, or BusinessService. If it finds one, it calls register_component
118
+ with the module name, class name, path, overwrite boolean, and the full Iris package name
119
+
120
+ :param file: the name of the file containing the component
121
+ :type file: str
122
+ :param overwrite: if the component already exists, overwrite it
123
+ :type overwrite: int
124
+ :param iris_package_name: the name of the iris package that you want to register the components to
125
+ :type iris_package_name: str
126
+ """
127
+ head_tail = os.path.split(file)
128
+ return _Utils._register_file(head_tail[1],head_tail[0],overwrite,iris_package_name)
129
+
130
+ @staticmethod
131
+ def _register_file(filename:str,path:str,overwrite:int=1,iris_package_name:str='Python'):
132
+ """
133
+ It takes a file name, a path, a boolean to overwrite existing components, and the name of the Iris
134
+ package that the file is in. It then opens the file, parses it, and looks for classes that extend
135
+ BusinessOperation, BusinessProcess, or BusinessService. If it finds one, it calls register_component
136
+ with the module name, class name, path, overwrite boolean, and the full Iris package name
137
+
138
+ :param filename: the name of the file containing the component
139
+ :type filename: str
140
+ :param path: the path to the directory containing the files to be registered
141
+ :type path: str
142
+ :param overwrite: if the component already exists, overwrite it
143
+ :type overwrite: int
144
+ :param iris_package_name: the name of the iris package that you want to register the components to
145
+ :type iris_package_name: str
146
+ """
147
+ #pour chaque classe dans le module, appeler register_component
148
+ f = os.path.join(path,filename)
149
+ with open(f) as file:
150
+ node = ast.parse(file.read())
151
+ #list of class in the file
152
+ classes = [n for n in node.body if isinstance(n, ast.ClassDef)]
153
+ for klass in classes:
154
+ extend = ''
155
+ if len(klass.bases) == 1:
156
+ if hasattr(klass.bases[0],'id'):
157
+ extend = klass.bases[0].id
158
+ else:
159
+ extend = klass.bases[0].attr
160
+ if extend in ('BusinessOperation','BusinessProcess','BusinessService','DuplexService','DuplexProcess','DuplexOperation','InboundAdapter','OutboundAdapter'):
161
+ module = _Utils.filename_to_module(filename)
162
+ iris_class_name = f"{iris_package_name}.{module}.{klass.name}"
163
+ # strip "_" for iris class name
164
+ iris_class_name = iris_class_name.replace('_','')
165
+ _Utils.register_component(module, klass.name, path, overwrite, iris_class_name)
166
+ @staticmethod
167
+ def register_package(package:str,path:str,overwrite:int=1,iris_package_name:str='Python'):
168
+ """
169
+ It takes a package name, a path to the package, a flag to overwrite existing files, and the name of
170
+ the iris package to register the files to. It then loops through all the files in the package and
171
+ registers them to the iris package
172
+
173
+ :param package: the name of the package you want to register
174
+ :type package: str
175
+ :param path: the path to the directory containing the package
176
+ :type path: str
177
+ :param overwrite: 0 = don't overwrite, 1 = overwrite
178
+ :type overwrite: int
179
+ :param iris_package_name: The name of the package in the Iris package manager
180
+ :type iris_package_name: str
181
+ """
182
+ for filename in os.listdir(os.path.join(path,package)):
183
+ if filename.endswith(".py"):
184
+ _Utils._register_file(filename, os.path.join(path,package), overwrite, iris_package_name)
185
+ else:
186
+ continue
187
+
188
+ @staticmethod
189
+ def filename_to_module(filename) -> str:
190
+ """
191
+ It takes a filename and returns the module name
192
+
193
+ :param filename: The name of the file to be imported
194
+ :return: The module name
195
+ """
196
+ module = ''
197
+
198
+ path,file = os.path.split(filename)
199
+ mod = file.split('.')[0]
200
+ packages = path.replace(os.sep, ('.'))
201
+ if len(packages) >1:
202
+ module = packages+'.'+mod
203
+ else:
204
+ module = mod
205
+
206
+ return module
207
+
208
+ @staticmethod
209
+ def migrate(filename=None):
210
+ """
211
+ Read the settings.py file and register all the components
212
+ settings.py file has two dictionaries:
213
+ * CLASSES
214
+ * key: the name of the class
215
+ * value: an instance of the class
216
+ * PRODUCTIONS
217
+ list of dictionaries:
218
+ * key: the name of the production
219
+ * value: a dictionary containing the settings for the production
220
+ * SCHEMAS
221
+ List of classes
222
+ """
223
+ path = None
224
+ # try to load the settings file
225
+ if filename:
226
+ # check if the filename is absolute or relative
227
+ if os.path.isabs(filename):
228
+ path = os.path.dirname(filename)
229
+ else:
230
+ raise ValueError("The filename must be absolute")
231
+ # add the path to the system path to the beginning
232
+ sys.path.insert(0,os.path.normpath(path))
233
+ # import settings from the specified file
234
+ settings = _Utils.import_module_from_path('settings',filename)
235
+ else:
236
+ # import settings from the settings module
237
+ import settings
238
+ # get the path of the settings file
239
+ path = os.path.dirname(inspect.getfile(settings))
240
+ try:
241
+ # set the classes settings
242
+ _Utils.set_classes_settings(settings.CLASSES,path)
243
+ except AttributeError:
244
+ print("No classes to register")
245
+ try:
246
+ # set the productions settings
247
+ _Utils.set_productions_settings(settings.PRODUCTIONS,path)
248
+ except AttributeError:
249
+ print("No productions to register")
250
+ try:
251
+ # set the schemas
252
+ for cls in settings.SCHEMAS:
253
+ _Utils.register_message_schema(cls)
254
+ except AttributeError:
255
+ print("No schemas to register")
256
+ try:
257
+ sys.path.remove(path)
258
+ except ValueError:
259
+ pass
260
+
261
+ @staticmethod
262
+ def import_module_from_path(module_name, file_path):
263
+ if not os.path.isabs(file_path):
264
+ raise ValueError("The file path must be absolute")
265
+
266
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
267
+ if spec is None:
268
+ raise ImportError(f"Cannot find module named {module_name} at {file_path}")
269
+
270
+ module = importlib.util.module_from_spec(spec)
271
+ sys.modules[module_name] = module
272
+ spec.loader.exec_module(module)
273
+ return module
274
+
275
+ @staticmethod
276
+ def set_classes_settings(class_items,root_path=None):
277
+ """
278
+ It takes a dictionary of classes and returns a dictionary of settings for each class
279
+
280
+ :param class_items: a dictionary of classes
281
+ :return: a dictionary of settings for each class
282
+ """
283
+ for key, value in class_items.items():
284
+ if inspect.isclass(value):
285
+ path = None
286
+ if root_path:
287
+ path = root_path
288
+ else:
289
+ path = os.path.dirname(inspect.getfile(value))
290
+ _Utils.register_component(value.__module__,value.__name__,path,1,key)
291
+ elif inspect.ismodule(value):
292
+ path = None
293
+ if root_path:
294
+ path = root_path
295
+ else:
296
+ path = os.path.dirname(inspect.getfile(value))
297
+ _Utils._register_file(value.__name__+'.py',path,1,key)
298
+ # if the value is a dict
299
+ elif isinstance(value,dict):
300
+ # if the dict has a key 'path' and a key 'module' and a key 'class'
301
+ if 'path' in value and 'module' in value and 'class' in value:
302
+ # register the component
303
+ _Utils.register_component(value['module'],value['class'],value['path'],1,key)
304
+ # if the dict has a key 'path' and a key 'package'
305
+ elif 'path' in value and 'package' in value:
306
+ # register the package
307
+ _Utils.register_package(value['package'],value['path'],1,key)
308
+ # if the dict has a key 'path' and a key 'file'
309
+ elif 'path' in value and 'file' in value:
310
+ # register the file
311
+ _Utils._register_file(value['file'],value['path'],1,key)
312
+ # if the dict has a key 'path'
313
+ elif 'path' in value:
314
+ # register folder
315
+ _Utils.register_folder(value['path'],1,key)
316
+ else:
317
+ raise ValueError(f"Invalid value for {key}.")
318
+
319
+ @staticmethod
320
+ def set_productions_settings(production_list,root_path=None):
321
+ """
322
+ It takes a list of dictionaries and registers the productions
323
+ """
324
+ # for each production in the list
325
+ for production in production_list:
326
+ # get the production name (first key in the dictionary)
327
+ production_name = list(production.keys())[0]
328
+ # set the first key to 'production'
329
+ production['Production'] = production.pop(production_name)
330
+ # handle Items
331
+ production = _Utils.handle_items(production,root_path)
332
+ # transform the json as an xml
333
+ xml = _Utils.dict_to_xml(production)
334
+ # register the production
335
+ _Utils.register_production(production_name,xml)
336
+
337
+ @staticmethod
338
+ def handle_items(production,root_path=None):
339
+ # if an item is a class, register it and replace it with the name of the class
340
+ if 'Item' in production['Production']:
341
+ # for each item in the list
342
+ for i,item in enumerate(production['Production']['Item']):
343
+ # if the attribute "@ClassName" is a class, register it and replace it with the name of the class
344
+ if '@ClassName' in item:
345
+ if inspect.isclass(item['@ClassName']):
346
+ path = None
347
+ if root_path:
348
+ path = root_path
349
+ else:
350
+ path = os.path.dirname(inspect.getfile(item['@ClassName']))
351
+ _Utils.register_component(item['@ClassName'].__module__,item['@ClassName'].__name__,path,1,item['@Name'])
352
+ # replace the class with the name of the class
353
+ production['Production']['Item'][i]['@ClassName'] = item['@Name']
354
+ # if the attribute "@ClassName" is a dict
355
+ elif isinstance(item['@ClassName'],dict):
356
+ # create a new dict where the key is the name of the class and the value is the dict
357
+ class_dict = {item['@Name']:item['@ClassName']}
358
+ # pass the new dict to set_classes_settings
359
+ _Utils.set_classes_settings(class_dict)
360
+ # replace the class with the name of the class
361
+ production['Production']['Item'][i]['@ClassName'] = item['@Name']
362
+ else:
363
+ raise ValueError(f"Invalid value for {item['@Name']}.")
364
+
365
+ return production
366
+
367
+ @staticmethod
368
+ def dict_to_xml(json):
369
+ """
370
+ It takes a json and returns an xml
371
+
372
+ :param json: a json
373
+ :return: an xml
374
+ """
375
+ xml = xmltodict.unparse(json,pretty=True)
376
+ # remove the xml version tag
377
+ xml = xml.replace('<?xml version="1.0" encoding="utf-8"?>','')
378
+ # remove the new line at the beginning of the xml
379
+ xml = xml[1:]
380
+ return xml
381
+
382
+ @staticmethod
383
+ def register_production(production_name,xml):
384
+ """
385
+ It takes a production name and an xml and registers the production
386
+
387
+ :param production_name: the name of the production
388
+ :type production_name: str
389
+ :param xml: the xml of the production
390
+ :type xml: str
391
+ """
392
+ # split the production name in the package name and the production name
393
+ # the production name is the last part of the string
394
+ package = '.'.join(production_name.split('.')[:-1])
395
+ production_name = production_name.split('.')[-1]
396
+ stream = _Utils.string_to_stream(xml)
397
+ # register the production
398
+ _Utils.raise_on_error(iris.cls('IOP.Utils').CreateProduction(package,production_name,stream))
399
+
400
+ @staticmethod
401
+ def export_production(production_name):
402
+ """
403
+ It takes a production name and exports the production
404
+
405
+ :param production_name: the name of the production
406
+ :type production_name: str
407
+ """
408
+ def postprocessor(path, key, value):
409
+ if value is None:
410
+ return key, ''
411
+ return key, value
412
+ # export the production
413
+ xdata = iris.cls('IOP.Utils').ExportProduction(production_name)
414
+ # for each chunk of 1024 characters
415
+ string = _Utils.stream_to_string(xdata)
416
+ # convert the xml to a dictionary
417
+ data = xmltodict.parse(string,postprocessor=postprocessor)
418
+ # return the dictionary
419
+ return data
420
+
421
+ @staticmethod
422
+ def stream_to_string(stream,buffer=1000000)-> str:
423
+ string = ""
424
+ stream.Rewind()
425
+ while not stream.AtEnd:
426
+ string += stream.Read(buffer)
427
+ return string
428
+
429
+ @staticmethod
430
+ def string_to_stream(string:str,buffer=1000000):
431
+ stream = iris.cls('%Stream.GlobalCharacter')._New()
432
+ n = buffer
433
+ chunks = [string[i:i+n] for i in range(0, len(string), n)]
434
+ for chunk in chunks:
435
+ stream.Write(chunk)
436
+ return stream
437
+
438
+ @staticmethod
439
+ def guess_path(module,path):
440
+ if "." in module:
441
+ if module.startswith("."):
442
+ # count the number of dots at the beginning of the module name
443
+ dots = 0
444
+ for c in module:
445
+ if c == ".":
446
+ dots += 1
447
+ else:
448
+ break
449
+ # remove the dots from the beginning of the module name
450
+ module = module[dots:]
451
+ # go to the parent directory dots times
452
+ for i in range(dots)-1:
453
+ path = os.path.dirname(path)
454
+ return os.path.join(path, module.replace(".", os.sep) + ".py")
455
+ else:
456
+ return os.path.join(path, module.replace(".", os.sep) + ".py")
457
+ else:
458
+ return os.path.join(path, module + ".py")
@@ -0,0 +1,35 @@
1
+ /* Copyright (c) 2021 by InterSystems Corporation.
2
+ Cambridge, Massachusetts, U.S.A. All rights reserved.
3
+ Confidential property of InterSystems Corporation. */
4
+
5
+ Class IOP.BusinessOperation Extends (Ens.BusinessOperation, IOP.Common) [ Inheritance = right, ProcedureBlock, System = 4 ]
6
+ {
7
+
8
+ Parameter SETTINGS = "%classname:Python BusinessOperation,%module:Python BusinessOperation,%settings:Python BusinessOperation,%classpaths:Python BusinessOperation";
9
+
10
+ Method OnMessage(
11
+ request As %Library.Persistent,
12
+ Output response As %Library.Persistent) As %Status
13
+ {
14
+ set tSC = $$$OK
15
+ try {
16
+ set response = ..%class."_dispatch_on_message"(request)
17
+ } catch ex {
18
+ set tSC = ex.AsStatus()
19
+ }
20
+ quit tSC
21
+ }
22
+
23
+ Method OnKeepalive(pStatus As %Status = {$$$OK}) As %Status
24
+ {
25
+ set tSC = $$$OK
26
+ try {
27
+ $$$ThrowOnError(##super(pStatus))
28
+ do ..%class."on_keepalive"()
29
+ } catch ex {
30
+ set tSC = ex.AsStatus()
31
+ }
32
+ quit tSC
33
+ }
34
+
35
+ }