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,55 @@
1
+ from __future__ import annotations
2
+
3
+ from .component import ComponentRef as ComponentRef
4
+ from .declarations import ComponentItem as ComponentItem
5
+ from .declarations import OperationItem as OperationItem
6
+ from .declarations import ProcessItem as ProcessItem
7
+ from .declarations import Route as Route
8
+ from .declarations import ServiceItem as ServiceItem
9
+ from .model import Production as Production
10
+ from .planning import ProductionApplyResult as ProductionApplyResult
11
+ from .planning import ProductionChangePlan as ProductionChangePlan
12
+ from .planning import ProductionPlanOperation as ProductionPlanOperation
13
+ from .planning import ProductionVerifyResult as ProductionVerifyResult
14
+ from .runtime import resolve_target as resolve_target
15
+ from .types import GraphEdge as GraphEdge
16
+ from .types import GraphNode as GraphNode
17
+ from .types import PersistentMessageRegistration as PersistentMessageRegistration
18
+ from .types import ProductionDiff as ProductionDiff
19
+ from .types import ProductionDiffEntry as ProductionDiffEntry
20
+ from .types import ProductionGraph as ProductionGraph
21
+ from .types import TargetSetting as TargetSetting
22
+ from .types import TargetSettingRef as TargetSettingRef
23
+ from .types import target as target
24
+ from .validation import ProductionValidationError as ProductionValidationError
25
+ from .validation import ProductionValidationIssue as ProductionValidationIssue
26
+ from .validation import ProductionValidationReport as ProductionValidationReport
27
+ from .validation import ProductionValidationWarning as ProductionValidationWarning
28
+
29
+ __all__ = [
30
+ "ComponentRef",
31
+ "ComponentItem",
32
+ "GraphEdge",
33
+ "GraphNode",
34
+ "OperationItem",
35
+ "PersistentMessageRegistration",
36
+ "ProcessItem",
37
+ "Production",
38
+ "ProductionApplyResult",
39
+ "ProductionChangePlan",
40
+ "ProductionDiff",
41
+ "ProductionDiffEntry",
42
+ "ProductionGraph",
43
+ "ProductionPlanOperation",
44
+ "ProductionVerifyResult",
45
+ "ProductionValidationError",
46
+ "ProductionValidationIssue",
47
+ "ProductionValidationReport",
48
+ "ProductionValidationWarning",
49
+ "Route",
50
+ "ServiceItem",
51
+ "TargetSetting",
52
+ "TargetSettingRef",
53
+ "resolve_target",
54
+ "target",
55
+ ]
@@ -0,0 +1,525 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from .component import ComponentRef
8
+ from .import_ import _normalize_queue_info
9
+ from .planning import (
10
+ ProductionApplyResult,
11
+ ProductionChangePlan,
12
+ ProductionVerifyResult,
13
+ create_backup,
14
+ production_fingerprint,
15
+ skipped_operation_results,
16
+ verify_change_plan,
17
+ )
18
+ from .rendering import message_to_classname_body
19
+ from .runtime import _has_remote_director, _ProductionRuntime, _temporary_env
20
+ from .types import TargetSettingRef
21
+
22
+
23
+ def start(production, detach: bool = True) -> None:
24
+ director = _ProductionRuntime(production).director
25
+ if detach:
26
+ director.start_production(production.name)
27
+ else:
28
+ director.start_production_with_log(production.name)
29
+
30
+
31
+ def stop(production) -> None:
32
+ director = _ProductionRuntime(production).director
33
+ require_current_production(production, director, "stop")
34
+ director.stop_production()
35
+
36
+
37
+ def restart(production) -> None:
38
+ director = _ProductionRuntime(production).director
39
+ require_current_production(production, director, "restart")
40
+ director.restart_production()
41
+
42
+
43
+ def kill(production) -> None:
44
+ director = _ProductionRuntime(production).director
45
+ require_current_production(production, director, "kill")
46
+ director.shutdown_production()
47
+
48
+
49
+ def status(production) -> dict:
50
+ return _ProductionRuntime(production).director.status_production()
51
+
52
+
53
+ def queue(production, *, refresh: bool = True) -> dict[str, dict[str, Any]]:
54
+ if not refresh:
55
+ return {name: dict(value) for name, value in production._queue_info.items()}
56
+ info = _ProductionRuntime(production).director.export_production_queue_info(
57
+ production.name
58
+ )
59
+ queue_map, _queue_warnings = _normalize_queue_info(info)
60
+ production._queue_info = queue_map
61
+ return {name: dict(value) for name, value in queue_map.items()}
62
+
63
+
64
+ def update(production) -> None:
65
+ director = _ProductionRuntime(production).director
66
+ require_current_production(production, director, "update")
67
+ director.update_production()
68
+
69
+
70
+ def start_component(
71
+ production,
72
+ component: ComponentRef | TargetSettingRef | str,
73
+ ) -> None:
74
+ component_name = production._runtime_component_name(component)
75
+ director = _ProductionRuntime(production).director
76
+ require_current_runtime(
77
+ production,
78
+ director,
79
+ f"start component {component_name!r} in production {production.name!r}",
80
+ )
81
+ director.start_component(component_name)
82
+
83
+
84
+ def stop_component(
85
+ production,
86
+ component: ComponentRef | TargetSettingRef | str,
87
+ ) -> None:
88
+ component_name = production._runtime_component_name(component)
89
+ director = _ProductionRuntime(production).director
90
+ require_current_runtime(
91
+ production,
92
+ director,
93
+ f"stop component {component_name!r} in production {production.name!r}",
94
+ )
95
+ director.stop_component(component_name)
96
+
97
+
98
+ def restart_component(
99
+ production,
100
+ component: ComponentRef | TargetSettingRef | str,
101
+ ) -> None:
102
+ component_name = production._runtime_component_name(component)
103
+ director = _ProductionRuntime(production).director
104
+ require_current_runtime(
105
+ production,
106
+ director,
107
+ f"restart component {component_name!r} in production {production.name!r}",
108
+ )
109
+ director.restart_component(component_name)
110
+
111
+
112
+ def export(production) -> dict:
113
+ return _ProductionRuntime(production).director.export_production(production.name)
114
+
115
+
116
+ def set_default(production) -> None:
117
+ _ProductionRuntime(production).director.set_default_production(production.name)
118
+
119
+
120
+ def sync(
121
+ production, *, root_path: str | None = None, update_runtime: bool = True
122
+ ) -> None:
123
+ if _has_remote_director(production):
124
+ raise NotImplementedError(
125
+ "Production.sync() can only register directly with local IRIS. "
126
+ "Use `iop --migrate <settings_file>` for remote migrations."
127
+ )
128
+ from ..migration import utils as migration_utils
129
+
130
+ with _temporary_env("IRISNAMESPACE", production.namespace):
131
+ migration_utils.set_productions_settings([production], root_path)
132
+ if update_runtime:
133
+ from ..runtime.local import _LocalDirector
134
+
135
+ _LocalDirector().update_production()
136
+
137
+
138
+ def apply(
139
+ production,
140
+ *,
141
+ plan: ProductionChangePlan | None = None,
142
+ allow_destructive: bool = False,
143
+ backup_dir: str = ".iop/backups",
144
+ root_path: str | None = None,
145
+ update_runtime: bool = True,
146
+ ) -> ProductionApplyResult:
147
+ director = _ProductionRuntime(production).director
148
+ if _director_is_remote(director) or _has_remote_director(production):
149
+ raise RuntimeError(
150
+ "Remote production plan apply is not supported in v1. "
151
+ "Run apply from a local IRIS environment."
152
+ )
153
+
154
+ if plan is None:
155
+ plan = production.plan()
156
+ if plan.production_name != production.name:
157
+ raise ValueError(
158
+ f"Plan targets {plan.production_name!r}, not {production.name!r}."
159
+ )
160
+
161
+ apply_operations = plan.operations_for_apply(
162
+ allow_destructive=allow_destructive,
163
+ )
164
+ skipped = skipped_operation_results(
165
+ plan,
166
+ allow_destructive=allow_destructive,
167
+ )
168
+ if not apply_operations:
169
+ return ProductionApplyResult(
170
+ plan_id=plan.id,
171
+ production_name=plan.production_name,
172
+ operations=tuple(skipped),
173
+ )
174
+
175
+ current, exported, connections, queues = _read_current_snapshot(production, director)
176
+ current_fingerprint = production_fingerprint(current)
177
+ if current_fingerprint != plan.source_fingerprint:
178
+ raise RuntimeError(
179
+ "Cannot apply production plan: current IRIS production changed since "
180
+ "the plan was created."
181
+ )
182
+
183
+ backup_path = create_backup(
184
+ backup_dir=backup_dir,
185
+ plan=plan,
186
+ current=current,
187
+ current_export=exported,
188
+ connections=connections,
189
+ queues=queues,
190
+ )
191
+
192
+ _register_plan_classes(production, root_path=root_path)
193
+ raw_result = director.apply_production_plan(
194
+ plan.to_dict(),
195
+ allow_destructive=allow_destructive,
196
+ )
197
+ if isinstance(raw_result, dict) and raw_result.get("error"):
198
+ raise RuntimeError(str(raw_result["error"]))
199
+ operation_results = _apply_operation_results(raw_result)
200
+ operation_results.extend(skipped)
201
+ updated_runtime = _update_runtime_if_current(
202
+ production,
203
+ director,
204
+ update_runtime=update_runtime,
205
+ )
206
+ return ProductionApplyResult(
207
+ plan_id=plan.id,
208
+ production_name=plan.production_name,
209
+ backup_path=str(backup_path),
210
+ operations=tuple(operation_results),
211
+ updated_runtime=updated_runtime,
212
+ )
213
+
214
+
215
+ def verify(production, plan: ProductionChangePlan) -> ProductionVerifyResult:
216
+ director = _ProductionRuntime(production).director
217
+ current = production.from_iris(
218
+ plan.production_name,
219
+ namespace=production.namespace,
220
+ director=director,
221
+ )
222
+ return verify_change_plan(plan, current)
223
+
224
+
225
+ def rollback_backup(
226
+ backup_path: str,
227
+ *,
228
+ director: Any = None,
229
+ namespace: str | None = None,
230
+ allow_destructive: bool = False,
231
+ update_runtime: bool = True,
232
+ ) -> ProductionVerifyResult:
233
+ if not allow_destructive:
234
+ raise RuntimeError(
235
+ "Rollback restores a full production export and requires "
236
+ "allow_destructive=True."
237
+ )
238
+ if director is None:
239
+ from ..runtime.local import _LocalDirector
240
+
241
+ director = _LocalDirector()
242
+ if _director_is_remote(director):
243
+ raise RuntimeError(
244
+ "Remote production rollback is not supported in v1. "
245
+ "Run rollback from a local IRIS environment."
246
+ )
247
+
248
+ path = Path(backup_path)
249
+ production_data = json.loads((path / "production.json").read_text(encoding="utf-8"))
250
+ metadata = json.loads((path / "metadata.json").read_text(encoding="utf-8"))
251
+ production_name = metadata.get("production") or next(iter(production_data))
252
+
253
+ from ..migration import utils as migration_utils
254
+
255
+ with _temporary_env("IRISNAMESPACE", namespace or metadata.get("namespace") or None):
256
+ migration_utils.register_production_definition(
257
+ production_name,
258
+ _normalized_production_definition(production_name, production_data),
259
+ )
260
+ if update_runtime:
261
+ _update_runtime_if_current_name(
262
+ production_name,
263
+ director,
264
+ update_runtime=True,
265
+ )
266
+
267
+ from .model import Production
268
+
269
+ restored = Production.from_iris(
270
+ production_name,
271
+ director=director,
272
+ namespace=namespace or metadata.get("namespace") or None,
273
+ )
274
+ expected = _production_from_export(
275
+ production_name,
276
+ production_data,
277
+ director=director,
278
+ namespace=namespace or metadata.get("namespace") or None,
279
+ )
280
+ result_plan = ProductionChangePlan(
281
+ id=str(metadata.get("plan_id") or ""),
282
+ production_name=production_name,
283
+ namespace=str(namespace or metadata.get("namespace") or ""),
284
+ source_fingerprint=production_fingerprint(expected),
285
+ desired_fingerprint=production_fingerprint(expected),
286
+ )
287
+ if production_fingerprint(restored) != production_fingerprint(expected):
288
+ return ProductionVerifyResult(
289
+ plan_id=result_plan.id,
290
+ production_name=production_name,
291
+ failed_operations=(
292
+ {"id": "rollback", "path": production_name, "status": "failed"},
293
+ ),
294
+ )
295
+ return ProductionVerifyResult(
296
+ plan_id=result_plan.id,
297
+ production_name=production_name,
298
+ converged_operations=("rollback",),
299
+ )
300
+
301
+
302
+ def log(production, top: int | None = None) -> None:
303
+ director = _ProductionRuntime(production).director
304
+ if top is None:
305
+ director.log_production()
306
+ else:
307
+ director.log_production_top(top)
308
+
309
+
310
+ def test_component(
311
+ production,
312
+ target_or_ref: str | TargetSettingRef | ComponentRef,
313
+ message: Any = None,
314
+ classname: str | None = None,
315
+ body: str | dict | None = None,
316
+ ) -> Any:
317
+ director = _ProductionRuntime(production).director
318
+ target_name = production.resolve_target(target_or_ref)
319
+
320
+ raise_if_existing_production_not_running(production, director)
321
+
322
+ if classname is None and body is None and message is not None:
323
+ classname, body = message_to_classname_body(message)
324
+ if classname is not None:
325
+ message = None
326
+ return director.test_component(
327
+ target_name,
328
+ message=message,
329
+ classname=classname,
330
+ body=body,
331
+ )
332
+
333
+
334
+ def raise_if_existing_production_not_running(production, director: Any) -> None:
335
+ production_name, state = read_status(production, director, "test")
336
+ state_lower = str(state).lower()
337
+ if production_name == production.name and str(state).lower() == "running":
338
+ return
339
+
340
+ if (
341
+ production_name
342
+ and production_name != production.name
343
+ and state_lower == "running"
344
+ ):
345
+ raise RuntimeError(
346
+ f"Production {production.name!r} exists but is not running "
347
+ f"(currently running production is {production_name!r}). "
348
+ f"{switch_running_production_message(production, production_name)} "
349
+ "Do that before calling `prod.test(...)`."
350
+ )
351
+ if production_name and production_name != production.name:
352
+ detail = f"current default production is {production_name!r}"
353
+ elif state:
354
+ detail = f"current status is {state!r}"
355
+ else:
356
+ detail = "runtime status did not report a running production"
357
+ raise RuntimeError(
358
+ f"Production {production.name!r} exists but is not running ({detail}). "
359
+ f"Start it with `iop --start {production.name} --detach` or "
360
+ "`prod.start()` before calling `prod.test(...)`."
361
+ )
362
+
363
+
364
+ def require_current_production(production, director: Any, action: str) -> None:
365
+ require_current_runtime(
366
+ production,
367
+ director,
368
+ f"{action} production {production.name!r}",
369
+ )
370
+
371
+
372
+ def require_current_runtime(production, director: Any, action: str) -> None:
373
+ production_name, state = read_status(production, director, action)
374
+ if production_name == production.name:
375
+ return
376
+
377
+ if production_name:
378
+ detail = f"current/default production is {production_name!r}"
379
+ elif state:
380
+ detail = f"current status is {state!r}"
381
+ else:
382
+ detail = "runtime status did not report a current/default production"
383
+ raise RuntimeError(
384
+ f"Cannot {action}: {detail}. "
385
+ f"Select {production.name!r} with `prod.set_default()` or start it with "
386
+ "`prod.start()` before using this lifecycle method."
387
+ )
388
+
389
+
390
+ def read_status(production, director: Any, action: str) -> tuple[str, str]:
391
+ try:
392
+ status = director.status_production()
393
+ except Exception as exc:
394
+ raise RuntimeError(
395
+ f"Cannot {action}: could not verify production status ({exc})."
396
+ ) from exc
397
+ if not isinstance(status, dict):
398
+ raise RuntimeError(
399
+ f"Cannot {action}: production status response is invalid ({status!r})."
400
+ )
401
+ production_name = status.get("Production") or status.get("production") or ""
402
+ state = status.get("Status") or status.get("status") or ""
403
+ return str(production_name), str(state)
404
+
405
+
406
+ def switch_running_production_message(production, current_production: str) -> str:
407
+ return (
408
+ f"IRIS can run only one production at a time. Stop {current_production!r} "
409
+ f"first with `iop --stop`, then start {production.name!r} with "
410
+ f"`iop --start {production.name} --detach`; or call `prod.stop()` and "
411
+ "`prod.start()` explicitly."
412
+ )
413
+
414
+
415
+ def _read_current_snapshot(production, director: Any):
416
+ exported = director.export_production(production.name)
417
+ try:
418
+ connections = director.export_production_connections(production.name)
419
+ except Exception as exc:
420
+ connections = {"warnings": [f"Could not export runtime connections: {exc}"]}
421
+ try:
422
+ queues = director.export_production_queue_info(production.name)
423
+ except Exception as exc:
424
+ queues = {"warnings": [f"Could not export queue info: {exc}"]}
425
+ current = production.from_dict(
426
+ exported,
427
+ connections=connections,
428
+ queue_info=queues,
429
+ namespace=production.namespace,
430
+ director=director,
431
+ )
432
+ return current, exported, connections, queues
433
+
434
+
435
+ def _register_plan_classes(production, *, root_path: str | None = None) -> None:
436
+ from ..migration import utils as migration_utils
437
+
438
+ with _temporary_env("IRISNAMESPACE", production.namespace):
439
+ migration_utils._register_production_object_messages(production)
440
+ migration_utils._register_production_object_components(production, root_path)
441
+
442
+
443
+ def _apply_operation_results(raw_result: Any) -> list[dict[str, Any]]:
444
+ if raw_result is None:
445
+ return []
446
+ if isinstance(raw_result, str):
447
+ raw_result = json.loads(raw_result)
448
+ if isinstance(raw_result, dict):
449
+ return [dict(item) for item in raw_result.get("operations", ())]
450
+ if isinstance(raw_result, list):
451
+ return [dict(item) for item in raw_result]
452
+ return []
453
+
454
+
455
+ def _update_runtime_if_current(
456
+ production,
457
+ director: Any,
458
+ *,
459
+ update_runtime: bool,
460
+ ) -> bool:
461
+ return _update_runtime_if_current_name(
462
+ production.name,
463
+ director,
464
+ update_runtime=update_runtime,
465
+ )
466
+
467
+
468
+ def _update_runtime_if_current_name(
469
+ production_name: str,
470
+ director: Any,
471
+ *,
472
+ update_runtime: bool,
473
+ ) -> bool:
474
+ if not update_runtime:
475
+ return False
476
+ try:
477
+ status = director.status_production()
478
+ except Exception:
479
+ return False
480
+ current = status.get("Production") or status.get("production") or ""
481
+ if current != production_name:
482
+ return False
483
+ director.update_production()
484
+ return True
485
+
486
+
487
+ def _director_is_remote(director: Any) -> bool:
488
+ try:
489
+ from ..runtime.remote import _RemoteDirector
490
+ except Exception:
491
+ return False
492
+ return isinstance(director, _RemoteDirector)
493
+
494
+
495
+ def _production_from_export(
496
+ production_name: str,
497
+ production_data: dict[str, Any],
498
+ *,
499
+ director: Any,
500
+ namespace: str | None,
501
+ ):
502
+ from .model import Production
503
+
504
+ return Production.from_dict(
505
+ production_data,
506
+ namespace=namespace,
507
+ director=director,
508
+ )
509
+
510
+
511
+ def _normalized_production_definition(
512
+ production_name: str,
513
+ production_data: dict[str, Any],
514
+ ) -> dict[str, Any]:
515
+ if "Production" in production_data:
516
+ definition = production_data["Production"]
517
+ else:
518
+ definition = production_data.get(production_name)
519
+ if definition is None and production_data:
520
+ definition = production_data[next(iter(production_data))]
521
+ if not isinstance(definition, dict):
522
+ raise ValueError("Production backup must contain a production definition.")
523
+ normalized = dict(definition)
524
+ normalized.setdefault("@Name", production_name)
525
+ return {"Production": normalized}
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ from typing import Any
5
+
6
+ PRODUCTION_SETTING_FIELDS: dict[str, tuple[str, Any]] = {
7
+ "shutdown_timeout": ("ShutdownTimeout", 120),
8
+ "update_timeout": ("UpdateTimeout", 10),
9
+ "alert_notification_manager": ("AlertNotificationManager", ""),
10
+ "alert_notification_operation": ("AlertNotificationOperation", ""),
11
+ "alert_notification_recipients": ("AlertNotificationRecipients", ""),
12
+ "alert_action_window": ("AlertActionWindow", 60),
13
+ }
14
+
15
+ PRODUCTION_SETTING_NAMES: dict[str, str] = {
16
+ field_name: iris_name
17
+ for field_name, (iris_name, _default) in PRODUCTION_SETTING_FIELDS.items()
18
+ }
19
+
20
+ PRODUCTION_SETTING_FIELDS_BY_IRIS: dict[str, str] = {
21
+ iris_name: field_name
22
+ for field_name, (iris_name, _default) in PRODUCTION_SETTING_FIELDS.items()
23
+ }
24
+
25
+ SETTING_NAME_ALIASES = {
26
+ "target_config_name": "TargetConfigName",
27
+ "target_config_names": "TargetConfigNames",
28
+ }
29
+
30
+
31
+ def _bool_text(value: bool | str) -> str:
32
+ if isinstance(value, bool):
33
+ return "true" if value else "false"
34
+ normalized = str(value).lower().strip()
35
+ if normalized in ("1", "yes", "on"):
36
+ return "true"
37
+ if normalized in ("0", "no", "off"):
38
+ return "false"
39
+ return normalized
40
+
41
+
42
+ def _text_value(value: Any) -> str:
43
+ if value is None:
44
+ return ""
45
+ if isinstance(value, bool):
46
+ return _bool_text(value)
47
+ return str(value)
48
+
49
+
50
+ def _auto_proxy_class_name(component_class: type) -> str:
51
+ return (
52
+ f"Python.{component_class.__module__}.{component_class.__name__}".replace(
53
+ "_", ""
54
+ )
55
+ )
56
+
57
+
58
+ def _adapter_type_from_component_class(component_class: type | None) -> str:
59
+ if component_class is None:
60
+ return ""
61
+ method = getattr(component_class, "get_adapter_type", None)
62
+ if callable(method):
63
+ value = method()
64
+ if value:
65
+ return str(value)
66
+ return ""
67
+
68
+
69
+ def _adapter_type_from_class_name(class_name: str | None) -> str:
70
+ if not class_name or not class_name.startswith("Python."):
71
+ return ""
72
+ python_name = class_name.removeprefix("Python.")
73
+ module_name, separator, class_attr = python_name.rpartition(".")
74
+ if not separator:
75
+ return ""
76
+ try:
77
+ module = importlib.import_module(module_name)
78
+ component_class = getattr(module, class_attr)
79
+ except Exception:
80
+ return ""
81
+ if not isinstance(component_class, type):
82
+ return ""
83
+ return _adapter_type_from_component_class(component_class)
84
+
85
+
86
+ def _settings_to_iris(target_name: str, values: dict[str, Any]) -> list[dict[str, str]]:
87
+ return [
88
+ {
89
+ "@Target": target_name,
90
+ "@Name": name,
91
+ "#text": _text_value(value),
92
+ }
93
+ for name, value in values.items()
94
+ ]
95
+
96
+
97
+ def _apply_settings_update(target: dict[str, Any], updates: Any) -> None:
98
+ """Merge *updates* into *target*, treating ``None`` values as removals."""
99
+ for key, value in (updates or {}).items():
100
+ if value is None:
101
+ target.pop(key, None)
102
+ else:
103
+ target[key] = value