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.
Files changed (119) hide show
  1. iop/__init__.py +276 -18
  2. iop/__main__.py +3 -3
  3. iop/cli/__init__.py +1 -0
  4. iop/cli/formatting.py +45 -0
  5. iop/{_cli.py → cli/main.py} +238 -216
  6. iop/cli/parser.py +165 -0
  7. iop/cli/types.py +73 -0
  8. iop/cls/IOP/Common.cls +11 -2
  9. iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
  10. iop/cls/IOP/Service/Remote/Rest/v1.cls +152 -5
  11. iop/cls/IOP/Utils.cls +814 -7
  12. iop/cls/IOP/Wrapper.cls +1 -1
  13. iop/components/__init__.py +16 -0
  14. iop/{_async_request.py → components/async_request.py} +7 -7
  15. iop/components/business_host.py +363 -0
  16. iop/components/business_operation.py +84 -0
  17. iop/{_business_process.py → components/business_process.py} +145 -125
  18. iop/components/business_service.py +106 -0
  19. iop/{_common.py → components/common.py} +160 -164
  20. iop/{_debugpy.py → components/debugpy.py} +11 -11
  21. iop/{_generator_request.py → components/generator_request.py} +4 -4
  22. iop/{_inbound_adapter.py → components/inbound_adapter.py} +10 -18
  23. iop/{_log_manager.py → components/log_manager.py} +1 -1
  24. iop/{_outbound_adapter.py → components/outbound_adapter.py} +1 -1
  25. iop/components/polling_business_service.py +48 -0
  26. iop/{_private_session_duplex.py → components/private_session_duplex.py} +7 -9
  27. iop/{_private_session_process.py → components/private_session_process.py} +2 -2
  28. iop/{_settings.py → components/settings.py} +23 -3
  29. iop/messages/__init__.py +7 -0
  30. iop/messages/base.py +61 -0
  31. iop/{_decorators.py → messages/decorators.py} +8 -2
  32. iop/messages/dispatch.py +351 -0
  33. iop/{_persistent_message.py → messages/persistent.py} +24 -19
  34. iop/{_serialization.py → messages/serialization.py} +34 -21
  35. iop/{_message_validator.py → messages/validation.py} +15 -13
  36. iop/migration/__init__.py +1 -0
  37. iop/migration/io.py +61 -0
  38. iop/migration/manifest.py +534 -0
  39. iop/migration/plans.py +80 -0
  40. iop/migration/utils.py +1126 -0
  41. iop/production/__init__.py +55 -0
  42. iop/production/actions.py +525 -0
  43. iop/production/common.py +103 -0
  44. iop/production/component.py +234 -0
  45. iop/production/declarations.py +190 -0
  46. iop/production/declarative.py +214 -0
  47. iop/production/diff.py +327 -0
  48. iop/production/import_.py +296 -0
  49. iop/production/inspection.py +92 -0
  50. iop/production/model.py +1143 -0
  51. iop/production/planning.py +678 -0
  52. iop/production/reconstruction.py +341 -0
  53. iop/production/rendering.py +610 -0
  54. iop/production/runtime.py +91 -0
  55. iop/production/source_inference.py +729 -0
  56. iop/production/types.py +546 -0
  57. iop/production/validation.py +452 -0
  58. iop/runtime/__init__.py +1 -0
  59. iop/runtime/director.py +415 -0
  60. iop/runtime/environment.py +59 -0
  61. iop/{_iris.py → runtime/iris.py} +2 -2
  62. iop/runtime/local.py +145 -0
  63. iop/{_director_protocol.py → runtime/protocol.py} +25 -8
  64. iop/runtime/remote/__init__.py +12 -0
  65. iop/runtime/remote/client.py +90 -0
  66. iop/runtime/remote/director.py +283 -0
  67. iop/runtime/remote/migration.py +57 -0
  68. iop/runtime/remote/settings.py +69 -0
  69. iop/runtime/remote/setup.py +72 -0
  70. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA +11 -2
  71. iris_pex_embedded_python-4.0.0.dist-info/RECORD +99 -0
  72. iris_pex_embedded_python-4.0.0.dist-info/entry_points.txt +2 -0
  73. iris_pex_embedded_python-4.0.0.dist-info/top_level.txt +1 -0
  74. grongier/__init__.py +0 -0
  75. grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -8
  76. grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -13
  77. grongier/cls/Grongier/PEX/BusinessService.cls +0 -8
  78. grongier/cls/Grongier/PEX/Common.cls +0 -10
  79. grongier/cls/Grongier/PEX/Director.cls +0 -10
  80. grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -4
  81. grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -13
  82. grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -4
  83. grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -8
  84. grongier/cls/Grongier/PEX/Message.cls +0 -13
  85. grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -8
  86. grongier/cls/Grongier/PEX/PickleMessage.cls +0 -13
  87. grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -8
  88. grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -14
  89. grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -14
  90. grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -14
  91. grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -14
  92. grongier/cls/Grongier/PEX/Test.cls +0 -10
  93. grongier/cls/Grongier/PEX/Utils.cls +0 -10
  94. grongier/cls/Grongier/Service/WSGI.cls +0 -4
  95. grongier/pex/__init__.py +0 -75
  96. grongier/pex/__main__.py +0 -4
  97. grongier/pex/_business_host.py +0 -1
  98. grongier/pex/_cli.py +0 -4
  99. grongier/pex/_common.py +0 -1
  100. grongier/pex/_director.py +0 -1
  101. grongier/pex/_utils.py +0 -1
  102. grongier/pex/wsgi/handlers.py +0 -104
  103. iop/_business_host.py +0 -322
  104. iop/_business_operation.py +0 -77
  105. iop/_business_service.py +0 -67
  106. iop/_director.py +0 -333
  107. iop/_dispatch.py +0 -194
  108. iop/_local.py +0 -106
  109. iop/_message.py +0 -45
  110. iop/_polling_business_service.py +0 -6
  111. iop/_remote.py +0 -473
  112. iop/_utils.py +0 -912
  113. iop/cls/IOP/Service/WSGI.cls +0 -310
  114. iop/wsgi/handlers.py +0 -112
  115. iris_pex_embedded_python-3.7.2b2.dist-info/RECORD +0 -96
  116. iris_pex_embedded_python-3.7.2b2.dist-info/entry_points.txt +0 -2
  117. iris_pex_embedded_python-3.7.2b2.dist-info/top_level.txt +0 -2
  118. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/WHEEL +0 -0
  119. {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()