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
|
@@ -15,10 +15,14 @@ from ._errors import MockgunError, ServerFaultError
|
|
|
15
15
|
from ._events import check_meta
|
|
16
16
|
from ._links import (
|
|
17
17
|
RecordState,
|
|
18
|
+
connection_backed,
|
|
19
|
+
connection_record,
|
|
18
20
|
deciding_record,
|
|
19
21
|
derived_name,
|
|
20
22
|
display_name,
|
|
21
23
|
hidden,
|
|
24
|
+
implicit_link_hidden,
|
|
25
|
+
link_target,
|
|
22
26
|
named_link,
|
|
23
27
|
)
|
|
24
28
|
from ._presets import (
|
|
@@ -44,7 +48,7 @@ _COLLATION_CHARACTERS = frozenset(ascii_letters + digits)
|
|
|
44
48
|
_COLLATION_IGNORED = str.maketrans('', '', '_-. ')
|
|
45
49
|
_EntityLink: TypeAlias = dict[str, FieldValue]
|
|
46
50
|
_LINK_PATH_PARTS = 3
|
|
47
|
-
|
|
51
|
+
_MEMBER_ORDER_MINIMUM = 2
|
|
48
52
|
_NUMERIC_TYPES = frozenset({'date', 'date_time', 'duration', 'float', 'number', 'percent'})
|
|
49
53
|
_ORDER_TYPES = _NUMERIC_TYPES | {
|
|
50
54
|
'checkbox',
|
|
@@ -220,9 +224,7 @@ class _RowReader:
|
|
|
220
224
|
self._schema = schema
|
|
221
225
|
self._assume: dict[tuple[str, int], RecordState] | None = None
|
|
222
226
|
|
|
223
|
-
def _direct_field_value(
|
|
224
|
-
self, entity_type: str, row: Row, field_name: str, *, membership: bool = False
|
|
225
|
-
) -> FieldValue:
|
|
227
|
+
def _direct_field_value(self, entity_type: str, row: Row, field_name: str) -> FieldValue:
|
|
226
228
|
"""Read a stored field or compute its non-retained display cache."""
|
|
227
229
|
if entity_type == 'Project' and field_name == 'last_accessed_by_current_user':
|
|
228
230
|
context = self._store.context
|
|
@@ -241,8 +243,8 @@ class _RowReader:
|
|
|
241
243
|
'actor has never accessed the Project'
|
|
242
244
|
)
|
|
243
245
|
return value
|
|
244
|
-
if '__unknown' in row
|
|
245
|
-
self._store.check_known(row, field_name
|
|
246
|
+
if '__unknown' in row:
|
|
247
|
+
self._store.check_known(row, field_name)
|
|
246
248
|
if entity_type == 'EventLogEntry' and field_name == 'meta':
|
|
247
249
|
check_meta(self._store, row)
|
|
248
250
|
if field_name == 'cached_display_name' and entity_type != 'EventLogEntry':
|
|
@@ -282,7 +284,7 @@ class _Projection(_RowReader):
|
|
|
282
284
|
if len(parts) != _LINK_PATH_PARTS:
|
|
283
285
|
value = self._direct_field_value(entity_type, row, path)
|
|
284
286
|
visible = self._visible_value(row, path, value)
|
|
285
|
-
return True,
|
|
287
|
+
return True, visible
|
|
286
288
|
field, linked_type, rest = parts
|
|
287
289
|
link_value = self._direct_field_value(entity_type, row, field)
|
|
288
290
|
if self._visible_value(row, field, link_value) != link_value:
|
|
@@ -368,9 +370,11 @@ class _Projection(_RowReader):
|
|
|
368
370
|
continue
|
|
369
371
|
if field_type in {'entity', 'multi_entity'}:
|
|
370
372
|
if isinstance(value, list):
|
|
371
|
-
value =
|
|
372
|
-
|
|
373
|
-
|
|
373
|
+
value = _member_order(
|
|
374
|
+
entity_type,
|
|
375
|
+
name,
|
|
376
|
+
[named_link(self._store, cast('_EntityLink', item)) for item in value],
|
|
377
|
+
)
|
|
374
378
|
elif isinstance(value, dict):
|
|
375
379
|
value = named_link(self._store, value)
|
|
376
380
|
elif field_type == 'date_time':
|
|
@@ -608,7 +612,7 @@ class _Query(_RowReader):
|
|
|
608
612
|
) -> bool:
|
|
609
613
|
"""Reduce identity conditions before reading link visibility."""
|
|
610
614
|
field_type = self._schema.field_type(entity_type, path)
|
|
611
|
-
actual = self._direct_field_value(entity_type, row, path
|
|
615
|
+
actual = self._direct_field_value(entity_type, row, path)
|
|
612
616
|
if (
|
|
613
617
|
field_type in {'entity', 'multi_entity'}
|
|
614
618
|
and relation == 'is'
|
|
@@ -646,7 +650,7 @@ class _Query(_RowReader):
|
|
|
646
650
|
if len(parts) != _LINK_PATH_PARTS:
|
|
647
651
|
return self._matches_direct(entity_type, row, path, relation, values)
|
|
648
652
|
field, linked_type, rest = parts
|
|
649
|
-
actual = self._direct_field_value(entity_type, row, field
|
|
653
|
+
actual = self._direct_field_value(entity_type, row, field)
|
|
650
654
|
actual = self._hidden_filter_value(row, field, actual, relation, values)
|
|
651
655
|
links = actual if isinstance(actual, list) else [actual]
|
|
652
656
|
matches: list[bool] = []
|
|
@@ -956,6 +960,19 @@ class _Query(_RowReader):
|
|
|
956
960
|
)
|
|
957
961
|
return bool(row['__retired'])
|
|
958
962
|
|
|
963
|
+
def _retired_hidden(self, row: Row, field: str, link: _EntityLink) -> _Decision:
|
|
964
|
+
"""Hide a retired holder's links to retired targets and retired connections."""
|
|
965
|
+
target = link_target(self._store, link)
|
|
966
|
+
if target is None or (target['type'], target['id']) == (row['type'], row['id']):
|
|
967
|
+
return False
|
|
968
|
+
retired_target = self._symbolic_retired(target)
|
|
969
|
+
if not connection_backed(row, field, target):
|
|
970
|
+
return retired_target
|
|
971
|
+
record = connection_record(self._store, row, field, target)
|
|
972
|
+
if record is None:
|
|
973
|
+
return self._any(retired_target, implicit_link_hidden(row, field, target))
|
|
974
|
+
return self._any(retired_target, self._symbolic_retired(record))
|
|
975
|
+
|
|
959
976
|
def _symbolic_hidden(self, row: Row, field: str, link: _EntityLink) -> _Decision:
|
|
960
977
|
"""Express hiding through the deciding record and the holder's scope."""
|
|
961
978
|
deciding = deciding_record(self._store, row, field, link)
|
|
@@ -978,7 +995,9 @@ class _Query(_RowReader):
|
|
|
978
995
|
(cast('str', row['type']), cast('int', row['id'])): RecordState(retired=False)
|
|
979
996
|
}
|
|
980
997
|
outcome = hidden(self._store, row, field, link, assume=active)
|
|
981
|
-
return self._choose(
|
|
998
|
+
return self._choose(
|
|
999
|
+
self._symbolic_retired(row), yes=self._retired_hidden(row, field, link), no=outcome
|
|
1000
|
+
)
|
|
982
1001
|
|
|
983
1002
|
def _symbolic_archive(self, row: Row, scope: ReadScope) -> _Decision:
|
|
984
1003
|
"""Retain a project visibility bit only when its archive status matters."""
|
|
@@ -1004,7 +1023,7 @@ class _Query(_RowReader):
|
|
|
1004
1023
|
relation = cast('str', condition['relation'])
|
|
1005
1024
|
values = cast('list[FieldValue]', condition['values'])
|
|
1006
1025
|
field_type = self._schema.field_type(kind, path)
|
|
1007
|
-
actual = self._direct_field_value(kind, row, path
|
|
1026
|
+
actual = self._direct_field_value(kind, row, path)
|
|
1008
1027
|
if field_type not in {'entity', 'multi_entity'}:
|
|
1009
1028
|
return self._matches_direct(kind, row, path, relation, values)
|
|
1010
1029
|
links = actual if isinstance(actual, list) else [actual]
|
|
@@ -1037,9 +1056,7 @@ class _Query(_RowReader):
|
|
|
1037
1056
|
if len(path.split('.', 2)) != _LINK_PATH_PARTS:
|
|
1038
1057
|
return self._symbolic_direct(row, condition)
|
|
1039
1058
|
field = path.split('.', 1)[0]
|
|
1040
|
-
actual = self._direct_field_value(
|
|
1041
|
-
cast('str', row['type']), row, field, membership=True
|
|
1042
|
-
)
|
|
1059
|
+
actual = self._direct_field_value(cast('str', row['type']), row, field)
|
|
1043
1060
|
links = actual if isinstance(actual, list) else [actual]
|
|
1044
1061
|
result: _Decision = False
|
|
1045
1062
|
for link in links:
|
|
@@ -1952,26 +1969,51 @@ def _operand_fault(field_type: str, relation: str, value: object) -> str | None:
|
|
|
1952
1969
|
return None
|
|
1953
1970
|
|
|
1954
1971
|
|
|
1955
|
-
def
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1972
|
+
def _settled_member_keys(keys: Collection[str]) -> bool:
|
|
1973
|
+
"""Return whether observed comparisons order distinct lowered member names.
|
|
1974
|
+
|
|
1975
|
+
Names compare by character code (2026-11-10), with non-ASCII characters
|
|
1976
|
+
after ASCII ones; two differing non-ASCII characters were observed only in
|
|
1977
|
+
lowercase.
|
|
1978
|
+
"""
|
|
1979
|
+
for left, right in combinations(keys, 2):
|
|
1980
|
+
for first, second in zip(left, right, strict=False):
|
|
1981
|
+
if first != second:
|
|
1982
|
+
if not (first.isascii() or second.isascii()) and (
|
|
1983
|
+
first != first.lower() or second != second.lower()
|
|
1984
|
+
):
|
|
1985
|
+
return False
|
|
1986
|
+
break
|
|
1987
|
+
return True
|
|
1988
|
+
|
|
1989
|
+
|
|
1990
|
+
def _member_order(entity_type: str, field: str, links: list[_EntityLink]) -> list[_EntityLink]:
|
|
1991
|
+
"""Order members as the site reads them: by lowercased name, then by ID.
|
|
1992
|
+
|
|
1993
|
+
Members without a modeled display name keep the stored order when the named
|
|
1994
|
+
members already appear in their observed order within it.
|
|
1995
|
+
"""
|
|
1996
|
+
if len(links) < _MEMBER_ORDER_MINIMUM:
|
|
1997
|
+
return links
|
|
1998
|
+
keyed: list[tuple[str, int, int]] = []
|
|
1999
|
+
for index, link in enumerate(links):
|
|
2000
|
+
name = link.get('name')
|
|
2001
|
+
if isinstance(name, str):
|
|
2002
|
+
keyed.append((name.translate(_ASCII_LOWER), cast('int', link['id']), index))
|
|
2003
|
+
if not _settled_member_keys({key for key, _, _ in keyed}):
|
|
2004
|
+
raise MockgunError(
|
|
2005
|
+
f'mockgun cannot order {entity_type}.{field} members: their names compare by '
|
|
2006
|
+
'unobserved rules'
|
|
2007
|
+
)
|
|
2008
|
+
order = [index for _, _, index in sorted(keyed)]
|
|
2009
|
+
if len(keyed) == len(links):
|
|
2010
|
+
return [links[index] for index in order]
|
|
2011
|
+
if order != sorted(order):
|
|
2012
|
+
raise MockgunError(
|
|
2013
|
+
f'mockgun cannot order {entity_type}.{field} members: the order depends on '
|
|
2014
|
+
'members without a modeled display name; declare their display fields'
|
|
2015
|
+
)
|
|
2016
|
+
return links
|
|
1975
2017
|
|
|
1976
2018
|
|
|
1977
2019
|
def _validate_operands(
|
|
@@ -5,10 +5,27 @@ from dataclasses import dataclass
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from typing import cast
|
|
7
7
|
|
|
8
|
-
from ._links import
|
|
8
|
+
from ._links import (
|
|
9
|
+
RetirementEdge,
|
|
10
|
+
hidden,
|
|
11
|
+
implicit_connection_fields,
|
|
12
|
+
link_target,
|
|
13
|
+
retirement_edge,
|
|
14
|
+
)
|
|
9
15
|
from ._sessions import RequestContext
|
|
10
16
|
from ._store import FieldValue, Row, Store
|
|
11
17
|
|
|
18
|
+
# A cascaded child revived while its parent stays retired hides its link to the
|
|
19
|
+
# parent and is untouched by the parent's later revival (a Shot's Version
|
|
20
|
+
# earlier; the rest on 2026-11-10). Each child type maps to its parent field and
|
|
21
|
+
# the parent types observed.
|
|
22
|
+
_SETTLED_ALONE = {
|
|
23
|
+
'Note': ('note_links', frozenset({'Asset', 'Sequence', 'Shot', 'Version'})),
|
|
24
|
+
'Shot': ('project', frozenset({'Project'})),
|
|
25
|
+
'Task': ('entity', frozenset({'Asset', 'Sequence', 'Shot'})),
|
|
26
|
+
'Version': ('entity', frozenset({'Asset', 'Sequence', 'Shot'})),
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
|
|
13
30
|
@dataclass(frozen=True)
|
|
14
31
|
class RetirementEffects:
|
|
@@ -86,21 +103,54 @@ def _holder_groups(
|
|
|
86
103
|
return list(groups.values())
|
|
87
104
|
|
|
88
105
|
|
|
106
|
+
def _mark_hidden_links(store: Store, rows: list[Row]) -> None:
|
|
107
|
+
"""Record the NoteLink and NoteTask members already hidden when these rows retired."""
|
|
108
|
+
operation = {_identity(row) for row in rows}
|
|
109
|
+
for row in rows:
|
|
110
|
+
members: list[FieldValue] = []
|
|
111
|
+
for field in implicit_connection_fields(cast('str', row['type'])):
|
|
112
|
+
value = row.get(field)
|
|
113
|
+
for link in cast(
|
|
114
|
+
'list[dict[str, FieldValue]]', value if isinstance(value, list) else []
|
|
115
|
+
):
|
|
116
|
+
target = link_target(store, link)
|
|
117
|
+
if (
|
|
118
|
+
target is not None
|
|
119
|
+
and target['__retired']
|
|
120
|
+
and _identity(target) not in operation
|
|
121
|
+
):
|
|
122
|
+
members.append([field, target['type'], target['id']])
|
|
123
|
+
if members:
|
|
124
|
+
store.remember(row)
|
|
125
|
+
row['__retired_hidden'] = members
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _names(value: FieldValue, target: Row) -> bool:
|
|
129
|
+
"""Return whether a single or multi-entity value names the target."""
|
|
130
|
+
identity: FieldValue = {'type': target['type'], 'id': target['id']}
|
|
131
|
+
return value == identity or (isinstance(value, list) and identity in value)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _settled_alone(root: Row, source: Row) -> bool:
|
|
135
|
+
"""Identify the observed standalone revivals of cascaded children."""
|
|
136
|
+
entry = _SETTLED_ALONE.get(cast('str', root['type']))
|
|
137
|
+
if entry is None or source['type'] not in entry[1]:
|
|
138
|
+
return False
|
|
139
|
+
if root['type'] == 'Note' and root.get('tasks'):
|
|
140
|
+
return False
|
|
141
|
+
return _names(root.get(entry[0]), source)
|
|
142
|
+
|
|
143
|
+
|
|
89
144
|
def _revived_alone(store: Store, roots: tuple[Row, Row | None], children: list[Row]) -> None:
|
|
90
|
-
"""Taint
|
|
145
|
+
"""Taint unobserved standalone revivals, keeping the observed ones settled."""
|
|
91
146
|
root, source = roots
|
|
92
147
|
if source is None or not source['__retired']:
|
|
93
148
|
return
|
|
94
|
-
if (
|
|
95
|
-
root['type'] == 'Version'
|
|
96
|
-
and source['type'] == 'Shot'
|
|
97
|
-
and root.get('entity') == {'type': 'Shot', 'id': source['id']}
|
|
98
|
-
):
|
|
99
|
-
return
|
|
100
149
|
reason = f'unsettled revival while {source["type"]} {source["id"]} remains retired'
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
150
|
+
if not _settled_alone(root, source):
|
|
151
|
+
for holder, field in store.linking(*_identity(source)):
|
|
152
|
+
if holder is root:
|
|
153
|
+
store.taint(root, reason, source, link=(field, source), active=True)
|
|
104
154
|
for row in children:
|
|
105
155
|
store.taint(row, reason, source)
|
|
106
156
|
|
|
@@ -146,23 +196,25 @@ def _holder_effect(
|
|
|
146
196
|
_touch(store, holder, context)
|
|
147
197
|
return 'touch'
|
|
148
198
|
return 'hide'
|
|
149
|
-
if effect == 'keep' and not store.tainted(target):
|
|
150
|
-
return 'keep'
|
|
151
199
|
for field, _ in edges:
|
|
152
200
|
store.taint(holder, reason, root, link=(field, target))
|
|
153
201
|
return 'taint'
|
|
154
202
|
|
|
155
203
|
|
|
156
204
|
def _restore_children(store: Store, parent: Row, context: RequestContext) -> list[Row]:
|
|
157
|
-
"""Revive
|
|
205
|
+
"""Revive the children this cascade retired that still link the parent."""
|
|
158
206
|
children: list[Row] = []
|
|
159
207
|
for kind in store.schema.fields:
|
|
160
208
|
for row in store.rows(kind, retired=True):
|
|
161
209
|
if row.get('__cascaded_by') == [parent['type'], parent['id']]:
|
|
162
210
|
store.check_writable(row)
|
|
163
|
-
store.revive(*_identity(row))
|
|
164
211
|
store.remember(row)
|
|
165
212
|
row.pop('__cascaded_by', None)
|
|
213
|
+
# 2026-11-10: a child relinked or cleared while retired stays retired.
|
|
214
|
+
if not any(_names(row.get(field), parent) for field in store.schema.fields[kind]):
|
|
215
|
+
continue
|
|
216
|
+
store.revive(*_identity(row))
|
|
217
|
+
row.pop('__retired_hidden', None)
|
|
166
218
|
_touch(store, row, context)
|
|
167
219
|
children.append(row)
|
|
168
220
|
return children
|
|
@@ -175,11 +227,59 @@ def _restore_touches(store: Store, parent: Row, context: RequestContext) -> list
|
|
|
175
227
|
for link in cast('list[list[FieldValue]]', parent.pop('__retirement_touched', [])):
|
|
176
228
|
holder = store.require(cast('str', link[0]), cast('int', link[1]))
|
|
177
229
|
store.check_writable(holder)
|
|
230
|
+
# 2026-11-10: a holder retired meanwhile keeps its audit fields.
|
|
231
|
+
if holder['__retired']:
|
|
232
|
+
continue
|
|
178
233
|
_touch(store, holder, context)
|
|
179
234
|
holders.append(holder)
|
|
180
235
|
return holders
|
|
181
236
|
|
|
182
237
|
|
|
238
|
+
def _revive_marked_notes(store: Store, parent: Row, context: RequestContext) -> list[Row]:
|
|
239
|
+
"""Revive the observed Notes retired after this parent, or taint other shapes.
|
|
240
|
+
|
|
241
|
+
A Note linking one Shot and one Asset, retired while the Shot was retired,
|
|
242
|
+
revives with the Shot; a one-link Note, or one retired after its Task,
|
|
243
|
+
stays retired (2026-11-10).
|
|
244
|
+
"""
|
|
245
|
+
revived: list[Row] = []
|
|
246
|
+
if not store.schema.declares('Note', 'note_links'):
|
|
247
|
+
return revived
|
|
248
|
+
for note in store.rows('Note', retired=True):
|
|
249
|
+
markers = cast('list[list[FieldValue]]', note.get('__retired_hidden', []))
|
|
250
|
+
fields = {
|
|
251
|
+
cast('str', marker[0])
|
|
252
|
+
for marker in markers
|
|
253
|
+
if marker[1:] == [parent['type'], parent['id']]
|
|
254
|
+
}
|
|
255
|
+
if not fields:
|
|
256
|
+
continue
|
|
257
|
+
links = cast('list[dict[str, FieldValue]]', note.get('note_links', []))
|
|
258
|
+
# 2026-11-10: a one-link Note, or one retired after its Task, stays retired.
|
|
259
|
+
if fields == {'tasks'} or (
|
|
260
|
+
fields == {'note_links'} and len(links) == 1 and not note.get('tasks')
|
|
261
|
+
):
|
|
262
|
+
continue
|
|
263
|
+
store.check_writable(note)
|
|
264
|
+
if (
|
|
265
|
+
fields == {'note_links'}
|
|
266
|
+
and parent['type'] == 'Shot'
|
|
267
|
+
and not note.get('tasks')
|
|
268
|
+
and sorted(cast('str', link['type']) for link in links) == ['Asset', 'Shot']
|
|
269
|
+
):
|
|
270
|
+
store.revive(*_identity(note))
|
|
271
|
+
store.remember(note)
|
|
272
|
+
note.pop('__retired_hidden', None)
|
|
273
|
+
_touch(store, note, context)
|
|
274
|
+
revived.append(note)
|
|
275
|
+
else:
|
|
276
|
+
reason = (
|
|
277
|
+
f'unsettled revival of {parent["type"]} {parent["id"]} for a Note retired after it'
|
|
278
|
+
)
|
|
279
|
+
store.taint(note, reason, None)
|
|
280
|
+
return revived
|
|
281
|
+
|
|
282
|
+
|
|
183
283
|
def _revive(store: Store, root: Row, context: RequestContext) -> RetirementEffects:
|
|
184
284
|
"""Restore marked children and touches, then resolve source-owned taints."""
|
|
185
285
|
store.remember(root)
|
|
@@ -191,6 +291,7 @@ def _revive(store: Store, root: Row, context: RequestContext) -> RetirementEffec
|
|
|
191
291
|
)
|
|
192
292
|
retired_at = cast('datetime | None', root.get('__retired_at'))
|
|
193
293
|
store.revive(*_identity(root), context=context)
|
|
294
|
+
root.pop('__retired_hidden', None)
|
|
194
295
|
cascaded: list[Row] = []
|
|
195
296
|
touched: list[Row] = []
|
|
196
297
|
queue = [root]
|
|
@@ -207,6 +308,7 @@ def _revive(store: Store, root: Row, context: RequestContext) -> RetirementEffec
|
|
|
207
308
|
cascaded.extend(children)
|
|
208
309
|
queue.extend(children)
|
|
209
310
|
touched.extend(_restore_touches(store, parent, context))
|
|
311
|
+
cascaded.extend(_revive_marked_notes(store, root, context))
|
|
210
312
|
cleared = _clear_source(store, root)
|
|
211
313
|
_revived_alone(store, (root, source), cascaded)
|
|
212
314
|
return RetirementEffects(
|
|
@@ -253,6 +355,7 @@ def _scan(store: Store, root: Row, context: RequestContext) -> RetirementEffects
|
|
|
253
355
|
queue.append(holder)
|
|
254
356
|
store.remember(root)
|
|
255
357
|
root['__retirement_touched'] = [[row['type'], row['id']] for row in touched]
|
|
358
|
+
_mark_hidden_links(store, [root, *cascaded])
|
|
256
359
|
return RetirementEffects(
|
|
257
360
|
changed=True,
|
|
258
361
|
root=root,
|
|
@@ -270,14 +373,16 @@ def kept_members(
|
|
|
270
373
|
entity_id: int,
|
|
271
374
|
fields: Mapping[str, object],
|
|
272
375
|
) -> dict[str, list[dict[str, FieldValue]]]:
|
|
273
|
-
"""Keep hidden Asset.shots members
|
|
376
|
+
"""Keep hidden Asset.shots members through an edit of the visible members.
|
|
377
|
+
|
|
378
|
+
The edit cannot name a hidden member: the site refuses links to retired
|
|
379
|
+
records before this runs.
|
|
274
380
|
|
|
275
381
|
Args:
|
|
276
382
|
store: Site records used to inspect existing shot links.
|
|
277
383
|
entity_type: Entity type being updated.
|
|
278
384
|
entity_id: Existing entity identifier.
|
|
279
|
-
fields: Incoming field values.
|
|
280
|
-
disables automatic preservation.
|
|
385
|
+
fields: Incoming field values.
|
|
281
386
|
|
|
282
387
|
Returns:
|
|
283
388
|
A `shots` entry containing preserved stored link dictionaries, or an
|
|
@@ -287,23 +392,13 @@ def kept_members(
|
|
|
287
392
|
ServerFaultError: If the Asset record cannot be resolved.
|
|
288
393
|
MockgunError: If an existing shot link's visibility is unsettled.
|
|
289
394
|
"""
|
|
290
|
-
if entity_type != 'Asset' or 'shots'
|
|
395
|
+
if entity_type != 'Asset' or not isinstance(fields.get('shots'), list):
|
|
291
396
|
return {}
|
|
292
397
|
row = store.require(entity_type, entity_id)
|
|
293
398
|
if row['__retired']:
|
|
294
399
|
return {}
|
|
295
400
|
members = cast('list[dict[str, FieldValue]]', row.get('shots', []))
|
|
296
401
|
kept = [link for link in members if hidden(store, row, 'shots', link)]
|
|
297
|
-
incoming = fields['shots']
|
|
298
|
-
if not isinstance(incoming, list):
|
|
299
|
-
return {}
|
|
300
|
-
identities = {
|
|
301
|
-
(cast('dict[str, object]', item).get('type'), cast('dict[str, object]', item).get('id'))
|
|
302
|
-
for item in cast('list[object]', incoming)
|
|
303
|
-
if isinstance(item, dict)
|
|
304
|
-
}
|
|
305
|
-
if any((link['type'], link['id']) in identities for link in kept):
|
|
306
|
-
return {}
|
|
307
402
|
return {'shots': kept} if kept else {}
|
|
308
403
|
|
|
309
404
|
|