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.
Files changed (80) hide show
  1. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/PKG-INFO +1 -1
  2. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/command_utils.py +1 -69
  3. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/misc_utils.py +10 -41
  4. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/portal_object_utils.py +89 -24
  5. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/portal_utils.py +37 -249
  6. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/schema_utils.py +50 -0
  7. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/structured_data.py +20 -31
  8. dcicutils-8.10.0.0b0/dcicutils/submitr/ref_lookup_strategy.py +67 -0
  9. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/pyproject.toml +1 -1
  10. dcicutils-8.9.0.1b5/dcicutils/submitr/ref_lookup_strategy.py +0 -73
  11. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/LICENSE.txt +0 -0
  12. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/README.rst +0 -0
  13. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/__init__.py +0 -0
  14. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/base.py +0 -0
  15. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/beanstalk_utils.py +0 -0
  16. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/bundle_utils.py +0 -0
  17. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/captured_output.py +0 -0
  18. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/cloudformation_utils.py +0 -0
  19. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/codebuild_utils.py +0 -0
  20. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/common.py +0 -0
  21. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/contribution_scripts.py +0 -0
  22. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/contribution_utils.py +0 -0
  23. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/creds_utils.py +0 -0
  24. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/data_readers.py +0 -0
  25. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/data_utils.py +0 -0
  26. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/datetime_utils.py +0 -0
  27. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/deployment_utils.py +0 -0
  28. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/diff_utils.py +0 -0
  29. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/docker_utils.py +0 -0
  30. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/ecr_scripts.py +0 -0
  31. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/ecr_utils.py +0 -0
  32. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/ecs_utils.py +0 -0
  33. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/env_base.py +0 -0
  34. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/env_manager.py +0 -0
  35. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/env_scripts.py +0 -0
  36. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/env_utils.py +0 -0
  37. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/env_utils_legacy.py +0 -0
  38. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/es_utils.py +0 -0
  39. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/exceptions.py +0 -0
  40. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/ff_mocks.py +0 -0
  41. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/ff_utils.py +0 -0
  42. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/file_utils.py +0 -0
  43. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/function_cache_decorator.py +0 -0
  44. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/glacier_utils.py +0 -0
  45. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/http_utils.py +0 -0
  46. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/jh_utils.py +0 -0
  47. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/kibana/dashboards.json +0 -0
  48. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/kibana/readme.md +0 -0
  49. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/lang_utils.py +0 -0
  50. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_policies/c4-infrastructure.jsonc +0 -0
  51. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_policies/c4-python-infrastructure.jsonc +0 -0
  52. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_policies/park-lab-common-server.jsonc +0 -0
  53. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_policies/park-lab-common.jsonc +0 -0
  54. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_policies/park-lab-gpl-pipeline.jsonc +0 -0
  55. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_policies/park-lab-pipeline.jsonc +0 -0
  56. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/license_utils.py +0 -0
  57. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/log_utils.py +0 -0
  58. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/obfuscation_utils.py +0 -0
  59. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/opensearch_utils.py +0 -0
  60. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/progress_bar.py +0 -0
  61. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/project_utils.py +0 -0
  62. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/qa_checkers.py +0 -0
  63. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/qa_utils.py +0 -0
  64. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/redis_tools.py +0 -0
  65. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/redis_utils.py +0 -0
  66. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/s3_utils.py +0 -0
  67. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/scripts/publish_to_pypi.py +0 -0
  68. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/scripts/run_license_checker.py +0 -0
  69. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/scripts/view_portal_object.py +0 -0
  70. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/secrets_utils.py +0 -0
  71. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/sheet_utils.py +0 -0
  72. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/snapshot_utils.py +0 -0
  73. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/ssl_certificate_utils.py +0 -0
  74. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/submitr/progress_constants.py +0 -0
  75. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/task_utils.py +0 -0
  76. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/tmpfile_utils.py +0 -0
  77. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/trace_utils.py +0 -0
  78. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/validation_utils.py +0 -0
  79. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/variant_utils.py +0 -0
  80. {dcicutils-8.9.0.1b5 → dcicutils-8.10.0.0b0}/dcicutils/zip_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcicutils
3
- Version: 8.9.0.1b5
3
+ Version: 8.10.0.0b0
4
4
  Summary: Utility package for interacting with the 4DN Data Portal and other 4DN resources
5
5
  Home-page: https://github.com/4dn-dcic/utils
6
6
  License: MIT
@@ -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 Callable, Optional
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 and returns the
2210
- result. This MAY well change the given target (dictionary or list) IN PLACE ... UNLESS the copy
2211
- argument is True, then the given target will not change as a local copy is made (and returned).
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
- if ((primitive_lists is True) and
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], expand_lists=expand_lists, _recursing=True)
2250
- elif expand_lists is True:
2251
- target[i] = merge_objects(target[i], source[len(source) - 1], expand_lists=expand_lists)
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 not in (None, {}, []):
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: Optional[Portal] = None, type: Optional[str] = None) -> None:
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
- # Migrating to and unifying this in portal_utils.Portal.get_identifying_paths (2024-05-26).
62
- return self._portal.get_identifying_property_names(self.type, portal_object=self._data) if self._portal else []
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
- for identifying_path in identifying_paths:
73
- if not first_identifying_path:
74
- first_identifying_path = identifying_path
75
- nlookups += 1
76
- if self._portal and (item := self._portal.get(identifying_path, raw=raw)) and (item.status_code == 200):
77
- return (
78
- PortalObject(item.json(), portal=self._portal, type=self.type if raw else None),
79
- identifying_path,
80
- nlookups
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
- if not self._portal and (uuid := self.uuid):
150
- return [f"/{uuid}"]
151
- # Migrating to and unifying this in portal_utils.Portal.get_identifying_paths (2024-05-26).
152
- return self._portal.get_identifying_paths(self._data,
153
- portal_type=self.schema,
154
- lookup_strategy=ref_lookup_strategy) if self._portal else None
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
  """