pyflyrail 0.1.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.
flyrail/__init__.py ADDED
@@ -0,0 +1,231 @@
1
+ from flyrail._inventory import InventoryEntry
2
+ from flyrail._resource_models import Ancestor, ResourceKind, ResourceRef
3
+ from flyrail.artifacts import (
4
+ Artifact,
5
+ AssetRef,
6
+ Audience,
7
+ Command,
8
+ EnvRef,
9
+ Family,
10
+ HookArtifact,
11
+ HookEvent,
12
+ HookOutcome,
13
+ HttpTransport,
14
+ InstructionArtifact,
15
+ McpArtifact,
16
+ NativeArtifact,
17
+ SkillArtifact,
18
+ SupportAsset,
19
+ )
20
+ from flyrail.authority import Claim, Ownership, ResourceAuthority
21
+ from flyrail.bundle import Bundle
22
+ from flyrail.configuration import (
23
+ ClaimObservation,
24
+ InstallationObservation,
25
+ InstallationResult,
26
+ InstallationTarget,
27
+ LifecyclePreview,
28
+ ResourceObservation,
29
+ ResourcePlan,
30
+ ResourceResult,
31
+ )
32
+ from flyrail.content import (
33
+ Content,
34
+ DocumentFormat,
35
+ DocumentSchema,
36
+ FileContent,
37
+ FileMode,
38
+ Key,
39
+ Member,
40
+ Placement,
41
+ Position,
42
+ SectionBoundaries,
43
+ SectionContent,
44
+ Selector,
45
+ StructuredContent,
46
+ TreeContent,
47
+ )
48
+ from flyrail.destinations import (
49
+ Capability,
50
+ RenderContext,
51
+ capabilities,
52
+ instruction_destination,
53
+ mcp_destination,
54
+ )
55
+ from flyrail.editors import (
56
+ Acquisition,
57
+ ArrayPosition,
58
+ CreatedContainer,
59
+ Disposition,
60
+ DocumentEdit,
61
+ DocumentProvenance,
62
+ DocumentRequest,
63
+ EditObservation,
64
+ EditStatus,
65
+ OwnedSelection,
66
+ SelectionSnapshot,
67
+ SharedContent,
68
+ content_claim,
69
+ edit_document,
70
+ )
71
+ from flyrail.hooks import HookRuntime, HookShell
72
+ from flyrail.inspection import inspect, render_skills
73
+ from flyrail.lifecycle import (
74
+ apply_preview,
75
+ inspect_installation,
76
+ install,
77
+ preview,
78
+ preview_removal,
79
+ recover_installation,
80
+ remove,
81
+ sync,
82
+ uninstall,
83
+ update,
84
+ )
85
+ from flyrail.models import BundleEntry, BundleIdentity, OperationStatus, SkillSpec
86
+ from flyrail.observations import (
87
+ Conflict,
88
+ ErrorCode,
89
+ Installation,
90
+ Modification,
91
+ ModificationKind,
92
+ Observation,
93
+ ObservationState,
94
+ TargetError,
95
+ TargetInspection,
96
+ TargetResult,
97
+ )
98
+ from flyrail.rendered import (
99
+ Dependency,
100
+ DependencyMode,
101
+ Notice,
102
+ NoticeKind,
103
+ RenderedArtifact,
104
+ RenderedBundle,
105
+ )
106
+ from flyrail.targets import Agent, Platform, Surface, Target, TargetScope
107
+ from flyrail.translation import render, render_many
108
+ from flyrail.values import (
109
+ ArrayValue,
110
+ ObjectValue,
111
+ Scalar,
112
+ Value,
113
+ freeze_value,
114
+ semantic_bytes,
115
+ value_from_record,
116
+ )
117
+
118
+ __all__ = [
119
+ "Acquisition",
120
+ "Agent",
121
+ "Ancestor",
122
+ "ArrayPosition",
123
+ "ArrayValue",
124
+ "Artifact",
125
+ "AssetRef",
126
+ "Audience",
127
+ "Bundle",
128
+ "BundleEntry",
129
+ "BundleIdentity",
130
+ "Capability",
131
+ "Claim",
132
+ "ClaimObservation",
133
+ "Command",
134
+ "Conflict",
135
+ "Content",
136
+ "CreatedContainer",
137
+ "Dependency",
138
+ "DependencyMode",
139
+ "Disposition",
140
+ "DocumentEdit",
141
+ "DocumentFormat",
142
+ "DocumentProvenance",
143
+ "DocumentRequest",
144
+ "DocumentSchema",
145
+ "EditObservation",
146
+ "EditStatus",
147
+ "EnvRef",
148
+ "ErrorCode",
149
+ "Family",
150
+ "FileContent",
151
+ "FileMode",
152
+ "HookArtifact",
153
+ "HookEvent",
154
+ "HookOutcome",
155
+ "HookRuntime",
156
+ "HookShell",
157
+ "HttpTransport",
158
+ "Installation",
159
+ "InstallationObservation",
160
+ "InstallationResult",
161
+ "InstallationTarget",
162
+ "InstructionArtifact",
163
+ "InventoryEntry",
164
+ "Key",
165
+ "LifecyclePreview",
166
+ "McpArtifact",
167
+ "Member",
168
+ "Modification",
169
+ "ModificationKind",
170
+ "NativeArtifact",
171
+ "Notice",
172
+ "NoticeKind",
173
+ "ObjectValue",
174
+ "Observation",
175
+ "ObservationState",
176
+ "OperationStatus",
177
+ "OwnedSelection",
178
+ "Ownership",
179
+ "Placement",
180
+ "Platform",
181
+ "Position",
182
+ "RenderContext",
183
+ "RenderedArtifact",
184
+ "RenderedBundle",
185
+ "ResourceAuthority",
186
+ "ResourceKind",
187
+ "ResourceObservation",
188
+ "ResourcePlan",
189
+ "ResourceRef",
190
+ "ResourceResult",
191
+ "Scalar",
192
+ "SectionBoundaries",
193
+ "SectionContent",
194
+ "SelectionSnapshot",
195
+ "Selector",
196
+ "SharedContent",
197
+ "SkillArtifact",
198
+ "SkillSpec",
199
+ "StructuredContent",
200
+ "SupportAsset",
201
+ "Surface",
202
+ "Target",
203
+ "TargetError",
204
+ "TargetInspection",
205
+ "TargetResult",
206
+ "TargetScope",
207
+ "TreeContent",
208
+ "Value",
209
+ "apply_preview",
210
+ "capabilities",
211
+ "content_claim",
212
+ "edit_document",
213
+ "freeze_value",
214
+ "inspect",
215
+ "inspect_installation",
216
+ "install",
217
+ "instruction_destination",
218
+ "mcp_destination",
219
+ "preview",
220
+ "preview_removal",
221
+ "recover_installation",
222
+ "remove",
223
+ "render",
224
+ "render_many",
225
+ "render_skills",
226
+ "semantic_bytes",
227
+ "sync",
228
+ "uninstall",
229
+ "update",
230
+ "value_from_record",
231
+ ]
flyrail/_codec.py ADDED
@@ -0,0 +1,109 @@
1
+ import json
2
+ from collections.abc import Callable
3
+ from dataclasses import fields, is_dataclass
4
+ from enum import StrEnum
5
+ from pathlib import Path
6
+
7
+ from flyrail._manifest import _reject_constant, _unique_object
8
+ from flyrail.authority import Claim, Ownership
9
+ from flyrail.content import DocumentFormat, DocumentSchema, Key, Member, SectionBoundaries, Selector
10
+ from flyrail.editors import (
11
+ ArrayPosition,
12
+ CreatedContainer,
13
+ Disposition,
14
+ DocumentProvenance,
15
+ OwnedSelection,
16
+ SelectionSnapshot,
17
+ )
18
+ from flyrail.values import ArrayValue, ObjectValue, Scalar, semantic_record, value_from_record
19
+
20
+ _TYPES: dict[str, Callable[..., object]] = {
21
+ cls.__name__: cls
22
+ for cls in (
23
+ Claim,
24
+ Ownership,
25
+ DocumentFormat,
26
+ DocumentSchema,
27
+ Key,
28
+ Member,
29
+ SectionBoundaries,
30
+ Selector,
31
+ ArrayPosition,
32
+ CreatedContainer,
33
+ Disposition,
34
+ DocumentProvenance,
35
+ OwnedSelection,
36
+ SelectionSnapshot,
37
+ )
38
+ }
39
+
40
+
41
+ def register(cls: type[object]) -> None:
42
+ _TYPES[cls.__name__] = cls
43
+
44
+
45
+ def record(value: object) -> object:
46
+ if isinstance(value, Scalar | ObjectValue | ArrayValue):
47
+ return {"value": semantic_record(value)}
48
+ if isinstance(value, StrEnum):
49
+ return {"enum": type(value).__name__, "value": str(value)}
50
+ if is_dataclass(value) and not isinstance(value, type):
51
+ return {
52
+ "type": type(value).__name__,
53
+ "fields": {field.name: record(getattr(value, field.name)) for field in fields(value)},
54
+ }
55
+ if isinstance(value, bytes):
56
+ return {"bytes": value.hex()}
57
+ if isinstance(value, Path):
58
+ return {"path": value.as_posix()}
59
+ if isinstance(value, tuple):
60
+ return [record(item) for item in value]
61
+ if value is None or type(value) in {str, int, bool}:
62
+ return value
63
+ raise TypeError("unsupported state value")
64
+
65
+
66
+ def _decode(value: object) -> object:
67
+ if isinstance(value, list):
68
+ return tuple(_decode(item) for item in value)
69
+ if not isinstance(value, dict):
70
+ if value is None or type(value) in {str, int, bool}:
71
+ return value
72
+ raise ValueError("invalid state scalar")
73
+ if value.keys() == {"value"}:
74
+ return value_from_record(value["value"])
75
+ if value.keys() == {"bytes"} and isinstance(value["bytes"], str):
76
+ return bytes.fromhex(value["bytes"])
77
+ if value.keys() == {"path"} and isinstance(value["path"], str):
78
+ path = Path(value["path"])
79
+ if not path.is_absolute() or ".." in path.parts:
80
+ raise ValueError("state paths must be absolute without traversal")
81
+ return path
82
+ if value.keys() == {"enum", "value"} and value["enum"] in _TYPES:
83
+ return _TYPES[value["enum"]](value["value"])
84
+ if value.keys() == {"type", "fields"} and value["type"] in _TYPES:
85
+ constructor = _TYPES[value["type"]]
86
+ arguments = value["fields"]
87
+ if not isinstance(arguments, dict) or not is_dataclass(constructor):
88
+ raise ValueError("invalid state record")
89
+ if arguments.keys() != {field.name for field in fields(constructor)}:
90
+ raise ValueError("state record fields do not match schema")
91
+ return constructor(**{key: _decode(item) for key, item in arguments.items()})
92
+ raise ValueError("unknown state record")
93
+
94
+
95
+ def encode(value: object) -> bytes:
96
+ return (
97
+ json.dumps(record(value), sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
98
+ ).encode()
99
+
100
+
101
+ def decode(data: bytes) -> object:
102
+ try:
103
+ raw = json.loads(data, object_pairs_hook=_unique_object, parse_constant=_reject_constant)
104
+ value = _decode(raw)
105
+ if record(value) != raw:
106
+ raise ValueError("state record is not canonical")
107
+ return value
108
+ except (RecursionError, TypeError, KeyError, AttributeError) as error:
109
+ raise ValueError("invalid state record") from error
@@ -0,0 +1,322 @@
1
+ from typing import Protocol
2
+
3
+ from flyrail._manifest import _object, _string
4
+ from flyrail._validation import validate_relative_path
5
+ from flyrail.artifacts import (
6
+ Artifact,
7
+ AssetRef,
8
+ Audience,
9
+ Command,
10
+ EnvRef,
11
+ Family,
12
+ HookArtifact,
13
+ HookEvent,
14
+ HookOutcome,
15
+ HttpTransport,
16
+ InstructionArtifact,
17
+ McpArtifact,
18
+ NativeArtifact,
19
+ SkillArtifact,
20
+ SupportAsset,
21
+ )
22
+ from flyrail.content import (
23
+ Content,
24
+ DocumentFormat,
25
+ FileContent,
26
+ FileMode,
27
+ Key,
28
+ Member,
29
+ Placement,
30
+ Position,
31
+ SectionBoundaries,
32
+ SectionContent,
33
+ Selector,
34
+ StructuredContent,
35
+ TreeContent,
36
+ )
37
+ from flyrail.models import BundleEntry, BundleIdentity
38
+ from flyrail.rendered import Dependency, DependencyMode
39
+ from flyrail.targets import Agent, Platform, Surface, TargetScope
40
+ from flyrail.values import Value, freeze_value, value_from_record
41
+
42
+
43
+ class ConfigSource(Protocol):
44
+ def read_file(self, path: str) -> bytes: ...
45
+
46
+ def tree_entries(
47
+ self, path: str, name: str, executables: tuple[str, ...]
48
+ ) -> tuple[BundleEntry, ...]: ...
49
+
50
+
51
+ def _list(value: object) -> list[object]:
52
+ if not isinstance(value, list):
53
+ raise ValueError("expected a JSON array")
54
+ return value
55
+
56
+
57
+ def _path(value: object) -> str:
58
+ path = _string(value, "source path")
59
+ validate_relative_path(path, "source path")
60
+ return path
61
+
62
+
63
+ def _text(fields: dict[str, object], source: ConfigSource) -> str:
64
+ if ("text" in fields) == ("path" in fields):
65
+ raise ValueError("provide exactly one of text or path")
66
+ return (
67
+ _string(fields["text"], "text")
68
+ if "text" in fields
69
+ else source.read_file(_path(fields["path"])).decode("utf-8")
70
+ )
71
+
72
+
73
+ def _tree(fields: dict[str, object], source: ConfigSource) -> TreeContent:
74
+ executables = tuple(_path(value) for value in _list(fields.get("executables", [])))
75
+ if len(set(executables)) != len(executables):
76
+ raise ValueError("duplicate executable path")
77
+ entries = source.tree_entries(_path(fields["path"]), "content", executables)
78
+ result = TreeContent(
79
+ BundleEntry(entry.path.removeprefix("content/"), entry.data, entry.executable)
80
+ for entry in entries
81
+ if entry.path != "content"
82
+ )
83
+ files = {entry.path for entry in result.entries if not entry.is_directory}
84
+ if any(executable not in files for executable in executables):
85
+ raise ValueError("every executable must name an existing file")
86
+ return result
87
+
88
+
89
+ def _argument(value: object) -> str | AssetRef:
90
+ if isinstance(value, dict):
91
+ fields = _object(value, {"asset"}, {"path"})
92
+ return AssetRef(
93
+ _string(fields["asset"], "referenced asset id"),
94
+ None if "path" not in fields else _path(fields["path"]),
95
+ )
96
+ return _string(value, "command argument")
97
+
98
+
99
+ def _command(value: object) -> Command:
100
+ fields = _object(value, {"argv"}, {"env", "cwd"})
101
+ raw_env = fields.get("env", {})
102
+ if not isinstance(raw_env, dict):
103
+ raise ValueError("command env must be a JSON object")
104
+ env: list[tuple[str, str | EnvRef]] = []
105
+ for key, item in raw_env.items():
106
+ if isinstance(item, dict):
107
+ reference = _object(item, {"env"}, set())
108
+ env.append((key, EnvRef(_string(reference["env"], "environment reference"))))
109
+ else:
110
+ env.append((key, _string(item, "environment value")))
111
+ return Command(
112
+ [_argument(item) for item in _list(fields["argv"])],
113
+ env,
114
+ None if "cwd" not in fields else _argument(fields["cwd"]),
115
+ )
116
+
117
+
118
+ def _member(value: object) -> Member:
119
+ fields = _object(value, set(), {"identity", "semantic_identity", "key"})
120
+ return Member(
121
+ _value(fields, "identity", "semantic_identity"),
122
+ [_string(name, "member key") for name in _list(fields.get("key", []))],
123
+ )
124
+
125
+
126
+ def _value(fields: dict[str, object], plain: str, typed: str) -> Value:
127
+ if (plain in fields) == (typed in fields):
128
+ raise ValueError(f"provide exactly one of {plain} or {typed}")
129
+ return freeze_value(fields[plain]) if plain in fields else value_from_record(fields[typed])
130
+
131
+
132
+ def _selector(value: object) -> Selector:
133
+ return Selector(Key(item) if isinstance(item, str) else _member(item) for item in _list(value))
134
+
135
+
136
+ def _content(value: object, source: ConfigSource) -> Content:
137
+ header = _object(
138
+ value,
139
+ {"kind"},
140
+ {
141
+ "path",
142
+ "text",
143
+ "mode",
144
+ "executables",
145
+ "marker",
146
+ "format",
147
+ "selector",
148
+ "value",
149
+ "semantic_value",
150
+ "placement",
151
+ "boundaries",
152
+ },
153
+ )
154
+ kind = header["kind"]
155
+ if kind == "file":
156
+ fields = _object(value, {"kind", "path"}, {"mode"})
157
+ return FileContent(
158
+ source.read_file(_path(fields["path"])),
159
+ FileMode(_string(fields.get("mode", "private"), "file mode")),
160
+ )
161
+ if kind == "tree":
162
+ return _tree(_object(value, {"kind", "path"}, {"executables"}), source)
163
+ if kind == "section":
164
+ fields = _object(value, {"kind", "marker"}, {"text", "path", "boundaries"})
165
+ boundaries = None
166
+ if "boundaries" in fields:
167
+ raw_boundaries = _object(fields["boundaries"], {"start", "end"}, set())
168
+ boundaries = SectionBoundaries(
169
+ _string(raw_boundaries["start"], "start boundary"),
170
+ _string(raw_boundaries["end"], "end boundary"),
171
+ )
172
+ return SectionContent(
173
+ _string(fields["marker"], "section marker"), _text(fields, source), boundaries
174
+ )
175
+ if kind == "structured":
176
+ fields = _object(
177
+ value, {"kind", "format", "selector"}, {"value", "semantic_value", "placement"}
178
+ )
179
+ placement = None
180
+ if "placement" in fields:
181
+ raw = _object(fields["placement"], {"position"}, {"anchor"})
182
+ placement = Placement(
183
+ Position(_string(raw["position"], "placement position")),
184
+ None if "anchor" not in raw else _member(raw["anchor"]),
185
+ )
186
+ return StructuredContent(
187
+ DocumentFormat(_string(fields["format"], "document format")),
188
+ _selector(fields["selector"]),
189
+ _value(fields, "value", "semantic_value"),
190
+ placement,
191
+ )
192
+ raise ValueError("unknown native content kind")
193
+
194
+
195
+ def _artifact(value: object, source: ConfigSource) -> Artifact:
196
+ header = _object(
197
+ value,
198
+ {"kind", "id"},
199
+ {
200
+ "name",
201
+ "path",
202
+ "executables",
203
+ "text",
204
+ "transport",
205
+ "event",
206
+ "command",
207
+ "timeout_ms",
208
+ "outcomes",
209
+ "tools",
210
+ "protocol_version",
211
+ "family",
212
+ "audience",
213
+ "destination",
214
+ "content",
215
+ },
216
+ )
217
+ identifier = _string(header["id"], "artifact id")
218
+ kind = header["kind"]
219
+ if kind == "skill":
220
+ fields = _object(value, {"kind", "id", "name", "path"}, {"executables"})
221
+ return SkillArtifact(
222
+ identifier, _string(fields["name"], "skill name"), _tree(fields, source)
223
+ )
224
+ if kind == "instruction":
225
+ fields = _object(value, {"kind", "id"}, {"text", "path"})
226
+ return InstructionArtifact(identifier, _text(fields, source))
227
+ if kind == "mcp":
228
+ fields = _object(value, {"kind", "id", "name", "transport"}, set())
229
+ transport = _object(fields["transport"], {"kind"}, {"command", "url", "bearer_token_env"})
230
+ if transport["kind"] == "stdio":
231
+ transport = _object(fields["transport"], {"kind", "command"}, set())
232
+ definition: Command | HttpTransport = _command(transport["command"])
233
+ elif transport["kind"] == "http":
234
+ transport = _object(fields["transport"], {"kind", "url"}, {"bearer_token_env"})
235
+ definition = HttpTransport(
236
+ _string(transport["url"], "MCP URL"),
237
+ None
238
+ if "bearer_token_env" not in transport
239
+ else EnvRef(_string(transport["bearer_token_env"], "bearer environment reference")),
240
+ )
241
+ else:
242
+ raise ValueError("unknown MCP transport")
243
+ return McpArtifact(identifier, _string(fields["name"], "MCP server name"), definition)
244
+ if kind == "hook":
245
+ fields = _object(
246
+ value,
247
+ {"kind", "id", "event", "command", "protocol_version"},
248
+ {"timeout_ms", "outcomes", "tools"},
249
+ )
250
+ if type(fields["protocol_version"]) is not int or fields["protocol_version"] != 1:
251
+ raise ValueError("hook protocol_version must be integer 1")
252
+ timeout = fields.get("timeout_ms", 10000)
253
+ if type(timeout) is not int:
254
+ raise ValueError("hook timeout must be an integer")
255
+ return HookArtifact(
256
+ identifier,
257
+ HookEvent(_string(fields["event"], "hook event")),
258
+ _command(fields["command"]),
259
+ timeout,
260
+ [
261
+ HookOutcome(_string(item, "hook outcome"))
262
+ for item in _list(fields.get("outcomes", ["continue"]))
263
+ ],
264
+ [_string(item, "hook tool") for item in _list(fields.get("tools", []))],
265
+ )
266
+ if kind == "native":
267
+ fields = _object(
268
+ value, {"kind", "id", "family", "audience", "destination", "content"}, set()
269
+ )
270
+ audience = _object(fields["audience"], {"agent", "scope"}, {"surface", "platform"})
271
+ return NativeArtifact(
272
+ identifier,
273
+ Family(_string(fields["family"], "native family")),
274
+ Audience(
275
+ Agent(_string(audience["agent"], "agent")),
276
+ TargetScope(_string(audience["scope"], "scope")),
277
+ Surface(_string(audience.get("surface", "cli"), "surface")),
278
+ None
279
+ if "platform" not in audience
280
+ else Platform(_string(audience["platform"], "platform")),
281
+ ),
282
+ _path(fields["destination"]),
283
+ _content(fields["content"], source),
284
+ )
285
+ raise ValueError("unknown artifact kind")
286
+
287
+
288
+ def read_config_manifest(
289
+ raw: object, source: ConfigSource
290
+ ) -> tuple[BundleIdentity, tuple[Artifact, ...], tuple[SupportAsset, ...], tuple[Dependency, ...]]:
291
+ fields = _object(
292
+ raw, {"schema_version", "id", "version", "artifacts"}, {"assets", "dependencies"}
293
+ )
294
+ if type(fields["schema_version"]) is not int or fields["schema_version"] != 2:
295
+ raise ValueError("configuration schema_version must be integer 2")
296
+ artifacts = tuple(_artifact(item, source) for item in _list(fields["artifacts"]))
297
+ assets: list[SupportAsset] = []
298
+ for item in _list(fields.get("assets", [])):
299
+ asset = _object(item, {"id", "family", "path"}, {"executables"})
300
+ assets.append(
301
+ SupportAsset(
302
+ _string(asset["id"], "asset id"),
303
+ Family(_string(asset["family"], "asset family")),
304
+ _tree(asset, source),
305
+ )
306
+ )
307
+ dependencies: list[Dependency] = []
308
+ for item in _list(fields.get("dependencies", [])):
309
+ edge = _object(item, {"dependent", "required"}, {"mode"})
310
+ dependencies.append(
311
+ Dependency(
312
+ _string(edge["dependent"], "dependent"),
313
+ _string(edge["required"], "required"),
314
+ DependencyMode(_string(edge.get("mode", "revision"), "dependency mode")),
315
+ )
316
+ )
317
+ return (
318
+ BundleIdentity(_string(fields["id"], "bundle id"), _string(fields["version"], "version")),
319
+ artifacts,
320
+ tuple(assets),
321
+ tuple(dependencies),
322
+ )