You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A resource carries a **`relationships`** block — linkage references (`type`+`id`)
5
-
for every related resource — and, when you request `?include=`, the response also
4
+
A resource carries a **`relationships`** block: linkage references (`type`plus`id`)
5
+
for every related resource. When you request `?include=`, the response also
6
6
carries a top-level **`included`** array holding the *full bodies* of those
7
7
relations.
8
8
9
9
pyTFE handles this on two levels:
10
10
11
-
1.**Typed hydration** — relationships the SDK models are parsed into typed
11
+
1.**Typed hydration**: relationships the SDK models are parsed into typed
12
12
fields, and when you pass `include=...` those fields are filled from
13
13
`included`. For example:
14
14
@@ -24,10 +24,10 @@ pyTFE handles this on two levels:
24
24
25
25
for o in ws.outputs: # fully hydrated from `included`
26
26
print(o.name, o.value)
27
-
print(ws.project.name) # not just the id — the real project record
27
+
print(ws.project.name) # not just the id, the real project record
28
28
```
29
29
30
-
2.**Lossless raw access** — even relations the SDK does **not** model as typed
30
+
2.**Lossless raw access**: even relations the SDK does **not** model as typed
31
31
fields are never lost. Every resource on the relationship-parsing path keeps
32
32
the raw blocks, reachable through these accessors:
33
33
@@ -40,11 +40,11 @@ pyTFE handles this on two levels:
40
40
|`model.included_by(type, id)`| one included object matched by `type` + `id`|
41
41
|`model.related(name)`| the references of relationship `name`, each resolved to its full included body (or left as a bare `{type, id}` ref if it wasn't `include`-d) |
42
42
43
-
`.relationships`/`.included` are always present and stably typed — they
44
-
return `{}`/`[]` whether the block was *empty* or *absent*. The API genuinely
43
+
`.relationships`and`.included` are always present and stably typed: they
44
+
return `{}`or`[]` whether the block was *empty* or *absent*. The API genuinely
45
45
distinguishes the two (SSH keys omit `relationships` entirely; `included`
46
-
only appears with `?include=`), so `has_relationships`/`has_included` tell
47
-
you which — without making the data accessors conditionally vanish.
46
+
only appears with `?include=`), so `has_relationships`and`has_included` tell
47
+
you which, without making the data accessors conditionally vanish.
48
48
49
49
`model.related(name)` takes the raw relationship key from
50
50
`model.relationships`, not the Python field name. These keys often contain
@@ -87,7 +87,83 @@ pyTFE handles this on two levels:
87
87
print(subscription["attributes"])
88
88
```
89
89
90
-
## Which should I use — the typed field or the raw accessor?
90
+
## Traversing the raw blocks
91
+
92
+
`model.relationships` and `model.included` are plain Python dicts and lists,
93
+
shaped exactly like the JSON:API the server returns. Once you know that shape,
94
+
walking them is straightforward.
95
+
96
+
### `relationships` shape
97
+
98
+
Each key is a wire relation name. Its `data` is a single reference (to-one) or a
99
+
list of references (to-many), and each reference is just a `type` and an `id`:
| List every relation name |`list(ws.relationships)`|
137
+
| Check whether a relation was returned |`"outputs" in ws.relationships`|
138
+
| Get a to-one ref |`ws.relationships["organization"]["data"]` (a `{type, id}` dict, or `None`) |
139
+
| Get to-many refs |`ws.relationships["outputs"]["data"]` (a list of `{type, id}`) |
140
+
| Resolve a relation to full bodies |`ws.related("outputs")` (always a list) |
141
+
| Look up one included body by ref |`ws.included_by(ref["type"], ref["id"])` (a dict, or `None`) |
142
+
| Read an attribute off a body |`body["attributes"]["name"]`|
143
+
| Walk the whole `included` array |`for item in ws.included: ...` then `item["type"]`, `item["id"]`, `item["attributes"]`|
144
+
145
+
`related(name)` smooths over the to-one vs to-many difference for you: it always
146
+
returns a list (a single relation becomes a one-item list), and each entry is the
147
+
full `included` body when available, otherwise the bare `{type, id}` reference.
148
+
149
+
```python
150
+
# Safe end-to-end traversal of any relation, modelled or not:
151
+
for ref in ws.related("outputs"):
152
+
if"attributes"in ref: # resolved from `included`
153
+
print(ref["attributes"]["name"])
154
+
else: # only the bare ref (not requested with ?include=)
155
+
print("unresolved:", ref["type"], ref["id"])
156
+
```
157
+
158
+
Two things to remember while traversing:
159
+
160
+
* Relation **keys are the wire names**, so they often contain hyphens
161
+
(`"current-run"`, `"remote-state-consumers"`), not the Python field names.
162
+
* A relation can be **absent** (key missing), **unset** (`{"data": None}`), or
163
+
**empty to-many** (`{"data": []}`). `related(name)` returns `[]` for all three,
164
+
so you rarely need to special-case them.
165
+
166
+
## Which should I use: the typed field or the raw accessor?
91
167
92
168
**The one rule:** when a typed relationship is present, it carries **at least
93
169
the `id`**. Pass `?include=<relation>` to fill in the rest.
@@ -98,7 +174,7 @@ from pytfe.models.policy_set import PolicySetReadOptions, PolicySetIncludeOpt
98
174
ps = client.policy_sets.read("polset-abc")
99
175
if ps.current_version:
100
176
ps.current_version.id # present on the id-only stub
101
-
ps.current_version.source # None — you didn't ask for it
177
+
ps.current_version.source # None, you didn't ask for it
102
178
103
179
ps = client.policy_sets.read_with_options(
104
180
"polset-abc",
@@ -109,14 +185,14 @@ if ps.current_version:
109
185
```
110
186
111
187
***Prefer the typed field** (`ps.current_version`, `ws.outputs`, `team.users`,
112
-
`org_membership.user`, `run_event.actor`) whenever the relation is modelled — it's
188
+
`org_membership.user`, `run_event.actor`) whenever the relation is modelled. It's
113
189
type-checked and stable, and `?include=<relation>` fills it on single-resource
114
190
reads. This works the *same way for every resource that models the relation*:
115
191
there are no single-resource read paths where a typed field silently stays a
116
192
stub after you `?include=` it.
117
193
***Use the raw accessors** (`model.related(name)`, `model.included_by(type, id)`)
118
-
only for relations the SDK does **not** model as a typed field — e.g. an
119
-
organization's `subscription`, or a workspace `readme`. The data is still returned
194
+
only for relations the SDK does **not** model as a typed field, for example an
195
+
organization's `subscription` or a workspace `readme`. The data is still returned
120
196
by `?include=`, just untyped.
121
197
122
198
You never need both for the same relation: if a typed field exists, `?include=` fills
@@ -128,14 +204,14 @@ it; if it doesn't, the raw accessors are the way in.
128
204
129
205
| Behaviour | Resources |
130
206
|---|---|
131
-
|**Typed hydration** —`include` fills the typed field |`workspaces`, `runs`, `agent_pools`, `stack_configuration`, `teams`, `task_stages`, `policy_set`, `organization_membership`, `variable_set`, `run_event`, `no_code_modules.read_variables`|
132
-
|**Raw capture** — relation not modelled as a typed field; reach it via `related()`/`included_by()`|`organizations` (`subscription`), `state_versions`, `agents`, `configuration_version`, `oauth_client`, `projects`, `query_run`, `registry_provider`, `run_task`|
133
-
|**List-only** —`?include=` exists only on the `list` endpoint |`registry_module`, `run_trigger`, `policy_check`|
207
+
|**Typed hydration**:`include` fills the typed field |`workspaces`, `runs`, `agent_pools`, `stack_configuration`, `teams`, `task_stages`, `policy_set`, `organization_membership`, `variable_set`, `run_event`, `no_code_modules.read_variables`|
208
+
|**Raw capture**: relation not modelled as a typed field, reach it via `related()`or`included_by()`|`organizations` (`subscription`), `state_versions`, `agents`, `configuration_version`, `oauth_client`, `projects`, `query_run`, `registry_provider`, `run_task`|
209
+
|**List-only**:`?include=` exists only on the `list` endpoint |`registry_module`, `run_trigger`, `policy_check`|
134
210
135
211
In every case the **`relationships`** block and raw accessors are populated,
136
212
so unmodelled relations are never lost. **List endpoints** currently capture
137
213
`relationships` but not `included` (the page-level `included` array is not yet threaded
138
-
through pagination — in progress).
214
+
through pagination, in progress).
139
215
140
216
For list endpoints, this means `model.has_relationships` can be `True`, but
141
217
`model.has_included` is currently `False` even when the list options expose an
@@ -145,17 +221,17 @@ id-only stubs until you read a single resource with the matching read options.
145
221
## Notes
146
222
147
223
- The raw blocks are **private attributes**, so they never appear in
148
-
`model_dump()`/ serialized output and add no public fields. They're an
149
-
untyped escape hatch, not a stable typed API — prefer the typed fields when a
224
+
`model_dump()`or serialized output and add no public fields. They're an
225
+
untyped escape hatch, not a stable typed API, so prefer the typed fields when a
150
226
relation is modelled.
151
227
- This complements `extra="allow"`, which retains unknown **attributes**;
152
228
`relationships`/`included` cover unknown **relations**. Together nothing the
153
229
API returns is silently dropped.
154
230
- Accessors are provided by `pytfe.models.TFEModel`, which top-level
155
-
resource models derive from — so `.relationships` / `.included` /
156
-
`.included_by` /`.related` are available everywhere. They're *populated* on
231
+
resource models derive from, so `.relationships`, `.included`,
232
+
`.included_by`, and`.related` are available everywhere. They're *populated* on
157
233
single-resource `read*` calls: the `relationships` block on reads that go
158
234
through a relationship-capturing parser, and the `included` array whenever you
159
235
pass `?include=`. **List endpoints** currently populate `relationships` but not
160
-
`included` — the shared top-level `included` array is not yet threaded through
236
+
`included`; the shared top-level `included` array is not yet threaded through
0 commit comments