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