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,516 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from base64 import b64encode
5
+ from dataclasses import dataclass
6
+ from hashlib import sha256
7
+ from re import fullmatch
8
+ from typing import Literal
9
+ from urllib.parse import parse_qsl, urlsplit
10
+ from uuid import UUID
11
+
12
+ from onec_runtime.errors import ProtocolError
13
+ from onec_runtime.experiment import bsl_string_literal
14
+
15
+
16
+ MAX_WORKER_STAGE_BATCH_SIZE = 10
17
+ MAX_WORKER_STAGE_OUTCOME_BYTES = 128 * 1024
18
+ MAX_WORKER_STAGE_URL_CODEPOINTS = 4096
19
+ MAX_WORKER_STAGE_DIAGNOSTIC_CODEPOINTS = 4096
20
+ WORKER_STAGE_SCHEMA = "onec-worker-stage-batch-receipt"
21
+ WORKER_STAGE_SCHEMA_VERSION = 2
22
+
23
+
24
+ def _safe_identifier(value: object) -> bool:
25
+ return (
26
+ isinstance(value, str)
27
+ and fullmatch(r"[A-Za-z_\u0400-\u04ff][A-Za-z0-9_\u0400-\u04ff]*", value)
28
+ is not None
29
+ )
30
+
31
+
32
+ def _ascii_registration_name(value: object) -> bool:
33
+ return (
34
+ type(value) is str
35
+ and fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", value) is not None
36
+ )
37
+
38
+
39
+ def _sha256(value: object) -> bool:
40
+ return type(value) is str and fullmatch(r"[0-9a-f]{64}", value) is not None
41
+
42
+
43
+ def _temp_storage_session_id(value: object) -> str | None:
44
+ if (
45
+ type(value) is not str
46
+ or not value
47
+ or len(value) > MAX_WORKER_STAGE_URL_CODEPOINTS
48
+ or any(ord(character) < 32 or ord(character) == 127 for character in value)
49
+ ):
50
+ return None
51
+ split = urlsplit(value)
52
+ if (
53
+ split.scheme
54
+ or split.netloc
55
+ or split.fragment
56
+ or not split.path.startswith("e1cib/tempstorage/")
57
+ or split.path == "e1cib/tempstorage/"
58
+ ):
59
+ return None
60
+ try:
61
+ query = parse_qsl(split.query, keep_blank_values=True, strict_parsing=True)
62
+ except ValueError:
63
+ return None
64
+ seance_ids = [item for name, item in query if name == "seanceId"]
65
+ return seance_ids[0] if len(seance_ids) == 1 and seance_ids[0] else None
66
+
67
+
68
+ def _qualified_temp_storage_url(value: object) -> bool:
69
+ return _temp_storage_session_id(value) is not None
70
+
71
+
72
+ @dataclass(frozen=True, slots=True, repr=False)
73
+ class WorkerStageEntry:
74
+ logical_name: str
75
+ artifact_sha256: str
76
+ registration_name: str
77
+ artifact_bytes: bytes
78
+
79
+ def __post_init__(self) -> None:
80
+ if (
81
+ type(self.logical_name) is not str
82
+ or not _safe_identifier(self.logical_name)
83
+ or not _sha256(self.artifact_sha256)
84
+ or not _ascii_registration_name(self.registration_name)
85
+ or type(self.artifact_bytes) is not bytes
86
+ or not self.artifact_bytes
87
+ ):
88
+ raise ValueError("worker stage entry is invalid")
89
+
90
+ def __repr__(self) -> str:
91
+ return (
92
+ "WorkerStageEntry("
93
+ f"logical_name={self.logical_name!r}, "
94
+ f"artifact_sha256={self.artifact_sha256!r}, payload=<redacted>)"
95
+ )
96
+
97
+
98
+ @dataclass(frozen=True, slots=True)
99
+ class WorkerStageBatch:
100
+ batch_index: int
101
+ entries: tuple[WorkerStageEntry, ...]
102
+
103
+ def __post_init__(self) -> None:
104
+ if (
105
+ type(self.batch_index) is not int
106
+ or self.batch_index < 0
107
+ or type(self.entries) is not tuple
108
+ or not self.entries
109
+ or len(self.entries) > MAX_WORKER_STAGE_BATCH_SIZE
110
+ or not all(type(entry) is WorkerStageEntry for entry in self.entries)
111
+ ):
112
+ raise ValueError("worker stage batch is invalid")
113
+ registrations = tuple(entry.registration_name.casefold() for entry in self.entries)
114
+ if len(set(registrations)) != len(registrations):
115
+ raise ValueError("worker stage batch registrations are not unique")
116
+
117
+
118
+ def build_worker_stage_batches(
119
+ entries: tuple[WorkerStageEntry, ...],
120
+ ) -> tuple[WorkerStageBatch, ...]:
121
+ if type(entries) is not tuple or not all(
122
+ type(entry) is WorkerStageEntry for entry in entries
123
+ ):
124
+ raise ValueError("worker stage entries are invalid")
125
+ registrations = tuple(entry.registration_name.casefold() for entry in entries)
126
+ if len(set(registrations)) != len(registrations):
127
+ raise ValueError("worker stage registrations are not unique")
128
+ return tuple(
129
+ WorkerStageBatch(
130
+ index // MAX_WORKER_STAGE_BATCH_SIZE,
131
+ entries[index : index + MAX_WORKER_STAGE_BATCH_SIZE],
132
+ )
133
+ for index in range(0, len(entries), MAX_WORKER_STAGE_BATCH_SIZE)
134
+ )
135
+
136
+
137
+ def _positive_index(value: object) -> bool:
138
+ return type(value) is int and value > 0
139
+
140
+
141
+ def _batch_count_is_valid(batch: WorkerStageBatch, batch_count: object) -> bool:
142
+ return _positive_index(batch_count) and batch.batch_index < batch_count
143
+
144
+
145
+ def worker_stage_batch_digest(batch: WorkerStageBatch, batch_count: int) -> str:
146
+ if type(batch) is not WorkerStageBatch or not _batch_count_is_valid(
147
+ batch, batch_count
148
+ ):
149
+ raise ValueError("worker stage batch coordinates are invalid")
150
+ identity = {
151
+ "batch_index": batch.batch_index,
152
+ "batch_count": batch_count,
153
+ "entries": [
154
+ {
155
+ "logical_name": entry.logical_name,
156
+ "registration_name": entry.registration_name,
157
+ "artifact_sha256": entry.artifact_sha256,
158
+ }
159
+ for entry in batch.entries
160
+ ],
161
+ }
162
+ payload = json.dumps(
163
+ identity,
164
+ ensure_ascii=False,
165
+ sort_keys=True,
166
+ separators=(",", ":"),
167
+ ).encode("utf-8")
168
+ return sha256(payload).hexdigest()
169
+
170
+
171
+ @dataclass(frozen=True, slots=True, repr=False)
172
+ class WorkerStageRegistrationReceipt:
173
+ registration_name: str
174
+ artifact_sha256: str
175
+ temp_storage_url: str
176
+
177
+ def __post_init__(self) -> None:
178
+ if (
179
+ not _ascii_registration_name(self.registration_name)
180
+ or not _sha256(self.artifact_sha256)
181
+ or not _qualified_temp_storage_url(self.temp_storage_url)
182
+ ):
183
+ raise ValueError("worker stage registration receipt is invalid")
184
+
185
+ def __repr__(self) -> str:
186
+ return (
187
+ "WorkerStageRegistrationReceipt("
188
+ f"registration_name={self.registration_name!r}, "
189
+ f"artifact_sha256={self.artifact_sha256!r}, "
190
+ "temp_storage_url=<redacted>)"
191
+ )
192
+
193
+
194
+ @dataclass(frozen=True, slots=True, repr=False)
195
+ class WorkerStageFailure:
196
+ item_index: int
197
+ phase: Literal["decode", "upload", "connect", "registration-result"]
198
+ outcome: Literal["known_pre_swap", "registration_outcome_unknown"]
199
+ diagnostic: str
200
+ orphan_url: str | None
201
+
202
+ def __post_init__(self) -> None:
203
+ if (
204
+ type(self.item_index) is not int
205
+ or self.item_index < 0
206
+ or type(self.phase) is not str
207
+ or self.phase not in {"decode", "upload", "connect", "registration-result"}
208
+ or type(self.outcome) is not str
209
+ or self.outcome not in {
210
+ "known_pre_swap",
211
+ "registration_outcome_unknown",
212
+ }
213
+ or type(self.diagnostic) is not str
214
+ or len(self.diagnostic) > MAX_WORKER_STAGE_DIAGNOSTIC_CODEPOINTS
215
+ or (
216
+ self.orphan_url is not None
217
+ and not _qualified_temp_storage_url(self.orphan_url)
218
+ )
219
+ ):
220
+ raise ValueError("worker stage failure is invalid")
221
+ expected_outcome = (
222
+ "known_pre_swap"
223
+ if self.phase in {"decode", "upload"}
224
+ else "registration_outcome_unknown"
225
+ )
226
+ if self.outcome != expected_outcome:
227
+ raise ValueError("worker stage failure outcome is invalid")
228
+ if self.phase in {"connect", "registration-result"} and self.orphan_url is None:
229
+ raise ValueError("worker stage failure orphan URL is missing")
230
+
231
+ def __repr__(self) -> str:
232
+ return (
233
+ "WorkerStageFailure("
234
+ f"item_index={self.item_index!r}, phase={self.phase!r}, "
235
+ f"outcome={self.outcome!r}, diagnostic=<redacted>, "
236
+ "orphan_url=<redacted>)"
237
+ )
238
+
239
+
240
+ @dataclass(frozen=True, slots=True, repr=False)
241
+ class WorkerStageBatchOutcome:
242
+ transaction_id: UUID
243
+ batch_index: int
244
+ batch_count: int
245
+ batch_digest: str
246
+ status: Literal["succeeded", "failed"]
247
+ connected: tuple[WorkerStageRegistrationReceipt, ...]
248
+ failure: WorkerStageFailure | None
249
+
250
+ def __post_init__(self) -> None:
251
+ if (
252
+ type(self.transaction_id) is not UUID
253
+ or type(self.batch_index) is not int
254
+ or self.batch_index < 0
255
+ or not _positive_index(self.batch_count)
256
+ or self.batch_index >= self.batch_count
257
+ or not _sha256(self.batch_digest)
258
+ or type(self.status) is not str
259
+ or self.status not in {"succeeded", "failed"}
260
+ or type(self.connected) is not tuple
261
+ or len(self.connected) > MAX_WORKER_STAGE_BATCH_SIZE
262
+ or not all(
263
+ type(item) is WorkerStageRegistrationReceipt
264
+ for item in self.connected
265
+ )
266
+ or (
267
+ self.failure is not None
268
+ and type(self.failure) is not WorkerStageFailure
269
+ )
270
+ or (self.status == "succeeded") != (self.failure is None)
271
+ ):
272
+ raise ValueError("worker stage batch outcome is invalid")
273
+
274
+ def __repr__(self) -> str:
275
+ return (
276
+ "WorkerStageBatchOutcome("
277
+ f"transaction_id={self.transaction_id!r}, "
278
+ f"batch_index={self.batch_index!r}, batch_count={self.batch_count!r}, "
279
+ f"batch_digest={self.batch_digest!r}, status={self.status!r}, "
280
+ f"connected={self.connected!r}, failure={self.failure!r})"
281
+ )
282
+
283
+
284
+ def stage_worker_batch_instruction(
285
+ batch: WorkerStageBatch,
286
+ *,
287
+ batch_count: int,
288
+ transaction_id: UUID,
289
+ ) -> str:
290
+ """Upload and connect a sealed batch and return one bounded JSON outcome."""
291
+ if (
292
+ type(batch) is not WorkerStageBatch
293
+ or not _batch_count_is_valid(batch, batch_count)
294
+ or type(transaction_id) is not UUID
295
+ ):
296
+ raise ValueError("worker stage batch coordinates are invalid")
297
+
298
+ digest = worker_stage_batch_digest(batch, batch_count)
299
+ lines = [
300
+ "ПодключенныеWorker = Новый Массив;",
301
+ "ОтказWorker = Неопределено;",
302
+ ]
303
+ for item_index, entry in enumerate(batch.entries):
304
+ suffix = str(item_index)
305
+ encoded = bsl_string_literal(b64encode(entry.artifact_bytes).decode("ascii"))
306
+ registration = bsl_string_literal(entry.registration_name)
307
+ artifact_digest = bsl_string_literal(entry.artifact_sha256)
308
+ lines.extend(
309
+ (
310
+ "Если ОтказWorker = Неопределено Тогда",
311
+ f' ФазаWorker{suffix} = "decode";',
312
+ f" АдресАртефактаWorker{suffix} = Неопределено;",
313
+ " Попытка",
314
+ f" ДанныеАртефактаWorker{suffix} = Base64Значение({encoded});",
315
+ f' ФазаWorker{suffix} = "upload";',
316
+ f" АдресАртефактаWorker{suffix} = Строка("
317
+ f"ПоместитьВоВременноеХранилище(ДанныеАртефактаWorker{suffix}));",
318
+ f' ФазаWorker{suffix} = "connect";',
319
+ f" ИмяАртефактаWorker{suffix} = ВнешниеОбработки.Подключить("
320
+ f"АдресАртефактаWorker{suffix}, {registration}, Ложь);",
321
+ f" Если ИмяАртефактаWorker{suffix} <> {registration} Тогда",
322
+ f' ФазаWorker{suffix} = "registration-result";',
323
+ f" ОтказWorker = Новый Структура("
324
+ '"item_index,phase,outcome,diagnostic,orphan_url", '
325
+ f'{item_index}, ФазаWorker{suffix}, "registration_outcome_unknown", '
326
+ '"registration identity mismatch", '
327
+ f"АдресАртефактаWorker{suffix});",
328
+ " Иначе",
329
+ " ПодключениеWorker = Новый Структура("
330
+ '"registration_name,artifact_sha256,temp_storage_url", '
331
+ f"{registration}, {artifact_digest}, АдресАртефактаWorker{suffix});",
332
+ " ПодключенныеWorker.Добавить(ПодключениеWorker);",
333
+ " КонецЕсли;",
334
+ " Исключение",
335
+ f" ИсходWorker{suffix} = ?(ФазаWorker{suffix} = \"decode\" "
336
+ f"ИЛИ ФазаWorker{suffix} = \"upload\", \"known_pre_swap\", "
337
+ '"registration_outcome_unknown");',
338
+ f" СиротскийАдресWorker{suffix} = ?("
339
+ f"АдресАртефактаWorker{suffix} = Неопределено, Ложь, "
340
+ f"АдресАртефактаWorker{suffix});",
341
+ " ОтказWorker = Новый Структура("
342
+ '"item_index,phase,outcome,diagnostic,orphan_url", '
343
+ f"{item_index}, ФазаWorker{suffix}, ИсходWorker{suffix}, "
344
+ "Лев(ОписаниеОшибки(), 4096), "
345
+ f"СиротскийАдресWorker{suffix});",
346
+ " КонецПопытки;",
347
+ "КонецЕсли;",
348
+ )
349
+ )
350
+
351
+ lines.extend(
352
+ (
353
+ 'СтатусWorker = ?(ОтказWorker = Неопределено, "succeeded", "failed");',
354
+ "ДокументWorker = Новый Структура("
355
+ '"schema,schema_version,transaction_id,batch_index,batch_count,'
356
+ 'batch_digest,status,connected,failure", '
357
+ f'{bsl_string_literal(WORKER_STAGE_SCHEMA)}, {WORKER_STAGE_SCHEMA_VERSION}, '
358
+ f'{bsl_string_literal(str(transaction_id))}, {batch.batch_index}, '
359
+ f'{batch_count}, {bsl_string_literal(digest)}, СтатусWorker, '
360
+ "ПодключенныеWorker, ?(ОтказWorker = Неопределено, Ложь, ОтказWorker));",
361
+ "ЗаписьWorker = Новый ЗаписьJSON;",
362
+ "ЗаписьWorker.УстановитьСтроку("
363
+ "Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Нет));",
364
+ "ЗаписатьJSON(ЗаписьWorker, ДокументWorker);",
365
+ "Результат = ЗаписьWorker.Закрыть();",
366
+ )
367
+ )
368
+ return "\n".join(lines)
369
+
370
+
371
+ def _unique_object(pairs: list[tuple[str, object]]) -> dict[str, object]:
372
+ result: dict[str, object] = {}
373
+ for key, value in pairs:
374
+ if key in result:
375
+ raise ProtocolError("Worker stage outcome is invalid")
376
+ result[key] = value
377
+ return result
378
+
379
+
380
+ def _invalid_constant(_value: str) -> object:
381
+ raise ProtocolError("Worker stage outcome is invalid")
382
+
383
+
384
+ def _exact_keys(value: object, expected: set[str]) -> dict[str, object]:
385
+ if type(value) is not dict or set(value) != expected:
386
+ raise ProtocolError("Worker stage outcome is invalid")
387
+ return value
388
+
389
+
390
+ def parse_worker_stage_batch_outcome(
391
+ payload: str,
392
+ batch: WorkerStageBatch,
393
+ *,
394
+ batch_count: int,
395
+ transaction_id: UUID,
396
+ ) -> WorkerStageBatchOutcome:
397
+ """Parse only the current JSON outcome correlated to one sealed batch."""
398
+ if (
399
+ type(batch) is not WorkerStageBatch
400
+ or not _batch_count_is_valid(batch, batch_count)
401
+ or type(transaction_id) is not UUID
402
+ ):
403
+ raise ValueError("worker stage batch coordinates are invalid")
404
+ if type(payload) is not str:
405
+ raise ProtocolError("Worker stage outcome is invalid")
406
+ try:
407
+ encoded = payload.encode("utf-8")
408
+ except UnicodeEncodeError as error:
409
+ raise ProtocolError("Worker stage outcome is invalid") from error
410
+ if len(encoded) > MAX_WORKER_STAGE_OUTCOME_BYTES:
411
+ raise ProtocolError("Worker stage outcome is invalid")
412
+ try:
413
+ document = json.loads(
414
+ payload,
415
+ object_pairs_hook=_unique_object,
416
+ parse_constant=_invalid_constant,
417
+ )
418
+ except (json.JSONDecodeError, ProtocolError, RecursionError) as error:
419
+ raise ProtocolError("Worker stage outcome is invalid") from error
420
+ document = _exact_keys(
421
+ document,
422
+ {
423
+ "schema",
424
+ "schema_version",
425
+ "transaction_id",
426
+ "batch_index",
427
+ "batch_count",
428
+ "batch_digest",
429
+ "status",
430
+ "connected",
431
+ "failure",
432
+ },
433
+ )
434
+ digest = worker_stage_batch_digest(batch, batch_count)
435
+ if (
436
+ document["schema"] != WORKER_STAGE_SCHEMA
437
+ or type(document["schema_version"]) is not int
438
+ or document["schema_version"] != WORKER_STAGE_SCHEMA_VERSION
439
+ or document["transaction_id"] != str(transaction_id)
440
+ or type(document["batch_index"]) is not int
441
+ or document["batch_index"] != batch.batch_index
442
+ or type(document["batch_count"]) is not int
443
+ or document["batch_count"] != batch_count
444
+ or document["batch_digest"] != digest
445
+ or document["status"] not in {"succeeded", "failed"}
446
+ or type(document["connected"]) is not list
447
+ ):
448
+ raise ProtocolError("Worker stage outcome is invalid")
449
+
450
+ connected: list[WorkerStageRegistrationReceipt] = []
451
+ for index, raw in enumerate(document["connected"]):
452
+ if index >= len(batch.entries):
453
+ raise ProtocolError("Worker stage outcome is invalid")
454
+ row = _exact_keys(
455
+ raw,
456
+ {"registration_name", "artifact_sha256", "temp_storage_url"},
457
+ )
458
+ entry = batch.entries[index]
459
+ if (
460
+ row["registration_name"] != entry.registration_name
461
+ or row["artifact_sha256"] != entry.artifact_sha256
462
+ ):
463
+ raise ProtocolError("Worker stage outcome is invalid")
464
+ try:
465
+ connected.append(
466
+ WorkerStageRegistrationReceipt(
467
+ entry.registration_name,
468
+ entry.artifact_sha256,
469
+ row["temp_storage_url"], # type: ignore[arg-type]
470
+ )
471
+ )
472
+ except ValueError as error:
473
+ raise ProtocolError("Worker stage outcome is invalid") from error
474
+ if len({item.temp_storage_url for item in connected}) != len(connected):
475
+ raise ProtocolError("Worker stage outcome is invalid")
476
+
477
+ status = document["status"]
478
+ failure: WorkerStageFailure | None
479
+ if status == "succeeded":
480
+ if document["failure"] is not False or len(connected) != len(batch.entries):
481
+ raise ProtocolError("Worker stage outcome is invalid")
482
+ failure = None
483
+ else:
484
+ raw_failure = _exact_keys(
485
+ document["failure"],
486
+ {"item_index", "phase", "outcome", "diagnostic", "orphan_url"},
487
+ )
488
+ if (
489
+ type(raw_failure["item_index"]) is not int
490
+ or raw_failure["item_index"] != len(connected)
491
+ or raw_failure["item_index"] >= len(batch.entries)
492
+ ):
493
+ raise ProtocolError("Worker stage outcome is invalid")
494
+ try:
495
+ orphan_url = raw_failure["orphan_url"]
496
+ if orphan_url is False:
497
+ orphan_url = None
498
+ failure = WorkerStageFailure(
499
+ raw_failure["item_index"],
500
+ raw_failure["phase"], # type: ignore[arg-type]
501
+ raw_failure["outcome"], # type: ignore[arg-type]
502
+ raw_failure["diagnostic"], # type: ignore[arg-type]
503
+ orphan_url, # type: ignore[arg-type]
504
+ )
505
+ except ValueError as error:
506
+ raise ProtocolError("Worker stage outcome is invalid") from error
507
+
508
+ return WorkerStageBatchOutcome(
509
+ transaction_id,
510
+ batch.batch_index,
511
+ batch_count,
512
+ digest,
513
+ status, # type: ignore[arg-type]
514
+ tuple(connected),
515
+ failure,
516
+ )