Skip to content

Commit 05cd92b

Browse files
docs: add DYNAMIC_MODELS.md for SDK doc parity
Mirrors `tango-node/docs/DYNAMIC_MODELS.md` so the two SDK repos have matching internal-docs structure. Translated entirely to Python idioms. Sections: - Overview - Components — ShapeParser / SchemaRegistry / TypeGenerator / ModelFactory - Full Shaping Pipeline (manual) - Attribute Access — Python-specific addition covering `__getattr__` and its helpful error messages - Type Safety - Caching - Nested Models - Predefined Shape Constants Key Python divergences from the Node version (corrected on this side rather than copied): - `SchemaRegistry.get_schema(ModelClass)` returns a full dict (not Node's `getField()`). - TypeGenerator uses LRU eviction, not the FIFO the Node doc claims. - No `ShapedListModel` — that class doesn't exist in the Python codebase. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a8a1d68 commit 05cd92b

1 file changed

Lines changed: 205 additions & 0 deletions

File tree

docs/DYNAMIC_MODELS.md

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# Tango Python SDK – Dynamic Models Guide
2+
3+
This document explains how the **Python dynamic shaping system** works.
4+
It mirrors the Node.js `DYNAMIC_MODELS.md` guide for the Python SDK.
5+
6+
---
7+
8+
## Overview
9+
10+
Tango's dynamic modeling allows you to:
11+
12+
- Request _exactly the fields you want_
13+
- Validate the shape string against Tango's schemas
14+
- Generate a typed model descriptor at runtime
15+
- Materialize shaped objects using correct:
16+
- date parsing
17+
- datetime parsing
18+
- decimal handling
19+
- list vs scalar logic
20+
- nested structure
21+
22+
---
23+
24+
## Components
25+
26+
### ShapeParser
27+
28+
Parses shape strings into a `ShapeSpec`.
29+
30+
```python
31+
from tango.shapes import ShapeParser
32+
33+
parser = ShapeParser()
34+
spec = parser.parse("key,piid,recipient(display_name)")
35+
```
36+
37+
### SchemaRegistry
38+
39+
Holds the field schemas for all models.
40+
41+
```python
42+
from tango.shapes import SchemaRegistry
43+
from tango.models import Contract
44+
45+
registry = SchemaRegistry()
46+
schema = registry.get_schema(Contract)
47+
award_date_field = schema["award_date"]
48+
# FieldSchema(name='award_date', type=date | None)
49+
```
50+
51+
### TypeGenerator
52+
53+
Builds a dynamic `TypedDict`-backed type from `(shape_spec, base_model)`.
54+
55+
```python
56+
from tango.shapes import ShapeParser, TypeGenerator
57+
from tango.models import Contract
58+
59+
parser = ShapeParser()
60+
spec = parser.parse("key,piid,recipient(display_name)")
61+
62+
gen = TypeGenerator()
63+
dynamic_type = gen.generate_type(
64+
shape_spec=spec,
65+
base_model=Contract,
66+
type_name="ContractShaped",
67+
)
68+
```
69+
70+
### ModelFactory
71+
72+
Takes a dynamic type + raw API JSON and produces typed `ShapedModel` instances.
73+
The `TangoClient` uses this pipeline automatically after fetching data.
74+
75+
```python
76+
from tango import TangoClient
77+
78+
client = TangoClient(api_key="your-api-key")
79+
contracts = client.list_contracts(
80+
shape="key,award_date,recipient(display_name)",
81+
)
82+
83+
# contracts.results are ShapedModel instances materialized by ModelFactory:
84+
# - date/datetime strings parsed to date/datetime objects
85+
# - decimals normalized via Decimal
86+
# - nested structures are themselves ShapedModel instances
87+
```
88+
89+
---
90+
91+
## Example: Full Shaping Pipeline (manual)
92+
93+
```python
94+
from tango.shapes import ShapeParser, TypeGenerator, ModelFactory, create_default_parser_registry
95+
from tango.models import Contract
96+
97+
parser = ShapeParser()
98+
spec = parser.parse("key,award_date,recipient(display_name)")
99+
100+
gen = TypeGenerator()
101+
dynamic_type = gen.generate_type(
102+
shape_spec=spec,
103+
base_model=Contract,
104+
type_name="ContractShaped",
105+
)
106+
107+
parsers = create_default_parser_registry()
108+
factory = ModelFactory(gen, parsers)
109+
110+
shaped = factory.create_instance(
111+
data={
112+
"key": "C-1",
113+
"award_date": "2024-01-15",
114+
"recipient": {"display_name": "Acme"},
115+
},
116+
shape_spec=spec,
117+
base_model=Contract,
118+
dynamic_type=dynamic_type,
119+
)
120+
```
121+
122+
`shaped` becomes:
123+
124+
```python
125+
ContractShaped(key='C-1', award_date=datetime.date(2024, 1, 15), recipient=ContractShaped_Recipient(display_name='Acme'))
126+
```
127+
128+
---
129+
130+
## Attribute Access
131+
132+
`ShapedModel` is a `dict` subclass with `__getattr__` so fields are accessible
133+
both as dictionary keys and as attributes:
134+
135+
```python
136+
# Both styles work
137+
shaped["key"] # "C-1"
138+
shaped.key # "C-1"
139+
140+
# Nested models are also ShapedModel instances
141+
shaped.recipient["display_name"] # "Acme"
142+
shaped.recipient.display_name # "Acme"
143+
```
144+
145+
Accessing a field that was not included in your shape raises a descriptive
146+
`AttributeError` with suggestions:
147+
148+
```python
149+
shaped.award_amount
150+
# AttributeError: Field 'award_amount' not found in ContractShaped.
151+
# Available fields: 'key', 'award_date', 'recipient'
152+
# This field may not be included in your shape specification.
153+
# To include this field, add it to your shape parameter.
154+
```
155+
156+
---
157+
158+
## Type Safety
159+
160+
The Python SDK enforces shape correctness at parse time via `ShapeParser.validate()`.
161+
Nested structures are recursively materialized as `ShapedModel` instances, guaranteeing
162+
the same access patterns at every depth. No static class generation happens at build time;
163+
shapes are resolved at runtime.
164+
165+
---
166+
167+
## Caching
168+
169+
`TypeGenerator` caches descriptors using a thread-safe LRU cache (default: 100 entries).
170+
171+
`ShapeParser` also caches parse results keyed on the raw shape string.
172+
173+
---
174+
175+
## Nested Models
176+
177+
If a field is nested in the schema (e.g. `"recipient"``RecipientProfile`),
178+
the generator recursively builds the nested descriptor, naming it
179+
`{ParentType}_{FieldName}` (e.g. `ContractShaped_Recipient`). Each nested object
180+
is also a `ShapedModel`, so attribute access and `repr` work uniformly at every level.
181+
182+
---
183+
184+
## Predefined Shape Constants
185+
186+
`ShapeConfig` provides opinionated defaults for each resource's list and detail methods.
187+
Each `TangoClient` method applies its corresponding default automatically; pass `shape=`
188+
to override.
189+
190+
```python
191+
from tango import TangoClient, ShapeConfig
192+
193+
client = TangoClient(api_key="your-api-key")
194+
195+
# These are equivalent — list_contracts defaults to CONTRACTS_MINIMAL
196+
contracts = client.list_contracts(limit=10)
197+
contracts = client.list_contracts(shape=ShapeConfig.CONTRACTS_MINIMAL, limit=10)
198+
199+
# Other resources
200+
entities = client.list_entities(shape=ShapeConfig.ENTITIES_MINIMAL)
201+
idvs = client.list_idvs(shape=ShapeConfig.IDVS_MINIMAL)
202+
```
203+
204+
See [API Reference – ShapeConfig](API_REFERENCE.md#shapeconfig-predefined-shapes) for the
205+
full table of constants.

0 commit comments

Comments
 (0)