tangle-cli 0.0.1a1__tar.gz → 0.0.1a3__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.
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/PKG-INFO +34 -73
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/README.md +32 -71
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/__init__.py +4 -4
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/api_cli.py +8 -4
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/cli_helpers.py +7 -6
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/client.py +8 -1
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/component_publisher.py +3 -3
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/handler.py +1 -1
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/models.py +51 -5
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/openapi/codegen.py +46 -192
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/openapi/parser.py +4 -3
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_run_search.py +3 -3
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/quickstart.py +11 -12
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/pyproject.toml +5 -2
- tangle_cli-0.0.1a1/packages/tangle-cli/src/tangle_cli/generated_runtime.py +0 -43
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/api_schema.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/api_transport.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/args_container.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/artifacts.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/artifacts_cli.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/cli.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/cli_options.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/component_from_func.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/component_generator.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/component_inspector.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/components_cli.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/dynamic_discovery_client.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/generated_model_extensions.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/hydration_trust.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/logger.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/module_bundler.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/openapi/__init__.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_hydrator.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_run_annotations.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_run_details.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_run_manager.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_runner.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_runs_cli.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipelines.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipelines_cli.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/published_components_cli.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/py.typed +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/secrets.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/secrets_cli.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/utils.py +0 -0
- {tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/version_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: tangle-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.1a3
|
|
4
4
|
Summary: CLI for Tangle, the open-source ML pipeline orchestration platform
|
|
5
5
|
Author: Alexey Volkov, Tangle authors
|
|
6
6
|
Author-email: Alexey Volkov <alexey.volkov@ark-kun.com>
|
|
@@ -13,8 +13,8 @@ Requires-Dist: platformdirs>=4.10.0
|
|
|
13
13
|
Requires-Dist: pydantic>=2.0
|
|
14
14
|
Requires-Dist: pyyaml>=6.0
|
|
15
15
|
Requires-Dist: requests>=2.32.0
|
|
16
|
+
Requires-Dist: tangle-api==0.0.1a3
|
|
16
17
|
Requires-Dist: tomli>=2.0 ; python_full_version < '3.11'
|
|
17
|
-
Requires-Dist: tangle-api==0.1.0 ; extra == 'native'
|
|
18
18
|
Requires-Python: >=3.10
|
|
19
19
|
Project-URL: Homepage, https://tangleml.com
|
|
20
20
|
Project-URL: Documentation, https://tangleml.com/docs/
|
|
@@ -58,7 +58,7 @@ By default `tangle api` uses `--schema-source auto`, which means official static
|
|
|
58
58
|
|
|
59
59
|
`tangle sdk` commands are hand-written workflows. They can be:
|
|
60
60
|
|
|
61
|
-
- **local-only**: no generated
|
|
61
|
+
- **local-only**: no generated API bindings required, e.g. pipeline validation/layout and component generation;
|
|
62
62
|
- **API-backed**: use the generated client but add domain behavior, e.g. pipeline-run submit payload construction, hydration, artifact lookup, publishing/version checks, or config batching.
|
|
63
63
|
|
|
64
64
|
Current SDK groups include:
|
|
@@ -111,22 +111,22 @@ Use `--log-type none` for quiet machine-readable runs, and `--log-type file` to
|
|
|
111
111
|
The repository contains two Python import packages with different responsibilities:
|
|
112
112
|
|
|
113
113
|
- `tangle_cli` is hand-written. It contains CLI wiring, SDK/business helpers, local pipeline/component workflows, dynamic API discovery, codegen, shared runtime classes, logging, and extension classes.
|
|
114
|
-
- `tangle_api` is generated/
|
|
114
|
+
- `tangle_api` is generated/static. It contains checked-in generated Pydantic models, generated endpoint operation methods, and the official OpenAPI snapshot.
|
|
115
115
|
|
|
116
|
-
The default `tangle-cli` package
|
|
116
|
+
The default public `tangle-cli` package depends on the matching `tangle-api` package, so normal installs include the checked-in generated bindings used by static API-backed commands and the handwritten `TangleApiClient` wrapper:
|
|
117
117
|
|
|
118
118
|
```bash
|
|
119
|
-
pip install
|
|
119
|
+
pip install tangle-cli
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
In this workspace, `uv` installs the workspace `tangle-api` package for development and tests:
|
|
122
|
+
The `native` extra remains as a compatibility no-op alias for older install instructions. In this workspace, `uv` installs the workspace `tangle-api` package for development and tests:
|
|
123
123
|
|
|
124
124
|
```bash
|
|
125
125
|
uv run tangle api --help
|
|
126
126
|
uv run tangle sdk pipelines validate pipeline.yaml
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
Custom API/codegen users can still run codegen from the fully capable install; generating bindings does not require removing the official `tangle-api` package. For project-local generated APIs, generate into a local source tree such as `src/tangle_api/generated` (and `src/tangle_api/schema/openapi.json` when you want `tangle api --schema-source official`) and run from that project so local `src/tangle_api` shadows site-packages. For packaged custom APIs, publish/provide a distribution named `tangle-api` with a version compatible with this `tangle-cli` release (for example `0.0.1a3+yourorg` for a `tangle-cli` dependency on `tangle-api==0.0.1a3`) via a private index, `--find-links`, or uv sources. As an expert escape hatch, `--no-deps` installs only `tangle-cli` and skips all dependencies, so that environment must manually provide every required runtime dependency plus its generated/custom `tangle_api`; this is acceptable for controlled codegen/custom scenarios but not normal UX.
|
|
130
130
|
|
|
131
131
|
## Quick command examples
|
|
132
132
|
|
|
@@ -229,9 +229,9 @@ uv run tangle api reset-cache --base-url https://api.example
|
|
|
229
229
|
|
|
230
230
|
Schema source modes are:
|
|
231
231
|
|
|
232
|
-
- `--schema-source auto` (default): official static operations plus cached-only backend extensions when a cache exists.
|
|
233
|
-
- `--schema-source official`: only the checked-in official static schema
|
|
234
|
-
- `--schema-source cache`: only the schema previously written by `tangle api refresh` for the selected base URL.
|
|
232
|
+
- `--schema-source auto` (default): official static operations plus cached-only backend extensions when a cache exists. Normal `tangle-cli` installs include the `tangle-api` package needed for official operations; custom API projects can shadow or replace that package as described in the codegen section.
|
|
233
|
+
- `--schema-source official`: only the checked-in official static schema from `tangle-api` (or a compatible custom `tangle-api` package on your environment's import path).
|
|
234
|
+
- `--schema-source cache`: only the schema previously written by `tangle api refresh` for the selected base URL. This is the custom/source-checkout fallback when a consumer environment does not provide an importable `tangle_api.schema` package.
|
|
235
235
|
|
|
236
236
|
For resource help, put `--schema-source` on the resource group:
|
|
237
237
|
|
|
@@ -331,7 +331,7 @@ existing = client.find_existing_components(
|
|
|
331
331
|
|
|
332
332
|
`TangleApiClient` is handwritten in `tangle_cli.client` and inherits generated endpoint methods from `tangle_api.generated.operations.GeneratedTangleApiOperations`. The generated endpoint methods call the handwritten transport/request logic. Handwritten semantic helpers such as `find_existing_components(...)` return domain models and normalize common compatibility cases.
|
|
333
333
|
|
|
334
|
-
The top-level `import tangle_cli` is lightweight and does not import
|
|
334
|
+
The top-level `import tangle_cli` is lightweight and does not import static bindings eagerly. Normal installs include `tangle-api`; source checkouts or downstream embeddings may instead provide a local `tangle_api.generated` package before importing `tangle_cli.client`.
|
|
335
335
|
|
|
336
336
|
## Codegen/autogen from OpenAPI
|
|
337
337
|
|
|
@@ -362,6 +362,17 @@ uv run python -m tangle_cli.openapi.codegen \
|
|
|
362
362
|
--out src/tangle_api/generated
|
|
363
363
|
```
|
|
364
364
|
|
|
365
|
+
For a project-local custom API package, write both the schema snapshot and generated modules under that project's source tree, then run tools/tests from the project environment so `src/tangle_api` is earlier on `sys.path` than the official site-packages package:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
uv run python -m tangle_cli.openapi.codegen \
|
|
369
|
+
--openapi-url https://api.example/openapi.json \
|
|
370
|
+
--openapi src/tangle_api/schema/openapi.json \
|
|
371
|
+
--out src/tangle_api/generated
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
That project-local `tangle_api` package can be an editable/package source tree. If you ship the custom API bindings as a wheel or source distribution, use the distribution name `tangle-api` and a compatible version for the `tangle-cli` release you are using. A PEP 440 local version such as `0.0.1a3+yourorg` can satisfy a public `==0.0.1a3` dependency while distinguishing your private build. Provide that package through your private index, `--find-links`, or uv source configuration so the resolver chooses it instead of the public official package.
|
|
375
|
+
|
|
365
376
|
Generate from a backend checkout explicitly:
|
|
366
377
|
|
|
367
378
|
```bash
|
|
@@ -372,48 +383,29 @@ uv run --group codegen python -m tangle_cli.openapi.codegen \
|
|
|
372
383
|
|
|
373
384
|
Important codegen options:
|
|
374
385
|
|
|
375
|
-
- `--out`: directory that receives `__init__.py`, `models.py`, and `operations.py`. Defaults to `packages/tangle-api/src/tangle_api/generated`.
|
|
386
|
+
- `--out`: directory that receives `__init__.py`, `runtime.py`, `models.py`, and `operations.py`. Defaults to `packages/tangle-api/src/tangle_api/generated`.
|
|
376
387
|
- `--operations-class-name`: generated operations mixin class name. Defaults to `GeneratedTangleApiOperations`.
|
|
377
|
-
- `--model-extension-module`: importable module with `MODEL_EXTENSIONS`; repeat to compose modules.
|
|
378
388
|
- `--model-alias`: expose a stable public model name from one or more source schema names, e.g. `ComponentSpec=ComponentSpecOutput,ComponentSpecInput`.
|
|
379
389
|
- `--request-body-schema` / `--request-body-schema-file`: override a specific operation's JSON request-body schema without mutating the fetched OpenAPI document.
|
|
380
390
|
|
|
381
391
|
At runtime, more `tangle api ...` commands become available in two ways:
|
|
382
392
|
|
|
383
|
-
1. Static codegen: regenerate and install/provide a `tangle_api.generated`
|
|
393
|
+
1. Static codegen: regenerate and install/provide a local or packaged `tangle_api` package containing `tangle_api.generated` and, for official-schema CLI discovery, `tangle_api.schema`.
|
|
384
394
|
2. Dynamic cache: run `tangle api refresh --base-url ...` and use `--schema-source auto` or `--schema-source cache` to expose cached-only operations through the dynamic CLI.
|
|
385
395
|
|
|
386
|
-
|
|
396
|
+
The supported workaround hierarchy for custom API consumers is: prefer a project-local `src/tangle_api` package that shadows site-packages for that project; if distributing bindings, prefer a compatible private `tangle-api` distribution; reserve `--no-deps` installs or manual uninstalls of the official package for controlled expert environments where you manually provide all dependencies and the generated/custom `tangle_api` package.
|
|
387
397
|
|
|
388
|
-
|
|
398
|
+
## Runtime generated model extension pattern
|
|
399
|
+
|
|
400
|
+
`tangle_api.generated.models` is a leaf package and codegen emits plain generated Pydantic models directly:
|
|
389
401
|
|
|
390
402
|
```python
|
|
391
|
-
class
|
|
403
|
+
class ComponentSpec(TangleGeneratedModel):
|
|
392
404
|
name: Any = None
|
|
393
405
|
# generated OpenAPI fields...
|
|
394
|
-
|
|
395
|
-
class ComponentSpec(ComponentSpecExtensions, _ComponentSpecGenerated):
|
|
396
|
-
pass
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
The public class is a subclass rather than an alias because the public class name is the stable contract while the generated base can be regenerated. Subclassing lets the public class keep the OpenAPI/Pydantic fields from `_ComponentSpecGenerated` and add or override behavior through normal Python MRO.
|
|
400
|
-
|
|
401
|
-
Extension bases are placed to the **left** of the generated base:
|
|
402
|
-
|
|
403
|
-
```python
|
|
404
|
-
class ComponentSpec(ComponentSpecExtensions, _ComponentSpecGenerated):
|
|
405
|
-
pass
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
That means extension methods/properties override generated-base behavior when names overlap, while generated fields and `TangleGeneratedModel` runtime helpers such as `to_dict()` remain available.
|
|
409
|
-
|
|
410
|
-
The built-in default extension module is:
|
|
411
|
-
|
|
412
|
-
```text
|
|
413
|
-
tangle_cli.generated_model_extensions
|
|
414
406
|
```
|
|
415
407
|
|
|
416
|
-
|
|
408
|
+
Generated models do not import `tangle_cli` and codegen does not bake downstream extension modules into `tangle_api`. Downstream packages compose their own extended model namespace at runtime. In `tangle_cli.models`, the default CLI mixins are declared in `tangle_cli.generated_model_extensions`:
|
|
417
409
|
|
|
418
410
|
```python
|
|
419
411
|
MODEL_EXTENSIONS = {
|
|
@@ -423,42 +415,11 @@ MODEL_EXTENSIONS = {
|
|
|
423
415
|
}
|
|
424
416
|
```
|
|
425
417
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
Downstream projects can layer their own extensions:
|
|
429
|
-
|
|
430
|
-
```python
|
|
431
|
-
# my_project/tangle_model_extensions.py
|
|
432
|
-
class MyComponentSpecExtensions:
|
|
433
|
-
@property
|
|
434
|
-
def owning_team(self) -> str | None:
|
|
435
|
-
return (self.metadata or {}).get("annotations", {}).get("team")
|
|
436
|
-
|
|
437
|
-
MODEL_EXTENSIONS = {
|
|
438
|
-
"ComponentSpec": "MyComponentSpecExtensions",
|
|
439
|
-
}
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
```bash
|
|
443
|
-
uv run python -m tangle_cli.openapi.codegen \
|
|
444
|
-
--openapi-url https://api.example/openapi.json \
|
|
445
|
-
--out src/tangle_api/generated \
|
|
446
|
-
--model-extension-module my_project.tangle_model_extensions
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
The default module is applied first. Repeated `--model-extension-module` values are applied in order, and later/downstream modules become leftmost in the generated public class MRO, so they override earlier/default extensions. If two modules export the same extension class name, codegen imports them with deterministic aliases.
|
|
450
|
-
|
|
451
|
-
Pass an empty string to disable built-in default extensions:
|
|
452
|
-
|
|
453
|
-
```bash
|
|
454
|
-
uv run python -m tangle_cli.openapi.codegen \
|
|
455
|
-
--from-snapshot \
|
|
456
|
-
--model-extension-module ""
|
|
457
|
-
```
|
|
418
|
+
`tangle_cli.models.compose_models(...)` reads those mappings and creates subclasses in the `tangle_cli.models` namespace, e.g. `ComponentSpec(ComponentSpecExtensions, tangle_api.generated.models.ComponentSpec)`, without mutating `tangle_api.generated.models`. The generated operations layer also calls `_response_model(model_name, default)` so `TangleApiClient` can deserialize responses into the CLI-composed classes while the base `GeneratedTangleApiOperations` remains downstream-agnostic.
|
|
458
419
|
|
|
459
|
-
|
|
420
|
+
Downstream projects can use the same pattern in their own namespace: import base classes from `tangle_api.generated.models`, define method/property-only mixins plus a `MODEL_EXTENSIONS` mapping, and compose subclasses locally. Avoid global monkey-patching of `tangle_api.generated.models`.
|
|
460
421
|
|
|
461
|
-
|
|
422
|
+
Built-in `--model-alias` defaults still keep stable public model names such as `ComponentSpec` even when a backend schema uses names like `ComponentSpecOutput` or `ComponentSpecInput`.
|
|
462
423
|
|
|
463
424
|
## Extending SDK behavior
|
|
464
425
|
|
|
@@ -33,7 +33,7 @@ By default `tangle api` uses `--schema-source auto`, which means official static
|
|
|
33
33
|
|
|
34
34
|
`tangle sdk` commands are hand-written workflows. They can be:
|
|
35
35
|
|
|
36
|
-
- **local-only**: no generated
|
|
36
|
+
- **local-only**: no generated API bindings required, e.g. pipeline validation/layout and component generation;
|
|
37
37
|
- **API-backed**: use the generated client but add domain behavior, e.g. pipeline-run submit payload construction, hydration, artifact lookup, publishing/version checks, or config batching.
|
|
38
38
|
|
|
39
39
|
Current SDK groups include:
|
|
@@ -86,22 +86,22 @@ Use `--log-type none` for quiet machine-readable runs, and `--log-type file` to
|
|
|
86
86
|
The repository contains two Python import packages with different responsibilities:
|
|
87
87
|
|
|
88
88
|
- `tangle_cli` is hand-written. It contains CLI wiring, SDK/business helpers, local pipeline/component workflows, dynamic API discovery, codegen, shared runtime classes, logging, and extension classes.
|
|
89
|
-
- `tangle_api` is generated/
|
|
89
|
+
- `tangle_api` is generated/static. It contains checked-in generated Pydantic models, generated endpoint operation methods, and the official OpenAPI snapshot.
|
|
90
90
|
|
|
91
|
-
The default `tangle-cli` package
|
|
91
|
+
The default public `tangle-cli` package depends on the matching `tangle-api` package, so normal installs include the checked-in generated bindings used by static API-backed commands and the handwritten `TangleApiClient` wrapper:
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
|
-
pip install
|
|
94
|
+
pip install tangle-cli
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
In this workspace, `uv` installs the workspace `tangle-api` package for development and tests:
|
|
97
|
+
The `native` extra remains as a compatibility no-op alias for older install instructions. In this workspace, `uv` installs the workspace `tangle-api` package for development and tests:
|
|
98
98
|
|
|
99
99
|
```bash
|
|
100
100
|
uv run tangle api --help
|
|
101
101
|
uv run tangle sdk pipelines validate pipeline.yaml
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
Custom API/codegen users can still run codegen from the fully capable install; generating bindings does not require removing the official `tangle-api` package. For project-local generated APIs, generate into a local source tree such as `src/tangle_api/generated` (and `src/tangle_api/schema/openapi.json` when you want `tangle api --schema-source official`) and run from that project so local `src/tangle_api` shadows site-packages. For packaged custom APIs, publish/provide a distribution named `tangle-api` with a version compatible with this `tangle-cli` release (for example `0.0.1a3+yourorg` for a `tangle-cli` dependency on `tangle-api==0.0.1a3`) via a private index, `--find-links`, or uv sources. As an expert escape hatch, `--no-deps` installs only `tangle-cli` and skips all dependencies, so that environment must manually provide every required runtime dependency plus its generated/custom `tangle_api`; this is acceptable for controlled codegen/custom scenarios but not normal UX.
|
|
105
105
|
|
|
106
106
|
## Quick command examples
|
|
107
107
|
|
|
@@ -204,9 +204,9 @@ uv run tangle api reset-cache --base-url https://api.example
|
|
|
204
204
|
|
|
205
205
|
Schema source modes are:
|
|
206
206
|
|
|
207
|
-
- `--schema-source auto` (default): official static operations plus cached-only backend extensions when a cache exists.
|
|
208
|
-
- `--schema-source official`: only the checked-in official static schema
|
|
209
|
-
- `--schema-source cache`: only the schema previously written by `tangle api refresh` for the selected base URL.
|
|
207
|
+
- `--schema-source auto` (default): official static operations plus cached-only backend extensions when a cache exists. Normal `tangle-cli` installs include the `tangle-api` package needed for official operations; custom API projects can shadow or replace that package as described in the codegen section.
|
|
208
|
+
- `--schema-source official`: only the checked-in official static schema from `tangle-api` (or a compatible custom `tangle-api` package on your environment's import path).
|
|
209
|
+
- `--schema-source cache`: only the schema previously written by `tangle api refresh` for the selected base URL. This is the custom/source-checkout fallback when a consumer environment does not provide an importable `tangle_api.schema` package.
|
|
210
210
|
|
|
211
211
|
For resource help, put `--schema-source` on the resource group:
|
|
212
212
|
|
|
@@ -306,7 +306,7 @@ existing = client.find_existing_components(
|
|
|
306
306
|
|
|
307
307
|
`TangleApiClient` is handwritten in `tangle_cli.client` and inherits generated endpoint methods from `tangle_api.generated.operations.GeneratedTangleApiOperations`. The generated endpoint methods call the handwritten transport/request logic. Handwritten semantic helpers such as `find_existing_components(...)` return domain models and normalize common compatibility cases.
|
|
308
308
|
|
|
309
|
-
The top-level `import tangle_cli` is lightweight and does not import
|
|
309
|
+
The top-level `import tangle_cli` is lightweight and does not import static bindings eagerly. Normal installs include `tangle-api`; source checkouts or downstream embeddings may instead provide a local `tangle_api.generated` package before importing `tangle_cli.client`.
|
|
310
310
|
|
|
311
311
|
## Codegen/autogen from OpenAPI
|
|
312
312
|
|
|
@@ -337,6 +337,17 @@ uv run python -m tangle_cli.openapi.codegen \
|
|
|
337
337
|
--out src/tangle_api/generated
|
|
338
338
|
```
|
|
339
339
|
|
|
340
|
+
For a project-local custom API package, write both the schema snapshot and generated modules under that project's source tree, then run tools/tests from the project environment so `src/tangle_api` is earlier on `sys.path` than the official site-packages package:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
uv run python -m tangle_cli.openapi.codegen \
|
|
344
|
+
--openapi-url https://api.example/openapi.json \
|
|
345
|
+
--openapi src/tangle_api/schema/openapi.json \
|
|
346
|
+
--out src/tangle_api/generated
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
That project-local `tangle_api` package can be an editable/package source tree. If you ship the custom API bindings as a wheel or source distribution, use the distribution name `tangle-api` and a compatible version for the `tangle-cli` release you are using. A PEP 440 local version such as `0.0.1a3+yourorg` can satisfy a public `==0.0.1a3` dependency while distinguishing your private build. Provide that package through your private index, `--find-links`, or uv source configuration so the resolver chooses it instead of the public official package.
|
|
350
|
+
|
|
340
351
|
Generate from a backend checkout explicitly:
|
|
341
352
|
|
|
342
353
|
```bash
|
|
@@ -347,48 +358,29 @@ uv run --group codegen python -m tangle_cli.openapi.codegen \
|
|
|
347
358
|
|
|
348
359
|
Important codegen options:
|
|
349
360
|
|
|
350
|
-
- `--out`: directory that receives `__init__.py`, `models.py`, and `operations.py`. Defaults to `packages/tangle-api/src/tangle_api/generated`.
|
|
361
|
+
- `--out`: directory that receives `__init__.py`, `runtime.py`, `models.py`, and `operations.py`. Defaults to `packages/tangle-api/src/tangle_api/generated`.
|
|
351
362
|
- `--operations-class-name`: generated operations mixin class name. Defaults to `GeneratedTangleApiOperations`.
|
|
352
|
-
- `--model-extension-module`: importable module with `MODEL_EXTENSIONS`; repeat to compose modules.
|
|
353
363
|
- `--model-alias`: expose a stable public model name from one or more source schema names, e.g. `ComponentSpec=ComponentSpecOutput,ComponentSpecInput`.
|
|
354
364
|
- `--request-body-schema` / `--request-body-schema-file`: override a specific operation's JSON request-body schema without mutating the fetched OpenAPI document.
|
|
355
365
|
|
|
356
366
|
At runtime, more `tangle api ...` commands become available in two ways:
|
|
357
367
|
|
|
358
|
-
1. Static codegen: regenerate and install/provide a `tangle_api.generated`
|
|
368
|
+
1. Static codegen: regenerate and install/provide a local or packaged `tangle_api` package containing `tangle_api.generated` and, for official-schema CLI discovery, `tangle_api.schema`.
|
|
359
369
|
2. Dynamic cache: run `tangle api refresh --base-url ...` and use `--schema-source auto` or `--schema-source cache` to expose cached-only operations through the dynamic CLI.
|
|
360
370
|
|
|
361
|
-
|
|
371
|
+
The supported workaround hierarchy for custom API consumers is: prefer a project-local `src/tangle_api` package that shadows site-packages for that project; if distributing bindings, prefer a compatible private `tangle-api` distribution; reserve `--no-deps` installs or manual uninstalls of the official package for controlled expert environments where you manually provide all dependencies and the generated/custom `tangle_api` package.
|
|
362
372
|
|
|
363
|
-
|
|
373
|
+
## Runtime generated model extension pattern
|
|
374
|
+
|
|
375
|
+
`tangle_api.generated.models` is a leaf package and codegen emits plain generated Pydantic models directly:
|
|
364
376
|
|
|
365
377
|
```python
|
|
366
|
-
class
|
|
378
|
+
class ComponentSpec(TangleGeneratedModel):
|
|
367
379
|
name: Any = None
|
|
368
380
|
# generated OpenAPI fields...
|
|
369
|
-
|
|
370
|
-
class ComponentSpec(ComponentSpecExtensions, _ComponentSpecGenerated):
|
|
371
|
-
pass
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
The public class is a subclass rather than an alias because the public class name is the stable contract while the generated base can be regenerated. Subclassing lets the public class keep the OpenAPI/Pydantic fields from `_ComponentSpecGenerated` and add or override behavior through normal Python MRO.
|
|
375
|
-
|
|
376
|
-
Extension bases are placed to the **left** of the generated base:
|
|
377
|
-
|
|
378
|
-
```python
|
|
379
|
-
class ComponentSpec(ComponentSpecExtensions, _ComponentSpecGenerated):
|
|
380
|
-
pass
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
That means extension methods/properties override generated-base behavior when names overlap, while generated fields and `TangleGeneratedModel` runtime helpers such as `to_dict()` remain available.
|
|
384
|
-
|
|
385
|
-
The built-in default extension module is:
|
|
386
|
-
|
|
387
|
-
```text
|
|
388
|
-
tangle_cli.generated_model_extensions
|
|
389
381
|
```
|
|
390
382
|
|
|
391
|
-
|
|
383
|
+
Generated models do not import `tangle_cli` and codegen does not bake downstream extension modules into `tangle_api`. Downstream packages compose their own extended model namespace at runtime. In `tangle_cli.models`, the default CLI mixins are declared in `tangle_cli.generated_model_extensions`:
|
|
392
384
|
|
|
393
385
|
```python
|
|
394
386
|
MODEL_EXTENSIONS = {
|
|
@@ -398,42 +390,11 @@ MODEL_EXTENSIONS = {
|
|
|
398
390
|
}
|
|
399
391
|
```
|
|
400
392
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
Downstream projects can layer their own extensions:
|
|
404
|
-
|
|
405
|
-
```python
|
|
406
|
-
# my_project/tangle_model_extensions.py
|
|
407
|
-
class MyComponentSpecExtensions:
|
|
408
|
-
@property
|
|
409
|
-
def owning_team(self) -> str | None:
|
|
410
|
-
return (self.metadata or {}).get("annotations", {}).get("team")
|
|
411
|
-
|
|
412
|
-
MODEL_EXTENSIONS = {
|
|
413
|
-
"ComponentSpec": "MyComponentSpecExtensions",
|
|
414
|
-
}
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
```bash
|
|
418
|
-
uv run python -m tangle_cli.openapi.codegen \
|
|
419
|
-
--openapi-url https://api.example/openapi.json \
|
|
420
|
-
--out src/tangle_api/generated \
|
|
421
|
-
--model-extension-module my_project.tangle_model_extensions
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
The default module is applied first. Repeated `--model-extension-module` values are applied in order, and later/downstream modules become leftmost in the generated public class MRO, so they override earlier/default extensions. If two modules export the same extension class name, codegen imports them with deterministic aliases.
|
|
425
|
-
|
|
426
|
-
Pass an empty string to disable built-in default extensions:
|
|
427
|
-
|
|
428
|
-
```bash
|
|
429
|
-
uv run python -m tangle_cli.openapi.codegen \
|
|
430
|
-
--from-snapshot \
|
|
431
|
-
--model-extension-module ""
|
|
432
|
-
```
|
|
393
|
+
`tangle_cli.models.compose_models(...)` reads those mappings and creates subclasses in the `tangle_cli.models` namespace, e.g. `ComponentSpec(ComponentSpecExtensions, tangle_api.generated.models.ComponentSpec)`, without mutating `tangle_api.generated.models`. The generated operations layer also calls `_response_model(model_name, default)` so `TangleApiClient` can deserialize responses into the CLI-composed classes while the base `GeneratedTangleApiOperations` remains downstream-agnostic.
|
|
433
394
|
|
|
434
|
-
|
|
395
|
+
Downstream projects can use the same pattern in their own namespace: import base classes from `tangle_api.generated.models`, define method/property-only mixins plus a `MODEL_EXTENSIONS` mapping, and compose subclasses locally. Avoid global monkey-patching of `tangle_api.generated.models`.
|
|
435
396
|
|
|
436
|
-
|
|
397
|
+
Built-in `--model-alias` defaults still keep stable public model names such as `ComponentSpec` even when a backend schema uses names like `ComponentSpecOutput` or `ComponentSpecInput`.
|
|
437
398
|
|
|
438
399
|
## Extending SDK behavior
|
|
439
400
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""tangle-cli public API.
|
|
2
2
|
|
|
3
|
-
The package import is intentionally lightweight:
|
|
4
|
-
|
|
5
|
-
Import ``tangle_cli.client.TangleApiClient``
|
|
6
|
-
bindings
|
|
3
|
+
The package import is intentionally lightweight: static API bindings live in
|
|
4
|
+
``tangle_api.generated`` (included by default in public installs, or supplied by
|
|
5
|
+
source/downstream environments). Import ``tangle_cli.client.TangleApiClient``
|
|
6
|
+
explicitly when generated bindings should be loaded.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from importlib.metadata import PackageNotFoundError
|
|
@@ -503,7 +503,8 @@ def _schema_for_current_invocation() -> dict[str, Any] | None:
|
|
|
503
503
|
raise SystemExit(
|
|
504
504
|
f"No cached OpenAPI schema for {_normalize_base_url(base_url)}. "
|
|
505
505
|
"Run `tangle api refresh` with the same --base-url/--auth-header/--header options, "
|
|
506
|
-
"or
|
|
506
|
+
"or use a tangle-cli environment with an official or custom tangle-api package "
|
|
507
|
+
"that provides tangle_api.schema."
|
|
507
508
|
)
|
|
508
509
|
return cached
|
|
509
510
|
|
|
@@ -571,9 +572,12 @@ def _api_tail_requests_help(api_tail: list[str]) -> bool:
|
|
|
571
572
|
|
|
572
573
|
def _missing_official_schema_message() -> str:
|
|
573
574
|
return (
|
|
574
|
-
"Official static Tangle API commands require
|
|
575
|
-
"
|
|
576
|
-
"
|
|
575
|
+
"Official static Tangle API commands require a tangle-api package "
|
|
576
|
+
"because the OpenAPI snapshot lives in tangle_api.schema. Normal "
|
|
577
|
+
"tangle-cli installs include the official package; custom generated "
|
|
578
|
+
"API projects should run with a local src/tangle_api package that "
|
|
579
|
+
"shadows site-packages or install a compatible private tangle-api "
|
|
580
|
+
"distribution. Otherwise run `tangle api refresh` and use "
|
|
577
581
|
"`--schema-source cache` for cached backend operations."
|
|
578
582
|
)
|
|
579
583
|
|
|
@@ -66,8 +66,8 @@ def api_arg_specs(
|
|
|
66
66
|
class LazyTangleApiClient:
|
|
67
67
|
"""Instantiate the generated API client only when a command uses it.
|
|
68
68
|
|
|
69
|
-
Importing CLI modules must
|
|
70
|
-
without
|
|
69
|
+
Importing CLI modules must not eagerly load generated bindings, so local-only
|
|
70
|
+
commands can run without importing ``tangle_api``. This proxy delays importing and
|
|
71
71
|
constructing ``TangleApiClient`` until an API method is actually accessed,
|
|
72
72
|
while keeping CLI-friendly error wording in the CLI helper layer.
|
|
73
73
|
"""
|
|
@@ -85,9 +85,10 @@ class LazyTangleApiClient:
|
|
|
85
85
|
except ModuleNotFoundError as exc:
|
|
86
86
|
if exc.name == "tangle_api":
|
|
87
87
|
raise SystemExit(
|
|
88
|
-
"
|
|
89
|
-
f"{self.command_name}. Install tangle-cli
|
|
90
|
-
"a local tangle_api
|
|
88
|
+
"Generated Tangle API bindings are required for "
|
|
89
|
+
f"{self.command_name}. Install the default tangle-cli package "
|
|
90
|
+
"with tangle-api, run from a project where local src/tangle_api "
|
|
91
|
+
"shadows site-packages, or install a compatible custom tangle-api package."
|
|
91
92
|
) from exc
|
|
92
93
|
raise
|
|
93
94
|
|
|
@@ -97,7 +98,7 @@ class LazyTangleApiClient:
|
|
|
97
98
|
return self._client
|
|
98
99
|
|
|
99
100
|
def require_available(self) -> None:
|
|
100
|
-
"""Materialize the client so CLI commands fail before
|
|
101
|
+
"""Materialize the client so CLI commands fail before helper imports."""
|
|
101
102
|
|
|
102
103
|
self._get_client()
|
|
103
104
|
|
|
@@ -26,11 +26,13 @@ from .api_transport import (
|
|
|
26
26
|
log_http_exchange,
|
|
27
27
|
tangle_verbose_enabled,
|
|
28
28
|
)
|
|
29
|
-
from tangle_api.generated.models import ComponentSpec, GetExecutionInfoResponse
|
|
30
29
|
from tangle_api.generated.operations import GeneratedTangleApiOperations
|
|
30
|
+
from . import models as _cli_models
|
|
31
31
|
from .logger import Logger, _null_logger, get_default_logger
|
|
32
32
|
from .models import (
|
|
33
33
|
ComponentInfo,
|
|
34
|
+
ComponentSpec,
|
|
35
|
+
GetExecutionInfoResponse,
|
|
34
36
|
GraphExecutionState,
|
|
35
37
|
PipelineRun,
|
|
36
38
|
RunDetails,
|
|
@@ -78,6 +80,11 @@ class TangleApiClient(GeneratedTangleApiOperations):
|
|
|
78
80
|
self.session = session or requests.Session()
|
|
79
81
|
self.include_env_credentials = include_env_credentials
|
|
80
82
|
|
|
83
|
+
def _response_model(self, model_name: str, default: Any) -> Any:
|
|
84
|
+
"""Use CLI-composed models for generated operation deserialization."""
|
|
85
|
+
|
|
86
|
+
return getattr(_cli_models, model_name, default)
|
|
87
|
+
|
|
81
88
|
def set_verbose(self, enabled: bool) -> None:
|
|
82
89
|
"""Enable or disable request logging."""
|
|
83
90
|
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/component_publisher.py
RENAMED
|
@@ -22,7 +22,7 @@ from .handler import TangleCliHandler
|
|
|
22
22
|
from .logger import Logger
|
|
23
23
|
|
|
24
24
|
if TYPE_CHECKING:
|
|
25
|
-
from
|
|
25
|
+
from tangle_cli.models import ComponentSpec
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class ProcessingOutcome(str, Enum):
|
|
@@ -171,12 +171,12 @@ class ComponentPublisher(TangleCliHandler):
|
|
|
171
171
|
if self.component_spec_model is not None:
|
|
172
172
|
return self.component_spec_model
|
|
173
173
|
try:
|
|
174
|
-
from
|
|
174
|
+
from tangle_cli.models import ComponentSpec
|
|
175
175
|
except ModuleNotFoundError as exc:
|
|
176
176
|
if exc.name == "tangle_api":
|
|
177
177
|
raise RuntimeError(
|
|
178
178
|
"Native generated Tangle API bindings are required for component publishing. "
|
|
179
|
-
"Install tangle-cli
|
|
179
|
+
"Install the default tangle-cli package with tangle-api, run from a project where local src/tangle_api shadows site-packages, or install a compatible custom tangle-api package."
|
|
180
180
|
) from exc
|
|
181
181
|
raise
|
|
182
182
|
return ComponentSpec
|
|
@@ -64,7 +64,7 @@ class TangleCliHandler:
|
|
|
64
64
|
if exc.name == "tangle_api":
|
|
65
65
|
self.log.error(
|
|
66
66
|
"❌ Native generated Tangle API bindings are required for Tangle API operations. "
|
|
67
|
-
"Install tangle-cli
|
|
67
|
+
"Install the default tangle-cli package with tangle-api, run from a project where local src/tangle_api shadows site-packages, or install a compatible custom tangle-api package."
|
|
68
68
|
)
|
|
69
69
|
return None
|
|
70
70
|
raise
|
|
@@ -11,9 +11,56 @@ from __future__ import annotations
|
|
|
11
11
|
from dataclasses import asdict, dataclass, field
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
-
from tangle_api.generated
|
|
14
|
+
from tangle_api.generated import models as _generated_models
|
|
15
15
|
|
|
16
16
|
from .artifacts import ArtifactComponentQuery, ArtifactInfo
|
|
17
|
+
from . import generated_model_extensions as _cli_model_extensions
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def extend_model(public_name: str, base: type[Any], *mixins: type[Any]) -> type[Any]:
|
|
21
|
+
"""Compose a generated model with downstream mixins in this namespace."""
|
|
22
|
+
|
|
23
|
+
if not mixins:
|
|
24
|
+
return base
|
|
25
|
+
model = type(public_name, (*mixins, base), {"__module__": __name__})
|
|
26
|
+
model_rebuild = getattr(model, "model_rebuild", None)
|
|
27
|
+
if callable(model_rebuild):
|
|
28
|
+
model_rebuild()
|
|
29
|
+
return model
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def compose_models(generated_models_module: Any, *extension_modules: Any) -> dict[str, type[Any]]:
|
|
33
|
+
"""Return generated models composed with MODEL_EXTENSIONS mappings.
|
|
34
|
+
|
|
35
|
+
Extension modules declare ``MODEL_EXTENSIONS = {"ModelName": "MixinName"}``.
|
|
36
|
+
Composition happens in this downstream namespace and never mutates
|
|
37
|
+
``tangle_api.generated.models``.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
composed: dict[str, type[Any]] = {}
|
|
41
|
+
public_names = getattr(generated_models_module, "__all__", None)
|
|
42
|
+
if public_names is None:
|
|
43
|
+
public_names = [
|
|
44
|
+
name
|
|
45
|
+
for name, value in vars(generated_models_module).items()
|
|
46
|
+
if not name.startswith("_") and isinstance(value, type)
|
|
47
|
+
]
|
|
48
|
+
for public_name in public_names:
|
|
49
|
+
base = getattr(generated_models_module, public_name)
|
|
50
|
+
mixins: list[type[Any]] = []
|
|
51
|
+
for module in extension_modules:
|
|
52
|
+
mapping = getattr(module, "MODEL_EXTENSIONS", {})
|
|
53
|
+
if not isinstance(mapping, dict):
|
|
54
|
+
continue
|
|
55
|
+
mixin_name = mapping.get(public_name)
|
|
56
|
+
if mixin_name:
|
|
57
|
+
mixins.append(getattr(module, mixin_name))
|
|
58
|
+
composed[public_name] = extend_model(public_name, base, *mixins)
|
|
59
|
+
return composed
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_COMPOSED_MODELS = compose_models(_generated_models, _cli_model_extensions)
|
|
63
|
+
globals().update(_COMPOSED_MODELS)
|
|
17
64
|
|
|
18
65
|
|
|
19
66
|
# ---- Execution / Run dataclasses -------------------------------------------
|
|
@@ -296,10 +343,9 @@ class SecretInfo:
|
|
|
296
343
|
# ---- Components ------------------------------------------------------------
|
|
297
344
|
|
|
298
345
|
|
|
299
|
-
# ``ComponentSpec`` is generated from OpenAPI and extended in
|
|
300
|
-
#
|
|
301
|
-
#
|
|
302
|
-
# from ``tangle_cli.models``.
|
|
346
|
+
# ``ComponentSpec`` is generated from OpenAPI and extended at import time in
|
|
347
|
+
# this module. Re-export it for compatibility with callers that import domain
|
|
348
|
+
# models from ``tangle_cli.models``.
|
|
303
349
|
|
|
304
350
|
|
|
305
351
|
@dataclass
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/openapi/codegen.py
RENAMED
|
@@ -22,9 +22,7 @@ import sys
|
|
|
22
22
|
import tempfile
|
|
23
23
|
import urllib.parse
|
|
24
24
|
import urllib.request
|
|
25
|
-
from collections import Counter
|
|
26
25
|
from collections.abc import Sequence
|
|
27
|
-
from dataclasses import dataclass
|
|
28
26
|
from pathlib import Path
|
|
29
27
|
from typing import Any
|
|
30
28
|
|
|
@@ -40,7 +38,6 @@ _REPO_ROOT = Path(__file__).resolve().parents[5]
|
|
|
40
38
|
_GENERATED_DIR = _REPO_ROOT / "packages" / "tangle-api" / "src" / "tangle_api" / "generated"
|
|
41
39
|
DEFAULT_BACKEND_PATH = _REPO_ROOT / "third_party" / "tangle"
|
|
42
40
|
DEFAULT_OPERATIONS_CLASS_NAME = "GeneratedTangleApiOperations"
|
|
43
|
-
DEFAULT_MODEL_EXTENSION_MODULE = "tangle_cli.generated_model_extensions"
|
|
44
41
|
DEFAULT_MODEL_ALIASES: dict[str, tuple[str, ...]] = {
|
|
45
42
|
"ComponentSpec": (
|
|
46
43
|
"ComponentSpec-Output",
|
|
@@ -373,171 +370,63 @@ def _apply_request_body_schema_overrides(
|
|
|
373
370
|
return output
|
|
374
371
|
|
|
375
372
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
"""Import reference for one generated model extension class."""
|
|
373
|
+
def generate_runtime() -> str:
|
|
374
|
+
"""Generate the small runtime module used by generated Pydantic models."""
|
|
379
375
|
|
|
380
|
-
|
|
381
|
-
class_name: str
|
|
382
|
-
alias: str
|
|
376
|
+
return '''"""Runtime helpers for generated Tangle API model packages."""
|
|
383
377
|
|
|
378
|
+
from __future__ import annotations
|
|
384
379
|
|
|
385
|
-
|
|
386
|
-
model_extension_module: str | Sequence[str] | None,
|
|
387
|
-
) -> list[str]:
|
|
388
|
-
"""Return ordered model extension modules, applying defaults first.
|
|
380
|
+
from typing import Any
|
|
389
381
|
|
|
390
|
-
|
|
391
|
-
downstream modules after the built-in default. The empty-string sentinel
|
|
392
|
-
disables the default module and is otherwise ignored.
|
|
393
|
-
"""
|
|
382
|
+
from pydantic import BaseModel
|
|
394
383
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
else:
|
|
400
|
-
modules = list(model_extension_module)
|
|
384
|
+
try:
|
|
385
|
+
from pydantic import ConfigDict
|
|
386
|
+
except ImportError: # pragma: no cover - pydantic v1 fallback
|
|
387
|
+
ConfigDict = None # type: ignore[assignment]
|
|
401
388
|
|
|
402
|
-
include_default = True
|
|
403
|
-
if "" in modules:
|
|
404
|
-
include_default = False
|
|
405
|
-
modules = [module for module in modules if module != ""]
|
|
406
389
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
seen: set[str] = set()
|
|
410
|
-
for module in ordered:
|
|
411
|
-
if not module or module in seen:
|
|
412
|
-
continue
|
|
413
|
-
seen.add(module)
|
|
414
|
-
deduped.append(module)
|
|
415
|
-
return deduped
|
|
390
|
+
class TangleGeneratedModel(BaseModel):
|
|
391
|
+
"""Base for generated response models with dict-like conveniences."""
|
|
416
392
|
|
|
393
|
+
if ConfigDict is not None:
|
|
394
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
395
|
+
else: # pragma: no cover - pydantic v1 fallback
|
|
396
|
+
class Config:
|
|
397
|
+
extra = "allow"
|
|
398
|
+
allow_population_by_field_name = True
|
|
417
399
|
|
|
418
|
-
def
|
|
419
|
-
|
|
420
|
-
if not parts or any(not re.fullmatch(r"[A-Za-z_]\w*", part) or keyword.iskeyword(part) for part in parts):
|
|
421
|
-
raise ValueError(f"Invalid model extension module name: {module_name!r}")
|
|
422
|
-
return module_name
|
|
400
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
401
|
+
return self.to_dict().get(key, default)
|
|
423
402
|
|
|
403
|
+
def __getitem__(self, key: str) -> Any:
|
|
404
|
+
return self.to_dict()[key]
|
|
424
405
|
|
|
425
|
-
def
|
|
426
|
-
|
|
406
|
+
def to_dict(self) -> dict[str, Any]:
|
|
407
|
+
if hasattr(self, "model_dump"):
|
|
408
|
+
return self.model_dump(by_alias=True)
|
|
409
|
+
return self.dict(by_alias=True)
|
|
427
410
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
411
|
+
@classmethod
|
|
412
|
+
def from_dict(cls, data: dict[str, Any]) -> Any:
|
|
413
|
+
if hasattr(cls, "model_validate"):
|
|
414
|
+
return cls.model_validate(data)
|
|
415
|
+
return cls.parse_obj(data)
|
|
433
416
|
|
|
434
|
-
mapping = getattr(module, "MODEL_EXTENSIONS", None)
|
|
435
|
-
if not isinstance(mapping, dict):
|
|
436
|
-
raise ValueError(
|
|
437
|
-
f"Model extension module {module_name!r} must define a MODEL_EXTENSIONS dict"
|
|
438
|
-
)
|
|
439
417
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if not isinstance(model_name, str) or not isinstance(extension_name, str):
|
|
443
|
-
raise ValueError("MODEL_EXTENSIONS keys and values must be strings")
|
|
444
|
-
_validate_class_name(model_name)
|
|
445
|
-
_validate_class_name(extension_name)
|
|
446
|
-
if not hasattr(module, extension_name):
|
|
447
|
-
raise ValueError(
|
|
448
|
-
f"Model extension module {module_name!r} does not define {extension_name!r}"
|
|
449
|
-
)
|
|
450
|
-
extensions[model_name] = extension_name
|
|
451
|
-
return extensions
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
def _model_extension_refs(
|
|
455
|
-
model_extension_module: str | Sequence[str] | None,
|
|
456
|
-
) -> dict[str, list[_ModelExtensionRef]]:
|
|
457
|
-
"""Resolve model extension refs by generated class in configured order."""
|
|
458
|
-
|
|
459
|
-
refs_by_model: dict[str, list[_ModelExtensionRef]] = {}
|
|
460
|
-
raw_refs: list[_ModelExtensionRef] = []
|
|
461
|
-
for module_name in _model_extension_modules(model_extension_module):
|
|
462
|
-
for model_name, extension_name in _model_extension_mapping(module_name).items():
|
|
463
|
-
ref = _ModelExtensionRef(
|
|
464
|
-
module_name=module_name,
|
|
465
|
-
class_name=extension_name,
|
|
466
|
-
alias=extension_name,
|
|
467
|
-
)
|
|
468
|
-
refs_by_model.setdefault(model_name, []).append(ref)
|
|
469
|
-
raw_refs.append(ref)
|
|
470
|
-
|
|
471
|
-
unique_ref_keys: list[tuple[str, str]] = []
|
|
472
|
-
seen_ref_keys: set[tuple[str, str]] = set()
|
|
473
|
-
for ref in raw_refs:
|
|
474
|
-
key = (ref.module_name, ref.class_name)
|
|
475
|
-
if key in seen_ref_keys:
|
|
476
|
-
continue
|
|
477
|
-
seen_ref_keys.add(key)
|
|
478
|
-
unique_ref_keys.append(key)
|
|
479
|
-
|
|
480
|
-
class_name_counts = Counter(class_name for _, class_name in unique_ref_keys)
|
|
481
|
-
alias_counts: Counter[str] = Counter()
|
|
482
|
-
aliases_by_ref: dict[tuple[str, str], str] = {}
|
|
483
|
-
for module_name, class_name in unique_ref_keys:
|
|
484
|
-
if class_name_counts[class_name] == 1:
|
|
485
|
-
alias = class_name
|
|
486
|
-
else:
|
|
487
|
-
alias_base = f"_{_safe_identifier(module_name)}_{class_name}"
|
|
488
|
-
alias_counts[alias_base] += 1
|
|
489
|
-
alias = alias_base if alias_counts[alias_base] == 1 else f"{alias_base}_{alias_counts[alias_base]}"
|
|
490
|
-
aliases_by_ref[(module_name, class_name)] = alias
|
|
491
|
-
|
|
492
|
-
aliased: dict[str, list[_ModelExtensionRef]] = {}
|
|
493
|
-
for model_name, refs in refs_by_model.items():
|
|
494
|
-
aliased[model_name] = []
|
|
495
|
-
for ref in refs:
|
|
496
|
-
aliased[model_name].append(
|
|
497
|
-
_ModelExtensionRef(
|
|
498
|
-
module_name=ref.module_name,
|
|
499
|
-
class_name=ref.class_name,
|
|
500
|
-
alias=aliases_by_ref[(ref.module_name, ref.class_name)],
|
|
501
|
-
)
|
|
502
|
-
)
|
|
503
|
-
return aliased
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
def _model_extension_import_lines(refs_by_model: dict[str, list[_ModelExtensionRef]]) -> list[str]:
|
|
507
|
-
"""Render deterministic import lines for configured model extensions."""
|
|
508
|
-
|
|
509
|
-
refs_by_module: dict[str, list[_ModelExtensionRef]] = {}
|
|
510
|
-
for refs in refs_by_model.values():
|
|
511
|
-
for ref in refs:
|
|
512
|
-
refs_by_module.setdefault(ref.module_name, []).append(ref)
|
|
513
|
-
|
|
514
|
-
lines: list[str] = []
|
|
515
|
-
for module_name, refs in sorted(refs_by_module.items()):
|
|
516
|
-
imports: list[str] = []
|
|
517
|
-
seen: set[tuple[str, str]] = set()
|
|
518
|
-
for ref in sorted(refs, key=lambda item: (item.class_name, item.alias)):
|
|
519
|
-
key = (ref.class_name, ref.alias)
|
|
520
|
-
if key in seen:
|
|
521
|
-
continue
|
|
522
|
-
seen.add(key)
|
|
523
|
-
if ref.alias == ref.class_name:
|
|
524
|
-
imports.append(ref.class_name)
|
|
525
|
-
else:
|
|
526
|
-
imports.append(f"{ref.class_name} as {ref.alias}")
|
|
527
|
-
lines.append(f"from {module_name} import {', '.join(imports)}")
|
|
528
|
-
return lines
|
|
418
|
+
__all__ = ["TangleGeneratedModel"]
|
|
419
|
+
'''
|
|
529
420
|
|
|
530
421
|
|
|
531
422
|
def generate_models(
|
|
532
423
|
schema: dict[str, Any],
|
|
533
|
-
model_extension_module: str | Sequence[str] | None = None,
|
|
534
424
|
model_aliases: dict[str, Sequence[str] | str] | Sequence[str] | str | None = None,
|
|
535
425
|
) -> str:
|
|
536
|
-
"""Generate Pydantic model classes
|
|
426
|
+
"""Generate plain/base Pydantic model classes."""
|
|
537
427
|
|
|
538
428
|
raw_schemas = schema.get("components", {}).get("schemas", {}) or {}
|
|
539
429
|
schemas, _ = _apply_model_aliases(raw_schemas, model_aliases)
|
|
540
|
-
extension_refs = _model_extension_refs(model_extension_module)
|
|
541
430
|
lines: list[str] = [
|
|
542
431
|
'"""Generated Pydantic models for the checked-in Tangle OpenAPI schema.\n\nDo not edit by hand; run ``uv run python -m tangle_cli.openapi.codegen``.\n"""',
|
|
543
432
|
"",
|
|
@@ -547,26 +436,10 @@ def generate_models(
|
|
|
547
436
|
"",
|
|
548
437
|
"from pydantic import Field",
|
|
549
438
|
"",
|
|
550
|
-
"from
|
|
439
|
+
"from tangle_api.generated.runtime import TangleGeneratedModel",
|
|
551
440
|
"",
|
|
552
441
|
]
|
|
553
442
|
|
|
554
|
-
generated_class_names = {
|
|
555
|
-
_class_name(schema_name)
|
|
556
|
-
for schema_name, schema_def in schemas.items()
|
|
557
|
-
if isinstance(schema_def, dict)
|
|
558
|
-
and (schema_def.get("type") in {"object", None} or "properties" in schema_def)
|
|
559
|
-
}
|
|
560
|
-
used_extensions = {
|
|
561
|
-
class_name: extension_refs[class_name]
|
|
562
|
-
for class_name in sorted(generated_class_names)
|
|
563
|
-
if class_name in extension_refs
|
|
564
|
-
}
|
|
565
|
-
imports = _model_extension_import_lines(used_extensions)
|
|
566
|
-
if imports:
|
|
567
|
-
lines.extend(imports)
|
|
568
|
-
lines.append("")
|
|
569
|
-
|
|
570
443
|
exports: list[str] = []
|
|
571
444
|
for schema_name, schema_def in sorted(schemas.items(), key=lambda item: _class_name(item[0])):
|
|
572
445
|
class_name = _class_name(schema_name)
|
|
@@ -575,9 +448,7 @@ def generate_models(
|
|
|
575
448
|
lines.extend([f"{class_name} = Any", ""])
|
|
576
449
|
continue
|
|
577
450
|
properties = schema_def.get("properties") or {}
|
|
578
|
-
|
|
579
|
-
generated_base_name = f"_{class_name}Generated"
|
|
580
|
-
lines.extend([f"class {generated_base_name}(TangleGeneratedModel):"])
|
|
451
|
+
lines.append(f"class {class_name}(TangleGeneratedModel):")
|
|
581
452
|
if not properties:
|
|
582
453
|
lines.append(" pass")
|
|
583
454
|
else:
|
|
@@ -588,13 +459,6 @@ def generate_models(
|
|
|
588
459
|
else:
|
|
589
460
|
lines.append(f" {field_name}: Any = None")
|
|
590
461
|
lines.append("")
|
|
591
|
-
extension_bases = [ref.alias for ref in reversed(extension_refs_for_class)]
|
|
592
|
-
bases = extension_bases + [generated_base_name]
|
|
593
|
-
lines.extend([
|
|
594
|
-
f"class {class_name}({', '.join(bases)}):",
|
|
595
|
-
" pass",
|
|
596
|
-
"",
|
|
597
|
-
])
|
|
598
462
|
|
|
599
463
|
lines.append(f"__all__ = {exports!r}")
|
|
600
464
|
lines.append("")
|
|
@@ -727,6 +591,11 @@ def generate_operations(
|
|
|
727
591
|
" response_model: Any = None,",
|
|
728
592
|
" ) -> Any: ...",
|
|
729
593
|
"",
|
|
594
|
+
" def _response_model(self, model_name: str, default: Any) -> Any:",
|
|
595
|
+
" \"\"\"Return the model class used to deserialize a generated response.\"\"\"",
|
|
596
|
+
"",
|
|
597
|
+
" return default",
|
|
598
|
+
"",
|
|
730
599
|
])
|
|
731
600
|
|
|
732
601
|
used_methods: set[str] = set()
|
|
@@ -742,7 +611,7 @@ def generate_operations(
|
|
|
742
611
|
raw_body_override=bool(operation.operation.get("x-tangle-cli-request-body-schema-override")),
|
|
743
612
|
)
|
|
744
613
|
response_model = _response_model_name(operation.operation, model_ref_aliases)
|
|
745
|
-
response_arg = response_model if response_model else "None"
|
|
614
|
+
response_arg = f"self._response_model({response_model!r}, {response_model})" if response_model else "None"
|
|
746
615
|
response_annotation = _response_return_annotation(operation.operation, model_ref_aliases)
|
|
747
616
|
if signature:
|
|
748
617
|
def_line = f" def {method_name}(self, {signature}) -> {response_annotation}:"
|
|
@@ -871,7 +740,6 @@ def generate(
|
|
|
871
740
|
generated_dir: str | Path = _GENERATED_DIR,
|
|
872
741
|
*,
|
|
873
742
|
operations_class_name: str = DEFAULT_OPERATIONS_CLASS_NAME,
|
|
874
|
-
model_extension_module: str | Sequence[str] | None = None,
|
|
875
743
|
model_aliases: dict[str, Sequence[str] | str] | Sequence[str] | str | None = None,
|
|
876
744
|
request_body_schemas: dict[str, dict[str, Any]] | Sequence[str] | str | None = None,
|
|
877
745
|
) -> tuple[dict[str, Any], list[Path]]:
|
|
@@ -880,6 +748,7 @@ def generate(
|
|
|
880
748
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
881
749
|
generated_files = [
|
|
882
750
|
output_dir / "__init__.py",
|
|
751
|
+
output_dir / "runtime.py",
|
|
883
752
|
output_dir / "models.py",
|
|
884
753
|
output_dir / "operations.py",
|
|
885
754
|
]
|
|
@@ -887,15 +756,15 @@ def generate(
|
|
|
887
756
|
'"""Generated OpenAPI support modules."""\n',
|
|
888
757
|
encoding="utf-8",
|
|
889
758
|
)
|
|
890
|
-
generated_files[1].write_text(
|
|
759
|
+
generated_files[1].write_text(generate_runtime(), encoding="utf-8")
|
|
760
|
+
generated_files[2].write_text(
|
|
891
761
|
generate_models(
|
|
892
762
|
schema,
|
|
893
|
-
model_extension_module=model_extension_module,
|
|
894
763
|
model_aliases=model_aliases,
|
|
895
764
|
),
|
|
896
765
|
encoding="utf-8",
|
|
897
766
|
)
|
|
898
|
-
generated_files[
|
|
767
|
+
generated_files[3].write_text(
|
|
899
768
|
generate_operations(
|
|
900
769
|
schema,
|
|
901
770
|
operations_class_name=operations_class_name,
|
|
@@ -961,19 +830,6 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
961
830
|
f"(default: {DEFAULT_OPERATIONS_CLASS_NAME})."
|
|
962
831
|
),
|
|
963
832
|
)
|
|
964
|
-
parser.add_argument(
|
|
965
|
-
"--model-extension-module",
|
|
966
|
-
action="append",
|
|
967
|
-
default=None,
|
|
968
|
-
help=(
|
|
969
|
-
"Importable module containing a MODEL_EXTENSIONS mapping from "
|
|
970
|
-
"generated model class names to extension class names. Repeat to "
|
|
971
|
-
"compose modules in order; later modules override earlier ones. "
|
|
972
|
-
"The built-in default module is applied first unless an empty string "
|
|
973
|
-
"is passed to disable it. "
|
|
974
|
-
f"(default first: {DEFAULT_MODEL_EXTENSION_MODULE})."
|
|
975
|
-
),
|
|
976
|
-
)
|
|
977
833
|
parser.add_argument(
|
|
978
834
|
"--model-alias",
|
|
979
835
|
action="append",
|
|
@@ -1031,7 +887,6 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
1031
887
|
args = parser.parse_args(argv)
|
|
1032
888
|
try:
|
|
1033
889
|
_validate_class_name(args.operations_class_name)
|
|
1034
|
-
_model_extension_refs(args.model_extension_module)
|
|
1035
890
|
_model_alias_mapping(args.model_alias)
|
|
1036
891
|
request_body_schema_overrides = _request_body_schema_mapping(args.request_body_schema)
|
|
1037
892
|
request_body_schema_overrides.update(_request_body_schema_file_mapping(args.request_body_schema_file))
|
|
@@ -1073,7 +928,6 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
1073
928
|
openapi_path,
|
|
1074
929
|
args.out,
|
|
1075
930
|
operations_class_name=args.operations_class_name,
|
|
1076
|
-
model_extension_module=args.model_extension_module,
|
|
1077
931
|
model_aliases=args.model_alias,
|
|
1078
932
|
request_body_schemas=request_body_schema_overrides,
|
|
1079
933
|
)
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/openapi/parser.py
RENAMED
|
@@ -49,9 +49,10 @@ def _load_default_openapi_schema() -> dict[str, Any]:
|
|
|
49
49
|
last_error = exc
|
|
50
50
|
if schema_text is None:
|
|
51
51
|
raise FileNotFoundError(
|
|
52
|
-
"Default OpenAPI snapshot not found. Install
|
|
53
|
-
"source checkout with
|
|
54
|
-
"or pass
|
|
52
|
+
"Default OpenAPI snapshot not found. Install the default or a compatible "
|
|
53
|
+
"custom tangle-api package, run from a source checkout with "
|
|
54
|
+
"packages/tangle-api/src/tangle_api/schema/openapi.json, or pass "
|
|
55
|
+
"--openapi PATH explicitly."
|
|
55
56
|
) from last_error
|
|
56
57
|
|
|
57
58
|
schema = json.loads(schema_text)
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_run_search.py
RENAMED
|
@@ -28,9 +28,9 @@ _CREATED_AT_WIDTH = 16
|
|
|
28
28
|
class PageChunk:
|
|
29
29
|
"""Metadata for a single page of search results.
|
|
30
30
|
|
|
31
|
-
Defined locally to keep this module importable without
|
|
32
|
-
``tangle-api``
|
|
33
|
-
dataclass when
|
|
31
|
+
Defined locally to keep this module importable without loading generated
|
|
32
|
+
``tangle-api`` bindings; ``tangle_cli.models`` re-exports an equivalent
|
|
33
|
+
dataclass when generated models are imported.
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
rows: list[dict[str, Any]]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Quickstart text for the root ``tangle`` CLI."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ from textwrap import dedent
|
|
|
7
7
|
from cyclopts import App
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
app = App(name="quickstart", help="Print a concise
|
|
10
|
+
app = App(name="quickstart", help="Print a concise guide to the Tangle CLI.")
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
QUICKSTART_TEXT = dedent(
|
|
@@ -76,16 +76,15 @@ QUICKSTART_TEXT = dedent(
|
|
|
76
76
|
dynamic schema discovery, codegen, logging, hydrator/resolver logic, and
|
|
77
77
|
extension hooks.
|
|
78
78
|
|
|
79
|
-
tangle_api is the generated
|
|
80
|
-
endpoint operation methods, and the official OpenAPI snapshot.
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
tangle_api is the generated package: checked-in Pydantic models,
|
|
80
|
+
endpoint operation methods, and the official OpenAPI snapshot. Public
|
|
81
|
+
tangle-cli installs include the matching tangle-api package by default.
|
|
82
|
+
Codegen/custom API projects can still generate a local src/tangle_api
|
|
83
|
+
package that shadows site-packages, or provide a compatible private
|
|
84
|
+
distribution named tangle-api for their environment.
|
|
83
85
|
|
|
84
|
-
Generated model extensions
|
|
85
|
-
|
|
86
|
-
_ComponentSpecGenerated). Extension bases are left of the generated base in
|
|
87
|
-
the MRO, and downstream --model-extension-module values can add/override
|
|
88
|
-
behavior while preserving generated fields and stable names.
|
|
86
|
+
Generated model extensions are composed at runtime in downstream namespaces
|
|
87
|
+
such as tangle_cli.models, leaving tangle_api generated models plain/leaf.
|
|
89
88
|
|
|
90
89
|
Discover more
|
|
91
90
|
-------------
|
|
@@ -105,6 +104,6 @@ QUICKSTART_TEXT = dedent(
|
|
|
105
104
|
|
|
106
105
|
@app.default
|
|
107
106
|
def quickstart() -> None:
|
|
108
|
-
"""Print a concise
|
|
107
|
+
"""Print a concise guide to the Tangle CLI."""
|
|
109
108
|
|
|
110
109
|
print(QUICKSTART_TEXT)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tangle-cli"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.1a3"
|
|
4
4
|
description = "CLI for Tangle, the open-source ML pipeline orchestration platform"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -18,6 +18,7 @@ dependencies = [
|
|
|
18
18
|
"pydantic>=2.0",
|
|
19
19
|
"pyyaml>=6.0",
|
|
20
20
|
"requests>=2.32.0",
|
|
21
|
+
"tangle-api==0.0.1a3",
|
|
21
22
|
"tomli>=2.0; python_version < '3.11'",
|
|
22
23
|
]
|
|
23
24
|
|
|
@@ -28,10 +29,12 @@ Repository = "https://github.com/TangleML/tangle-cli"
|
|
|
28
29
|
Issues = "https://github.com/TangleML/tangle-cli/issues"
|
|
29
30
|
|
|
30
31
|
[project.optional-dependencies]
|
|
31
|
-
|
|
32
|
+
# Compatibility alias: tangle-api is now part of the default public install.
|
|
33
|
+
native = []
|
|
32
34
|
|
|
33
35
|
[project.scripts]
|
|
34
36
|
tangle = "tangle_cli.cli:main"
|
|
37
|
+
tangle-cli = "tangle_cli.cli:main"
|
|
35
38
|
|
|
36
39
|
[build-system]
|
|
37
40
|
requires = ["uv_build>=0.11.2,<0.12.0"]
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"""Runtime helpers shared by generated Tangle API model packages."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
from pydantic import BaseModel
|
|
8
|
-
|
|
9
|
-
try:
|
|
10
|
-
from pydantic import ConfigDict
|
|
11
|
-
except ImportError: # pragma: no cover - pydantic v1 fallback
|
|
12
|
-
ConfigDict = None # type: ignore[assignment]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class TangleGeneratedModel(BaseModel):
|
|
16
|
-
"""Base for generated response models with dict-like conveniences."""
|
|
17
|
-
|
|
18
|
-
if ConfigDict is not None:
|
|
19
|
-
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
20
|
-
else: # pragma: no cover - pydantic v1 fallback
|
|
21
|
-
class Config:
|
|
22
|
-
extra = "allow"
|
|
23
|
-
allow_population_by_field_name = True
|
|
24
|
-
|
|
25
|
-
def get(self, key: str, default: Any = None) -> Any:
|
|
26
|
-
return self.to_dict().get(key, default)
|
|
27
|
-
|
|
28
|
-
def __getitem__(self, key: str) -> Any:
|
|
29
|
-
return self.to_dict()[key]
|
|
30
|
-
|
|
31
|
-
def to_dict(self) -> dict[str, Any]:
|
|
32
|
-
if hasattr(self, "model_dump"):
|
|
33
|
-
return self.model_dump(by_alias=True)
|
|
34
|
-
return self.dict(by_alias=True)
|
|
35
|
-
|
|
36
|
-
@classmethod
|
|
37
|
-
def from_dict(cls, data: dict[str, Any]) -> Any:
|
|
38
|
-
if hasattr(cls, "model_validate"):
|
|
39
|
-
return cls.model_validate(data)
|
|
40
|
-
return cls.parse_obj(data)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
__all__ = ["TangleGeneratedModel"]
|
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/api_transport.py
RENAMED
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/args_container.py
RENAMED
|
File without changes
|
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/artifacts_cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/component_from_func.py
RENAMED
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/component_generator.py
RENAMED
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/component_inspector.py
RENAMED
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/components_cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/hydration_trust.py
RENAMED
|
File without changes
|
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/module_bundler.py
RENAMED
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/openapi/__init__.py
RENAMED
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py
RENAMED
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_hydrator.py
RENAMED
|
File without changes
|
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_run_details.py
RENAMED
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_run_manager.py
RENAMED
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_runner.py
RENAMED
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipeline_runs_cli.py
RENAMED
|
File without changes
|
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/pipelines_cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.0.1a1 → tangle_cli-0.0.1a3}/packages/tangle-cli/src/tangle_cli/version_manager.py
RENAMED
|
File without changes
|