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.