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.
- grongier/__init__.py +0 -0
- grongier/cls/Grongier/PEX/BusinessOperation.cls +8 -0
- grongier/cls/Grongier/PEX/BusinessProcess.cls +13 -0
- grongier/cls/Grongier/PEX/BusinessService.cls +8 -0
- grongier/cls/Grongier/PEX/Common.cls +10 -0
- grongier/cls/Grongier/PEX/Director.cls +10 -0
- grongier/cls/Grongier/PEX/Duplex/Operation.cls +4 -0
- grongier/cls/Grongier/PEX/Duplex/Process.cls +13 -0
- grongier/cls/Grongier/PEX/Duplex/Service.cls +4 -0
- grongier/cls/Grongier/PEX/InboundAdapter.cls +8 -0
- grongier/cls/Grongier/PEX/Message.cls +13 -0
- grongier/cls/Grongier/PEX/OutboundAdapter.cls +8 -0
- grongier/cls/Grongier/PEX/PickleMessage.cls +13 -0
- grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +8 -0
- grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +14 -0
- grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +14 -0
- grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +14 -0
- grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +14 -0
- grongier/cls/Grongier/PEX/Test.cls +10 -0
- grongier/cls/Grongier/PEX/Utils.cls +10 -0
- grongier/cls/Grongier/Service/WSGI.cls +4 -0
- grongier/pex/__init__.py +24 -0
- grongier/pex/__main__.py +4 -0
- grongier/pex/_business_host.py +1 -0
- grongier/pex/_cli.py +4 -0
- grongier/pex/_common.py +1 -0
- grongier/pex/_director.py +1 -0
- grongier/pex/_utils.py +1 -0
- grongier/pex/wsgi/handlers.py +104 -0
- iop/__init__.py +25 -0
- iop/__main__.py +4 -0
- iop/_async_request.py +67 -0
- iop/_business_host.py +256 -0
- iop/_business_operation.py +75 -0
- iop/_business_process.py +224 -0
- iop/_business_service.py +63 -0
- iop/_cli.py +247 -0
- iop/_common.py +334 -0
- iop/_debugpy.py +187 -0
- iop/_decorators.py +49 -0
- iop/_director.py +301 -0
- iop/_dispatch.py +136 -0
- iop/_generator_request.py +30 -0
- iop/_inbound_adapter.py +34 -0
- iop/_iris.py +8 -0
- iop/_log_manager.py +100 -0
- iop/_message.py +40 -0
- iop/_message_validator.py +49 -0
- iop/_outbound_adapter.py +23 -0
- iop/_private_session_duplex.py +103 -0
- iop/_private_session_process.py +41 -0
- iop/_remote.py +91 -0
- iop/_serialization.py +199 -0
- iop/_utils.py +671 -0
- iop/cls/IOP/BusinessOperation.cls +35 -0
- iop/cls/IOP/BusinessProcess.cls +156 -0
- iop/cls/IOP/BusinessService.cls +40 -0
- iop/cls/IOP/Common.cls +569 -0
- iop/cls/IOP/Director.cls +70 -0
- iop/cls/IOP/Duplex/Operation.cls +29 -0
- iop/cls/IOP/Duplex/Process.cls +229 -0
- iop/cls/IOP/Duplex/Service.cls +9 -0
- iop/cls/IOP/Generator/Message/Ack.cls +31 -0
- iop/cls/IOP/Generator/Message/Poll.cls +31 -0
- iop/cls/IOP/Generator/Message/Start.cls +15 -0
- iop/cls/IOP/Generator/Message/StartPickle.cls +15 -0
- iop/cls/IOP/Generator/Message/Stop.cls +32 -0
- iop/cls/IOP/InboundAdapter.cls +22 -0
- iop/cls/IOP/Message/JSONSchema.cls +125 -0
- iop/cls/IOP/Message.cls +754 -0
- iop/cls/IOP/OutboundAdapter.cls +36 -0
- iop/cls/IOP/PickleMessage.cls +58 -0
- iop/cls/IOP/PrivateSession/Duplex.cls +260 -0
- iop/cls/IOP/PrivateSession/Message/Ack.cls +32 -0
- iop/cls/IOP/PrivateSession/Message/Poll.cls +32 -0
- iop/cls/IOP/PrivateSession/Message/Start.cls +31 -0
- iop/cls/IOP/PrivateSession/Message/Stop.cls +48 -0
- iop/cls/IOP/Projection.cls +49 -0
- iop/cls/IOP/Service/Remote/Handler.cls +30 -0
- iop/cls/IOP/Service/Remote/Rest/v1.cls +97 -0
- iop/cls/IOP/Service/WSGI.cls +310 -0
- iop/cls/IOP/Test.cls +85 -0
- iop/cls/IOP/Utils.cls +503 -0
- iop/cls/IOP/Wrapper.cls +58 -0
- iop/wsgi/handlers.py +104 -0
- iris_pex_embedded_python-3.5.5b4.dist-info/METADATA +91 -0
- iris_pex_embedded_python-3.5.5b4.dist-info/RECORD +91 -0
- iris_pex_embedded_python-3.5.5b4.dist-info/WHEEL +5 -0
- iris_pex_embedded_python-3.5.5b4.dist-info/entry_points.txt +2 -0
- iris_pex_embedded_python-3.5.5b4.dist-info/licenses/LICENSE +21 -0
- 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)
|