feat(client): typo-safe protocol enums + context manager; add wafer_tool example

Interface cleanup so the report_* family matches the typo-safe ethos of
eq.names instead of leaking raw protobuf errors on a misspelled value.

- Milestone / ModuleState / JobState: importable str-enums (member == its
  wire name, so plain strings still work) — autocomplete + a typo-checked
  happy path. The clean rule: equipment-specific *names* live on eq.names;
  fixed protocol *value-sets* are enums.
- _enum_value(): resolves an enum-or-string arg client-side and, on a bad
  value, raises ValueError with a close-match hint *before* the wire. Wired
  into report_job / report_substrate / report_module / request_control_state
  (all previously raised a raw protobuf ValueError).
- Equipment is now a context manager (with Equipment(...) as eq: ...).
- examples/wafer_tool.py: a cluster tool tracking one wafer through one
  module end-to-end (E90 + E157), showing the enums + context manager.
- tests/test_enums.py: asserts the enums stay in lockstep with the proto and
  that the typo path is helpful. Wired into run_interop.sh (pyclient step).
- Interop drives both the enum and string forms on the wire + the ValueError
  typo path. Docs (ch16/ch42) updated; names-vs-enums rule documented.

All Python unit tests + 25 pyclient interop checks pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 22:09:48 +02:00
parent 2218b854ce
commit 8a55137e57
12 changed files with 290 additions and 82 deletions
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""The exported protocol enums must stay in lockstep with the proto, and the
typo-safe resolver must reject bad values with a helpful message. Plain
asserts — run directly."""
import sys
from secsgem_client import JobState, Milestone, ModuleState
from secsgem_client._client import _enum_value
from secsgem_client._proto import equipment_pb2 as pb
def members(e):
return {m.value for m in e}
def main() -> int:
# Each enum mirrors its proto source exactly — no drift, no missing member.
assert members(Milestone) == set(pb.SubstrateReport.Milestone.keys())
assert members(ModuleState) == set(pb.ModuleReport.State.keys())
assert members(JobState) == set(pb.ProcessJobState.State.keys())
# A member IS its wire name, so enum and plain string are interchangeable.
assert Milestone.ARRIVED == "ARRIVED"
assert _enum_value(pb.SubstrateReport.Milestone, Milestone.ARRIVED, "milestone") \
== _enum_value(pb.SubstrateReport.Milestone, "ARRIVED", "milestone")
# A typo raises ValueError (client-side, not a daemon round-trip) and the
# message offers the close match instead of leaking a raw protobuf error.
try:
_enum_value(pb.SubstrateReport.Milestone, "AT_WROK", "milestone")
raise SystemExit("expected ValueError for a misspelled milestone")
except ValueError as e:
assert "AT_WORK" in str(e), str(e)
print("enums: proto-sync + typo-safety checks passed")
return 0
if __name__ == "__main__":
sys.exit(main())