From 6b856f6c59d45f023641672f567aace84a209b15 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Sun, 27 Sep 2026 12:17:52 +0200 Subject: [PATCH] Pin mthds==0.16.0, the version pipelex pins exactly pipelex pins mthds==0.16.0 exactly, so pipelex-sdk's exact 0.15.0 pin made the two packages impossible to install together. mthds 0.16.0 adds a User-Agent builder to MthdsAPIClient, which declares app_info with its own AppInfo type; PipelexAPIClient keeps its own model and builder for now, so one narrow type ignore covers that attribute, and the docs say the base's seam is not adopted yet. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + docs/architecture.md | 2 +- docs/client-identification.md | 2 +- pipelex_sdk/client.py | 5 ++++- pipelex_sdk/user_agent.py | 4 ++-- pyproject.toml | 2 +- uv.lock | 8 ++++---- 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb735ba..c2a99bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - **`RunErrorReport` carries every field of the runner's report and moves to `pipelex_sdk.error_models` (Breaking)**: import it from `pipelex_sdk.error_models` instead of `pipelex_sdk.product_models`. Beside `message` and `error_type` it now declares `title`, `type_uri`, `error_domain`, `error_category`, `retryable`, `user_action`, `model`, `provider`, `provider_metadata`, `caller_facing_message`, `validation_errors` and `migration`, every one optional, the model open to fields the runner adds, and each field read leniently — a value that does not fit its type reads as `None` rather than failing the status read, the run list or the results read that carries the report — so `PipelineRun.error` in the run lists reads the whole report too. - **`RunRead.error` is the typed report, no longer a raw dict (Breaking)**: `error` is now declared on `RunPublic`, so the status read's report is a `RunErrorReport` rather than the dict that rode `model_extra`; read `run.error.message` where code read `run.error["message"]` or `run.model_extra["error"]`. - **A failed run's status comes from the results read's `run_status` member (Breaking)**: `get_run_result` no longer parses the status out of the `409`'s `detail` sentence; it reads the problem document's `run_status` member, and a `409` without a status this SDK knows reads as `FAILED`. +- **Requires `mthds` 0.16.0 (Breaking)**: the exact pin moves from 0.15.0 to the version `pipelex` pins exactly, so `pipelex-sdk` and `pipelex` can be installed together again. Nothing in this client's own surface changes: `PipelexAPIClient` keeps building its `User-Agent` with `pipelex_sdk.user_agent` and its own `AppInfo`, and does not yet use the builder that `mthds` 0.16.0 adds to `MthdsAPIClient`. ## [v0.12.0] - 2026-09-24 diff --git a/docs/architecture.md b/docs/architecture.md index 3616b53..0cd8ccb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -292,7 +292,7 @@ Each stays deferred rather than silently missing. Everything else — the protoc **The run-results surface** — `RunResults` tracks `@pipelex/sdk` 0.20.0 field for field, and is at parity with it. Every field is declared, with the two-path semantics [`run-results.md`](./run-results.md) states: `graph_spec` (lifted on the blocking path instead of written `None`), `graph_assembly_error`, the three I/O artifacts typed from `mthds.protocol` with `pipe_io_artifacts_error` beside them, the usage pair, `working_memory` (the hosted artifact relayed as its own key, the blocking one lifted off `pipe_output.working_memory`), and `pipe_output`; and the usage pair's fold, `summarize_usage`, with the summary types it returns. What stays different is idiomatic and deliberate: a key the hosted body did not carry is absent from `model_fields_set` where the JS reads `undefined`, and the page says how to read that. -**Models** — field-for-field across the run-lifecycle types and the product wire models. Deliberate idiomatic ports (not gaps): milliseconds → seconds (`interval_seconds` / `timeout_seconds` / `elapsed_seconds`); the JS `AbortSignal` → Python `asyncio` cancellation (no `signal` field, and no `aborted` flag on the download verdict — a cancelled `download_artifacts` raises `CancelledError` with its partial files already unlinked); the JS artifact request object → `DownloadArtifactsOptions` beside an explicit `dir_path` (`dir` being a Python builtin) and the raw bulk call named `resolve_storage_urls_bulk` after its route rather than `resolveStorageUrls`, so it cannot be misread as the single-reference `resolve_storage_url`; the returned `Response` of `fetchArtifact` → an async context manager, because an httpx stream is only live inside its own block; JS inline string-unions promoted to `StrEnum`s (`OrgRole`, `PipeStatus`, the onboarding fields) with identical wire values; response models are `extra="allow"` for forward-compat. The Pipelex validation narrowing is **owned here** (`pipelex_sdk.validation_models`), narrowing `mthds`'s neutral verdict bases (the resolved follow-up #9); the brand-neutral `Dict*` wire concretes (`DictRunResultExecute`, and the `DictPipeOutputAbstract` / `DictWorkingMemoryAbstract` pair that types `RunResults.pipe_output` and `RunResults.working_memory`) are reused from `mthds` by inheritance or by import — they are a shared wire contract the `pipelex` runtime also builds on — rather than duplicated as `pipelex-sdk-js` does. One addition runs ahead of the JS SDK: `write_codegen_tree` has no `@pipelex/sdk` counterpart, because the JS writer lives inside `pipelex-starter-js`'s own harness, mixed in with project policy; the byte-fidelity contract is small and load-bearing enough to belong to the SDK, where every consumer shares one correct implementation. The drift check goes the other way and takes a different shape on purpose: `@pipelex/sdk`'s `runCodegenCheck` is **pure** — the caller walks its own tree and hands in the text — because that module must stay free of Node builtins for a browser bundle, and its doc consequently loads the caller with obligations (walk the whole tree, do not reformat, decode strictly) whose every breach yields a wrong verdict rather than an error. Python has no such constraint, this package already does filesystem work, and `pipelex`'s own surface takes a root — so `run_codegen_check(root=…)` takes one too. Every caller obligation becomes the library's, the verdict is provable against `pipelex` by calling both with the same directory, and it composes with `write_codegen_tree(report, output_dir=…)` as the same path in and out. Two divergences worth naming: the page envelopes keep the wire's snake_case `next_cursor`, where the JS mirror renamed it `nextCursor` for its own consumers; and the method-files catalog converter (`parse_method_files` / `serialize_method_files`) lives in this package, where the JS pair lives in `mthds-js` because `pipelex-mcp` consumes the same format and wanted one owner. There is no second Python consumer, and the catalog serialization is a Pipelex product concern rather than an MTHDS protocol one, so this SDK is a proper home for it. `mthds` has since grown the canonical pair as `mthds.protocol.method_files`, shipped in the `mthds==0.15.0` this package pins exactly, and the local pair is kept beside it on purpose rather than left over. Adoption is a step of its own rather than a rename: that parser raises `PipelineRequestError`, which does not subclass `ValueError`, and pydantic converts only `ValueError` out of a validator — so re-pointing `MethodData`'s validator at it would stop every caller's `except ValidationError` from catching a malformed stored source. The exception's base class is `mthds`'s to settle, and it is settled first; until then the two implementations are kept in step, and they disagree on blankness (this one is Python's `str.strip`, the canonical one is ECMAScript's), on the serialized bytes (default separators and ASCII escapes here, `JSON.stringify`'s there), and on the JSON constants and integer-literal cap the canonical one closes with `parse_constant=` and `parse_int=float`. +**Models** — field-for-field across the run-lifecycle types and the product wire models. Deliberate idiomatic ports (not gaps): milliseconds → seconds (`interval_seconds` / `timeout_seconds` / `elapsed_seconds`); the JS `AbortSignal` → Python `asyncio` cancellation (no `signal` field, and no `aborted` flag on the download verdict — a cancelled `download_artifacts` raises `CancelledError` with its partial files already unlinked); the JS artifact request object → `DownloadArtifactsOptions` beside an explicit `dir_path` (`dir` being a Python builtin) and the raw bulk call named `resolve_storage_urls_bulk` after its route rather than `resolveStorageUrls`, so it cannot be misread as the single-reference `resolve_storage_url`; the returned `Response` of `fetchArtifact` → an async context manager, because an httpx stream is only live inside its own block; JS inline string-unions promoted to `StrEnum`s (`OrgRole`, `PipeStatus`, the onboarding fields) with identical wire values; response models are `extra="allow"` for forward-compat. The Pipelex validation narrowing is **owned here** (`pipelex_sdk.validation_models`), narrowing `mthds`'s neutral verdict bases (the resolved follow-up #9); the brand-neutral `Dict*` wire concretes (`DictRunResultExecute`, and the `DictPipeOutputAbstract` / `DictWorkingMemoryAbstract` pair that types `RunResults.pipe_output` and `RunResults.working_memory`) are reused from `mthds` by inheritance or by import — they are a shared wire contract the `pipelex` runtime also builds on — rather than duplicated as `pipelex-sdk-js` does. One addition runs ahead of the JS SDK: `write_codegen_tree` has no `@pipelex/sdk` counterpart, because the JS writer lives inside `pipelex-starter-js`'s own harness, mixed in with project policy; the byte-fidelity contract is small and load-bearing enough to belong to the SDK, where every consumer shares one correct implementation. The drift check goes the other way and takes a different shape on purpose: `@pipelex/sdk`'s `runCodegenCheck` is **pure** — the caller walks its own tree and hands in the text — because that module must stay free of Node builtins for a browser bundle, and its doc consequently loads the caller with obligations (walk the whole tree, do not reformat, decode strictly) whose every breach yields a wrong verdict rather than an error. Python has no such constraint, this package already does filesystem work, and `pipelex`'s own surface takes a root — so `run_codegen_check(root=…)` takes one too. Every caller obligation becomes the library's, the verdict is provable against `pipelex` by calling both with the same directory, and it composes with `write_codegen_tree(report, output_dir=…)` as the same path in and out. Two divergences worth naming: the page envelopes keep the wire's snake_case `next_cursor`, where the JS mirror renamed it `nextCursor` for its own consumers; and the method-files catalog converter (`parse_method_files` / `serialize_method_files`) lives in this package, where the JS pair lives in `mthds-js` because `pipelex-mcp` consumes the same format and wanted one owner. There is no second Python consumer, and the catalog serialization is a Pipelex product concern rather than an MTHDS protocol one, so this SDK is a proper home for it. `mthds` has since grown the canonical pair as `mthds.protocol.method_files`, shipped since `mthds` 0.15.0 and present in the `mthds==0.16.0` this package pins exactly, and the local pair is kept beside it on purpose rather than left over. Adoption is a step of its own rather than a rename: that parser raises `PipelineRequestError`, which does not subclass `ValueError`, and pydantic converts only `ValueError` out of a validator — so re-pointing `MethodData`'s validator at it would stop every caller's `except ValidationError` from catching a malformed stored source. The exception's base class is `mthds`'s to settle, and it is settled first; until then the two implementations are kept in step, and they disagree on blankness (this one is Python's `str.strip`, the canonical one is ECMAScript's), on the serialized bytes (default separators and ASCII escapes here, `JSON.stringify`'s there), and on the JSON constants and integer-literal cap the canonical one closes with `parse_constant=` and `parse_int=float`. **Errors** — `ApiResponseError`, `ApiUnreachableError`, `PipelineExecuteTimeoutError`, `RunFailedError`, `RunTimeoutError`, `RunLifecycleUnavailableError`, `PagingNotTerminatingError` are owned here; `RunStillRunningError` is re-exported from `mthds`. `ClientAuthenticationError` is **not** ported: it is a dormant export in the JS barrel (defined and exported but never raised by the client), and in Python it already lives in `mthds.runners.api.exceptions` — importable directly if ever needed, with no barrel here to re-export it through. diff --git a/docs/client-identification.md b/docs/client-identification.md index 8f441ff..c48c6aa 100644 --- a/docs/client-identification.md +++ b/docs/client-identification.md @@ -45,4 +45,4 @@ Do not put a secret, a user identifier, an email address or a hostname in `app_i ## Relation to `mthds` -The spec places the header builder of the `mthds` library in `mthds.runners.api.user_agent`. The `mthds` version this SDK pins does not ship it yet, so `pipelex_sdk.user_agent` builds the whole header itself and mirrors the public shape the `mthds` builder has: an `AppInfo` model with `name`, `version`, `url` and `details`, and a `ValueError` on an invalid token. +The spec places the header builder of the `mthds` library in `mthds.runners.api.user_agent`, which `mthds` ships since 0.16.0, the version this SDK pins: `MthdsAPIClient` builds its own `User-Agent` there and lets a subclass prepend its token through `user_agent_sdk_tokens()` and `init_user_agent(app_info)`. This SDK does not adopt that seam yet. `pipelex_sdk.user_agent` still builds the whole header itself, with its own `AppInfo` model of the same shape (`name`, `version`, `url` and `details`, and a `ValueError` on an invalid token), and `PipelexAPIClient` sets `client.user_agent` from it, which the inherited transport sends on every request. diff --git a/pipelex_sdk/client.py b/pipelex_sdk/client.py index 03d84b2..10f2628 100644 --- a/pipelex_sdk/client.py +++ b/pipelex_sdk/client.py @@ -297,7 +297,10 @@ def __init__( request_timeout_seconds if request_timeout_seconds is not None else self._DEFAULT_REQUEST_TIMEOUT_SECONDS ) #: The integrator's own name, placed before this SDK's tokens in the `User-Agent`. - self.app_info: AppInfo | None = app_info + # Since `mthds` 0.16.0 the base declares `app_info` as its own `AppInfo`, which it only ever + # writes (in `init_user_agent`, never called here). This SDK keeps its own model and builder + # until it adopts the base's `user_agent_sdk_tokens` seam, so the narrow ignore covers that one divergence. + self.app_info: AppInfo | None = app_info # type: ignore[assignment] #: The `User-Agent` sent on every request (spec: `docs/specs/client-identification.md`), #: built once here so an over-long header fails at construction, not on the first call. self.user_agent: str = build_user_agent(app_info) diff --git a/pipelex_sdk/user_agent.py b/pipelex_sdk/user_agent.py index a251440..6d5430e 100644 --- a/pipelex_sdk/user_agent.py +++ b/pipelex_sdk/user_agent.py @@ -8,8 +8,8 @@ The header is self-declared and unauthenticated: the platform reads it for analytics only. -The builder lives here rather than in `mthds` because the pinned `mthds` does not ship one yet; -the public shape (`AppInfo`, `ValueError` on an invalid token) matches the one `mthds` will expose. +The builder predates the one `mthds` ships in `mthds.runners.api.user_agent` since 0.16.0, and this +SDK has not adopted that one yet; the public shape (`AppInfo`, `ValueError` on an invalid token) matches it. """ from __future__ import annotations diff --git a/pyproject.toml b/pyproject.toml index c745cfa..9691c7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ classifiers = [ ] dependencies = [ - "mthds==0.15.0", + "mthds==0.16.0", "pydantic>=2.10.6,<3.0.0", "typing-extensions>=4.0.0", "httpx>=0.23.0,<1.0.0", diff --git a/uv.lock b/uv.lock index 071ecac..4e72dca 100644 --- a/uv.lock +++ b/uv.lock @@ -212,7 +212,7 @@ wheels = [ [[package]] name = "mthds" -version = "0.15.0" +version = "0.16.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/54/16/1aa1219f44bc469213018478f582eb4eb28f69ea43a09fe73afc9cfb4e34/mthds-0.15.0.tar.gz", hash = "sha256:2e612174b089cae799eb236f5b1bcd183b8847a62a06a265f72f0e50b4c89555", size = 250552, upload-time = "2026-09-18T20:23:09.888Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/03/4aca733536f741f741d0cba3ae3d23dfd56f691c53a0d7e3e7baea371585/mthds-0.16.0.tar.gz", hash = "sha256:cc8f54bc76c9ed13273e7cd2e1c66767e5478fc3640a26481bf099451d56aa29", size = 259394, upload-time = "2026-09-23T12:13:31.752Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5c/7526815d8bc8cd8690c8cf4de3819e3f322886280af05bf9aed7bdce450c/mthds-0.15.0-py3-none-any.whl", hash = "sha256:98670ca1f97fc2211ace0f404acd416ce5882edb728845d48440d6e73eedc34c", size = 99711, upload-time = "2026-09-18T20:23:08.283Z" }, + { url = "https://files.pythonhosted.org/packages/d0/00/a80f90f0ed7ddff9886f925fbb8dd0061ec0b0e1af49901971664f1a3e12/mthds-0.16.0-py3-none-any.whl", hash = "sha256:67468669451278e8c17409c5fed60eafab8453fd5cd92f333d788b21f7500ebe", size = 103813, upload-time = "2026-09-23T12:13:30.288Z" }, ] [[package]] @@ -326,7 +326,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, - { name = "mthds", specifier = "==0.15.0" }, + { name = "mthds", specifier = "==0.16.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" },