dcicutils 8.7.1.1b4__py3-none-any.whl → 8.7.1.1b6__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.
- dcicutils/data_readers.py +13 -3
- dcicutils/misc_utils.py +23 -4
- dcicutils/portal_object_utils.py +126 -68
- dcicutils/structured_data.py +42 -28
- {dcicutils-8.7.1.1b4.dist-info → dcicutils-8.7.1.1b6.dist-info}/METADATA +1 -1
- {dcicutils-8.7.1.1b4.dist-info → dcicutils-8.7.1.1b6.dist-info}/RECORD +9 -9
- {dcicutils-8.7.1.1b4.dist-info → dcicutils-8.7.1.1b6.dist-info}/LICENSE.txt +0 -0
- {dcicutils-8.7.1.1b4.dist-info → dcicutils-8.7.1.1b6.dist-info}/WHEEL +0 -0
- {dcicutils-8.7.1.1b4.dist-info → dcicutils-8.7.1.1b6.dist-info}/entry_points.txt +0 -0
dcicutils/data_readers.py
CHANGED
@@ -7,11 +7,21 @@ from dcicutils.misc_utils import create_dict, right_trim
|
|
7
7
|
# Forward type references for type hints.
|
8
8
|
Excel = Type["Excel"]
|
9
9
|
|
10
|
+
# Cell values(s) indicating property deletion.
|
11
|
+
_CELL_DELETION_VALUES = ["*delete*"]
|
12
|
+
|
13
|
+
|
14
|
+
# Special cell deletion sentinel value (note make sure on deepcopy it remains the same).
|
15
|
+
class _CellDeletionSentinal(str):
|
16
|
+
def __new__(cls):
|
17
|
+
return super(_CellDeletionSentinal, cls).__new__(cls, _CELL_DELETION_VALUES[0])
|
18
|
+
def __deepcopy__(self, memo): # noqa
|
19
|
+
return self
|
20
|
+
|
10
21
|
|
11
22
|
class RowReader(abc.ABC):
|
12
23
|
|
13
|
-
|
14
|
-
CELL_DELETION_SENTINEL = object() # special cell deletion sentinel value
|
24
|
+
CELL_DELETION_SENTINEL = _CellDeletionSentinal()
|
15
25
|
|
16
26
|
def __init__(self):
|
17
27
|
self.header = None
|
@@ -51,7 +61,7 @@ class RowReader(abc.ABC):
|
|
51
61
|
def cell_value(self, value: Optional[Any]) -> str:
|
52
62
|
if value is None:
|
53
63
|
return ""
|
54
|
-
elif (value := str(value).strip()) in
|
64
|
+
elif (value := str(value).strip()) in _CELL_DELETION_VALUES:
|
55
65
|
return RowReader.CELL_DELETION_SENTINEL
|
56
66
|
else:
|
57
67
|
return value
|
dcicutils/misc_utils.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
This file contains functions that might be generally useful.
|
3
3
|
"""
|
4
4
|
|
5
|
+
from collections import namedtuple
|
5
6
|
import contextlib
|
6
7
|
import datetime
|
7
8
|
import functools
|
@@ -17,6 +18,7 @@ import re
|
|
17
18
|
import rfc3986.validators
|
18
19
|
import rfc3986.exceptions
|
19
20
|
import time
|
21
|
+
import uuid
|
20
22
|
import warnings
|
21
23
|
import webtest # importing the library makes it easier to mock testing
|
22
24
|
|
@@ -1148,16 +1150,22 @@ def remove_suffix(suffix: str, text: str, required: bool = False):
|
|
1148
1150
|
return text[:len(text)-len(suffix)]
|
1149
1151
|
|
1150
1152
|
|
1151
|
-
def remove_empty_properties(data: Optional[Union[list, dict]]
|
1153
|
+
def remove_empty_properties(data: Optional[Union[list, dict]],
|
1154
|
+
isempty: Optional[Callable] = None,
|
1155
|
+
isempty_array_element: Optional[Callable] = None) -> None:
|
1156
|
+
def _isempty(value: Any) -> bool: # noqa
|
1157
|
+
return isempty(value) if callable(isempty) else value in [None, "", {}, []]
|
1152
1158
|
if isinstance(data, dict):
|
1153
1159
|
for key in list(data.keys()):
|
1154
|
-
if (value := data[key])
|
1160
|
+
if _isempty(value := data[key]):
|
1155
1161
|
del data[key]
|
1156
1162
|
else:
|
1157
|
-
remove_empty_properties(value)
|
1163
|
+
remove_empty_properties(value, isempty=isempty, isempty_array_element=isempty_array_element)
|
1158
1164
|
elif isinstance(data, list):
|
1159
1165
|
for item in data:
|
1160
|
-
remove_empty_properties(item)
|
1166
|
+
remove_empty_properties(item, isempty=isempty, isempty_array_element=isempty_array_element)
|
1167
|
+
if callable(isempty_array_element):
|
1168
|
+
data[:] = [item for item in data if not isempty_array_element(item)]
|
1161
1169
|
|
1162
1170
|
|
1163
1171
|
class ObsoleteError(Exception):
|
@@ -1519,6 +1527,17 @@ def create_dict(**kwargs) -> dict:
|
|
1519
1527
|
return result
|
1520
1528
|
|
1521
1529
|
|
1530
|
+
def create_readonly_object(**kwargs):
|
1531
|
+
"""
|
1532
|
+
Returns a new/unique object instance with readonly properties equal to the give kwargs.
|
1533
|
+
"""
|
1534
|
+
readonly_class_name = "readonlyclass_" + str(uuid.uuid4()).replace("-", "")
|
1535
|
+
readonly_class_args = " ".join(kwargs.keys())
|
1536
|
+
readonly_class = namedtuple(readonly_class_name, readonly_class_args)
|
1537
|
+
readonly_object = readonly_class(**kwargs)
|
1538
|
+
return readonly_object
|
1539
|
+
|
1540
|
+
|
1522
1541
|
def is_c4_arn(arn: str) -> bool:
|
1523
1542
|
"""
|
1524
1543
|
Returns True iff the given (presumed) AWS ARN string value looks like it
|
dcicutils/portal_object_utils.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
from copy import deepcopy
|
1
2
|
from functools import lru_cache
|
2
3
|
import re
|
3
|
-
from typing import Any,
|
4
|
+
from typing import Any, List, Optional, Tuple, Type, Union
|
4
5
|
from dcicutils.data_readers import RowReader
|
6
|
+
from dcicutils.misc_utils import create_readonly_object
|
5
7
|
from dcicutils.portal_utils import Portal
|
6
8
|
from dcicutils.schema_utils import Schema
|
7
9
|
|
@@ -10,6 +12,8 @@ PortalObject = Type["PortalObject"] # Forward type reference for type hints.
|
|
10
12
|
|
11
13
|
class PortalObject:
|
12
14
|
|
15
|
+
_PROPERTY_DELETION_SENTINEL = RowReader.CELL_DELETION_SENTINEL
|
16
|
+
|
13
17
|
def __init__(self, portal: Portal, portal_object: dict, portal_object_type: Optional[str] = None) -> None:
|
14
18
|
self._portal = portal
|
15
19
|
self._data = portal_object
|
@@ -19,6 +23,10 @@ class PortalObject:
|
|
19
23
|
def data(self):
|
20
24
|
return self._data
|
21
25
|
|
26
|
+
@property
|
27
|
+
def portal(self):
|
28
|
+
return self._portal
|
29
|
+
|
22
30
|
@property
|
23
31
|
@lru_cache(maxsize=1)
|
24
32
|
def type(self):
|
@@ -39,6 +47,9 @@ class PortalObject:
|
|
39
47
|
def schema(self):
|
40
48
|
return self._portal.get_schema(self.type)
|
41
49
|
|
50
|
+
def copy(self) -> PortalObject:
|
51
|
+
return PortalObject(self.portal, deepcopy(self.data), self.type)
|
52
|
+
|
42
53
|
@property
|
43
54
|
@lru_cache(maxsize=1)
|
44
55
|
def identifying_properties(self) -> List[str]:
|
@@ -114,77 +125,124 @@ class PortalObject:
|
|
114
125
|
pass
|
115
126
|
return None, self.identifying_path
|
116
127
|
|
117
|
-
def compare(self, value: Union[dict, PortalObject],
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
nonlocal self
|
131
|
-
if (schema := self.schema) and (property_type := Schema.get_property_by_path(schema, property_path)):
|
132
|
-
if link_to := property_type.get("linkTo"):
|
133
|
-
"""
|
134
|
-
This works basically except not WRT sub/super-types (e.g. CellCultureSample vs Sample);
|
135
|
-
this is only preferable as it only requires one Portal GET rather than two, as below.
|
136
|
-
if (a := self._portal.get(f"/{link_to}/{property_value_a}")) and (a.status_code == 200):
|
137
|
-
if a_identifying_paths := PortalObject(self._portal, a.json()).identifying_paths:
|
138
|
-
if f"/{link_to}/{property_value_b}" in a_identifying_paths:
|
139
|
-
return True
|
140
|
-
"""
|
141
|
-
if a := self._portal.get(f"/{link_to}/{property_value_a}", raw=True):
|
142
|
-
if (a.status_code == 200) and (a := a.json()):
|
143
|
-
if b := self._portal.get(f"/{link_to}/{property_value_b}", raw=True):
|
144
|
-
if (b.status_code == 200) and (b := b.json()):
|
145
|
-
return a == b
|
146
|
-
return False
|
147
|
-
return PortalObject._compare(self._data, value.data if isinstance(value, PortalObject) else value,
|
148
|
-
compare=are_properties_equal if consider_link_to else None)
|
128
|
+
def compare(self, value: Union[dict, PortalObject],
|
129
|
+
consider_refs: bool = False, resolved_refs: List[dict] = None) -> dict:
|
130
|
+
if consider_refs and isinstance(resolved_refs, list):
|
131
|
+
this_data = self.normalized_refs(refs=resolved_refs).data
|
132
|
+
else:
|
133
|
+
this_data = self.data
|
134
|
+
if isinstance(value, PortalObject):
|
135
|
+
comparing_data = value.data
|
136
|
+
elif isinstance(value, dict):
|
137
|
+
comparing_data = value
|
138
|
+
else:
|
139
|
+
return {}
|
140
|
+
return PortalObject._compare(this_data, comparing_data, self.type)
|
149
141
|
|
150
142
|
_ARRAY_KEY_REGULAR_EXPRESSION = re.compile(rf"^({Schema._ARRAY_NAME_SUFFIX_CHAR}\d+)$")
|
151
143
|
|
152
144
|
@staticmethod
|
153
|
-
def _compare(a:
|
154
|
-
def
|
155
|
-
nonlocal
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
145
|
+
def _compare(a: Any, b: Any, value_type: str, _path: Optional[str] = None) -> dict:
|
146
|
+
def diff_creating(value: Any) -> object: # noqa
|
147
|
+
nonlocal value_type
|
148
|
+
return create_readonly_object(value=value, type=value_type,
|
149
|
+
creating_value=True, updating_value=None, deleting_value=False)
|
150
|
+
def diff_updating(value: Any, updating_value: Any) -> object: # noqa
|
151
|
+
nonlocal value_type
|
152
|
+
return create_readonly_object(value=value, type=value_type,
|
153
|
+
creating_value=False, updating_value=updating_value, deleting_value=False)
|
154
|
+
def diff_deleting(value: Any) -> object: # noqa
|
155
|
+
nonlocal value_type
|
156
|
+
return create_readonly_object(value=value, type=value_type,
|
157
|
+
creating_value=False, updating_value=None, deleting_value=True)
|
164
158
|
diffs = {}
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
if
|
169
|
-
|
159
|
+
if isinstance(a, dict) and isinstance(b, dict):
|
160
|
+
for key in a:
|
161
|
+
path = f"{_path}.{key}" if _path else key
|
162
|
+
if key not in b:
|
163
|
+
if a[key] != PortalObject._PROPERTY_DELETION_SENTINEL:
|
164
|
+
diffs[path] = diff_creating(a[key])
|
165
|
+
else:
|
166
|
+
diffs.update(PortalObject._compare(a[key], b[key], type, _path=path))
|
167
|
+
elif isinstance(a, list) and isinstance(b, list):
|
168
|
+
# Ignore order of array elements; not absolutely technically correct but suits our purpose.
|
169
|
+
for index in range(len(a)):
|
170
|
+
path = f"{_path or ''}#{index}"
|
171
|
+
if not isinstance(a[index], dict) and not isinstance(a[index], list):
|
172
|
+
if a[index] not in b:
|
173
|
+
if a[index] != PortalObject._PROPERTY_DELETION_SENTINEL:
|
174
|
+
if index < len(b):
|
175
|
+
diffs[path] = diff_updating(a[index], b[index])
|
176
|
+
else:
|
177
|
+
diffs[path] = diff_creating(a[index])
|
178
|
+
else:
|
179
|
+
if index < len(b):
|
180
|
+
diffs[path] = diff_deleting(b[index])
|
181
|
+
elif len(b) < index:
|
182
|
+
diffs.update(PortalObject._compare(a[index], b[index], value_type, _path=path))
|
183
|
+
else:
|
184
|
+
diffs[path] = diff_creating(a[index])
|
185
|
+
elif a != b:
|
186
|
+
if a == PortalObject._PROPERTY_DELETION_SENTINEL:
|
187
|
+
diffs[_path] = diff_deleting(b)
|
170
188
|
else:
|
171
|
-
|
172
|
-
diffs.update(PortalObject._compare(a[key], b[key], compare=compare, _path=path))
|
173
|
-
elif isinstance(a[key], list) and isinstance(b[key], list):
|
174
|
-
# Note that lists will be compared in order, which means the when dealing with
|
175
|
-
# insertions/deletions to/from the list, we my easily mistakenly regard elements
|
176
|
-
# of the list to be different when they are really the same, since they occupy
|
177
|
-
# different indices within the array. This is just a known restriction of this
|
178
|
-
# comparison functionality; and perhaps actually technically correct, but probably
|
179
|
-
# in practice, at the application/semantic level, we likely regard the order of
|
180
|
-
# lists as unimportant, and with a little more work here we could try to detect
|
181
|
-
# and exclude from the diffs for a list, those elements in the list which are
|
182
|
-
# equal to each other but which reside at different indices with then two lists.
|
183
|
-
diffs.update(PortalObject._compare(list_to_dictionary(a[key]),
|
184
|
-
list_to_dictionary(b[key]), compare=compare, _path=path))
|
185
|
-
elif a[key] != b[key]:
|
186
|
-
if a[key] == RowReader.CELL_DELETION_SENTINEL:
|
187
|
-
diffs[path] = {"value": b[key], "deleting_value": True}
|
188
|
-
elif not callable(compare) or not compare(path, a[key], b[key]):
|
189
|
-
diffs[path] = {"value": a[key], "updating_value": b[key]}
|
189
|
+
diffs[_path] = diff_updating(a, b)
|
190
190
|
return diffs
|
191
|
+
|
192
|
+
def normalize_refs(self, refs: List[dict]) -> None:
|
193
|
+
"""
|
194
|
+
Turns any (linkTo) references which are paths (e.g. /SubmissionCenter/uwsc_gcc) within
|
195
|
+
this Portal object into the uuid style reference (e.g. d1b67068-300f-483f-bfe8-63d23c93801f),
|
196
|
+
based on the given "refs" list which is assumed to be a list of dictionaries, where each
|
197
|
+
contains a "path" and a "uuid" property; this list is typically (for our first usage of
|
198
|
+
this function) the value of structured_data.StructuredDataSet.resolved_refs_with_uuid.
|
199
|
+
Changes are made to this Portal object in place; use normalized_refs function to make a copy.
|
200
|
+
If there are no "refs" (None or empty) or if the speicified reference is not found in this
|
201
|
+
list then the references will be looked up via Portal calls (via Portal.get_metadata).
|
202
|
+
"""
|
203
|
+
PortalObject._normalize_refs(self.data, refs=refs, schema=self.schema, portal=self.portal)
|
204
|
+
|
205
|
+
def normalized_refs(self, refs: List[dict]) -> PortalObject:
|
206
|
+
"""
|
207
|
+
Same as normalize_ref but does not make this change to this Portal object in place,
|
208
|
+
rather it returns a new instance of this Portal object wrapped in a new PortalObject.
|
209
|
+
"""
|
210
|
+
portal_object = self.copy()
|
211
|
+
portal_object.normalize_refs(refs)
|
212
|
+
return portal_object
|
213
|
+
|
214
|
+
@staticmethod
|
215
|
+
def _normalize_refs(value: Any, refs: List[dict], schema: dict, portal: Portal, _path: Optional[str] = None) -> Any:
|
216
|
+
if not value or not isinstance(schema, dict):
|
217
|
+
return value
|
218
|
+
if isinstance(value, dict):
|
219
|
+
for key in value:
|
220
|
+
path = f"{_path}.{key}" if _path else key
|
221
|
+
value[key] = PortalObject._normalize_refs(value[key], refs=refs,
|
222
|
+
schema=schema, portal=portal, _path=path)
|
223
|
+
elif isinstance(value, list):
|
224
|
+
for index in range(len(value)):
|
225
|
+
path = f"{_path or ''}#{index}"
|
226
|
+
value[index] = PortalObject._normalize_refs(value[index], refs=refs,
|
227
|
+
schema=schema, portal=portal, _path=path)
|
228
|
+
elif value_type := Schema.get_property_by_path(schema, _path):
|
229
|
+
if link_to := value_type.get("linkTo"):
|
230
|
+
ref_path = f"/{link_to}/{value}"
|
231
|
+
if not isinstance(refs, list):
|
232
|
+
refs = []
|
233
|
+
if ref_uuids := [ref.get("uuid") for ref in refs if ref.get("path") == ref_path]:
|
234
|
+
ref_uuid = ref_uuids[0]
|
235
|
+
else:
|
236
|
+
ref_uuid = None
|
237
|
+
if ref_uuid:
|
238
|
+
return ref_uuid
|
239
|
+
# Here our (linkTo) reference appears not to be in the given refs; if these refs came
|
240
|
+
# from structured_data.StructuredDataSet.resolved_refs_with_uuid (in the context of
|
241
|
+
# smaht-submitr, which is the typical/first use case for this function) then this could
|
242
|
+
# be because the reference was to an internal object, i.e. another object existing within
|
243
|
+
# the data/spreadsheet being submitted. In any case, we don't have the associated uuid
|
244
|
+
# so let us look it up here.
|
245
|
+
if isinstance(portal, Portal):
|
246
|
+
if (ref_object := portal.get_metadata(ref_path)) and (ref_uuid := ref_object.get("uuid")):
|
247
|
+
return ref_uuid
|
248
|
+
return value
|
dcicutils/structured_data.py
CHANGED
@@ -46,8 +46,8 @@ class StructuredDataSet:
|
|
46
46
|
def __init__(self, file: Optional[str] = None, portal: Optional[Union[VirtualApp, TestApp, Portal]] = None,
|
47
47
|
schemas: Optional[List[dict]] = None, autoadd: Optional[dict] = None,
|
48
48
|
order: Optional[List[str]] = None, prune: bool = True) -> None:
|
49
|
-
self.
|
50
|
-
self._portal = Portal(portal, data=self.
|
49
|
+
self._data = {}
|
50
|
+
self._portal = Portal(portal, data=self._data, schemas=schemas) if portal else None
|
51
51
|
self._order = order
|
52
52
|
self._prune = prune
|
53
53
|
self._warnings = {}
|
@@ -57,6 +57,10 @@ class StructuredDataSet:
|
|
57
57
|
self._autoadd_properties = autoadd if isinstance(autoadd, dict) and autoadd else None
|
58
58
|
self._load_file(file) if file else None
|
59
59
|
|
60
|
+
@property
|
61
|
+
def data(self) -> dict:
|
62
|
+
return self._data
|
63
|
+
|
60
64
|
@staticmethod
|
61
65
|
def load(file: str, portal: Optional[Union[VirtualApp, TestApp, Portal]] = None,
|
62
66
|
schemas: Optional[List[dict]] = None, autoadd: Optional[dict] = None,
|
@@ -66,10 +70,15 @@ class StructuredDataSet:
|
|
66
70
|
def validate(self, force: bool = False) -> None:
|
67
71
|
def data_without_deleted_properties(data: dict) -> dict:
|
68
72
|
nonlocal self
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
return
|
73
|
+
def isempty(value: Any) -> bool: # noqa
|
74
|
+
if value == RowReader.CELL_DELETION_SENTINEL:
|
75
|
+
return True
|
76
|
+
return self._prune and value in [None, "", {}, []]
|
77
|
+
def isempty_array_element(value: Any) -> bool: # noqa
|
78
|
+
return value == RowReader.CELL_DELETION_SENTINEL
|
79
|
+
data = copy.deepcopy(data)
|
80
|
+
remove_empty_properties(data, isempty=isempty, isempty_array_element=isempty_array_element)
|
81
|
+
return data
|
73
82
|
if self._validated and not force:
|
74
83
|
return
|
75
84
|
self._validated = True
|
@@ -106,7 +115,11 @@ class StructuredDataSet:
|
|
106
115
|
|
107
116
|
@property
|
108
117
|
def resolved_refs(self) -> List[str]:
|
109
|
-
return list(self._resolved_refs)
|
118
|
+
return list([resolved_ref[0] for resolved_ref in self._resolved_refs])
|
119
|
+
|
120
|
+
@property
|
121
|
+
def resolved_refs_with_uuids(self) -> List[str]:
|
122
|
+
return list([{"path": resolved_ref[0], "uuid": resolved_ref[1]} for resolved_ref in self._resolved_refs])
|
110
123
|
|
111
124
|
@property
|
112
125
|
def upload_files(self) -> List[str]:
|
@@ -192,10 +205,10 @@ class StructuredDataSet:
|
|
192
205
|
def _add(self, type_name: str, data: Union[dict, List[dict]]) -> None:
|
193
206
|
if self._prune:
|
194
207
|
remove_empty_properties(data)
|
195
|
-
if type_name in self.
|
196
|
-
self.
|
208
|
+
if type_name in self._data:
|
209
|
+
self._data[type_name].extend([data] if isinstance(data, dict) else data)
|
197
210
|
else:
|
198
|
-
self.
|
211
|
+
self._data[type_name] = [data] if isinstance(data, dict) else data
|
199
212
|
|
200
213
|
def _add_properties(self, structured_row: dict, properties: dict, schema: Optional[dict] = None) -> None:
|
201
214
|
for name in properties:
|
@@ -356,17 +369,9 @@ class Schema:
|
|
356
369
|
def validate(self, data: dict) -> List[str]:
|
357
370
|
errors = []
|
358
371
|
for error in SchemaValidator(self.data, format_checker=SchemaValidator.FORMAT_CHECKER).iter_errors(data):
|
359
|
-
errors.append(error.message)
|
372
|
+
errors.append(f"Validation error at '{error.json_path}': {error.message}")
|
360
373
|
return errors
|
361
374
|
|
362
|
-
@property
|
363
|
-
def unresolved_refs(self) -> List[dict]:
|
364
|
-
return self._unresolved_refs
|
365
|
-
|
366
|
-
@property
|
367
|
-
def resolved_refs(self) -> List[str]:
|
368
|
-
return list(self._resolved_refs)
|
369
|
-
|
370
375
|
def get_typeinfo(self, column_name: str) -> Optional[dict]:
|
371
376
|
if isinstance(info := self._typeinfo.get(column_name), str):
|
372
377
|
info = self._typeinfo.get(info)
|
@@ -430,9 +435,14 @@ class Schema:
|
|
430
435
|
if not (resolved := portal.ref_exists(link_to, value)):
|
431
436
|
self._unresolved_refs.append({"src": src, "error": f"/{link_to}/{value}"})
|
432
437
|
elif len(resolved) > 1:
|
433
|
-
|
438
|
+
# TODO: Don't think we need this anymore; see TODO on Portal.ref_exists.
|
439
|
+
self._unresolved_refs.append({
|
440
|
+
"src": src,
|
441
|
+
"error": f"/{link_to}/{value}",
|
442
|
+
"types": [resolved_ref["type"] for resolved_ref in resolved]})
|
434
443
|
else:
|
435
|
-
|
444
|
+
# A resolved-ref set value is a tuple of the reference path and its uuid.
|
445
|
+
self._resolved_refs.add((f"/{link_to}/{value}", resolved[0].get("uuid")))
|
436
446
|
return value
|
437
447
|
return lambda value, src: map_ref(value, typeinfo.get("linkTo"), self._portal, src)
|
438
448
|
|
@@ -624,8 +634,9 @@ class Portal(PortalBase):
|
|
624
634
|
|
625
635
|
def ref_exists(self, type_name: str, value: str) -> List[str]:
|
626
636
|
resolved = []
|
627
|
-
|
628
|
-
|
637
|
+
is_resolved, resolved_uuid = self._ref_exists_single(type_name, value)
|
638
|
+
if is_resolved:
|
639
|
+
resolved.append({"type": type_name, "uuid": resolved_uuid})
|
629
640
|
# TODO: Added this return on 2024-01-14 (dmichaels).
|
630
641
|
# Why did I orginally check for multiple existing values?
|
631
642
|
# Why not just return right away if I find that the ref exists?
|
@@ -638,20 +649,23 @@ class Portal(PortalBase):
|
|
638
649
|
if (schemas_super_type_map := self.get_schemas_super_type_map()):
|
639
650
|
if (sub_type_names := schemas_super_type_map.get(type_name)):
|
640
651
|
for sub_type_name in sub_type_names:
|
641
|
-
|
642
|
-
|
652
|
+
is_resolved, resolved_uuid = self._ref_exists_single(sub_type_name, value)
|
653
|
+
if is_resolved:
|
654
|
+
resolved.append({"type": type_name, "uuid": resolved_uuid})
|
643
655
|
# TODO: Added this return on 2024-01-14 (dmichaels). See above TODO.
|
644
656
|
return resolved
|
645
657
|
return resolved
|
646
658
|
|
647
|
-
def _ref_exists_single(self, type_name: str, value: str) -> bool:
|
659
|
+
def _ref_exists_single(self, type_name: str, value: str) -> Tuple[bool, Optional[str]]:
|
648
660
|
if self._data and (items := self._data.get(type_name)) and (schema := self.get_schema(type_name)):
|
649
661
|
iproperties = set(schema.get("identifyingProperties", [])) | {"identifier", "uuid"}
|
650
662
|
for item in items:
|
651
663
|
if (ivalue := next((item[iproperty] for iproperty in iproperties if iproperty in item), None)):
|
652
664
|
if isinstance(ivalue, list) and value in ivalue or ivalue == value:
|
653
|
-
return True
|
654
|
-
|
665
|
+
return True, None
|
666
|
+
if (value := self.get_metadata(f"/{type_name}/{value}")) is None:
|
667
|
+
return False, None
|
668
|
+
return True, value.get("uuid")
|
655
669
|
|
656
670
|
@staticmethod
|
657
671
|
def create_for_testing(arg: Optional[Union[str, bool, List[dict], dict, Callable]] = None,
|
@@ -9,7 +9,7 @@ dcicutils/common.py,sha256=YE8Mt5-vaZWWz4uaChSVhqGFbFtW5QKtnIyOr4zG4vM,3955
|
|
9
9
|
dcicutils/contribution_scripts.py,sha256=0k5Gw1TumcD5SAcXVkDd6-yvuMEw-jUp5Kfb7FJH6XQ,2015
|
10
10
|
dcicutils/contribution_utils.py,sha256=vYLS1JUB3sKd24BUxZ29qUBqYeQBLK9cwo8x3k64uPg,25653
|
11
11
|
dcicutils/creds_utils.py,sha256=xrLekD49Ex0GOpL9n7LlJA4gvNcY7txTVFOSYD7LvEU,11113
|
12
|
-
dcicutils/data_readers.py,sha256=
|
12
|
+
dcicutils/data_readers.py,sha256=ooEoFkYDWJMPdvnpIb9-Wpacyx--XkB8_fwnbPYLJK4,6239
|
13
13
|
dcicutils/data_utils.py,sha256=k2OxOlsx7AJ6jF-YNlMyGus_JqSUBe4_n1s65Mv1gQQ,3098
|
14
14
|
dcicutils/deployment_utils.py,sha256=rcNUFMe_tsrG4CHEtgBe41cZx4Pk4JqISPsjrJRMoEs,68891
|
15
15
|
dcicutils/diff_utils.py,sha256=sQx-yz56DHAcQWOChYbAG3clXu7TbiZKlw-GggeveO0,8118
|
@@ -41,10 +41,10 @@ dcicutils/license_policies/park-lab-gpl-pipeline.jsonc,sha256=vLZkwm3Js-kjV44nug
|
|
41
41
|
dcicutils/license_policies/park-lab-pipeline.jsonc,sha256=9qlY0ASy3iUMQlr3gorVcXrSfRHnVGbLhkS427UaRy4,283
|
42
42
|
dcicutils/license_utils.py,sha256=d1cq6iwv5Ju-VjdoINi6q7CPNNL7Oz6rcJdLMY38RX0,46978
|
43
43
|
dcicutils/log_utils.py,sha256=7pWMc6vyrorUZQf-V-M3YC6zrPgNhuV_fzm9xqTPph0,10883
|
44
|
-
dcicutils/misc_utils.py,sha256=
|
44
|
+
dcicutils/misc_utils.py,sha256=9JqdVjHLkZUDTryngF3Dbu0m7XcbitbR7izWnxUSWc4,101953
|
45
45
|
dcicutils/obfuscation_utils.py,sha256=fo2jOmDRC6xWpYX49u80bVNisqRRoPskFNX3ymFAmjw,5963
|
46
46
|
dcicutils/opensearch_utils.py,sha256=V2exmFYW8Xl2_pGFixF4I2Cc549Opwe4PhFi5twC0M8,1017
|
47
|
-
dcicutils/portal_object_utils.py,sha256=
|
47
|
+
dcicutils/portal_object_utils.py,sha256=Aa61u0o9OyWR7M26dGceMW9MSgDEZLqrwAkwu8FD228,12445
|
48
48
|
dcicutils/portal_utils.py,sha256=8XVRCy882RrB8QT2gGvW36c4nT1RJCgUApt_wLP-EG8,26706
|
49
49
|
dcicutils/project_utils.py,sha256=qPdCaFmWUVBJw4rw342iUytwdQC0P-XKpK4mhyIulMM,31250
|
50
50
|
dcicutils/qa_checkers.py,sha256=cdXjeL0jCDFDLT8VR8Px78aS10hwNISOO5G_Zv2TZ6M,20534
|
@@ -59,15 +59,15 @@ dcicutils/secrets_utils.py,sha256=8dppXAsiHhJzI6NmOcvJV5ldvKkQZzh3Fl-cb8Wm7MI,19
|
|
59
59
|
dcicutils/sheet_utils.py,sha256=VlmzteONW5VF_Q4vo0yA5vesz1ViUah1MZ_yA1rwZ0M,33629
|
60
60
|
dcicutils/snapshot_utils.py,sha256=ymP7PXH6-yEiXAt75w0ldQFciGNqWBClNxC5gfX2FnY,22961
|
61
61
|
dcicutils/ssl_certificate_utils.py,sha256=F0ifz_wnRRN9dfrfsz7aCp4UDLgHEY8LaK7PjnNvrAQ,9707
|
62
|
-
dcicutils/structured_data.py,sha256=
|
62
|
+
dcicutils/structured_data.py,sha256=voFT8RqVXLX6-sZiEfhsgmwXEiMcJUrHneYUDXNm9wk,35647
|
63
63
|
dcicutils/task_utils.py,sha256=MF8ujmTD6-O2AC2gRGPHyGdUrVKgtr8epT5XU8WtNjk,8082
|
64
64
|
dcicutils/tmpfile_utils.py,sha256=n95XF8dZVbQRSXBZTGToXXfSs3JUVRyN6c3ZZ0nhAWI,1403
|
65
65
|
dcicutils/trace_utils.py,sha256=g8kwV4ebEy5kXW6oOrEAUsurBcCROvwtZqz9fczsGRE,1769
|
66
66
|
dcicutils/validation_utils.py,sha256=cMZIU2cY98FYtzK52z5WUYck7urH6JcqOuz9jkXpqzg,14797
|
67
67
|
dcicutils/variant_utils.py,sha256=2H9azNx3xAj-MySg-uZ2SFqbWs4kZvf61JnK6b-h4Qw,4343
|
68
68
|
dcicutils/zip_utils.py,sha256=rnjNv_k6L9jT2SjDSgVXp4BEJYLtz9XN6Cl2Fy-tqnM,2027
|
69
|
-
dcicutils-8.7.1.
|
70
|
-
dcicutils-8.7.1.
|
71
|
-
dcicutils-8.7.1.
|
72
|
-
dcicutils-8.7.1.
|
73
|
-
dcicutils-8.7.1.
|
69
|
+
dcicutils-8.7.1.1b6.dist-info/LICENSE.txt,sha256=qnwSmfnEWMl5l78VPDEzAmEbLVrRqQvfUQiHT0ehrOo,1102
|
70
|
+
dcicutils-8.7.1.1b6.dist-info/METADATA,sha256=YLm-JVegaODLOnyLLQgUzFTwUPRlK-9dn0eZP1ANmHY,3314
|
71
|
+
dcicutils-8.7.1.1b6.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
|
72
|
+
dcicutils-8.7.1.1b6.dist-info/entry_points.txt,sha256=8wbw5csMIgBXhkwfgsgJeuFcoUc0WsucUxmOyml2aoA,209
|
73
|
+
dcicutils-8.7.1.1b6.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|