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
iop/production/model.py
ADDED
|
@@ -0,0 +1,1143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..runtime.protocol import DirectorProtocol as _DirectorProtocol
|
|
6
|
+
from . import actions as _actions
|
|
7
|
+
from .common import (
|
|
8
|
+
PRODUCTION_SETTING_FIELDS,
|
|
9
|
+
_adapter_type_from_component_class,
|
|
10
|
+
_apply_settings_update,
|
|
11
|
+
_auto_proxy_class_name,
|
|
12
|
+
)
|
|
13
|
+
from .component import ComponentRef
|
|
14
|
+
from .declarative import _DeclarativeProductionMixin
|
|
15
|
+
from .diff import _diff_productions
|
|
16
|
+
from .inspection import component_runtime_info, inspect_component
|
|
17
|
+
from .planning import (
|
|
18
|
+
ProductionApplyResult,
|
|
19
|
+
ProductionChangePlan,
|
|
20
|
+
ProductionVerifyResult,
|
|
21
|
+
build_change_plan,
|
|
22
|
+
)
|
|
23
|
+
from .reconstruction import production_from_dict
|
|
24
|
+
from .rendering import (
|
|
25
|
+
production_graph,
|
|
26
|
+
production_to_class,
|
|
27
|
+
production_to_dict,
|
|
28
|
+
production_to_python,
|
|
29
|
+
production_to_xml,
|
|
30
|
+
)
|
|
31
|
+
from .runtime import _ProductionRuntime
|
|
32
|
+
from .types import (
|
|
33
|
+
GraphEdge,
|
|
34
|
+
PersistentMessageRegistration,
|
|
35
|
+
ProductionDiff,
|
|
36
|
+
ProductionGraph,
|
|
37
|
+
TargetSettingRef,
|
|
38
|
+
_edge_identity,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_MISSING = object()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Production(_DeclarativeProductionMixin):
|
|
45
|
+
"""Purpose:
|
|
46
|
+
Python authoring DSL for IRIS interoperability production topology.
|
|
47
|
+
|
|
48
|
+
Use when:
|
|
49
|
+
A project should declare services, processes, operations, settings, and
|
|
50
|
+
graph connections in Python.
|
|
51
|
+
|
|
52
|
+
Lifecycle:
|
|
53
|
+
A settings.py module exports PRODUCTIONS = [prod]. Migration renders the
|
|
54
|
+
graph to IRIS; IRIS remains the runtime source of truth.
|
|
55
|
+
|
|
56
|
+
Best practices:
|
|
57
|
+
Declare topology with service(), process(), operation(), target(), and
|
|
58
|
+
connect(). Keep the Production object import-safe.
|
|
59
|
+
|
|
60
|
+
Common mistakes:
|
|
61
|
+
Do not execute migration or runtime side effects at module import time.
|
|
62
|
+
Do not use raw CLASSES entries for graph components.
|
|
63
|
+
|
|
64
|
+
Minimal example:
|
|
65
|
+
prod = Production("Demo.Production")
|
|
66
|
+
svc = prod.service("Demo.Service", MyService)
|
|
67
|
+
op = prod.operation("Demo.Operation", MyOperation)
|
|
68
|
+
prod.connect(svc.Output, op)
|
|
69
|
+
PRODUCTIONS = [prod]
|
|
70
|
+
|
|
71
|
+
Related:
|
|
72
|
+
docs/cookbooks/hello-world-production.md,
|
|
73
|
+
docs/production-graph.md
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
name: str | object = _MISSING,
|
|
79
|
+
*,
|
|
80
|
+
testing_enabled: bool | str | object = _MISSING,
|
|
81
|
+
log_general_trace_events: bool | str | object = _MISSING,
|
|
82
|
+
actor_pool_size: int | str | object = _MISSING,
|
|
83
|
+
description: str | object = _MISSING,
|
|
84
|
+
shutdown_timeout: int | str | object = _MISSING,
|
|
85
|
+
update_timeout: int | str | object = _MISSING,
|
|
86
|
+
alert_notification_manager: str | object = _MISSING,
|
|
87
|
+
alert_notification_operation: str | object = _MISSING,
|
|
88
|
+
alert_notification_recipients: str | object = _MISSING,
|
|
89
|
+
alert_action_window: int | str | object = _MISSING,
|
|
90
|
+
namespace: str | None | object = _MISSING,
|
|
91
|
+
director: _DirectorProtocol | None | object = _MISSING,
|
|
92
|
+
):
|
|
93
|
+
self._initialize(
|
|
94
|
+
name,
|
|
95
|
+
testing_enabled=testing_enabled,
|
|
96
|
+
log_general_trace_events=log_general_trace_events,
|
|
97
|
+
actor_pool_size=actor_pool_size,
|
|
98
|
+
description=description,
|
|
99
|
+
shutdown_timeout=shutdown_timeout,
|
|
100
|
+
update_timeout=update_timeout,
|
|
101
|
+
alert_notification_manager=alert_notification_manager,
|
|
102
|
+
alert_notification_operation=alert_notification_operation,
|
|
103
|
+
alert_notification_recipients=alert_notification_recipients,
|
|
104
|
+
alert_action_window=alert_action_window,
|
|
105
|
+
namespace=namespace,
|
|
106
|
+
director=director,
|
|
107
|
+
hydrate_declarations=True,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def _new_unhydrated(
|
|
112
|
+
cls,
|
|
113
|
+
name: str | object = _MISSING,
|
|
114
|
+
*,
|
|
115
|
+
testing_enabled: bool | str | object = _MISSING,
|
|
116
|
+
log_general_trace_events: bool | str | object = _MISSING,
|
|
117
|
+
actor_pool_size: int | str | object = _MISSING,
|
|
118
|
+
description: str | object = _MISSING,
|
|
119
|
+
shutdown_timeout: int | str | object = _MISSING,
|
|
120
|
+
update_timeout: int | str | object = _MISSING,
|
|
121
|
+
alert_notification_manager: str | object = _MISSING,
|
|
122
|
+
alert_notification_operation: str | object = _MISSING,
|
|
123
|
+
alert_notification_recipients: str | object = _MISSING,
|
|
124
|
+
alert_action_window: int | str | object = _MISSING,
|
|
125
|
+
namespace: str | None | object = _MISSING,
|
|
126
|
+
director: _DirectorProtocol | None | object = _MISSING,
|
|
127
|
+
) -> Production:
|
|
128
|
+
production = object.__new__(cls)
|
|
129
|
+
Production._initialize(
|
|
130
|
+
production,
|
|
131
|
+
name,
|
|
132
|
+
testing_enabled=testing_enabled,
|
|
133
|
+
log_general_trace_events=log_general_trace_events,
|
|
134
|
+
actor_pool_size=actor_pool_size,
|
|
135
|
+
description=description,
|
|
136
|
+
shutdown_timeout=shutdown_timeout,
|
|
137
|
+
update_timeout=update_timeout,
|
|
138
|
+
alert_notification_manager=alert_notification_manager,
|
|
139
|
+
alert_notification_operation=alert_notification_operation,
|
|
140
|
+
alert_notification_recipients=alert_notification_recipients,
|
|
141
|
+
alert_action_window=alert_action_window,
|
|
142
|
+
namespace=namespace,
|
|
143
|
+
director=director,
|
|
144
|
+
hydrate_declarations=False,
|
|
145
|
+
)
|
|
146
|
+
return production
|
|
147
|
+
|
|
148
|
+
def _initialize(
|
|
149
|
+
self,
|
|
150
|
+
name: str | object = _MISSING,
|
|
151
|
+
*,
|
|
152
|
+
testing_enabled: bool | str | object = _MISSING,
|
|
153
|
+
log_general_trace_events: bool | str | object = _MISSING,
|
|
154
|
+
actor_pool_size: int | str | object = _MISSING,
|
|
155
|
+
description: str | object = _MISSING,
|
|
156
|
+
shutdown_timeout: int | str | object = _MISSING,
|
|
157
|
+
update_timeout: int | str | object = _MISSING,
|
|
158
|
+
alert_notification_manager: str | object = _MISSING,
|
|
159
|
+
alert_notification_operation: str | object = _MISSING,
|
|
160
|
+
alert_notification_recipients: str | object = _MISSING,
|
|
161
|
+
alert_action_window: int | str | object = _MISSING,
|
|
162
|
+
namespace: str | None | object = _MISSING,
|
|
163
|
+
director: _DirectorProtocol | None | object = _MISSING,
|
|
164
|
+
hydrate_declarations: bool,
|
|
165
|
+
) -> None:
|
|
166
|
+
self.name = self._resolve_production_name(name)
|
|
167
|
+
self.testing_enabled = self._production_default(
|
|
168
|
+
"testing_enabled", testing_enabled, False
|
|
169
|
+
)
|
|
170
|
+
self.log_general_trace_events = self._production_default(
|
|
171
|
+
"log_general_trace_events", log_general_trace_events, False
|
|
172
|
+
)
|
|
173
|
+
self.actor_pool_size = self._production_default(
|
|
174
|
+
"actor_pool_size", actor_pool_size, 2
|
|
175
|
+
)
|
|
176
|
+
self.description = self._production_default("description", description, "")
|
|
177
|
+
self.shutdown_timeout = self._production_default(
|
|
178
|
+
"shutdown_timeout",
|
|
179
|
+
shutdown_timeout,
|
|
180
|
+
PRODUCTION_SETTING_FIELDS["shutdown_timeout"][1],
|
|
181
|
+
)
|
|
182
|
+
self.update_timeout = self._production_default(
|
|
183
|
+
"update_timeout",
|
|
184
|
+
update_timeout,
|
|
185
|
+
PRODUCTION_SETTING_FIELDS["update_timeout"][1],
|
|
186
|
+
)
|
|
187
|
+
self.alert_notification_manager = self._production_default(
|
|
188
|
+
"alert_notification_manager",
|
|
189
|
+
alert_notification_manager,
|
|
190
|
+
PRODUCTION_SETTING_FIELDS["alert_notification_manager"][1],
|
|
191
|
+
)
|
|
192
|
+
self.alert_notification_operation = self._production_default(
|
|
193
|
+
"alert_notification_operation",
|
|
194
|
+
alert_notification_operation,
|
|
195
|
+
PRODUCTION_SETTING_FIELDS["alert_notification_operation"][1],
|
|
196
|
+
)
|
|
197
|
+
self.alert_notification_recipients = self._production_default(
|
|
198
|
+
"alert_notification_recipients",
|
|
199
|
+
alert_notification_recipients,
|
|
200
|
+
PRODUCTION_SETTING_FIELDS["alert_notification_recipients"][1],
|
|
201
|
+
)
|
|
202
|
+
self.alert_action_window = self._production_default(
|
|
203
|
+
"alert_action_window",
|
|
204
|
+
alert_action_window,
|
|
205
|
+
PRODUCTION_SETTING_FIELDS["alert_action_window"][1],
|
|
206
|
+
)
|
|
207
|
+
self.namespace = self._production_default("namespace", namespace, None)
|
|
208
|
+
self._director = self._production_default("director", director, None)
|
|
209
|
+
self._items: list[ComponentRef] = []
|
|
210
|
+
self._items_by_name: dict[str, ComponentRef] = {}
|
|
211
|
+
self._connections: dict[tuple[str, str], list[str]] = {}
|
|
212
|
+
self._edges: list[GraphEdge] = []
|
|
213
|
+
self._graph_warnings: list[str] = []
|
|
214
|
+
self._queue_info: dict[str, dict[str, Any]] = {}
|
|
215
|
+
self._messages: list[PersistentMessageRegistration] = []
|
|
216
|
+
if hydrate_declarations:
|
|
217
|
+
self._hydrate_declared_items()
|
|
218
|
+
|
|
219
|
+
def _resolve_production_name(self, name: str | object) -> str:
|
|
220
|
+
if name is not _MISSING:
|
|
221
|
+
return name # type: ignore[return-value]
|
|
222
|
+
class_name = getattr(type(self), "name", _MISSING)
|
|
223
|
+
if class_name is not _MISSING:
|
|
224
|
+
return class_name
|
|
225
|
+
return f"{type(self).__module__}.{type(self).__name__}"
|
|
226
|
+
|
|
227
|
+
def _production_default(
|
|
228
|
+
self,
|
|
229
|
+
field_name: str,
|
|
230
|
+
explicit: Any,
|
|
231
|
+
default: Any,
|
|
232
|
+
) -> Any:
|
|
233
|
+
if explicit is not _MISSING:
|
|
234
|
+
return explicit
|
|
235
|
+
class_value = getattr(type(self), field_name, _MISSING)
|
|
236
|
+
if class_value is not _MISSING:
|
|
237
|
+
return class_value
|
|
238
|
+
return default
|
|
239
|
+
|
|
240
|
+
def testing(self, enabled: bool | str = True) -> Production:
|
|
241
|
+
self.testing_enabled = enabled
|
|
242
|
+
return self
|
|
243
|
+
|
|
244
|
+
def tracing(self, enabled: bool | str = True) -> Production:
|
|
245
|
+
self.log_general_trace_events = enabled
|
|
246
|
+
return self
|
|
247
|
+
|
|
248
|
+
def actor_pool(self, size: int | str) -> Production:
|
|
249
|
+
self.actor_pool_size = size
|
|
250
|
+
return self
|
|
251
|
+
|
|
252
|
+
def describe(self, text: str) -> Production:
|
|
253
|
+
self.description = text
|
|
254
|
+
return self
|
|
255
|
+
|
|
256
|
+
def timeouts(
|
|
257
|
+
self,
|
|
258
|
+
*,
|
|
259
|
+
shutdown: int | str | None = None,
|
|
260
|
+
update: int | str | None = None,
|
|
261
|
+
) -> Production:
|
|
262
|
+
if shutdown is not None:
|
|
263
|
+
self.shutdown_timeout = shutdown
|
|
264
|
+
if update is not None:
|
|
265
|
+
self.update_timeout = update
|
|
266
|
+
return self
|
|
267
|
+
|
|
268
|
+
def alerting(
|
|
269
|
+
self,
|
|
270
|
+
*,
|
|
271
|
+
manager: str | None = None,
|
|
272
|
+
operation: str | None = None,
|
|
273
|
+
recipients: str | None = None,
|
|
274
|
+
action_window: int | str | None = None,
|
|
275
|
+
) -> Production:
|
|
276
|
+
if manager is not None:
|
|
277
|
+
self.alert_notification_manager = manager
|
|
278
|
+
if operation is not None:
|
|
279
|
+
self.alert_notification_operation = operation
|
|
280
|
+
if recipients is not None:
|
|
281
|
+
self.alert_notification_recipients = recipients
|
|
282
|
+
if action_window is not None:
|
|
283
|
+
self.alert_action_window = action_window
|
|
284
|
+
return self
|
|
285
|
+
|
|
286
|
+
def in_namespace(self, namespace: str | None) -> Production:
|
|
287
|
+
self.namespace = namespace
|
|
288
|
+
return self
|
|
289
|
+
|
|
290
|
+
def with_director(self, director: _DirectorProtocol | None) -> Production:
|
|
291
|
+
self._director = director
|
|
292
|
+
return self
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def from_iris(
|
|
296
|
+
cls,
|
|
297
|
+
name: str,
|
|
298
|
+
*,
|
|
299
|
+
namespace: str | None = None,
|
|
300
|
+
director: _DirectorProtocol | None = None,
|
|
301
|
+
) -> Production:
|
|
302
|
+
seed = cls._new_unhydrated(
|
|
303
|
+
name,
|
|
304
|
+
namespace=namespace,
|
|
305
|
+
director=director,
|
|
306
|
+
)
|
|
307
|
+
runtime_director = _ProductionRuntime(seed).director
|
|
308
|
+
exported = runtime_director.export_production(name)
|
|
309
|
+
connections = None
|
|
310
|
+
try:
|
|
311
|
+
connections = runtime_director.export_production_connections(name)
|
|
312
|
+
except AttributeError:
|
|
313
|
+
connections = None
|
|
314
|
+
except Exception as exc:
|
|
315
|
+
connections = {"warnings": [f"Could not export runtime connections: {exc}"]}
|
|
316
|
+
if connections is not None and not isinstance(connections, (dict, list)):
|
|
317
|
+
connections = None
|
|
318
|
+
|
|
319
|
+
return cls.from_dict(
|
|
320
|
+
exported,
|
|
321
|
+
connections=connections,
|
|
322
|
+
namespace=namespace,
|
|
323
|
+
director=director,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
@classmethod
|
|
327
|
+
def from_dict(
|
|
328
|
+
cls,
|
|
329
|
+
data: dict[str, Any],
|
|
330
|
+
*,
|
|
331
|
+
connections: Any = None,
|
|
332
|
+
queue_info: Any = None,
|
|
333
|
+
namespace: str | None = None,
|
|
334
|
+
director: Any = None,
|
|
335
|
+
) -> Production:
|
|
336
|
+
return production_from_dict(
|
|
337
|
+
cls,
|
|
338
|
+
data,
|
|
339
|
+
connections=connections,
|
|
340
|
+
queue_info=queue_info,
|
|
341
|
+
namespace=namespace,
|
|
342
|
+
director=director,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def items(self) -> tuple[ComponentRef, ...]:
|
|
347
|
+
return tuple(self._items)
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def edges(self) -> tuple[GraphEdge, ...]:
|
|
351
|
+
return tuple(self._edges)
|
|
352
|
+
|
|
353
|
+
def item(self, name: str) -> ComponentRef:
|
|
354
|
+
try:
|
|
355
|
+
return self._items_by_name[name]
|
|
356
|
+
except KeyError as exc:
|
|
357
|
+
raise ValueError(f"Production item does not exist: {name}") from exc
|
|
358
|
+
|
|
359
|
+
def component_ref(
|
|
360
|
+
self,
|
|
361
|
+
component: ComponentRef | TargetSettingRef | str,
|
|
362
|
+
) -> ComponentRef:
|
|
363
|
+
component_name = self._runtime_component_name(component)
|
|
364
|
+
return self.item(component_name)
|
|
365
|
+
|
|
366
|
+
def get_component(
|
|
367
|
+
self,
|
|
368
|
+
component: ComponentRef | TargetSettingRef | str,
|
|
369
|
+
) -> ComponentRef:
|
|
370
|
+
return self.component_ref(component)
|
|
371
|
+
|
|
372
|
+
def component(
|
|
373
|
+
self,
|
|
374
|
+
name_or_cls: str | type,
|
|
375
|
+
cls: type | None = None,
|
|
376
|
+
*,
|
|
377
|
+
class_name: str | None = None,
|
|
378
|
+
adapter_class: type | str | None = None,
|
|
379
|
+
adapter_class_name: str | None = None,
|
|
380
|
+
kind: str = "component",
|
|
381
|
+
enabled: bool | str = True,
|
|
382
|
+
pool_size: int | str = 1,
|
|
383
|
+
category: str = "",
|
|
384
|
+
foreground: bool | str = False,
|
|
385
|
+
comment: str = "",
|
|
386
|
+
log_trace_events: bool | str = False,
|
|
387
|
+
schedule: str = "",
|
|
388
|
+
settings: dict[str, Any] | None = None,
|
|
389
|
+
adapter_settings: dict[str, Any] | None = None,
|
|
390
|
+
) -> ComponentRef:
|
|
391
|
+
item_name: str
|
|
392
|
+
component_class: type | None
|
|
393
|
+
|
|
394
|
+
if isinstance(name_or_cls, type) and cls is None:
|
|
395
|
+
component_class = name_or_cls
|
|
396
|
+
item_name = component_class.__name__
|
|
397
|
+
else:
|
|
398
|
+
item_name = str(name_or_cls)
|
|
399
|
+
component_class = cls
|
|
400
|
+
|
|
401
|
+
if item_name in self._items_by_name:
|
|
402
|
+
raise ValueError(f"Production item already exists: {item_name}")
|
|
403
|
+
|
|
404
|
+
adapter_class_ref: type | None = None
|
|
405
|
+
resolved_adapter_class_name = adapter_class_name or ""
|
|
406
|
+
if adapter_class is not None:
|
|
407
|
+
if isinstance(adapter_class, type):
|
|
408
|
+
adapter_class_ref = adapter_class
|
|
409
|
+
resolved_adapter_class_name = (
|
|
410
|
+
resolved_adapter_class_name
|
|
411
|
+
or _auto_proxy_class_name(adapter_class)
|
|
412
|
+
)
|
|
413
|
+
else:
|
|
414
|
+
resolved_adapter_class_name = (
|
|
415
|
+
resolved_adapter_class_name
|
|
416
|
+
or str(adapter_class)
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
ref = ComponentRef(
|
|
420
|
+
production=self,
|
|
421
|
+
name=item_name,
|
|
422
|
+
component_class=component_class,
|
|
423
|
+
class_name=class_name,
|
|
424
|
+
adapter_class=adapter_class_ref,
|
|
425
|
+
adapter_class_name=resolved_adapter_class_name,
|
|
426
|
+
kind=kind,
|
|
427
|
+
category=category,
|
|
428
|
+
pool_size=pool_size,
|
|
429
|
+
enabled=enabled,
|
|
430
|
+
foreground=foreground,
|
|
431
|
+
comment=comment,
|
|
432
|
+
log_trace_events=log_trace_events,
|
|
433
|
+
schedule=schedule,
|
|
434
|
+
host_settings=dict(settings or {}),
|
|
435
|
+
adapter_settings=dict(adapter_settings or {}),
|
|
436
|
+
)
|
|
437
|
+
self._add_item(ref)
|
|
438
|
+
return ref
|
|
439
|
+
|
|
440
|
+
def add_component(self, *args: Any, **kwargs: Any) -> ComponentRef:
|
|
441
|
+
return self.component(*args, **kwargs)
|
|
442
|
+
|
|
443
|
+
def update_component(
|
|
444
|
+
self,
|
|
445
|
+
item: ComponentRef | str,
|
|
446
|
+
**kwargs: Any,
|
|
447
|
+
) -> ComponentRef:
|
|
448
|
+
ref = self._component_ref(item)
|
|
449
|
+
scalar_fields = {
|
|
450
|
+
"kind",
|
|
451
|
+
"category",
|
|
452
|
+
"pool_size",
|
|
453
|
+
"enabled",
|
|
454
|
+
"foreground",
|
|
455
|
+
"comment",
|
|
456
|
+
"log_trace_events",
|
|
457
|
+
"schedule",
|
|
458
|
+
}
|
|
459
|
+
for field_name in scalar_fields:
|
|
460
|
+
if field_name in kwargs:
|
|
461
|
+
setattr(ref, field_name, kwargs.pop(field_name))
|
|
462
|
+
|
|
463
|
+
if "component_class" in kwargs:
|
|
464
|
+
ref.component_class = kwargs.pop("component_class")
|
|
465
|
+
if "class_name" not in kwargs and ref.component_class is not None:
|
|
466
|
+
ref.class_name = _auto_proxy_class_name(ref.component_class)
|
|
467
|
+
if "class_name" in kwargs:
|
|
468
|
+
ref.class_name = kwargs.pop("class_name")
|
|
469
|
+
if ref.class_name is None and ref.component_class is None:
|
|
470
|
+
raise ValueError("component_class or class_name is required")
|
|
471
|
+
if "adapter_class" in kwargs:
|
|
472
|
+
adapter_class = kwargs.pop("adapter_class")
|
|
473
|
+
if adapter_class is None:
|
|
474
|
+
ref.adapter_class = None
|
|
475
|
+
elif isinstance(adapter_class, type):
|
|
476
|
+
ref.adapter_class = adapter_class
|
|
477
|
+
if "adapter_class_name" not in kwargs:
|
|
478
|
+
ref.adapter_class_name = _auto_proxy_class_name(adapter_class)
|
|
479
|
+
else:
|
|
480
|
+
ref.adapter_class = None
|
|
481
|
+
if "adapter_class_name" not in kwargs:
|
|
482
|
+
ref.adapter_class_name = str(adapter_class)
|
|
483
|
+
if "adapter_class_name" in kwargs:
|
|
484
|
+
ref.adapter_class_name = str(kwargs.pop("adapter_class_name") or "")
|
|
485
|
+
if not ref.adapter_class_name:
|
|
486
|
+
ref.adapter_class_name = _adapter_type_from_component_class(
|
|
487
|
+
ref.component_class
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
if "settings" in kwargs:
|
|
491
|
+
_apply_settings_update(ref.host_settings, kwargs.pop("settings"))
|
|
492
|
+
if "host_settings" in kwargs:
|
|
493
|
+
_apply_settings_update(ref.host_settings, kwargs.pop("host_settings"))
|
|
494
|
+
if "adapter_settings" in kwargs:
|
|
495
|
+
_apply_settings_update(ref.adapter_settings, kwargs.pop("adapter_settings"))
|
|
496
|
+
if "other_settings" in kwargs:
|
|
497
|
+
ref.other_settings = [dict(value) for value in kwargs.pop("other_settings")]
|
|
498
|
+
if "target_settings" in kwargs:
|
|
499
|
+
ref.target_setting_names.update(
|
|
500
|
+
str(value) for value in kwargs.pop("target_settings")
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
if kwargs:
|
|
504
|
+
names = ", ".join(sorted(kwargs))
|
|
505
|
+
raise ValueError(f"Unsupported component update fields: {names}")
|
|
506
|
+
return ref
|
|
507
|
+
|
|
508
|
+
def delete_component(self, item: ComponentRef | str) -> None:
|
|
509
|
+
ref = self._component_ref(item)
|
|
510
|
+
self._items = [existing for existing in self._items if existing is not ref]
|
|
511
|
+
self._items_by_name.pop(ref.name, None)
|
|
512
|
+
|
|
513
|
+
for edge in list(self._edges):
|
|
514
|
+
if edge.target != ref.name:
|
|
515
|
+
continue
|
|
516
|
+
source_item = edge.source_item
|
|
517
|
+
source_target_setting = edge.source_target_setting
|
|
518
|
+
source_ref = self._items_by_name.get(source_item)
|
|
519
|
+
if source_ref is not None and source_target_setting:
|
|
520
|
+
if source_ref.host_settings.get(source_target_setting) == ref.name:
|
|
521
|
+
source_ref.host_settings.pop(source_target_setting, None)
|
|
522
|
+
|
|
523
|
+
self._connections = {
|
|
524
|
+
key: targets
|
|
525
|
+
for key, targets in (
|
|
526
|
+
(key, [target for target in targets if target != ref.name])
|
|
527
|
+
for key, targets in self._connections.items()
|
|
528
|
+
if key[0] != ref.name
|
|
529
|
+
)
|
|
530
|
+
if targets
|
|
531
|
+
}
|
|
532
|
+
self._edges = [
|
|
533
|
+
edge
|
|
534
|
+
for edge in self._edges
|
|
535
|
+
if edge.source_item != ref.name and edge.target != ref.name
|
|
536
|
+
]
|
|
537
|
+
|
|
538
|
+
def disconnect(self, source: TargetSettingRef | str) -> None:
|
|
539
|
+
if isinstance(source, TargetSettingRef):
|
|
540
|
+
if source.production is not self:
|
|
541
|
+
raise ValueError(
|
|
542
|
+
"source target setting belongs to a different Production"
|
|
543
|
+
)
|
|
544
|
+
item_name = source.item_name
|
|
545
|
+
target_setting_name = source.name
|
|
546
|
+
else:
|
|
547
|
+
item_name, separator, target_setting_name = str(source).rpartition(".")
|
|
548
|
+
if not separator:
|
|
549
|
+
raise ValueError(
|
|
550
|
+
"source must be a TargetSettingRef or an "
|
|
551
|
+
"Item.TargetSetting string"
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
key = (item_name, target_setting_name)
|
|
555
|
+
if key not in self._connections:
|
|
556
|
+
raise ValueError(
|
|
557
|
+
"Production target setting is not connected: "
|
|
558
|
+
f"{item_name}.{target_setting_name}"
|
|
559
|
+
)
|
|
560
|
+
self._connections.pop(key, None)
|
|
561
|
+
ref = self.item(item_name)
|
|
562
|
+
ref.host_settings.pop(target_setting_name, None)
|
|
563
|
+
self._edges = [
|
|
564
|
+
edge
|
|
565
|
+
for edge in self._edges
|
|
566
|
+
if not (
|
|
567
|
+
edge.source_item == item_name
|
|
568
|
+
and edge.source_target_setting == target_setting_name
|
|
569
|
+
)
|
|
570
|
+
]
|
|
571
|
+
|
|
572
|
+
def service(self, name_or_cls: str | type, cls: type | None = None, **kwargs):
|
|
573
|
+
"""Add a BusinessService item to the production graph.
|
|
574
|
+
|
|
575
|
+
See docs/cookbooks/hello-world-production.md and
|
|
576
|
+
docs/cookbooks/hl7v2-native-input.md.
|
|
577
|
+
"""
|
|
578
|
+
return self.component(name_or_cls, cls, kind="service", **kwargs)
|
|
579
|
+
|
|
580
|
+
def process(self, name_or_cls: str | type, cls: type | None = None, **kwargs):
|
|
581
|
+
"""Add a BusinessProcess item to the production graph.
|
|
582
|
+
|
|
583
|
+
See docs/cookbooks/add-business-process.md.
|
|
584
|
+
"""
|
|
585
|
+
return self.component(name_or_cls, cls, kind="process", **kwargs)
|
|
586
|
+
|
|
587
|
+
def operation(self, name_or_cls: str | type, cls: type | None = None, **kwargs):
|
|
588
|
+
"""Add a BusinessOperation item to the production graph.
|
|
589
|
+
|
|
590
|
+
See docs/cookbooks/add-business-operation.md.
|
|
591
|
+
"""
|
|
592
|
+
return self.component(name_or_cls, cls, kind="operation", **kwargs)
|
|
593
|
+
|
|
594
|
+
def message(
|
|
595
|
+
self,
|
|
596
|
+
iris_classname: str,
|
|
597
|
+
msg_cls: type,
|
|
598
|
+
*,
|
|
599
|
+
sync_schema: bool = True,
|
|
600
|
+
) -> Production:
|
|
601
|
+
from ..messages.persistent import (
|
|
602
|
+
get_python_classname,
|
|
603
|
+
is_persistent_message_class,
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
iris_classname = str(iris_classname or "").strip()
|
|
607
|
+
if not iris_classname:
|
|
608
|
+
raise ValueError("PersistentMessage IRIS classname is required")
|
|
609
|
+
if not is_persistent_message_class(msg_cls):
|
|
610
|
+
raise TypeError("msg_cls must be a concrete PersistentMessage subclass")
|
|
611
|
+
|
|
612
|
+
python_classname = get_python_classname(msg_cls)
|
|
613
|
+
for registration in self._messages:
|
|
614
|
+
registered_python_classname = get_python_classname(
|
|
615
|
+
registration.message_class
|
|
616
|
+
)
|
|
617
|
+
if registration.iris_classname == iris_classname:
|
|
618
|
+
if registered_python_classname != python_classname:
|
|
619
|
+
raise ValueError(
|
|
620
|
+
"PersistentMessage IRIS classname already registered: "
|
|
621
|
+
f"{iris_classname}"
|
|
622
|
+
)
|
|
623
|
+
if registration.sync_schema != bool(sync_schema):
|
|
624
|
+
raise ValueError(
|
|
625
|
+
"PersistentMessage registration has conflicting sync_schema "
|
|
626
|
+
f"for {iris_classname}"
|
|
627
|
+
)
|
|
628
|
+
return self
|
|
629
|
+
if registered_python_classname == python_classname:
|
|
630
|
+
raise ValueError(
|
|
631
|
+
"PersistentMessage class already registered with IRIS classname "
|
|
632
|
+
f"{registration.iris_classname}"
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
self._messages.append(
|
|
636
|
+
PersistentMessageRegistration(
|
|
637
|
+
iris_classname=iris_classname,
|
|
638
|
+
message_class=msg_cls,
|
|
639
|
+
sync_schema=bool(sync_schema),
|
|
640
|
+
)
|
|
641
|
+
)
|
|
642
|
+
return self
|
|
643
|
+
|
|
644
|
+
def connect(
|
|
645
|
+
self,
|
|
646
|
+
source: TargetSettingRef,
|
|
647
|
+
target_component: ComponentRef | str | None = None,
|
|
648
|
+
*,
|
|
649
|
+
mode: str = "replace",
|
|
650
|
+
) -> None:
|
|
651
|
+
if not isinstance(source, TargetSettingRef):
|
|
652
|
+
raise TypeError(
|
|
653
|
+
"source must be a TargetSettingRef returned from a ComponentRef"
|
|
654
|
+
)
|
|
655
|
+
if source.production is not self:
|
|
656
|
+
raise ValueError("source target setting belongs to a different Production")
|
|
657
|
+
|
|
658
|
+
mode = _connection_mode(mode)
|
|
659
|
+
if mode == "remove":
|
|
660
|
+
self._remove_connection(source, target_component)
|
|
661
|
+
return
|
|
662
|
+
if target_component is None:
|
|
663
|
+
raise ValueError("target_component is required unless mode='remove'")
|
|
664
|
+
|
|
665
|
+
target_name = self._component_name(target_component)
|
|
666
|
+
if mode == "replace":
|
|
667
|
+
source.component.set_host_setting(source.name, target_name)
|
|
668
|
+
else:
|
|
669
|
+
existing = source.component.host_settings.get(source.name, "")
|
|
670
|
+
existing_targets = [t.strip() for t in existing.split(",") if t.strip()]
|
|
671
|
+
if target_name not in existing_targets:
|
|
672
|
+
source.component.host_settings[source.name] = ",".join(
|
|
673
|
+
existing_targets + [target_name]
|
|
674
|
+
)
|
|
675
|
+
source.component.target_setting_names.add(source.name)
|
|
676
|
+
self._register_connection(
|
|
677
|
+
source.item_name,
|
|
678
|
+
source.name,
|
|
679
|
+
target_name,
|
|
680
|
+
replace_target_setting=mode == "replace",
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
def _remove_connection(
|
|
684
|
+
self,
|
|
685
|
+
source: TargetSettingRef,
|
|
686
|
+
target_component: ComponentRef | str | None,
|
|
687
|
+
) -> None:
|
|
688
|
+
if target_component is None:
|
|
689
|
+
self.disconnect(source)
|
|
690
|
+
return
|
|
691
|
+
|
|
692
|
+
target_name = self._component_name(target_component)
|
|
693
|
+
key = (source.item_name, source.name)
|
|
694
|
+
targets = self._connections.get(key, [])
|
|
695
|
+
edge_exists = any(
|
|
696
|
+
edge.source_item == source.item_name
|
|
697
|
+
and edge.source_target_setting == source.name
|
|
698
|
+
and edge.target == target_name
|
|
699
|
+
for edge in self._edges
|
|
700
|
+
)
|
|
701
|
+
if target_name not in targets and not edge_exists:
|
|
702
|
+
raise ValueError(
|
|
703
|
+
"Production target setting is not connected: "
|
|
704
|
+
f"{source.path} -> {target_name}"
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
remaining_targets = [target for target in targets if target != target_name]
|
|
708
|
+
if remaining_targets:
|
|
709
|
+
self._connections[key] = remaining_targets
|
|
710
|
+
source.component.host_settings[source.name] = ",".join(remaining_targets)
|
|
711
|
+
else:
|
|
712
|
+
self._connections.pop(key, None)
|
|
713
|
+
source.component.host_settings.pop(source.name, None)
|
|
714
|
+
|
|
715
|
+
self._edges = [
|
|
716
|
+
edge
|
|
717
|
+
for edge in self._edges
|
|
718
|
+
if not (
|
|
719
|
+
edge.source_item == source.item_name
|
|
720
|
+
and edge.source_target_setting == source.name
|
|
721
|
+
and edge.target == target_name
|
|
722
|
+
)
|
|
723
|
+
]
|
|
724
|
+
|
|
725
|
+
def resolve_target_setting_ref(self, target_setting_ref: TargetSettingRef) -> str:
|
|
726
|
+
if target_setting_ref.production is not self:
|
|
727
|
+
raise ValueError(
|
|
728
|
+
"target setting ref belongs to a different Production"
|
|
729
|
+
)
|
|
730
|
+
return self._resolve_connection_target(
|
|
731
|
+
target_setting_ref.item_name,
|
|
732
|
+
target_setting_ref.name,
|
|
733
|
+
target_setting_ref.path,
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
def _resolve_connection_target(
|
|
737
|
+
self,
|
|
738
|
+
item_name: str,
|
|
739
|
+
target_setting_name: str,
|
|
740
|
+
path: str,
|
|
741
|
+
) -> str:
|
|
742
|
+
key = (item_name, target_setting_name)
|
|
743
|
+
targets = self._connections.get(key)
|
|
744
|
+
if not targets:
|
|
745
|
+
raise ValueError(f"Production target setting is not connected: {path}")
|
|
746
|
+
if len(targets) > 1:
|
|
747
|
+
target_list = ", ".join(repr(target) for target in targets)
|
|
748
|
+
raise ValueError(
|
|
749
|
+
f"Production target setting is ambiguous: {path} resolves to "
|
|
750
|
+
f"multiple targets ({target_list})"
|
|
751
|
+
)
|
|
752
|
+
return targets[0]
|
|
753
|
+
|
|
754
|
+
def resolve_target(
|
|
755
|
+
self,
|
|
756
|
+
target_or_ref: str | TargetSettingRef | ComponentRef,
|
|
757
|
+
) -> str:
|
|
758
|
+
if isinstance(target_or_ref, TargetSettingRef):
|
|
759
|
+
return self.resolve_target_setting_ref(target_or_ref)
|
|
760
|
+
if isinstance(target_or_ref, ComponentRef):
|
|
761
|
+
return self._component_name(target_or_ref)
|
|
762
|
+
|
|
763
|
+
target_name = str(target_or_ref)
|
|
764
|
+
if target_name in self._items_by_name:
|
|
765
|
+
return target_name
|
|
766
|
+
|
|
767
|
+
if "." in target_name:
|
|
768
|
+
item_name, _, target_setting_name = target_name.rpartition(".")
|
|
769
|
+
if item_name not in self._items_by_name:
|
|
770
|
+
raise ValueError(f"Production item does not exist: {item_name}")
|
|
771
|
+
key = (item_name, target_setting_name)
|
|
772
|
+
if key not in self._connections:
|
|
773
|
+
raise ValueError(
|
|
774
|
+
f"Production target setting is not connected: {target_name}"
|
|
775
|
+
)
|
|
776
|
+
return self._resolve_connection_target(
|
|
777
|
+
item_name,
|
|
778
|
+
target_setting_name,
|
|
779
|
+
target_name,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
raise ValueError(f"Production item does not exist: {target_name}")
|
|
783
|
+
|
|
784
|
+
def to_dict(self) -> dict[str, Any]:
|
|
785
|
+
return production_to_dict(self)
|
|
786
|
+
|
|
787
|
+
def to_xml(self) -> str:
|
|
788
|
+
return production_to_xml(self)
|
|
789
|
+
|
|
790
|
+
def to_python(self) -> str:
|
|
791
|
+
return production_to_python(self)
|
|
792
|
+
|
|
793
|
+
def to_class(self) -> str:
|
|
794
|
+
return production_to_class(self)
|
|
795
|
+
|
|
796
|
+
def to_mermaid(self, *, direction: str = "LR") -> str:
|
|
797
|
+
return self.graph().to_mermaid(direction=direction)
|
|
798
|
+
|
|
799
|
+
def graph(self) -> ProductionGraph:
|
|
800
|
+
return production_graph(self)
|
|
801
|
+
|
|
802
|
+
def diff(
|
|
803
|
+
self,
|
|
804
|
+
other: Production | dict[str, Any] | None = None,
|
|
805
|
+
) -> ProductionDiff:
|
|
806
|
+
"""Compare deployable IRIS shape against current/imported state.
|
|
807
|
+
|
|
808
|
+
With no *other* argument, IRIS is queried through ``from_iris()`` and the
|
|
809
|
+
returned diff describes what would change to make that runtime
|
|
810
|
+
reconstruction match this Python object.
|
|
811
|
+
"""
|
|
812
|
+
return self.diff_deployable(other)
|
|
813
|
+
|
|
814
|
+
def diff_deployable(
|
|
815
|
+
self,
|
|
816
|
+
other: Production | dict[str, Any] | None = None,
|
|
817
|
+
) -> ProductionDiff:
|
|
818
|
+
"""Compare settings, items, and deployable routes.
|
|
819
|
+
|
|
820
|
+
Edge import metadata such as ``origin`` is ignored so an authored Python
|
|
821
|
+
route and an equivalent IRIS setting-inferred route do not produce a
|
|
822
|
+
false deployment change.
|
|
823
|
+
"""
|
|
824
|
+
current = self._diff_current(other)
|
|
825
|
+
return _diff_productions(desired=self, current=current)
|
|
826
|
+
|
|
827
|
+
def graph_diff(
|
|
828
|
+
self,
|
|
829
|
+
other: Production | dict[str, Any] | None = None,
|
|
830
|
+
) -> ProductionDiff:
|
|
831
|
+
"""Compare graph topology including edge origin and route metadata."""
|
|
832
|
+
current = self._diff_current(other)
|
|
833
|
+
return _diff_productions(
|
|
834
|
+
desired=self,
|
|
835
|
+
current=current,
|
|
836
|
+
include_graph_metadata=True,
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
def plan(
|
|
840
|
+
self,
|
|
841
|
+
other: Production | dict[str, Any] | None = None,
|
|
842
|
+
) -> ProductionChangePlan:
|
|
843
|
+
"""Build a conservative granular change plan against current/imported state."""
|
|
844
|
+
current = self._diff_current(other)
|
|
845
|
+
return build_change_plan(self, current)
|
|
846
|
+
|
|
847
|
+
def component_registrations(self) -> tuple[ComponentRef, ...]:
|
|
848
|
+
return tuple(
|
|
849
|
+
item for item in self._items if item.component_class is not None
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
def adapter_registrations(self) -> tuple[ComponentRef, ...]:
|
|
853
|
+
return tuple(
|
|
854
|
+
item for item in self._items if item.adapter_class is not None
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
def message_registrations(self) -> tuple[PersistentMessageRegistration, ...]:
|
|
858
|
+
return tuple(self._messages)
|
|
859
|
+
|
|
860
|
+
def validate(self, *, strict: bool = False):
|
|
861
|
+
from .validation import validate_production
|
|
862
|
+
|
|
863
|
+
return validate_production(self, strict=strict, warn=not strict)
|
|
864
|
+
|
|
865
|
+
def start(self, detach: bool = True) -> None:
|
|
866
|
+
_actions.start(self, detach=detach)
|
|
867
|
+
|
|
868
|
+
def stop(self) -> None:
|
|
869
|
+
_actions.stop(self)
|
|
870
|
+
|
|
871
|
+
def restart(self) -> None:
|
|
872
|
+
_actions.restart(self)
|
|
873
|
+
|
|
874
|
+
def kill(self) -> None:
|
|
875
|
+
_actions.kill(self)
|
|
876
|
+
|
|
877
|
+
def status(self) -> dict:
|
|
878
|
+
return _actions.status(self)
|
|
879
|
+
|
|
880
|
+
def queue(self, *, refresh: bool = True) -> dict[str, dict[str, Any]]:
|
|
881
|
+
return _actions.queue(self, refresh=refresh)
|
|
882
|
+
|
|
883
|
+
def queue_info(self, *, refresh: bool = True) -> dict[str, dict[str, Any]]:
|
|
884
|
+
return self.queue(refresh=refresh)
|
|
885
|
+
|
|
886
|
+
def update(self) -> None:
|
|
887
|
+
_actions.update(self)
|
|
888
|
+
|
|
889
|
+
def inspect_component(
|
|
890
|
+
self,
|
|
891
|
+
component: ComponentRef | TargetSettingRef | str,
|
|
892
|
+
*,
|
|
893
|
+
refresh: bool = True,
|
|
894
|
+
) -> dict[str, Any]:
|
|
895
|
+
return inspect_component(self, component, refresh=refresh)
|
|
896
|
+
|
|
897
|
+
def start_component(self, component: ComponentRef | TargetSettingRef | str) -> None:
|
|
898
|
+
_actions.start_component(self, component)
|
|
899
|
+
|
|
900
|
+
def stop_component(self, component: ComponentRef | TargetSettingRef | str) -> None:
|
|
901
|
+
_actions.stop_component(self, component)
|
|
902
|
+
|
|
903
|
+
def restart_component(
|
|
904
|
+
self,
|
|
905
|
+
component: ComponentRef | TargetSettingRef | str,
|
|
906
|
+
) -> None:
|
|
907
|
+
_actions.restart_component(self, component)
|
|
908
|
+
|
|
909
|
+
def export(self) -> dict:
|
|
910
|
+
return _actions.export(self)
|
|
911
|
+
|
|
912
|
+
def set_default(self) -> None:
|
|
913
|
+
_actions.set_default(self)
|
|
914
|
+
|
|
915
|
+
def sync(self, *, root_path: str | None = None, update: bool = True) -> None:
|
|
916
|
+
_actions.sync(self, root_path=root_path, update_runtime=update)
|
|
917
|
+
|
|
918
|
+
def apply(
|
|
919
|
+
self,
|
|
920
|
+
plan: ProductionChangePlan | None = None,
|
|
921
|
+
*,
|
|
922
|
+
allow_destructive: bool = False,
|
|
923
|
+
backup_dir: str = ".iop/backups",
|
|
924
|
+
root_path: str | None = None,
|
|
925
|
+
update: bool = True,
|
|
926
|
+
) -> ProductionApplyResult:
|
|
927
|
+
return _actions.apply(
|
|
928
|
+
self,
|
|
929
|
+
plan=plan,
|
|
930
|
+
allow_destructive=allow_destructive,
|
|
931
|
+
backup_dir=backup_dir,
|
|
932
|
+
root_path=root_path,
|
|
933
|
+
update_runtime=update,
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
def verify(self, plan: ProductionChangePlan) -> ProductionVerifyResult:
|
|
937
|
+
return _actions.verify(self, plan)
|
|
938
|
+
|
|
939
|
+
@staticmethod
|
|
940
|
+
def rollback_backup(
|
|
941
|
+
backup_path: str,
|
|
942
|
+
*,
|
|
943
|
+
director: _DirectorProtocol | None = None,
|
|
944
|
+
namespace: str | None = None,
|
|
945
|
+
allow_destructive: bool = False,
|
|
946
|
+
update: bool = True,
|
|
947
|
+
) -> ProductionVerifyResult:
|
|
948
|
+
return _actions.rollback_backup(
|
|
949
|
+
backup_path,
|
|
950
|
+
director=director,
|
|
951
|
+
namespace=namespace,
|
|
952
|
+
allow_destructive=allow_destructive,
|
|
953
|
+
update_runtime=update,
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
def log(self, top: int | None = None) -> None:
|
|
957
|
+
_actions.log(self, top)
|
|
958
|
+
|
|
959
|
+
def test_component(
|
|
960
|
+
self,
|
|
961
|
+
target_or_ref: str | TargetSettingRef | ComponentRef,
|
|
962
|
+
message: Any = None,
|
|
963
|
+
classname: str | None = None,
|
|
964
|
+
body: str | dict | None = None,
|
|
965
|
+
) -> Any:
|
|
966
|
+
return _actions.test_component(
|
|
967
|
+
self,
|
|
968
|
+
target_or_ref,
|
|
969
|
+
message=message,
|
|
970
|
+
classname=classname,
|
|
971
|
+
body=body,
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
def test(
|
|
975
|
+
self,
|
|
976
|
+
target_or_ref: str | TargetSettingRef | ComponentRef,
|
|
977
|
+
message: Any = None,
|
|
978
|
+
classname: str | None = None,
|
|
979
|
+
body: str | dict | None = None,
|
|
980
|
+
) -> Any:
|
|
981
|
+
return self.test_component(
|
|
982
|
+
target_or_ref,
|
|
983
|
+
message=message,
|
|
984
|
+
classname=classname,
|
|
985
|
+
body=body,
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
def _raise_if_existing_production_not_running(self, director: Any) -> None:
|
|
989
|
+
_actions.raise_if_existing_production_not_running(self, director)
|
|
990
|
+
|
|
991
|
+
def _require_current_production(self, director: Any, action: str) -> None:
|
|
992
|
+
_actions.require_current_production(self, director, action)
|
|
993
|
+
|
|
994
|
+
def _require_current_runtime(self, director: Any, action: str) -> None:
|
|
995
|
+
_actions.require_current_runtime(self, director, action)
|
|
996
|
+
|
|
997
|
+
def _read_status(self, director: Any, action: str) -> tuple[str, str]:
|
|
998
|
+
return _actions.read_status(self, director, action)
|
|
999
|
+
|
|
1000
|
+
def _switch_running_production_message(self, current_production: str) -> str:
|
|
1001
|
+
return _actions.switch_running_production_message(self, current_production)
|
|
1002
|
+
|
|
1003
|
+
def _component_runtime_info(self, component_name: str) -> dict[str, Any]:
|
|
1004
|
+
return component_runtime_info(self, component_name)
|
|
1005
|
+
|
|
1006
|
+
def _component_name(self, target_component: ComponentRef | str) -> str:
|
|
1007
|
+
if isinstance(target_component, ComponentRef):
|
|
1008
|
+
if target_component.production is not self:
|
|
1009
|
+
raise ValueError("target component belongs to a different Production")
|
|
1010
|
+
return target_component.name
|
|
1011
|
+
target_name = str(target_component)
|
|
1012
|
+
if target_name not in self._items_by_name:
|
|
1013
|
+
raise ValueError(f"Production item does not exist: {target_name}")
|
|
1014
|
+
return target_name
|
|
1015
|
+
|
|
1016
|
+
def _runtime_component_name(
|
|
1017
|
+
self,
|
|
1018
|
+
component: ComponentRef | TargetSettingRef | str,
|
|
1019
|
+
) -> str:
|
|
1020
|
+
if isinstance(component, TargetSettingRef):
|
|
1021
|
+
component_name = self.resolve_target_setting_ref(component)
|
|
1022
|
+
elif isinstance(component, ComponentRef):
|
|
1023
|
+
component_name = self._component_name(component)
|
|
1024
|
+
else:
|
|
1025
|
+
component_name = self.resolve_target(str(component))
|
|
1026
|
+
if component_name not in self._items_by_name:
|
|
1027
|
+
raise ValueError(f"Production item does not exist: {component_name}")
|
|
1028
|
+
return component_name
|
|
1029
|
+
|
|
1030
|
+
def _component_ref(self, item: ComponentRef | str) -> ComponentRef:
|
|
1031
|
+
if isinstance(item, ComponentRef):
|
|
1032
|
+
if item.production is not self:
|
|
1033
|
+
raise ValueError("component belongs to a different Production")
|
|
1034
|
+
return item
|
|
1035
|
+
return self.item(str(item))
|
|
1036
|
+
|
|
1037
|
+
def _add_item(self, ref: ComponentRef) -> None:
|
|
1038
|
+
if not ref.name:
|
|
1039
|
+
raise ValueError("Production item name is required")
|
|
1040
|
+
if ref.name in self._items_by_name:
|
|
1041
|
+
raise ValueError(f"Production item already exists: {ref.name}")
|
|
1042
|
+
if ref.component_class is not None:
|
|
1043
|
+
for existing in self._items:
|
|
1044
|
+
if (
|
|
1045
|
+
existing.component_class is not None
|
|
1046
|
+
and existing.component_class is not ref.component_class
|
|
1047
|
+
and existing.class_name == ref.class_name
|
|
1048
|
+
):
|
|
1049
|
+
raise ValueError(
|
|
1050
|
+
f"Python classes {existing.component_class.__qualname__!r} and "
|
|
1051
|
+
f"{ref.component_class.__qualname__!r} produce the same IRIS "
|
|
1052
|
+
f"proxy class name {ref.class_name!r}. Rename one of the "
|
|
1053
|
+
"Python classes or supply an explicit class_name= to resolve "
|
|
1054
|
+
"the collision."
|
|
1055
|
+
)
|
|
1056
|
+
self._items.append(ref)
|
|
1057
|
+
self._items_by_name[ref.name] = ref
|
|
1058
|
+
|
|
1059
|
+
def _diff_current(
|
|
1060
|
+
self,
|
|
1061
|
+
other: Production | dict[str, Any] | None,
|
|
1062
|
+
) -> Production:
|
|
1063
|
+
if other is None:
|
|
1064
|
+
return self.from_iris(
|
|
1065
|
+
self.name,
|
|
1066
|
+
namespace=self.namespace,
|
|
1067
|
+
director=self._director,
|
|
1068
|
+
)
|
|
1069
|
+
if isinstance(other, Production):
|
|
1070
|
+
return other
|
|
1071
|
+
if isinstance(other, dict):
|
|
1072
|
+
return self.from_dict(
|
|
1073
|
+
other,
|
|
1074
|
+
namespace=self.namespace,
|
|
1075
|
+
director=self._director,
|
|
1076
|
+
)
|
|
1077
|
+
raise TypeError("other must be a Production, production dictionary, or None")
|
|
1078
|
+
|
|
1079
|
+
def _register_connection(
|
|
1080
|
+
self,
|
|
1081
|
+
source_item: str,
|
|
1082
|
+
source_target_setting: str,
|
|
1083
|
+
target_name: str,
|
|
1084
|
+
*,
|
|
1085
|
+
origin: str = "authored",
|
|
1086
|
+
interaction: str = "request",
|
|
1087
|
+
metadata: dict[str, Any] | None = None,
|
|
1088
|
+
replace_target_setting: bool = False,
|
|
1089
|
+
validate_target: bool = True,
|
|
1090
|
+
) -> None:
|
|
1091
|
+
if source_item not in self._items_by_name:
|
|
1092
|
+
raise ValueError(f"Production item does not exist: {source_item}")
|
|
1093
|
+
if validate_target and target_name not in self._items_by_name:
|
|
1094
|
+
raise ValueError(f"Production item does not exist: {target_name}")
|
|
1095
|
+
if source_target_setting:
|
|
1096
|
+
key = (source_item, source_target_setting)
|
|
1097
|
+
if replace_target_setting:
|
|
1098
|
+
self._connections[key] = [target_name]
|
|
1099
|
+
else:
|
|
1100
|
+
self._connections.setdefault(key, [])
|
|
1101
|
+
if target_name not in self._connections[key]:
|
|
1102
|
+
self._connections[key].append(target_name)
|
|
1103
|
+
|
|
1104
|
+
edge = GraphEdge(
|
|
1105
|
+
source_item=source_item,
|
|
1106
|
+
source_target_setting=source_target_setting,
|
|
1107
|
+
target=target_name,
|
|
1108
|
+
origin=origin,
|
|
1109
|
+
interaction=interaction,
|
|
1110
|
+
metadata=dict(metadata or {}),
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
if replace_target_setting and source_target_setting:
|
|
1114
|
+
self._edges = [
|
|
1115
|
+
existing
|
|
1116
|
+
for existing in self._edges
|
|
1117
|
+
if not (
|
|
1118
|
+
existing.source_item == source_item
|
|
1119
|
+
and existing.source_target_setting == source_target_setting
|
|
1120
|
+
)
|
|
1121
|
+
]
|
|
1122
|
+
|
|
1123
|
+
edge_key = _edge_identity(edge)
|
|
1124
|
+
self._edges = [
|
|
1125
|
+
existing
|
|
1126
|
+
for existing in self._edges
|
|
1127
|
+
if _edge_identity(existing) != edge_key
|
|
1128
|
+
]
|
|
1129
|
+
self._edges.append(edge)
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def _connection_mode(mode: str) -> str:
|
|
1133
|
+
normalized = str(mode or "").strip().lower()
|
|
1134
|
+
if normalized in {"replace", "set"}:
|
|
1135
|
+
return "replace"
|
|
1136
|
+
if normalized in {"add", "append"}:
|
|
1137
|
+
return "add"
|
|
1138
|
+
if normalized in {"remove", "delete", "disconnect"}:
|
|
1139
|
+
return "remove"
|
|
1140
|
+
raise ValueError(
|
|
1141
|
+
"Unsupported connection mode: "
|
|
1142
|
+
f"{mode!r}. Expected 'replace', 'add', or 'remove'."
|
|
1143
|
+
)
|