app.services.decisions¶
Formal accept/reject decisions (POST /api/v1/records/{id}/decisions).
A decision binds to the record’s logical identity and the digest of its
current content at request time; the server sets the event id and
timestamp. Checks, in this order (the same rules give the detail
page’s can_decide through decision_blocker()):
the record exists -> else 404
record_not_found;the approval policy is configured -> else 409
policy_missing;the policy requires approval -> else 409
approval_not_required;the caller is a policy reviewer (
issuer|subjector a role) -> else 403not_eligible;the content is approvable (valid UUID, valid UTF-8) -> else 409
not_approvable;not a self-approval (when the policy disallows it) -> else 403
self_approval. Self-approval means the reviewer’s verified email equals the recordauthor(case-insensitive), or the author equals the reviewer’sissuer|subjectexactly. Records without an author (polled filesystem/GitLab rows) cannot be detected;expected_digest, when sent, equals the current digest -> else 409digest_changed(the reviewer never decides content they did not see);no decision exists for (identity, digest) -> else 409
already_decided(also on a concurrent race, through the unique constraint).
Events are append-only (database trigger); nothing here updates or deletes one.
Public API: Reviewer, DecisionBlocker,
Approvable, approval_blocker(), decision_blocker(),
record_decision().
- class app.services.decisions.Approvable(*args, **kwargs)¶
Bases:
ProtocolThe fields of a stored row that decide whether it is approvable.
- class app.services.decisions.DecisionBlocker(*, code, message, hint, forbidden=False)¶
Bases:
objectWhy the caller cannot decide a record now.
- Parameters:
- to_error()¶
Convert to the domain error raised by the decision endpoint.
- Return type:
- Returns:
A
ForbiddenErrororConflictError.
- class app.services.decisions.Reviewer(*, issuer, subject, display, email, roles)¶
Bases:
objectThe caller of a decision, from verified token claims.
- Parameters:
- app.services.decisions.approval_blocker(record)¶
Explain why a stored row’s content cannot be decided at all.
Mirrors
app.domain.records.ParsedRecord.approval_blocker.- Parameters:
record (
Approvable) – The row.- Return type:
- Returns:
A reader-facing reason, or
Noneif approvable.
- app.services.decisions.decision_blocker(record, events, policy_config, reviewer, *, expected_digest=None)¶
Tell why reviewer cannot decide record now, if they cannot.
- Parameters:
record (
StoredRecord) – The record row.events (
Sequence[DecisionEvent]) – Every decision event of the record’s identity.policy_config (
PolicyConfig|None) – The approval policy status, orNone.reviewer (
Reviewer) – The caller.expected_digest (
str|None) – Digest the caller saw, if sent.
- Return type:
- Returns:
The first failing check (module docstring), or
None.
- app.services.decisions.record_decision(provider, record_id, *, action, comment, expected_digest, reviewer, policy_config)¶
Append an accept/reject decision for a record’s current content.
- Parameters:
provider (
CatalogStoreProvider) – Storage provider.record_id (
UUID) – Record id.action (
DecisionAction) – Accepted or rejected.comment (
str|None) – Optional reviewer comment (blank means none).expected_digest (
str|None) – Digest the reviewer saw, if sent.reviewer (
Reviewer) – The caller.policy_config (
PolicyConfig|None) – The approval policy status.
- Return type:
- Returns:
The stored event.
- Raises:
NotFoundError –
record_not_found.ForbiddenError –
not_eligible/self_approval.ConflictError –
policy_missing,approval_not_required,not_approvable,digest_changed,already_decided.