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,610 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import keyword
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import is_dataclass
|
|
7
|
+
from pprint import pformat
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from .common import PRODUCTION_SETTING_FIELDS, _bool_text, _text_value
|
|
13
|
+
from .types import GraphNode, ProductionGraph
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def production_to_dict(production) -> dict[str, Any]:
|
|
17
|
+
data: dict[str, Any] = {
|
|
18
|
+
"@Name": production.name,
|
|
19
|
+
"@TestingEnabled": _bool_text(production.testing_enabled),
|
|
20
|
+
"@LogGeneralTraceEvents": _bool_text(production.log_general_trace_events),
|
|
21
|
+
"Description": production.description,
|
|
22
|
+
"ActorPoolSize": _text_value(production.actor_pool_size),
|
|
23
|
+
}
|
|
24
|
+
production_settings = _production_settings_to_iris(production)
|
|
25
|
+
if production_settings:
|
|
26
|
+
data["Setting"] = production_settings
|
|
27
|
+
if production._items:
|
|
28
|
+
data["Item"] = [item.to_dict() for item in production._items]
|
|
29
|
+
return {production.name: data}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _production_settings_to_iris(production) -> list[dict[str, str]]:
|
|
33
|
+
settings: list[dict[str, str]] = []
|
|
34
|
+
for field_name, (iris_name, default) in PRODUCTION_SETTING_FIELDS.items():
|
|
35
|
+
value = getattr(production, field_name)
|
|
36
|
+
if _text_value(value) == _text_value(default):
|
|
37
|
+
continue
|
|
38
|
+
settings.append(
|
|
39
|
+
{
|
|
40
|
+
"@Name": iris_name,
|
|
41
|
+
"#text": _text_value(value),
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
return settings
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def production_to_xml(production) -> str:
|
|
48
|
+
from ..migration.io import dict_to_xml
|
|
49
|
+
|
|
50
|
+
return dict_to_xml({"Production": production_to_dict(production)[production.name]})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def production_to_python(production) -> str:
|
|
54
|
+
names = {item.name for item in production.items}
|
|
55
|
+
variables = _component_variable_names(production.items)
|
|
56
|
+
route_targets = _route_targets(production, names)
|
|
57
|
+
|
|
58
|
+
lines = [
|
|
59
|
+
"# Generated from IRIS production export.",
|
|
60
|
+
"# Review before using as source of truth; some runtime/dynamic routing intent",
|
|
61
|
+
"# cannot be fully reconstructed from deployed IRIS metadata.",
|
|
62
|
+
"from iop import Production",
|
|
63
|
+
"",
|
|
64
|
+
"",
|
|
65
|
+
]
|
|
66
|
+
lines.extend(_production_constructor_lines(production))
|
|
67
|
+
lines.append("")
|
|
68
|
+
|
|
69
|
+
for item in production.items:
|
|
70
|
+
lines.extend(_component_lines(item, variables[item.name], route_targets))
|
|
71
|
+
lines.append("")
|
|
72
|
+
|
|
73
|
+
route_lines = _connection_lines(production, variables, names)
|
|
74
|
+
if route_lines:
|
|
75
|
+
lines.extend(route_lines)
|
|
76
|
+
lines.append("")
|
|
77
|
+
|
|
78
|
+
if production.graph().warnings:
|
|
79
|
+
lines.append("# Import warnings:")
|
|
80
|
+
lines.extend(f"# - {warning}" for warning in production.graph().warnings)
|
|
81
|
+
lines.append("")
|
|
82
|
+
|
|
83
|
+
lines.append("PRODUCTIONS = [prod]")
|
|
84
|
+
return "\n".join(lines) + "\n"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def production_to_class(production) -> str:
|
|
88
|
+
item_names = {item.name for item in production.items}
|
|
89
|
+
route_targets = _valid_route_targets(
|
|
90
|
+
_route_targets(production, item_names),
|
|
91
|
+
item_names,
|
|
92
|
+
)
|
|
93
|
+
item_groups = _class_item_groups(production.items)
|
|
94
|
+
used_items = {
|
|
95
|
+
_class_item_type(kind)
|
|
96
|
+
for kind, items in item_groups.items()
|
|
97
|
+
if items
|
|
98
|
+
}
|
|
99
|
+
has_routes = bool(route_targets)
|
|
100
|
+
imports = ["Production", *sorted(used_items)]
|
|
101
|
+
if has_routes:
|
|
102
|
+
imports.append("Route")
|
|
103
|
+
|
|
104
|
+
class_name = _production_class_name(production.name)
|
|
105
|
+
lines = [
|
|
106
|
+
"# Generated from IRIS production export.",
|
|
107
|
+
"# Review before using as source of truth; some runtime/dynamic routing intent",
|
|
108
|
+
"# cannot be fully reconstructed from deployed IRIS metadata.",
|
|
109
|
+
]
|
|
110
|
+
if _has_string_python_proxy_items(production.items):
|
|
111
|
+
lines.extend(
|
|
112
|
+
[
|
|
113
|
+
"# TODO: replace Python.* string class names with imported Python",
|
|
114
|
+
"# classes before re-migration, or ensure the proxy classes already exist.",
|
|
115
|
+
]
|
|
116
|
+
)
|
|
117
|
+
lines.extend(
|
|
118
|
+
[
|
|
119
|
+
f"from iop import {', '.join(imports)}",
|
|
120
|
+
"",
|
|
121
|
+
"",
|
|
122
|
+
f"class {class_name}(Production):",
|
|
123
|
+
f" name = {_literal(production.name)}",
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
lines.extend(_class_production_setting_lines(production))
|
|
127
|
+
lines.append("")
|
|
128
|
+
|
|
129
|
+
for kind in ("component", "service", "process", "operation"):
|
|
130
|
+
items = item_groups.get(kind, [])
|
|
131
|
+
if not items:
|
|
132
|
+
continue
|
|
133
|
+
attr_name = _class_item_attr(kind)
|
|
134
|
+
lines.append(f" {attr_name} = (")
|
|
135
|
+
for item in items:
|
|
136
|
+
lines.extend(_class_item_lines(item, kind, route_targets))
|
|
137
|
+
lines.append(" )")
|
|
138
|
+
lines.append("")
|
|
139
|
+
|
|
140
|
+
unresolved = _unresolved_class_routes(production, item_names)
|
|
141
|
+
if unresolved:
|
|
142
|
+
lines.append(" # TODO: review unresolved or runtime-only routes:")
|
|
143
|
+
lines.extend(f" # - {value}" for value in unresolved)
|
|
144
|
+
lines.append("")
|
|
145
|
+
if production.graph().warnings:
|
|
146
|
+
lines.append(" # Import warnings:")
|
|
147
|
+
lines.extend(f" # - {warning}" for warning in production.graph().warnings)
|
|
148
|
+
lines.append("")
|
|
149
|
+
|
|
150
|
+
lines.append("")
|
|
151
|
+
lines.append(f"PRODUCTIONS = [{class_name}()]")
|
|
152
|
+
return "\n".join(lines) + "\n"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _class_production_setting_lines(production) -> list[str]:
|
|
156
|
+
return [
|
|
157
|
+
f" {name} = {_literal(value)}"
|
|
158
|
+
for name, value, default in _production_setting_literals(production)
|
|
159
|
+
if value != default
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _production_setting_literals(production) -> list[tuple[str, Any, Any]]:
|
|
164
|
+
return [
|
|
165
|
+
("testing_enabled", _bool_literal(production.testing_enabled), False),
|
|
166
|
+
(
|
|
167
|
+
"log_general_trace_events",
|
|
168
|
+
_bool_literal(production.log_general_trace_events),
|
|
169
|
+
False,
|
|
170
|
+
),
|
|
171
|
+
("actor_pool_size", _int_literal(production.actor_pool_size), 2),
|
|
172
|
+
("description", production.description, ""),
|
|
173
|
+
("shutdown_timeout", _int_literal(production.shutdown_timeout), 120),
|
|
174
|
+
("update_timeout", _int_literal(production.update_timeout), 10),
|
|
175
|
+
("alert_notification_manager", production.alert_notification_manager, ""),
|
|
176
|
+
("alert_notification_operation", production.alert_notification_operation, ""),
|
|
177
|
+
("alert_notification_recipients", production.alert_notification_recipients, ""),
|
|
178
|
+
("alert_action_window", _int_literal(production.alert_action_window), 60),
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _class_item_groups(items) -> dict[str, list[Any]]:
|
|
183
|
+
groups: dict[str, list[Any]] = {
|
|
184
|
+
"component": [],
|
|
185
|
+
"service": [],
|
|
186
|
+
"process": [],
|
|
187
|
+
"operation": [],
|
|
188
|
+
}
|
|
189
|
+
for item in items:
|
|
190
|
+
groups[_class_item_kind(item)].append(item)
|
|
191
|
+
return groups
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _class_item_kind(item) -> str:
|
|
195
|
+
if item.kind in {"service", "process", "operation"}:
|
|
196
|
+
return item.kind
|
|
197
|
+
return "component"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _class_item_type(kind: str) -> str:
|
|
201
|
+
return {
|
|
202
|
+
"component": "ComponentItem",
|
|
203
|
+
"service": "ServiceItem",
|
|
204
|
+
"process": "ProcessItem",
|
|
205
|
+
"operation": "OperationItem",
|
|
206
|
+
}[kind]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _class_item_attr(kind: str) -> str:
|
|
210
|
+
return {
|
|
211
|
+
"component": "components",
|
|
212
|
+
"service": "services",
|
|
213
|
+
"process": "processes",
|
|
214
|
+
"operation": "operations",
|
|
215
|
+
}[kind]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _class_item_lines(item, kind: str, route_targets) -> list[str]:
|
|
219
|
+
kwargs: list[tuple[str, Any]] = []
|
|
220
|
+
optional_fields = [
|
|
221
|
+
("category", item.category, ""),
|
|
222
|
+
("pool_size", _int_literal(item.pool_size), 1),
|
|
223
|
+
("enabled", _bool_literal(item.enabled), True),
|
|
224
|
+
("foreground", _bool_literal(item.foreground), False),
|
|
225
|
+
("comment", item.comment, ""),
|
|
226
|
+
("log_trace_events", _bool_literal(item.log_trace_events), False),
|
|
227
|
+
("schedule", item.schedule, ""),
|
|
228
|
+
]
|
|
229
|
+
kwargs.extend(
|
|
230
|
+
(name, value)
|
|
231
|
+
for name, value, default in optional_fields
|
|
232
|
+
if value != default
|
|
233
|
+
)
|
|
234
|
+
settings = {
|
|
235
|
+
name: value
|
|
236
|
+
for name, value in item.host_settings.items()
|
|
237
|
+
if (item.name, name) not in route_targets
|
|
238
|
+
}
|
|
239
|
+
if settings:
|
|
240
|
+
kwargs.append(("settings", settings))
|
|
241
|
+
if item.adapter_settings:
|
|
242
|
+
kwargs.append(("adapter_settings", dict(item.adapter_settings)))
|
|
243
|
+
if item.other_settings:
|
|
244
|
+
kwargs.append(("other_settings", [dict(value) for value in item.other_settings]))
|
|
245
|
+
|
|
246
|
+
routes = _class_item_route_values(item, route_targets)
|
|
247
|
+
if routes:
|
|
248
|
+
kwargs.append(("routes", routes))
|
|
249
|
+
|
|
250
|
+
lines = []
|
|
251
|
+
if _is_string_python_proxy_item(item):
|
|
252
|
+
lines.extend(
|
|
253
|
+
[
|
|
254
|
+
" # TODO: replace this proxy class name with the Python",
|
|
255
|
+
" # class object if this item should be auto-registered.",
|
|
256
|
+
]
|
|
257
|
+
)
|
|
258
|
+
lines.extend(
|
|
259
|
+
[
|
|
260
|
+
f" {_class_item_type(kind)}(",
|
|
261
|
+
f" {_literal(item.name)},",
|
|
262
|
+
f" {_literal(item.class_name or '')},",
|
|
263
|
+
]
|
|
264
|
+
)
|
|
265
|
+
for name, value in kwargs:
|
|
266
|
+
lines.extend(_class_keyword_lines(name, value))
|
|
267
|
+
lines.append(" ),")
|
|
268
|
+
return lines
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _class_item_route_values(item, route_targets) -> list[tuple[str, list[str]]]:
|
|
272
|
+
values: list[tuple[str, list[str]]] = []
|
|
273
|
+
for source_item, source_target_setting in sorted(route_targets):
|
|
274
|
+
if source_item != item.name:
|
|
275
|
+
continue
|
|
276
|
+
targets = route_targets[(source_item, source_target_setting)]
|
|
277
|
+
values.append((source_target_setting, list(targets)))
|
|
278
|
+
return values
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _valid_route_targets(
|
|
282
|
+
route_targets: dict[tuple[str, str], list[str]],
|
|
283
|
+
item_names: set[str],
|
|
284
|
+
) -> dict[tuple[str, str], list[str]]:
|
|
285
|
+
valid_routes: dict[tuple[str, str], list[str]] = {}
|
|
286
|
+
for source, targets in route_targets.items():
|
|
287
|
+
valid_targets = [target for target in targets if target in item_names]
|
|
288
|
+
if valid_targets:
|
|
289
|
+
valid_routes[source] = valid_targets
|
|
290
|
+
return valid_routes
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _class_keyword_lines(name: str, value: Any) -> list[str]:
|
|
294
|
+
if isinstance(value, dict):
|
|
295
|
+
return _indented_dict_keyword_lines(name, value, indent=12)
|
|
296
|
+
if isinstance(value, list) and name == "routes":
|
|
297
|
+
return _class_route_keyword_lines(value)
|
|
298
|
+
if isinstance(value, list):
|
|
299
|
+
return _indented_list_keyword_lines(name, value, indent=12)
|
|
300
|
+
return [f" {name}={_literal(value)},"]
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _class_route_keyword_lines(routes: list[tuple[str, list[str]]]) -> list[str]:
|
|
304
|
+
if len(routes) == 1:
|
|
305
|
+
target_setting, targets = routes[0]
|
|
306
|
+
target_literal = _class_route_targets_literal(targets)
|
|
307
|
+
return [
|
|
308
|
+
f" routes=(Route({_literal(target_setting)}, {target_literal}),),"
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
lines = [" routes=("]
|
|
312
|
+
for target_setting, targets in routes:
|
|
313
|
+
target_literal = _class_route_targets_literal(targets)
|
|
314
|
+
lines.append(f" Route({_literal(target_setting)}, {target_literal}),")
|
|
315
|
+
lines.append(" ),")
|
|
316
|
+
return lines
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _class_route_targets_literal(targets: list[str]) -> str:
|
|
320
|
+
return _literal(targets[0]) if len(targets) == 1 else _literal(tuple(targets))
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _has_string_python_proxy_items(items) -> bool:
|
|
324
|
+
return any(_is_string_python_proxy_item(item) for item in items)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _is_string_python_proxy_item(item) -> bool:
|
|
328
|
+
return str(item.class_name or "").startswith("Python.")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _indented_dict_keyword_lines(
|
|
332
|
+
name: str,
|
|
333
|
+
value: dict[str, Any],
|
|
334
|
+
*,
|
|
335
|
+
indent: int,
|
|
336
|
+
) -> list[str]:
|
|
337
|
+
prefix = " " * indent
|
|
338
|
+
item_prefix = " " * (indent + 4)
|
|
339
|
+
lines = [f"{prefix}{name}={{"]
|
|
340
|
+
for key, item in value.items():
|
|
341
|
+
lines.append(f"{item_prefix}{_literal(key)}: {_literal(item)},")
|
|
342
|
+
lines.append(f"{prefix}}},")
|
|
343
|
+
return lines
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _indented_list_keyword_lines(
|
|
347
|
+
name: str,
|
|
348
|
+
value: list[Any],
|
|
349
|
+
*,
|
|
350
|
+
indent: int,
|
|
351
|
+
) -> list[str]:
|
|
352
|
+
prefix = " " * indent
|
|
353
|
+
item_prefix = " " * (indent + 4)
|
|
354
|
+
lines = [f"{prefix}{name}=["]
|
|
355
|
+
for item in value:
|
|
356
|
+
lines.append(f"{item_prefix}{_literal(item)},")
|
|
357
|
+
lines.append(f"{prefix}],")
|
|
358
|
+
return lines
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _unresolved_class_routes(production, item_names: set[str]) -> list[str]:
|
|
362
|
+
unresolved: list[str] = []
|
|
363
|
+
for edge in production.edges:
|
|
364
|
+
if not edge.source_target_setting:
|
|
365
|
+
unresolved.append(f"{edge.source_item} -> {edge.target}")
|
|
366
|
+
elif edge.target not in item_names:
|
|
367
|
+
unresolved.append(
|
|
368
|
+
f"{edge.source_item}.{edge.source_target_setting} -> {edge.target}"
|
|
369
|
+
)
|
|
370
|
+
return sorted(set(unresolved))
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _production_class_name(production_name: str) -> str:
|
|
374
|
+
parts = [
|
|
375
|
+
part
|
|
376
|
+
for part in re.split(r"\W+", production_name)
|
|
377
|
+
if part
|
|
378
|
+
]
|
|
379
|
+
if not parts:
|
|
380
|
+
return "GeneratedProduction"
|
|
381
|
+
candidate = parts[-1]
|
|
382
|
+
if candidate == "Production" and len(parts) > 1:
|
|
383
|
+
candidate = "".join(parts[-2:])
|
|
384
|
+
else:
|
|
385
|
+
candidate = "".join(
|
|
386
|
+
part[:1].upper() + part[1:]
|
|
387
|
+
for part in re.split(r"[_\s]+", candidate)
|
|
388
|
+
if part
|
|
389
|
+
)
|
|
390
|
+
if not candidate or not candidate[0].isalpha():
|
|
391
|
+
candidate = f"Generated{candidate}"
|
|
392
|
+
if keyword.iskeyword(candidate) or candidate == "Production":
|
|
393
|
+
candidate = f"{candidate}Definition"
|
|
394
|
+
return candidate
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _production_constructor_lines(production) -> list[str]:
|
|
398
|
+
rendered = [
|
|
399
|
+
(name, value)
|
|
400
|
+
for name, value, default in _production_setting_literals(production)
|
|
401
|
+
if value != default
|
|
402
|
+
]
|
|
403
|
+
if not rendered:
|
|
404
|
+
return [f"prod = Production({_literal(production.name)})"]
|
|
405
|
+
|
|
406
|
+
lines = [f"prod = Production({_literal(production.name)},"]
|
|
407
|
+
for name, value in rendered:
|
|
408
|
+
lines.append(f" {name}={_literal(value)},")
|
|
409
|
+
lines.append(")")
|
|
410
|
+
return lines
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _component_lines(item, variable_name: str, route_targets) -> list[str]:
|
|
414
|
+
kwargs: list[tuple[str, Any]] = [
|
|
415
|
+
("class_name", item.class_name or ""),
|
|
416
|
+
]
|
|
417
|
+
optional_fields = [
|
|
418
|
+
("kind", item.kind, "component"),
|
|
419
|
+
("category", item.category, ""),
|
|
420
|
+
("pool_size", _int_literal(item.pool_size), 1),
|
|
421
|
+
("enabled", _bool_literal(item.enabled), True),
|
|
422
|
+
("foreground", _bool_literal(item.foreground), False),
|
|
423
|
+
("comment", item.comment, ""),
|
|
424
|
+
("log_trace_events", _bool_literal(item.log_trace_events), False),
|
|
425
|
+
("schedule", item.schedule, ""),
|
|
426
|
+
]
|
|
427
|
+
kwargs.extend(
|
|
428
|
+
(name, value)
|
|
429
|
+
for name, value, default in optional_fields
|
|
430
|
+
if value != default
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
settings = {
|
|
434
|
+
name: value
|
|
435
|
+
for name, value in item.host_settings.items()
|
|
436
|
+
if (item.name, name) not in route_targets
|
|
437
|
+
}
|
|
438
|
+
if settings:
|
|
439
|
+
kwargs.append(("settings", settings))
|
|
440
|
+
if item.adapter_settings:
|
|
441
|
+
kwargs.append(("adapter_settings", dict(item.adapter_settings)))
|
|
442
|
+
|
|
443
|
+
lines = [f"{variable_name} = prod.component("]
|
|
444
|
+
lines.append(f" {_literal(item.name)},")
|
|
445
|
+
for name, value in kwargs:
|
|
446
|
+
lines.extend(_keyword_lines(name, value))
|
|
447
|
+
lines.append(")")
|
|
448
|
+
|
|
449
|
+
for setting in item.other_settings:
|
|
450
|
+
target = setting.get("@Target", "")
|
|
451
|
+
name = setting.get("@Name", "")
|
|
452
|
+
value = setting.get("#text", "")
|
|
453
|
+
lines.append(
|
|
454
|
+
f"{variable_name}.other_setting({_literal(target)}, "
|
|
455
|
+
f"{_literal(name)}, {_literal(value)})"
|
|
456
|
+
)
|
|
457
|
+
return lines
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _connection_lines(production, variables: dict[str, str], item_names: set[str]) -> list[str]:
|
|
461
|
+
grouped = _route_targets(production, item_names)
|
|
462
|
+
lines: list[str] = []
|
|
463
|
+
unresolved: list[str] = []
|
|
464
|
+
|
|
465
|
+
for source_item, source_target_setting in sorted(grouped):
|
|
466
|
+
targets = grouped[(source_item, source_target_setting)]
|
|
467
|
+
source_var = variables[source_item]
|
|
468
|
+
source_expr = (
|
|
469
|
+
f"{source_var}.target_setting({_literal(source_target_setting)})"
|
|
470
|
+
)
|
|
471
|
+
valid_targets = [target for target in targets if target in variables]
|
|
472
|
+
invalid_targets = [target for target in targets if target not in variables]
|
|
473
|
+
for target in invalid_targets:
|
|
474
|
+
unresolved.append(f"{source_item}.{source_target_setting} -> {target}")
|
|
475
|
+
if not valid_targets:
|
|
476
|
+
continue
|
|
477
|
+
if len(valid_targets) == 1:
|
|
478
|
+
lines.append(
|
|
479
|
+
f"prod.connect({source_expr}, {variables[valid_targets[0]]})"
|
|
480
|
+
)
|
|
481
|
+
else:
|
|
482
|
+
lines.append(
|
|
483
|
+
f"prod.connect({source_expr}, {variables[valid_targets[0]]})"
|
|
484
|
+
)
|
|
485
|
+
for target in valid_targets[1:]:
|
|
486
|
+
lines.append(
|
|
487
|
+
f"prod.connect({source_expr}, {variables[target]}, mode='add')"
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
for edge in production.edges:
|
|
491
|
+
if not edge.source_target_setting:
|
|
492
|
+
unresolved.append(f"{edge.source_item} -> {edge.target}")
|
|
493
|
+
|
|
494
|
+
if unresolved:
|
|
495
|
+
lines.append("# TODO: review unresolved or runtime-only routes:")
|
|
496
|
+
lines.extend(f"# - {value}" for value in sorted(set(unresolved)))
|
|
497
|
+
return lines
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _route_targets(production, item_names: set[str]) -> dict[tuple[str, str], list[str]]:
|
|
501
|
+
grouped: dict[tuple[str, str], list[str]] = {}
|
|
502
|
+
for edge in production.edges:
|
|
503
|
+
if not edge.source_target_setting:
|
|
504
|
+
continue
|
|
505
|
+
if edge.source_item not in item_names:
|
|
506
|
+
continue
|
|
507
|
+
key = (edge.source_item, edge.source_target_setting)
|
|
508
|
+
grouped.setdefault(key, [])
|
|
509
|
+
if edge.target not in grouped[key]:
|
|
510
|
+
grouped[key].append(edge.target)
|
|
511
|
+
return grouped
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _component_variable_names(items) -> dict[str, str]:
|
|
515
|
+
names: dict[str, str] = {}
|
|
516
|
+
used: set[str] = set()
|
|
517
|
+
for item in items:
|
|
518
|
+
base = _safe_identifier(item.name)
|
|
519
|
+
candidate = base
|
|
520
|
+
index = 2
|
|
521
|
+
while candidate in used:
|
|
522
|
+
candidate = f"{base}_{index}"
|
|
523
|
+
index += 1
|
|
524
|
+
used.add(candidate)
|
|
525
|
+
names[item.name] = candidate
|
|
526
|
+
return names
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _safe_identifier(value: str) -> str:
|
|
530
|
+
candidate = re.sub(r"\W+", "_", value).strip("_").lower()
|
|
531
|
+
if not candidate:
|
|
532
|
+
candidate = "component"
|
|
533
|
+
if candidate[0].isdigit():
|
|
534
|
+
candidate = f"component_{candidate}"
|
|
535
|
+
if keyword.iskeyword(candidate):
|
|
536
|
+
candidate = f"{candidate}_component"
|
|
537
|
+
return candidate
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _keyword_lines(name: str, value: Any) -> list[str]:
|
|
541
|
+
if isinstance(value, dict):
|
|
542
|
+
return _dict_keyword_lines(name, value)
|
|
543
|
+
return [f" {name}={_literal(value)},"]
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _dict_keyword_lines(name: str, value: dict[str, Any]) -> list[str]:
|
|
547
|
+
if not value:
|
|
548
|
+
return []
|
|
549
|
+
lines = [f" {name}={{"]
|
|
550
|
+
for key, item in value.items():
|
|
551
|
+
lines.append(f" {_literal(key)}: {_literal(item)},")
|
|
552
|
+
lines.append(" },")
|
|
553
|
+
return lines
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _literal(value: Any) -> str:
|
|
557
|
+
if isinstance(value, dict):
|
|
558
|
+
return pformat(value, sort_dicts=False)
|
|
559
|
+
return repr(value)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _bool_literal(value: Any) -> bool | str:
|
|
563
|
+
if isinstance(value, bool):
|
|
564
|
+
return value
|
|
565
|
+
normalized = str(value).strip().lower()
|
|
566
|
+
if normalized in {"true", "1", "yes", "on"}:
|
|
567
|
+
return True
|
|
568
|
+
if normalized in {"false", "0", "no", "off"}:
|
|
569
|
+
return False
|
|
570
|
+
return str(value)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _int_literal(value: Any) -> int | str:
|
|
574
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
575
|
+
return value
|
|
576
|
+
text = str(value).strip()
|
|
577
|
+
if text.isdigit():
|
|
578
|
+
return int(text)
|
|
579
|
+
return text
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def production_graph(production) -> ProductionGraph:
|
|
583
|
+
nodes = tuple(
|
|
584
|
+
GraphNode(
|
|
585
|
+
name=item.name,
|
|
586
|
+
class_name=item.class_name or "",
|
|
587
|
+
kind=item.kind,
|
|
588
|
+
enabled=item.enabled,
|
|
589
|
+
category=item.category,
|
|
590
|
+
adapter_class_name=item.adapter_class_name,
|
|
591
|
+
)
|
|
592
|
+
for item in production._items
|
|
593
|
+
)
|
|
594
|
+
return ProductionGraph(
|
|
595
|
+
production_name=production.name,
|
|
596
|
+
nodes=nodes,
|
|
597
|
+
edges=tuple(production._edges),
|
|
598
|
+
warnings=tuple(production._graph_warnings),
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def message_to_classname_body(message: Any) -> tuple[str | None, str | dict | None]:
|
|
603
|
+
classname = f"{message.__class__.__module__}.{message.__class__.__name__}"
|
|
604
|
+
if isinstance(message, BaseModel):
|
|
605
|
+
return classname, message.model_dump_json()
|
|
606
|
+
if is_dataclass(message):
|
|
607
|
+
from ..messages.serialization import dataclass_to_dict
|
|
608
|
+
|
|
609
|
+
return classname, json.dumps(dataclass_to_dict(message))
|
|
610
|
+
return None, None
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import TYPE_CHECKING, Any, cast, overload
|
|
5
|
+
|
|
6
|
+
from ..runtime.environment import temporary_env as _temporary_env
|
|
7
|
+
from ..runtime.protocol import DirectorProtocol as _DirectorProtocol
|
|
8
|
+
from .types import TargetSettingRef
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .model import Production
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _has_remote_director(production: Production) -> bool:
|
|
15
|
+
try:
|
|
16
|
+
from ..runtime.remote import _RemoteDirector
|
|
17
|
+
except Exception:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
return isinstance(production._director, _RemoteDirector)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@overload
|
|
24
|
+
def resolve_target(target_value: TargetSettingRef) -> str: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@overload
|
|
28
|
+
def resolve_target(target_value: str) -> str: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@overload
|
|
32
|
+
def resolve_target(target_value: Any) -> Any: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve_target(target_value: Any) -> Any:
|
|
36
|
+
"""Resolve TargetSettingRef values to the current IRIS dispatch string."""
|
|
37
|
+
if isinstance(target_value, TargetSettingRef):
|
|
38
|
+
return target_value.resolve()
|
|
39
|
+
return target_value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _NamespaceDirectorProxy:
|
|
43
|
+
def __init__(self, director: Any, namespace: str):
|
|
44
|
+
self._director = director
|
|
45
|
+
self._namespace = namespace
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def namespace(self) -> str:
|
|
49
|
+
return self._namespace
|
|
50
|
+
|
|
51
|
+
def __getattr__(self, name: str) -> Any:
|
|
52
|
+
attribute = getattr(self._director, name)
|
|
53
|
+
if not callable(attribute):
|
|
54
|
+
return attribute
|
|
55
|
+
|
|
56
|
+
@wraps(attribute)
|
|
57
|
+
def call_with_namespace(*args: Any, **kwargs: Any) -> Any:
|
|
58
|
+
with _temporary_env("IRISNAMESPACE", self._namespace):
|
|
59
|
+
return attribute(*args, **kwargs)
|
|
60
|
+
|
|
61
|
+
return call_with_namespace
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _ProductionRuntime:
|
|
65
|
+
def __init__(self, production: Production):
|
|
66
|
+
self.production = production
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def director(self) -> _DirectorProtocol:
|
|
70
|
+
if self.production._director is not None:
|
|
71
|
+
return self.production._director
|
|
72
|
+
|
|
73
|
+
from ..runtime.local import _LocalDirector
|
|
74
|
+
from ..runtime.remote import _RemoteDirector, get_remote_settings
|
|
75
|
+
|
|
76
|
+
remote_settings = get_remote_settings()
|
|
77
|
+
if remote_settings:
|
|
78
|
+
if self.production.namespace:
|
|
79
|
+
remote_settings = dict(remote_settings)
|
|
80
|
+
remote_settings["namespace"] = self.production.namespace
|
|
81
|
+
return _RemoteDirector(remote_settings)
|
|
82
|
+
|
|
83
|
+
if self.production.namespace:
|
|
84
|
+
return cast(
|
|
85
|
+
_DirectorProtocol,
|
|
86
|
+
_NamespaceDirectorProxy(
|
|
87
|
+
_LocalDirector(),
|
|
88
|
+
self.production.namespace,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
return _LocalDirector()
|