plesty.lib.test.schema_refresh ============================== .. py:module:: plesty.lib.test.schema_refresh .. autoapi-nested-parse:: Turn field-test observations into a reviewed parameter-schema update. A field test already reads every parameter and asks the instrument for its own limits. That is precisely the information a hand-written schema drifts away from — the shipped envelope is what the console *can* do, while the attached sensor is usually narrower. This module turns those observations into a **proposal**, never a silent edit. Nothing is written to a schema without a developer saying yes, because the three kinds of change carry very different confidence: ``type`` Ground truth. The instrument's response type is what the parser must handle, and a mismatch is a real defect. ``min_value`` / ``max_value`` / ``options`` The attached sensor's actual limits, which is what a schema envelope should describe. High confidence, and usually the whole point. ``default`` **Advisory only.** The value read is whatever the current experiment set, not a sensible shipping default. Writing "wavelength = 780 nm" into a module's schema because one measurement happened to use 780 nm makes an experiment-specific value permanent for every future user. Proposed so it is visible, flagged so it is not applied by reflex. Typical usage:: proposal = pipeline.propose_schema_update() if proposal.has_changes: print(proposal.to_markdown()) proposal.write("reports/schema-proposed") # never touches the original proposal.apply(confirm=confirm_interactively) # asks first Attributes ---------- .. autoapisummary:: plesty.lib.test.schema_refresh.TRUSTED_FIELDS plesty.lib.test.schema_refresh.ADVISORY_FIELDS plesty.lib.test.schema_refresh._TYPE_NAMES Classes ------- .. autoapisummary:: plesty.lib.test.schema_refresh.SchemaChange plesty.lib.test.schema_refresh.SchemaProposal Functions --------- .. autoapisummary:: plesty.lib.test.schema_refresh.confirm_interactively plesty.lib.test.schema_refresh.propose_schema_update plesty.lib.test.schema_refresh._iter_entries plesty.lib.test.schema_refresh._schema_range plesty.lib.test.schema_refresh._set_field Module Contents --------------- .. py:data:: TRUSTED_FIELDS :type: tuple[str, Ellipsis] :value: ('type', 'min_value', 'max_value', 'options') .. py:data:: ADVISORY_FIELDS :type: tuple[str, Ellipsis] :value: ('default',) .. py:data:: _TYPE_NAMES :type: dict[type, str] .. py:class:: SchemaChange One proposed difference between a schema entry and the instrument. :ivar key: Runtime parameter key. :ivar field: Schema field the change applies to. :ivar current: Value currently in the schema. :ivar observed: Value the instrument reported. :ivar advisory: ``True`` when the change needs judgement rather than acceptance — see :data:`ADVISORY_FIELDS`. :ivar note: Why this change is proposed, or why it needs care. .. py:attribute:: key :type: str .. py:attribute:: field :type: str .. py:attribute:: current :type: Any .. py:attribute:: observed :type: Any .. py:attribute:: advisory :type: bool .. py:attribute:: note :type: str :value: '' .. py:method:: to_dict() -> dict[str, Any] Return a JSON-serialisable mapping of the change. .. py:class:: SchemaProposal Proposed schema changes derived from what an instrument reported. :ivar schema_path: The schema file the proposal was built against. :ivar device: Device class name. :ivar address: Instrument address the observations came from. :ivar observed_utc: When the observations were taken. :ivar changes: Every proposed change, trusted ones first. .. py:attribute:: schema_path :type: str .. py:attribute:: device :type: str :value: '' .. py:attribute:: address :type: str :value: '' .. py:attribute:: observed_utc :type: str :value: '' .. py:attribute:: changes :type: list[SchemaChange] :value: [] .. py:property:: has_changes :type: bool ``True`` when the instrument disagreed with the schema anywhere. .. py:property:: trusted :type: list[SchemaChange] Changes the instrument is authoritative about. .. py:property:: advisory :type: list[SchemaChange] Changes that need a developer's judgement. .. py:method:: to_dict() -> dict[str, Any] Return a JSON-serialisable mapping of the proposal. .. py:method:: to_markdown() -> str Render the proposal as a reviewable diff. .. py:method:: updated_schema(include_advisory: bool = False) -> dict[str, Any] Return the schema with the proposed changes applied in memory. The file on disk is untouched. :param include_advisory: Also apply the changes flagged for review. .. py:method:: write(path: str | pathlib.Path, include_advisory: bool = False) -> list[pathlib.Path] Write the proposal beside the schema without modifying the original. :param path: Base path; writes ``.json`` (the full updated schema) and ``.md`` (the reviewable diff). :param include_advisory: Also apply the changes flagged for review. :returns: The paths written, schema first. .. py:method:: apply(confirm: collections.abc.Callable[[str], bool], include_advisory: bool = False) -> bool Overwrite the schema in place, but only if *confirm* returns ``True``. There is deliberately no way to apply without asking: a schema is a module's published contract, and a reading taken during one experiment is not automatically the right thing to ship. :param confirm: Called with the rendered diff; returns ``True`` to proceed. Callers on a terminal can pass :func:`confirm_interactively`. :param include_advisory: Also apply the changes flagged for review. :returns: ``True`` if the schema was rewritten, ``False`` if there was nothing to change or the developer declined. .. py:function:: confirm_interactively(diff: str) -> bool Print the diff and ask on the terminal whether to apply it. Returns ``False`` without prompting when stdin is not a terminal, so an unattended run can never answer the question by accident. :param diff: The rendered proposal. .. py:function:: propose_schema_update(schema_path: str | pathlib.Path, observations: dict[str, dict[str, Any]], *, device: str = '', address: str = '') -> SchemaProposal Build a schema proposal from what an instrument reported. :param schema_path: Path to the parameter schema JSON. :param observations: Per runtime key, any of ``value``, ``min_value``, ``max_value`` and ``options`` as read from the instrument. :param device: Device class name, recorded in the proposal. :param address: Instrument address, recorded in the proposal. :returns: The :class:`SchemaProposal`; empty when the schema already agrees. .. py:function:: _iter_entries(schema: dict[str, Any]) -> collections.abc.Iterator[tuple[str, dict[str, Any]]] Yield ``(runtime_key, entry)`` for every parameter, entries mutable. Handles the flat, dotted and grouped schema shapes, matching :mod:`plesty.lib.test.device_pipeline`. The yielded entry is the live dictionary from the loaded schema, so writing to it updates the schema. :param schema: The loaded schema document. .. py:function:: _schema_range(entry: dict[str, Any]) -> tuple[Any, Any] Return the ``(min, max)`` a schema entry declares, in either spelling. :param entry: One parameter entry. .. py:function:: _set_field(entry: dict[str, Any], field_name: str, value: Any) -> None Write one proposed field back into a schema entry, keeping its shape. :param entry: The parameter entry to update. :param field_name: One of the fields carried by :class:`SchemaChange`. :param value: The new value.