pyflightstream 0.2.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.
- pyflightstream/__init__.py +27 -0
- pyflightstream/cases/__init__.py +332 -0
- pyflightstream/cases/matrix_legacy.py +337 -0
- pyflightstream/commands/__init__.py +544 -0
- pyflightstream/commands/_meta.yaml +18 -0
- pyflightstream/commands/actuators.yaml +167 -0
- pyflightstream/commands/advanced_settings.yaml +136 -0
- pyflightstream/commands/aeroelastic_coupling.yaml +172 -0
- pyflightstream/commands/boundary_conditions.yaml +153 -0
- pyflightstream/commands/ccs_wing_mesh.yaml +74 -0
- pyflightstream/commands/coordinate_systems.yaml +123 -0
- pyflightstream/commands/file_io.yaml +82 -0
- pyflightstream/commands/mesh_import_export.yaml +72 -0
- pyflightstream/commands/motion_definitions.yaml +185 -0
- pyflightstream/commands/probe_points.yaml +116 -0
- pyflightstream/commands/runtime_settings.yaml +160 -0
- pyflightstream/commands/scenes.yaml +14 -0
- pyflightstream/commands/script_controls.yaml +39 -0
- pyflightstream/commands/simulation_controls.yaml +30 -0
- pyflightstream/commands/solver_analysis.yaml +118 -0
- pyflightstream/commands/solver_export.yaml +138 -0
- pyflightstream/commands/solver_initialization.yaml +95 -0
- pyflightstream/commands/solver_settings.yaml +120 -0
- pyflightstream/commands/streamlines.yaml +63 -0
- pyflightstream/commands/surface_sections.yaml +145 -0
- pyflightstream/commands/sweeper.yaml +115 -0
- pyflightstream/commands/unsteady_solver.yaml +68 -0
- pyflightstream/commands/volume_sections.yaml +148 -0
- pyflightstream/farfield/__init__.py +613 -0
- pyflightstream/files/__init__.py +375 -0
- pyflightstream/fsi/__init__.py +57 -0
- pyflightstream/fsi/beam.py +417 -0
- pyflightstream/fsi/centrifugal.py +418 -0
- pyflightstream/fsi/cli.py +206 -0
- pyflightstream/fsi/config.py +316 -0
- pyflightstream/fsi/driver.py +501 -0
- pyflightstream/fsi/kinematics.py +153 -0
- pyflightstream/fsi/loads.py +740 -0
- pyflightstream/fsi/nodes.py +463 -0
- pyflightstream/fsi/state.py +181 -0
- pyflightstream/post/__init__.py +18 -0
- pyflightstream/post/writers.py +195 -0
- pyflightstream/probes/__init__.py +453 -0
- pyflightstream/probes/geometry.py +281 -0
- pyflightstream/probes/planar.py +477 -0
- pyflightstream/qa/__init__.py +59 -0
- pyflightstream/qa/cli.py +364 -0
- pyflightstream/qa/compat.py +285 -0
- pyflightstream/qa/drift.py +406 -0
- pyflightstream/qa/geometry.py +421 -0
- pyflightstream/qa/physics.py +1744 -0
- pyflightstream/qa/probes.py +1023 -0
- pyflightstream/qa/references/PHY-01.yaml +38 -0
- pyflightstream/qa/references/PHY-02.yaml +28 -0
- pyflightstream/qa/references/PHY-05.yaml +29 -0
- pyflightstream/qa/references/PHY-06.yaml +90 -0
- pyflightstream/qa/references/SMI-01.yaml +28 -0
- pyflightstream/qa/references/SMI-02.yaml +28 -0
- pyflightstream/qa/specs.py +1405 -0
- pyflightstream/reference.py +465 -0
- pyflightstream/results/__init__.py +547 -0
- pyflightstream/run/__init__.py +677 -0
- pyflightstream/script/__init__.py +474 -0
- pyflightstream/script/helpers.py +844 -0
- pyflightstream/versions.py +191 -0
- pyflightstream-0.2.0.dist-info/METADATA +88 -0
- pyflightstream-0.2.0.dist-info/RECORD +71 -0
- pyflightstream-0.2.0.dist-info/WHEEL +5 -0
- pyflightstream-0.2.0.dist-info/entry_points.txt +3 -0
- pyflightstream-0.2.0.dist-info/licenses/LICENSE +21 -0
- pyflightstream-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
"""The FlightStream command database and per-version registry.
|
|
2
|
+
|
|
3
|
+
Pipeline role: single source of truth for which ASCII commands exist in
|
|
4
|
+
which FlightStream version, with typed arguments, script layout, emission
|
|
5
|
+
phase, and a manual page citation (``manual_ref``) per entry. The script
|
|
6
|
+
builder validates every emission against this database.
|
|
7
|
+
|
|
8
|
+
Data lives in the YAML files next to this module, one file per manual
|
|
9
|
+
chapter; ``_meta.yaml`` holds the ordered version list, which is the only
|
|
10
|
+
ordering authority (CLAUDE.md invariant 4). Version keys in the YAML
|
|
11
|
+
files are quoted strings ("26.120"); an unquoted key would be parsed as
|
|
12
|
+
a float and rejected by the loader.
|
|
13
|
+
|
|
14
|
+
Statuses follow the evidence rules of CLAUDE.md invariant 3:
|
|
15
|
+
``documented`` cites the manual through ``manual_ref``; ``verified`` and
|
|
16
|
+
``broken`` additionally cite a committed probe report; ``removed``
|
|
17
|
+
records the manual page stating the removal and, when known, a
|
|
18
|
+
successor command.
|
|
19
|
+
|
|
20
|
+
A command whose argument grammar differs between versions declares the
|
|
21
|
+
grammar of the latest documented version in ``args`` and overrides it
|
|
22
|
+
per version through ``versions.<v>.args``; the per-version view
|
|
23
|
+
resolves the override, so the script builder binds and renders the
|
|
24
|
+
grammar of its target version (the four-versus-three argument forms of
|
|
25
|
+
CREATE_BULK_SEPARATION, SRC-003 p.342 versus SRC-725 p.341, are the
|
|
26
|
+
motivating case).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import enum
|
|
32
|
+
import re
|
|
33
|
+
from collections.abc import Iterator, Mapping
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
from functools import lru_cache
|
|
36
|
+
from importlib import resources
|
|
37
|
+
|
|
38
|
+
import yaml
|
|
39
|
+
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
|
40
|
+
|
|
41
|
+
from pyflightstream.versions import FsVersion, known_versions, resolve
|
|
42
|
+
|
|
43
|
+
_MANUAL_REF_PATTERN = re.compile(r"^SRC-\d{3} pp?\.\d+")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Layout(enum.StrEnum):
|
|
47
|
+
"""Script layout grammars a command can use.
|
|
48
|
+
|
|
49
|
+
``bare`` has no arguments; ``inline`` takes arguments on the command
|
|
50
|
+
line; ``param_lines`` takes the command name alone and its
|
|
51
|
+
parameters each on a following line, ended by a blank line (the
|
|
52
|
+
multi-line function grammar of SRC-003 p.279, for example OPEN on
|
|
53
|
+
p.282); ``payload_lines`` takes a count followed by that many data
|
|
54
|
+
lines; ``keyword_block`` takes KEY VALUE lines until a terminator.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
BARE = "bare"
|
|
58
|
+
INLINE = "inline"
|
|
59
|
+
PARAM_LINES = "param_lines"
|
|
60
|
+
PAYLOAD_LINES = "payload_lines"
|
|
61
|
+
KEYWORD_BLOCK = "keyword_block"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Phase(enum.StrEnum):
|
|
65
|
+
"""Script phases, in emission order.
|
|
66
|
+
|
|
67
|
+
The script builder tracks the highest phase reached and rejects a
|
|
68
|
+
command whose phase precedes it (SAD phase-ordering rule).
|
|
69
|
+
``control`` marks script-control commands (STOP, PRINT, log export;
|
|
70
|
+
SRC-003 pp.281-283) that may appear anywhere; the builder exempts
|
|
71
|
+
them from phase ordering.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
GEOMETRY = "geometry"
|
|
75
|
+
SETUP = "setup"
|
|
76
|
+
INIT = "init"
|
|
77
|
+
EXEC = "exec"
|
|
78
|
+
ANALYSIS = "analysis"
|
|
79
|
+
EXPORT = "export"
|
|
80
|
+
CONTROL = "control"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Status(enum.StrEnum):
|
|
84
|
+
"""Evidence status of a command in one FlightStream version.
|
|
85
|
+
|
|
86
|
+
``documented``: the manual says so (manual_ref is the evidence).
|
|
87
|
+
``verified``: a Tier 2 probe passed on a licensed machine.
|
|
88
|
+
``broken``: a probe recorded a manual-versus-reality discrepancy.
|
|
89
|
+
``removed``: the manual states the command is no longer supported.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
DOCUMENTED = "documented"
|
|
93
|
+
VERIFIED = "verified"
|
|
94
|
+
BROKEN = "broken"
|
|
95
|
+
REMOVED = "removed"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ArgType(enum.StrEnum):
|
|
99
|
+
"""Argument types a command can declare.
|
|
100
|
+
|
|
101
|
+
``int_list`` is a comma-separated list of integer indices on one
|
|
102
|
+
data line, the grammar FlightStream uses for boundary and surface
|
|
103
|
+
selections (for example SRC-003 p.319). ``float_list`` is a list of
|
|
104
|
+
real values, for example the custom sweep values of
|
|
105
|
+
SWEEPER_SET_AOA_SWEEP (SRC-003 p.406). ``str_list`` is a list of
|
|
106
|
+
preformatted composite tokens, one per data line, used where a
|
|
107
|
+
single line pairs an index with a toggle, such as the per-surface
|
|
108
|
+
``index,ENABLE`` lines of INITIALIZE_SOLVER (SRC-003 p.337); the
|
|
109
|
+
typed pair validation lives in the curated helper that emits it.
|
|
110
|
+
``bool`` is a presence keyword of a keyword_block: True emits the
|
|
111
|
+
bare keyword line and False (or omission) emits nothing, the
|
|
112
|
+
grammar of the CLEAR keyword of IMPORT (SRC-003 p.307).
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
INT = "int"
|
|
116
|
+
FLOAT = "float"
|
|
117
|
+
STR = "str"
|
|
118
|
+
BOOL = "bool"
|
|
119
|
+
PATH = "path"
|
|
120
|
+
INT_LIST = "int_list"
|
|
121
|
+
FLOAT_LIST = "float_list"
|
|
122
|
+
STR_LIST = "str_list"
|
|
123
|
+
ENUM = "enum"
|
|
124
|
+
ENUM_LIST = "enum_list"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ListSeparator(enum.StrEnum):
|
|
128
|
+
"""How a list-typed argument joins its values in the script.
|
|
129
|
+
|
|
130
|
+
The manual samples show three grammars: comma-separated on one
|
|
131
|
+
line (SRC-003 p.332), space-separated on one line (p.364), and one
|
|
132
|
+
value per line (pp.338, 352).
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
COMMA = "comma"
|
|
136
|
+
SPACE = "space"
|
|
137
|
+
NEWLINE = "newline"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class CommandNotInVersionError(LookupError):
|
|
141
|
+
"""A command is unavailable in the requested FlightStream version.
|
|
142
|
+
|
|
143
|
+
Raised by a per-version view when the command is removed in that
|
|
144
|
+
version or has no recorded evidence for it. The message carries the
|
|
145
|
+
manual citation and the successor command when one is recorded.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ArgSpec(BaseModel):
|
|
150
|
+
"""Typed specification of one command argument.
|
|
151
|
+
|
|
152
|
+
Attributes
|
|
153
|
+
----------
|
|
154
|
+
name : str
|
|
155
|
+
Argument name, English, lowercase.
|
|
156
|
+
type : ArgType
|
|
157
|
+
Value type; ``enum`` and ``enum_list`` restrict values to
|
|
158
|
+
``values``.
|
|
159
|
+
values : tuple of str, optional
|
|
160
|
+
Allowed tokens; required for ``enum`` and ``enum_list`` types
|
|
161
|
+
and forbidden otherwise.
|
|
162
|
+
unit : str, optional
|
|
163
|
+
Physical unit of the value as the solver expects it (for
|
|
164
|
+
example ``"m/s"``); absent for dimensionless or textual
|
|
165
|
+
arguments.
|
|
166
|
+
required : bool
|
|
167
|
+
Whether the argument must be supplied; optional arguments are
|
|
168
|
+
the ones the manual marks as such (for example
|
|
169
|
+
LOAD_SOLVER_INITIALIZATION of OPEN, SRC-003 p.282).
|
|
170
|
+
separator : ListSeparator
|
|
171
|
+
How a list-typed argument joins its values when rendered; the
|
|
172
|
+
manual fixes it per command (see :class:`ListSeparator`).
|
|
173
|
+
own_line : bool
|
|
174
|
+
For inline commands whose file path follows on the line after
|
|
175
|
+
the inline arguments (for example SET_PROP_ACTUATOR_PROFILE,
|
|
176
|
+
SRC-003 pp.323-324).
|
|
177
|
+
joins_previous : bool
|
|
178
|
+
The value is appended to the script line of the preceding
|
|
179
|
+
argument instead of taking its own KEY VALUE line; the copy
|
|
180
|
+
count that PERIODIC symmetry appends to the SYMMETRY line of
|
|
181
|
+
INITIALIZE_SOLVER (SRC-003 p.337) is the documented case.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
185
|
+
|
|
186
|
+
name: str
|
|
187
|
+
type: ArgType
|
|
188
|
+
values: tuple[str, ...] | None = None
|
|
189
|
+
unit: str | None = None
|
|
190
|
+
required: bool = True
|
|
191
|
+
separator: ListSeparator = ListSeparator.COMMA
|
|
192
|
+
own_line: bool = False
|
|
193
|
+
joins_previous: bool = False
|
|
194
|
+
|
|
195
|
+
@model_validator(mode="after")
|
|
196
|
+
def _enum_types_carry_values(self) -> ArgSpec:
|
|
197
|
+
is_enum = self.type in (ArgType.ENUM, ArgType.ENUM_LIST)
|
|
198
|
+
if is_enum and not self.values:
|
|
199
|
+
raise ValueError(f"argument {self.name!r} is {self.type} and must list its values")
|
|
200
|
+
if not is_enum and self.values is not None:
|
|
201
|
+
raise ValueError(f"argument {self.name!r} is {self.type} and must not list values")
|
|
202
|
+
return self
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def is_list(self) -> bool:
|
|
206
|
+
"""Whether the argument holds several values."""
|
|
207
|
+
return self.type in (
|
|
208
|
+
ArgType.INT_LIST,
|
|
209
|
+
ArgType.FLOAT_LIST,
|
|
210
|
+
ArgType.STR_LIST,
|
|
211
|
+
ArgType.ENUM_LIST,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
@model_validator(mode="after")
|
|
215
|
+
def _separator_only_for_lists(self) -> ArgSpec:
|
|
216
|
+
if self.separator is not ListSeparator.COMMA and not self.is_list:
|
|
217
|
+
raise ValueError(f"argument {self.name!r} is scalar and takes no separator")
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
@model_validator(mode="after")
|
|
221
|
+
def _joins_previous_only_for_scalars(self) -> ArgSpec:
|
|
222
|
+
if self.joins_previous and self.is_list:
|
|
223
|
+
raise ValueError(f"argument {self.name!r} is a list and cannot join the previous line")
|
|
224
|
+
return self
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _check_layout_rules(name: str, layout: Layout, args: tuple[ArgSpec, ...]) -> None:
|
|
228
|
+
"""Reject an argument tuple that contradicts its command's layout.
|
|
229
|
+
|
|
230
|
+
Shared between the entry-level ``args`` and every per-version
|
|
231
|
+
override, so an override cannot smuggle in a grammar the layout
|
|
232
|
+
renderer does not support.
|
|
233
|
+
"""
|
|
234
|
+
if layout is Layout.BARE and args:
|
|
235
|
+
raise ValueError(f"{name} has layout bare and must not declare arguments")
|
|
236
|
+
if layout is not Layout.INLINE and any(arg.own_line for arg in args):
|
|
237
|
+
raise ValueError(f"{name}: own_line only applies to inline commands")
|
|
238
|
+
if layout is not Layout.KEYWORD_BLOCK and any(arg.type is ArgType.BOOL for arg in args):
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"{name}: bool arguments are bare presence keywords of a "
|
|
241
|
+
"keyword_block (SRC-003 p.307); other layouts spell their toggles "
|
|
242
|
+
"as ENABLE/DISABLE enums"
|
|
243
|
+
)
|
|
244
|
+
for position, arg in enumerate(args):
|
|
245
|
+
if not arg.joins_previous:
|
|
246
|
+
continue
|
|
247
|
+
if layout is not Layout.KEYWORD_BLOCK or position == 0:
|
|
248
|
+
raise ValueError(
|
|
249
|
+
f"{name}: joins_previous requires a keyword_block layout and a "
|
|
250
|
+
"preceding argument line to append to"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class VersionStatus(BaseModel):
|
|
255
|
+
"""Evidence record of a command in one FlightStream version.
|
|
256
|
+
|
|
257
|
+
Attributes
|
|
258
|
+
----------
|
|
259
|
+
status : Status
|
|
260
|
+
Evidence status; see :class:`Status`.
|
|
261
|
+
successor : str, optional
|
|
262
|
+
Replacement command name; only meaningful for ``removed``.
|
|
263
|
+
note : str, optional
|
|
264
|
+
Short paraphrased justification, with citation when needed.
|
|
265
|
+
report : str, optional
|
|
266
|
+
Repository-relative path of the committed probe report; required
|
|
267
|
+
for ``verified`` and ``broken`` (CLAUDE.md invariant 3).
|
|
268
|
+
args : tuple of ArgSpec, optional
|
|
269
|
+
Per-version argument grammar override. Declared when this
|
|
270
|
+
version's manual documents a different signature than the
|
|
271
|
+
entry-level ``args``; the per-version view substitutes it, so
|
|
272
|
+
emission for this version binds and renders the overridden
|
|
273
|
+
grammar. Absent for a version whose grammar matches the
|
|
274
|
+
entry-level one, and meaningless for ``removed``.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
278
|
+
|
|
279
|
+
status: Status
|
|
280
|
+
successor: str | None = None
|
|
281
|
+
note: str | None = None
|
|
282
|
+
report: str | None = None
|
|
283
|
+
args: tuple[ArgSpec, ...] | None = None
|
|
284
|
+
|
|
285
|
+
@model_validator(mode="after")
|
|
286
|
+
def _statuses_follow_the_evidence_rules(self) -> VersionStatus:
|
|
287
|
+
if self.status in (Status.VERIFIED, Status.BROKEN) and not self.report:
|
|
288
|
+
raise ValueError(
|
|
289
|
+
f"status {self.status} requires a committed probe report; statuses are "
|
|
290
|
+
"promoted only through pyfs-qa apply-compat, never edited by hand"
|
|
291
|
+
)
|
|
292
|
+
if self.successor is not None and self.status is not Status.REMOVED:
|
|
293
|
+
raise ValueError("successor is only recorded for removed commands")
|
|
294
|
+
if self.args is not None and self.status is Status.REMOVED:
|
|
295
|
+
raise ValueError(
|
|
296
|
+
"a removed version has no grammar to emit; args overrides are only "
|
|
297
|
+
"recorded for versions where the command exists"
|
|
298
|
+
)
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class CommandEntry(BaseModel):
|
|
303
|
+
"""One command of the FlightStream scripting interface.
|
|
304
|
+
|
|
305
|
+
Attributes
|
|
306
|
+
----------
|
|
307
|
+
name : str
|
|
308
|
+
Command name as the solver script expects it; supplied by the
|
|
309
|
+
loader from the YAML mapping key.
|
|
310
|
+
chapter : str
|
|
311
|
+
Stem of the chapter YAML file the entry came from; supplied by
|
|
312
|
+
the loader and used to group the generated reference.
|
|
313
|
+
layout : Layout
|
|
314
|
+
Script layout grammar.
|
|
315
|
+
phase : Phase
|
|
316
|
+
Emission phase used by the script builder's ordering check.
|
|
317
|
+
args : tuple of ArgSpec
|
|
318
|
+
Typed argument specifications, in emission order.
|
|
319
|
+
manual_ref : str
|
|
320
|
+
Manual citation, for example ``"SRC-003 p.352"``. Paraphrase
|
|
321
|
+
evidence only; manual text is never reproduced.
|
|
322
|
+
versions : mapping of str to VersionStatus
|
|
323
|
+
Evidence per canonical version identifier (quoted ``"26.XXX"``
|
|
324
|
+
keys). Versions without an entry have no recorded evidence.
|
|
325
|
+
notes : str, optional
|
|
326
|
+
Paraphrased usage caveats with citations.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
330
|
+
|
|
331
|
+
name: str
|
|
332
|
+
chapter: str = ""
|
|
333
|
+
layout: Layout
|
|
334
|
+
phase: Phase
|
|
335
|
+
args: tuple[ArgSpec, ...] = ()
|
|
336
|
+
manual_ref: str
|
|
337
|
+
versions: dict[str, VersionStatus]
|
|
338
|
+
notes: str | None = None
|
|
339
|
+
|
|
340
|
+
@model_validator(mode="before")
|
|
341
|
+
@classmethod
|
|
342
|
+
def _version_keys_are_quoted_strings(cls, data: dict) -> dict:
|
|
343
|
+
versions = data.get("versions")
|
|
344
|
+
if isinstance(versions, dict):
|
|
345
|
+
for key in versions:
|
|
346
|
+
if not isinstance(key, str):
|
|
347
|
+
raise ValueError(
|
|
348
|
+
f"version key {key!r} parsed as {type(key).__name__}; quote canonical "
|
|
349
|
+
'identifiers in the YAML ("26.120")'
|
|
350
|
+
)
|
|
351
|
+
return data
|
|
352
|
+
|
|
353
|
+
@field_validator("manual_ref")
|
|
354
|
+
@classmethod
|
|
355
|
+
def _manual_ref_cites_a_page(cls, value: str) -> str:
|
|
356
|
+
if not _MANUAL_REF_PATTERN.match(value):
|
|
357
|
+
raise ValueError(
|
|
358
|
+
f"manual_ref {value!r} must cite a source and page, for example 'SRC-003 p.352'"
|
|
359
|
+
)
|
|
360
|
+
return value
|
|
361
|
+
|
|
362
|
+
@model_validator(mode="after")
|
|
363
|
+
def _versions_are_registered_and_present(self) -> CommandEntry:
|
|
364
|
+
if not self.versions:
|
|
365
|
+
raise ValueError(f"{self.name} records no version evidence")
|
|
366
|
+
registered = {version.canonical for version in known_versions()}
|
|
367
|
+
unknown = set(self.versions) - registered
|
|
368
|
+
if unknown:
|
|
369
|
+
raise ValueError(
|
|
370
|
+
f"{self.name} references unregistered versions {sorted(unknown)}; register "
|
|
371
|
+
"them in commands/_meta.yaml first"
|
|
372
|
+
)
|
|
373
|
+
return self
|
|
374
|
+
|
|
375
|
+
@model_validator(mode="after")
|
|
376
|
+
def _args_obey_the_layout_rules(self) -> CommandEntry:
|
|
377
|
+
_check_layout_rules(self.name, self.layout, self.args)
|
|
378
|
+
return self
|
|
379
|
+
|
|
380
|
+
@model_validator(mode="after")
|
|
381
|
+
def _version_arg_overrides_obey_the_layout_rules(self) -> CommandEntry:
|
|
382
|
+
for canonical, record in self.versions.items():
|
|
383
|
+
if record.args is not None:
|
|
384
|
+
label = f"{self.name} ({canonical} args override)"
|
|
385
|
+
_check_layout_rules(label, self.layout, record.args)
|
|
386
|
+
return self
|
|
387
|
+
|
|
388
|
+
def status_in(self, version: FsVersion) -> VersionStatus | None:
|
|
389
|
+
"""Return the evidence record for ``version``, honoring hotfix inheritance.
|
|
390
|
+
|
|
391
|
+
A hotfix build (last canonical digit not zero) inherits the
|
|
392
|
+
record of its base release until probe evidence overrides it
|
|
393
|
+
(SAD Section 2).
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
version : FsVersion
|
|
398
|
+
Registered version to look up.
|
|
399
|
+
|
|
400
|
+
Returns
|
|
401
|
+
-------
|
|
402
|
+
VersionStatus or None
|
|
403
|
+
The evidence record, or None when the command has no
|
|
404
|
+
recorded evidence for this version.
|
|
405
|
+
"""
|
|
406
|
+
record = self.versions.get(version.canonical)
|
|
407
|
+
if record is not None:
|
|
408
|
+
return record
|
|
409
|
+
base_canonical = version.canonical[:-1] + "0"
|
|
410
|
+
if base_canonical != version.canonical:
|
|
411
|
+
return self.versions.get(base_canonical)
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@dataclass(frozen=True)
|
|
416
|
+
class VersionView:
|
|
417
|
+
"""Read-only view of the commands available in one FlightStream version.
|
|
418
|
+
|
|
419
|
+
Obtained through :meth:`CommandRegistry.for_version`. Mapping-style
|
|
420
|
+
access raises :class:`CommandNotInVersionError` with the manual
|
|
421
|
+
citation when a command is removed or has no recorded evidence.
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
version: FsVersion
|
|
425
|
+
_registry: CommandRegistry
|
|
426
|
+
|
|
427
|
+
def __getitem__(self, name: str) -> CommandEntry:
|
|
428
|
+
"""Return the entry for ``name`` or explain why it is unavailable."""
|
|
429
|
+
entry = self._registry.commands.get(name)
|
|
430
|
+
if entry is None:
|
|
431
|
+
raise CommandNotInVersionError(
|
|
432
|
+
f"{name} is not in the command database. The database only holds "
|
|
433
|
+
"commands drafted from the manual with citations; see CONTRIBUTING "
|
|
434
|
+
"for how to add one."
|
|
435
|
+
)
|
|
436
|
+
record = entry.status_in(self.version)
|
|
437
|
+
if record is None:
|
|
438
|
+
recorded = ", ".join(
|
|
439
|
+
f"{canonical} ({entry.versions[canonical].status})"
|
|
440
|
+
for canonical in sorted(entry.versions)
|
|
441
|
+
)
|
|
442
|
+
raise CommandNotInVersionError(
|
|
443
|
+
f"{name} has no recorded evidence for FlightStream {self.version.canonical}. "
|
|
444
|
+
f"Recorded evidence: {recorded}. Earlier versions await release-notes review "
|
|
445
|
+
"or backfill probing."
|
|
446
|
+
)
|
|
447
|
+
if record.status is Status.REMOVED:
|
|
448
|
+
reason = record.note or "no longer supported"
|
|
449
|
+
successor = (
|
|
450
|
+
f"Use {record.successor} instead."
|
|
451
|
+
if record.successor
|
|
452
|
+
else "No direct successor is recorded."
|
|
453
|
+
)
|
|
454
|
+
last = self._last_documented(entry)
|
|
455
|
+
last_part = f" Last documented in {last.canonical}." if last else ""
|
|
456
|
+
raise CommandNotInVersionError(
|
|
457
|
+
f"{name} is removed in FlightStream {self.version.canonical} "
|
|
458
|
+
f"({reason}, {entry.manual_ref}).{last_part} {successor}"
|
|
459
|
+
)
|
|
460
|
+
if record.args is not None:
|
|
461
|
+
# Per-version grammar override: the returned entry carries the
|
|
462
|
+
# argument signature this version's manual documents.
|
|
463
|
+
return entry.model_copy(update={"args": record.args})
|
|
464
|
+
return entry
|
|
465
|
+
|
|
466
|
+
def __contains__(self, name: str) -> bool:
|
|
467
|
+
"""Return whether ``name`` is available in this version."""
|
|
468
|
+
try:
|
|
469
|
+
self[name]
|
|
470
|
+
except CommandNotInVersionError:
|
|
471
|
+
return False
|
|
472
|
+
return True
|
|
473
|
+
|
|
474
|
+
def __iter__(self) -> Iterator[str]:
|
|
475
|
+
"""Iterate over the command names available in this version."""
|
|
476
|
+
return (name for name in self._registry.commands if name in self)
|
|
477
|
+
|
|
478
|
+
def _last_documented(self, entry: CommandEntry) -> FsVersion | None:
|
|
479
|
+
documented = [
|
|
480
|
+
version
|
|
481
|
+
for version in known_versions()
|
|
482
|
+
if entry.versions.get(version.canonical) is not None
|
|
483
|
+
and entry.versions[version.canonical].status is not Status.REMOVED
|
|
484
|
+
]
|
|
485
|
+
return documented[-1] if documented else None
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@dataclass(frozen=True)
|
|
489
|
+
class CommandRegistry:
|
|
490
|
+
"""The whole command database, all versions.
|
|
491
|
+
|
|
492
|
+
Attributes
|
|
493
|
+
----------
|
|
494
|
+
commands : mapping of str to CommandEntry
|
|
495
|
+
Every command, keyed by name, loaded from the chapter YAML
|
|
496
|
+
files next to this module.
|
|
497
|
+
"""
|
|
498
|
+
|
|
499
|
+
commands: Mapping[str, CommandEntry]
|
|
500
|
+
|
|
501
|
+
@classmethod
|
|
502
|
+
@lru_cache(maxsize=1)
|
|
503
|
+
def load(cls) -> CommandRegistry:
|
|
504
|
+
"""Load and validate the whole database from the installed package.
|
|
505
|
+
|
|
506
|
+
Returns
|
|
507
|
+
-------
|
|
508
|
+
CommandRegistry
|
|
509
|
+
Validated registry; every entry satisfied the schema and
|
|
510
|
+
the evidence rules.
|
|
511
|
+
|
|
512
|
+
Raises
|
|
513
|
+
------
|
|
514
|
+
ValueError
|
|
515
|
+
If two chapter files define the same command name, or an
|
|
516
|
+
entry violates the schema (pydantic validation error).
|
|
517
|
+
"""
|
|
518
|
+
commands: dict[str, CommandEntry] = {}
|
|
519
|
+
package = resources.files("pyflightstream.commands")
|
|
520
|
+
for resource in sorted(package.iterdir(), key=lambda item: item.name):
|
|
521
|
+
if not resource.name.endswith(".yaml") or resource.name == "_meta.yaml":
|
|
522
|
+
continue
|
|
523
|
+
entries = yaml.safe_load(resource.read_text(encoding="utf-8")) or {}
|
|
524
|
+
for name, body in entries.items():
|
|
525
|
+
if name in commands:
|
|
526
|
+
raise ValueError(f"{name} is defined in more than one chapter file")
|
|
527
|
+
chapter = resource.name.removesuffix(".yaml")
|
|
528
|
+
commands[name] = CommandEntry(name=name, chapter=chapter, **body)
|
|
529
|
+
return cls(commands=commands)
|
|
530
|
+
|
|
531
|
+
def for_version(self, version: str | FsVersion) -> VersionView:
|
|
532
|
+
"""Return the view of the commands available in one version.
|
|
533
|
+
|
|
534
|
+
Parameters
|
|
535
|
+
----------
|
|
536
|
+
version : str or FsVersion
|
|
537
|
+
Canonical identifier, display alias, or resolved version.
|
|
538
|
+
|
|
539
|
+
Returns
|
|
540
|
+
-------
|
|
541
|
+
VersionView
|
|
542
|
+
Per-version, read-only view.
|
|
543
|
+
"""
|
|
544
|
+
return VersionView(version=resolve(version), _registry=self)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Ordered FlightStream version list: release order, append only.
|
|
2
|
+
# This list is the only ordering authority (CLAUDE.md invariant 4).
|
|
3
|
+
# canonical: 26.XXX scheme, last digit indexes vendor hotfix builds.
|
|
4
|
+
# alias: the vendor-facing release name.
|
|
5
|
+
versions:
|
|
6
|
+
- canonical: "26.000"
|
|
7
|
+
alias: "26.0"
|
|
8
|
+
- canonical: "26.100"
|
|
9
|
+
alias: "26.1"
|
|
10
|
+
- canonical: "26.120"
|
|
11
|
+
alias: "26.12"
|
|
12
|
+
|
|
13
|
+
manual_editions:
|
|
14
|
+
"26.100": >
|
|
15
|
+
SRC-725: Altair FlightStream User Manual, 26.1 edition (local,
|
|
16
|
+
_private/exe/FlightStream_26100/), scripting reference from p.278;
|
|
17
|
+
cited in version notes as "SRC-725 p.N"
|
|
18
|
+
"26.120": "SRC-003: FlightStream User Manual v2612, scripting reference pp. 286-371"
|