iris-pex-embedded-python 3.7.2b2__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iop/__init__.py +276 -18
- iop/__main__.py +3 -3
- iop/cli/__init__.py +1 -0
- iop/cli/formatting.py +45 -0
- iop/{_cli.py → cli/main.py} +238 -216
- iop/cli/parser.py +165 -0
- iop/cli/types.py +73 -0
- iop/cls/IOP/Common.cls +11 -2
- iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
- iop/cls/IOP/Service/Remote/Rest/v1.cls +152 -5
- iop/cls/IOP/Utils.cls +814 -7
- iop/cls/IOP/Wrapper.cls +1 -1
- iop/components/__init__.py +16 -0
- iop/{_async_request.py → components/async_request.py} +7 -7
- iop/components/business_host.py +363 -0
- iop/components/business_operation.py +84 -0
- iop/{_business_process.py → components/business_process.py} +145 -125
- iop/components/business_service.py +106 -0
- iop/{_common.py → components/common.py} +160 -164
- iop/{_debugpy.py → components/debugpy.py} +11 -11
- iop/{_generator_request.py → components/generator_request.py} +4 -4
- iop/{_inbound_adapter.py → components/inbound_adapter.py} +10 -18
- iop/{_log_manager.py → components/log_manager.py} +1 -1
- iop/{_outbound_adapter.py → components/outbound_adapter.py} +1 -1
- iop/components/polling_business_service.py +48 -0
- iop/{_private_session_duplex.py → components/private_session_duplex.py} +7 -9
- iop/{_private_session_process.py → components/private_session_process.py} +2 -2
- iop/{_settings.py → components/settings.py} +23 -3
- iop/messages/__init__.py +7 -0
- iop/messages/base.py +61 -0
- iop/{_decorators.py → messages/decorators.py} +8 -2
- iop/messages/dispatch.py +351 -0
- iop/{_persistent_message.py → messages/persistent.py} +24 -19
- iop/{_serialization.py → messages/serialization.py} +34 -21
- iop/{_message_validator.py → messages/validation.py} +15 -13
- iop/migration/__init__.py +1 -0
- iop/migration/io.py +61 -0
- iop/migration/manifest.py +534 -0
- iop/migration/plans.py +80 -0
- iop/migration/utils.py +1126 -0
- iop/production/__init__.py +55 -0
- iop/production/actions.py +525 -0
- iop/production/common.py +103 -0
- iop/production/component.py +234 -0
- iop/production/declarations.py +190 -0
- iop/production/declarative.py +214 -0
- iop/production/diff.py +327 -0
- iop/production/import_.py +296 -0
- iop/production/inspection.py +92 -0
- iop/production/model.py +1143 -0
- iop/production/planning.py +678 -0
- iop/production/reconstruction.py +341 -0
- iop/production/rendering.py +610 -0
- iop/production/runtime.py +91 -0
- iop/production/source_inference.py +729 -0
- iop/production/types.py +546 -0
- iop/production/validation.py +452 -0
- iop/runtime/__init__.py +1 -0
- iop/runtime/director.py +415 -0
- iop/runtime/environment.py +59 -0
- iop/{_iris.py → runtime/iris.py} +2 -2
- iop/runtime/local.py +145 -0
- iop/{_director_protocol.py → runtime/protocol.py} +25 -8
- iop/runtime/remote/__init__.py +12 -0
- iop/runtime/remote/client.py +90 -0
- iop/runtime/remote/director.py +283 -0
- iop/runtime/remote/migration.py +57 -0
- iop/runtime/remote/settings.py +69 -0
- iop/runtime/remote/setup.py +72 -0
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA +11 -2
- iris_pex_embedded_python-4.0.0.dist-info/RECORD +99 -0
- iris_pex_embedded_python-4.0.0.dist-info/entry_points.txt +2 -0
- iris_pex_embedded_python-4.0.0.dist-info/top_level.txt +1 -0
- grongier/__init__.py +0 -0
- grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -8
- grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -13
- grongier/cls/Grongier/PEX/BusinessService.cls +0 -8
- grongier/cls/Grongier/PEX/Common.cls +0 -10
- grongier/cls/Grongier/PEX/Director.cls +0 -10
- grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -4
- grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -13
- grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -4
- grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/Message.cls +0 -13
- grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/PickleMessage.cls +0 -13
- grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -8
- grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -14
- grongier/cls/Grongier/PEX/Test.cls +0 -10
- grongier/cls/Grongier/PEX/Utils.cls +0 -10
- grongier/cls/Grongier/Service/WSGI.cls +0 -4
- grongier/pex/__init__.py +0 -75
- grongier/pex/__main__.py +0 -4
- grongier/pex/_business_host.py +0 -1
- grongier/pex/_cli.py +0 -4
- grongier/pex/_common.py +0 -1
- grongier/pex/_director.py +0 -1
- grongier/pex/_utils.py +0 -1
- grongier/pex/wsgi/handlers.py +0 -104
- iop/_business_host.py +0 -322
- iop/_business_operation.py +0 -77
- iop/_business_service.py +0 -67
- iop/_director.py +0 -333
- iop/_dispatch.py +0 -194
- iop/_local.py +0 -106
- iop/_message.py +0 -45
- iop/_polling_business_service.py +0 -6
- iop/_remote.py +0 -473
- iop/_utils.py +0 -912
- iop/cls/IOP/Service/WSGI.cls +0 -310
- iop/wsgi/handlers.py +0 -112
- iris_pex_embedded_python-3.7.2b2.dist-info/RECORD +0 -96
- iris_pex_embedded_python-3.7.2b2.dist-info/entry_points.txt +0 -2
- iris_pex_embedded_python-3.7.2b2.dist-info/top_level.txt +0 -2
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/WHEEL +0 -0
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import inspect
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..production.validation import (
|
|
10
|
+
ProductionValidationError,
|
|
11
|
+
ProductionValidationReport,
|
|
12
|
+
validate_production_entry,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class ClassRegistration:
|
|
18
|
+
iris_classname: str
|
|
19
|
+
action: str
|
|
20
|
+
target: str
|
|
21
|
+
plan_kind: str = "component"
|
|
22
|
+
source: str = "classes"
|
|
23
|
+
cls: type | None = None
|
|
24
|
+
module: str = ""
|
|
25
|
+
classname: str = ""
|
|
26
|
+
path: str | None = None
|
|
27
|
+
package: str = ""
|
|
28
|
+
file: str = ""
|
|
29
|
+
sync_schema: bool = True
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def plan_entry(self) -> str:
|
|
33
|
+
return f"{self.iris_classname} -> {self.target} ({self.plan_kind})"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class SchemaRegistration:
|
|
38
|
+
schema_class: type
|
|
39
|
+
target: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class ProductionRegistration:
|
|
44
|
+
name: str
|
|
45
|
+
definition: dict[str, Any]
|
|
46
|
+
validation_subject: Any
|
|
47
|
+
validation_report: ProductionValidationReport
|
|
48
|
+
source: str
|
|
49
|
+
class_registrations: tuple[ClassRegistration, ...] = ()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class MigrationManifest:
|
|
54
|
+
settings: str
|
|
55
|
+
mode: str | None = None
|
|
56
|
+
namespace: str | None = None
|
|
57
|
+
class_registrations: list[ClassRegistration] = field(default_factory=list)
|
|
58
|
+
schema_registrations: list[SchemaRegistration] = field(default_factory=list)
|
|
59
|
+
production_registrations: list[ProductionRegistration] = field(
|
|
60
|
+
default_factory=list
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def plan_class_entries(self) -> list[str]:
|
|
64
|
+
entries = [entry.plan_entry for entry in self.class_registrations]
|
|
65
|
+
for production in self.production_registrations:
|
|
66
|
+
entries.extend(entry.plan_entry for entry in production.class_registrations)
|
|
67
|
+
return entries
|
|
68
|
+
|
|
69
|
+
def plan_schema_entries(self) -> list[str]:
|
|
70
|
+
return [entry.target for entry in self.schema_registrations]
|
|
71
|
+
|
|
72
|
+
def plan_production_entries(self) -> list[str]:
|
|
73
|
+
return [entry.name for entry in self.production_registrations]
|
|
74
|
+
|
|
75
|
+
def plan_validation_entries(self) -> list[str]:
|
|
76
|
+
entries: list[str] = []
|
|
77
|
+
for production in self.production_registrations:
|
|
78
|
+
report = production.validation_report
|
|
79
|
+
if report.has_issues:
|
|
80
|
+
entries.extend(
|
|
81
|
+
f"{report.production_name}: {issue.to_text()}"
|
|
82
|
+
for issue in report.issues
|
|
83
|
+
)
|
|
84
|
+
return entries
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class MigrationManifestBuilder:
|
|
88
|
+
"""Normalize migration settings once for planning and execution."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, utils):
|
|
91
|
+
self._utils = utils
|
|
92
|
+
|
|
93
|
+
def build(
|
|
94
|
+
self,
|
|
95
|
+
settings,
|
|
96
|
+
path: str | None,
|
|
97
|
+
*,
|
|
98
|
+
filename: str | None = None,
|
|
99
|
+
mode: str | None = None,
|
|
100
|
+
namespace: str | None = None,
|
|
101
|
+
strict_production_validation: bool = False,
|
|
102
|
+
) -> MigrationManifest:
|
|
103
|
+
if not path:
|
|
104
|
+
path = self._settings_path(settings)
|
|
105
|
+
|
|
106
|
+
manifest = MigrationManifest(
|
|
107
|
+
settings=filename or inspect.getfile(settings),
|
|
108
|
+
mode=mode,
|
|
109
|
+
namespace=namespace,
|
|
110
|
+
)
|
|
111
|
+
manifest.class_registrations.extend(
|
|
112
|
+
self.build_class_registrations(
|
|
113
|
+
getattr(settings, "CLASSES", {}),
|
|
114
|
+
path,
|
|
115
|
+
source="classes",
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
manifest.schema_registrations.extend(
|
|
119
|
+
self.build_schema_registrations(getattr(settings, "SCHEMAS", None))
|
|
120
|
+
)
|
|
121
|
+
manifest.production_registrations.extend(
|
|
122
|
+
self.build_production_registrations(
|
|
123
|
+
getattr(settings, "PRODUCTIONS", None),
|
|
124
|
+
path,
|
|
125
|
+
strict_production_validation=strict_production_validation,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
return manifest
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _settings_path(settings) -> str:
|
|
132
|
+
return os.path.dirname(inspect.getfile(settings))
|
|
133
|
+
|
|
134
|
+
def build_class_registrations(
|
|
135
|
+
self,
|
|
136
|
+
class_items: dict[str, Any] | None,
|
|
137
|
+
root_path: str | None = None,
|
|
138
|
+
*,
|
|
139
|
+
source: str = "classes",
|
|
140
|
+
) -> list[ClassRegistration]:
|
|
141
|
+
if class_items is None:
|
|
142
|
+
return []
|
|
143
|
+
if not isinstance(class_items, dict):
|
|
144
|
+
raise ValueError("CLASSES must be a dictionary.")
|
|
145
|
+
|
|
146
|
+
registrations: list[ClassRegistration] = []
|
|
147
|
+
for iris_classname, value in class_items.items():
|
|
148
|
+
registrations.append(
|
|
149
|
+
self.class_registration(
|
|
150
|
+
iris_classname,
|
|
151
|
+
value,
|
|
152
|
+
root_path,
|
|
153
|
+
source=source,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
return registrations
|
|
157
|
+
|
|
158
|
+
def class_registration(
|
|
159
|
+
self,
|
|
160
|
+
iris_classname: str,
|
|
161
|
+
value: Any,
|
|
162
|
+
root_path: str | None = None,
|
|
163
|
+
*,
|
|
164
|
+
source: str = "classes",
|
|
165
|
+
) -> ClassRegistration:
|
|
166
|
+
if inspect.isclass(value):
|
|
167
|
+
if self._utils.is_persistent_message_class(value):
|
|
168
|
+
return ClassRegistration(
|
|
169
|
+
iris_classname=iris_classname,
|
|
170
|
+
action="persistent_message",
|
|
171
|
+
target=self._utils._python_classname(value),
|
|
172
|
+
plan_kind="PersistentMessage",
|
|
173
|
+
source=source,
|
|
174
|
+
cls=value,
|
|
175
|
+
)
|
|
176
|
+
if self._utils._is_message_schema_class(value):
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"{self._utils._python_classname(value)} is a Message/"
|
|
179
|
+
"PydanticMessage and cannot be registered in CLASSES. "
|
|
180
|
+
f"Use SCHEMAS = [{value.__name__}] if you need DTL "
|
|
181
|
+
"support. Otherwise, no migration is required for this "
|
|
182
|
+
"message."
|
|
183
|
+
)
|
|
184
|
+
return ClassRegistration(
|
|
185
|
+
iris_classname=iris_classname,
|
|
186
|
+
action="component_class",
|
|
187
|
+
target=self._utils._python_classname(value),
|
|
188
|
+
source=source,
|
|
189
|
+
cls=value,
|
|
190
|
+
module=value.__module__,
|
|
191
|
+
classname=value.__name__,
|
|
192
|
+
path=self._class_path(value, root_path),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if inspect.ismodule(value):
|
|
196
|
+
return ClassRegistration(
|
|
197
|
+
iris_classname=iris_classname,
|
|
198
|
+
action="module_file",
|
|
199
|
+
target=f"{value.__name__}.*",
|
|
200
|
+
source=source,
|
|
201
|
+
module=value.__name__,
|
|
202
|
+
file=f"{value.__name__}.py",
|
|
203
|
+
path=self._module_path(value, root_path),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if isinstance(value, dict):
|
|
207
|
+
return self._dict_class_registration(
|
|
208
|
+
iris_classname,
|
|
209
|
+
value,
|
|
210
|
+
source=source,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
raise ValueError(f"Invalid migration class entry: {value!r}.")
|
|
214
|
+
|
|
215
|
+
def classify_class_setting(
|
|
216
|
+
self,
|
|
217
|
+
value: Any,
|
|
218
|
+
root_path: str | None = None,
|
|
219
|
+
) -> tuple[str, str]:
|
|
220
|
+
if inspect.isclass(value):
|
|
221
|
+
if self._utils.is_persistent_message_class(value):
|
|
222
|
+
return "persistent_message", self._utils._python_classname(value)
|
|
223
|
+
if self._utils._is_message_schema_class(value):
|
|
224
|
+
return "message_schema", self._utils._python_classname(value)
|
|
225
|
+
return "component", self._utils._python_classname(value)
|
|
226
|
+
|
|
227
|
+
if inspect.ismodule(value):
|
|
228
|
+
return "component", f"{value.__name__}.*"
|
|
229
|
+
|
|
230
|
+
if isinstance(value, dict):
|
|
231
|
+
if "path" in value and "module" in value and "class" in value:
|
|
232
|
+
cls = self._utils._try_import_class(
|
|
233
|
+
value["module"],
|
|
234
|
+
value["class"],
|
|
235
|
+
value["path"],
|
|
236
|
+
)
|
|
237
|
+
target = f"{value['module']}.{value['class']}"
|
|
238
|
+
if cls is not None:
|
|
239
|
+
if self._utils.is_persistent_message_class(cls):
|
|
240
|
+
return "persistent_message", target
|
|
241
|
+
if self._utils._is_message_schema_class(cls):
|
|
242
|
+
return "message_schema", target
|
|
243
|
+
return "component", target
|
|
244
|
+
if "path" in value and "package" in value:
|
|
245
|
+
return "component", f"{value['package']} package"
|
|
246
|
+
if "path" in value and "file" in value:
|
|
247
|
+
return "component", value["file"]
|
|
248
|
+
if "path" in value:
|
|
249
|
+
return "component", value["path"]
|
|
250
|
+
|
|
251
|
+
raise ValueError(f"Invalid migration class entry: {value!r}.")
|
|
252
|
+
|
|
253
|
+
def _dict_class_registration(
|
|
254
|
+
self,
|
|
255
|
+
iris_classname: str,
|
|
256
|
+
value: dict[str, Any],
|
|
257
|
+
*,
|
|
258
|
+
source: str,
|
|
259
|
+
) -> ClassRegistration:
|
|
260
|
+
if "path" in value and "module" in value and "class" in value:
|
|
261
|
+
cls = self._utils._try_import_class(
|
|
262
|
+
value["module"],
|
|
263
|
+
value["class"],
|
|
264
|
+
value["path"],
|
|
265
|
+
)
|
|
266
|
+
target = f"{value['module']}.{value['class']}"
|
|
267
|
+
if cls is not None:
|
|
268
|
+
if self._utils.is_persistent_message_class(cls):
|
|
269
|
+
return ClassRegistration(
|
|
270
|
+
iris_classname=iris_classname,
|
|
271
|
+
action="persistent_message",
|
|
272
|
+
target=target,
|
|
273
|
+
plan_kind="PersistentMessage",
|
|
274
|
+
source=source,
|
|
275
|
+
cls=cls,
|
|
276
|
+
)
|
|
277
|
+
if self._utils._is_message_schema_class(cls):
|
|
278
|
+
raise ValueError(
|
|
279
|
+
f"{target} is a Message/PydanticMessage and cannot be "
|
|
280
|
+
"registered in CLASSES. Use SCHEMAS if you need DTL "
|
|
281
|
+
"support. Otherwise, no migration is required for this "
|
|
282
|
+
"message."
|
|
283
|
+
)
|
|
284
|
+
return ClassRegistration(
|
|
285
|
+
iris_classname=iris_classname,
|
|
286
|
+
action="component_descriptor",
|
|
287
|
+
target=target,
|
|
288
|
+
source=source,
|
|
289
|
+
module=value["module"],
|
|
290
|
+
classname=value["class"],
|
|
291
|
+
path=value["path"],
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if "path" in value and "package" in value:
|
|
295
|
+
return ClassRegistration(
|
|
296
|
+
iris_classname=iris_classname,
|
|
297
|
+
action="package",
|
|
298
|
+
target=f"{value['package']} package",
|
|
299
|
+
source=source,
|
|
300
|
+
package=value["package"],
|
|
301
|
+
path=value["path"],
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
if "path" in value and "file" in value:
|
|
305
|
+
return ClassRegistration(
|
|
306
|
+
iris_classname=iris_classname,
|
|
307
|
+
action="file",
|
|
308
|
+
target=value["file"],
|
|
309
|
+
source=source,
|
|
310
|
+
file=value["file"],
|
|
311
|
+
path=value["path"],
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if "path" in value:
|
|
315
|
+
return ClassRegistration(
|
|
316
|
+
iris_classname=iris_classname,
|
|
317
|
+
action="folder",
|
|
318
|
+
target=value["path"],
|
|
319
|
+
source=source,
|
|
320
|
+
path=value["path"],
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
raise ValueError(f"Invalid value for {iris_classname}.")
|
|
324
|
+
|
|
325
|
+
def build_schema_registrations(
|
|
326
|
+
self,
|
|
327
|
+
schemas: list[type] | None,
|
|
328
|
+
) -> list[SchemaRegistration]:
|
|
329
|
+
if schemas is None:
|
|
330
|
+
return []
|
|
331
|
+
if not isinstance(schemas, list):
|
|
332
|
+
raise ValueError("SCHEMAS must be a list of message classes.")
|
|
333
|
+
registrations: list[SchemaRegistration] = []
|
|
334
|
+
for cls in schemas:
|
|
335
|
+
self._utils._validate_dtl_schema_class(cls, "SCHEMAS")
|
|
336
|
+
registrations.append(
|
|
337
|
+
SchemaRegistration(
|
|
338
|
+
schema_class=cls,
|
|
339
|
+
target=self._utils._python_classname(cls),
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
return registrations
|
|
343
|
+
|
|
344
|
+
def build_production_registrations(
|
|
345
|
+
self,
|
|
346
|
+
productions: list[Any] | None,
|
|
347
|
+
root_path: str | None = None,
|
|
348
|
+
*,
|
|
349
|
+
strict_production_validation: bool = False,
|
|
350
|
+
) -> list[ProductionRegistration]:
|
|
351
|
+
if productions is None:
|
|
352
|
+
return []
|
|
353
|
+
if not isinstance(productions, list):
|
|
354
|
+
raise ValueError("PRODUCTIONS must be a list.")
|
|
355
|
+
|
|
356
|
+
registrations: list[ProductionRegistration] = []
|
|
357
|
+
for production in productions:
|
|
358
|
+
registration = self.production_registration(production, root_path)
|
|
359
|
+
if strict_production_validation and registration.validation_report.has_issues:
|
|
360
|
+
raise ProductionValidationError(registration.validation_report)
|
|
361
|
+
registrations.append(registration)
|
|
362
|
+
return registrations
|
|
363
|
+
|
|
364
|
+
def production_registration(
|
|
365
|
+
self,
|
|
366
|
+
production: Any,
|
|
367
|
+
root_path: str | None = None,
|
|
368
|
+
) -> ProductionRegistration:
|
|
369
|
+
plan_report = validate_production_entry(production, strict=False, warn=False)
|
|
370
|
+
if self._utils._is_production_object(production):
|
|
371
|
+
definition, extra_registrations = self.normalize_production_dict(
|
|
372
|
+
production.to_dict(),
|
|
373
|
+
root_path,
|
|
374
|
+
)
|
|
375
|
+
class_registrations = [
|
|
376
|
+
*self.production_object_class_registrations(production, root_path),
|
|
377
|
+
*extra_registrations,
|
|
378
|
+
]
|
|
379
|
+
return ProductionRegistration(
|
|
380
|
+
name=production.name,
|
|
381
|
+
definition=definition,
|
|
382
|
+
validation_subject=production,
|
|
383
|
+
validation_report=plan_report,
|
|
384
|
+
source="production_object",
|
|
385
|
+
class_registrations=tuple(class_registrations),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if not isinstance(production, dict) or not production:
|
|
389
|
+
raise ValueError("Each PRODUCTION entry must be a non-empty dict.")
|
|
390
|
+
|
|
391
|
+
production_copy = copy.deepcopy(production)
|
|
392
|
+
production_name = next(iter(production_copy.keys()))
|
|
393
|
+
definition, class_registrations = self.normalize_production_dict(
|
|
394
|
+
production_copy,
|
|
395
|
+
root_path,
|
|
396
|
+
)
|
|
397
|
+
return ProductionRegistration(
|
|
398
|
+
name=production_name,
|
|
399
|
+
definition=definition,
|
|
400
|
+
validation_subject=definition,
|
|
401
|
+
validation_report=plan_report,
|
|
402
|
+
source="legacy_dict",
|
|
403
|
+
class_registrations=tuple(class_registrations),
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def production_object_class_registrations(
|
|
407
|
+
self,
|
|
408
|
+
production,
|
|
409
|
+
root_path: str | None = None,
|
|
410
|
+
) -> list[ClassRegistration]:
|
|
411
|
+
registrations: list[ClassRegistration] = []
|
|
412
|
+
registered: set[tuple[str, str, str]] = set()
|
|
413
|
+
|
|
414
|
+
for registration in getattr(production, "message_registrations", lambda: ())():
|
|
415
|
+
registrations.append(
|
|
416
|
+
ClassRegistration(
|
|
417
|
+
iris_classname=registration.iris_classname,
|
|
418
|
+
action="persistent_message",
|
|
419
|
+
target=self._utils._python_classname(registration.message_class),
|
|
420
|
+
plan_kind="PersistentMessage",
|
|
421
|
+
source="production",
|
|
422
|
+
cls=registration.message_class,
|
|
423
|
+
sync_schema=registration.sync_schema,
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
for item in production.component_registrations():
|
|
428
|
+
cls = item.component_class
|
|
429
|
+
if not inspect.isclass(cls):
|
|
430
|
+
continue
|
|
431
|
+
key = (item.class_name, cls.__module__, cls.__name__)
|
|
432
|
+
if key in registered:
|
|
433
|
+
continue
|
|
434
|
+
registered.add(key)
|
|
435
|
+
registrations.append(
|
|
436
|
+
ClassRegistration(
|
|
437
|
+
iris_classname=item.class_name,
|
|
438
|
+
action="component_class",
|
|
439
|
+
target=self._utils._python_classname(cls),
|
|
440
|
+
source="production",
|
|
441
|
+
cls=cls,
|
|
442
|
+
module=cls.__module__,
|
|
443
|
+
classname=cls.__name__,
|
|
444
|
+
path=self._class_path(cls, root_path),
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
for item in getattr(production, "adapter_registrations", lambda: ())():
|
|
449
|
+
cls = item.adapter_class
|
|
450
|
+
if not inspect.isclass(cls):
|
|
451
|
+
continue
|
|
452
|
+
key = (item.adapter_class_name, cls.__module__, cls.__name__)
|
|
453
|
+
if key in registered:
|
|
454
|
+
continue
|
|
455
|
+
registered.add(key)
|
|
456
|
+
registrations.append(
|
|
457
|
+
ClassRegistration(
|
|
458
|
+
iris_classname=item.adapter_class_name,
|
|
459
|
+
action="component_class",
|
|
460
|
+
target=self._utils._python_classname(cls),
|
|
461
|
+
source="production",
|
|
462
|
+
cls=cls,
|
|
463
|
+
module=cls.__module__,
|
|
464
|
+
classname=cls.__name__,
|
|
465
|
+
path=self._class_path(cls, root_path),
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
return registrations
|
|
469
|
+
|
|
470
|
+
def normalize_production_dict(
|
|
471
|
+
self,
|
|
472
|
+
production: dict[str, Any],
|
|
473
|
+
root_path: str | None = None,
|
|
474
|
+
) -> tuple[dict[str, Any], list[ClassRegistration]]:
|
|
475
|
+
if not isinstance(production, dict) or not production:
|
|
476
|
+
raise ValueError("Each PRODUCTION entry must be a non-empty dict.")
|
|
477
|
+
|
|
478
|
+
production_name = next(iter(production.keys()))
|
|
479
|
+
production["Production"] = production.pop(production_name)
|
|
480
|
+
return production, self.normalize_production_items(production, root_path)
|
|
481
|
+
|
|
482
|
+
def normalize_production_items(
|
|
483
|
+
self,
|
|
484
|
+
production: dict[str, Any],
|
|
485
|
+
root_path: str | None = None,
|
|
486
|
+
) -> list[ClassRegistration]:
|
|
487
|
+
registrations: list[ClassRegistration] = []
|
|
488
|
+
if "Item" not in production["Production"]:
|
|
489
|
+
return registrations
|
|
490
|
+
|
|
491
|
+
for index, item in enumerate(production["Production"]["Item"]):
|
|
492
|
+
if "@ClassName" not in item:
|
|
493
|
+
raise ValueError(f"Missing @ClassName for {item.get('@Name')}.")
|
|
494
|
+
class_ref = item["@ClassName"]
|
|
495
|
+
item_name = item["@Name"]
|
|
496
|
+
if inspect.isclass(class_ref):
|
|
497
|
+
registrations.append(
|
|
498
|
+
ClassRegistration(
|
|
499
|
+
iris_classname=item_name,
|
|
500
|
+
action="component_class",
|
|
501
|
+
target=self._utils._python_classname(class_ref),
|
|
502
|
+
source="production",
|
|
503
|
+
cls=class_ref,
|
|
504
|
+
module=class_ref.__module__,
|
|
505
|
+
classname=class_ref.__name__,
|
|
506
|
+
path=self._class_path(class_ref, root_path),
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
production["Production"]["Item"][index]["@ClassName"] = item_name
|
|
510
|
+
elif isinstance(class_ref, dict):
|
|
511
|
+
registrations.extend(
|
|
512
|
+
self.build_class_registrations(
|
|
513
|
+
{item_name: class_ref},
|
|
514
|
+
None,
|
|
515
|
+
source="production",
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
production["Production"]["Item"][index]["@ClassName"] = item_name
|
|
519
|
+
elif not isinstance(class_ref, str):
|
|
520
|
+
raise ValueError(f"Invalid value for {item_name}.")
|
|
521
|
+
|
|
522
|
+
return registrations
|
|
523
|
+
|
|
524
|
+
@staticmethod
|
|
525
|
+
def _class_path(cls: type, root_path: str | None) -> str:
|
|
526
|
+
if root_path:
|
|
527
|
+
return root_path
|
|
528
|
+
return os.path.dirname(inspect.getfile(cls))
|
|
529
|
+
|
|
530
|
+
@staticmethod
|
|
531
|
+
def _module_path(module, root_path: str | None) -> str:
|
|
532
|
+
if root_path:
|
|
533
|
+
return root_path
|
|
534
|
+
return os.path.dirname(inspect.getfile(module))
|
iop/migration/plans.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .manifest import MigrationManifestBuilder
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def format_migration_success(filename: str, namespace: str | None = None) -> str:
|
|
11
|
+
suffix = f" in namespace {namespace}" if namespace else ""
|
|
12
|
+
return f"Migration succeeded{suffix}: {filename}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def format_migration_plan(plan: dict[str, Any]) -> str:
|
|
16
|
+
"""Format a migration plan for CLI and migration output."""
|
|
17
|
+
lines = [f"Migration plan: {plan['settings']}"]
|
|
18
|
+
if plan.get("mode"):
|
|
19
|
+
lines.append(f"Mode: {plan['mode']}")
|
|
20
|
+
if plan.get("namespace"):
|
|
21
|
+
lines.append(f"Namespace: {plan['namespace']}")
|
|
22
|
+
lines.append("")
|
|
23
|
+
lines.extend(format_plan_section("CLASSES", plan["classes"]))
|
|
24
|
+
lines.extend(format_plan_section("SCHEMAS", plan["schemas"]))
|
|
25
|
+
lines.extend(format_plan_section("PRODUCTIONS", plan["productions"]))
|
|
26
|
+
lines.extend(format_plan_section("VALIDATION", plan.get("validation", [])))
|
|
27
|
+
return "\n".join(lines)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def format_plan_section(title: str, entries: list[str]) -> list[str]:
|
|
31
|
+
lines = [f"{title}:"]
|
|
32
|
+
if entries:
|
|
33
|
+
lines.extend(f" {entry}" for entry in entries)
|
|
34
|
+
else:
|
|
35
|
+
lines.append(" none")
|
|
36
|
+
lines.append("")
|
|
37
|
+
return lines
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MigrationPlanner:
|
|
41
|
+
"""Build migration plan payloads from the normalized migration manifest."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, utils):
|
|
44
|
+
self._utils = utils
|
|
45
|
+
|
|
46
|
+
def build(
|
|
47
|
+
self,
|
|
48
|
+
settings,
|
|
49
|
+
path,
|
|
50
|
+
filename=None,
|
|
51
|
+
mode: str | None = None,
|
|
52
|
+
namespace: str | None = None,
|
|
53
|
+
strict_production_validation: bool = False,
|
|
54
|
+
) -> dict[str, Any]:
|
|
55
|
+
"""Build and validate a migration plan from a settings module."""
|
|
56
|
+
if not path:
|
|
57
|
+
path = self._settings_path(settings)
|
|
58
|
+
|
|
59
|
+
manifest = MigrationManifestBuilder(self._utils).build(
|
|
60
|
+
settings,
|
|
61
|
+
path,
|
|
62
|
+
filename=filename,
|
|
63
|
+
mode=mode,
|
|
64
|
+
namespace=namespace,
|
|
65
|
+
strict_production_validation=strict_production_validation,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"settings": manifest.settings,
|
|
70
|
+
"mode": mode,
|
|
71
|
+
"namespace": namespace,
|
|
72
|
+
"classes": manifest.plan_class_entries(),
|
|
73
|
+
"schemas": manifest.plan_schema_entries(),
|
|
74
|
+
"productions": manifest.plan_production_entries(),
|
|
75
|
+
"validation": manifest.plan_validation_entries(),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def _settings_path(settings) -> str:
|
|
80
|
+
return os.path.dirname(inspect.getfile(settings))
|