pycforge 0.15.2__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.
- pycforge/__init__.py +8 -0
- pycforge/__main__.py +2 -0
- pycforge/_version.py +1 -0
- pycforge/converter/__init__.py +0 -0
- pycforge/converter/analysis/__init__.py +5 -0
- pycforge/converter/analysis/functions.py +723 -0
- pycforge/converter/analysis/model.py +158 -0
- pycforge/converter/analysis/planning.py +464 -0
- pycforge/converter/analysis/stage.py +1976 -0
- pycforge/converter/analysis/symbols.py +237 -0
- pycforge/converter/analysis/validation.py +2166 -0
- pycforge/converter/c_output/__init__.py +2 -0
- pycforge/converter/c_output/conformance.py +297 -0
- pycforge/converter/c_output/renderer.py +220 -0
- pycforge/converter/conditional_regions/__init__.py +71 -0
- pycforge/converter/conditional_regions/analysis.py +481 -0
- pycforge/converter/conditional_regions/lowering.py +579 -0
- pycforge/converter/conditional_regions/model.py +243 -0
- pycforge/converter/conditional_regions/validation.py +690 -0
- pycforge/converter/containers/__init__.py +19 -0
- pycforge/converter/containers/analysis.py +542 -0
- pycforge/converter/containers/lowering.py +290 -0
- pycforge/converter/containers/model.py +146 -0
- pycforge/converter/contracts/__init__.py +0 -0
- pycforge/converter/contracts/configuration.py +178 -0
- pycforge/converter/contracts/identifiers.py +68 -0
- pycforge/converter/contracts/versions.py +71 -0
- pycforge/converter/core/__init__.py +0 -0
- pycforge/converter/core/artifact_io.py +87 -0
- pycforge/converter/core/cancellation.py +7 -0
- pycforge/converter/core/canonicalization.py +254 -0
- pycforge/converter/core/context.py +13 -0
- pycforge/converter/core/diagnostics.py +95 -0
- pycforge/converter/core/enums.py +23 -0
- pycforge/converter/core/fingerprint.py +22 -0
- pycforge/converter/core/pipeline.py +8 -0
- pycforge/converter/core/progress.py +25 -0
- pycforge/converter/core/request.py +50 -0
- pycforge/converter/core/resource_policy.py +31 -0
- pycforge/converter/core/result.py +23 -0
- pycforge/converter/core/serialization.py +36 -0
- pycforge/converter/core/stage.py +10 -0
- pycforge/converter/core/stage_artifact.py +80 -0
- pycforge/converter/core/stage_outcome.py +15 -0
- pycforge/converter/decision_trace/__init__.py +0 -0
- pycforge/converter/decision_trace/recorder.py +52 -0
- pycforge/converter/facade.py +459 -0
- pycforge/converter/frontend/__init__.py +4 -0
- pycforge/converter/frontend/normalizer.py +59 -0
- pycforge/converter/frontend/parser.py +20 -0
- pycforge/converter/frontend/source_document.py +93 -0
- pycforge/converter/frontend/stages.py +518 -0
- pycforge/converter/frontend/validation.py +76 -0
- pycforge/converter/io/__init__.py +0 -0
- pycforge/converter/io/atomic_writer.py +33 -0
- pycforge/converter/ir/__init__.py +1 -0
- pycforge/converter/ir/c_ir/__init__.py +4 -0
- pycforge/converter/ir/c_ir/builder.py +26 -0
- pycforge/converter/ir/c_ir/model.py +318 -0
- pycforge/converter/ir/c_ir/serialization.py +88 -0
- pycforge/converter/ir/c_ir/validation.py +834 -0
- pycforge/converter/ir/python_ir/__init__.py +17 -0
- pycforge/converter/ir/python_ir/nodes.py +86 -0
- pycforge/converter/keyword_calls/__init__.py +68 -0
- pycforge/converter/keyword_calls/analysis.py +458 -0
- pycforge/converter/keyword_calls/lowering.py +271 -0
- pycforge/converter/keyword_calls/model.py +304 -0
- pycforge/converter/keyword_calls/validation.py +802 -0
- pycforge/converter/keyword_only_calls/__init__.py +68 -0
- pycforge/converter/keyword_only_calls/analysis.py +640 -0
- pycforge/converter/keyword_only_calls/lowering.py +376 -0
- pycforge/converter/keyword_only_calls/model.py +328 -0
- pycforge/converter/keyword_only_calls/validation.py +1357 -0
- pycforge/converter/lowering.py +1000 -0
- pycforge/converter/modules/__init__.py +13 -0
- pycforge/converter/modules/analysis.py +1250 -0
- pycforge/converter/modules/lowering.py +200 -0
- pycforge/converter/modules/model.py +73 -0
- pycforge/converter/modules/stage.py +192 -0
- pycforge/converter/numeric_semantics/__init__.py +23 -0
- pycforge/converter/numeric_semantics/analysis.py +420 -0
- pycforge/converter/numeric_semantics/lowering.py +153 -0
- pycforge/converter/numeric_semantics/model.py +123 -0
- pycforge/converter/records/__init__.py +34 -0
- pycforge/converter/records/analysis.py +1394 -0
- pycforge/converter/records/lowering.py +393 -0
- pycforge/converter/records/model.py +319 -0
- pycforge/converter/rules/__init__.py +3 -0
- pycforge/converter/support_templates/__init__.py +49 -0
- pycforge/converter/support_templates/assembly.py +106 -0
- pycforge/converter/support_templates/factories.py +348 -0
- pycforge/converter/support_templates/model.py +159 -0
- pycforge/converter/support_templates/registry.py +388 -0
- pycforge/converter/telemetry/__init__.py +0 -0
- pycforge/converter/telemetry/sink.py +39 -0
- pycforge/ide/__init__.py +19 -0
- pycforge/ide/__main__.py +5 -0
- pycforge/ide/_worker_event_codec.py +276 -0
- pycforge/ide/_worker_protocol_json.py +197 -0
- pycforge/ide/_worker_protocol_types.py +90 -0
- pycforge/ide/_worker_request_codec.py +200 -0
- pycforge/ide/_worker_request_data.py +260 -0
- pycforge/ide/_worker_result_data.py +343 -0
- pycforge/ide/action_contract.py +469 -0
- pycforge/ide/action_validation.py +229 -0
- pycforge/ide/command_palette.py +223 -0
- pycforge/ide/controller.py +904 -0
- pycforge/ide/controller_conversion.py +546 -0
- pycforge/ide/controller_io.py +205 -0
- pycforge/ide/editor.py +951 -0
- pycforge/ide/editor_commands_qt.py +274 -0
- pycforge/ide/editor_lexical.py +43 -0
- pycforge/ide/editor_sidebars.py +236 -0
- pycforge/ide/editor_syntax.py +243 -0
- pycforge/ide/find_replace.py +535 -0
- pycforge/ide/icons.py +91 -0
- pycforge/ide/io_service.py +196 -0
- pycforge/ide/model.py +164 -0
- pycforge/ide/panels.py +903 -0
- pycforge/ide/positions.py +220 -0
- pycforge/ide/process_worker.py +130 -0
- pycforge/ide/qt.py +230 -0
- pycforge/ide/qt_actions.py +418 -0
- pycforge/ide/qt_close.py +169 -0
- pycforge/ide/qt_command_palette.py +297 -0
- pycforge/ide/qt_contract.py +220 -0
- pycforge/ide/qt_documents.py +590 -0
- pycforge/ide/qt_editor_buffers.py +113 -0
- pycforge/ide/qt_editor_surfaces.py +112 -0
- pycforge/ide/qt_menus.py +324 -0
- pycforge/ide/qt_projection.py +500 -0
- pycforge/ide/qt_shell.py +348 -0
- pycforge/ide/qt_shell_interactions.py +320 -0
- pycforge/ide/qt_state.py +393 -0
- pycforge/ide/qt_workspace_features.py +398 -0
- pycforge/ide/qt_workspace_navigation.py +208 -0
- pycforge/ide/qt_workspace_observers.py +173 -0
- pycforge/ide/qt_workspace_panels.py +545 -0
- pycforge/ide/qt_workspace_widgets.py +228 -0
- pycforge/ide/resources/icons/about.svg +1 -0
- pycforge/ide/resources/icons/add-document.svg +1 -0
- pycforge/ide/resources/icons/brand-mark.svg +1 -0
- pycforge/ide/resources/icons/bundle-search.svg +1 -0
- pycforge/ide/resources/icons/cancel.svg +1 -0
- pycforge/ide/resources/icons/check.svg +1 -0
- pycforge/ide/resources/icons/chevron-right.svg +1 -0
- pycforge/ide/resources/icons/close.svg +1 -0
- pycforge/ide/resources/icons/collapse-all.svg +1 -0
- pycforge/ide/resources/icons/command-palette.svg +1 -0
- pycforge/ide/resources/icons/convert.svg +1 -0
- pycforge/ide/resources/icons/copy.svg +1 -0
- pycforge/ide/resources/icons/cut.svg +1 -0
- pycforge/ide/resources/icons/decision-trace.svg +1 -0
- pycforge/ide/resources/icons/details.svg +1 -0
- pycforge/ide/resources/icons/diagnostics.svg +1 -0
- pycforge/ide/resources/icons/duplicate-line.svg +1 -0
- pycforge/ide/resources/icons/exit.svg +1 -0
- pycforge/ide/resources/icons/expand-all.svg +1 -0
- pycforge/ide/resources/icons/export.svg +1 -0
- pycforge/ide/resources/icons/find.svg +1 -0
- pycforge/ide/resources/icons/go-to-line.svg +1 -0
- pycforge/ide/resources/icons/go-to-output.svg +1 -0
- pycforge/ide/resources/icons/go-to-source.svg +1 -0
- pycforge/ide/resources/icons/history.svg +1 -0
- pycforge/ide/resources/icons/indent.svg +1 -0
- pycforge/ide/resources/icons/link-c.svg +1 -0
- pycforge/ide/resources/icons/mappings.svg +1 -0
- pycforge/ide/resources/icons/module.svg +1 -0
- pycforge/ide/resources/icons/move-down.svg +1 -0
- pycforge/ide/resources/icons/move-line-down.svg +1 -0
- pycforge/ide/resources/icons/move-line-up.svg +1 -0
- pycforge/ide/resources/icons/move-up.svg +1 -0
- pycforge/ide/resources/icons/next.svg +1 -0
- pycforge/ide/resources/icons/open.svg +1 -0
- pycforge/ide/resources/icons/outdent.svg +1 -0
- pycforge/ide/resources/icons/outline.svg +1 -0
- pycforge/ide/resources/icons/paste.svg +1 -0
- pycforge/ide/resources/icons/previous.svg +1 -0
- pycforge/ide/resources/icons/primary-module.svg +1 -0
- pycforge/ide/resources/icons/redo.svg +1 -0
- pycforge/ide/resources/icons/remove-document.svg +1 -0
- pycforge/ide/resources/icons/replace.svg +1 -0
- pycforge/ide/resources/icons/save-as.svg +1 -0
- pycforge/ide/resources/icons/save.svg +1 -0
- pycforge/ide/resources/icons/select-all.svg +1 -0
- pycforge/ide/resources/icons/settings.svg +1 -0
- pycforge/ide/resources/icons/show-c.svg +1 -0
- pycforge/ide/resources/icons/split-view.svg +1 -0
- pycforge/ide/resources/icons/summary.svg +1 -0
- pycforge/ide/resources/icons/telemetry.svg +1 -0
- pycforge/ide/resources/icons/toggle-comment.svg +1 -0
- pycforge/ide/resources/icons/toggle-fold.svg +1 -0
- pycforge/ide/resources/icons/undo.svg +1 -0
- pycforge/ide/resources/icons/whitespace.svg +1 -0
- pycforge/ide/revisions.py +400 -0
- pycforge/ide/search_service.py +280 -0
- pycforge/ide/session_history.py +150 -0
- pycforge/ide/source_editing.py +417 -0
- pycforge/ide/source_structure.py +558 -0
- pycforge/ide/source_structure_async.py +191 -0
- pycforge/ide/supervisor.py +503 -0
- pycforge/ide/theme.py +119 -0
- pycforge/ide/theme_stylesheet.py +598 -0
- pycforge/ide/theme_workspace_stylesheet.py +155 -0
- pycforge/ide/visual_tokens.py +142 -0
- pycforge/ide/worker_protocol.py +80 -0
- pycforge/ide/workspace_search.py +462 -0
- pycforge/ide/workspace_session.py +256 -0
- pycforge/laboratory/__init__.py +6 -0
- pycforge/laboratory/audits.py +2244 -0
- pycforge/laboratory/checkpoint_e.py +2685 -0
- pycforge/laboratory/cli.py +119 -0
- pycforge/laboratory/exit_codes.py +11 -0
- pycforge/laboratory/keyword_audit.py +661 -0
- pycforge/laboratory/keyword_only_audit.py +481 -0
- pycforge-0.15.2.dist-info/METADATA +209 -0
- pycforge-0.15.2.dist-info/RECORD +221 -0
- pycforge-0.15.2.dist-info/WHEEL +5 -0
- pycforge-0.15.2.dist-info/entry_points.txt +5 -0
- pycforge-0.15.2.dist-info/licenses/LICENSE +674 -0
- pycforge-0.15.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Progress, terminal-result, and worker-failure event codec."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pycforge.converter.core.progress import ConversionProgress
|
|
6
|
+
from pycforge.converter.core.result import ConversionResult
|
|
7
|
+
|
|
8
|
+
from ._worker_protocol_json import (
|
|
9
|
+
_contains_surrogate,
|
|
10
|
+
_decode_bounded,
|
|
11
|
+
_encode_bounded,
|
|
12
|
+
_exact_fields,
|
|
13
|
+
_generation,
|
|
14
|
+
_object,
|
|
15
|
+
_receive_frame,
|
|
16
|
+
_send_frame,
|
|
17
|
+
_sha256,
|
|
18
|
+
)
|
|
19
|
+
from ._worker_protocol_types import (
|
|
20
|
+
FAILURE_CLASSIFICATIONS,
|
|
21
|
+
MAX_EVENT_BYTES,
|
|
22
|
+
MAX_FAILURE_MESSAGE_CHARS,
|
|
23
|
+
PROTOCOL_SCHEMA,
|
|
24
|
+
ByteConnection,
|
|
25
|
+
WorkerEvent,
|
|
26
|
+
WorkerProtocolError,
|
|
27
|
+
WorkerRequest,
|
|
28
|
+
)
|
|
29
|
+
from ._worker_request_codec import _validate_worker_request
|
|
30
|
+
from ._worker_result_data import (
|
|
31
|
+
_mappings_from_data,
|
|
32
|
+
_progress_from_data,
|
|
33
|
+
_result_from_data,
|
|
34
|
+
_result_to_data,
|
|
35
|
+
_validate_result_mappings,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_ENVELOPE_FIELDS = frozenset(
|
|
40
|
+
{
|
|
41
|
+
"schema",
|
|
42
|
+
"kind",
|
|
43
|
+
"generation",
|
|
44
|
+
"bundle_fingerprint",
|
|
45
|
+
"transport_fingerprint",
|
|
46
|
+
"progress",
|
|
47
|
+
"result",
|
|
48
|
+
"mappings",
|
|
49
|
+
"failure_classification",
|
|
50
|
+
"message",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def encode_progress(request: WorkerRequest, progress: ConversionProgress) -> bytes:
|
|
56
|
+
_validate_worker_request(request)
|
|
57
|
+
if not isinstance(progress, ConversionProgress):
|
|
58
|
+
raise WorkerProtocolError("progress event must be ConversionProgress")
|
|
59
|
+
return _encode_event(
|
|
60
|
+
request,
|
|
61
|
+
"progress",
|
|
62
|
+
progress={
|
|
63
|
+
"state": progress.state,
|
|
64
|
+
"stage_id": progress.stage_id,
|
|
65
|
+
"completed_stages": progress.completed_stages,
|
|
66
|
+
"total_stages": progress.total_stages,
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def encode_terminal(request: WorkerRequest, result: ConversionResult) -> bytes:
|
|
72
|
+
_validate_worker_request(request)
|
|
73
|
+
if not isinstance(result, ConversionResult):
|
|
74
|
+
raise WorkerProtocolError("terminal event must contain ConversionResult")
|
|
75
|
+
artifact = result.stage_artifact
|
|
76
|
+
mappings: list[object] = []
|
|
77
|
+
if artifact is not None:
|
|
78
|
+
raw_mappings = artifact.payload.get("source_output_mappings", ())
|
|
79
|
+
if not isinstance(raw_mappings, (tuple, list)):
|
|
80
|
+
raise WorkerProtocolError("terminal source/output mappings are malformed")
|
|
81
|
+
mappings = list(raw_mappings)
|
|
82
|
+
return _encode_event(
|
|
83
|
+
request,
|
|
84
|
+
"terminal",
|
|
85
|
+
result=_result_to_data(result),
|
|
86
|
+
mappings=mappings,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def encode_failure(
|
|
91
|
+
request: WorkerRequest | None,
|
|
92
|
+
failure_classification: str,
|
|
93
|
+
message: str,
|
|
94
|
+
) -> bytes:
|
|
95
|
+
if failure_classification not in FAILURE_CLASSIFICATIONS:
|
|
96
|
+
raise WorkerProtocolError("unknown worker failure classification")
|
|
97
|
+
if (
|
|
98
|
+
not isinstance(message, str)
|
|
99
|
+
or not message
|
|
100
|
+
or len(message) > MAX_FAILURE_MESSAGE_CHARS
|
|
101
|
+
or _contains_surrogate(message)
|
|
102
|
+
):
|
|
103
|
+
raise WorkerProtocolError("worker failure message is malformed")
|
|
104
|
+
return _encode_event(
|
|
105
|
+
request,
|
|
106
|
+
"failure",
|
|
107
|
+
failure_classification=failure_classification,
|
|
108
|
+
message=message,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def decode_event(
|
|
113
|
+
frame: bytes,
|
|
114
|
+
*,
|
|
115
|
+
expected: WorkerRequest | None = None,
|
|
116
|
+
expected_generation: int | None = None,
|
|
117
|
+
expected_bundle_fingerprint: str | None = None,
|
|
118
|
+
expected_transport_fingerprint: str | None = None,
|
|
119
|
+
) -> WorkerEvent:
|
|
120
|
+
value = _decode_bounded(frame, MAX_EVENT_BYTES, "event")
|
|
121
|
+
data = _object(value, "event envelope")
|
|
122
|
+
_exact_fields(data, _ENVELOPE_FIELDS, "event envelope")
|
|
123
|
+
if data["schema"] != PROTOCOL_SCHEMA:
|
|
124
|
+
raise WorkerProtocolError("incompatible worker event schema")
|
|
125
|
+
kind = data["kind"]
|
|
126
|
+
if kind not in {"progress", "terminal", "failure"}:
|
|
127
|
+
raise WorkerProtocolError("unknown worker event kind")
|
|
128
|
+
|
|
129
|
+
identities = (
|
|
130
|
+
data["generation"],
|
|
131
|
+
data["bundle_fingerprint"],
|
|
132
|
+
data["transport_fingerprint"],
|
|
133
|
+
)
|
|
134
|
+
if all(item is None for item in identities):
|
|
135
|
+
if kind != "failure":
|
|
136
|
+
raise WorkerProtocolError("non-failure event lacks request identity")
|
|
137
|
+
generation = None
|
|
138
|
+
bundle_fingerprint = None
|
|
139
|
+
transport_fingerprint = None
|
|
140
|
+
elif any(item is None for item in identities):
|
|
141
|
+
raise WorkerProtocolError("worker event has partial request identity")
|
|
142
|
+
else:
|
|
143
|
+
generation = _generation(data["generation"])
|
|
144
|
+
bundle_fingerprint = _sha256(
|
|
145
|
+
data["bundle_fingerprint"], "bundle fingerprint"
|
|
146
|
+
)
|
|
147
|
+
transport_fingerprint = _sha256(
|
|
148
|
+
data["transport_fingerprint"], "transport fingerprint"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if expected is not None:
|
|
152
|
+
_validate_worker_request(expected)
|
|
153
|
+
expected_generation = expected.generation
|
|
154
|
+
expected_bundle_fingerprint = expected.bundle_fingerprint
|
|
155
|
+
expected_transport_fingerprint = expected.transport_fingerprint
|
|
156
|
+
if generation is not None:
|
|
157
|
+
if (
|
|
158
|
+
expected_generation is not None
|
|
159
|
+
and generation != _generation(expected_generation)
|
|
160
|
+
):
|
|
161
|
+
raise WorkerProtocolError("worker event generation mismatch")
|
|
162
|
+
if (
|
|
163
|
+
expected_bundle_fingerprint is not None
|
|
164
|
+
and bundle_fingerprint
|
|
165
|
+
!= _sha256(expected_bundle_fingerprint, "expected bundle fingerprint")
|
|
166
|
+
):
|
|
167
|
+
raise WorkerProtocolError("worker event bundle fingerprint mismatch")
|
|
168
|
+
if (
|
|
169
|
+
expected_transport_fingerprint is not None
|
|
170
|
+
and transport_fingerprint
|
|
171
|
+
!= _sha256(
|
|
172
|
+
expected_transport_fingerprint,
|
|
173
|
+
"expected transport fingerprint",
|
|
174
|
+
)
|
|
175
|
+
):
|
|
176
|
+
raise WorkerProtocolError("worker event transport fingerprint mismatch")
|
|
177
|
+
|
|
178
|
+
progress: ConversionProgress | None = None
|
|
179
|
+
result: ConversionResult | None = None
|
|
180
|
+
mappings: tuple[dict[str, object], ...] = ()
|
|
181
|
+
failure_classification: str | None = None
|
|
182
|
+
message: str | None = None
|
|
183
|
+
|
|
184
|
+
if kind == "progress":
|
|
185
|
+
if any(
|
|
186
|
+
data[key] is not None
|
|
187
|
+
for key in ("result", "failure_classification", "message")
|
|
188
|
+
) or data["mappings"] != []:
|
|
189
|
+
raise WorkerProtocolError("progress event contains terminal fields")
|
|
190
|
+
progress = _progress_from_data(data["progress"])
|
|
191
|
+
elif kind == "terminal":
|
|
192
|
+
if (
|
|
193
|
+
data["progress"] is not None
|
|
194
|
+
or data["failure_classification"] is not None
|
|
195
|
+
or data["message"] is not None
|
|
196
|
+
):
|
|
197
|
+
raise WorkerProtocolError("terminal event contains non-terminal fields")
|
|
198
|
+
result = _result_from_data(data["result"])
|
|
199
|
+
mappings = _mappings_from_data(data["mappings"])
|
|
200
|
+
_validate_result_mappings(result, mappings)
|
|
201
|
+
else:
|
|
202
|
+
if (
|
|
203
|
+
data["progress"] is not None
|
|
204
|
+
or data["result"] is not None
|
|
205
|
+
or data["mappings"] != []
|
|
206
|
+
):
|
|
207
|
+
raise WorkerProtocolError("failure event contains conversion fields")
|
|
208
|
+
failure_classification = data["failure_classification"]
|
|
209
|
+
message = data["message"]
|
|
210
|
+
if failure_classification not in FAILURE_CLASSIFICATIONS:
|
|
211
|
+
raise WorkerProtocolError("unknown worker failure classification")
|
|
212
|
+
if (
|
|
213
|
+
not isinstance(message, str)
|
|
214
|
+
or not message
|
|
215
|
+
or len(message) > MAX_FAILURE_MESSAGE_CHARS
|
|
216
|
+
or _contains_surrogate(message)
|
|
217
|
+
):
|
|
218
|
+
raise WorkerProtocolError("worker failure message is malformed")
|
|
219
|
+
|
|
220
|
+
return WorkerEvent(
|
|
221
|
+
kind,
|
|
222
|
+
generation,
|
|
223
|
+
bundle_fingerprint,
|
|
224
|
+
transport_fingerprint,
|
|
225
|
+
progress,
|
|
226
|
+
result,
|
|
227
|
+
mappings,
|
|
228
|
+
failure_classification,
|
|
229
|
+
message,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def send_event(connection: ByteConnection, frame: bytes) -> None:
|
|
234
|
+
_send_frame(connection, frame, MAX_EVENT_BYTES, "event")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def receive_event(
|
|
238
|
+
connection: ByteConnection,
|
|
239
|
+
*,
|
|
240
|
+
expected: WorkerRequest | None = None,
|
|
241
|
+
) -> WorkerEvent:
|
|
242
|
+
return decode_event(
|
|
243
|
+
_receive_frame(connection, MAX_EVENT_BYTES, "event"),
|
|
244
|
+
expected=expected,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _encode_event(
|
|
249
|
+
request: WorkerRequest | None,
|
|
250
|
+
kind: str,
|
|
251
|
+
*,
|
|
252
|
+
progress: object = None,
|
|
253
|
+
result: object = None,
|
|
254
|
+
mappings: list[object] | None = None,
|
|
255
|
+
failure_classification: str | None = None,
|
|
256
|
+
message: str | None = None,
|
|
257
|
+
) -> bytes:
|
|
258
|
+
if request is not None:
|
|
259
|
+
_validate_worker_request(request)
|
|
260
|
+
data = {
|
|
261
|
+
"schema": PROTOCOL_SCHEMA,
|
|
262
|
+
"kind": kind,
|
|
263
|
+
"generation": None if request is None else request.generation,
|
|
264
|
+
"bundle_fingerprint": (
|
|
265
|
+
None if request is None else request.bundle_fingerprint
|
|
266
|
+
),
|
|
267
|
+
"transport_fingerprint": (
|
|
268
|
+
None if request is None else request.transport_fingerprint
|
|
269
|
+
),
|
|
270
|
+
"progress": progress,
|
|
271
|
+
"result": result,
|
|
272
|
+
"mappings": [] if mappings is None else mappings,
|
|
273
|
+
"failure_classification": failure_classification,
|
|
274
|
+
"message": message,
|
|
275
|
+
}
|
|
276
|
+
return _encode_bounded(data, MAX_EVENT_BYTES, "event")
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Strict canonical-JSON parsing and byte-only connection framing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pycforge.converter.core.fingerprint import canonical_json
|
|
11
|
+
|
|
12
|
+
from ._worker_protocol_types import (
|
|
13
|
+
MAX_JSON_DEPTH,
|
|
14
|
+
ByteConnection,
|
|
15
|
+
WorkerProtocolError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _send_frame(
|
|
23
|
+
connection: ByteConnection,
|
|
24
|
+
frame: bytes,
|
|
25
|
+
maximum: int,
|
|
26
|
+
role: str,
|
|
27
|
+
) -> None:
|
|
28
|
+
if not isinstance(frame, bytes) or len(frame) > maximum:
|
|
29
|
+
raise WorkerProtocolError(f"worker {role} frame exceeds protocol bounds")
|
|
30
|
+
connection.send_bytes(frame)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _receive_frame(
|
|
34
|
+
connection: ByteConnection,
|
|
35
|
+
maximum: int,
|
|
36
|
+
role: str,
|
|
37
|
+
) -> bytes:
|
|
38
|
+
try:
|
|
39
|
+
frame = connection.recv_bytes(maximum)
|
|
40
|
+
except OSError as exc:
|
|
41
|
+
raise WorkerProtocolError(
|
|
42
|
+
f"worker {role} frame exceeds protocol bounds or is unreadable"
|
|
43
|
+
) from exc
|
|
44
|
+
if not isinstance(frame, bytes) or len(frame) > maximum:
|
|
45
|
+
raise WorkerProtocolError(f"worker {role} frame exceeds protocol bounds")
|
|
46
|
+
return frame
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _encode_bounded(value: object, maximum: int, role: str) -> bytes:
|
|
50
|
+
try:
|
|
51
|
+
frame = canonical_json(value)
|
|
52
|
+
except (TypeError, ValueError, UnicodeError) as exc:
|
|
53
|
+
raise WorkerProtocolError(f"worker {role} is not canonical JSON") from exc
|
|
54
|
+
if len(frame) > maximum:
|
|
55
|
+
raise WorkerProtocolError(f"worker {role} frame exceeds protocol bounds")
|
|
56
|
+
return frame
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _decode_bounded(frame: bytes, maximum: int, role: str) -> object:
|
|
60
|
+
if not isinstance(frame, bytes) or len(frame) > maximum:
|
|
61
|
+
raise WorkerProtocolError(f"worker {role} frame exceeds protocol bounds")
|
|
62
|
+
try:
|
|
63
|
+
text = frame.decode("utf-8")
|
|
64
|
+
value = json.loads(
|
|
65
|
+
text,
|
|
66
|
+
object_pairs_hook=_unique_object,
|
|
67
|
+
parse_constant=_reject_constant,
|
|
68
|
+
parse_int=_bounded_json_int,
|
|
69
|
+
parse_float=_finite_json_float,
|
|
70
|
+
)
|
|
71
|
+
_validate_json_tree(value)
|
|
72
|
+
if canonical_json(value) != frame:
|
|
73
|
+
raise WorkerProtocolError(
|
|
74
|
+
f"worker {role} frame is not canonical JSON"
|
|
75
|
+
)
|
|
76
|
+
except WorkerProtocolError:
|
|
77
|
+
raise
|
|
78
|
+
except (UnicodeError, json.JSONDecodeError, TypeError, ValueError) as exc:
|
|
79
|
+
raise WorkerProtocolError(f"worker {role} frame is not valid JSON") from exc
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _unique_object(pairs: list[tuple[str, object]]) -> dict[str, object]:
|
|
84
|
+
result: dict[str, object] = {}
|
|
85
|
+
for key, value in pairs:
|
|
86
|
+
if key in result:
|
|
87
|
+
raise WorkerProtocolError("worker JSON contains duplicate object keys")
|
|
88
|
+
result[key] = value
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _reject_constant(value: str) -> object:
|
|
93
|
+
raise WorkerProtocolError(f"worker JSON contains forbidden constant {value}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _bounded_json_int(value: str) -> int:
|
|
97
|
+
if len(value.lstrip("-")) > 19:
|
|
98
|
+
raise WorkerProtocolError("worker JSON integer exceeds protocol bounds")
|
|
99
|
+
return int(value)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _finite_json_float(value: str) -> float:
|
|
103
|
+
result = float(value)
|
|
104
|
+
if not math.isfinite(result):
|
|
105
|
+
raise WorkerProtocolError("worker JSON number must be finite")
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _validate_json_tree(value: object, depth: int = 0) -> None:
|
|
110
|
+
if depth > MAX_JSON_DEPTH:
|
|
111
|
+
raise WorkerProtocolError("worker JSON nesting exceeds protocol bounds")
|
|
112
|
+
if isinstance(value, str):
|
|
113
|
+
if _contains_surrogate(value):
|
|
114
|
+
raise WorkerProtocolError("worker JSON contains invalid Unicode")
|
|
115
|
+
return
|
|
116
|
+
if value is None or isinstance(value, (bool, int)):
|
|
117
|
+
return
|
|
118
|
+
if isinstance(value, float):
|
|
119
|
+
if not math.isfinite(value):
|
|
120
|
+
raise WorkerProtocolError("worker JSON number must be finite")
|
|
121
|
+
return
|
|
122
|
+
if isinstance(value, list):
|
|
123
|
+
for item in value:
|
|
124
|
+
_validate_json_tree(item, depth + 1)
|
|
125
|
+
return
|
|
126
|
+
if isinstance(value, dict):
|
|
127
|
+
for key, item in value.items():
|
|
128
|
+
if not isinstance(key, str) or _contains_surrogate(key):
|
|
129
|
+
raise WorkerProtocolError("worker JSON object key is invalid")
|
|
130
|
+
_validate_json_tree(item, depth + 1)
|
|
131
|
+
return
|
|
132
|
+
raise WorkerProtocolError("worker JSON contains unsupported value")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _contains_surrogate(value: str) -> bool:
|
|
136
|
+
return any(0xD800 <= ord(character) <= 0xDFFF for character in value)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _object(value: object, role: str) -> dict[str, Any]:
|
|
140
|
+
if not isinstance(value, dict):
|
|
141
|
+
raise WorkerProtocolError(f"{role} must be an object")
|
|
142
|
+
return value
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _optional_object(value: object, role: str) -> dict[str, Any] | None:
|
|
146
|
+
return None if value is None else _object(value, role)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _list(value: object, role: str) -> list[Any]:
|
|
150
|
+
if not isinstance(value, list):
|
|
151
|
+
raise WorkerProtocolError(f"{role} must be an array")
|
|
152
|
+
return value
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _exact_fields(
|
|
156
|
+
value: dict[str, Any],
|
|
157
|
+
expected: set[str] | frozenset[str],
|
|
158
|
+
role: str,
|
|
159
|
+
) -> None:
|
|
160
|
+
if set(value) != set(expected):
|
|
161
|
+
raise WorkerProtocolError(f"{role} fields are incomplete or unknown")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _string(value: object, role: str) -> str:
|
|
165
|
+
if not isinstance(value, str) or _contains_surrogate(value):
|
|
166
|
+
raise WorkerProtocolError(f"{role} must be valid Unicode text")
|
|
167
|
+
return value
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _nonempty_string(value: object, role: str) -> str:
|
|
171
|
+
result = _string(value, role)
|
|
172
|
+
if not result:
|
|
173
|
+
raise WorkerProtocolError(f"{role} must be non-empty")
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _string_list(value: object, role: str) -> tuple[str, ...]:
|
|
178
|
+
return tuple(_string(item, role) for item in _list(value, role))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _nonnegative_integer(value: object, role: str) -> int:
|
|
182
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
183
|
+
raise WorkerProtocolError(f"{role} must be a non-negative integer")
|
|
184
|
+
return value
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _generation(value: object) -> int:
|
|
188
|
+
result = _nonnegative_integer(value, "worker generation")
|
|
189
|
+
if result == 0 or result > 2**63 - 1:
|
|
190
|
+
raise WorkerProtocolError("worker generation is outside protocol bounds")
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _sha256(value: object, role: str) -> str:
|
|
195
|
+
if not isinstance(value, str) or not _SHA256.fullmatch(value):
|
|
196
|
+
raise WorkerProtocolError(f"{role} must be a lowercase SHA-256 value")
|
|
197
|
+
return value
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Shared immutable types and bounds for the worker protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Protocol
|
|
7
|
+
|
|
8
|
+
from pycforge.converter.core.progress import ConversionProgress
|
|
9
|
+
from pycforge.converter.core.request import ConversionRequest, ObservationOptions
|
|
10
|
+
from pycforge.converter.core.result import ConversionResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
PROTOCOL_SCHEMA = "pycforge.worker-protocol/0.1"
|
|
14
|
+
MAX_REQUEST_BYTES = 8 * 1024 * 1024
|
|
15
|
+
MAX_CONTROL_BYTES = 4 * 1024
|
|
16
|
+
MAX_EVENT_BYTES = 128 * 1024 * 1024
|
|
17
|
+
MAX_SOURCE_BYTES = 1_000_000
|
|
18
|
+
MAX_SOURCE_DOCUMENTS = 64
|
|
19
|
+
MAX_JSON_DEPTH = 256
|
|
20
|
+
MAX_DIAGNOSTICS = 1_000
|
|
21
|
+
MAX_FAILURE_MESSAGE_CHARS = 1_000
|
|
22
|
+
|
|
23
|
+
FAILURE_CLASSIFICATIONS = frozenset(
|
|
24
|
+
{
|
|
25
|
+
"protocol-error",
|
|
26
|
+
"request-transport-error",
|
|
27
|
+
"worker-resource-exhaustion",
|
|
28
|
+
"worker-internal-error",
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WorkerProtocolError(ValueError):
|
|
34
|
+
"""A frame is malformed, incompatible, oversized, or fails custody checks."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ByteConnection(Protocol):
|
|
38
|
+
"""The deliberately narrow subset of a multiprocessing connection we use."""
|
|
39
|
+
|
|
40
|
+
def send_bytes(self, buffer: bytes) -> None: ...
|
|
41
|
+
|
|
42
|
+
def recv_bytes(self, maxlength: int | None = None) -> bytes: ...
|
|
43
|
+
|
|
44
|
+
def close(self) -> None: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class WorkerRequest:
|
|
49
|
+
generation: int
|
|
50
|
+
bundle_fingerprint: str
|
|
51
|
+
request: ConversionRequest
|
|
52
|
+
observation: ObservationOptions
|
|
53
|
+
transport_fingerprint: str
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def create(
|
|
57
|
+
cls,
|
|
58
|
+
generation: int,
|
|
59
|
+
bundle_fingerprint: str,
|
|
60
|
+
request: ConversionRequest,
|
|
61
|
+
observation: ObservationOptions | None = None,
|
|
62
|
+
) -> "WorkerRequest":
|
|
63
|
+
from ._worker_request_codec import create_worker_request
|
|
64
|
+
|
|
65
|
+
return create_worker_request(
|
|
66
|
+
generation,
|
|
67
|
+
bundle_fingerprint,
|
|
68
|
+
request,
|
|
69
|
+
observation,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True, slots=True)
|
|
74
|
+
class WorkerControl:
|
|
75
|
+
generation: int
|
|
76
|
+
bundle_fingerprint: str
|
|
77
|
+
transport_fingerprint: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True, slots=True)
|
|
81
|
+
class WorkerEvent:
|
|
82
|
+
kind: str
|
|
83
|
+
generation: int | None
|
|
84
|
+
bundle_fingerprint: str | None
|
|
85
|
+
transport_fingerprint: str | None
|
|
86
|
+
progress: ConversionProgress | None = None
|
|
87
|
+
result: ConversionResult | None = None
|
|
88
|
+
mappings: tuple[dict[str, Any], ...] = ()
|
|
89
|
+
failure_classification: str | None = None
|
|
90
|
+
message: str | None = None
|