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,546 @@
1
+ from __future__ import annotations
2
+
3
+ import html
4
+ import json
5
+ import re
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, overload
8
+
9
+ from ..components.settings import Category, Setting, controls
10
+
11
+
12
+ class TargetSetting(Setting):
13
+ """Production target setting descriptor created by target()."""
14
+
15
+ def __init__(self, **kwargs: Any):
16
+ kwargs.setdefault("iris_type", "Ens.DataType.ConfigName")
17
+ kwargs.setdefault("category", Category.BASIC)
18
+ kwargs.setdefault("control", controls.production_item())
19
+ super().__init__("", **kwargs)
20
+
21
+ @overload
22
+ def __get__(self, instance: None, owner: type | None = None) -> TargetSetting: ...
23
+
24
+ @overload
25
+ def __get__(self, instance: object, owner: type | None = None) -> str: ...
26
+
27
+ def __get__(
28
+ self,
29
+ instance: object | None,
30
+ owner: type | None = None,
31
+ ) -> TargetSetting | str:
32
+ if instance is None:
33
+ return self
34
+ return super().__get__(instance, owner)
35
+
36
+
37
+ def target(**kwargs: Any) -> TargetSetting:
38
+ """Purpose:
39
+ Declare a configurable outbound target setting on a component class.
40
+
41
+ Use when:
42
+ A service or process sends messages to another production component.
43
+
44
+ Lifecycle:
45
+ The descriptor becomes an IRIS production setting. Production.connect(...)
46
+ binds it to a concrete target component.
47
+
48
+ Best practices:
49
+ Use target() instead of hard-coded component names for routes that
50
+ should be visible and configurable in the production graph.
51
+
52
+ Common mistakes:
53
+ Do not use setting(...) for outbound routing when target() should expose
54
+ a production target.
55
+
56
+ Minimal example:
57
+ class Router(BusinessProcess):
58
+ Output = target()
59
+
60
+ prod.connect(router.Output, operation)
61
+
62
+ Related:
63
+ docs/cookbooks/production-settings-and-targets.md,
64
+ docs/production-graph.md
65
+ """
66
+ return TargetSetting(**kwargs)
67
+
68
+
69
+ @dataclass
70
+ class TargetSettingRef:
71
+ """Bound reference to a component target setting inside a Production."""
72
+
73
+ production: Any
74
+ component: Any
75
+ name: str
76
+
77
+ @property
78
+ def item_name(self) -> str:
79
+ return self.component.name
80
+
81
+ @property
82
+ def path(self) -> str:
83
+ return f"{self.item_name}.{self.name}"
84
+
85
+ def resolve(self) -> str:
86
+ return self.production.resolve_target_setting_ref(self)
87
+
88
+ def __str__(self) -> str:
89
+ return self.path
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class PersistentMessageRegistration:
94
+ iris_classname: str
95
+ message_class: type
96
+ sync_schema: bool = True
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class GraphNode:
101
+ name: str
102
+ class_name: str
103
+ kind: str = "component"
104
+ enabled: bool | str = True
105
+ category: str = ""
106
+ adapter_class_name: str = ""
107
+
108
+ def to_dict(self) -> dict[str, Any]:
109
+ data: dict[str, Any] = {
110
+ "name": self.name,
111
+ "class_name": self.class_name,
112
+ "kind": self.kind,
113
+ "enabled": self.enabled,
114
+ "category": self.category,
115
+ }
116
+ if self.adapter_class_name:
117
+ data["adapter_class_name"] = self.adapter_class_name
118
+ return data
119
+
120
+
121
+ @dataclass(frozen=True)
122
+ class GraphEdge:
123
+ """Possible communication route between two production items.
124
+
125
+ An IRIS production topology can be modeled as a directed multigraph of
126
+ possible communication routes. A graph edge is not an execution dependency
127
+ and does not imply DAG semantics.
128
+ """
129
+
130
+ source_item: str
131
+ target: str
132
+ source_target_setting: str = ""
133
+ origin: str = "authored"
134
+ interaction: str = "request"
135
+ metadata: dict[str, Any] = field(default_factory=dict)
136
+
137
+ def __post_init__(self) -> None:
138
+ origin = self.origin or "authored"
139
+ if origin not in {"authored", "runtime", "inferred"}:
140
+ raise ValueError(f"Unsupported GraphEdge origin: {origin!r}")
141
+ object.__setattr__(self, "origin", origin)
142
+ object.__setattr__(self, "interaction", self.interaction or "unknown")
143
+ object.__setattr__(self, "metadata", dict(self.metadata or {}))
144
+
145
+ @property
146
+ def runtime(self) -> bool:
147
+ return self.origin == "runtime"
148
+
149
+ @property
150
+ def inferred(self) -> bool:
151
+ return self.origin == "inferred"
152
+
153
+ @property
154
+ def source(self) -> str:
155
+ if self.source_target_setting:
156
+ return f"{self.source_item}.{self.source_target_setting}"
157
+ return self.source_item
158
+
159
+ def to_dict(self) -> dict[str, Any]:
160
+ data: dict[str, Any] = {
161
+ "source": self.source,
162
+ "source_item": self.source_item,
163
+ "source_target_setting": self.source_target_setting,
164
+ "target": self.target,
165
+ "origin": self.origin,
166
+ "interaction": self.interaction,
167
+ }
168
+ if self.metadata:
169
+ data["metadata"] = dict(self.metadata)
170
+ if self.runtime:
171
+ data["runtime"] = True
172
+ if self.inferred:
173
+ data["inferred"] = True
174
+ return data
175
+
176
+
177
+ @dataclass(frozen=True)
178
+ class ProductionGraph:
179
+ production_name: str
180
+ nodes: tuple[GraphNode, ...]
181
+ edges: tuple[GraphEdge, ...]
182
+ warnings: tuple[str, ...] = ()
183
+
184
+ def to_dict(self) -> dict[str, Any]:
185
+ data: dict[str, Any] = {
186
+ "production": self.production_name,
187
+ "nodes": [node.to_dict() for node in self.nodes],
188
+ "edges": [edge.to_dict() for edge in self.edges],
189
+ }
190
+ if self.warnings:
191
+ data["warnings"] = list(self.warnings)
192
+ return data
193
+
194
+ def to_text(self) -> str:
195
+ outgoing: dict[str, list[GraphEdge]] = {node.name: [] for node in self.nodes}
196
+ node_names = set(outgoing)
197
+ for edge in self.edges:
198
+ outgoing.setdefault(edge.source_item, []).append(edge)
199
+
200
+ lines = [self.production_name]
201
+ for node in self.nodes:
202
+ label = f" [{node.class_name}]" if node.class_name else ""
203
+ lines.append(f" {node.name}{label}")
204
+ for edge in sorted(
205
+ outgoing.get(node.name, ()),
206
+ key=lambda item: (item.source_target_setting, item.target),
207
+ ):
208
+ source_target_setting = edge.source_target_setting or "(runtime)"
209
+ suffix = "" if edge.target in node_names else " (unresolved)"
210
+ labels = []
211
+ if edge.origin != "authored":
212
+ labels.append(edge.origin)
213
+ if edge.interaction not in ("", "request"):
214
+ labels.append(edge.interaction)
215
+ label = f" [{', '.join(labels)}]" if labels else ""
216
+ lines.append(
217
+ f" {source_target_setting} -> {edge.target}{suffix}{label}"
218
+ )
219
+ if self.warnings:
220
+ lines.append(" warnings:")
221
+ lines.extend(f" {warning}" for warning in self.warnings)
222
+ return "\n".join(lines)
223
+
224
+ def to_mermaid(self, *, direction: str = "LR") -> str:
225
+ """Render the production topology as a Mermaid flowchart."""
226
+ direction = _mermaid_direction(direction)
227
+ node_ids = _mermaid_node_ids(self.nodes, self.edges)
228
+ defined_node_names = {node.name for node in self.nodes}
229
+
230
+ lines = [
231
+ f"flowchart {direction}",
232
+ f" %% Production: {_mermaid_comment(self.production_name)}",
233
+ ]
234
+ lines.extend(_mermaid_production_layout_node_lines(self.nodes, node_ids))
235
+
236
+ unresolved_names = sorted({
237
+ name
238
+ for edge in self.edges
239
+ for name in (edge.source_item, edge.target)
240
+ if name not in defined_node_names
241
+ })
242
+ for name in unresolved_names:
243
+ lines.append(
244
+ f" {node_ids[name]}[\"{_mermaid_text(name)}<br/>(unresolved)\"]"
245
+ )
246
+
247
+ lines.extend(_mermaid_production_layout_hint_lines(self.nodes, node_ids))
248
+
249
+ for edge in sorted(
250
+ self.edges,
251
+ key=lambda item: (
252
+ item.source_item,
253
+ item.source_target_setting,
254
+ item.target,
255
+ item.origin,
256
+ item.interaction,
257
+ ),
258
+ ):
259
+ edge_line = _mermaid_edge_line(
260
+ node_ids[edge.source_item],
261
+ node_ids[edge.target],
262
+ edge,
263
+ )
264
+ lines.append(
265
+ f" {edge_line}"
266
+ )
267
+
268
+ if self.warnings:
269
+ lines.append(" %% Warnings:")
270
+ lines.extend(
271
+ f" %% - {_mermaid_comment(warning)}"
272
+ for warning in self.warnings
273
+ )
274
+ return "\n".join(lines) + "\n"
275
+
276
+ def __str__(self) -> str:
277
+ return self.to_text()
278
+
279
+
280
+ @dataclass(frozen=True)
281
+ class ProductionDiffEntry:
282
+ """One deterministic desired-vs-current production difference."""
283
+
284
+ action: str
285
+ kind: str
286
+ path: str
287
+ before: Any = None
288
+ after: Any = None
289
+ detail: str = ""
290
+
291
+ def to_dict(self) -> dict[str, Any]:
292
+ data: dict[str, Any] = {
293
+ "action": self.action,
294
+ "kind": self.kind,
295
+ "path": self.path,
296
+ }
297
+ if self.before is not None:
298
+ data["before"] = self.before
299
+ if self.after is not None:
300
+ data["after"] = self.after
301
+ if self.detail:
302
+ data["detail"] = self.detail
303
+ return data
304
+
305
+ def to_text(self) -> str:
306
+ line = f"{self.action} {self.kind} {self.path}"
307
+ if self.detail:
308
+ return f"{line}: {self.detail}"
309
+ if self.action == "change":
310
+ return f"{line}: {_diff_value_text(self.before)} -> {_diff_value_text(self.after)}"
311
+ if self.action == "add":
312
+ return f"{line}: {_diff_value_text(self.after)}"
313
+ if self.action == "remove":
314
+ return f"{line}: {_diff_value_text(self.before)}"
315
+ return line
316
+
317
+
318
+ @dataclass(frozen=True)
319
+ class ProductionDiff:
320
+ """Directional diff from current runtime/imported state to desired Python state."""
321
+
322
+ production_name: str
323
+ changes: tuple[ProductionDiffEntry, ...]
324
+ warnings: tuple[str, ...] = ()
325
+
326
+ @property
327
+ def has_changes(self) -> bool:
328
+ return bool(self.changes)
329
+
330
+ def to_dict(self) -> dict[str, Any]:
331
+ data: dict[str, Any] = {
332
+ "production": self.production_name,
333
+ "has_changes": self.has_changes,
334
+ "changes": [change.to_dict() for change in self.changes],
335
+ }
336
+ if self.warnings:
337
+ data["warnings"] = list(self.warnings)
338
+ return data
339
+
340
+ def to_text(self) -> str:
341
+ lines = [f"Production diff: {self.production_name}"]
342
+ if self.changes:
343
+ lines.extend(f" {change.to_text()}" for change in self.changes)
344
+ else:
345
+ lines.append(" no changes")
346
+ if self.warnings:
347
+ lines.append(" warnings:")
348
+ lines.extend(f" {warning}" for warning in self.warnings)
349
+ return "\n".join(lines)
350
+
351
+ def __str__(self) -> str:
352
+ return self.to_text()
353
+
354
+
355
+ def _edge_identity(edge: GraphEdge) -> tuple[Any, ...]:
356
+ return (
357
+ edge.source_item,
358
+ edge.source_target_setting,
359
+ edge.target,
360
+ edge.origin,
361
+ edge.interaction,
362
+ tuple(
363
+ sorted(
364
+ (str(key), _canonical_value(value))
365
+ for key, value in edge.metadata.items()
366
+ )
367
+ ),
368
+ )
369
+
370
+
371
+ def _canonical_value(value: Any) -> Any:
372
+ from .common import _bool_text
373
+
374
+ if isinstance(value, dict):
375
+ return {
376
+ str(key): _canonical_value(value[key])
377
+ for key in sorted(value, key=lambda item: str(item))
378
+ }
379
+ if isinstance(value, (list, tuple)):
380
+ return [_canonical_value(item) for item in value]
381
+ if isinstance(value, bool):
382
+ return _bool_text(value)
383
+ if value is None:
384
+ return ""
385
+ return value
386
+
387
+
388
+ def _diff_value_text(value: Any) -> str:
389
+ if isinstance(value, str):
390
+ return repr(value)
391
+ try:
392
+ return json.dumps(value, sort_keys=True)
393
+ except TypeError:
394
+ return repr(value)
395
+
396
+
397
+ def _mermaid_direction(value: str) -> str:
398
+ direction = str(value or "").strip().upper()
399
+ if direction not in {"TB", "TD", "BT", "RL", "LR"}:
400
+ raise ValueError(f"Unsupported Mermaid flowchart direction: {value!r}")
401
+ return direction
402
+
403
+
404
+ def _mermaid_node_ids(
405
+ nodes: tuple[GraphNode, ...],
406
+ edges: tuple[GraphEdge, ...],
407
+ ) -> dict[str, str]:
408
+ names: list[str] = [node.name for node in nodes]
409
+ for edge in edges:
410
+ for name in (edge.source_item, edge.target):
411
+ if name not in names:
412
+ names.append(name)
413
+
414
+ used: set[str] = set()
415
+ node_ids: dict[str, str] = {}
416
+ for name in names:
417
+ base = re.sub(r"\W+", "_", name).strip("_")
418
+ if not base:
419
+ base = "node"
420
+ base = f"node_{base}"
421
+ candidate = base
422
+ index = 2
423
+ while candidate in used:
424
+ candidate = f"{base}_{index}"
425
+ index += 1
426
+ used.add(candidate)
427
+ node_ids[name] = candidate
428
+ return node_ids
429
+
430
+
431
+ def _mermaid_production_layout_node_lines(
432
+ nodes: tuple[GraphNode, ...],
433
+ node_ids: dict[str, str],
434
+ ) -> list[str]:
435
+ lines: list[str] = []
436
+ for kind, title in _MERMAID_PRODUCTION_GROUPS:
437
+ group_nodes = [
438
+ node
439
+ for node in nodes
440
+ if _mermaid_production_group_kind(node.kind) == kind
441
+ ]
442
+ if not group_nodes:
443
+ continue
444
+ lines.append(f" subgraph group_{kind}[\"{title}\"]")
445
+ lines.append(" direction TB")
446
+ lines.extend(
447
+ f" {node_ids[node.name]}[\"{_mermaid_node_label(node)}\"]"
448
+ for node in group_nodes
449
+ )
450
+ lines.append(" end")
451
+ return lines
452
+
453
+
454
+ def _mermaid_production_layout_hint_lines(
455
+ nodes: tuple[GraphNode, ...],
456
+ node_ids: dict[str, str],
457
+ ) -> list[str]:
458
+ first_node_by_kind: dict[str, GraphNode] = {}
459
+ for node in nodes:
460
+ kind = _mermaid_production_group_kind(node.kind)
461
+ first_node_by_kind.setdefault(kind, node)
462
+
463
+ ordered_ids = [
464
+ node_ids[first_node_by_kind[kind].name]
465
+ for kind, _title in _MERMAID_PRODUCTION_GROUPS
466
+ if kind in first_node_by_kind
467
+ ]
468
+ return [
469
+ f" {left} ~~~ {right}"
470
+ for left, right in zip(ordered_ids, ordered_ids[1:], strict=False)
471
+ ]
472
+
473
+
474
+ _MERMAID_PRODUCTION_GROUPS = (
475
+ ("service", "Services"),
476
+ ("process", "Processes"),
477
+ ("operation", "Operations"),
478
+ ("component", "Other Components"),
479
+ )
480
+
481
+
482
+ def _mermaid_production_group_kind(kind: str) -> str:
483
+ normalized = str(kind or "").strip().lower()
484
+ if normalized in {"service", "process", "operation"}:
485
+ return normalized
486
+ return "component"
487
+
488
+
489
+ def _mermaid_node_label(node: GraphNode) -> str:
490
+ label = _mermaid_text(node.name)
491
+ if node.class_name:
492
+ label = f"{label}<br/>{_mermaid_text(node.class_name)}"
493
+ return label
494
+
495
+
496
+ def _mermaid_edge_label(edge: GraphEdge) -> str:
497
+ parts = [edge.source_target_setting or "(runtime)"]
498
+ message = _mermaid_message_label(edge.metadata)
499
+ if message:
500
+ parts.append(message)
501
+ if edge.origin != "authored":
502
+ parts.append(edge.origin)
503
+ if edge.interaction not in ("", "request"):
504
+ parts.append(edge.interaction)
505
+ return _mermaid_text(" | ".join(parts))
506
+
507
+
508
+ def _mermaid_edge_line(source_id: str, target_id: str, edge: GraphEdge) -> str:
509
+ label = _mermaid_edge_label(edge)
510
+ if _mermaid_edge_is_sync(edge):
511
+ return f"{source_id} <-- \"{label}\" --> {target_id}"
512
+ return (
513
+ f"{source_id} -- "
514
+ f"\"{label}\" "
515
+ f"--> {target_id}"
516
+ )
517
+
518
+
519
+ def _mermaid_edge_is_sync(edge: GraphEdge) -> bool:
520
+ return str(edge.interaction or "").strip().lower() == "sync"
521
+
522
+
523
+ def _mermaid_message_label(metadata: dict[str, Any]) -> str:
524
+ messages = (
525
+ metadata.get("messages")
526
+ or metadata.get("message_types")
527
+ or metadata.get("accepted_messages")
528
+ )
529
+ if isinstance(messages, (list, tuple)):
530
+ return ", ".join(str(message) for message in messages if message)
531
+ if messages:
532
+ return str(messages)
533
+ message = metadata.get("message") or metadata.get("request")
534
+ if message:
535
+ return str(message)
536
+ return ""
537
+
538
+
539
+ def _mermaid_text(value: Any) -> str:
540
+ text = str(value).replace("\r\n", "\n").replace("\r", "\n")
541
+ text = " ".join(part.strip() for part in text.split("\n"))
542
+ return html.escape(text, quote=True)
543
+
544
+
545
+ def _mermaid_comment(value: Any) -> str:
546
+ return str(value).replace("\r\n", " ").replace("\n", " ")