onec-interactive-runtime-core 0.1.19__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 (87) hide show
  1. onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
  2. onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
  3. onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
  4. onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
  5. onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
  6. onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
  7. onec_runtime/AGENTS.md +7 -0
  8. onec_runtime/__init__.py +3 -0
  9. onec_runtime/artifacts.py +59 -0
  10. onec_runtime/bootstrap.py +310 -0
  11. onec_runtime/breakpoint_workspace.py +249 -0
  12. onec_runtime/bsl/__init__.py +139 -0
  13. onec_runtime/bsl/diagnostics.py +1046 -0
  14. onec_runtime/bsl/full_ast_worker_projection.py +522 -0
  15. onec_runtime/bsl/generated_semantic_parser.py +6322 -0
  16. onec_runtime/bsl/lexer.py +164 -0
  17. onec_runtime/bsl/module_catalog.py +420 -0
  18. onec_runtime/bsl/module_delta.py +1079 -0
  19. onec_runtime/bsl/module_universe.py +1877 -0
  20. onec_runtime/bsl/notebook_cells.py +232 -0
  21. onec_runtime/bsl/notebook_method_globals.py +74 -0
  22. onec_runtime/bsl/notebook_methods.py +175 -0
  23. onec_runtime/bsl/parser_artifact_identity.py +119 -0
  24. onec_runtime/bsl/parser_target.py +388 -0
  25. onec_runtime/bsl/preprocessor.py +16 -0
  26. onec_runtime/bsl/semantic_lowering.py +1539 -0
  27. onec_runtime/bsl/source_maps.py +2411 -0
  28. onec_runtime/bsl/worker_dependency_resolver.py +289 -0
  29. onec_runtime/bsl/worker_preprocessor.py +247 -0
  30. onec_runtime/bsl/worker_projection_model.py +137 -0
  31. onec_runtime/bsl/worker_reload_source_map.py +1697 -0
  32. onec_runtime/capture.py +113 -0
  33. onec_runtime/capture_source.py +453 -0
  34. onec_runtime/compact_table.py +376 -0
  35. onec_runtime/compact_table_backend.py +500 -0
  36. onec_runtime/config.py +198 -0
  37. onec_runtime/configurator_agent.py +520 -0
  38. onec_runtime/controller_worker.py +400 -0
  39. onec_runtime/credentials.py +18 -0
  40. onec_runtime/epf_container.py +196 -0
  41. onec_runtime/errors.py +205 -0
  42. onec_runtime/experiment.py +130 -0
  43. onec_runtime/extension_bundle.py +944 -0
  44. onec_runtime/extension_lifecycle.py +402 -0
  45. onec_runtime/extension_state.py +222 -0
  46. onec_runtime/fault_injection.py +30 -0
  47. onec_runtime/kernel.py +179 -0
  48. onec_runtime/lease.py +123 -0
  49. onec_runtime/observation.py +287 -0
  50. onec_runtime/performance_profile.py +245 -0
  51. onec_runtime/privacy.py +263 -0
  52. onec_runtime/processes.py +265 -0
  53. onec_runtime/prototype_runtime.py +2644 -0
  54. onec_runtime/rdbg/__init__.py +1 -0
  55. onec_runtime/rdbg/models.py +130 -0
  56. onec_runtime/rdbg/reconnect.py +59 -0
  57. onec_runtime/rdbg/session.py +913 -0
  58. onec_runtime/rdbg/transport.py +138 -0
  59. onec_runtime/rdbg/xml_codec.py +980 -0
  60. onec_runtime/recovery.py +106 -0
  61. onec_runtime/recovery_journal.py +79 -0
  62. onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
  63. onec_runtime/resources/extension/extension-manifest.json +108 -0
  64. onec_runtime/runtime_api.py +5272 -0
  65. onec_runtime/runtime_contracts.py +196 -0
  66. onec_runtime/server_worker.py +2034 -0
  67. onec_runtime/session.py +2123 -0
  68. onec_runtime/startup_diagnostics.py +45 -0
  69. onec_runtime/stop_routing.py +65 -0
  70. onec_runtime/supervised_processes.py +232 -0
  71. onec_runtime/supervised_runtime.py +512 -0
  72. onec_runtime/supervisor.py +363 -0
  73. onec_runtime/supervisor_model.py +148 -0
  74. onec_runtime/supervisor_protocol.py +77 -0
  75. onec_runtime/table_materialization.py +471 -0
  76. onec_runtime/table_transfer_backend.py +203 -0
  77. onec_runtime/table_value.py +311 -0
  78. onec_runtime/toolchain.py +730 -0
  79. onec_runtime/value_materialization.py +432 -0
  80. onec_runtime/value_transfer_backend.py +199 -0
  81. onec_runtime/worker_breakpoints.py +1431 -0
  82. onec_runtime/worker_epf.py +313 -0
  83. onec_runtime/worker_epf_template.py +53 -0
  84. onec_runtime/worker_stage_protocol.py +516 -0
  85. onec_runtime/worker_universe.py +3772 -0
  86. onec_runtime_build/__init__.py +1 -0
  87. onec_runtime_build/parser_target_development.py +116 -0
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from onec_runtime.bsl.lexer import KEYWORDS
6
+ from onec_runtime.experiment import bsl_string_literal
7
+
8
+
9
+ def _validate_capture_variable(name: str) -> None:
10
+ valid = bool(name) and (name[0].isalpha() or name[0] == "_")
11
+ valid = valid and all(character.isalnum() or character == "_" for character in name)
12
+ if not valid or name.upper() in KEYWORDS:
13
+ raise ValueError(f"Unsafe capture variable name: {name!r}")
14
+
15
+
16
+ def build_capture_structure_expression(variable_names: Iterable[str]) -> str:
17
+ names: list[str] = []
18
+ seen: set[str] = set()
19
+ for name in variable_names:
20
+ _validate_capture_variable(name)
21
+ folded = name.casefold()
22
+ if folded in seen:
23
+ continue
24
+ seen.add(folded)
25
+ names.append(name)
26
+ if not names:
27
+ raise ValueError("At least one capture variable is required")
28
+ fields = ",".join(names)
29
+ values = ", ".join(names)
30
+ return f'Новый Структура("{fields}", {values})'
31
+
32
+
33
+ def build_extension_call(variable_names: Iterable[str], instruction: str) -> str:
34
+ structure = build_capture_structure_expression(variable_names)
35
+ code = bsl_string_literal(instruction)
36
+ return (
37
+ "RuntimeKernelServer.ВыполнитьВКонтекстеОтладки("
38
+ f"{structure}, {code})"
39
+ )
40
+
41
+
42
+ def build_lowered_extension_call(
43
+ variable_names: Iterable[str], instruction: str
44
+ ) -> str:
45
+ structure = build_capture_structure_expression(variable_names)
46
+ code = bsl_string_literal(instruction)
47
+ return (
48
+ "RuntimeKernelServer.ВыполнитьПониженныйКодВКонтекстеОтладки("
49
+ f"{structure}, {code})"
50
+ )
51
+
52
+
53
+ def build_capture_begin_call(variable_names: Iterable[str]) -> str:
54
+ structure = build_capture_structure_expression(variable_names)
55
+ return f"RuntimeKernelServer.НачатьКонтекстОтладки({structure})"
56
+
57
+
58
+ def build_current_capture_call(instruction: str) -> str:
59
+ code = bsl_string_literal(instruction)
60
+ return (
61
+ "RuntimeKernelServer.ВыполнитьКодТекущегоКонтекстаОтладки("
62
+ f"{code})"
63
+ )
64
+
65
+
66
+ def build_capture_root_expression(name: str) -> str:
67
+ _validate_capture_variable(name)
68
+ return (
69
+ "RuntimeKernelServer.ПолучитьЗначениеКонтекстаОтладки("
70
+ f'"{name}")'
71
+ )
72
+
73
+
74
+ def build_capture_end_call() -> str:
75
+ return "RuntimeKernelServer.ЗавершитьКонтекстОтладки()"
76
+
77
+
78
+ def build_capture_transfer_call(variable_names: Iterable[str]) -> str:
79
+ structure = build_capture_structure_expression(variable_names)
80
+ return f"ПоместитьВоВременноеХранилище({structure})"
81
+
82
+
83
+ def build_live_capture_begin_call(address: str) -> str:
84
+ return (
85
+ "RuntimeKernelServer.НачатьКонтекстОтладкиВКонтексте(Контекст, "
86
+ + bsl_string_literal(address)
87
+ + ")"
88
+ )
89
+
90
+
91
+ def build_live_current_capture_call(instruction: str) -> str:
92
+ return (
93
+ "RuntimeKernelServer.ВыполнитьКодВКонтекстеОтладки(Контекст, "
94
+ + bsl_string_literal(instruction)
95
+ + ")"
96
+ )
97
+
98
+
99
+ def build_live_capture_root_transfer_call(name: str) -> str:
100
+ _validate_capture_variable(name)
101
+ return (
102
+ "RuntimeKernelServer.ПоместитьЗначениеКонтекстаОтладки(Контекст, "
103
+ + bsl_string_literal(name)
104
+ + ")"
105
+ )
106
+
107
+
108
+ def build_temporary_storage_value_expression(address: str) -> str:
109
+ return "ПолучитьИзВременногоХранилища(" + bsl_string_literal(address) + ")"
110
+
111
+
112
+ def build_live_capture_end_call() -> str:
113
+ return "RuntimeKernelServer.ЗавершитьКонтекстОтладкиВКонтексте(Контекст)"
@@ -0,0 +1,453 @@
1
+ """Target-neutral symbolic capture resolution for exported 1C source."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Mapping, Sequence
7
+ from dataclasses import dataclass
8
+ from hashlib import sha256
9
+ from pathlib import Path
10
+ from typing import Protocol
11
+ from uuid import UUID
12
+ from xml.etree import ElementTree
13
+
14
+ from onec_runtime.errors import ProtocolError
15
+ from onec_runtime.kernel import COMMON_MODULE_PROPERTY_ID
16
+ from onec_runtime.rdbg.models import ModuleLocation
17
+
18
+ MAX_SOURCE_EXCERPT = 4_096
19
+ _DECLARATION = re.compile(
20
+ r"^\s*(Процедура|Функция)\s+([\w\u0400-\u04ff]+)\s*\(",
21
+ re.IGNORECASE,
22
+ )
23
+
24
+
25
+ def _identifier(value: object, *, name: str) -> str:
26
+ if not isinstance(value, str) or not value or len(value) > 256:
27
+ raise ValueError(f"{name} must be a bounded identifier")
28
+ if not value.isidentifier():
29
+ raise ValueError(f"{name} must be an identifier without expressions")
30
+ return value
31
+
32
+
33
+ def _qualified_identifier(value: object, *, name: str) -> str:
34
+ if not isinstance(value, str) or len(value) > 512:
35
+ raise ValueError(f"{name} must be bounded")
36
+ parts = value.split(".")
37
+ if not parts:
38
+ raise ValueError(f"{name} must be a qualified identifier")
39
+ for part in parts:
40
+ _identifier(part, name=name)
41
+ return value
42
+
43
+
44
+ def _positive(value: object, *, name: str) -> int:
45
+ if type(value) is not int or value <= 0:
46
+ raise ValueError(f"{name} must be positive")
47
+ return value
48
+
49
+
50
+ def _sha256(value: object, *, name: str) -> str:
51
+ if (
52
+ not isinstance(value, str)
53
+ or len(value) != 64
54
+ or any(character not in "0123456789abcdef" for character in value)
55
+ ):
56
+ raise ValueError(f"{name} must be a lowercase SHA-256 hex digest")
57
+ return value
58
+
59
+
60
+ def _mapping(value: object, *, name: str, allowed: set[str]) -> Mapping[str, object]:
61
+ if not isinstance(value, Mapping) or set(value) - allowed:
62
+ raise ValueError(f"{name} has unsupported fields")
63
+ return value
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class CaptureSourceConfig:
68
+ project: str
69
+ source_root: Path | str
70
+
71
+ def __post_init__(self) -> None:
72
+ object.__setattr__(self, "source_root", Path(self.source_root).resolve())
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class CapturePointRequest:
77
+ name: str
78
+ project: str = ""
79
+ module: str = ""
80
+ procedure: str = ""
81
+ line: int | None = None
82
+ source_fragment: str | None = None
83
+
84
+ def __post_init__(self) -> None:
85
+ _identifier(self.name, name="capture point name")
86
+ has_location = any(
87
+ value not in {"", None}
88
+ for value in (self.project, self.module, self.procedure, self.line)
89
+ )
90
+ if has_location:
91
+ _identifier(self.project, name="project")
92
+ _qualified_identifier(self.module, name="module")
93
+ _identifier(self.procedure, name="procedure")
94
+ _positive(self.line, name="line")
95
+ if self.source_fragment is not None and (
96
+ not isinstance(self.source_fragment, str)
97
+ or not self.source_fragment.strip()
98
+ or len(self.source_fragment) > MAX_SOURCE_EXCERPT
99
+ ):
100
+ raise ValueError("source_fragment must be a bounded non-empty string")
101
+ if not has_location and self.source_fragment is None:
102
+ raise ValueError(
103
+ "capture point requires a source location or stable fragment"
104
+ )
105
+
106
+ @classmethod
107
+ def from_wire(cls, value: object) -> CapturePointRequest:
108
+ raw = _mapping(
109
+ value,
110
+ name="capture point request",
111
+ allowed={
112
+ "name",
113
+ "project",
114
+ "module",
115
+ "procedure",
116
+ "line",
117
+ "source_fragment",
118
+ },
119
+ )
120
+ return cls(**dict(raw)) # type: ignore[arg-type]
121
+
122
+
123
+ @dataclass(frozen=True, slots=True)
124
+ class ResolvedCapturePoint:
125
+ name: str
126
+ project: str
127
+ module: str
128
+ procedure: str
129
+ line: int
130
+ source_revision: int
131
+ source_sha256: str
132
+ executable_line: int
133
+ excerpt: str
134
+
135
+ def __post_init__(self) -> None:
136
+ CapturePointRequest(
137
+ name=self.name,
138
+ project=self.project,
139
+ module=self.module,
140
+ procedure=self.procedure,
141
+ line=self.line,
142
+ )
143
+ _positive(self.source_revision, name="source_revision")
144
+ _sha256(self.source_sha256, name="source_sha256")
145
+ _positive(self.executable_line, name="executable_line")
146
+ if (
147
+ not isinstance(self.excerpt, str)
148
+ or not self.excerpt.strip()
149
+ or len(self.excerpt) > MAX_SOURCE_EXCERPT
150
+ ):
151
+ raise ValueError("excerpt must be a bounded non-empty string")
152
+
153
+ @classmethod
154
+ def from_wire(cls, value: object) -> ResolvedCapturePoint:
155
+ raw = _mapping(
156
+ value,
157
+ name="resolved capture point",
158
+ allowed={
159
+ "name",
160
+ "project",
161
+ "module",
162
+ "procedure",
163
+ "line",
164
+ "source_revision",
165
+ "source_sha256",
166
+ "executable_line",
167
+ "excerpt",
168
+ },
169
+ )
170
+ return cls(**dict(raw)) # type: ignore[arg-type]
171
+
172
+
173
+ @dataclass(frozen=True, slots=True)
174
+ class CaptureBinding:
175
+ point: ResolvedCapturePoint
176
+ location: ModuleLocation
177
+
178
+
179
+ class CapturePointResolver(Protocol):
180
+ def resolve(
181
+ self, points: Sequence[CapturePointRequest]
182
+ ) -> tuple[CaptureBinding, ...]: ...
183
+
184
+ def verify(self, bindings: Sequence[CaptureBinding]) -> None: ...
185
+
186
+
187
+ @dataclass(frozen=True, slots=True)
188
+ class _ModuleSource:
189
+ name: str
190
+ path: Path
191
+ object_id: UUID
192
+ content: bytes
193
+ text: str
194
+
195
+ @property
196
+ def digest(self) -> str:
197
+ return sha256(self.content).hexdigest()
198
+
199
+
200
+ class CommonModuleCaptureResolver:
201
+ """Resolve stable source coordinates in an exported 1C source tree."""
202
+
203
+ def __init__(self, project: str, source_root: Path) -> None:
204
+ if not isinstance(project, str) or not project.isidentifier():
205
+ raise ValueError("project must be an identifier")
206
+ self.project = project
207
+ self.source_root = Path(source_root).resolve()
208
+ self.extension_name = self._designer_extension_name()
209
+
210
+ def resolve(
211
+ self, points: Sequence[CapturePointRequest]
212
+ ) -> tuple[CaptureBinding, ...]:
213
+ result: list[CaptureBinding] = []
214
+ for request in points:
215
+ if (
216
+ request.project
217
+ and request.project.casefold() != self.project.casefold()
218
+ ):
219
+ raise ValueError(
220
+ "capture point project does not match resolver project"
221
+ )
222
+ if request.module:
223
+ source = self._load_module(request.module)
224
+ line, procedure = self._resolve_in_module(source, request)
225
+ else:
226
+ source, line, procedure = self._resolve_fragment_globally(request)
227
+ excerpt = source.text.splitlines()[line - 1].strip()
228
+ resolved = ResolvedCapturePoint(
229
+ name=request.name,
230
+ project=self.project,
231
+ module=source.name,
232
+ procedure=procedure,
233
+ line=line,
234
+ source_revision=1,
235
+ source_sha256=source.digest,
236
+ executable_line=line,
237
+ excerpt=excerpt,
238
+ )
239
+ result.append(
240
+ CaptureBinding(
241
+ resolved,
242
+ ModuleLocation(
243
+ module_type=(
244
+ "ExtensionModule"
245
+ if self.extension_name
246
+ else "ConfigModule"
247
+ ),
248
+ url="",
249
+ object_id=source.object_id,
250
+ property_id=UUID(COMMON_MODULE_PROPERTY_ID),
251
+ line=line,
252
+ extension_name=self.extension_name,
253
+ ),
254
+ )
255
+ )
256
+ return tuple(result)
257
+
258
+ def resolve_module_line(self, module_name: str, line: int) -> ModuleLocation:
259
+ """Resolve a common-module source line to its debugger location."""
260
+ if type(line) is not int or line < 1:
261
+ raise ValueError("capture point line must be positive")
262
+ source = self._load_module(module_name)
263
+ lines = source.text.splitlines()
264
+ if line > len(lines):
265
+ raise ValueError("capture point line is outside module source")
266
+ excerpt = lines[line - 1].strip()
267
+ if (
268
+ not excerpt
269
+ or excerpt.startswith("//")
270
+ or _DECLARATION.match(excerpt)
271
+ or excerpt.casefold() in {"конецпроцедуры", "конецфункции"}
272
+ ):
273
+ raise ValueError("capture point line is not executable source")
274
+ return ModuleLocation(
275
+ module_type="ExtensionModule" if self.extension_name else "ConfigModule",
276
+ url="",
277
+ object_id=source.object_id,
278
+ property_id=UUID(COMMON_MODULE_PROPERTY_ID),
279
+ line=line,
280
+ extension_name=self.extension_name,
281
+ )
282
+
283
+ def verify(self, bindings: Sequence[CaptureBinding]) -> None:
284
+ for binding in bindings:
285
+ source = self._load_module(binding.point.module)
286
+ if source.digest != binding.point.source_sha256:
287
+ raise ProtocolError("capture source changed after resolution")
288
+
289
+ def _resolve_in_module(
290
+ self,
291
+ source: _ModuleSource,
292
+ request: CapturePointRequest,
293
+ ) -> tuple[int, str]:
294
+ regions = self._procedure_regions(source.text)
295
+ expected = request.procedure.casefold()
296
+ matching = [region for name, *region in regions if name.casefold() == expected]
297
+ if len(matching) != 1:
298
+ raise ValueError("capture point procedure was not found exactly once")
299
+ start, end = matching[0]
300
+ if request.source_fragment is not None:
301
+ matches = self._fragment_lines(
302
+ source.text, request.source_fragment, start, end
303
+ )
304
+ if len(matches) != 1:
305
+ raise ValueError("capture source fragment must occur exactly one time")
306
+ line = matches[0]
307
+ else:
308
+ assert request.line is not None
309
+ line = request.line
310
+ if not start <= line <= end:
311
+ raise ValueError("capture point line is outside the named procedure")
312
+ excerpt = source.text.splitlines()[line - 1].strip()
313
+ if not excerpt or excerpt.startswith("//"):
314
+ raise ValueError("capture point line is not executable source")
315
+ return line, request.procedure
316
+
317
+ def _resolve_fragment_globally(
318
+ self,
319
+ request: CapturePointRequest,
320
+ ) -> tuple[_ModuleSource, int, str]:
321
+ assert request.source_fragment is not None
322
+ matches: list[tuple[_ModuleSource, int, str]] = []
323
+ for module_dir in self._common_modules().iterdir():
324
+ if not module_dir.is_dir():
325
+ continue
326
+ try:
327
+ source = self._load_module(module_dir.name)
328
+ except ProtocolError:
329
+ continue
330
+ for procedure, start, end in self._procedure_regions(source.text):
331
+ for line in self._fragment_lines(
332
+ source.text, request.source_fragment, start, end
333
+ ):
334
+ matches.append((source, line, procedure))
335
+ if len(matches) != 1:
336
+ raise ValueError("capture source fragment must occur exactly one time")
337
+ return matches[0]
338
+
339
+ @staticmethod
340
+ def _fragment_lines(text: str, fragment: str, start: int, end: int) -> list[int]:
341
+ return [
342
+ number
343
+ for number, line in enumerate(text.splitlines(), start=1)
344
+ if start <= number <= end and fragment.strip() in line
345
+ ]
346
+
347
+ @staticmethod
348
+ def _procedure_regions(text: str) -> tuple[tuple[str, int, int], ...]:
349
+ lines = text.splitlines()
350
+ regions: list[tuple[str, int, int]] = []
351
+ active: tuple[str, int, str] | None = None
352
+ for number, line in enumerate(lines, start=1):
353
+ match = _DECLARATION.match(line)
354
+ if match is not None:
355
+ if active is not None:
356
+ raise ProtocolError("nested common-module procedure declaration")
357
+ kind = match.group(1).casefold()
358
+ terminator = "конецпроцедуры" if kind == "процедура" else "конецфункции"
359
+ active = (match.group(2), number, terminator)
360
+ continue
361
+ if active is not None and line.strip().casefold() == active[2]:
362
+ regions.append((active[0], active[1], number))
363
+ active = None
364
+ if active is not None:
365
+ raise ProtocolError("common-module procedure has no terminator")
366
+ return tuple(regions)
367
+
368
+ def _designer_extension_name(self) -> str:
369
+ configuration_path = self.source_root / "Configuration.xml"
370
+ if not configuration_path.is_file():
371
+ return ""
372
+ try:
373
+ root = ElementTree.parse(configuration_path).getroot()
374
+ configuration = next(
375
+ element
376
+ for element in root.iter()
377
+ if element.tag.rsplit("}", 1)[-1] == "Configuration"
378
+ )
379
+ properties = next(
380
+ element
381
+ for element in configuration
382
+ if element.tag.rsplit("}", 1)[-1] == "Properties"
383
+ )
384
+ values = {
385
+ element.tag.rsplit("}", 1)[-1]: element.text or ""
386
+ for element in properties
387
+ }
388
+ except (OSError, ElementTree.ParseError, StopIteration) as error:
389
+ raise ProtocolError(
390
+ f"configuration source is invalid: {configuration_path}"
391
+ ) from error
392
+ if "ConfigurationExtensionPurpose" not in values:
393
+ return ""
394
+ name = values.get("Name", "")
395
+ try:
396
+ return _identifier(name, name="extension name")
397
+ except ValueError as error:
398
+ raise ProtocolError(
399
+ f"configuration extension name is invalid: {configuration_path}"
400
+ ) from error
401
+
402
+ def _common_modules(self) -> Path:
403
+ path = self.source_root / "CommonModules"
404
+ if not path.is_dir():
405
+ raise ProtocolError("common-module source root is missing")
406
+ return path
407
+
408
+ def _load_module(self, module_name: str) -> _ModuleSource:
409
+ leaf_name = module_name.rsplit(".", 1)[-1]
410
+ module_root = self._common_modules() / leaf_name
411
+ edt_metadata = module_root / f"{leaf_name}.mdo"
412
+ edt_source = module_root / "Module.bsl"
413
+ designer_metadata = self._common_modules() / f"{leaf_name}.xml"
414
+ designer_source = module_root / "Ext" / "Module.bsl"
415
+ if edt_metadata.is_file() and edt_source.is_file():
416
+ metadata_path, source_path = edt_metadata, edt_source
417
+ elif designer_metadata.is_file() and designer_source.is_file():
418
+ metadata_path, source_path = designer_metadata, designer_source
419
+ else:
420
+ raise ProtocolError(f"common module source is missing: {module_name}")
421
+ try:
422
+ root = ElementTree.parse(metadata_path).getroot()
423
+ candidates = [
424
+ element
425
+ for element in root.iter()
426
+ if element.tag.rsplit("}", 1)[-1] == "CommonModule"
427
+ ]
428
+ owner = root if "uuid" in root.attrib else candidates[0]
429
+ object_id = UUID(owner.attrib["uuid"])
430
+ content = source_path.read_bytes()
431
+ text = content.decode("utf-8-sig")
432
+ except (
433
+ OSError,
434
+ UnicodeDecodeError,
435
+ ElementTree.ParseError,
436
+ IndexError,
437
+ KeyError,
438
+ ValueError,
439
+ ) as error:
440
+ raise ProtocolError(
441
+ f"common module source is invalid: {module_name}"
442
+ ) from error
443
+ return _ModuleSource(module_name, source_path, object_id, content, text)
444
+
445
+
446
+ __all__ = [
447
+ "CaptureBinding",
448
+ "CapturePointRequest",
449
+ "CapturePointResolver",
450
+ "CaptureSourceConfig",
451
+ "CommonModuleCaptureResolver",
452
+ "ResolvedCapturePoint",
453
+ ]