pipelex-api 0.90.0__tar.gz → 0.91.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.90.0 → pipelex_api-0.91.0}/PKG-INFO +9 -8
  2. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/README.md +7 -6
  3. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/method_tree_request.py +47 -18
  4. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/pipelex/crate_ops.py +7 -7
  5. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/pipelex/pipeline.py +71 -31
  6. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/pipelex/validate.py +9 -5
  7. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/schemas/models.py +33 -6
  8. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/.gitignore +0 -0
  9. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/LICENSE +0 -0
  10. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/hatch_build.py +0 -0
  11. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/__init__.py +0 -0
  12. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/api.toml +0 -0
  13. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/api_config.py +0 -0
  14. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/blob_store.py +0 -0
  15. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/blob_upload.py +0 -0
  16. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/bundle.py +0 -0
  17. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/bundle_resources.py +0 -0
  18. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/exception_handlers.py +0 -0
  19. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/exceptions.py +0 -0
  20. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/json_body.py +0 -0
  21. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/limits.py +0 -0
  22. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/main.py +0 -0
  23. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/method_cache.py +0 -0
  24. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/method_source.py +0 -0
  25. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/middleware.py +0 -0
  26. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/openapi_responses.py +0 -0
  27. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/openapi_schema.py +0 -0
  28. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/problem_document.py +0 -0
  29. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/problem_view.py +0 -0
  30. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/py.typed +0 -0
  31. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/request_refusals.py +0 -0
  32. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/__init__.py +0 -0
  33. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/blob_upload.py +0 -0
  34. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/health.py +0 -0
  35. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/pipelex/__init__.py +0 -0
  36. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/pipelex/agent/__init__.py +0 -0
  37. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/pipelex/agent/models.py +0 -0
  38. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/pipelex/blobs.py +0 -0
  39. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/pipelex/codegen.py +0 -0
  40. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/pipelex/method_selection.py +0 -0
  41. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/pipelex/pipe_io.py +0 -0
  42. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/pipelex/resolve.py +0 -0
  43. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/pipelex/tools.py +0 -0
  44. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/pipelex/utils.py +0 -0
  45. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/routes/version.py +0 -0
  46. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/run_extras_binding.py +0 -0
  47. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/schemas/__init__.py +0 -0
  48. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pipelex_api/security.py +0 -0
  49. {pipelex_api-0.90.0 → pipelex_api-0.91.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pipelex-api
3
- Version: 0.90.0
3
+ Version: 0.91.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.90.0
19
+ Requires-Dist: pipelex[anthropic,bedrock,fal,google,google-genai,mistralai]==0.91.0
20
20
  Requires-Dist: pyjwt>=2.10.1
21
21
  Requires-Dist: uvicorn>=0.37.0
22
22
  Provides-Extra: dev
@@ -115,26 +115,27 @@ The API is now running at `http://localhost:8081`. To customize behavior (enable
115
115
 
116
116
  # 🧪 Run your first pipeline
117
117
 
118
- Once `/health` is green, send an inline pipeline definition and inputs to `/v1/execute`. The example below summarizes a string with a one-pipe MTHDS bundle — no files, no auth, copy-paste:
118
+ Once `/health` is green, send a method and its inputs to `/v1/execute`. The method travels in `files`, a map from each file's path in the method's folder to that file's content. The example below summarizes a string with a method made of a single `.mthds` file, with no auth, ready to copy and paste:
119
119
 
120
120
  ```bash
121
121
  curl -s http://localhost:8081/v1/execute \
122
122
  -H "Content-Type: application/json" \
123
123
  -d '{
124
- "pipe_code": "summarize",
125
- "mthds_contents": ["domain = \"hello\"\nmain_pipe = \"summarize\"\n\n[pipe.summarize]\ntype = \"PipeLLM\"\ndescription = \"Summarize the input text in one sentence\"\ninputs = { text = \"Text\" }\noutput = \"Text\"\nprompt = \"Summarize in one sentence:\\n@text\"\n"],
124
+ "files": {
125
+ "summarize.mthds": "domain = \"hello\"\nmain_pipe = \"summarize\"\n\n[pipe.summarize]\ntype = \"PipeLLM\"\ndescription = \"Summarize the input text in one sentence\"\ninputs = { text = \"Text\" }\noutput = \"Text\"\nprompt = \"Summarize in one sentence:\\n@text\"\n"
126
+ },
126
127
  "inputs": { "text": "Pipelex turns plain-language pipeline definitions into reproducible AI workflows that run as HTTP endpoints." }
127
128
  }'
128
129
  ```
129
130
 
130
- You'll get back a JSON response with `state: "COMPLETED"` and the summary under `pipe_output.working_memory.root.<main_stuff_name>.content`.
131
+ You'll get back a JSON response with `state: "COMPLETED"` and the summary under `pipe_output.working_memory.root.<main_stuff_name>.content`. The run starts at the method's `main_pipe`; to start at another pipe of the method, name it in `pipe_code`.
131
132
 
132
133
  **Passing files (PDFs, images) as inputs.** Use the `Document` concept and point it at any HTTP(S) URL:
133
134
 
134
135
  ```json
135
136
  {
136
137
  "pipe_code": "your_pipe",
137
- "mthds_contents": ["...your MTHDS..."],
138
+ "files": { "your_method.mthds": "...your MTHDS..." },
138
139
  "inputs": {
139
140
  "cv": { "concept": "Document", "content": { "url": "https://example.com/resume.pdf" } }
140
141
  }
@@ -143,7 +144,7 @@ You'll get back a JSON response with `state: "COMPLETED"` and the summary under
143
144
 
144
145
  `Document` accepts public HTTP/HTTPS URLs, `pipelex-storage://` URIs, or base64 data URLs. For images, use the `Image` concept with the same `{ "url": "..." }` shape.
145
146
 
146
- For inline MTHDS in the request, `mthds_contents` is a JSON array of raw `.mthds` (TOML) file contents as strings — typically `[open("my_pipe.mthds").read()]` from a client. See the [Pipe Run page](https://docs.pipelex.com/latest/api-server/pipe-run/) for every supported input shape and the full `/execute` reference.
147
+ **Sending the method.** `files` holds the whole method, as the MTHDS standard's method tree: each key is a file's path relative to the method's folder, such as `summarize.mthds` or `templates/invoice.docx`, and each value is that file's content. A text file, such as a `.mthds` bundle or a Python module, is sent as its UTF-8 text in a JSON string, and a file that is not text, such as an office template or an image, as `{"base64": "..."}`. This server also keeps a blob store, which `GET /v1/version` reports as `blob_store: true`, so a client can send a file as `{"sha256": "...", "size": n}` instead: it asks `POST /v1/blobs/missing` which of its files the server lacks, uploads each of those to the target the answer names, and then sends the method with a hash entry for each file, so a method that has not changed is not uploaded again. The older `mthds_contents` field, a JSON array of `.mthds` texts, is still accepted, but it is being retired together with `bundle_b64` and `resources`, and a later release refuses them, so new clients send `files`. See the [Pipe Run page](https://docs.pipelex.com/latest/api-server/pipe-run/) for every supported input shape, the rules of [a method sent as a tree](https://docs.pipelex.com/latest/api-server/pipe-run/#sending-a-method-as-a-tree) and of [sending files by hash](https://docs.pipelex.com/latest/api-server/pipe-run/#sending-files-by-hash), and the full `/execute` reference.
147
148
 
148
149
  # 📈 How to scale Pipelex
149
150
 
@@ -69,26 +69,27 @@ The API is now running at `http://localhost:8081`. To customize behavior (enable
69
69
 
70
70
  # 🧪 Run your first pipeline
71
71
 
72
- Once `/health` is green, send an inline pipeline definition and inputs to `/v1/execute`. The example below summarizes a string with a one-pipe MTHDS bundle — no files, no auth, copy-paste:
72
+ Once `/health` is green, send a method and its inputs to `/v1/execute`. The method travels in `files`, a map from each file's path in the method's folder to that file's content. The example below summarizes a string with a method made of a single `.mthds` file, with no auth, ready to copy and paste:
73
73
 
74
74
  ```bash
75
75
  curl -s http://localhost:8081/v1/execute \
76
76
  -H "Content-Type: application/json" \
77
77
  -d '{
78
- "pipe_code": "summarize",
79
- "mthds_contents": ["domain = \"hello\"\nmain_pipe = \"summarize\"\n\n[pipe.summarize]\ntype = \"PipeLLM\"\ndescription = \"Summarize the input text in one sentence\"\ninputs = { text = \"Text\" }\noutput = \"Text\"\nprompt = \"Summarize in one sentence:\\n@text\"\n"],
78
+ "files": {
79
+ "summarize.mthds": "domain = \"hello\"\nmain_pipe = \"summarize\"\n\n[pipe.summarize]\ntype = \"PipeLLM\"\ndescription = \"Summarize the input text in one sentence\"\ninputs = { text = \"Text\" }\noutput = \"Text\"\nprompt = \"Summarize in one sentence:\\n@text\"\n"
80
+ },
80
81
  "inputs": { "text": "Pipelex turns plain-language pipeline definitions into reproducible AI workflows that run as HTTP endpoints." }
81
82
  }'
82
83
  ```
83
84
 
84
- You'll get back a JSON response with `state: "COMPLETED"` and the summary under `pipe_output.working_memory.root.<main_stuff_name>.content`.
85
+ You'll get back a JSON response with `state: "COMPLETED"` and the summary under `pipe_output.working_memory.root.<main_stuff_name>.content`. The run starts at the method's `main_pipe`; to start at another pipe of the method, name it in `pipe_code`.
85
86
 
86
87
  **Passing files (PDFs, images) as inputs.** Use the `Document` concept and point it at any HTTP(S) URL:
87
88
 
88
89
  ```json
89
90
  {
90
91
  "pipe_code": "your_pipe",
91
- "mthds_contents": ["...your MTHDS..."],
92
+ "files": { "your_method.mthds": "...your MTHDS..." },
92
93
  "inputs": {
93
94
  "cv": { "concept": "Document", "content": { "url": "https://example.com/resume.pdf" } }
94
95
  }
@@ -97,7 +98,7 @@ You'll get back a JSON response with `state: "COMPLETED"` and the summary under
97
98
 
98
99
  `Document` accepts public HTTP/HTTPS URLs, `pipelex-storage://` URIs, or base64 data URLs. For images, use the `Image` concept with the same `{ "url": "..." }` shape.
99
100
 
100
- For inline MTHDS in the request, `mthds_contents` is a JSON array of raw `.mthds` (TOML) file contents as strings — typically `[open("my_pipe.mthds").read()]` from a client. See the [Pipe Run page](https://docs.pipelex.com/latest/api-server/pipe-run/) for every supported input shape and the full `/execute` reference.
101
+ **Sending the method.** `files` holds the whole method, as the MTHDS standard's method tree: each key is a file's path relative to the method's folder, such as `summarize.mthds` or `templates/invoice.docx`, and each value is that file's content. A text file, such as a `.mthds` bundle or a Python module, is sent as its UTF-8 text in a JSON string, and a file that is not text, such as an office template or an image, as `{"base64": "..."}`. This server also keeps a blob store, which `GET /v1/version` reports as `blob_store: true`, so a client can send a file as `{"sha256": "...", "size": n}` instead: it asks `POST /v1/blobs/missing` which of its files the server lacks, uploads each of those to the target the answer names, and then sends the method with a hash entry for each file, so a method that has not changed is not uploaded again. The older `mthds_contents` field, a JSON array of `.mthds` texts, is still accepted, but it is being retired together with `bundle_b64` and `resources`, and a later release refuses them, so new clients send `files`. See the [Pipe Run page](https://docs.pipelex.com/latest/api-server/pipe-run/) for every supported input shape, the rules of [a method sent as a tree](https://docs.pipelex.com/latest/api-server/pipe-run/#sending-a-method-as-a-tree) and of [sending files by hash](https://docs.pipelex.com/latest/api-server/pipe-run/#sending-files-by-hash), and the full `/execute` reference.
101
102
 
102
103
  # 📈 How to scale Pipelex
103
104
 
@@ -36,8 +36,8 @@ import base64
36
36
  import json
37
37
  import shutil
38
38
  import tempfile
39
- from collections.abc import Generator, Mapping
40
- from contextlib import ExitStack, contextmanager
39
+ from collections.abc import AsyncGenerator, Generator, Mapping
40
+ from contextlib import ExitStack, asynccontextmanager, contextmanager
41
41
  from pathlib import Path
42
42
  from typing import Any, NamedTuple, NoReturn, cast
43
43
 
@@ -56,6 +56,7 @@ from pipelex.cli.installed_methods import PROVENANCE_FILENAME
56
56
  from pipelex.config import is_pipe_func_sandbox_hosted
57
57
  from pipelex.core.bundle_resources.bundle_resource import SUBMITTED_FILES_FIELD, root_escape_of
58
58
  from pipelex.errors.registry.pipelex_codes import PipelexErrorCode
59
+ from pipelex.libraries.method_load_threads import run_method_load
59
60
  from pipelex.libraries.written_trees import WRITTEN_METHOD_TREES, write_method_tree
60
61
  from pipelex.methods.package_reason import MethodPackageReason
61
62
  from pipelex.methods.structures_check import ensure_no_structured_content_in_library_sources, ensure_no_structured_content_python
@@ -778,6 +779,29 @@ class WrittenMethod(NamedTuple):
778
779
  tree: MethodTree
779
780
 
780
781
 
782
+ def _write_method(*, request_method: RequestMethod, releases: ExitStack) -> WrittenMethod:
783
+ """Write a method's trees into a temporary directory and hold each as written, each release pushed on `releases`.
784
+
785
+ The removal of the directory is pushed before anything is written, and each holding as soon as it is taken, so
786
+ closing `releases` undoes exactly what was done, however far the writing got. Blocking work.
787
+ """
788
+ temp_dir = Path(tempfile.mkdtemp(prefix="pipelex-method-"))
789
+ releases.callback(shutil.rmtree, temp_dir, ignore_errors=True)
790
+ method_dir = temp_dir / "tree"
791
+ written = write_method_tree(contents=request_method.tree.contents, destination=method_dir, expected_digest=request_method.tree.tree.digest)
792
+ releases.enter_context(WRITTEN_METHOD_TREES.holding(tree=written))
793
+ methods_dirs: list[Path] = []
794
+ for index_dependency, (address, dependency) in enumerate(request_method.dependencies.items()):
795
+ methods_dir = temp_dir / f"methods-{index_dependency}"
796
+ dependency_dir = methods_dir / dependency_dir_name(address=address)
797
+ dependency_written = write_method_tree(contents=dependency.contents, destination=dependency_dir, expected_digest=dependency.tree.digest)
798
+ releases.enter_context(WRITTEN_METHOD_TREES.holding(tree=dependency_written))
799
+ if dependency.has_python and is_pipe_func_sandbox_hosted():
800
+ ensure_no_structured_content_python(package_dir=dependency_dir, package_address=address)
801
+ methods_dirs.append(methods_dir)
802
+ return WrittenMethod(method_dir=method_dir, methods_dirs=methods_dirs or None, tree=written)
803
+
804
+
781
805
  @contextmanager
782
806
  def written_method(*, request_method: RequestMethod) -> Generator[WrittenMethod, None, None]:
783
807
  """Write a method's trees into a temporary directory, hold each as written, and remove them all on the way out.
@@ -786,20 +810,25 @@ def written_method(*, request_method: RequestMethod) -> Generator[WrittenMethod,
786
810
  `<name>/` in a methods directory of its own, the layout the engine's `methods_dirs` take, so two dependencies sharing
787
811
  a name never meet. On a sandbox-hosted deployment, a dependency tree declaring a Python structure class is refused,
788
812
  naming it by its address, as a fetched package is (`MethodStructuresRefusedError`, a 422).
813
+
814
+ Writing and removing the files is blocking work, done on the calling thread: asynchronous code uses
815
+ `written_method_async`.
789
816
  """
790
- with ExitStack() as stack:
791
- temp_dir = Path(tempfile.mkdtemp(prefix="pipelex-method-"))
792
- stack.callback(shutil.rmtree, temp_dir, ignore_errors=True)
793
- method_dir = temp_dir / "tree"
794
- written = write_method_tree(contents=request_method.tree.contents, destination=method_dir, expected_digest=request_method.tree.tree.digest)
795
- stack.enter_context(WRITTEN_METHOD_TREES.holding(tree=written))
796
- methods_dirs: list[Path] = []
797
- for index_dependency, (address, dependency) in enumerate(request_method.dependencies.items()):
798
- methods_dir = temp_dir / f"methods-{index_dependency}"
799
- dependency_dir = methods_dir / dependency_dir_name(address=address)
800
- dependency_written = write_method_tree(contents=dependency.contents, destination=dependency_dir, expected_digest=dependency.tree.digest)
801
- stack.enter_context(WRITTEN_METHOD_TREES.holding(tree=dependency_written))
802
- if dependency.has_python and is_pipe_func_sandbox_hosted():
803
- ensure_no_structured_content_python(package_dir=dependency_dir, package_address=address)
804
- methods_dirs.append(methods_dir)
805
- yield WrittenMethod(method_dir=method_dir, methods_dirs=methods_dirs or None, tree=written)
817
+ with ExitStack() as releases:
818
+ yield _write_method(request_method=request_method, releases=releases)
819
+
820
+
821
+ @asynccontextmanager
822
+ async def written_method_async(*, request_method: RequestMethod) -> AsyncGenerator[WrittenMethod, None]:
823
+ """`written_method` for asynchronous code: the trees are written, and removed, in the method-load pool, off the event loop.
824
+
825
+ The directory is removed and its trees released whatever happens, a refusal while writing, a failure of the block
826
+ and a cancellation included. `run_method_load` lets a cancellation through only once its call has ended, or at once
827
+ when no thread has taken the call yet, so the removal never runs while the writing is still at work, and a
828
+ cancellation repeated during the removal waits for it too.
829
+ """
830
+ releases = ExitStack()
831
+ try:
832
+ yield await run_method_load(_write_method, request_method=request_method, releases=releases)
833
+ finally:
834
+ await run_method_load(releases.close)
@@ -22,7 +22,6 @@ worker thread (`project_resolved_method`), never on the event loop: a slow resol
22
22
  requests, and the current library, a context variable, is bound, read and cleared in that one thread's context.
23
23
  """
24
24
 
25
- import asyncio
26
25
  from collections.abc import Callable
27
26
  from enum import StrEnum
28
27
  from typing import Literal, NamedTuple, NoReturn, TypeVar
@@ -34,6 +33,7 @@ from mthds.protocol.problem.registry import CoreCode
34
33
  from pipelex.errors.problem import ProblemView
35
34
  from pipelex.interpreter_hub import clear_current_library, get_current_library_id_or_none, get_library_manager, get_required_entry_pipe
36
35
  from pipelex.libraries.library_crate import LibraryCrate
36
+ from pipelex.libraries.method_load_threads import run_method_load
37
37
  from pipelex.libraries.pipe.exceptions import EntryPipeAmbiguousError, EntryPipeNotFoundError, PipeNotFoundError
38
38
  from pipelex.libraries.pipe.pipe_suggestions import MAIN_PIPE_DECLARATION
39
39
  from pipelex.methods.method_ref import looks_like_method_ref
@@ -160,11 +160,11 @@ async def project_resolved_method(
160
160
  """Resolve the method the request names into its normalized crate, and return what `project` makes of it.
161
161
 
162
162
  The method is read as `/validate` reads it (`select_request_method`), then written out as a run writes it and loaded
163
- alone as the caller's own library directory, its dependency trees as the packages it ships, in a worker thread.
164
- `project` runs in that thread with the freshly opened library loaded and current, so it can read live pipes, and the
165
- written tree held; both are released when it returns or raises. A method that does not resolve returns its invalid
166
- verdict instead, the `ValidateBundleError` carrying the structured validation items, with `project` never called and
167
- nothing left loaded.
163
+ alone as the caller's own library directory, its dependency trees as the packages it ships, in a worker thread of the
164
+ method-load pool (`run_method_load`). `project` runs in that thread with the freshly opened library loaded and
165
+ current, so it can read live pipes, and the written tree held; both are released when it returns or raises. A
166
+ method that does not resolve returns its invalid verdict instead, the `ValidateBundleError` carrying the structured
167
+ validation items, with `project` never called and nothing left loaded.
168
168
 
169
169
  Raises (as problem documents): a 422 `request.unsupported` for a registry-form `method_ref`; the refusals of
170
170
  `select_request_method`; the address-form fetch and location failures (`MethodRefError` subclasses); and whatever
@@ -184,7 +184,7 @@ async def project_resolved_method(
184
184
  selected = await select_request_method(
185
185
  request=request, method_ref=request_data.method_ref, resources=request_data.resources, read_scope=request_data.read_scope
186
186
  )
187
- return await asyncio.to_thread(
187
+ return await run_method_load(
188
188
  _project_written_method,
189
189
  request_method=selected.request_method,
190
190
  method_label=selected.method_label,
@@ -16,14 +16,16 @@ from pipelex.core.pipes.pipe_output import PipeOutput
16
16
  from pipelex.errors.bundle_validation_failure import BundleValidationFailure
17
17
  from pipelex.errors.problem import ProblemView
18
18
  from pipelex.errors.registry.pipelex_codes import PipelexErrorCode
19
+ from pipelex.libraries.method_load_threads import run_method_load
19
20
  from pipelex.libraries.method_requirements import ensure_served_method_requirements_met
21
+ from pipelex.libraries.method_tree_listing import MethodTreeListing, scoped_tree_listing
20
22
  from pipelex.pipe_run.delivery_assignment import DeliveryAssignment, StorageTarget, WebhookTarget
21
23
  from pipelex.pipe_run.exceptions import AsyncExecutionNotEnabledError
22
24
  from pipelex.pipe_run.pipe_run_protocol import PipeRunProtocol
23
25
  from pipelex.pipeline.exceptions import ValidateBundleError
24
26
  from pipelex.pipeline.pipeline_response import PipelexRunResultExecute, PipelexRunResultStart, RunState
25
27
  from pipelex.pipeline.pipeline_run_setup import pipeline_run_setup
26
- from pipelex.pipeline.runner import PipelexMTHDSProtocol
28
+ from pipelex.pipeline.runner import PipelexMTHDSProtocol, refuse_method_files
27
29
  from pipelex.pipeline.validate_bundle_translation import translate_to_validate_bundle_error
28
30
  from pipelex.reporting.usage_records import apply_tokens_usage_wire_shape
29
31
  from pipelex.runtime_bridge.exceptions import MissingBundleValidatorError, MissingOrchestratorError
@@ -52,7 +54,7 @@ from pipelex_api.method_tree_request import (
52
54
  select_tree_entry,
53
55
  tree_documents_of,
54
56
  with_resources,
55
- written_method,
57
+ written_method_async,
56
58
  )
57
59
  from pipelex_api.middleware import request_id_of
58
60
  from pipelex_api.openapi_responses import (
@@ -81,6 +83,7 @@ from pipelex_api.security import AuthMode, get_auth_mode
81
83
  if TYPE_CHECKING:
82
84
  from collections.abc import AsyncGenerator
83
85
 
86
+ from mthds.method_tree.file_entry import MethodFiles
84
87
  from mthds.protocol.pipe_output import VariableMultiplicity
85
88
  from mthds.protocol.pipeline_inputs import PipelineInputs
86
89
  from mthds.protocol.working_memory import WorkingMemoryAbstract
@@ -343,14 +346,17 @@ class ApiRunner(PipelexMTHDSProtocol):
343
346
  async def execute(
344
347
  self,
345
348
  pipe_code: str | None = None,
346
- mthds_contents: list[str] | None = None,
349
+ files: MethodFiles | None = None,
347
350
  inputs: PipelineInputs | WorkingMemoryAbstract[Any] | None = None,
348
351
  output_name: str | None = None,
349
352
  output_multiplicity: VariableMultiplicity | None = None,
350
353
  dynamic_output_concept_ref: str | None = None,
351
354
  extra: dict[str, Any] | None = None,
355
+ *,
356
+ mthds_contents: list[str] | None = None,
352
357
  delivery_assignment: DeliveryAssignment | None = None,
353
358
  request_id: str | None = None,
359
+ tree_listing: MethodTreeListing | None = None,
354
360
  requested_orchestration_mode: str | None = None,
355
361
  ) -> PipelexRunResultExecute:
356
362
  """Execute a method synchronously, dispatching by the resolved `orchestration_mode`.
@@ -374,7 +380,14 @@ class ApiRunner(PipelexMTHDSProtocol):
374
380
  `RunMetadata.request_id` for log correlation, so it reaches the job a worker is handed.
375
381
  `requested_orchestration_mode` is the optional per-request backend override
376
382
  (`PipelineApiExtras.orchestration_mode`).
383
+
384
+ The method's trees are listed once for the request: the check of its requirements lists them through
385
+ `tree_listing`, or a listing of its own when none is given, and the run loads them from that same listing.
377
386
  """
387
+ # The route resolves a method sent as `files` into a written tree before it calls this runner, which takes it
388
+ # as its library directory, so the map reaches the runner only from in-process code, and is refused as the
389
+ # base runner refuses it, before any backend is looked up.
390
+ refuse_method_files(files=files)
378
391
  # Resolve the effective orchestration mode FIRST — a per-request override the deployment
379
392
  # policy forbids is refused (422) here, before any library load / run registration. Mirrors start().
380
393
  api_config = get_api_config()
@@ -382,7 +395,8 @@ class ApiRunner(PipelexMTHDSProtocol):
382
395
  orchestrator = get_orchestrator_registry().get_optional(mode=orchestration_mode)
383
396
  if orchestrator is None:
384
397
  raise _missing_orchestrator_error(mode=orchestration_mode, api_config=api_config)
385
- await self._ensure_served_method_requirements_met()
398
+ listing = tree_listing if tree_listing is not None else MethodTreeListing()
399
+ await self._ensure_served_method_requirements_met(tree_listing=listing)
386
400
 
387
401
  # Dispatch the run through the mode-selected orchestrator by injecting it as this runner's
388
402
  # PipeRun, then delegate to the base execute, which owns the full run lifecycle. The
@@ -391,6 +405,7 @@ class ApiRunner(PipelexMTHDSProtocol):
391
405
  self._pipe_run = _OrchestratorPipeRun(orchestrator=orchestrator, orchestration_mode=orchestration_mode)
392
406
  return await super().execute(
393
407
  pipe_code=pipe_code,
408
+ files=files,
394
409
  mthds_contents=mthds_contents,
395
410
  inputs=inputs,
396
411
  output_name=output_name,
@@ -399,18 +414,21 @@ class ApiRunner(PipelexMTHDSProtocol):
399
414
  extra=extra,
400
415
  delivery_assignment=delivery_assignment,
401
416
  request_id=request_id,
417
+ tree_listing=listing,
402
418
  )
403
419
 
404
420
  @override
405
421
  async def start(
406
422
  self,
407
423
  pipe_code: str | None = None,
408
- mthds_contents: list[str] | None = None,
424
+ files: MethodFiles | None = None,
409
425
  inputs: PipelineInputs | WorkingMemoryAbstract[Any] | None = None,
410
426
  output_name: str | None = None,
411
427
  output_multiplicity: VariableMultiplicity | None = None,
412
428
  dynamic_output_concept_ref: str | None = None,
413
429
  extra: dict[str, Any] | None = None,
430
+ *,
431
+ mthds_contents: list[str] | None = None,
414
432
  pipeline_run_id: str | None = None,
415
433
  callback_urls: list[str] | None = None,
416
434
  request_id: str | None = None,
@@ -451,6 +469,10 @@ class ApiRunner(PipelexMTHDSProtocol):
451
469
  (`PipelineApiExtras.orchestration_mode`); it is resolved against the deployment's
452
470
  `api.toml` policy and a forbidden override is refused with a 422.
453
471
  """
472
+ # The route resolves a method sent as `files` into a written tree before it calls this runner, which takes it
473
+ # as its library directory, so the map reaches the runner only from in-process code, and is refused as the
474
+ # base runner refuses it.
475
+ refuse_method_files(files=files)
454
476
  if extra:
455
477
  # Nothing on the wire reaches `extra`: the route parses the extras itself and passes them by name, so a
456
478
  # value here is a fault of the in-process code calling this runner, never the caller's.
@@ -472,7 +494,10 @@ class ApiRunner(PipelexMTHDSProtocol):
472
494
  f"async-capable orchestration, and this deployment has none. Use /execute (synchronous) instead."
473
495
  )
474
496
  raise AsyncExecutionNotEnabledError(msg)
475
- await self._ensure_served_method_requirements_met()
497
+ # The method's trees are listed once for the request: the requirements check lists them, and the job's load
498
+ # takes them from the same listing.
499
+ tree_listing = MethodTreeListing()
500
+ await self._ensure_served_method_requirements_met(tree_listing=tree_listing)
476
501
  created_at = get_current_iso_timestamp()
477
502
  pipelex_inputs: PipelineInputs | WorkingMemory | None = cast("PipelineInputs | WorkingMemory | None", inputs)
478
503
 
@@ -506,6 +531,7 @@ class ApiRunner(PipelexMTHDSProtocol):
506
531
  submitted_files=self.submitted_files,
507
532
  pipeline_run_id=pipeline_run_id,
508
533
  request_id=request_id,
534
+ tree_listing=tree_listing,
509
535
  )
510
536
 
511
537
  delivery_assignment = DeliveryAssignment(
@@ -554,13 +580,15 @@ class ApiRunner(PipelexMTHDSProtocol):
554
580
  crate_fingerprint=pipe_job.job_metadata.run_metadata.crate_fingerprint,
555
581
  )
556
582
 
557
- async def _ensure_served_method_requirements_met(self) -> None:
583
+ async def _ensure_served_method_requirements_met(self, *, tree_listing: MethodTreeListing) -> None:
558
584
  """Refuse the request's method when its requirements.txt is not met, before anything loads or is dispatched.
559
585
 
560
586
  This runner's `library_dirs` are the directories it wrote the request's method tree into, the caller's own,
561
587
  whose load reports a requirements refusal as the caller's too; judging it here first refuses the run before
562
588
  any library opens or any run is registered, naming no path on this host. The tree is held as written, so its
563
- listing is the tree the request sent. The listing reads and hashes files, so it runs off the event loop.
589
+ listing is the tree the request sent. The listing reads and hashes files, so it runs off the event loop, in the method-load pool,
590
+ and it is made through `tree_listing`, which the caller hands to the request's load, so the load reads the trees
591
+ checked here without listing them again.
564
592
 
565
593
  Raises:
566
594
  MethodRequirementsFileError: The method's requirements.txt holds a line the standard refuses.
@@ -571,7 +599,9 @@ class ApiRunner(PipelexMTHDSProtocol):
571
599
  if not self.library_dirs:
572
600
  return
573
601
  library_dirs = [Path(library_dir) for library_dir in self.library_dirs]
574
- await asyncio.to_thread(ensure_served_method_requirements_met, library_dirs=library_dirs)
602
+ # In the method-load pool, whose cancellation waits for the check to end: the directories it lists are removed
603
+ # when the request's scope exits, which a client disconnect can make happen while the check still hashes them.
604
+ await run_method_load(ensure_served_method_requirements_met, library_dirs=library_dirs, tree_listing=tree_listing)
575
605
 
576
606
  async def validate_verdict(
577
607
  self,
@@ -621,6 +651,10 @@ class ApiRunner(PipelexMTHDSProtocol):
621
651
  arm hands the method's trees to its worker through. The route builds a validating runner with the
622
652
  `read_scope` its request names, `None` when it names none, and never with the caller's id a run
623
653
  would fall back to: nothing lists or erases what a validation would leave under that prefix.
654
+
655
+ The method's trees are listed once for the request: the requirements check lists them, and the validation is
656
+ called with that listing in scope (`scoped_tree_listing`), which the in-process validator loads the trees from,
657
+ while a validator dispatching to a worker never reads it.
624
658
  """
625
659
  # Resolve the effective mode FIRST — a per-request override the deployment policy forbids
626
660
  # is refused (422) here, before any validator dispatch / library load. Mirrors start().
@@ -631,33 +665,39 @@ class ApiRunner(PipelexMTHDSProtocol):
631
665
  raise _missing_bundle_validator_error(mode=orchestration_mode, api_config=api_config)
632
666
  library_dirs = [Path(library_dir) for library_dir in self.library_dirs] if self.library_dirs else None
633
667
  # The method's requirements.txt is judged here, on the runner, which holds the method's files and the package set
634
- # its PipeFunc steps run with, before any dispatch: a dispatched validator never reads these directories. A
635
- # refusal is the method's invalid verdict, as a run of it is refused before its files load.
668
+ # its PipeFunc steps run with, before any dispatch, so a refusal reaches the caller whichever validator runs. A
669
+ # worker a method tree's validation is dispatched to never reads these directories: it receives the method's
670
+ # trees through its method file store and judges their requirements again as it loads them. A refusal is the
671
+ # method's invalid verdict, as a run of it is refused before its files load.
672
+ tree_listing = MethodTreeListing()
636
673
  try:
637
674
  with translate_to_validate_bundle_error():
638
- await self._ensure_served_method_requirements_met()
675
+ await self._ensure_served_method_requirements_met(tree_listing=tree_listing)
639
676
  except ValidateBundleError as invalid_method:
640
677
  return BundleValidationFailure(
641
678
  problem=invalid_method.to_problem(view=ProblemView.OPERATOR),
642
679
  validation_errors=invalid_method.validation_error_items(),
643
680
  )
644
- verdict = await validator.validate_bundles(
645
- mthds_contents=mthds_contents,
646
- mthds_sources=mthds_sources,
647
- allow_signatures=allow_signatures,
648
- # A method directory is the whole method: no library directory loads beside it.
649
- library_dirs=None if method_dir is not None else library_dirs,
650
- method_dir=method_dir,
651
- methods_dirs=self.methods_dirs if method_dir is not None else None,
652
- # A validation is not a run, but its dry runs and its `pipe_dry_run` event are still
653
- # done for the caller this runner was built for — the user and groups a run would state.
654
- caller_identity=self.caller_identity,
655
- graph_pipe_code=graph_pipe_code,
656
- # The files submitted with the contents, which the validation's dry run reads, the template check included.
657
- submitted_files=self.submitted_files,
658
- # The scope the host named, under which a dispatched validator's worker reads the method's trees.
659
- read_scope=self.read_scope,
660
- )
681
+ # The validator seam takes no listing: the one the requirements check made is in scope around the call, so the
682
+ # in-process validator loads the trees it checked without listing them again.
683
+ with scoped_tree_listing(tree_listing=tree_listing):
684
+ verdict = await validator.validate_bundles(
685
+ mthds_contents=mthds_contents,
686
+ mthds_sources=mthds_sources,
687
+ allow_signatures=allow_signatures,
688
+ # A method directory is the whole method: no library directory loads beside it.
689
+ library_dirs=None if method_dir is not None else library_dirs,
690
+ method_dir=method_dir,
691
+ methods_dirs=self.methods_dirs if method_dir is not None else None,
692
+ # A validation is not a run, but its dry runs and its `pipe_dry_run` event are still
693
+ # done for the caller this runner was built for — the user and groups a run would state.
694
+ caller_identity=self.caller_identity,
695
+ graph_pipe_code=graph_pipe_code,
696
+ # The files submitted with the contents, which the validation's dry run reads, the template check included.
697
+ submitted_files=self.submitted_files,
698
+ # The scope the host named, under which a dispatched validator's worker reads the method's trees.
699
+ read_scope=self.read_scope,
700
+ )
661
701
  # The core seam types its valid arm at the protocol-level ValidationReport (a leaf type)
662
702
  # to stay import-acyclic in core; every registered validator in fact produces the canonical
663
703
  # PipelexValidationReport. Recover the precise type here — the single narrowing point — so
@@ -874,7 +914,7 @@ async def _request_method_of(*, run_request: RunRequest, body: bytes, blob_store
874
914
  async def _run_source(*, run_request: RunRequest, body: bytes, blob_scope: str) -> AsyncGenerator[_ResolvedRunSource, None]:
875
915
  """Resolve the request's run source: a method tree (`files`, `bundle_b64` or a `method_ref`), or the inline contents.
876
916
 
877
- A method tree is written into a temporary directory and held as written, then handed to the runner as its one
917
+ A method tree is written into a temporary directory in a worker thread and held as written, then handed to the runner as its one
878
918
  library directory, the caller's own, with the dependency trees it carries as `methods_dirs`: the load reads the
879
919
  tree whole, its `.mthds` files as the method's bundles and every other file as one a step may read, such as a
880
920
  `PipeDocGen` template or a `PipeFunc` module, and names each by its tree path. A refusal while loading it is the
@@ -912,7 +952,7 @@ async def _run_source(*, run_request: RunRequest, body: bytes, blob_scope: str)
912
952
  )
913
953
  return
914
954
  pipe_code = run_request.pipe_code or select_tree_entry(tree=request_method.tree, method_label=method_label).pipe_ref
915
- with written_method(request_method=request_method) as written:
955
+ async with written_method_async(request_method=request_method) as written:
916
956
  yield _ResolvedRunSource(
917
957
  mthds_contents=None,
918
958
  library_dirs=[str(written.method_dir)],
@@ -21,7 +21,7 @@ from pipelex_api.bundle_resources import decode_bundle_resources
21
21
  from pipelex_api.exception_handlers import problem_response
22
22
  from pipelex_api.exceptions import RequestFieldConflictError
23
23
  from pipelex_api.json_body import JsonBodyRoute
24
- from pipelex_api.method_tree_request import RequestMethod, select_tree_entry, written_method
24
+ from pipelex_api.method_tree_request import RequestMethod, select_tree_entry, written_method_async
25
25
  from pipelex_api.middleware import request_id_of
26
26
  from pipelex_api.openapi_responses import PROBLEM_403_READ_SCOPE, PROBLEM_404_METHOD_PACKAGE, PROBLEM_500_HELD_BLOBS, PROBLEM_501_RUN_POLICY
27
27
  from pipelex_api.problem_view import problem_view_of
@@ -79,7 +79,10 @@ def _resolve_validation_views(views: list[str]) -> set[ValidationView]:
79
79
 
80
80
 
81
81
  class ValidateRequest(MthdsContentsRequest, CallerAnalyticsGroupsMixin):
82
- """The shared `mthds_contents` + `allow_signatures` payload, plus optional per-file sources.
82
+ """The method to validate, in `files`, by `method_ref` or as `mthds_contents`, plus `allow_signatures` and optional per-file sources.
83
+
84
+ `mthds_contents`, and the `mthds_sources` and `resources` that serve it, are being retired: a later release
85
+ refuses them, and a method is sent in `files`, whose bundles are named by their tree paths.
83
86
 
84
87
  `mthds_sources`, when provided, pairs each `mthds_contents[i]` with a logical source (e.g. the
85
88
  file's path relative to the submitted directory). The runner threads it onto
@@ -91,7 +94,8 @@ class ValidateRequest(MthdsContentsRequest, CallerAnalyticsGroupsMixin):
91
94
  mthds_sources: list[str] | None = Field(
92
95
  default=None,
93
96
  description=(
94
- "Optional per-file sources, parallel to `mthds_contents`. When provided, each entry is threaded "
97
+ "Being retired with `mthds_contents`, and refused by a later release: the bundles of a method sent in `files` "
98
+ "are named by their tree paths. Optional per-file sources, parallel to `mthds_contents`. When provided, each entry is threaded "
95
99
  "onto the corresponding bundle's `source` so server-side validation errors carry a `source` pointing "
96
100
  "at the owning file. Must match `mthds_contents` in length when present."
97
101
  ),
@@ -366,7 +370,7 @@ async def validate_mthds(request: Request, request_data: ValidateRequest) -> JSO
366
370
  - **Either arm:** a member with no value is absent rather than `null`, at the arm's top level and in its
367
371
  problem items; the structured artifacts of the valid arm keep their own serialization.
368
372
  - **No verdict (non-2xx):** a malformed request body, an `mthds_sources` length mismatch, or
369
- both/neither of `mthds_contents` / `method_ref` is a request-shape **422**, and so is a forbidden
373
+ more or fewer than one of `files` / `method_ref` / `mthds_contents` is a request-shape **422**, and so is a forbidden
370
374
  `orchestration_mode` override; a host-wiring programmer error or a genuine orchestrator fault is a
371
375
  **5xx**; auth is **401/403**. A `method_ref` **resolution failure** is also a no-verdict condition —
372
376
  never an `is_valid: false`: each answers the status its code declares, such as a **422** for a
@@ -425,7 +429,7 @@ async def validate_mthds(request: Request, request_data: ValidateRequest) -> JSO
425
429
  entry = _optional_tree_entry(request_method=request_method, method_label=method_label)
426
430
  tree_default_pipe_ref = entry.pipe_ref if entry is not None else None
427
431
  default_pipe_ref_known = True
428
- with written_method(request_method=request_method) as written:
432
+ async with written_method_async(request_method=request_method) as written:
429
433
  verdict = await ApiRunner(
430
434
  library_dirs=[str(written.method_dir)],
431
435
  library_dirs_are_callers=True,
@@ -88,7 +88,7 @@ DEPENDENCIES_FIELD_DESCRIPTION = (
88
88
  "by. They are searched before the runner's installed copies, for this request alone. Accompanies `files`."
89
89
  )
90
90
 
91
- RESOURCES_FIELD_DESCRIPTION = (
91
+ _RESOURCES_FIELD_FACTS = (
92
92
  "The files submitted with the bundle that its steps read, such as a PipeDocGen step's `template_file` (an "
93
93
  "`.xlsx`, `.docx`, `.pptx` or `.html` template): a JSON object mapping each file's bundle-relative path, exactly "
94
94
  "as the step names it, to the file's bytes in standard base64. A path must be relative and written with forward "
@@ -102,6 +102,21 @@ RESOURCES_FIELD_DESCRIPTION = (
102
102
  "names by its digest: no file's bytes travel with the run, each being read where a step uses it."
103
103
  )
104
104
 
105
+ RESOURCES_FIELD_DESCRIPTION = (
106
+ f"{_RESOURCES_FIELD_FACTS} Being retired with `mthds_contents`, and refused by a later release: send these files in "
107
+ "the method's tree, in `files`."
108
+ )
109
+ """`resources` on the routes that also take `mthds_contents`, the run routes and `/validate`, with which it is retired."""
110
+
111
+ TREE_ROUTE_RESOURCES_FIELD_DESCRIPTION = (
112
+ f"{_RESOURCES_FIELD_FACTS} Being retired, and refused by a later release: a file the steps read travels inside the "
113
+ "method's tree, in `files`, at its path beside the bundle that names it."
114
+ )
115
+ """`resources` on the routes that take a method only as `files` or `method_ref`: `/resolve`, `/codegen` and `/pipe-io`."""
116
+
117
+ MTHDS_CONTENTS_RETIRING_NOTE = "Being retired, and refused by a later release: send the method in `files`."
118
+ """Opens the description of every request field that takes a method's bundles as `mthds_contents`."""
119
+
105
120
 
106
121
  class RunRequest(BaseModel):
107
122
  """Body of `POST /execute` — this server's typed request model.
@@ -124,7 +139,8 @@ class RunRequest(BaseModel):
124
139
 
125
140
  Attributes:
126
141
  pipe_code: Code of the pipe to execute.
127
- mthds_contents: List of MTHDS bundle contents to load.
142
+ mthds_contents: List of MTHDS bundle contents to load. Being retired, with `bundle_b64`
143
+ and `resources`, in favor of `files`.
128
144
  inputs: Inputs in PipelineInputs format, as plain JSON — Pydantic validation is
129
145
  skipped to preserve the flexible format (strings, lists, objects); the runtime
130
146
  shapes each value against the entry pipe's declared input.
@@ -154,7 +170,14 @@ class RunRequest(BaseModel):
154
170
  model_config = ConfigDict(extra="allow")
155
171
 
156
172
  pipe_code: str | None = None
157
- mthds_contents: list[str] | None = None
173
+ mthds_contents: list[str] | None = Field(
174
+ default=None,
175
+ description=(
176
+ f"{MTHDS_CONTENTS_RETIRING_NOTE} The method's `.mthds` files as raw strings, always an array, even for a single "
177
+ "file; with no `pipe_code`, a file must declare `main_pipe`. Mutually exclusive with `files`, `bundle_b64` and "
178
+ "`method_ref`."
179
+ ),
180
+ )
158
181
  inputs: Annotated[PipelineInputs | None, SkipValidation] = None
159
182
  output_name: str | None = None
160
183
  output_multiplicity: VariableMultiplicity | None = None
@@ -166,7 +189,8 @@ class RunRequest(BaseModel):
166
189
  "map of its entries, so the archive's names are judged by the method tree's rule as the keys of `files` are. "
167
190
  "The method packages it calls by address may travel with it under `.mthds/methods/<name>/`, each with its "
168
191
  "`METHODS.toml`; they are searched before the runner's installed copies, for this run alone. "
169
- "Mutually exclusive with `files`. Custom `.py` is only honored on a sandbox-hosted deployment."
192
+ "Mutually exclusive with `files`. Custom `.py` is only honored on a sandbox-hosted deployment. Being retired "
193
+ "with `mthds_contents`, and refused by a later release: send the method in `files`."
170
194
  ),
171
195
  )
172
196
  # Documents the wire only: the map is read from the request's raw JSON text (`pipelex_api.method_tree_request`),
@@ -660,7 +684,7 @@ class MethodTreeRequest(BaseModel):
660
684
  "until a method registry exists). Exactly one of `files` / `method_ref`."
661
685
  ),
662
686
  )
663
- resources: dict[str, str] | None = Field(default=None, description=RESOURCES_FIELD_DESCRIPTION)
687
+ resources: dict[str, str] | None = Field(default=None, description=TREE_ROUTE_RESOURCES_FIELD_DESCRIPTION)
664
688
  read_scope: str | None = Field(default=None, description=_TREE_READ_SCOPE_DESCRIPTION)
665
689
 
666
690
  @field_validator("read_scope")
@@ -735,7 +759,10 @@ class MthdsContentsRequest(BaseModel):
735
759
  default=None,
736
760
  min_length=1,
737
761
  max_length=MAX_MTHDS_FILES_PER_REQUEST,
738
- description="MTHDS contents to load (always an array, even for a single file). Exactly one of `mthds_contents` / `files` / `method_ref`.",
762
+ description=(
763
+ f"{MTHDS_CONTENTS_RETIRING_NOTE} MTHDS contents to load (always an array, even for a single file). "
764
+ "Exactly one of `mthds_contents` / `files` / `method_ref`."
765
+ ),
739
766
  )
740
767
  # Documents the wire only: the route reads the map from the request's raw JSON text, never from this field.
741
768
  files: Annotated[dict[str, FileEntry] | None, SkipValidation] = Field(
File without changes
File without changes