pipelex-api 0.71.0__tar.gz → 0.72.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/PKG-INFO +7 -6
  2. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/README.md +5 -4
  3. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/exception_handlers.py +4 -0
  4. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/limits.py +0 -3
  5. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/main.py +1 -1
  6. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/method_cache.py +1 -0
  7. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/method_source.py +3 -3
  8. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/openapi_responses.py +1 -1
  9. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/__init__.py +0 -2
  10. pipelex_api-0.72.0/pipelex_api/routes/pipelex/agent/__init__.py +7 -0
  11. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/agent/models.py +1 -1
  12. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/codegen.py +2 -2
  13. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/crate_ops.py +10 -13
  14. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/validate.py +9 -8
  15. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/schemas/models.py +11 -9
  16. pipelex_api-0.71.0/pipelex_api/routes/pipelex/agent/__init__.py +0 -11
  17. pipelex_api-0.71.0/pipelex_api/routes/pipelex/agent/concept.py +0 -60
  18. pipelex_api-0.71.0/pipelex_api/routes/pipelex/agent/pipe_spec.py +0 -59
  19. pipelex_api-0.71.0/pipelex_api/routes/pipelex/build/__init__.py +0 -11
  20. pipelex_api-0.71.0/pipelex_api/routes/pipelex/build/inputs.py +0 -192
  21. pipelex_api-0.71.0/pipelex_api/routes/pipelex/build/output.py +0 -163
  22. pipelex_api-0.71.0/pipelex_api/routes/pipelex/build/runner.py +0 -236
  23. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/.gitignore +0 -0
  24. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/LICENSE +0 -0
  25. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/hatch_build.py +0 -0
  26. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/__init__.py +0 -0
  27. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/api.toml +0 -0
  28. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/api_config.py +0 -0
  29. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/bundle.py +0 -0
  30. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/disclosure.py +0 -0
  31. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/error_types.py +0 -0
  32. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/error_uri.py +0 -0
  33. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/errors.py +0 -0
  34. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/json_body.py +0 -0
  35. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/middleware.py +0 -0
  36. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/openapi_schema.py +0 -0
  37. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/problem_document.py +0 -0
  38. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/py.typed +0 -0
  39. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/routes/__init__.py +0 -0
  40. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/routes/health.py +0 -0
  41. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/pipe_io.py +0 -0
  42. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/pipeline.py +0 -0
  43. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/resolve.py +0 -0
  44. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/tools.py +0 -0
  45. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/utils.py +0 -0
  46. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/routes/version.py +0 -0
  47. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/schemas/__init__.py +0 -0
  48. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pipelex_api/security.py +0 -0
  49. {pipelex_api-0.71.0 → pipelex_api-0.72.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pipelex-api
3
- Version: 0.71.0
3
+ Version: 0.72.0
4
4
  Summary: Pipelex API
5
5
  Project-URL: Homepage, https://pipelex.com
6
6
  Project-URL: Repository, https://github.com/Pipelex/pipelex
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3.14
17
17
  Requires-Python: <3.15,>=3.11
18
18
  Requires-Dist: fastapi<0.142,>=0.118.0
19
- Requires-Dist: pipelex[anthropic,bedrock,fal,google,google-genai,mistralai]==0.71.0
19
+ Requires-Dist: pipelex[anthropic,bedrock,fal,google,google-genai,mistralai]==0.72.0
20
20
  Requires-Dist: pyjwt>=2.10.1
21
21
  Requires-Dist: uvicorn>=0.37.0
22
22
  Provides-Extra: dev
@@ -49,7 +49,7 @@ Description-Content-Type: text/markdown
49
49
 
50
50
  <h2 align="center">Pipelex API</h2>
51
51
 
52
- The official REST API server for building and executing Pipelex pipelines. Deploy your pipelines as HTTP endpoints and integrate them into any application or workflow.
52
+ The official REST API server for validating and executing Pipelex pipelines. Deploy your pipelines as HTTP endpoints and integrate them into any application or workflow.
53
53
 
54
54
  <div>
55
55
  <a href="https://docs.pipelex.com/latest/api-server/"><strong>API Documentation</strong></a> -
@@ -61,7 +61,7 @@ The official REST API server for building and executing Pipelex pipelines. Deplo
61
61
  <p align="center">
62
62
  <a href="https://docs.pipelex.com/latest/license/"><img src="https://img.shields.io/badge/License-Elastic--2.0-blue.svg" alt="Elastic License 2.0"></a>
63
63
  <a href="https://go.pipelex.com/discord"><img src="https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
64
- <a href="https://docs.pipelex.com/"><img src="https://img.shields.io/badge/Docs-03bb95?logo=read-the-docs&logoColor=white&style=flat" alt="Documentation"></a>
64
+ <a href="https://docs.pipelex.com/latest/api-server/"><img src="https://img.shields.io/badge/Docs-03bb95?logo=read-the-docs&logoColor=white&style=flat" alt="Documentation"></a>
65
65
  </p>
66
66
  </div>
67
67
 
@@ -83,7 +83,7 @@ The official REST API server for building and executing Pipelex pipelines. Deplo
83
83
 
84
84
  The **Pipelex API Server** is a FastAPI-based REST API that allows you to execute [Pipelex](https://github.com/Pipelex/pipelex) pipelines via HTTP requests. Deploy your pipelines as HTTP endpoints and integrate them into any application or workflow.
85
85
 
86
- It is the source-available reference implementation of the **[MTHDS Protocol](https://mthds.ai)** — the minimal HTTP contract every MTHDS runner implements (`POST /execute`, `POST /start`, `POST /validate`, `GET /models`, `GET /version`). The contracts nest: **MTHDS Protocol ⊂ Pipelex API (this server) ⊂ Pipelex hosted API**. This server adds the build tooling extensions (`/build/*`) on top of the protocol; the hosted API at `api.pipelex.com/v1` adds durable runs, the method catalog, and account management on top of this server — same shapes throughout. All routes live under the `/v1` base path; the committed contract is [`pipelex-api.openapi.yaml`](https://github.com/Pipelex/pipelex/blob/main/docs/api-server/openapi/pipelex-api.openapi.yaml).
86
+ It is the source-available reference implementation of the **[MTHDS Protocol](https://mthds.ai)** — the minimal HTTP contract every MTHDS runner implements (`POST /execute`, `POST /start`, `POST /validate`, `GET /models`, `GET /version`). The contracts nest: **MTHDS Protocol ⊂ Pipelex API (this server) ⊂ Pipelex hosted API**. This server adds the tooling extensions (`/resolve`, `/codegen`, `/pipe-io`, `/lint`, `/format`) on top of the protocol; the hosted API at `api.pipelex.com/v1` adds durable runs, the method catalog, and account management on top of this server — same shapes throughout. All routes live under the `/v1` base path; the committed contract is [`pipelex-api.openapi.yaml`](https://github.com/Pipelex/pipelex/blob/main/docs/api-server/openapi/pipelex-api.openapi.yaml).
87
87
 
88
88
  # 🚀 Quick Start with Docker
89
89
 
@@ -165,7 +165,8 @@ The full reference for this API server is part of the Pipelex documentation, und
165
165
  - [Overview](https://docs.pipelex.com/latest/api-server/) — endpoints, authentication, deployment
166
166
  - [Pipe Run](https://docs.pipelex.com/latest/api-server/pipe-run/) — `/execute`, `/start`, every input shape
167
167
  - [Pipe Validate](https://docs.pipelex.com/latest/api-server/pipe-validate/) — `/validate`
168
- - [Pipe Builder](https://docs.pipelex.com/latest/api-server/pipe-builder/) — `/build/inputs`, `/build/output`, `/build/runner`
168
+ - [Pipe I/O](https://docs.pipelex.com/latest/api-server/pipe-io/) — `/pipe-io`
169
+ - [Resolve & Codegen](https://docs.pipelex.com/latest/api-server/codegen/) — `/resolve`, `/codegen`
169
170
  - [Configuration](https://docs.pipelex.com/latest/api-server/configuration/) — env vars, mounting your own `.pipelex/` config
170
171
 
171
172
  For broader Pipelex documentation (MTHDS language, concepts, pipe types, the Gateway): **[https://docs.pipelex.com/](https://docs.pipelex.com/)**
@@ -3,7 +3,7 @@
3
3
 
4
4
  <h2 align="center">Pipelex API</h2>
5
5
 
6
- The official REST API server for building and executing Pipelex pipelines. Deploy your pipelines as HTTP endpoints and integrate them into any application or workflow.
6
+ The official REST API server for validating and executing Pipelex pipelines. Deploy your pipelines as HTTP endpoints and integrate them into any application or workflow.
7
7
 
8
8
  <div>
9
9
  <a href="https://docs.pipelex.com/latest/api-server/"><strong>API Documentation</strong></a> -
@@ -15,7 +15,7 @@ The official REST API server for building and executing Pipelex pipelines. Deplo
15
15
  <p align="center">
16
16
  <a href="https://docs.pipelex.com/latest/license/"><img src="https://img.shields.io/badge/License-Elastic--2.0-blue.svg" alt="Elastic License 2.0"></a>
17
17
  <a href="https://go.pipelex.com/discord"><img src="https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
18
- <a href="https://docs.pipelex.com/"><img src="https://img.shields.io/badge/Docs-03bb95?logo=read-the-docs&logoColor=white&style=flat" alt="Documentation"></a>
18
+ <a href="https://docs.pipelex.com/latest/api-server/"><img src="https://img.shields.io/badge/Docs-03bb95?logo=read-the-docs&logoColor=white&style=flat" alt="Documentation"></a>
19
19
  </p>
20
20
  </div>
21
21
 
@@ -37,7 +37,7 @@ The official REST API server for building and executing Pipelex pipelines. Deplo
37
37
 
38
38
  The **Pipelex API Server** is a FastAPI-based REST API that allows you to execute [Pipelex](https://github.com/Pipelex/pipelex) pipelines via HTTP requests. Deploy your pipelines as HTTP endpoints and integrate them into any application or workflow.
39
39
 
40
- It is the source-available reference implementation of the **[MTHDS Protocol](https://mthds.ai)** — the minimal HTTP contract every MTHDS runner implements (`POST /execute`, `POST /start`, `POST /validate`, `GET /models`, `GET /version`). The contracts nest: **MTHDS Protocol ⊂ Pipelex API (this server) ⊂ Pipelex hosted API**. This server adds the build tooling extensions (`/build/*`) on top of the protocol; the hosted API at `api.pipelex.com/v1` adds durable runs, the method catalog, and account management on top of this server — same shapes throughout. All routes live under the `/v1` base path; the committed contract is [`pipelex-api.openapi.yaml`](https://github.com/Pipelex/pipelex/blob/main/docs/api-server/openapi/pipelex-api.openapi.yaml).
40
+ It is the source-available reference implementation of the **[MTHDS Protocol](https://mthds.ai)** — the minimal HTTP contract every MTHDS runner implements (`POST /execute`, `POST /start`, `POST /validate`, `GET /models`, `GET /version`). The contracts nest: **MTHDS Protocol ⊂ Pipelex API (this server) ⊂ Pipelex hosted API**. This server adds the tooling extensions (`/resolve`, `/codegen`, `/pipe-io`, `/lint`, `/format`) on top of the protocol; the hosted API at `api.pipelex.com/v1` adds durable runs, the method catalog, and account management on top of this server — same shapes throughout. All routes live under the `/v1` base path; the committed contract is [`pipelex-api.openapi.yaml`](https://github.com/Pipelex/pipelex/blob/main/docs/api-server/openapi/pipelex-api.openapi.yaml).
41
41
 
42
42
  # 🚀 Quick Start with Docker
43
43
 
@@ -119,7 +119,8 @@ The full reference for this API server is part of the Pipelex documentation, und
119
119
  - [Overview](https://docs.pipelex.com/latest/api-server/) — endpoints, authentication, deployment
120
120
  - [Pipe Run](https://docs.pipelex.com/latest/api-server/pipe-run/) — `/execute`, `/start`, every input shape
121
121
  - [Pipe Validate](https://docs.pipelex.com/latest/api-server/pipe-validate/) — `/validate`
122
- - [Pipe Builder](https://docs.pipelex.com/latest/api-server/pipe-builder/) — `/build/inputs`, `/build/output`, `/build/runner`
122
+ - [Pipe I/O](https://docs.pipelex.com/latest/api-server/pipe-io/) — `/pipe-io`
123
+ - [Resolve & Codegen](https://docs.pipelex.com/latest/api-server/codegen/) — `/resolve`, `/codegen`
123
124
  - [Configuration](https://docs.pipelex.com/latest/api-server/configuration/) — env vars, mounting your own `.pipelex/` config
124
125
 
125
126
  For broader Pipelex documentation (MTHDS language, concepts, pipe types, the Gateway): **[https://docs.pipelex.com/](https://docs.pipelex.com/)**
@@ -340,6 +340,10 @@ _ERROR_TYPE_STATUS_OVERRIDES: dict[str, int] = {
340
340
  # cannot be processed under this deployment's bounds → 422 (not 413, which is about
341
341
  # the request entity itself).
342
342
  "MethodPackageTooLargeError": 422,
343
+ # - The selected package carries a symlink, file or directory, which fetched packages must
344
+ # not: a rule about the fetched content that the method's author fixes by removing the
345
+ # link → 422, like the ceilings above (not 403, kept for refusing in-process Python).
346
+ "MethodPackageSymlinkError": 422,
343
347
  # - The method's Python declares structure classes, whether it was sent as a bundle or
344
348
  # fetched by `method_ref`: a policy refusal (hosted execution accepts MTHDS concepts and
345
349
  # sandboxed PipeFuncs, not in-process Python) → 403, the same status as the bundle
@@ -24,7 +24,6 @@ MAX_METHOD_REF_LEN = 512 # `method_ref` selector strings; a fixed schema bound,
24
24
  MAX_JSON_NESTING_DEPTH = 128
25
25
  DEFAULT_MAX_CALLBACK_URLS = 5
26
26
  DEFAULT_MAX_CALLBACK_URL_LEN = 2048
27
- DEFAULT_MAX_AGENT_SPEC_KIB = 256 # 256 KiB for JSON concept/pipe specs
28
27
  DEFAULT_MAX_BUNDLE_FILES = 128 # entries in a materialized method bundle (.mthds + .py + requirements.txt)
29
28
  DEFAULT_MAX_BUNDLE_TOTAL_KIB = 8 * 1024 # 8 MiB decompressed across the whole bundle (zip-bomb guard)
30
29
  DEFAULT_MAX_METHOD_CACHE_CLONES = 64 # cached method-package clones (one per resolved commit SHA)
@@ -57,8 +56,6 @@ MAX_PIPE_CODE_LEN = _read_positive_int("MAX_PIPE_CODE_LEN", DEFAULT_MAX_PIPE_COD
57
56
  MAX_CALLBACK_URLS = _read_positive_int("MAX_CALLBACK_URLS", DEFAULT_MAX_CALLBACK_URLS)
58
57
  MAX_CALLBACK_URL_LEN = _read_positive_int("MAX_CALLBACK_URL_LEN", DEFAULT_MAX_CALLBACK_URL_LEN)
59
58
 
60
- MAX_AGENT_SPEC_BYTES = _read_positive_int("MAX_AGENT_SPEC_KIB", DEFAULT_MAX_AGENT_SPEC_KIB) * 1024
61
-
62
59
  MAX_BUNDLE_FILES = _read_positive_int("MAX_BUNDLE_FILES", DEFAULT_MAX_BUNDLE_FILES)
63
60
  MAX_BUNDLE_TOTAL_BYTES = _read_positive_int("MAX_BUNDLE_TOTAL_KIB", DEFAULT_MAX_BUNDLE_TOTAL_KIB) * 1024
64
61
 
@@ -150,7 +150,7 @@ fastapi_app = PipelexFastAPI(
150
150
  f"This server implements the [MTHDS Protocol](https://mthds.ai) v{PROTOCOL_VERSION} "
151
151
  "(`POST /execute`, `POST /start`, `POST /validate`, `GET /models`, `GET /version` — "
152
152
  "marked `x-mthds-protocol: true`) plus the Pipelex API extensions: resolve and codegen "
153
- "(`/resolve`, `/codegen`), build tooling (`/build/*`), and editor tooling (`/lint`, `/format`). "
153
+ "(`/resolve`, `/codegen`), the pipe I/O route (`/pipe-io`), and editor tooling (`/lint`, `/format`). "
154
154
  "Contract layering: MTHDS Protocol ⊂ Pipelex API (this server) ⊂ Pipelex hosted API. "
155
155
  "All endpoints are served under the `/v1` base path; "
156
156
  "every error is an RFC 7807 `application/problem+json` problem document, documented per "
@@ -157,6 +157,7 @@ class MethodCloneCache:
157
157
  MethodPackageNotFoundError: If no package matches the requested address.
158
158
  MethodPackageAmbiguityError: If more than one package matches.
159
159
  MethodPackageTooLargeError: If the selected package exceeds the ceilings.
160
+ MethodPackageSymlinkError: If the selected package contains a symlink.
160
161
  """
161
162
  commit_sha = resolve_remote_commit_sha(ref=ref, clone_url=clone_url)
162
163
  clone_dir = self._root / commit_sha
@@ -13,10 +13,10 @@ manifest-identity package location, the bounds, and the structures check all liv
13
13
  `mthds_sources`, so diagnostics carry true per-file labels), and only the non-`.mthds`
14
14
  files are materialized into a temporary `library_dirs` entry — exactly the split the
15
15
  method-bundle transport uses, so a fetched package runs the same proven path.
16
- - The tooling routes (`/resolve`, `/codegen`, `/build/*`) use
16
+ - The tooling routes (`/resolve`, `/codegen`, `/pipe-io`) use
17
17
  :func:`fetch_method_mthds_files`: only the `.mthds` files, as `files[]` items, paired with
18
- the manifest's `main_pipe` so the per-pipe projections default their selector exactly as a
19
- run does. No Python ever loads there, so the execution-locus gate does not apply.
18
+ the manifest's `main_pipe` so the per-pipe route defaults its selector exactly as a run
19
+ does. No Python ever loads there, so the execution-locus gate does not apply.
20
20
 
21
21
  The security gate (packaging invariant 7 — execution locus decides): `.mthds` content is
22
22
  data, always acceptable. On a deployment that is NOT sandbox-hosted, a fetched package
@@ -82,7 +82,7 @@ class ProblemDocument(BaseModel):
82
82
  description=(
83
83
  "Structured per-error diagnostics, carried by a `ValidateBundleError`. Each item may carry a `suggested_fix`. "
84
84
  "Retained under `ERROR_DISCLOSURE=strict` — it describes the caller's own bundle, not server internals. "
85
- "On `/validate`, `/resolve`, `/codegen` and `/build/*` an invalid bundle is a **200** verdict instead, so the "
85
+ "On `/validate`, `/resolve`, `/codegen` and `/pipe-io` an invalid bundle is a **200** verdict instead, so the "
86
86
  "items ride the response body there rather than a problem document."
87
87
  ),
88
88
  )
@@ -1,7 +1,6 @@
1
1
  from fastapi import APIRouter
2
2
 
3
3
  from .agent import router as agent_router
4
- from .build import router as build_router
5
4
  from .codegen import router as codegen_router
6
5
  from .pipe_io import router as pipe_io_router
7
6
  from .pipeline import router as pipeline_router
@@ -11,7 +10,6 @@ from .validate import router as validate_router
11
10
 
12
11
  router = APIRouter()
13
12
 
14
- router.include_router(build_router)
15
13
  router.include_router(pipeline_router)
16
14
  router.include_router(validate_router)
17
15
  router.include_router(resolve_router)
@@ -0,0 +1,7 @@
1
+ from fastapi import APIRouter
2
+
3
+ from .models import router as models_router
4
+
5
+ router = APIRouter()
6
+
7
+ router.include_router(models_router)
@@ -18,7 +18,7 @@ async def get_models(
18
18
  request: Request,
19
19
  model_type: Annotated[
20
20
  str | None,
21
- Query(alias="type", description="Filter by model category: llm, extract, img_gen, search. Single value (protocol arity)."),
21
+ Query(alias="type", description="Filter by model category: llm, extract, img_gen, search, judgment. Single value (protocol arity)."),
22
22
  ] = None,
23
23
  ) -> PipelexModelDeck:
24
24
  """List the model deck this runner can route to (MTHDS Protocol `GET /models`).
@@ -32,8 +32,8 @@ class CodegenRouteKind(StrEnum):
32
32
  Membership follows the **trust chain**: a kind is served here exactly when its artifacts are
33
33
  stamped and locked, so a client can write them verbatim and pass the offline `codegen check` —
34
34
  the promise this route's valid arm makes by carrying a `lock`. Input templates are user-editable
35
- scaffolds, never stamped or locked, so they cannot make that promise; they ride
36
- `POST /build/inputs` instead and `inputs` is therefore not a kind here. Future per-pipe kinds
35
+ scaffolds, never stamped or locked, so they cannot make that promise: a client projects one from
36
+ the input form `POST /pipe-io` serves, and `inputs` is therefore not a kind here. Future per-pipe kinds
37
37
  (`docs`, `tools`, `tests`) do emit tracked artifacts, so they join this enum and select their
38
38
  pipe via `pipe_ref`. An unknown kind is a request-shape 422 listing the served set.
39
39
  """
@@ -1,4 +1,4 @@
1
- """Shared crate-resolution plumbing for the `/resolve`, `/codegen`, `/pipe-io` and `/build/*` routes.
1
+ """Shared crate-resolution plumbing for the `/resolve`, `/codegen` and `/pipe-io` routes.
2
2
 
3
3
  They all select a closure the same way (inline `files[]` XOR `method_ref`), resolve it through the
4
4
  same engine core (`pipelex.pipeline.resolve_bundle.resolve_crate_from_contents`), and speak the same
@@ -8,10 +8,8 @@ This module holds the pieces they share so the envelopes cannot drift.
8
8
 
9
9
  The invalid arm here is the *crate* verdict: it deliberately omits `/validate`'s runnability facts
10
10
  (`pending_signatures`, `is_runnable`) — resolution is static (no dry-run sweep, matching
11
- `pipelex resolve`), so runnability is not part of its vocabulary. The per-pipe projections
12
- (`/build/{inputs,output}`) ride that same static core: a template is a read of the pipe's *declared*
13
- IO, so a valid verdict there says the closure is structurally sound, never that the pipe runs.
14
- `/build/runner` is the exception — it needs the dry-run sweep, so it keeps `validate_bundle`.
11
+ `pipelex resolve`), so runnability is not part of its vocabulary. The per-pipe route (`/pipe-io`)
12
+ rides that same static core: it reads the selected pipe's *declared* IO with no dry-run sweep.
15
13
  """
16
14
 
17
15
  from enum import StrEnum
@@ -87,15 +85,14 @@ class SelectedFiles(NamedTuple):
87
85
  def selected_files(request_data: MthdsFilesRequest) -> SelectedFiles:
88
86
  """The files the closure selector names: inline `files[]`, or an address `method_ref`'s package.
89
87
 
90
- Shared by every route on the `files[]` envelope — including `/build/runner`, which cannot use
91
- `resolve_requested_crate` (it needs `validate_bundle`'s dry-run sweep) but owes the caller the
92
- same answer on every selector.
88
+ Every route on the `files[]` envelope reaches it through `resolve_requested_crate`, so they all
89
+ give the same answer on every selector.
93
90
 
94
91
  An **address-form** `method_ref` (`github.com/<owner>/<repo>[/<selector>][@<tag>]`) resolves
95
92
  through the same fetch path the run routes use: the package's `.mthds` files come back as
96
93
  `files[]` items with their real relative paths as per-file sources, and the manifest's
97
- `main_pipe` rides beside them so the per-pipe projections can default their selector the way
98
- a run by address does. Only `.mthds` data travels — the package's Python (if any) never loads
94
+ `main_pipe` rides beside them so the per-pipe route can default its selector the way a run
95
+ by address does. Only `.mthds` data travels — the package's Python (if any) never loads
99
96
  on these routes. The **registry form** (any non-address reference) stays reserved and keeps
100
97
  its 501 until the packaging program's registry phase.
101
98
 
@@ -159,10 +156,10 @@ def resolve_requested_crate(request_data: MthdsFilesRequest) -> ResolvedClosure:
159
156
 
160
157
 
161
158
  class RequestedPipe(NamedTuple):
162
- """The pipe a per-pipe projection was asked for: its resolved qualified ref and the live pipe."""
159
+ """The pipe a per-pipe route was asked for: its resolved qualified ref and the live pipe."""
163
160
 
164
161
  ref: str
165
- """The qualified `domain.pipe_code` actually projected — always qualified, whatever the request spelled."""
162
+ """The qualified `domain.pipe_code` actually selected — always qualified, whatever the request spelled."""
166
163
 
167
164
  pipe: PipeAbstract
168
165
  """The live pipe, read from the library `resolve_requested_crate` left loaded + current."""
@@ -197,7 +194,7 @@ class _SelectorOrigin(StrEnum):
197
194
 
198
195
 
199
196
  def resolve_requested_pipe(crate: LibraryCrate, *, pipe_ref: str | None, manifest_main_pipe: str | None) -> RequestedPipe:
200
- """Select the pipe a per-pipe projection targets, defaulting to the manifest's, then the closure's, `main_pipe`.
197
+ """Select the pipe a per-pipe route targets, defaulting to the manifest's, then the closure's, `main_pipe`.
201
198
 
202
199
  The request's `pipe_ref` wins; omitted, the default chain (`select_default_pipe`) decides. Every
203
200
  failed selection — an unknown ref, an ambiguous one, a chain that finds no entry pipe or several —
@@ -166,13 +166,13 @@ def _effective_default_pipe_ref(report: PipelexValidationReport, *, manifest_mai
166
166
  The run routes' precedence, minus the request selector `/validate` does not have
167
167
  (`pipeline.py`: `pipe_code or fetched.main_pipe`, then the batch's primary blueprint via
168
168
  `select_primary_blueprint`): a fetched package's manifest `main_pipe` wins, and only when there
169
- is none does the closure's own declaration decide. The per-pipe tooling routes apply that same
169
+ is none does the closure's own declaration decide. The per-pipe tooling route applies that same
170
170
  chain in `crate_ops.resolve_requested_pipe`, so a caller reading this field and a caller omitting
171
- `pipe_ref` on `/build/*` are told about the same pipe wherever `/build/*` defaults at all — see
171
+ `pipe_ref` on `/pipe-io` are told about the same pipe wherever `/pipe-io` defaults at all — see
172
172
  the one case below where it refuses to.
173
173
 
174
- It deliberately states the RUN default, not the build routes' stricter one: a closure whose
175
- domains each declare a `main_pipe` is refused by `/build/*` but runs happily — `execute` and
174
+ It deliberately states the RUN default, not the per-pipe route's stricter one: a closure whose
175
+ domains each declare a `main_pipe` is refused by `/pipe-io` but runs happily — `execute` and
176
176
  `start` take the first declaring blueprint — so reporting `null` for it would make a consumer
177
177
  refuse to prepare a method the server would run. The report's `bundle_blueprint` IS that first
178
178
  declaring blueprint (`build_validation_report` selects it through the same
@@ -374,10 +374,11 @@ async def validate_mthds(request: Request, request_data: ValidateRequest) -> JSO
374
374
  # so it is never rendered as `is_valid: false` (that verdict is about MTHDS content).
375
375
  with fetched_method_source(request_data.method_ref) as fetched:
376
376
  validated_contents = fetched.mthds_contents
377
- # The manifest's declared entry pipe outranks the closure's own in the run/build default
378
- # chain, so it must reach `default_pipe_ref` — the report itself is manifest-blind — and
379
- # the graph arm, which would otherwise draw the primary blueprint's `main_pipe`: a
380
- # different pipe when the two disagree, and no graph at all when the bundles declare none.
377
+ # The manifest's declared entry pipe outranks the closure's own in the default chain the
378
+ # run routes and `/pipe-io` share, so it must reach `default_pipe_ref` — the report itself
379
+ # is manifest-blind — and the graph arm, which would otherwise draw the primary blueprint's
380
+ # `main_pipe`: a different pipe when the two disagree, and no graph at all when the bundles
381
+ # declare none.
381
382
  # The bare code is passed as is, so the graph arm resolves it exactly as the run does.
382
383
  manifest_main_pipe = fetched.main_pipe
383
384
  verdict = await ApiRunner(
@@ -283,7 +283,7 @@ _CALLER_ANALYTICS_GROUPS_DESCRIPTION = (
283
283
  class CallerAnalyticsGroupsMixin(BaseModel):
284
284
  """The optional `analytics_groups` of a request whose work is done for a caller without being a run.
285
285
 
286
- `/validate` and `/build/runner` dry-run the submitted pipes, and the runtime attributes that
286
+ `/validate` dry-runs the submitted pipes, and the runtime attributes that
287
287
  telemetry to the caller it is handed (`pipelex.system.caller_identity.CallerIdentity`). The user
288
288
  comes from the trusted auth layer, as on a run; the groups come from the body, as on a run, and are
289
289
  refused at the wire with the runtime's own rules.
@@ -569,15 +569,17 @@ class MthdsFilesRequest(BaseModel):
569
569
 
570
570
 
571
571
  class MthdsPipeRequest(MthdsFilesRequest):
572
- """Shared base for the per-pipe `/build/*` projections: the closure selector plus a pipe selector.
572
+ """The base of a per-pipe route's request (`/pipe-io`): the closure selector plus a pipe selector.
573
573
 
574
574
  The pipe selector is the **qualified** ref `domain.pipe_code`, mirroring `pipelex codegen inputs
575
575
  --pipe`. It is optional, and an omitted selector defaults with the run routes' precedence: on a
576
576
  `method_ref` request, to the fetched package manifest's `main_pipe` (the package author's declared
577
577
  entry pipe); otherwise to the closure's declared `main_pipe`, which is what a single-bundle caller
578
578
  almost always wants. A closure that declares no `main_pipe` — or several, across domains — cannot
579
- be defaulted from its own declarations, so an omitted selector is a request-shape 422 there when
580
- no manifest `main_pipe` settles it (the same two arms the CLI rejects on).
579
+ be defaulted from its own declarations, so when no manifest `main_pipe` settles it a route that
580
+ needs one pipe refuses the omitted selector with an input 422, `EntryPipeNotFoundError` or
581
+ `EntryPipeAmbiguousError` (`crate_ops.resolve_requested_pipe`), never the request-shape
582
+ `ValidationError` of a malformed body.
581
583
  """
582
584
 
583
585
  pipe_ref: str | None = Field(
@@ -585,9 +587,9 @@ class MthdsPipeRequest(MthdsFilesRequest):
585
587
  min_length=1,
586
588
  max_length=MAX_PIPE_CODE_LEN,
587
589
  description=(
588
- "Qualified pipe ref (`domain.pipe_code`) to project. Optional — defaults to the fetched package manifest's "
590
+ "Qualified pipe ref (`domain.pipe_code`) to select. Optional — defaults to the fetched package manifest's "
589
591
  "`main_pipe` on a `method_ref` request, else to the closure's declared `main_pipe`; a closure declaring none, "
590
- "or several, with no manifest `main_pipe` to settle it, requires it explicitly."
592
+ "or several, with no manifest `main_pipe` to settle it, requires it unless `all_pipes` is set."
591
593
  ),
592
594
  )
593
595
 
@@ -596,8 +598,8 @@ ALLOW_SIGNATURES_DESCRIPTION = (
596
598
  "When true, the validation sweep tolerates unimplemented pipe signatures instead of rejecting the "
597
599
  "bundle (signatures dry-run trivially by minting a mock). Defaults to false (strict)."
598
600
  )
599
- """Shared by the two routes that still run the dry-run sweep (`/validate`, `/build/runner`) — the flag only
600
- parameterizes that sweep, so the static `/build/{inputs,output}` projections do not accept it."""
601
+ """Used by `/validate`, the one route that runs the dry-run sweep: the flag only parameterizes that sweep,
602
+ so the static crate routes do not accept it."""
601
603
 
602
604
 
603
605
  class MthdsContentsRequest(BaseModel):
@@ -606,7 +608,7 @@ class MthdsContentsRequest(BaseModel):
606
608
  This is the flat-list envelope. `/validate` uses it through this class (its `ValidateRequest`
607
609
  subclass adds a parallel `mthds_sources` for per-file source labels), and the run routes
608
610
  `/execute` and `/start` carry the same `mthds_contents` field independently on `RunRequest`.
609
- The newer Pipelex-API extension routes (`/resolve`, `/codegen`, `/build/*`) use
611
+ The newer Pipelex-API extension routes (`/resolve`, `/codegen`, `/pipe-io`) use
610
612
  `MthdsFilesRequest` instead (`files[]` pairing each content with its `source`, XOR a
611
613
  `method_ref`), which folds the source label into each entry. `/validate` keeps the flat-list
612
614
  shape deliberately: it is an MTHDS Protocol route, so changing its inline envelope is a
@@ -1,11 +0,0 @@
1
- from fastapi import APIRouter
2
-
3
- from .concept import router as concept_router
4
- from .models import router as models_router
5
- from .pipe_spec import router as pipe_spec_router
6
-
7
- router = APIRouter()
8
-
9
- router.include_router(models_router)
10
- router.include_router(concept_router)
11
- router.include_router(pipe_spec_router)
@@ -1,60 +0,0 @@
1
- """Concept endpoint — convert JSON concept spec to TOML."""
2
-
3
- import json
4
- from typing import Any
5
-
6
- from fastapi import APIRouter
7
- from pipelex.builder.operations.concept_ops import concept_spec_to_toml, parse_concept_spec
8
- from pydantic import BaseModel, Field, ValidationError, field_validator
9
-
10
- from pipelex_api.errors import raise_validation_error
11
- from pipelex_api.json_body import JsonBodyRoute
12
- from pipelex_api.limits import MAX_AGENT_SPEC_BYTES
13
-
14
- router = APIRouter(tags=["agent"], route_class=JsonBodyRoute)
15
-
16
-
17
- class BuildConceptRequest(BaseModel):
18
- spec: dict[str, Any] = Field(..., description="JSON concept specification.")
19
-
20
- @field_validator("spec")
21
- @classmethod
22
- def _bound_spec_size(cls, value: dict[str, Any]) -> dict[str, Any]:
23
- if len(json.dumps(value).encode("utf-8")) > MAX_AGENT_SPEC_BYTES:
24
- msg = f"spec exceeds {MAX_AGENT_SPEC_BYTES // 1024} KiB limit"
25
- raise ValueError(msg)
26
- return value
27
-
28
-
29
- class BuildConceptResponse(BaseModel):
30
- success: bool = Field(default=True, description="Whether the operation was successful")
31
- concept_code: str = Field(..., description="The concept code that was generated")
32
- toml: str = Field(..., description="Generated TOML content for the concept")
33
-
34
-
35
- @router.post("/build/concept")
36
- async def build_concept(request_data: BuildConceptRequest) -> BuildConceptResponse:
37
- """Convert a JSON concept spec to TOML format.
38
-
39
- A malformed spec surfaces as a Pydantic `ValidationError` — an API-owned
40
- 422. Pipelex domain failures propagate untouched to the global
41
- `PipelexError` handler in `pipelex_api.exception_handlers`.
42
-
43
- A malformed shape — a non-mapping `structure` (`{"structure": "string"}`)
44
- or a `structure` field that is neither a string nor a mapping
45
- (`{"structure": {"f": 42}}`) — is refused by `parse_concept_spec` itself
46
- with a typed `ConceptSpecError`, which that same handler answers. Nothing
47
- broader is caught here: a bare exception from pipelex is the signal of a
48
- real programming bug, and a route-level catch would mask it.
49
- """
50
- try:
51
- concept_spec = parse_concept_spec(request_data.spec)
52
- toml_content = concept_spec_to_toml(concept_spec)
53
-
54
- return BuildConceptResponse(
55
- success=True,
56
- concept_code=concept_spec.concept_code,
57
- toml=toml_content,
58
- )
59
- except ValidationError as exc:
60
- raise_validation_error(message=str(exc))
@@ -1,59 +0,0 @@
1
- """Pipe spec endpoint — convert JSON pipe spec to TOML."""
2
-
3
- import json
4
- from typing import Any
5
-
6
- from fastapi import APIRouter
7
- from pipelex.builder.operations.pipe_ops import parse_pipe_spec, pipe_spec_to_toml
8
- from pydantic import BaseModel, Field, ValidationError, field_validator
9
-
10
- from pipelex_api.errors import raise_validation_error
11
- from pipelex_api.json_body import JsonBodyRoute
12
- from pipelex_api.limits import MAX_AGENT_SPEC_BYTES
13
-
14
- router = APIRouter(tags=["agent"], route_class=JsonBodyRoute)
15
-
16
-
17
- class BuildPipeSpecRequest(BaseModel):
18
- pipe_type: str = Field(..., min_length=1, max_length=128, description="The pipe type (e.g. PipeLLM, PipeSequence, etc.).")
19
- spec: dict[str, Any] = Field(..., description="JSON pipe specification.")
20
-
21
- @field_validator("spec")
22
- @classmethod
23
- def _bound_spec_size(cls, value: dict[str, Any]) -> dict[str, Any]:
24
- if len(json.dumps(value).encode("utf-8")) > MAX_AGENT_SPEC_BYTES:
25
- msg = f"spec exceeds {MAX_AGENT_SPEC_BYTES // 1024} KiB limit"
26
- raise ValueError(msg)
27
- return value
28
-
29
-
30
- class BuildPipeSpecResponse(BaseModel):
31
- success: bool = Field(default=True, description="Whether the operation was successful")
32
- pipe_code: str = Field(..., description="The pipe code that was generated")
33
- pipe_type: str = Field(..., description="The pipe type")
34
- toml: str = Field(..., description="Generated TOML content for the pipe")
35
-
36
-
37
- @router.post("/build/pipe-spec")
38
- async def build_pipe_spec(request_data: BuildPipeSpecRequest) -> BuildPipeSpecResponse:
39
- """Convert a JSON pipe spec to TOML format.
40
-
41
- Two caller-mistake paths surface as a 422 here: a `ValidationError` from
42
- Pydantic when the spec shape doesn't match the chosen pipe type, and a
43
- `ValueError` from `parse_pipe_spec` when `pipe_type` is not one of the
44
- known pipe types (documented in `parse_pipe_spec`'s docstring; raised at
45
- exactly one site). Pipelex domain failures propagate untouched to the
46
- global `PipelexError` handler in `pipelex_api.exception_handlers`.
47
- """
48
- try:
49
- pipe_spec = parse_pipe_spec(request_data.spec, pipe_type=request_data.pipe_type)
50
- toml_content = pipe_spec_to_toml(pipe_spec)
51
-
52
- return BuildPipeSpecResponse(
53
- success=True,
54
- pipe_code=pipe_spec.pipe_code,
55
- pipe_type=request_data.pipe_type,
56
- toml=toml_content,
57
- )
58
- except (ValidationError, ValueError) as exc:
59
- raise_validation_error(message=str(exc))
@@ -1,11 +0,0 @@
1
- from fastapi import APIRouter
2
-
3
- from .inputs import router as inputs_router
4
- from .output import router as output_router
5
- from .runner import router as runner_router
6
-
7
- router = APIRouter()
8
-
9
- router.include_router(inputs_router)
10
- router.include_router(output_router)
11
- router.include_router(runner_router)
@@ -1,192 +0,0 @@
1
- import json
2
- from typing import Annotated, Any, Literal, Self, Union
3
-
4
- from fastapi import APIRouter
5
- from fastapi.responses import JSONResponse
6
- from pipelex.core.pipes.inputs.exceptions import NoInputsRequiredError
7
- from pipelex.pipe_machinery.rendering.input_renderer import InputsTemplateFormat, render_inputs, render_inputs_toml
8
- from pipelex.pipeline.exceptions import ValidateBundleError
9
- from pydantic import BaseModel, Field, model_validator
10
-
11
- from pipelex_api.json_body import JsonBodyRoute
12
- from pipelex_api.openapi_responses import PROBLEM_404_METHOD_PACKAGE, PROBLEM_501_METHOD_REF
13
- from pipelex_api.routes.pipelex.crate_ops import (
14
- CrateInvalidReport,
15
- RequestedPipe,
16
- invalid_crate_report_response,
17
- resolve_requested_crate,
18
- resolve_requested_pipe,
19
- teardown_current_library,
20
- )
21
- from pipelex_api.schemas.models import MthdsPipeRequest
22
-
23
- router = APIRouter(tags=["build"], route_class=JsonBodyRoute)
24
-
25
- INPUTS_GENERATED_MESSAGE = "Inputs template generated successfully"
26
- NO_INPUTS_MESSAGE = "This pipe declares no inputs — the template is empty."
27
-
28
-
29
- class BuildInputsRequest(MthdsPipeRequest):
30
- """The inputs-template request: the shared closure + pipe selectors, plus the two rendering axes.
31
-
32
- Both axes mirror `pipelex codegen inputs` exactly — `--format` and `--explicit` — and every
33
- combination of them is served, as on the CLI.
34
- """
35
-
36
- format: InputsTemplateFormat = Field(
37
- default=InputsTemplateFormat.JSON,
38
- description="Template encoding. `json` returns the parsed template in `inputs`; `toml` returns the raw text in `inputs_toml`.",
39
- )
40
- explicit: bool = Field(
41
- default=False,
42
- description=(
43
- "When true, emit the ceremonial `{concept, content}` envelope for every input. Defaults to false — the light, "
44
- "signature-driven shape that smart inputs accepts (a bare string for a Text input, and so on)."
45
- ),
46
- )
47
-
48
-
49
- class BuildInputsValidReport(BaseModel):
50
- """The 200 **valid** arm: the example inputs template for the requested pipe.
51
-
52
- The template rides **one of two fields, chosen by `format`** — `inputs` (parsed object) for
53
- `json`, `inputs_toml` (raw text) for `toml`. TOML cannot be carried as a parsed object without
54
- losing what makes it worth asking for (its concept comments and key order), and the JSON case
55
- must stay a real object, since that is what the deploy dialog and the SDKs consume. So the two
56
- are separate, honestly-typed fields, and the unused one is omitted from the body entirely.
57
-
58
- A pipe that declares no inputs is a *valid* verdict, not an error (the CLI likewise exits 0):
59
- the template is simply empty, and `message` says so.
60
- """
61
-
62
- is_valid: Literal[True] = True
63
- pipe_ref: str = Field(..., description="The qualified pipe the template was generated for — the resolved selector.")
64
- requested_pipe_ref: str | None = Field(
65
- default=None,
66
- description=(
67
- "The `pipe_ref` as submitted. Absent when it was omitted and defaulted — to the fetched package manifest's "
68
- "`main_pipe` on a `method_ref` request, else to the closure's declared `main_pipe`."
69
- ),
70
- )
71
- format: InputsTemplateFormat = Field(..., description="The template encoding (echo of the request).")
72
- explicit: bool = Field(..., description="Whether the ceremonial envelope shape was emitted (echo of the request).")
73
- inputs: dict[str, Any] | None = Field(
74
- default=None,
75
- description="The parsed inputs template — present exactly when `format` is `json`.",
76
- )
77
- inputs_toml: str | None = Field(
78
- default=None,
79
- description="The inputs template as TOML text — present exactly when `format` is `toml`.",
80
- )
81
- message: str = Field(default=INPUTS_GENERATED_MESSAGE, description="Status message")
82
-
83
- @model_validator(mode="after")
84
- def _template_field_matches_format(self) -> Self:
85
- # The invariant the two-field shape exists to keep: the caller can read the field its own
86
- # `format` names, without probing. A route bug that set the other one would be a silent
87
- # contract break, so it fails loudly here instead.
88
- match self.format:
89
- case InputsTemplateFormat.JSON:
90
- if self.inputs is None or self.inputs_toml is not None:
91
- msg = "format='json' must carry `inputs` and no `inputs_toml`"
92
- raise ValueError(msg)
93
- case InputsTemplateFormat.TOML:
94
- if self.inputs_toml is None or self.inputs is not None:
95
- msg = "format='toml' must carry `inputs_toml` and no `inputs`"
96
- raise ValueError(msg)
97
- return self
98
-
99
-
100
- # Discriminated 200 response union: the `/validate` discipline — the verdict rides `is_valid`, never
101
- # the HTTP status.
102
- BuildInputsResponse = Annotated[Union[BuildInputsValidReport, CrateInvalidReport], Field(discriminator="is_valid")]
103
-
104
-
105
- def _render_report(*, requested: BuildInputsRequest, requested_pipe: RequestedPipe) -> BuildInputsValidReport:
106
- """Render the requested pipe's declared inputs into the valid arm, on the `format` axis.
107
-
108
- A pipe with no inputs raises `NoInputsRequiredError` out of the engine renderers. That is not a
109
- failure — it is the honest answer "run this with nothing" — so it lands on the valid arm as an
110
- empty template, mirroring the CLI's exit-0.
111
- """
112
- inputs: dict[str, Any] | None = None
113
- inputs_toml: str | None = None
114
- message: str = INPUTS_GENERATED_MESSAGE
115
- match requested.format:
116
- case InputsTemplateFormat.JSON:
117
- try:
118
- inputs = json.loads(render_inputs(requested_pipe.pipe, explicit=requested.explicit))
119
- except NoInputsRequiredError:
120
- inputs = {}
121
- message = NO_INPUTS_MESSAGE
122
- case InputsTemplateFormat.TOML:
123
- try:
124
- inputs_toml = render_inputs_toml(requested_pipe.pipe, explicit=requested.explicit)
125
- except NoInputsRequiredError:
126
- inputs_toml = ""
127
- message = NO_INPUTS_MESSAGE
128
- return BuildInputsValidReport(
129
- pipe_ref=requested_pipe.ref,
130
- requested_pipe_ref=requested.pipe_ref,
131
- format=requested.format,
132
- explicit=requested.explicit,
133
- inputs=inputs,
134
- inputs_toml=inputs_toml,
135
- message=message,
136
- )
137
-
138
-
139
- @router.post(
140
- "/build/inputs",
141
- response_model=BuildInputsResponse,
142
- # On top of the composite router's shared 401/413/422/500: the `method_ref` closure selector the
143
- # envelope accepts but no server-side method registry resolves yet (shared with /resolve, /codegen).
144
- responses={404: PROBLEM_404_METHOD_PACKAGE, 501: PROBLEM_501_METHOD_REF},
145
- )
146
- async def build_inputs(request_data: BuildInputsRequest) -> JSONResponse:
147
- """Generate an example inputs template for a pipe (the inputs projection, per pipe).
148
-
149
- Rides the **same static core** as `POST /resolve` and `POST /codegen`: the closure is resolved to
150
- its normalized crate, the requested pipe is read live from the library that leaves loaded, and its
151
- *declared* inputs are rendered — the exact projection `pipelex codegen inputs` writes, on both of
152
- its axes (`format`, `explicit`).
153
-
154
- Static is the point: a template is a read of the pipe's declared IO, so there is **no dry-run
155
- sweep** here (that is `/validate`'s vocabulary, and `/build/runner`'s need). A valid verdict says
156
- the closure is structurally sound and the template matches what the pipe declares — it is *not* a
157
- promise the pipe runs. Ask `/validate` for that.
158
-
159
- This projection is deliberately **not** a `/codegen` kind: the templates are user-editable
160
- scaffolds, never stamped or locked, so they cannot ride the trust chain `/codegen`'s valid arm
161
- promises (see `CodegenRouteKind`).
162
-
163
- Response contract (the `/validate` discipline):
164
-
165
- - **Valid verdict (200, `is_valid: true`):** the template, in `inputs` or `inputs_toml` per `format`.
166
- - **Invalid verdict (200, `is_valid: false`):** the closure could not be parsed, loaded, or
167
- validated — `validation_errors[]` from pipelex's one shared builder; no template exists.
168
- - **No verdict (non-2xx):** a selection refusal — an unknown pipe ref, or an omitted `pipe_ref` that
169
- nothing defaults (no fetched-manifest `main_pipe`, and a closure declaring no — or several —
170
- `main_pipe`) — is an input 422 whose `error_type` is `EntryPipeNotFoundError` or
171
- `EntryPipeAmbiguousError`; a malformed closure selector is a request-shape 422 problem+json; a
172
- registry-form `method_ref` is a 501 until server-side method-registry resolution exists.
173
-
174
- An omitted `pipe_ref` defaults the way a run by address does: to the fetched package manifest's
175
- `main_pipe` on a `method_ref` request, else to the closure's own declared `main_pipe`.
176
- """
177
- try:
178
- resolved = resolve_requested_crate(request_data)
179
- except ValidateBundleError as validate_error:
180
- return invalid_crate_report_response(validate_error.to_error_report())
181
- try:
182
- requested_pipe = resolve_requested_pipe(
183
- resolved.crate,
184
- pipe_ref=request_data.pipe_ref,
185
- manifest_main_pipe=resolved.manifest_main_pipe,
186
- )
187
- report = _render_report(requested=request_data, requested_pipe=requested_pipe)
188
- # exclude_none drops the template field the `format` did not select (and an absent
189
- # `requested_pipe_ref`), so exactly the fields the caller's own request implies are present.
190
- return JSONResponse(content=report.model_dump(mode="json", by_alias=True, exclude_none=True))
191
- finally:
192
- teardown_current_library()
@@ -1,163 +0,0 @@
1
- import json
2
- from typing import Annotated, Any, Literal, Self, Union
3
-
4
- from fastapi import APIRouter
5
- from fastapi.responses import JSONResponse
6
- from pipelex.core.concepts.concept_representation_generator import ConceptRepresentationFormat
7
- from pipelex.pipe_machinery.rendering.output_renderer import render_output
8
- from pipelex.pipeline.exceptions import ValidateBundleError
9
- from pydantic import BaseModel, Field, model_validator
10
-
11
- from pipelex_api.errors import raise_validation_error
12
- from pipelex_api.json_body import JsonBodyRoute
13
- from pipelex_api.openapi_responses import PROBLEM_404_METHOD_PACKAGE, PROBLEM_501_METHOD_REF
14
- from pipelex_api.routes.pipelex.crate_ops import (
15
- CrateInvalidReport,
16
- RequestedPipe,
17
- invalid_crate_report_response,
18
- resolve_requested_crate,
19
- resolve_requested_pipe,
20
- teardown_current_library,
21
- )
22
- from pipelex_api.schemas.models import MthdsPipeRequest
23
-
24
- router = APIRouter(tags=["build"], route_class=JsonBodyRoute)
25
-
26
-
27
- class BuildOutputRequest(MthdsPipeRequest):
28
- """The output-representation request: the shared closure + pipe selectors, plus the format axis."""
29
-
30
- format: ConceptRepresentationFormat = Field(
31
- default=ConceptRepresentationFormat.SCHEMA,
32
- description=(
33
- "Representation to render. `schema` (JSON Schema) and `json` (example value) return a parsed object in "
34
- "`output`; `python` returns Python source in `output_python`."
35
- ),
36
- )
37
-
38
-
39
- class BuildOutputValidReport(BaseModel):
40
- """The 200 **valid** arm: the example output representation for the requested pipe.
41
-
42
- The representation rides **one of two fields, chosen by `format`**, for the same reason
43
- `/build/inputs` splits `inputs` / `inputs_toml`: `schema` and `json` are objects, `python` is
44
- source text. (Before this split the route parsed *every* format as JSON, so `format=python` was a
45
- hard 500 — it fed Python source to `json.loads`.)
46
- """
47
-
48
- is_valid: Literal[True] = True
49
- pipe_ref: str = Field(..., description="The qualified pipe the representation was generated for — the resolved selector.")
50
- requested_pipe_ref: str | None = Field(
51
- default=None,
52
- description=(
53
- "The `pipe_ref` as submitted. Absent when it was omitted and defaulted — to the fetched package manifest's "
54
- "`main_pipe` on a `method_ref` request, else to the closure's declared `main_pipe`."
55
- ),
56
- )
57
- format: ConceptRepresentationFormat = Field(..., description="The representation format (echo of the request).")
58
- output: dict[str, Any] | None = Field(
59
- default=None,
60
- description="The parsed output representation — present exactly when `format` is `schema` or `json`.",
61
- )
62
- output_python: str | None = Field(
63
- default=None,
64
- description="The output representation as Python source — present exactly when `format` is `python`.",
65
- )
66
- message: str = Field(default="Output representation generated successfully", description="Status message")
67
-
68
- @model_validator(mode="after")
69
- def _representation_field_matches_format(self) -> Self:
70
- # Same invariant as `/build/inputs`: the caller reads the field its own `format` names.
71
- match self.format:
72
- case ConceptRepresentationFormat.SCHEMA | ConceptRepresentationFormat.JSON:
73
- if self.output is None or self.output_python is not None:
74
- msg = "format='schema'/'json' must carry `output` and no `output_python`"
75
- raise ValueError(msg)
76
- case ConceptRepresentationFormat.PYTHON:
77
- if self.output_python is None or self.output is not None:
78
- msg = "format='python' must carry `output_python` and no `output`"
79
- raise ValueError(msg)
80
- return self
81
-
82
-
83
- # Discriminated 200 response union: the `/validate` discipline — the verdict rides `is_valid`, never
84
- # the HTTP status.
85
- BuildOutputResponse = Annotated[Union[BuildOutputValidReport, CrateInvalidReport], Field(discriminator="is_valid")]
86
-
87
-
88
- def _render_report(*, requested: BuildOutputRequest, requested_pipe: RequestedPipe) -> BuildOutputValidReport:
89
- """Render the requested pipe's declared output into the valid arm, on the `format` axis.
90
-
91
- `render_output` raises a bare `ValueError` (documented in its docstring) when the pipe's output is
92
- `native.Anything` and no concrete option can be determined — a fact about the *requested pipe*,
93
- not about the closure, so it is a no-verdict 422 rather than an invalid-crate verdict.
94
- """
95
- output: dict[str, Any] | None = None
96
- output_python: str | None = None
97
- try:
98
- rendered = render_output(requested_pipe.pipe, output_format=requested.format)
99
- except ValueError as exc:
100
- raise_validation_error(f"Cannot render the output representation of pipe '{requested_pipe.ref}': {exc}")
101
- match requested.format:
102
- case ConceptRepresentationFormat.SCHEMA | ConceptRepresentationFormat.JSON:
103
- output = json.loads(rendered)
104
- case ConceptRepresentationFormat.PYTHON:
105
- output_python = rendered
106
- return BuildOutputValidReport(
107
- pipe_ref=requested_pipe.ref,
108
- requested_pipe_ref=requested.pipe_ref,
109
- format=requested.format,
110
- output=output,
111
- output_python=output_python,
112
- )
113
-
114
-
115
- @router.post(
116
- "/build/output",
117
- response_model=BuildOutputResponse,
118
- # On top of the composite router's shared 401/413/422/500: the `method_ref` closure selector the
119
- # envelope accepts but no server-side method registry resolves yet (shared with /resolve, /codegen).
120
- responses={404: PROBLEM_404_METHOD_PACKAGE, 501: PROBLEM_501_METHOD_REF},
121
- )
122
- async def build_output(request_data: BuildOutputRequest) -> JSONResponse:
123
- """Generate an example output representation for a pipe (the output projection, per pipe).
124
-
125
- Rides the **same static core** as `POST /resolve`, `POST /codegen` and `POST /build/inputs`: the
126
- closure is resolved to its normalized crate, the requested pipe is read live from the library that
127
- leaves loaded, and its *declared* output is rendered.
128
-
129
- Static is the point: the representation is a read of the pipe's declared IO, so there is **no
130
- dry-run sweep** here. A valid verdict says the closure is structurally sound and the shape matches
131
- what the pipe declares — it is *not* a promise the pipe runs. Ask `/validate` for that.
132
-
133
- Response contract (the `/validate` discipline):
134
-
135
- - **Valid verdict (200, `is_valid: true`):** the representation, in `output` or `output_python` per `format`.
136
- - **Invalid verdict (200, `is_valid: false`):** the closure could not be parsed, loaded, or
137
- validated — `validation_errors[]`; no representation exists.
138
- - **No verdict (non-2xx):** a selection refusal — an unknown pipe ref, or an omitted `pipe_ref` that
139
- nothing defaults (no fetched-manifest `main_pipe`, and a closure declaring no — or several —
140
- `main_pipe`) — is an input 422 whose `error_type` is `EntryPipeNotFoundError` or
141
- `EntryPipeAmbiguousError`; a pipe whose `native.Anything` output has no determinable shape, or a
142
- malformed closure selector, is a request-shape 422 problem+json; a registry-form `method_ref` is a 501 until server-side
143
- method-registry resolution exists.
144
-
145
- An omitted `pipe_ref` defaults the way a run by address does: to the fetched package manifest's
146
- `main_pipe` on a `method_ref` request, else to the closure's own declared `main_pipe`.
147
- """
148
- try:
149
- resolved = resolve_requested_crate(request_data)
150
- except ValidateBundleError as validate_error:
151
- return invalid_crate_report_response(validate_error.to_error_report())
152
- try:
153
- requested_pipe = resolve_requested_pipe(
154
- resolved.crate,
155
- pipe_ref=request_data.pipe_ref,
156
- manifest_main_pipe=resolved.manifest_main_pipe,
157
- )
158
- report = _render_report(requested=request_data, requested_pipe=requested_pipe)
159
- # exclude_none drops the representation field the `format` did not select (and an absent
160
- # `requested_pipe_ref`), so exactly the fields the caller's own request implies are present.
161
- return JSONResponse(content=report.model_dump(mode="json", by_alias=True, exclude_none=True))
162
- finally:
163
- teardown_current_library()
@@ -1,236 +0,0 @@
1
- from typing import Annotated, Literal, Union
2
-
3
- from fastapi import APIRouter, Request
4
- from fastapi.responses import JSONResponse
5
- from mthds.package.manifest.schema import MTHDS_STANDARD_VERSION
6
- from pipelex.base_exceptions import PipelexUnexpectedError
7
- from pipelex.builder.runner_code import generate_runner_code
8
- from pipelex.codegen.emission import build_stamped_projection
9
- from pipelex.codegen.emitters.naming import runtime_to_emitted_class_names
10
- from pipelex.codegen.emitters.target import CodegenKind, CodegenTarget
11
- from pipelex.codegen.emitters.types_emitter import emit_types
12
- from pipelex.codegen.lock import CODEGEN_LOCK_FILENAME
13
- from pipelex.codegen.resolved_concepts import resolve_concepts_from_crate
14
- from pipelex.core.pipes.variable_multiplicity import parse_concept_with_multiplicity
15
- from pipelex.interpreter_hub import get_current_library_id_or_none, get_library_manager
16
- from pipelex.libraries.crate_normalization import normalize_crate
17
- from pipelex.libraries.pipe.exceptions import EntryPipeNotFoundError, PipeNotFoundError
18
- from pipelex.mthds_parsing.pipelex_bundle_blueprint import PipelexBundleBlueprint
19
- from pipelex.pipeline.bundle_validator import DryRunOutput, DryRunStatus
20
- from pipelex.pipeline.exceptions import ValidateBundleError
21
- from pipelex.pipeline.validate_bundle import validate_bundle
22
- from pipelex.system.caller_identity import CallerIdentity
23
- from pipelex.tools.misc.package_utils import get_package_version
24
- from pipelex.tools.typing.pydantic_utils import empty_list_factory_of
25
- from pydantic import BaseModel, Field
26
-
27
- from pipelex_api.errors import raise_validation_error
28
- from pipelex_api.json_body import JsonBodyRoute
29
- from pipelex_api.openapi_responses import PROBLEM_404_METHOD_PACKAGE, PROBLEM_501_METHOD_REF
30
- from pipelex_api.routes.pipelex.crate_ops import (
31
- CrateInvalidReport,
32
- GeneratedArtifact,
33
- invalid_crate_report_response,
34
- resolve_requested_pipe,
35
- selected_files,
36
- teardown_current_library,
37
- )
38
- from pipelex_api.routes.pipelex.pipeline import get_request_user_id
39
- from pipelex_api.schemas.models import ALLOW_SIGNATURES_DESCRIPTION, CallerAnalyticsGroupsMixin, MthdsPipeRequest
40
-
41
- router = APIRouter(tags=["build"], route_class=JsonBodyRoute)
42
-
43
-
44
- class BuildRunnerRequest(MthdsPipeRequest, CallerAnalyticsGroupsMixin):
45
- """The runner-script request: the shared closure + pipe selectors, plus the sweep's `allow_signatures`.
46
-
47
- Alone among the `/build/*` projections this route keeps `allow_signatures`, because alone among
48
- them it still runs the dry-run sweep — the flag only ever parameterized that sweep.
49
- """
50
-
51
- allow_signatures: bool = Field(default=False, description=ALLOW_SIGNATURES_DESCRIPTION)
52
-
53
-
54
- class RunnerStructures(BaseModel):
55
- """The typed-structures projection the runner script imports from (`from structures.structures import ...`).
56
-
57
- The same stamped `python-structures` artifacts + `codegen.lock` a local `pipelex build runner`
58
- scaffolds into `<output>/structures/` — write `artifacts` and the lock under `directory` and
59
- the returned `python_code` runs against them, with the offline `codegen check` passing there.
60
- """
61
-
62
- directory: str = Field(default="structures", description="Directory (relative to the runner script) to write the artifacts and lock into.")
63
- artifacts: list[GeneratedArtifact] = Field(
64
- default_factory=empty_list_factory_of(GeneratedArtifact),
65
- description="The stamped generated files (paths relative to `directory`).",
66
- )
67
- lock: str = Field(..., description="The lock content (TOML) tracking the artifact set — write verbatim inside `directory`.")
68
- lock_filename: str = Field(default=CODEGEN_LOCK_FILENAME, description="Filename the lock content must be written as.")
69
-
70
-
71
- class BuildRunnerValidReport(BaseModel):
72
- """The 200 **valid** arm: the runner script plus the structures projection it imports from."""
73
-
74
- is_valid: Literal[True] = True
75
- pipe_ref: str = Field(..., description="The qualified pipe the runner was generated for — the resolved selector.")
76
- requested_pipe_ref: str | None = Field(
77
- default=None,
78
- description=(
79
- "The `pipe_ref` as submitted. Absent when it was omitted and defaulted — to the fetched package manifest's "
80
- "`main_pipe` on a `method_ref` request, else to the closure's declared `main_pipe`."
81
- ),
82
- )
83
- python_code: str = Field(..., description="Generated Python script for running the pipeline, imports spelled with the emitted class names.")
84
- structures: RunnerStructures = Field(..., description="The typed-structures projection the script imports from.")
85
- message: str = Field(default="Runner code generated successfully", description="Status message")
86
-
87
-
88
- # Discriminated 200 response union: the `/validate` discipline — the verdict rides `is_valid`,
89
- # never the HTTP status (breaking change from the previous `success`-bool body + 422-on-invalid).
90
- BuildRunnerResponse = Annotated[Union[BuildRunnerValidReport, CrateInvalidReport], Field(discriminator="is_valid")]
91
-
92
-
93
- def _reject_if_requested_pipe_skipped(sweep_result: dict[str, DryRunOutput], *, pipe_ref: str) -> None:
94
- """Reject when the *requested* pipe was SKIPPED (its cross-package dependency is unresolved).
95
-
96
- `validate_pipes` records a pipe SKIPPED — rather than failing the whole sweep — when a controller
97
- references a sub-pipe in a package not included in the request (the cross-package tolerance shared
98
- with `/validate`). For a code-generation endpoint that is a footgun: `generate_runner_code` reads
99
- only the pipe's own inputs/output and never resolves sub-pipes, so it would happily emit runner
100
- code for a pipeline that cannot actually run. A SKIPPED requested pipe is a no-verdict condition
101
- (its dependency closure is absent from the request, so nothing could be diagnosed) → request-shape
102
- 422; unrelated SKIPPED pipes elsewhere in the bundle stay tolerated.
103
- """
104
- for output in sweep_result.values():
105
- if pipe_ref != output.pipe_ref:
106
- continue
107
- match output.status:
108
- case DryRunStatus.SKIPPED:
109
- detail = output.error_message or "its dependencies could not be resolved"
110
- raise_validation_error(f"Cannot generate runner code for pipe '{pipe_ref}': {detail}")
111
- case DryRunStatus.SUCCESS | DryRunStatus.FAILURE:
112
- return
113
-
114
-
115
- def _output_is_list(blueprints: list[PipelexBundleBlueprint], *, pipe_ref: str) -> bool:
116
- """Whether the requested pipe's declared output carries a list multiplicity marker (mirrors the CLI).
117
-
118
- Matched on the **qualified** ref. A blueprint's `pipe` map is keyed by bare code, so scanning by
119
- bare code alone would return the first blueprint declaring that code — which in a multi-domain
120
- closure can be a *different* pipe that merely shares the name, and whose multiplicity may differ.
121
- The runner would then be generated with the wrong output handling. Compare the owning domain too.
122
- """
123
- domain_code, _, pipe_code = pipe_ref.rpartition(".")
124
- for blueprint in blueprints:
125
- if blueprint.domain != domain_code or not blueprint.pipe or pipe_code not in blueprint.pipe:
126
- continue
127
- output_parse = parse_concept_with_multiplicity(blueprint.pipe[pipe_code].output)
128
- return output_parse.multiplicity is not None
129
- return False
130
-
131
-
132
- @router.post(
133
- "/build/runner",
134
- response_model=BuildRunnerResponse,
135
- # On top of the composite router's shared 401/413/422/500: the `method_ref` closure selector the
136
- # envelope accepts but no server-side method registry resolves yet (shared with /resolve, /codegen).
137
- responses={404: PROBLEM_404_METHOD_PACKAGE, 501: PROBLEM_501_METHOD_REF},
138
- )
139
- async def build_runner(request: Request, request_data: BuildRunnerRequest) -> JSONResponse:
140
- """Generate a Python runner script for a pipe, riding the codegen types projection.
141
-
142
- The one `/build/*` projection that is **not** static: a runner script is a promise the pipe can
143
- actually run, so this route keeps the dry-run sweep its siblings dropped (and with it
144
- `allow_signatures`, which only ever parameterized that sweep). `validate_bundle` opens a single
145
- library, loads the closure, sweeps, and on success leaves the library loaded + current; on failure
146
- it tears it down itself. On the success path the crate is read from that library, the
147
- `python-structures` projection is emitted and stamped, and the runner script is generated with the
148
- **emitted** class names — the same flow as a local `pipelex build runner`.
149
-
150
- The sweep is scoped to the requested pipe, so unrelated broken siblings do not block a good pipe.
151
- When `pipe_ref` is omitted the scope is not settled before the closure loads, so the whole closure
152
- is swept and the pipe then defaults the way a run by address does — to the fetched package
153
- manifest's `main_pipe` on a `method_ref` request, else to the closure's own declared `main_pipe` —
154
- a stricter verdict, and the honest one for a caller who did not say which pipe they meant.
155
-
156
- Response contract (the `/validate` discipline): an invalid closure — including a failed dry-run of
157
- the requested pipe — is a produced verdict: a **200** `is_valid: false` with the structured
158
- `validation_errors[]`. Non-2xx is reserved for no-verdict conditions: a selection refusal (an
159
- unknown pipe ref, an omitted `pipe_ref` that nothing defaults — no fetched-manifest `main_pipe`,
160
- and a closure declaring no, or several, `main_pipe`), an input 422 whose `error_type` is
161
- `EntryPipeNotFoundError` or `EntryPipeAmbiguousError`; a request-shape 422 for a requested pipe
162
- whose cross-package dependencies are absent from the request; a 501 for a registry-form `method_ref`, auth, server
163
- fault — RFC 7807 via the global handlers.
164
- """
165
- library_manager = get_library_manager()
166
- selection = selected_files(request_data)
167
-
168
- try:
169
- # If this raises, validate_bundle has already torn down its own library — nothing to clean up here.
170
- validate_result = await validate_bundle(
171
- mthds_contents=[item.content for item in selection.files],
172
- mthds_sources=[item.source for item in selection.files],
173
- allow_signatures=request_data.allow_signatures,
174
- dry_run_pipe_codes=[request_data.pipe_ref] if request_data.pipe_ref else None,
175
- # The sweep is done for the caller, so its dry runs and `pipe_dry_run` event name them.
176
- caller_identity=CallerIdentity.make_from_host(user_id=get_request_user_id(request), extras=request_data.analytics_groups),
177
- )
178
- except ValidateBundleError as validate_error:
179
- return invalid_crate_report_response(validate_error.to_error_report())
180
- except PipeNotFoundError as exc:
181
- # The engine deliberately lets this one through untranslated (see `translate_to_validate_bundle_error`)
182
- # so the caller can own it: a pipe ref naming nothing in the closure is an input 422, not an
183
- # invalid-closure verdict — nothing about the closure is wrong. It is re-raised as the entry-lookup
184
- # class the slice miss already is, so its `error_type` matches `resolve_requested_pipe`'s refusals.
185
- msg = f"Pipe '{request_data.pipe_ref}' not found in the submitted closure: {exc}"
186
- raise EntryPipeNotFoundError(msg) from exc
187
-
188
- # Success: validate_bundle left its library loaded + current. Build everything from it, then own its teardown.
189
- library_id = get_current_library_id_or_none()
190
- try:
191
- crate = library_manager.get_crate(library_id) if library_id else None
192
- if crate is None:
193
- # Unreachable after a successful in-memory validate (the blueprints were accumulated),
194
- # so a None crate is an internal invariant break — a server fault (5xx), never a
195
- # caller-facing verdict (mirrors resolve_crate_from_contents's identical guard).
196
- msg = "library crate unavailable after a successful bundle load"
197
- raise PipelexUnexpectedError(msg)
198
- normalized_crate = normalize_crate(crate, mthds_version=MTHDS_STANDARD_VERSION)
199
- requested_pipe = resolve_requested_pipe(
200
- normalized_crate,
201
- pipe_ref=request_data.pipe_ref,
202
- manifest_main_pipe=selection.manifest_main_pipe,
203
- )
204
-
205
- # The sweep tolerates a cross-package unresolved dependency by recording the pipe SKIPPED
206
- # instead of failing. Don't hand back runner code for the *requested* pipe in that state.
207
- _reject_if_requested_pipe_skipped(validate_result.dry_run_result, pipe_ref=requested_pipe.ref)
208
-
209
- emitted = emit_types(normalized_crate, target=CodegenTarget.PYTHON_STRUCTURES)
210
- projection = build_stamped_projection(
211
- emitted,
212
- crate_fingerprint=normalized_crate.fingerprint,
213
- engine_version=get_package_version(),
214
- kind=CodegenKind.TYPES,
215
- target=CodegenTarget.PYTHON_STRUCTURES,
216
- )
217
- class_name_overrides = runtime_to_emitted_class_names(resolve_concepts_from_crate(normalized_crate))
218
-
219
- python_code = generate_runner_code(
220
- pipe=requested_pipe.pipe,
221
- output_multiplicity=_output_is_list(validate_result.blueprints, pipe_ref=requested_pipe.ref),
222
- class_name_overrides=class_name_overrides,
223
- )
224
-
225
- report = BuildRunnerValidReport(
226
- pipe_ref=requested_pipe.ref,
227
- requested_pipe_ref=request_data.pipe_ref,
228
- python_code=python_code,
229
- structures=RunnerStructures(
230
- artifacts=[GeneratedArtifact(path=stamped.filename, content=stamped.content) for stamped in projection.files],
231
- lock=projection.lock_content,
232
- ),
233
- )
234
- return JSONResponse(content=report.model_dump(mode="json", by_alias=True, exclude_none=True))
235
- finally:
236
- teardown_current_library()
File without changes
File without changes