Skip to content

Commit 3064235

Browse files
authored
Merge pull request #10 from TangleML/piforge/tangle-cli-fix/leaf-tangle-api-runtime-model-co-fcb039f
refactor: make tangle-api leaf via runtime model composition
2 parents e5d882e + 5c15051 commit 3064235

28 files changed

Lines changed: 447 additions & 903 deletions

.github/workflows/release.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ jobs:
3030
# which distribution is installed for tangle-cli smoke tests.
3131
- name: Smoke test tangle-cli wheel
3232
run: |
33-
uv run --isolated --no-project --with dist/tangle_cli-*.whl tangle version
34-
uv run --isolated --no-project --with dist/tangle_cli-*.whl tangle-cli version
33+
uv run --isolated --no-project --find-links dist --with dist/tangle_cli-*.whl tangle version
34+
uv run --isolated --no-project --find-links dist --with dist/tangle_cli-*.whl tangle-cli version
3535
- name: Smoke test tangle-cli source distribution
3636
run: |
37-
uv run --isolated --no-project --with dist/tangle_cli-*.tar.gz tangle version
38-
uv run --isolated --no-project --with dist/tangle_cli-*.tar.gz tangle-cli version
37+
uv run --isolated --no-project --find-links dist --with dist/tangle_cli-*.tar.gz tangle version
38+
uv run --isolated --no-project --find-links dist --with dist/tangle_cli-*.tar.gz tangle-cli version
3939
- name: Smoke test tangle-api wheel
4040
run: |
4141
uv run --isolated --no-project \
@@ -48,7 +48,7 @@ jobs:
4848
--with dist/tangle_cli-*.tar.gz \
4949
--with dist/tangle_api-*.tar.gz \
5050
python -c "import importlib.metadata as m; import tangle_api.generated.models; import tangle_api.schema; assert m.version('tangle-api') == m.version('tangle-cli')"
51-
- name: Smoke test native extra resolution from local dist
51+
- name: Smoke test native extra compatibility alias from local dist
5252
run: |
5353
cli_wheel="$(echo dist/tangle_cli-*.whl)"
5454
uv run --isolated --no-project --find-links dist --with "${cli_wheel}[native]" tangle-cli version

README.md

Lines changed: 32 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ By default `tangle api` uses `--schema-source auto`, which means official static
3333

3434
`tangle sdk` commands are hand-written workflows. They can be:
3535

36-
- **local-only**: no generated/native API bindings required, e.g. pipeline validation/layout and component generation;
36+
- **local-only**: no generated API bindings required, e.g. pipeline validation/layout and component generation;
3737
- **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.
3838

3939
Current SDK groups include:
@@ -86,22 +86,22 @@ Use `--log-type none` for quiet machine-readable runs, and `--log-type file` to
8686
The repository contains two Python import packages with different responsibilities:
8787

8888
- `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/native. It contains checked-in generated Pydantic models, generated endpoint operation methods, and the official OpenAPI snapshot.
89+
- `tangle_api` is generated/static. It contains checked-in generated Pydantic models, generated endpoint operation methods, and the official OpenAPI snapshot.
9090

91-
The default `tangle-cli` package keeps the top-level import and local-only SDK commands native-free. Install the native extra when you want static API-backed commands and the handwritten `TangleApiClient` wrapper to use the checked-in generated bindings:
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:
9292

9393
```bash
94-
pip install 'tangle-cli[native]'
94+
pip install tangle-cli
9595
```
9696

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:
9898

9999
```bash
100100
uv run tangle api --help
101101
uv run tangle sdk pipelines validate pipeline.yaml
102102
```
103103

104-
If you are embedding `tangle_cli` in a downstream project, you can provide your own local `tangle_api.generated` package produced from your backend schema instead of using this repo's official generated package.
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.
105105

106106
## Quick command examples
107107

@@ -204,9 +204,9 @@ uv run tangle api reset-cache --base-url https://api.example
204204

205205
Schema source modes are:
206206

207-
- `--schema-source auto` (default): official static operations plus cached-only backend extensions when a cache exists. Requires the native `tangle-api` package for official operations.
208-
- `--schema-source official`: only the checked-in official static schema. Requires the native `tangle-api` package.
209-
- `--schema-source cache`: only the schema previously written by `tangle api refresh` for the selected base URL. Does not require the native package.
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.
210210

211211
For resource help, put `--schema-source` on the resource group:
212212

@@ -306,7 +306,7 @@ existing = client.find_existing_components(
306306

307307
`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.
308308

309-
The top-level `import tangle_cli` is lightweight and does not import native static bindings. Install the native extra or otherwise provide a local `tangle_api.generated` package before importing `tangle_cli.client`.
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`.
310310

311311
## Codegen/autogen from OpenAPI
312312

@@ -337,6 +337,17 @@ uv run python -m tangle_cli.openapi.codegen \
337337
--out src/tangle_api/generated
338338
```
339339

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+
340351
Generate from a backend checkout explicitly:
341352

342353
```bash
@@ -347,48 +358,29 @@ uv run --group codegen python -m tangle_cli.openapi.codegen \
347358

348359
Important codegen options:
349360

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`.
351362
- `--operations-class-name`: generated operations mixin class name. Defaults to `GeneratedTangleApiOperations`.
352-
- `--model-extension-module`: importable module with `MODEL_EXTENSIONS`; repeat to compose modules.
353363
- `--model-alias`: expose a stable public model name from one or more source schema names, e.g. `ComponentSpec=ComponentSpecOutput,ComponentSpecInput`.
354364
- `--request-body-schema` / `--request-body-schema-file`: override a specific operation's JSON request-body schema without mutating the fetched OpenAPI document.
355365

356366
At runtime, more `tangle api ...` commands become available in two ways:
357367

358-
1. Static codegen: regenerate and install/provide a `tangle_api.generated` package for the schema.
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`.
359369
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.
360370

361-
## Generated model extension pattern
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.
362372

363-
Generated models use a generated implementation base plus a stable public subclass. For example, codegen emits this shape for a model with a handwritten extension:
373+
## Runtime generated model extension pattern
374+
375+
`tangle_api.generated.models` is a leaf package and codegen emits plain generated Pydantic models directly:
364376

365377
```python
366-
class _ComponentSpecGenerated(TangleGeneratedModel):
378+
class ComponentSpec(TangleGeneratedModel):
367379
name: Any = None
368380
# 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
389381
```
390382

391-
It defines:
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`:
392384

393385
```python
394386
MODEL_EXTENSIONS = {
@@ -398,42 +390,11 @@ MODEL_EXTENSIONS = {
398390
}
399391
```
400392

401-
During codegen, `tangle_api.generated.models` imports those extension classes from `tangle_cli.generated_model_extensions`. This preserves the package boundary: `tangle_api` remains generated bindings, while `tangle_cli` owns handwritten runtime and extension behavior.
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.
433394

434-
The same empty-string sentinel can disable built-in `--model-alias` defaults. Built-in aliases keep stable public model names such as `ComponentSpec` even when a backend schema uses names like `ComponentSpecOutput` or `ComponentSpecInput`.
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`.
435396

436-
Extension classes should be importable from their modules and should not import generated model classes. They should be mixins over generated data, not replacements for generated schemas.
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`.
437398

438399
## Extending SDK behavior
439400

packages/tangle-api/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
# tangle-api
22

3-
Checked-in generated Tangle API models, operation proxies, and schema snapshot used by `tangle-cli[native]`.
3+
Checked-in generated Tangle API models, operation proxies, and schema snapshot used by the default `tangle-cli` install.
4+
5+
This package is intentionally a leaf package: it depends on Pydantic, but not on `tangle-cli`. Custom API consumers can provide their own compatible distribution named `tangle-api` or a project-local `src/tangle_api` package that shadows the official package in that project environment.

packages/tangle-api/pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "tangle-api"
3-
version = "0.0.1a2"
3+
version = "0.0.1a3"
44
description = "Checked-in generated Tangle API models and operation proxies"
55
readme = "README.md"
66
authors = [
@@ -10,7 +10,6 @@ authors = [
1010
requires-python = ">=3.10"
1111
dependencies = [
1212
"pydantic>=2.0",
13-
"tangle-cli==0.0.1a2",
1413
]
1514

1615
[build-system]

0 commit comments

Comments
 (0)