infoxtractor/src/ix/contracts/job.py
Dirk Riemann 02db3b05cc
All checks were successful
tests / test (push) Successful in 1m2s
tests / test (pull_request) Successful in 1m0s
feat(contracts): ResponseIX + Provenance + Job envelope (spec §3, §9.3)
Completes the data-contract layer. Highlights:

- `ResponseIX.context` is an internal mutable accumulator used by pipeline
  steps (pages, files, texts, use_case classes, segment index). It MUST NOT
  leak into the serialised response, so we mark the field with
  `Field(exclude=True)` and carry the shape in a small `_InternalContext`
  sub-model with `extra="allow"` so steps can stash arbitrary state without
  schema churn. Tested: `model_dump()` and `model_dump_json()` both drop it.

- `FieldProvenance` gains `provenance_verified: bool | None` and
  `text_agreement: bool | None` — the two MVP reliability flags written by
  the new ReliabilityStep. Both default None so rows predating the
  ReliabilityStep (empty LLM output, cloud-import replay) parse cleanly.

- `quality_metrics` stays a free-form `dict[str, Any]` — the MVP adds
  `verified_fields` and `text_agreement_fields` counters without carving
  them into the schema, which keeps future metric additions free.

- `Job.status` and `Job.callback_status` are `Literal[...]` so Pydantic
  rejects unknown states at the edge. Invariant
  (`status='done' iff response.error is None`) stays worker-enforced —
  callers sometimes hydrate in-flight rows and we do not want validation
  to reject them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 10:50:22 +02:00

46 lines
1.4 KiB
Python

"""Job envelope stored in ``ix_jobs`` and returned by REST.
Mirrors spec §3 ("Job envelope") and §4 ("Job store"). The lifecycle
enum is a ``Literal`` so Pydantic rejects unknown values at parse time.
``callback_status`` is nullable until the worker attempts delivery (or
skips delivery when there's no ``callback_url``).
"""
from __future__ import annotations
from datetime import datetime
from typing import Literal
from uuid import UUID
from pydantic import BaseModel, ConfigDict
from ix.contracts.request import RequestIX
from ix.contracts.response import ResponseIX
JobStatus = Literal["pending", "running", "done", "error"]
CallbackStatus = Literal["pending", "delivered", "failed"]
class Job(BaseModel):
"""Row of ``ix_jobs`` + its request/response bodies.
The invariant ``status='done' iff response.error is None`` is enforced by
the worker, not here — callers occasionally hydrate a stale or in-flight
row and we don't want the Pydantic validator to reject it.
"""
model_config = ConfigDict(extra="forbid")
job_id: UUID
ix_id: str
client_id: str
request_id: str
status: JobStatus
request: RequestIX
response: ResponseIX | None = None
callback_url: str | None = None
callback_status: CallbackStatus | None = None
attempts: int = 0
created_at: datetime
started_at: datetime | None = None
finished_at: datetime | None = None