1"""The parts database: resolving a part number into a configured component.
3Phase 02's second exit criterion. The behaviour worth protecting beyond that is
4the separation of datasheet values from measured ones: a measurement overrides
5a claim without erasing it, so "this valve flows 5% under its rating" stays a
6question anyone can ask later.
9from __future__
import annotations
12from pathlib
import Path
25CATALOG_TOML = textwrap.dedent(
27 ["swagelok-ss-8bk-v51"]
29 manufacturer = "Swagelok"
30 description = "1/2 in. ball valve, PTFE seat"
32 ["swagelok-ss-8bk-v51".params.Cv]
35 source = "manufacturer"
36 reference = "SS-8BK-V51 datasheet rev C, table 2"
38 ["swagelok-ss-8bk-v51".params.bore]
41 source = "manufacturer"
42 reference = "SS-8BK-V51 datasheet rev C"
44 ["swagelok-ss-8bk-v51".measured.Cv]
48 reference = "CF-2026-03, 12 points, R^2 0.998"
52 manufacturer = "generic"
53 description = "3/8 in. x 0.035 wall stainless tube"
55 ["tube-375-035".params.bore]
58 source = "manufacturer"
59 reference = "0.375 OD less 2 x 0.035 wall"
61 ["tube-375-035".params.length]
65 reference = "placeholder; set per instance"
72 path = tmp_path /
"parts.toml"
73 path.write_text(CATALOG_TOML)
74 return Catalog.from_file(path)
78 """Phase 02's exit criterion, stated directly."""
79 valve = catalog.instantiate(
80 "SOL-01",
"swagelok-ss-8bk-v51", connections={
"inlet":
"n1",
"outlet":
"n2"}
83 assert valve.id ==
"SOL-01"
84 assert valve.type ==
"valve"
85 assert valve.part ==
"swagelok-ss-8bk-v51"
86 assert valve.si(
"bore") == pytest.approx(0.009525)
87 assert valve.connections == {
"inlet":
"n1",
"outlet":
"n2"}
91 valve = catalog.instantiate(
"SOL-01",
"swagelok-ss-8bk-v51")
92 assert valve.si(
"Cv") == pytest.approx(1.14)
93 assert valve.params[
"Cv"].source
is Provenance.MEASURED
94 assert "CF-2026-03" in valve.params[
"Cv"].reference
98 """Overwriting would destroy the only record that a discrepancy exists.
100 A part measuring consistently below its rating is a finding -- mis-specified,
101 installed wrong, or an optimistic datasheet -- and all three become invisible
102 if the override replaces the original in place.
104 part = catalog.get(
"swagelok-ss-8bk-v51")
105 assert part.params[
"Cv"].value == 1.2
106 assert part.measured[
"Cv"].value == 1.14
108 disagreements = part.disagreements(tolerance=0.01)
109 assert "Cv" in disagreements
110 claimed, found = disagreements[
"Cv"]
111 assert claimed.si == pytest.approx(1.2)
112 assert found.si == pytest.approx(1.14)
116 part = catalog.get(
"swagelok-ss-8bk-v51")
117 assert part.disagreements(tolerance=0.10) == {}
121 """One valve was trimmed; this run of tube is longer. No new part number."""
122 line = catalog.instantiate(
125 overrides={
"length":
Param(0.46,
"m", Provenance.MEASURED,
"tape, 2026-09-08")},
127 assert line.si(
"length") == pytest.approx(0.46)
128 assert line.params[
"length"].source
is Provenance.MEASURED
130 assert line.si(
"bore") == pytest.approx(0.007747)
136 """With the parameter named, rather than as a missing key mid-solve."""
137 path = tmp_path /
"incomplete.toml"
144 ["half-a-valve".params.Cv]
151 catalog = Catalog.from_file(path)
152 with pytest.raises(ValueError, match=
"'bore' is required"):
153 catalog.instantiate(
"SOL-01",
"half-a-valve")
157 with pytest.raises(CatalogError, match=
"Known parts include"):
158 catalog.get(
"swagelok-ss-9zz-v99")
162 path = tmp_path /
"typeless.toml"
163 path.write_text(
'["mystery"]\nmanufacturer = "unknown"\n')
164 with pytest.raises(CatalogError, match=
"does not say what type it is"):
165 Catalog.from_file(path)
169 """The rule holds inside a catalog file too, not only in code."""
170 path = tmp_path /
"sourceless.toml"
177 ["a-part".params.bore]
183 with pytest.raises(ValueError, match=
"missing required field"):
184 Catalog.from_file(path)
188 """A shipped catalog, a team catalog, a campaign catalog -- in that order."""
189 override = tmp_path /
"campaign.toml"
193 ["swagelok-ss-8bk-v51"]
195 manufacturer = "Swagelok"
197 ["swagelok-ss-8bk-v51".params.Cv]
200 source = "manufacturer"
201 reference = "datasheet"
203 ["swagelok-ss-8bk-v51".params.bore]
206 source = "manufacturer"
207 reference = "datasheet"
209 ["swagelok-ss-8bk-v51".measured.Cv]
213 reference = "CF-2026-11, after 40 cycles"
217 catalog.load(override)
218 assert catalog.instantiate(
"SOL-01",
"swagelok-ss-8bk-v51").si(
220 ) == pytest.approx(1.09)
224 assert catalog.parts() == [
"swagelok-ss-8bk-v51",
"tube-375-035"]
225 assert catalog.parts(
"pipe") == [
"tube-375-035"]
226 assert len(catalog) == 2
227 assert "tube-375-035" in catalog
231 data = catalog.to_dict()
232 rebuilt =
Catalog(Part.from_dict(pid, body)
for pid, body
in data.items())
233 assert rebuilt.to_dict() == data
237 """What Phase 12 writes back when it fits a parameter to a flow test."""
238 p = measured(1.14,
"Cv",
"CF-2026-03")
239 assert p.source
is Provenance.MEASURED
240 assert p.reference ==
"CF-2026-03"
None test_agreement_within_tolerance_is_not_flagged(Catalog catalog)
None test_catalogs_layer(Path tmp_path, Catalog catalog)
None test_a_catalog_round_trips_through_dicts(Catalog catalog)
None test_the_datasheet_value_is_not_erased(Catalog catalog)
None test_a_measurement_overrides_the_datasheet(Catalog catalog)
None test_parts_can_be_listed_and_filtered(Catalog catalog)
None test_an_unknown_part_lists_what_is_available(Catalog catalog)
None test_per_instance_overrides_beat_the_catalog(Catalog catalog)
None test_a_part_number_resolves_to_a_configured_component(Catalog catalog)
Catalog catalog(Path tmp_path)
None test_catalog_params_still_require_a_source(Path tmp_path)
None test_a_part_must_declare_its_type(Path tmp_path)
None test_a_catalog_entry_missing_a_required_parameter_fails_at_load(Path tmp_path)
None test_measured_helper_tags_provenance()