diff --git a/CHANGELOG.md b/CHANGELOG.md index 764994e..3a67481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [v0.9.0] - 2026-09-02 + +### Added + +- **The validate report carries both structured views, and `validate` can ask for them.** `PipelexValidationReport` gains `input_form` and `output_form`, and `validate` / `validate_files` gain a `views` parameter with `VALIDATION_VIEW_INPUT_FORM` and `VALIDATION_VIEW_OUTPUT_FORM` beside it. Neither existed here before: `input_form` shipped in `pipelex-api` at 0.18.0 and never reached this SDK, and `output_form` followed it — so a caller had no route to the two artifacts a form or a result renderer needs. + + Both are typed by importing the standard's own models (`InputForm`, `OutputForm` from `mthds.protocol`) rather than restated as opaque dicts, matching what `pipe_io_contracts` already does: one declaration per language is what makes drift impossible. + + Both are `None` rather than an empty map when absent, and that distinction carries weight here where it does not on `pipe_io_contracts` — an opt-in view's absence means the request did not ask for it, not that the method has nothing to describe. `views` is sent **only** when asked, unlike `render` which this client always injects: the point of an opt-in view is that the default response stays byte-identical, and the highest-frequency callers should not pay for bytes they discard. + +### Changed + +- **Requires `mthds` 0.13.0 (breaking).** The pin moves from 0.11.1, which predated `mthds.protocol.output_form` entirely. The version it moves to makes `json_schema` required on `PipeOutputContract`, and the contracts are CLOSED shapes — so a verdict from a runner that predates the output payload schema now fails the parse rather than being read half-way. That closure is deliberate: version drift should be loud. + ## [v0.8.0] - 2026-08-29 ### Added diff --git a/pipelex_sdk/validation_models.py b/pipelex_sdk/validation_models.py index 96c275f..e5947b7 100644 --- a/pipelex_sdk/validation_models.py +++ b/pipelex_sdk/validation_models.py @@ -45,19 +45,26 @@ from mthds.protocol.input_form import InputForm from mthds.protocol.models import InvalidValidationReport, ValidationDiagnostic, ValidationReport +from mthds.protocol.output_form import OutputForm from mthds.protocol.pipe_io_contracts import PipeIOContracts from pydantic import BaseModel, ConfigDict, Field, TypeAdapter from pipelex_sdk._pydantic_utils import empty_list_factory_of VALIDATION_VIEW_INPUT_FORM: Final[str] = "input_form" -"""The one `views` token the server supports today — asks for `PipelexValidationReport.input_form`. +"""A `views` token — asks for `PipelexValidationReport.input_form`. Deliberately a constant rather than a closed enum: the request boundary is open, the server resolves the tokens as a set and lenient-ignores the ones it does not know (never a `422`), so a stale token must never fail a call. """ +VALIDATION_VIEW_OUTPUT_FORM: Final[str] = "output_form" +"""A `views` token — asks for `PipelexValidationReport.output_form`. + +The twin of the above on the other side of the pipe. Named as a constant for the same reason: +a caller passing the literal string gets no help from a type checker when the token changes.""" + class DryRunStatus(StrEnum): """Per-pipe dry-run sweep outcome on `ValidatedPipeEntry.status`.""" @@ -359,6 +366,16 @@ class PipelexValidationReport(ValidationReport): (`VALIDATION_VIEW_INPUT_FORM`), and an older runner emitted it unconditionally — `None` by default is the one typing that reads a body from either runner correctly.""" + output_form: OutputForm | None = None + """Opt-in structured view: the per-pipe output-form descriptors, the twin of `input_form` on + the other side of the pipe, requested through `views: ["output_form"]`. + + One `field` rather than a list of them, and no `presence` or `gating` — those are facts of a + slot a caller fills, and a result is not one. Read together with that pipe's + `output.json_schema` off `pipe_io_contracts`: the descriptor says what the result IS, the + schema names the property its payload arrives under, and a consumer holding one but not the + other is back to inferring the other from the value.""" + rendered_markdown: str | None = None """Opt-in Pipelex-API presentation extra: the server-rendered Markdown view of the verdict, present only when the request asked for it (`render: ["markdown"]`); absent (None) otherwise.""" diff --git a/pyproject.toml b/pyproject.toml index 23f7b6a..6fbe71d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pipelex-sdk" -version = "0.8.0" +version = "0.9.0" description = "The Python client for the Pipelex hosted API — the MTHDS Protocol surface plus the durable run lifecycle and the Pipelex product surface, built on the `mthds` protocol base." authors = [{ name = "Evotis S.A.S.", email = "oss@pipelex.com" }] maintainers = [{ name = "Pipelex staff", email = "oss@pipelex.com" }] @@ -18,7 +18,7 @@ classifiers = [ ] dependencies = [ - "mthds==0.11.1", + "mthds==0.13.0", "pydantic>=2.10.6,<3.0.0", "typing-extensions>=4.0.0", "httpx>=0.23.0,<1.0.0", @@ -348,6 +348,7 @@ convention = "google" [tool.uv] required-version = ">=0.7.2" + [tool.pylint.main] py-version = "3.11" reports = false diff --git a/tests/unit/test_validation_contract.py b/tests/unit/test_validation_contract.py index b009a00..0044989 100644 --- a/tests/unit/test_validation_contract.py +++ b/tests/unit/test_validation_contract.py @@ -61,6 +61,11 @@ "multiplicity": "single", "item_count": None, "optional": False, + # Required on the contract since the output side gained a payload + # schema. The empty schema rather than a plausible one: this fixture + # is about the ENVELOPE parsing, and a shape here would invite an + # assertion that belongs in a test about schemas. + "json_schema": {}, }, } }, diff --git a/uv.lock b/uv.lock index aa5f335..1a38edf 100644 --- a/uv.lock +++ b/uv.lock @@ -212,7 +212,7 @@ wheels = [ [[package]] name = "mthds" -version = "0.11.1" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -221,9 +221,9 @@ dependencies = [ { name = "tomlkit" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/9a/93d688320010ad14a31c863c3882fabc2f8637622f69fd8e4aed64a3f2bb/mthds-0.11.1.tar.gz", hash = "sha256:5f52b703835abe9e40a000d76023e2bd29124655c049b1ef9eeab4a8761b6e2c", size = 170790, upload-time = "2026-08-28T11:17:02.146Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/6c67b86693d01d473444523db0aeadc1afdffaa1748a264dfab9c1c77b1c/mthds-0.13.0.tar.gz", hash = "sha256:ef59ffa94902da72371390cc39d8c0d85dfd2a524dbca2a8909a0a1ba6255607", size = 216142, upload-time = "2026-09-02T15:09:15.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/1c/74abc1b8b63813c0de69218b003e777cc92a7327d21b90911c10502b2e69/mthds-0.11.1-py3-none-any.whl", hash = "sha256:760cd7d3f7e86b8f962e1d3a4e87e6146b94968b9422ddfc22e006a3fc305ba3", size = 71954, upload-time = "2026-08-28T11:17:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/89/26/f73e625b54cfba2e09a29df5e7173e508c25d3f7ce106be69fae6065d76c/mthds-0.13.0-py3-none-any.whl", hash = "sha256:606fef7978bbc8a5b249882c0b9377fed37123f618f8183f29e89b71a4eadac0", size = 85993, upload-time = "2026-09-02T15:09:14.334Z" }, ] [[package]] @@ -303,7 +303,7 @@ wheels = [ [[package]] name = "pipelex-sdk" -version = "0.8.0" +version = "0.9.0" source = { editable = "." } dependencies = [ { name = "httpx" }, @@ -326,7 +326,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, - { name = "mthds", specifier = "==0.11.1" }, + { name = "mthds", specifier = "==0.13.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = "==1.19.1" }, { name = "pydantic", specifier = ">=2.10.6,<3.0.0" }, { name = "pylint", marker = "extra == 'dev'", specifier = "==4.0.4" },