Configurator¶
These modules cover typed oa-configurator 1.x inspection and the provider-neutral write flow. Start with the configuration guide for the analyst experience, provider responsibilities, workflow composition, and secret boundary.
Read-only adapter over the public shape of oa-configurator stack models.
OAConfiguratorAdapter ¶
Build safe, read-only views from an oa-configurator 1.x stack.
Core sections are read from the public StackConfig and concrete pydantic models.
[tools.*] sections are typed from the omop.config entry-point registry, so a
package that registers a PackageConfigBase gets the same sensitivity and reference
inspection without the caller doing anything. A caller may still pass resolved
instances, which win over the registry. Editable candidates and persistence remain
outside groundskeeping.
A section with no usable schema is rendered by shape only -- see
:meth:_untyped_entry for why that is the safe reading rather than the cautious one.
Textual-free models for presenting stack configuration.
ConfigReferenceStatus ¶
ConfigReferenceView
dataclass
¶
ConfigReferenceView(
section: ConfigTargetKind,
name: str,
status: ConfigReferenceStatus,
expected_type: str,
actual_type: str | None = None,
)
Presentation-safe description of one RefTo field.
ConfigSectionView
dataclass
¶
ConfigSectionView(
target: ConfigTarget,
fields: Mapping[str, object] = dict(),
children: tuple[ConfigSectionView, ...] = (),
notes: tuple[str, ...] = (),
)
Read-only view of one configuration section.
fields
class-attribute
instance-attribute
¶
fields: Mapping[str, object] = field(default_factory=dict)
ConfigTarget
dataclass
¶
ConfigTarget(
kind: ConfigTargetKind,
key: str,
title: str,
status: SemanticStatus = SemanticStatus.INFO,
)
ConfigTargetKind ¶
Bases: StrEnum
Stable kinds a configuration provider may expose for inspection.
ConfiguratorSnapshot
dataclass
¶
ConfiguratorSnapshot(
title: str,
path: str | None,
sections: tuple[ConfigSectionView, ...],
)
Read-only snapshot of an effective oa-configurator stack.
path records where the inspected configuration came from. It is display
metadata only and must not be interpreted as an instruction to write there.
Provider-neutral contracts for safe configuration write flows.
Groundskeeping owns the operator sequence, not the configuration candidate. A mutation service keeps real candidate state behind an opaque session token and returns only presentation-safe plans and results. Submitted values are method arguments rather than dataclass fields so they cannot accidentally settle in snapshots, reprs, or widget state.
ConfigApplyIntent
dataclass
¶
ConfigApplyIntent(
target: ConfigTarget,
operation: MutationOperation,
apply_token: str,
expected_revision: str | None,
)
ConfigApplyResult
dataclass
¶
ConfigApplyResult(
status: ConfigApplyStatus,
summary: str,
detail: str | None = None,
refresh_pages: frozenset[str] = frozenset(),
)
Portable outcome returned by a mutation provider.
summary and detail are rendered to the operator and written to application
logs, so both must be presentation-safe: no submitted values, no secrets, no
provider tracebacks, no absolute paths a host would not otherwise disclose.
summary says what happened in one line. detail carries the operator's next
action and should be present whenever the status is not APPLIED — "Reload the
configuration and review the change again" for a conflict, "Grant write access to
the configuration directory and retry" for a failure. Omit it when there is nothing
useful to add; do not restate summary.
refresh_pages names the host page keys whose data this change invalidated.
ConfigApplyStatus ¶
Bases: StrEnum
How one apply attempt ended.
A host renders different remediation for each member, so a provider must classify its errors rather than reach for the nearest label. Two questions separate the four: was anything written, and if not, was the request itself at fault?
| Status | Meaning | Typical cause |
|---|---|---|
APPLIED |
The change was persisted. | — |
CONFLICTED |
The stored configuration changed after the plan was prepared; the expected revision no longer matches. Nothing was written. | Another writer saved between plan and apply. |
REJECTED |
The request itself was not acceptable, and no write was attempted. | Consumed or unknown apply token, intent not matching the prepared plan, candidate failing validation, ownership or policy forbidding the write. |
FAILED |
The write was attempted and errored. The previous configuration remains authoritative. | Filesystem permissions, disk full, I/O error, serialisation failure. |
APPLIED
class-attribute
instance-attribute
¶
APPLIED = 'applied'
The change was persisted; the stored configuration now reflects it.
CONFLICTED
class-attribute
instance-attribute
¶
CONFLICTED = 'conflicted'
The stored revision moved after planning. Nothing was written.
FAILED
class-attribute
instance-attribute
¶
FAILED = 'failed'
The write was attempted and errored; the previous configuration stands.
REJECTED
class-attribute
instance-attribute
¶
REJECTED = 'rejected'
The request was not acceptable, and no write was attempted.
ConfigDiff
dataclass
¶
ConfigDiff(
target: ConfigTarget,
entries: tuple[ConfigDiffEntry, ...],
)
ConfigDiffEntry
dataclass
¶
ConfigDiffEntry(
field: str,
before: object,
after: object,
sensitive: bool = False,
)
One presentation-safe change in a configuration plan.
ConfigDraft
dataclass
¶
ConfigDraft(
target: ConfigTarget,
operation: MutationOperation,
session_token: str,
changed_fields: frozenset[str] = frozenset(),
expected_revision: str | None = None,
)
Safe identity and progress for provider-owned candidate state.
ConfigMutationService ¶
Bases: Protocol
Provider boundary for configuration candidate state and persistence.
Implementations must consume an apply token at the start of every apply attempt,
including conflict, rejection, and failure. submit may inspect real values only
for the duration of the call; the service owns any candidate state retained behind
draft.session_token. Returned objects must be safe to render and log.
Every method may raise :class:UnavailableMutationService when the provider as a
whole cannot serve requests. begin() additionally raises
:class:MutationOperationUnsupported to refuse one operation. Any other exception
is treated as a provider defect: the controller logs it without its message and
shows the operator a generic failure, so a condition a host should act on must use
one of the two typed exceptions.
begin ¶
begin(
target: ConfigTarget, operation: MutationOperation
) -> ConfigDraft
Open a provider-owned candidate session and capture its base revision.
Raises:
| Type | Description |
|---|---|
MutationOperationUnsupported
|
The operation is not available for this target — already exists, does not exist, read-only, or forbidden by policy. The message is shown to the operator. |
UnavailableMutationService
|
The provider cannot serve requests at all. |
capabilities ¶
capabilities(
target: ConfigTarget, operation: MutationOperation
) -> MutationCapabilities
Report whether one operation is available for one target.
This is the non-raising way to ask what begin() would do. It still raises
:class:UnavailableMutationService when the provider cannot answer at all.
submit ¶
submit(
draft: ConfigDraft,
step_key: str,
values: Mapping[str, object],
*,
discard_fields: frozenset[str] = frozenset(),
) -> ConfigStepResult
ConfigPlan
dataclass
¶
ConfigPlan(
target: ConfigTarget,
operation: MutationOperation,
diff: ConfigDiff,
effects: tuple[EffectRef, ...] = (),
issues: tuple[ValidationIssue, ...] = (),
warnings: tuple[str, ...] = (),
apply_token: str | None = None,
expected_revision: str | None = None,
)
Provider-produced, presentation-safe plan for an apply attempt.
ConfigStepResult
dataclass
¶
ConfigStepResult(
issues: tuple[ValidationIssue, ...] = (),
changed_fields: frozenset[str] = frozenset(),
future_fields: tuple[FieldSpec, ...] = (),
)
Presentation-safe outcome of staging one wizard step.
changed_fields is the complete current set for the provider session, not only
the fields submitted in this call. This lets a provider account for defaults,
unchanged updates, and branch invalidation without exposing candidate values.
future_fields may replace presentation descriptors for later, uncompleted
workflow fields after an accepted step. The controller rejects callbacks, changes
to completed fields, and changes that weaken a field's kind or sensitivity.
EffectRef
dataclass
¶
EffectRef(
impact_kind: str,
source_target: ConfigTarget,
label: str,
destination_target: ConfigTarget | None = None,
field_key: str | None = None,
status: SemanticStatus = SemanticStatus.INFO,
)
A structural, presentation-safe impact between configuration targets.
destination_target
class-attribute
instance-attribute
¶
destination_target: ConfigTarget | None = None
MutationCapabilities
dataclass
¶
MutationCapabilities(
target: ConfigTarget,
operation: MutationOperation,
supported: bool,
reason: str | None = None,
)
MutationOperation ¶
MutationOperationUnsupported ¶
Bases: ValueError
This provider will not begin this operation on this target.
Raise this from begin() when the operation is legitimately unavailable — the
entry already exists and the provider only creates, the entry does not exist and
the provider only updates, the configuration is read-only, or policy forbids the
change. The message is shown to the operator, so it must be presentation-safe and
say why.
It subclasses ValueError so hosts that already catch ValueError around
begin() keep working, but a typed refusal lets a host separate "not available
right now" from a programming error it should surface as a bug.
capabilities() answers the same question without raising, and a host that calls
it first will normally never see this. Providers should raise it anyway: a
capability answer can go stale between the check and the call.
UnavailableMutationService ¶
Bases: RuntimeError
The provider exists but cannot currently serve mutation requests.
Raise this when the whole service is out of action — an unreadable or malformed
configuration file, a backing store that cannot be reached. It is not the way to
refuse one operation; see :class:MutationOperationUnsupported.
build_config_diff ¶
build_config_diff(
target: ConfigTarget,
original_fields: Mapping[str, object],
candidate_fields: Mapping[str, object],
*,
sensitive_fields: frozenset[str] = frozenset(),
) -> ConfigDiff
Build a diff after replacing every declared sensitive value.
Both mappings must come from the same projection of the same configuration shape. A field absent from one side and present on the other is reported as a change, so projecting the two sides differently produces a diff that is wrong in both directions.
The usual way to get this wrong is to normalise only one side — flattening the
stored base with something like exclude_none=True while the candidate comes
back from a validator with every default materialised. Every defaulted field then
appears as None -> <default>. Flatten both sides through the same call, with
the same options, before calling this function.
sensitive_fields is the set of field keys whose values must never be rendered;
both sides of such an entry are replaced with :class:RedactedValue, which also
means a sensitive field that changed is still reported as a change.
resolve_operation ¶
resolve_operation(
service: ConfigMutationService, target: ConfigTarget
) -> MutationOperation
Return the operation a host should use for target: update it, or create it.
A :class:~groundskeeping.configurator.controller.ConfigWorkflowSpec is built for
one fixed operation, so a host that offers a single "Configure" action must decide
which one before constructing the controller. This encodes that decision once:
UPDATE when the provider supports updating this target, CREATE otherwise.
operation = resolve_operation(service, target)
controller = ConfigWizardController(workflow(operation), service)
Checking UPDATE first is deliberate. A provider that supports both reports both
as supported, and updating an entry the operator already has is the safer default.
A host that wants create-only or update-only behaviour should keep passing an explicit operation instead of calling this — the controller then blocks with the provider's own reason when that operation is unsupported, which is the correct outcome for a "Create database" action aimed at a database that already exists.
Raises:
| Type | Description |
|---|---|
UnavailableMutationService
|
The provider cannot answer capability questions. |
Generic configuration wizard built over :mod:.mutation contracts.
The workflow is declarative: applications group provider field keys into ordered steps and attach simple equality conditions for branches. Groundskeeping owns safe navigation state and branch recalculation. Providers own validation, candidate values, planning, and apply semantics. There is intentionally no callback-shaped branching API and no provider candidate object crosses this module.
ConfigBranchCondition
dataclass
¶
ConfigBranchCondition(
field_key: str,
values: frozenset[object],
negated: bool = False,
)
ConfigWizardController ¶
ConfigWizardController(
workflow: ConfigWorkflowSpec,
service: ConfigMutationService,
)
Drive one provider-owned mutation session through the generic wizard UI.
spec
instance-attribute
¶
spec = WizardSpec(
key=workflow.key,
title=workflow.title,
purpose=workflow.purpose,
apply_label=workflow.apply_label,
)
ConfigWorkflowSpec
dataclass
¶
ConfigWorkflowSpec(
key: str,
target: ConfigTarget,
operation: MutationOperation,
title: str,
purpose: str,
steps: tuple[ConfigWorkflowStep, ...],
apply_label: str = "Apply",
)
Application-owned copy and branching for a generic write flow.
ConfigWorkflowStep
dataclass
¶
ConfigWorkflowStep(
key: str,
title: str,
field_keys: tuple[str, ...],
purpose: str | None = None,
kind: ConfigWorkflowStepKind = ConfigWorkflowStepKind.FORM,
when: tuple[ConfigBranchCondition, ...] = (),
)
Declarative grouping of provider fields into one wizard step.
kind
class-attribute
instance-attribute
¶
kind: ConfigWorkflowStepKind = ConfigWorkflowStepKind.FORM
Reusable lifecycle assertions for external configuration providers.
InvalidSubmissionHook
module-attribute
¶
InvalidSubmissionHook = Callable[
[ConfigMutationService, ConfigDraft], ConfigStepResult
]
MutationServiceFactory
module-attribute
¶
MutationServiceFactory = Callable[[], ConfigMutationService]
MutationServiceHook
module-attribute
¶
MutationServiceHook = Callable[
[ConfigMutationService], None
]
MutationConformanceError ¶
Bases: AssertionError
A provider did not honour an observable mutation-service guarantee.
MutationConformanceHooks
dataclass
¶
MutationConformanceHooks(
invalid_submission: InvalidSubmissionHook | None = None,
expected_invalid_fields: frozenset[
str | None
] = frozenset(),
advance_revision: MutationServiceHook | None = None,
prepare_warning: MutationServiceHook | None = None,
prepare_plan_error: MutationServiceHook | None = None,
prepare_rejection: MutationServiceHook | None = None,
prepare_failure: MutationServiceHook | None = None,
make_unavailable: MutationServiceHook | None = None,
prepare_unsupported: MutationServiceHook | None = None,
unsupported_operation: MutationOperation | None = None,
)
Provider-specific fault injection for the portable lifecycle suite.
Hooks receive a fresh service instance. They should alter external state or a test
double's next outcome without returning candidate values. A production provider test
commonly uses advance_revision to perform an out-of-band write after planning.
advance_revision
class-attribute
instance-attribute
¶
advance_revision: MutationServiceHook | None = field(
default=None, repr=False, compare=False
)
expected_invalid_fields
class-attribute
instance-attribute
¶
expected_invalid_fields: frozenset[str | None] = frozenset()
invalid_submission
class-attribute
instance-attribute
¶
invalid_submission: InvalidSubmissionHook | None = field(
default=None, repr=False, compare=False
)
make_unavailable
class-attribute
instance-attribute
¶
make_unavailable: MutationServiceHook | None = field(
default=None, repr=False, compare=False
)
prepare_failure
class-attribute
instance-attribute
¶
prepare_failure: MutationServiceHook | None = field(
default=None, repr=False, compare=False
)
prepare_plan_error
class-attribute
instance-attribute
¶
prepare_plan_error: MutationServiceHook | None = field(
default=None, repr=False, compare=False
)
prepare_rejection
class-attribute
instance-attribute
¶
prepare_rejection: MutationServiceHook | None = field(
default=None, repr=False, compare=False
)
prepare_unsupported
class-attribute
instance-attribute
¶
prepare_unsupported: MutationServiceHook | None = field(
default=None, repr=False, compare=False
)
prepare_warning
class-attribute
instance-attribute
¶
prepare_warning: MutationServiceHook | None = field(
default=None, repr=False, compare=False
)
unsupported_operation
class-attribute
instance-attribute
¶
unsupported_operation: MutationOperation | None = None
assert_mutation_service_conformance ¶
assert_mutation_service_conformance(
service_factory: MutationServiceFactory,
target: ConfigTarget,
operation: MutationOperation,
submissions: Sequence[tuple[str, Mapping[str, object]]],
*,
hooks: MutationConformanceHooks | None = None,
secret_canary: str | None = None,
) -> None
Exercise supported lifecycle behavior against isolated provider instances.
The core assertions cover capability discovery, begin/fields/stage/plan/apply, single-use tokens, diff projection symmetry, and cancellation. Supplying hooks additionally proves validation, warning/non-ready planning, stale revision conflict, rejection, operational failure, unavailability, and unsupported-operation behavior. Values remain transient method arguments and are never included in conformance errors.
submissions must reach a valid candidate; they are staged twice against the same
provider instance so the suite can require that restaging applied values reports no
change. A provider that stops supporting operation once the entry exists — a
create-only provider, most commonly — is not asked that question, so run the suite
for UPDATE as well as CREATE when the provider supports both.