dcicutils 8.9.0.1b5__tar.gz → 8.10.0.0b0__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/PKG-INFO +1 -1
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/command_utils.py +1 -69
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/misc_utils.py +10 -41
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/portal_object_utils.py +89 -24
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/portal_utils.py +37 -249
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/schema_utils.py +50 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/structured_data.py +20 -31
- dcicutils-8.10.0.0b0/dcicutils/submitr/ref_lookup_strategy.py +67 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/pyproject.toml +1 -1
- dcicutils-8.9.0.1b5/dcicutils/submitr/ref_lookup_strategy.py +0 -73
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/LICENSE.txt +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/README.rst +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/__init__.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/base.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/beanstalk_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/bundle_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/captured_output.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/cloudformation_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/codebuild_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/common.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/contribution_scripts.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/contribution_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/creds_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/data_readers.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/data_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/datetime_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/deployment_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/diff_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/docker_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/ecr_scripts.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/ecr_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/ecs_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/env_base.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/env_manager.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/env_scripts.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/env_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/env_utils_legacy.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/es_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/exceptions.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/ff_mocks.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/ff_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/file_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/function_cache_decorator.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/glacier_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/http_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/jh_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/kibana/dashboards.json +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/kibana/readme.md +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/lang_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_policies/c4-infrastructure.jsonc +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_policies/c4-python-infrastructure.jsonc +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_policies/park-lab-common-server.jsonc +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_policies/park-lab-common.jsonc +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_policies/park-lab-gpl-pipeline.jsonc +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_policies/park-lab-pipeline.jsonc +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/log_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/obfuscation_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/opensearch_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/progress_bar.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/project_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/qa_checkers.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/qa_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/redis_tools.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/redis_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/s3_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/scripts/publish_to_pypi.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/scripts/run_license_checker.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/scripts/view_portal_object.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/secrets_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/sheet_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/snapshot_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/ssl_certificate_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/submitr/progress_constants.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/task_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/tmpfile_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/trace_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/validation_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/variant_utils.py +0 -0
- {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/zip_utils.py +0 -0
@@ -1,4 +1,3 @@
|
|
1
|
-
from __future__ import annotations
|
2
1
|
import contextlib
|
3
2
|
import functools
|
4
3
|
import glob
|
@@ -8,7 +7,7 @@ import re
|
|
8
7
|
import requests
|
9
8
|
import subprocess
|
10
9
|
|
11
|
-
from typing import
|
10
|
+
from typing import Optional
|
12
11
|
from .exceptions import InvalidParameterError
|
13
12
|
from .lang_utils import there_are
|
14
13
|
from .misc_utils import INPUT, PRINT, environ_bool, print_error_message, decorator
|
@@ -385,70 +384,3 @@ def script_catch_errors():
|
|
385
384
|
message = str(e) # Note: We ignore the type, which isn't intended to be shown.
|
386
385
|
PRINT(message)
|
387
386
|
exit(1)
|
388
|
-
|
389
|
-
|
390
|
-
class Question:
|
391
|
-
"""
|
392
|
-
Supports asking the user (via stdin) a yes/no question, possibly repeatedly; and after
|
393
|
-
some maximum number times of the same answer in a row (consecutively), then asks them
|
394
|
-
if they want to automatically give that same answer to any/all subsequent questions.
|
395
|
-
Supports static/global list of such Question instances, hashed (only) by the question text.
|
396
|
-
"""
|
397
|
-
_static_instances = {}
|
398
|
-
|
399
|
-
@staticmethod
|
400
|
-
def instance(question: Optional[str] = None,
|
401
|
-
max: Optional[int] = None, printf: Optional[Callable] = None) -> Question:
|
402
|
-
question = question if isinstance(question, str) else ""
|
403
|
-
if not (instance := Question._static_instances.get(question)):
|
404
|
-
Question._static_instances[question] = (instance := Question(question, max=max, printf=printf))
|
405
|
-
return instance
|
406
|
-
|
407
|
-
@staticmethod
|
408
|
-
def yes(question: Optional[str] = None,
|
409
|
-
max: Optional[int] = None, printf: Optional[Callable] = None) -> bool:
|
410
|
-
return Question.instance(question, max=max, printf=printf).ask()
|
411
|
-
|
412
|
-
def __init__(self, question: Optional[str] = None,
|
413
|
-
max: Optional[int] = None, printf: Optional[Callable] = None) -> None:
|
414
|
-
self._question = question if isinstance(question, str) else ""
|
415
|
-
self._max = max if isinstance(max, int) and max > 0 else None
|
416
|
-
self._print = printf if callable(printf) else print
|
417
|
-
self._yes_consecutive_count = 0
|
418
|
-
self._no_consecutive_count = 0
|
419
|
-
self._yes_automatic = False
|
420
|
-
self._no_automatic = False
|
421
|
-
|
422
|
-
def ask(self, question: Optional[str] = None) -> bool:
|
423
|
-
|
424
|
-
def question_automatic(value: str) -> bool:
|
425
|
-
nonlocal self
|
426
|
-
RARROW = "▶"
|
427
|
-
LARROW = "◀"
|
428
|
-
if yes_or_no(f"{RARROW}{RARROW}{RARROW}"
|
429
|
-
f" Do you want to answer {value} to all such questions?"
|
430
|
-
f" {LARROW}{LARROW}{LARROW}"):
|
431
|
-
return True
|
432
|
-
self._yes_consecutive_count = 0
|
433
|
-
self._no_consecutive_count = 0
|
434
|
-
|
435
|
-
if self._yes_automatic:
|
436
|
-
return True
|
437
|
-
elif self._no_automatic:
|
438
|
-
return False
|
439
|
-
elif yes_or_no((question if isinstance(question, str) else "") or self._question or "Undefined question"):
|
440
|
-
self._yes_consecutive_count += 1
|
441
|
-
self._no_consecutive_count = 0
|
442
|
-
if (self._no_consecutive_count == 0) and self._max and (self._yes_consecutive_count >= self._max):
|
443
|
-
# Have reached the maximum number of consecutive YES answers; ask if YES to all subsequent.
|
444
|
-
if question_automatic("YES"):
|
445
|
-
self._yes_automatic = True
|
446
|
-
return True
|
447
|
-
else:
|
448
|
-
self._no_consecutive_count += 1
|
449
|
-
self._yes_consecutive_count = 0
|
450
|
-
if (self._yes_consecutive_count == 0) and self._max and (self._no_consecutive_count >= self._max):
|
451
|
-
# Have reached the maximum number of consecutive NO answers; ask if NO to all subsequent.
|
452
|
-
if question_automatic("NO"):
|
453
|
-
self._no_automatic = True
|
454
|
-
return False
|
@@ -4,7 +4,6 @@ This file contains functions that might be generally useful.
|
|
4
4
|
|
5
5
|
from collections import namedtuple
|
6
6
|
import appdirs
|
7
|
-
from copy import deepcopy
|
8
7
|
import contextlib
|
9
8
|
import datetime
|
10
9
|
import functools
|
@@ -2200,58 +2199,28 @@ def merge_key_value_dict_lists(x, y):
|
|
2200
2199
|
return [key_value_dict(k, v) for k, v in merged.items()]
|
2201
2200
|
|
2202
2201
|
|
2203
|
-
def merge_objects(target: Union[dict, List[Any]], source: Union[dict, List[Any]],
|
2204
|
-
full: bool = False, # deprecated
|
2205
|
-
expand_lists: Optional[bool] = None,
|
2206
|
-
primitive_lists: bool = False,
|
2207
|
-
copy: bool = False, _recursing: bool = False) -> Union[dict, List[Any]]:
|
2202
|
+
def merge_objects(target: Union[dict, List[Any]], source: Union[dict, List[Any]], full: bool = False) -> dict:
|
2208
2203
|
"""
|
2209
|
-
Merges the given source dictionary or list into the target dictionary or list
|
2210
|
-
|
2211
|
-
argument is True
|
2212
|
-
|
2213
|
-
If the expand_lists argument is True then any target lists longer than the
|
2214
|
-
source be will be filled out with the last element(s) of the source; the full
|
2215
|
-
argument (is deprecated and) is a synomym for this. The default is False.
|
2216
|
-
|
2217
|
-
If the primitive_lists argument is True then lists of primitives (i.e. lists in which
|
2218
|
-
NONE of its elements are dictionaries, lists, or tuples) will themselves be treated
|
2219
|
-
like primitives, meaning the whole of a source list will replace the corresponding
|
2220
|
-
target; otherwise they will be merged normally, meaning each element of a source list
|
2221
|
-
will be merged, recursively, into the corresponding target list. The default is False.
|
2204
|
+
Merges the given source dictionary or list into the target dictionary or list.
|
2205
|
+
This MAY well change the given target (dictionary or list) IN PLACE.
|
2206
|
+
The the full argument is True then any target lists longer than the
|
2207
|
+
source be will be filled out with the last element(s) of the source.
|
2222
2208
|
"""
|
2223
|
-
def is_primitive_list(value: Any) -> bool: # noqa
|
2224
|
-
if not isinstance(value, list):
|
2225
|
-
return False
|
2226
|
-
for item in value:
|
2227
|
-
if isinstance(item, (dict, list, tuple)):
|
2228
|
-
return False
|
2229
|
-
return True
|
2230
|
-
|
2231
2209
|
if target is None:
|
2232
2210
|
return source
|
2233
|
-
if expand_lists not in (True, False):
|
2234
|
-
expand_lists = full is True
|
2235
|
-
if (copy is True) and (_recursing is not True):
|
2236
|
-
target = deepcopy(target)
|
2237
2211
|
if isinstance(target, dict) and isinstance(source, dict) and source:
|
2238
2212
|
for key, value in source.items():
|
2239
|
-
|
2240
|
-
(key in target) and is_primitive_list(target[key]) and is_primitive_list(value)): # noqa
|
2241
|
-
target[key] = value
|
2242
|
-
else:
|
2243
|
-
target[key] = merge_objects(target[key], value,
|
2244
|
-
expand_lists=expand_lists, _recursing=True) if key in target else value
|
2213
|
+
target[key] = merge_objects(target[key], value, full) if key in target else value
|
2245
2214
|
elif isinstance(target, list) and isinstance(source, list) and source:
|
2246
2215
|
for i in range(max(len(source), len(target))):
|
2247
2216
|
if i < len(target):
|
2248
2217
|
if i < len(source):
|
2249
|
-
target[i] = merge_objects(target[i], source[i],
|
2250
|
-
elif
|
2251
|
-
target[i] = merge_objects(target[i], source[len(source) - 1],
|
2218
|
+
target[i] = merge_objects(target[i], source[i], full)
|
2219
|
+
elif full:
|
2220
|
+
target[i] = merge_objects(target[i], source[len(source) - 1], full)
|
2252
2221
|
else:
|
2253
2222
|
target.append(source[i])
|
2254
|
-
elif source
|
2223
|
+
elif source:
|
2255
2224
|
target = source
|
2256
2225
|
return target
|
2257
2226
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
from copy import deepcopy
|
2
2
|
from functools import lru_cache
|
3
|
+
import re
|
3
4
|
from typing import Any, Callable, List, Optional, Tuple, Type, Union
|
4
5
|
from dcicutils.data_readers import RowReader
|
5
6
|
from dcicutils.misc_utils import create_readonly_object
|
@@ -13,9 +14,11 @@ class PortalObject:
|
|
13
14
|
|
14
15
|
_PROPERTY_DELETION_SENTINEL = RowReader.CELL_DELETION_SENTINEL
|
15
16
|
|
16
|
-
def __init__(self, data: dict, portal:
|
17
|
+
def __init__(self, data: dict, portal: Portal = None,
|
18
|
+
schema: Optional[Union[dict, Schema]] = None, type: Optional[str] = None) -> None:
|
17
19
|
self._data = data if isinstance(data, dict) else {}
|
18
20
|
self._portal = portal if isinstance(portal, Portal) else None
|
21
|
+
self._schema = schema if isinstance(schema, dict) else (schema.data if isinstance(schema, Schema) else None)
|
19
22
|
self._type = type if isinstance(type, str) else ""
|
20
23
|
|
21
24
|
@property
|
@@ -29,7 +32,7 @@ class PortalObject:
|
|
29
32
|
@property
|
30
33
|
@lru_cache(maxsize=1)
|
31
34
|
def type(self) -> str:
|
32
|
-
return self._type or Portal.get_schema_type(self._data) or ""
|
35
|
+
return self._type or Portal.get_schema_type(self._data) or (Schema(self._schema).type if self._schema else "")
|
33
36
|
|
34
37
|
@property
|
35
38
|
@lru_cache(maxsize=1)
|
@@ -44,7 +47,7 @@ class PortalObject:
|
|
44
47
|
@property
|
45
48
|
@lru_cache(maxsize=1)
|
46
49
|
def schema(self) -> Optional[dict]:
|
47
|
-
return self._portal.get_schema(self.type) if self._portal else None
|
50
|
+
return self._schema if self._schema else (self._portal.get_schema(self.type) if self._portal else None)
|
48
51
|
|
49
52
|
def copy(self) -> PortalObject:
|
50
53
|
return PortalObject(deepcopy(self.data), portal=self.portal, type=self.type)
|
@@ -56,29 +59,39 @@ class PortalObject:
|
|
56
59
|
Returns the list of all identifying property names of this Portal object which actually have values.
|
57
60
|
Implicitly include "uuid" and "identifier" properties as identifying properties if they are actually
|
58
61
|
properties in the object schema, and favor these (first); defavor "aliases"; no other ordering defined.
|
59
|
-
Changed (2024-05-26) to use portal_utils.get_identifying_property_names; migrating some intricate stuff there.
|
60
62
|
"""
|
61
|
-
|
62
|
-
|
63
|
+
if not (schema := self.schema) or not (schema_identifying_properties := schema.get("identifyingProperties")):
|
64
|
+
return None
|
65
|
+
identifying_properties = []
|
66
|
+
for identifying_property in schema_identifying_properties:
|
67
|
+
if identifying_property not in ["uuid", "identifier", "aliases"]:
|
68
|
+
if self._data.get(identifying_property):
|
69
|
+
identifying_properties.append(identifying_property)
|
70
|
+
if self._data.get("identifier"):
|
71
|
+
identifying_properties.insert(0, "identifier")
|
72
|
+
if self._data.get("uuid"):
|
73
|
+
identifying_properties.insert(0, "uuid")
|
74
|
+
if "aliases" in schema_identifying_properties and self._data.get("aliases"):
|
75
|
+
identifying_properties.append("aliases")
|
76
|
+
return identifying_properties or None
|
63
77
|
|
64
78
|
@lru_cache(maxsize=8192)
|
65
79
|
def lookup(self, raw: bool = False,
|
66
80
|
ref_lookup_strategy: Optional[Callable] = None) -> Tuple[Optional[PortalObject], Optional[str], int]:
|
67
|
-
if not (identifying_paths := self._get_identifying_paths(ref_lookup_strategy=ref_lookup_strategy)):
|
68
|
-
return None, None, 0
|
69
81
|
nlookups = 0
|
70
82
|
first_identifying_path = None
|
71
83
|
try:
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
84
|
+
if identifying_paths := self._get_identifying_paths(ref_lookup_strategy=ref_lookup_strategy):
|
85
|
+
for identifying_path in identifying_paths:
|
86
|
+
if not first_identifying_path:
|
87
|
+
first_identifying_path = identifying_path
|
88
|
+
nlookups += 1
|
89
|
+
if (value := self._portal.get(identifying_path, raw=raw)) and (value.status_code == 200):
|
90
|
+
return (
|
91
|
+
PortalObject(value.json(), portal=self._portal, type=self.type if raw else None),
|
92
|
+
identifying_path,
|
93
|
+
nlookups
|
94
|
+
)
|
82
95
|
except Exception:
|
83
96
|
pass
|
84
97
|
return None, first_identifying_path, nlookups
|
@@ -146,12 +159,64 @@ class PortalObject:
|
|
146
159
|
|
147
160
|
@lru_cache(maxsize=1)
|
148
161
|
def _get_identifying_paths(self, ref_lookup_strategy: Optional[Callable] = None) -> Optional[List[str]]:
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
162
|
+
"""
|
163
|
+
Returns a list of the possible Portal URL paths identifying this Portal object.
|
164
|
+
"""
|
165
|
+
identifying_paths = []
|
166
|
+
if not (identifying_properties := self.identifying_properties):
|
167
|
+
if self.uuid:
|
168
|
+
if self.type:
|
169
|
+
identifying_paths.append(f"/{self.type}/{self.uuid}")
|
170
|
+
identifying_paths.append(f"/{self.uuid}")
|
171
|
+
return identifying_paths
|
172
|
+
for identifying_property in identifying_properties:
|
173
|
+
if identifying_value := self._data.get(identifying_property):
|
174
|
+
if identifying_property == "uuid":
|
175
|
+
if self.type:
|
176
|
+
identifying_paths.append(f"/{self.type}/{identifying_value}")
|
177
|
+
identifying_paths.append(f"/{identifying_value}")
|
178
|
+
# For now at least we include the path both with and without the schema type component,
|
179
|
+
# as for some identifying values, it works (only) with, and some, it works (only) without.
|
180
|
+
# For example: If we have FileSet with "accession", an identifying property, with value
|
181
|
+
# SMAFSFXF1RO4 then /SMAFSFXF1RO4 works but /FileSet/SMAFSFXF1RO4 does not; and
|
182
|
+
# conversely using "submitted_id", also an identifying property, with value
|
183
|
+
# UW_FILE-SET_COLO-829BL_HI-C_1 then /UW_FILE-SET_COLO-829BL_HI-C_1 does
|
184
|
+
# not work but /FileSet/UW_FILE-SET_COLO-829BL_HI-C_1 does work.
|
185
|
+
elif isinstance(identifying_value, list):
|
186
|
+
for identifying_value_item in identifying_value:
|
187
|
+
if self.type:
|
188
|
+
identifying_paths.append(f"/{self.type}/{identifying_value_item}")
|
189
|
+
identifying_paths.append(f"/{identifying_value_item}")
|
190
|
+
else:
|
191
|
+
# TODO: Import from somewhere ...
|
192
|
+
lookup_options = 0
|
193
|
+
if schema := self.schema:
|
194
|
+
# TODO: Hook into the ref_lookup_strategy thing in structured_data to make
|
195
|
+
# sure we check accession format (since it does not have a pattern).
|
196
|
+
if callable(ref_lookup_strategy):
|
197
|
+
lookup_options, ref_validator = ref_lookup_strategy(
|
198
|
+
self._portal, self.type, schema, identifying_value)
|
199
|
+
if callable(ref_validator):
|
200
|
+
if ref_validator(schema, identifying_property, identifying_value) is False:
|
201
|
+
continue
|
202
|
+
if pattern := schema.get("properties", {}).get(identifying_property, {}).get("pattern"):
|
203
|
+
if not re.match(pattern, identifying_value):
|
204
|
+
# If this identifying value is for a (identifying) property which has a
|
205
|
+
# pattern, and the value does NOT match the pattern, then do NOT include
|
206
|
+
# this value as an identifying path, since it cannot possibly be found.
|
207
|
+
continue
|
208
|
+
if not lookup_options:
|
209
|
+
lookup_options = Portal.LOOKUP_DEFAULT
|
210
|
+
if Portal.is_lookup_root_first(lookup_options):
|
211
|
+
identifying_paths.append(f"/{identifying_value}")
|
212
|
+
if Portal.is_lookup_specified_type(lookup_options) and self.type:
|
213
|
+
identifying_paths.append(f"/{self.type}/{identifying_value}")
|
214
|
+
if Portal.is_lookup_root(lookup_options) and not Portal.is_lookup_root_first(lookup_options):
|
215
|
+
identifying_paths.append(f"/{identifying_value}")
|
216
|
+
if Portal.is_lookup_subtypes(lookup_options):
|
217
|
+
for subtype_name in self._portal.get_schema_subtype_names(self.type):
|
218
|
+
identifying_paths.append(f"/{subtype_name}/{identifying_value}")
|
219
|
+
return identifying_paths or None
|
155
220
|
|
156
221
|
def _normalized_refs(self, refs: List[dict]) -> Tuple[PortalObject, int]:
|
157
222
|
"""
|