pipelex-api 0.71.1__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.
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/PKG-INFO +6 -5
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/README.md +4 -3
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/limits.py +0 -3
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/main.py +1 -1
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/method_source.py +3 -3
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/openapi_responses.py +1 -1
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/__init__.py +0 -2
- pipelex_api-0.72.0/pipelex_api/routes/pipelex/agent/__init__.py +7 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/agent/models.py +1 -1
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/codegen.py +2 -2
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/crate_ops.py +10 -13
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/validate.py +9 -8
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/schemas/models.py +11 -9
- pipelex_api-0.71.1/pipelex_api/routes/pipelex/agent/__init__.py +0 -11
- pipelex_api-0.71.1/pipelex_api/routes/pipelex/agent/concept.py +0 -60
- pipelex_api-0.71.1/pipelex_api/routes/pipelex/agent/pipe_spec.py +0 -59
- pipelex_api-0.71.1/pipelex_api/routes/pipelex/build/__init__.py +0 -11
- pipelex_api-0.71.1/pipelex_api/routes/pipelex/build/inputs.py +0 -192
- pipelex_api-0.71.1/pipelex_api/routes/pipelex/build/output.py +0 -163
- pipelex_api-0.71.1/pipelex_api/routes/pipelex/build/runner.py +0 -236
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/.gitignore +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/LICENSE +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/hatch_build.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/__init__.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/api.toml +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/api_config.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/bundle.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/disclosure.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/error_types.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/error_uri.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/errors.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/exception_handlers.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/json_body.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/method_cache.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/middleware.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/openapi_schema.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/problem_document.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/py.typed +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/routes/__init__.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/routes/health.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/pipe_io.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/pipeline.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/resolve.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/tools.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/routes/pipelex/utils.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/routes/version.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/schemas/__init__.py +0 -0
- {pipelex_api-0.71.1 → pipelex_api-0.72.0}/pipelex_api/security.py +0 -0
- {pipelex_api-0.71.1 → 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.
|
|
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.
|
|
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
|
|
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> -
|
|
@@ -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
|
|
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
|
|
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
|
|
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> -
|
|
@@ -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
|
|
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
|
|
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/)**
|
|
@@ -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`),
|
|
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 "
|
|
@@ -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`, `/
|
|
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
|
|
19
|
-
|
|
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 `/
|
|
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)
|
|
@@ -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
|
|
36
|
-
`POST /
|
|
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
|
|
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
|
|
12
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
98
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 `/
|
|
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
|
|
175
|
-
domains each declare a `main_pipe` is refused by `/
|
|
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
|
|
378
|
-
#
|
|
379
|
-
# the graph arm, which would otherwise draw the primary blueprint's
|
|
380
|
-
# different pipe when the two disagree, and no graph at all when the bundles
|
|
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`
|
|
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
|
-
"""
|
|
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
|
|
580
|
-
|
|
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
|
|
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
|
|
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
|
-
"""
|
|
600
|
-
|
|
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`, `/
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|