sg-python-api 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
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.
- {sg_python_api-0.2.0.dist-info → sg_python_api-0.2.1.dist-info}/METADATA +14 -14
- {sg_python_api-0.2.0.dist-info → sg_python_api-0.2.1.dist-info}/RECORD +12 -12
- shotgun_api3/lib/mockgun/_events.py +25 -4
- shotgun_api3/lib/mockgun/_handlers.py +1 -6
- shotgun_api3/lib/mockgun/_links.py +256 -98
- shotgun_api3/lib/mockgun/_query.py +79 -37
- shotgun_api3/lib/mockgun/_retirement.py +124 -29
- shotgun_api3/lib/mockgun/_store.py +153 -40
- shotgun_api3/lib/mockgun/_summaries.py +18 -17
- shotgun_api3/lib/mockgun/_threads.py +4 -4
- {sg_python_api-0.2.0.dist-info → sg_python_api-0.2.1.dist-info}/WHEEL +0 -0
- {sg_python_api-0.2.0.dist-info → sg_python_api-0.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,7 +5,7 @@ from dataclasses import dataclass
|
|
|
5
5
|
from typing import cast
|
|
6
6
|
|
|
7
7
|
from ._errors import MockgunError
|
|
8
|
-
from ._store import FieldValue, Row, Store
|
|
8
|
+
from ._store import CONNECTION_FIELDS, FieldValue, Row, Store
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@dataclass(frozen=True)
|
|
@@ -43,19 +43,28 @@ _DISPLAY_FIELDS = {
|
|
|
43
43
|
'ApiUser': ('firstname', 'lastname'),
|
|
44
44
|
}
|
|
45
45
|
_DISPLAY_LIMIT = 255
|
|
46
|
+
# Fields the site keeps in NoteLink and NoteTask records, which mockgun does not
|
|
47
|
+
# store; a retired holder hides their members (2026-11-10 live comparison).
|
|
48
|
+
_IMPLICIT_CONNECTIONS = {
|
|
49
|
+
'Note': frozenset({'note_links', 'tasks'}),
|
|
50
|
+
'Shot': frozenset({'notes'}),
|
|
51
|
+
}
|
|
46
52
|
_RETIREMENT_EDGES = {
|
|
53
|
+
# 2026-11-10: Attachment and Note links hide; neither end is touched or logged.
|
|
47
54
|
('Attachment', 'attachment_links', 'Note'): _RetirementRule(
|
|
48
|
-
'
|
|
55
|
+
'hide', 'always', touched=False, settled=True
|
|
49
56
|
),
|
|
50
57
|
('Attachment', 'attachment_links', 'Version'): _RetirementRule(
|
|
51
|
-
'
|
|
58
|
+
'hide', 'always', touched=False, settled=True
|
|
52
59
|
),
|
|
53
60
|
('Note', 'attachments', 'Attachment'): _RetirementRule(
|
|
54
|
-
'
|
|
61
|
+
'hide', 'always', touched=False, settled=True
|
|
55
62
|
),
|
|
56
|
-
|
|
63
|
+
# 2026-11-10: a live Task whose Shot is revived gains no event rows.
|
|
64
|
+
('Task', 'entity', 'Shot'): _RetirementRule('cascade', 'always', touched=False, settled=True),
|
|
65
|
+
# 2026-11-10: a live Version whose Shot is revived gains no event rows.
|
|
57
66
|
('Version', 'entity', 'Shot'): _RetirementRule(
|
|
58
|
-
'cascade', 'always', touched=False, settled=
|
|
67
|
+
'cascade', 'always', touched=False, settled=True
|
|
59
68
|
),
|
|
60
69
|
('Task', 'entity', 'Asset'): _RetirementRule(
|
|
61
70
|
'cascade', 'always', touched=False, settled=False
|
|
@@ -108,8 +117,13 @@ _RETIREMENT_EDGES = {
|
|
|
108
117
|
),
|
|
109
118
|
('CutItem', 'shot', 'Shot'): _RetirementRule('hide', 'always', touched=False, settled=True),
|
|
110
119
|
('PublishedFile', 'entity', 'Asset'): _RetirementRule(
|
|
111
|
-
'hide', 'always', touched=False, settled=
|
|
120
|
+
'hide', 'always', touched=False, settled=True
|
|
112
121
|
),
|
|
122
|
+
('PublishedFile', 'entity', 'Sequence'): _RetirementRule(
|
|
123
|
+
'hide', 'always', touched=False, settled=True
|
|
124
|
+
),
|
|
125
|
+
('TimeLog', 'entity', 'Shot'): _RetirementRule('hide', 'always', touched=False, settled=True),
|
|
126
|
+
('TimeLog', 'entity', 'Task'): _RetirementRule('hide', 'always', touched=False, settled=True),
|
|
113
127
|
('Shot', 'sg_sequence', 'Sequence'): _RetirementRule(
|
|
114
128
|
'hide', 'always', touched=False, settled=True
|
|
115
129
|
),
|
|
@@ -122,10 +136,11 @@ _RETIREMENT_EDGES = {
|
|
|
122
136
|
),
|
|
123
137
|
('Version', 'sg_task', 'Task'): _RetirementRule('hide', 'always', touched=False, settled=True),
|
|
124
138
|
('PublishedFile', 'task', 'Task'): _RetirementRule(
|
|
125
|
-
'hide', 'always', touched=False, settled=
|
|
139
|
+
'hide', 'always', touched=False, settled=True
|
|
126
140
|
),
|
|
127
141
|
('Sequence', 'shots', 'Shot'): _RetirementRule('hide', 'always', touched=False, settled=False),
|
|
128
|
-
|
|
142
|
+
# 2026-11-10: retiring or reviving a Task alone adds no row for its Shot.
|
|
143
|
+
('Shot', 'tasks', 'Task'): _RetirementRule('hide', 'always', touched=False, settled=True),
|
|
129
144
|
('Shot', 'notes', 'Note'): _RetirementRule('hide', 'always', touched=False, settled=False),
|
|
130
145
|
('Version', 'published_files', 'PublishedFile'): _RetirementRule(
|
|
131
146
|
'hide', 'always', touched=False, settled=False
|
|
@@ -143,7 +158,27 @@ _RETIREMENT_EDGES = {
|
|
|
143
158
|
('Asset', 'assets', 'Asset'): _RetirementRule('hide', 'always', touched=True, settled=False),
|
|
144
159
|
('Shot', 'assets', 'Asset'): _RetirementRule('hide', 'always', touched=True, settled=False),
|
|
145
160
|
('Asset', 'shots', 'Shot'): _RetirementRule('hide', 'always', touched=True, settled=False),
|
|
146
|
-
|
|
161
|
+
# 2026-11-10: a CutItem outlives its Cut with the link hidden and no rows.
|
|
162
|
+
('CutItem', 'cut', 'Cut'): _RetirementRule('hide', 'always', touched=False, settled=True),
|
|
163
|
+
# 2026-11-10: a connection record retires with either end and revives with it.
|
|
164
|
+
('AssetShotConnection', 'shot', 'Shot'): _RetirementRule(
|
|
165
|
+
'cascade', 'always', touched=False, settled=False
|
|
166
|
+
),
|
|
167
|
+
('AssetShotConnection', 'asset', 'Asset'): _RetirementRule(
|
|
168
|
+
'cascade', 'always', touched=False, settled=False
|
|
169
|
+
),
|
|
170
|
+
('AssetAssetConnection', 'parent', 'Asset'): _RetirementRule(
|
|
171
|
+
'cascade', 'always', touched=False, settled=False
|
|
172
|
+
),
|
|
173
|
+
('AssetAssetConnection', 'asset', 'Asset'): _RetirementRule(
|
|
174
|
+
'cascade', 'always', touched=False, settled=False
|
|
175
|
+
),
|
|
176
|
+
('PlaylistVersionConnection', 'version', 'Version'): _RetirementRule(
|
|
177
|
+
'cascade', 'always', touched=False, settled=False
|
|
178
|
+
),
|
|
179
|
+
('PlaylistVersionConnection', 'playlist', 'Playlist'): _RetirementRule(
|
|
180
|
+
'cascade', 'always', touched=False, settled=False
|
|
181
|
+
),
|
|
147
182
|
}
|
|
148
183
|
_NOTE_COMBINED_LIMIT = 83
|
|
149
184
|
_NOTE_SUBJECT_LIMIT = 120
|
|
@@ -154,8 +189,8 @@ class RetirementEdge:
|
|
|
154
189
|
"""Describe an immutable link retirement effect and its completeness.
|
|
155
190
|
|
|
156
191
|
Attributes:
|
|
157
|
-
effect: `hide`, `
|
|
158
|
-
|
|
192
|
+
effect: `hide`, `cascade`, or `taint`, describing the effect on the
|
|
193
|
+
holder when the target is retired.
|
|
159
194
|
touched: Whether hiding the link affects the holder's update audit
|
|
160
195
|
fields.
|
|
161
196
|
settled: Whether the edge's lifecycle-event effects are fully modeled.
|
|
@@ -195,26 +230,23 @@ def _listed(holder: Row, field_name: str, link: Mapping[str, FieldValue]) -> boo
|
|
|
195
230
|
)
|
|
196
231
|
|
|
197
232
|
|
|
198
|
-
def
|
|
199
|
-
"""
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if
|
|
204
|
-
return
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
if other is not None and not other['__retired'] and not store.tainted(other):
|
|
216
|
-
return RetirementEdge('hide')
|
|
217
|
-
return RetirementEdge('taint')
|
|
233
|
+
def _retired_target(target: Row, assume: Mapping[tuple[str, int], RecordState] | None) -> bool:
|
|
234
|
+
"""Return a target's assumed or settled retirement, rejecting an unsettled one."""
|
|
235
|
+
state = (
|
|
236
|
+
assume.get((cast('str', target['type']), cast('int', target['id']))) if assume else None
|
|
237
|
+
)
|
|
238
|
+
if state is not None:
|
|
239
|
+
return state.retired
|
|
240
|
+
taint = cast('dict[str, FieldValue]', target.get('__taint', {}))
|
|
241
|
+
if taint and not taint.get('active'):
|
|
242
|
+
raise MockgunError(f'{target["type"]} {target["id"]}: {taint["reason"]}')
|
|
243
|
+
return bool(target['__retired'])
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _uncertain(row: Row) -> bool:
|
|
247
|
+
"""Return whether a record's own retirement state is unsettled."""
|
|
248
|
+
taint = cast('dict[str, FieldValue]', row.get('__taint', {}))
|
|
249
|
+
return bool(taint) and not taint.get('active')
|
|
218
250
|
|
|
219
251
|
|
|
220
252
|
def _unsettled(entity_type: str, fields: tuple[str, ...]) -> MockgunError:
|
|
@@ -293,6 +325,45 @@ def _display(store: Store, entity_type: str, entity_id: int) -> tuple[str | None
|
|
|
293
325
|
return name, derived
|
|
294
326
|
|
|
295
327
|
|
|
328
|
+
def connection_backed(holder: Row, field_name: str, target: Row) -> bool:
|
|
329
|
+
"""Return whether the site keeps a link's membership in a connection record.
|
|
330
|
+
|
|
331
|
+
A retired holder hides such a member while its connection record is
|
|
332
|
+
retired, even when the target is active.
|
|
333
|
+
"""
|
|
334
|
+
kind = cast('str', holder['type'])
|
|
335
|
+
return (kind, field_name, cast('str', target['type'])) in CONNECTION_FIELDS or (
|
|
336
|
+
field_name in _IMPLICIT_CONNECTIONS.get(kind, frozenset())
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def connection_record(store: Store, holder: Row, field_name: str, target: Row) -> Row | None:
|
|
341
|
+
"""Return the stored connection record behind one member, retired or not.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
The live stored connection row, or `None` when the schema declares no
|
|
345
|
+
such record or none joins the two records.
|
|
346
|
+
"""
|
|
347
|
+
definition = CONNECTION_FIELDS.get(
|
|
348
|
+
(cast('str', holder['type']), field_name, cast('str', target['type']))
|
|
349
|
+
)
|
|
350
|
+
if definition is None:
|
|
351
|
+
return None
|
|
352
|
+
connection_type, holder_side, target_side = definition
|
|
353
|
+
if not (
|
|
354
|
+
store.schema.declares(connection_type, holder_side)
|
|
355
|
+
and store.schema.declares(connection_type, target_side)
|
|
356
|
+
):
|
|
357
|
+
return None
|
|
358
|
+
return store.connection_row(
|
|
359
|
+
connection_type,
|
|
360
|
+
{
|
|
361
|
+
holder_side: {'type': holder['type'], 'id': holder['id']},
|
|
362
|
+
target_side: {'type': target['type'], 'id': target['id']},
|
|
363
|
+
},
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
296
367
|
def derived_name(store: Store, entity_type: str, entity_id: int) -> bool:
|
|
297
368
|
"""Return whether a modeled name uses Task fallback or Note truncation.
|
|
298
369
|
|
|
@@ -311,37 +382,99 @@ def display_name(store: Store, entity_type: str, entity_id: int) -> str | None:
|
|
|
311
382
|
return _display(store, entity_type, entity_id)[0]
|
|
312
383
|
|
|
313
384
|
|
|
314
|
-
def
|
|
315
|
-
"""Return a
|
|
385
|
+
def implicit_connection_fields(entity_type: str) -> frozenset[str]:
|
|
386
|
+
"""Return a type's connection-backed fields whose records mockgun does not store."""
|
|
387
|
+
return _IMPLICIT_CONNECTIONS.get(entity_type, frozenset())
|
|
316
388
|
|
|
317
|
-
|
|
318
|
-
|
|
389
|
+
|
|
390
|
+
def implicit_link_hidden(holder: Row, field_name: str, target: Row) -> bool:
|
|
391
|
+
"""Return whether a retired holder hides an active NoteLink or NoteTask member.
|
|
392
|
+
|
|
393
|
+
The internal record stays retired with the holder, unless the target
|
|
394
|
+
retired first and so revived it on its own revival (2026-11-10).
|
|
319
395
|
"""
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if name is not None:
|
|
323
|
-
result['name'] = name
|
|
324
|
-
return result
|
|
396
|
+
member: FieldValue = [field_name, target['type'], target['id']]
|
|
397
|
+
return member not in cast('list[FieldValue]', holder.get('__retired_hidden', []))
|
|
325
398
|
|
|
326
399
|
|
|
327
|
-
def
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
400
|
+
def _retired_holder_hidden(
|
|
401
|
+
store: Store,
|
|
402
|
+
holder: Row,
|
|
403
|
+
field_name: str,
|
|
404
|
+
target: Row,
|
|
405
|
+
assume: Mapping[tuple[str, int], RecordState] | None,
|
|
406
|
+
) -> bool:
|
|
407
|
+
"""Resolve a retired holder's link from its target and any connection record."""
|
|
408
|
+
if _retired_target(target, assume):
|
|
409
|
+
return True
|
|
410
|
+
if not connection_backed(holder, field_name, target):
|
|
411
|
+
return False
|
|
412
|
+
record = connection_record(store, holder, field_name, target)
|
|
413
|
+
if record is not None:
|
|
414
|
+
return _retired_target(record, assume)
|
|
415
|
+
return implicit_link_hidden(holder, field_name, target)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def link_target(store: Store, link: Mapping[str, FieldValue]) -> Row | None:
|
|
419
|
+
"""Return the stored record a link names, including a retired one.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
The live stored row, or `None` when the link is malformed, names an
|
|
423
|
+
undeclared entity type, or names a missing record.
|
|
424
|
+
"""
|
|
425
|
+
kind, identity = link.get('type'), link.get('id')
|
|
426
|
+
if (
|
|
427
|
+
not isinstance(kind, str)
|
|
428
|
+
or not isinstance(identity, int)
|
|
429
|
+
or not store.schema.declares(kind)
|
|
340
430
|
):
|
|
431
|
+
return None
|
|
432
|
+
return store.row(kind, identity)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _live(store: Store, link: Mapping[str, FieldValue]) -> bool:
|
|
436
|
+
"""Return whether a link names an existing active record with settled state."""
|
|
437
|
+
row = link_target(store, link)
|
|
438
|
+
return row is not None and not row['__retired'] and not store.tainted(row)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _chained_task(store: Store, holder: Row, target: Row) -> bool:
|
|
442
|
+
"""Identify a Note linked to a Shot and to one live Task of that Shot."""
|
|
443
|
+
links = cast('list[dict[str, FieldValue]]', holder.get('note_links', []))
|
|
444
|
+
tasks = cast('list[dict[str, FieldValue]]', holder.get('tasks', []))
|
|
445
|
+
identity = {'type': target['type'], 'id': target['id']}
|
|
446
|
+
if target['type'] != 'Shot' or len(tasks) != 1 or links != [identity]:
|
|
447
|
+
return False
|
|
448
|
+
task = link_target(store, tasks[0])
|
|
449
|
+
return task is not None and task.get('entity') == identity and _live(store, tasks[0])
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _note_edge(store: Store, holder: Row, target: Row) -> RetirementEdge:
|
|
453
|
+
"""Resolve the observed Note.note_links shapes, including the Shot-Task chain."""
|
|
454
|
+
links = cast('list[dict[str, FieldValue]]', holder.get('note_links', []))
|
|
455
|
+
if holder.get('tasks'):
|
|
456
|
+
# A Note created for a Task also links the Task's Shot; both retire with the Shot.
|
|
457
|
+
return RetirementEdge('cascade' if _chained_task(store, holder, target) else 'taint')
|
|
458
|
+
if len(links) == 1:
|
|
459
|
+
return RetirementEdge('cascade')
|
|
460
|
+
others = [
|
|
461
|
+
link for link in links if (link['type'], link['id']) != (target['type'], target['id'])
|
|
462
|
+
]
|
|
463
|
+
# 2026-11-10: a two-link Note survives either link's retirement while the other is live.
|
|
464
|
+
if len(others) == 1 and len(links) == len(others) + 1 and _live(store, others[0]):
|
|
465
|
+
return RetirementEdge('hide')
|
|
466
|
+
return RetirementEdge('taint')
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _task_note_edge(store: Store, holder: Row, rule: _RetirementRule) -> RetirementEdge:
|
|
470
|
+
"""Resolve a Note's single Task link by whether a live note_links target remains."""
|
|
471
|
+
links = cast('list[dict[str, FieldValue]]', holder.get('note_links', []))
|
|
472
|
+
if len(cast('list[FieldValue]', holder.get('tasks', []))) != 1:
|
|
341
473
|
return RetirementEdge('taint')
|
|
342
|
-
if
|
|
343
|
-
return RetirementEdge(
|
|
344
|
-
|
|
474
|
+
if not links:
|
|
475
|
+
return RetirementEdge(rule.effect, rule.touched, rule.settled)
|
|
476
|
+
# 2026-11-10: a Note keeping a live note_links target survives its Task.
|
|
477
|
+
return RetirementEdge('hide' if all(_live(store, link) for link in links) else 'taint')
|
|
345
478
|
|
|
346
479
|
|
|
347
480
|
def deciding_record(
|
|
@@ -356,29 +489,21 @@ def deciding_record(
|
|
|
356
489
|
link: Target identity with `type` and `id`.
|
|
357
490
|
|
|
358
491
|
Returns:
|
|
359
|
-
The
|
|
360
|
-
`None` when no modeled uncertainty requires a deciding
|
|
492
|
+
The holder, target or connection row whose state decides link
|
|
493
|
+
visibility, or `None` when no modeled uncertainty requires a deciding
|
|
494
|
+
record.
|
|
361
495
|
"""
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
or not isinstance(link.get('id'), int)
|
|
365
|
-
or not store.schema.declares(cast('str', link['type']))
|
|
366
|
-
):
|
|
496
|
+
target = link_target(store, link)
|
|
497
|
+
if target is None or (target['type'], target['id']) == (holder['type'], holder['id']):
|
|
367
498
|
return None
|
|
368
499
|
if _listed(holder, field_name, link):
|
|
369
500
|
return holder
|
|
370
|
-
|
|
371
|
-
if target is None:
|
|
372
|
-
return None
|
|
373
|
-
edge = retirement_edge(store, holder, field_name, target)
|
|
374
|
-
if edge.effect == 'keep' and target['__retired']:
|
|
375
|
-
return holder
|
|
376
|
-
if (
|
|
377
|
-
edge.effect == 'hide'
|
|
378
|
-
and store.tainted(target)
|
|
379
|
-
and not cast('dict[str, FieldValue]', target['__taint']).get('active')
|
|
380
|
-
):
|
|
501
|
+
if _uncertain(target):
|
|
381
502
|
return target
|
|
503
|
+
if holder['__retired'] and connection_backed(holder, field_name, target):
|
|
504
|
+
record = connection_record(store, holder, field_name, target)
|
|
505
|
+
if record is not None and _uncertain(record):
|
|
506
|
+
return record
|
|
382
507
|
return None
|
|
383
508
|
|
|
384
509
|
|
|
@@ -392,6 +517,11 @@ def hidden(
|
|
|
392
517
|
) -> bool:
|
|
393
518
|
"""Resolve observed link hiding under optional record-state assumptions.
|
|
394
519
|
|
|
520
|
+
A link is hidden while its target is retired, whether the holder is active
|
|
521
|
+
or retired. A retired holder also hides a connection-backed member while
|
|
522
|
+
the connection record is retired. A link to the holder itself stays
|
|
523
|
+
visible.
|
|
524
|
+
|
|
395
525
|
Args:
|
|
396
526
|
store: Site records used to resolve link targets.
|
|
397
527
|
holder: Stored record containing the outgoing link.
|
|
@@ -402,39 +532,67 @@ def hidden(
|
|
|
402
532
|
|
|
403
533
|
Returns:
|
|
404
534
|
Whether the link is hidden under the observed rules and supplied
|
|
405
|
-
assumptions.
|
|
535
|
+
assumptions.
|
|
406
536
|
|
|
407
537
|
Raises:
|
|
408
538
|
MockgunError: If visibility depends on an uncertain record for which no
|
|
409
539
|
explicit state was supplied.
|
|
410
540
|
"""
|
|
541
|
+
target = link_target(store, link)
|
|
542
|
+
if target is None or (target['type'], target['id']) == (holder['type'], holder['id']):
|
|
543
|
+
return False
|
|
411
544
|
identity = (cast('str', holder['type']), cast('int', holder['id']))
|
|
412
545
|
holder_state = assume.get(identity) if assume is not None else None
|
|
413
|
-
|
|
414
|
-
return False
|
|
546
|
+
retired = holder_state.retired if holder_state is not None else bool(holder['__retired'])
|
|
415
547
|
deciding = deciding_record(store, holder, field_name, link)
|
|
416
548
|
if deciding is not None:
|
|
417
549
|
key = (cast('str', deciding['type']), cast('int', deciding['id']))
|
|
418
550
|
state = assume.get(key) if assume is not None else None
|
|
419
551
|
if state is None:
|
|
420
|
-
|
|
421
|
-
|
|
552
|
+
raise MockgunError(
|
|
553
|
+
f'{holder["type"]} {holder["id"]}.{field_name}: {store.tainted(deciding)}'
|
|
554
|
+
)
|
|
422
555
|
if deciding is holder:
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if
|
|
433
|
-
return
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
556
|
+
if not state.retired:
|
|
557
|
+
return (
|
|
558
|
+
field_name,
|
|
559
|
+
cast('str', link['type']),
|
|
560
|
+
cast('int', link['id']),
|
|
561
|
+
) in state.hidden
|
|
562
|
+
retired = True
|
|
563
|
+
elif not retired:
|
|
564
|
+
return state.retired
|
|
565
|
+
if retired:
|
|
566
|
+
return _retired_holder_hidden(store, holder, field_name, target, assume)
|
|
567
|
+
return _retired_target(target, assume)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def named_link(store: Store, link: Mapping[str, FieldValue]) -> dict[str, FieldValue]:
|
|
571
|
+
"""Return a detached identity with a name when the target is modeled.
|
|
572
|
+
|
|
573
|
+
Raises:
|
|
574
|
+
MockgunError: If the target's display name is unsettled.
|
|
575
|
+
"""
|
|
576
|
+
result = {'type': link['type'], 'id': link['id']}
|
|
577
|
+
name = display_name(store, cast('str', link['type']), cast('int', link['id']))
|
|
578
|
+
if name is not None:
|
|
579
|
+
result['name'] = name
|
|
580
|
+
return result
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def retirement_edge(store: Store, holder: Row, field_name: str, target: Row) -> RetirementEdge:
|
|
584
|
+
"""Resolve a retirement edge, including its observed holder conditions."""
|
|
585
|
+
kind = cast('str', holder['type'])
|
|
586
|
+
target_type = cast('str', target['type'])
|
|
587
|
+
if kind == 'EventLogEntry' and field_name == 'entity':
|
|
588
|
+
return RetirementEdge('hide')
|
|
589
|
+
rule = _RETIREMENT_EDGES.get((kind, field_name, target_type))
|
|
590
|
+
if rule is None:
|
|
591
|
+
return RetirementEdge('taint')
|
|
592
|
+
if rule.condition == 'note_links':
|
|
593
|
+
return _note_edge(store, holder, target)
|
|
594
|
+
if rule.condition == 'tasks':
|
|
595
|
+
return _task_note_edge(store, holder, rule)
|
|
596
|
+
if rule.condition == 'note_project' and (holder.get('note_links') or holder.get('tasks')):
|
|
597
|
+
return RetirementEdge('taint')
|
|
598
|
+
return RetirementEdge(rule.effect, rule.touched, rule.settled)
|