Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## [v0.12.0] - 2026-09-24

### Added

- **`locate_artifacts` and `ArtifactLocation`**: `pipelex_sdk.artifacts.locate_artifacts(value)` is the artifact walk with its paths — every `pipelex-storage://` reference in a JSON value, deduplicated in discovery order exactly as `collect_artifacts` returns them, each as an `ArtifactLocation` whose `found_at` lists every `$`-rooted path at which it sits (`$.rooms[3].staged_photo.url`, `$.items[0].url`, `$["a key"].url`), written the way `@pipelex/sdk`'s `locateArtifacts` writes them.

### Changed

- **`download_artifacts` names each file after the field it fills, and `artifact_filename` takes a location (Breaking)**: a saved file is named after the first path at which its reference sits — `$.rooms[3].staged_photo.url` is saved as `rooms-3-staged_photo.png`, and an output that is one image as `main_stuff.png` — instead of after the last segment of its storage key, which now supplies only the extension, so the same run saves under the same names from either SDK. `artifact_filename(location, content_type, scope)` replaces `artifact_filename(uri, content_type, index)` and raises `ArtifactOperationError` for anything but an `ArtifactLocation` whose first path is in the walk's notation; a field whose name Windows reserves for a device (`aux`, `nul`, `com1` and the like) is saved with a trailing `_` (`aux_.png`); the full rule is on `docs/artifact-download.md`.
- **`DownloadedArtifact` carries a required `found_at` (Breaking)**: every item of a `download_artifacts` verdict carries its reference's `found_at`, on the saved arm and the error arm alike, so a file that was not saved still says which field it would have filled. `DownloadedArtifact` is now an `ArtifactLocation`, so `artifact_filename` takes a verdict item as it is, and code that builds `DownloadedArtifact` values — a test fake standing in for `download_artifacts` — must now supply the field.
- **Requires `mthds` 0.15.0 (Breaking)**: the exact pin moves from 0.14.0, so `pipelex-sdk` can again be installed beside `pipelex`, which has pinned `mthds==0.15.0` exactly since its 0.60.0. Nothing in this client's own surface changes: the release's breaking cut to `ConceptAbstract` and `StuffAbstract` sits in protocol models this SDK does not build on, and `parse_method_files` / `serialize_method_files` stay beside the canonical `mthds.protocol.method_files` for the reason `docs/architecture.md` gives.

## [v0.11.0] - 2026-09-23

### Added
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,9 @@ The wire models are snake_case Pydantic v2. Response models are extension-open (

## Artifact stack (`pipelex_sdk/artifacts.py` + `pipelex_sdk/artifact_models.py`)

The download twin of input preparation, and the Python twin of `@pipelex/sdk`'s `src/artifacts.ts`: `collect_artifacts` walks a value for `pipelex-storage://` references, `resolve_artifacts` mints fresh links for a whole list through the bulk route (chunked at its bound), `fetch_artifact` yields one bounded stream, and `download_artifacts` saves a run's produced files under a directory as a produced verdict. The operations take a `Protocol` rather than the client, so `PipelexAPIClient` satisfies them structurally and a test injects a fake; the client also carries all four as methods, the way it carries `upload_file` and `prepare_inputs`. The whole contract, the options and the error taxonomy are in [artifact-download.md](./artifact-download.md).
The download twin of input preparation, and the Python twin of `@pipelex/sdk`'s `src/artifacts.ts`: `locate_artifacts` walks a value for `pipelex-storage://` references and says every `$`-rooted path each one sits at, `collect_artifacts` is the same walk's bare references, `resolve_artifacts` mints fresh links for a whole list through the bulk route (chunked at its bound), `fetch_artifact` yields one bounded stream, and `download_artifacts` saves a run's produced files under a directory as a produced verdict, each file named after the field it fills by `artifact_filename`'s rule. The operations take a `Protocol` rather than the client, so `PipelexAPIClient` satisfies them structurally and a test injects a fake; the client also carries `resolve_artifacts`, `fetch_artifact` and `download_artifacts` as methods, the way it carries `upload_file` and `prepare_inputs`. The whole contract, the options and the error taxonomy are in [artifact-download.md](./artifact-download.md).

**The shapes are owned by `pipelex_sdk/artifact_models.py`** — the scope enum, the wire items, the options and the verdict, plus the public defaults — beside `product_models` and `crate_models`. That split is not cosmetic: `pipelex_sdk/errors.py` types `ScopeUnavailableError.scope` and `ArtifactAuthenticationError.verdict` with two of them and the operations module imports those errors, so keeping the shapes in the operations module would be an import cycle (`reportImportCycles` is an error here). The JS twin needs no such split, TypeScript tolerating the cycle.
**The shapes are owned by `pipelex_sdk/artifact_models.py`** — the scope enum, the wire items, the location a walk answers, the options and the verdict, plus the public defaults — beside `product_models` and `crate_models`. That split is not cosmetic: `pipelex_sdk/errors.py` types `ScopeUnavailableError.scope` and `ArtifactAuthenticationError.verdict` with two of them and the operations module imports those errors, so keeping the shapes in the operations module would be an import cycle (`reportImportCycles` is an error here). The JS twin needs no such split, TypeScript tolerating the cycle.

**The object store is fetched on its own httpx client**, never the API client's: no `Authorization` header of ours can ride along to the store, redirects are refused rather than followed, and the call's timeout is both a budget over the whole exchange and httpx's per-operation timeout. `_new_storage_client` is the one seam the module opens, which is where the unit suite injects an `httpx.MockTransport`.

Expand Down Expand Up @@ -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`, on its `dev` branch and **not in the `mthds==0.14.0` this package pins exactly** — so adoption waits on a release before anything else. It is a step of its own rather than a rename in any case: 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 release and the exception's base class are 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 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`.

**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.

Expand Down
Loading
Loading