dcicutils 8.10.0.0b0__tar.gz → 8.10.0.1b2__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.10.0.0b0 → dcicutils-8.10.0.1b2}/PKG-INFO +1 -1
  2. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/command_utils.py +72 -3
  3. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/deployment_utils.py +2 -2
  4. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/ecr_scripts.py +4 -3
  5. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/env_scripts.py +5 -4
  6. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/license_policies/park-lab-common.jsonc +6 -0
  7. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/misc_utils.py +41 -10
  8. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/portal_object_utils.py +24 -89
  9. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/portal_utils.py +237 -36
  10. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/progress_bar.py +1 -1
  11. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/schema_utils.py +0 -50
  12. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/scripts/publish_to_pypi.py +2 -1
  13. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/scripts/view_portal_object.py +7 -7
  14. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/structured_data.py +31 -20
  15. dcicutils-8.10.0.1b2/dcicutils/submitr/ref_lookup_strategy.py +73 -0
  16. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/pyproject.toml +1 -1
  17. dcicutils-8.10.0.0b0/dcicutils/submitr/ref_lookup_strategy.py +0 -67
  18. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/LICENSE.txt +0 -0
  19. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/README.rst +0 -0
  20. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/__init__.py +0 -0
  21. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/base.py +0 -0
  22. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/beanstalk_utils.py +0 -0
  23. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/bundle_utils.py +0 -0
  24. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/captured_output.py +0 -0
  25. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/cloudformation_utils.py +0 -0
  26. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/codebuild_utils.py +0 -0
  27. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/common.py +0 -0
  28. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/contribution_scripts.py +0 -0
  29. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/contribution_utils.py +0 -0
  30. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/creds_utils.py +0 -0
  31. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/data_readers.py +0 -0
  32. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/data_utils.py +0 -0
  33. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/datetime_utils.py +0 -0
  34. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/diff_utils.py +0 -0
  35. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/docker_utils.py +0 -0
  36. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/ecr_utils.py +0 -0
  37. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/ecs_utils.py +0 -0
  38. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/env_base.py +0 -0
  39. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/env_manager.py +0 -0
  40. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/env_utils.py +0 -0
  41. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/env_utils_legacy.py +0 -0
  42. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/es_utils.py +0 -0
  43. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/exceptions.py +0 -0
  44. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/ff_mocks.py +0 -0
  45. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/ff_utils.py +0 -0
  46. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/file_utils.py +0 -0
  47. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/function_cache_decorator.py +0 -0
  48. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/glacier_utils.py +0 -0
  49. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/http_utils.py +0 -0
  50. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/jh_utils.py +0 -0
  51. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/kibana/dashboards.json +0 -0
  52. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/kibana/readme.md +0 -0
  53. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/lang_utils.py +0 -0
  54. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/license_policies/c4-infrastructure.jsonc +0 -0
  55. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/license_policies/c4-python-infrastructure.jsonc +0 -0
  56. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/license_policies/park-lab-common-server.jsonc +0 -0
  57. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/license_policies/park-lab-gpl-pipeline.jsonc +0 -0
  58. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/license_policies/park-lab-pipeline.jsonc +0 -0
  59. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/license_utils.py +0 -0
  60. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/log_utils.py +0 -0
  61. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/obfuscation_utils.py +0 -0
  62. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/opensearch_utils.py +0 -0
  63. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/project_utils.py +0 -0
  64. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/qa_checkers.py +0 -0
  65. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/qa_utils.py +0 -0
  66. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/redis_tools.py +0 -0
  67. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/redis_utils.py +0 -0
  68. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/s3_utils.py +0 -0
  69. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/scripts/run_license_checker.py +0 -0
  70. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/secrets_utils.py +0 -0
  71. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/sheet_utils.py +0 -0
  72. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/snapshot_utils.py +0 -0
  73. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/ssl_certificate_utils.py +0 -0
  74. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/submitr/progress_constants.py +0 -0
  75. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/task_utils.py +0 -0
  76. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/tmpfile_utils.py +0 -0
  77. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/trace_utils.py +0 -0
  78. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/validation_utils.py +0 -0
  79. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/variant_utils.py +0 -0
  80. {dcicutils-8.10.0.0b0 → dcicutils-8.10.0.1b2}/dcicutils/zip_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcicutils
3
- Version: 8.10.0.0b0
3
+ Version: 8.10.0.1b2
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,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  import contextlib
2
3
  import functools
3
4
  import glob
@@ -6,8 +7,9 @@ import os
6
7
  import re
7
8
  import requests
8
9
  import subprocess
10
+ import sys
9
11
 
10
- from typing import Optional
12
+ from typing import Callable, Optional
11
13
  from .exceptions import InvalidParameterError
12
14
  from .lang_utils import there_are
13
15
  from .misc_utils import INPUT, PRINT, environ_bool, print_error_message, decorator
@@ -371,7 +373,7 @@ def script_catch_errors():
371
373
  raise ScriptFailure(' '.join(message))
372
374
  try:
373
375
  yield fail
374
- exit(0)
376
+ sys.exit(0)
375
377
  except (Exception, ScriptFailure) as e:
376
378
  if DEBUG_SCRIPT:
377
379
  # If debugging, let the error propagate, do not trap it.
@@ -383,4 +385,71 @@ def script_catch_errors():
383
385
  else:
384
386
  message = str(e) # Note: We ignore the type, which isn't intended to be shown.
385
387
  PRINT(message)
386
- exit(1)
388
+ sys.exit(1)
389
+
390
+
391
+ class Question:
392
+ """
393
+ Supports asking the user (via stdin) a yes/no question, possibly repeatedly; and after
394
+ some maximum number times of the same answer in a row (consecutively), then asks them
395
+ if they want to automatically give that same answer to any/all subsequent questions.
396
+ Supports static/global list of such Question instances, hashed (only) by the question text.
397
+ """
398
+ _static_instances = {}
399
+
400
+ @staticmethod
401
+ def instance(question: Optional[str] = None,
402
+ max: Optional[int] = None, printf: Optional[Callable] = None) -> Question:
403
+ question = question if isinstance(question, str) else ""
404
+ if not (instance := Question._static_instances.get(question)):
405
+ Question._static_instances[question] = (instance := Question(question, max=max, printf=printf))
406
+ return instance
407
+
408
+ @staticmethod
409
+ def yes(question: Optional[str] = None,
410
+ max: Optional[int] = None, printf: Optional[Callable] = None) -> bool:
411
+ return Question.instance(question, max=max, printf=printf).ask()
412
+
413
+ def __init__(self, question: Optional[str] = None,
414
+ max: Optional[int] = None, printf: Optional[Callable] = None) -> None:
415
+ self._question = question if isinstance(question, str) else ""
416
+ self._max = max if isinstance(max, int) and max > 0 else None
417
+ self._print = printf if callable(printf) else print
418
+ self._yes_consecutive_count = 0
419
+ self._no_consecutive_count = 0
420
+ self._yes_automatic = False
421
+ self._no_automatic = False
422
+
423
+ def ask(self, question: Optional[str] = None) -> bool:
424
+
425
+ def question_automatic(value: str) -> bool:
426
+ nonlocal self
427
+ RARROW = "▶"
428
+ LARROW = "◀"
429
+ if yes_or_no(f"{RARROW}{RARROW}{RARROW}"
430
+ f" Do you want to answer {value} to all such questions?"
431
+ f" {LARROW}{LARROW}{LARROW}"):
432
+ return True
433
+ self._yes_consecutive_count = 0
434
+ self._no_consecutive_count = 0
435
+
436
+ if self._yes_automatic:
437
+ return True
438
+ elif self._no_automatic:
439
+ return False
440
+ elif yes_or_no((question if isinstance(question, str) else "") or self._question or "Undefined question"):
441
+ self._yes_consecutive_count += 1
442
+ self._no_consecutive_count = 0
443
+ if (self._no_consecutive_count == 0) and self._max and (self._yes_consecutive_count >= self._max):
444
+ # Have reached the maximum number of consecutive YES answers; ask if YES to all subsequent.
445
+ if question_automatic("YES"):
446
+ self._yes_automatic = True
447
+ return True
448
+ else:
449
+ self._no_consecutive_count += 1
450
+ self._yes_consecutive_count = 0
451
+ if (self._yes_consecutive_count == 0) and self._max and (self._no_consecutive_count >= self._max):
452
+ # Have reached the maximum number of consecutive NO answers; ask if NO to all subsequent.
453
+ if question_automatic("NO"):
454
+ self._no_automatic = True
455
+ return False
@@ -396,9 +396,9 @@ class EBDeployer:
396
396
  packaging_was_successful = cls.build_application_version(args.repo, args.version_name, branch=args.branch)
397
397
  if packaging_was_successful: # XXX: how to best detect?
398
398
  time.sleep(5) # give EB a second to catch up (it needs it)
399
- exit(cls.deploy_new_version(args.env, args.repo, args.version_name))
399
+ sys.exit(cls.deploy_new_version(args.env, args.repo, args.version_name))
400
400
  else:
401
- exit(cls.deploy_indexer(args.env, args.application_version))
401
+ sys.exit(cls.deploy_indexer(args.env, args.application_version))
402
402
 
403
403
 
404
404
  class IniFileManager:
@@ -3,6 +3,7 @@ import botocore.exceptions
3
3
  import boto3
4
4
  import contextlib
5
5
  import os
6
+ import sys
6
7
 
7
8
  from typing import Optional, Union, List
8
9
  from .command_utils import yes_or_no
@@ -68,7 +69,7 @@ def ecr_command_context(account_number, ecs_repository=None, ecr_client=None):
68
69
  elif account_number != account_number_in_environ:
69
70
  raise RuntimeError("The account number you have specified does not match your declared credentials.")
70
71
  yield ECRCommandContext(account_number=account_number, ecs_repository=ecs_repository, ecr_client=ecr_client)
71
- exit(0)
72
+ sys.exit(0)
72
73
  except botocore.exceptions.ClientError as e:
73
74
  error_info = e.response.get('Error', {})
74
75
  message = error_info.get('Message')
@@ -77,12 +78,12 @@ def ecr_command_context(account_number, ecs_repository=None, ecr_client=None):
77
78
  raise RuntimeError("Your security token seems to have expired.")
78
79
  elif message:
79
80
  PRINT(f"{code}: {message}")
80
- exit(1)
81
+ sys.exit(1)
81
82
  else:
82
83
  raise
83
84
  except Exception as e:
84
85
  PRINT(f"{full_class_name(e)}: {e}")
85
- exit(1)
86
+ sys.exit(1)
86
87
 
87
88
 
88
89
  class ECRCommandContext:
@@ -1,6 +1,7 @@
1
1
  import argparse
2
2
  import boto3
3
3
  import json
4
+ import sys
4
5
  import yaml
5
6
 
6
7
  from .lang_utils import disjoined_list
@@ -52,7 +53,7 @@ def show_global_env_bucket(bucket, mode='json', key=None):
52
53
  else:
53
54
  PRINT(f"There is no default bucket. Please use a '--bucket' argument"
54
55
  f" to specify one of {disjoined_list(envs_buckets)}.")
55
- exit(1)
56
+ sys.exit(1)
56
57
 
57
58
  print_heading(bucket, style='=')
58
59
 
@@ -70,7 +71,7 @@ def show_global_env_bucket(bucket, mode='json', key=None):
70
71
  except Exception as e:
71
72
  PRINT("Bucket contents could not be downlaoded.")
72
73
  print_error_message(e)
73
- exit(1)
74
+ sys.exit(1)
74
75
 
75
76
  object = None # Without this, PyCharm fusses that object might not get set. -kmp 20-Jul-2022
76
77
  try:
@@ -78,7 +79,7 @@ def show_global_env_bucket(bucket, mode='json', key=None):
78
79
  except Exception as e:
79
80
  PRINT("Bucket contents could not be parsed as JSON.")
80
81
  print_error_message(e)
81
- exit(1)
82
+ sys.exit(1)
82
83
 
83
84
  if mode == 'json':
84
85
  PRINT(json.dumps(object, indent=2, default=str))
@@ -86,7 +87,7 @@ def show_global_env_bucket(bucket, mode='json', key=None):
86
87
  PRINT(yaml.dump(object))
87
88
  else:
88
89
  PRINT(f"Unknown mode: {mode}. Try 'json' or 'yaml'.")
89
- exit(1)
90
+ sys.exit(1)
90
91
 
91
92
 
92
93
  DEFAULT_BUCKET = get_env_bucket()
@@ -248,6 +248,12 @@
248
248
  "docutils" // Used only privately as a separate documentation-generation task for ReadTheDocs
249
249
  ],
250
250
 
251
+
252
+ "GNU General Public License v2 (GPLv2)": [
253
+ "pyinstaller",
254
+ "pyinstaller-hooks-contrib"
255
+ ],
256
+
251
257
  "MIT/X11 Derivative": [
252
258
  // The license used by libxkbcommon is complicated and involves numerous included licenses,
253
259
  // but all are permissive.
@@ -4,6 +4,7 @@ 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
7
8
  import contextlib
8
9
  import datetime
9
10
  import functools
@@ -2199,28 +2200,58 @@ def merge_key_value_dict_lists(x, y):
2199
2200
  return [key_value_dict(k, v) for k, v in merged.items()]
2200
2201
 
2201
2202
 
2202
- def merge_objects(target: Union[dict, List[Any]], source: Union[dict, List[Any]], full: bool = False) -> dict:
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]]:
2203
2208
  """
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.
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.
2208
2222
  """
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
+
2209
2231
  if target is None:
2210
2232
  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)
2211
2237
  if isinstance(target, dict) and isinstance(source, dict) and source:
2212
2238
  for key, value in source.items():
2213
- target[key] = merge_objects(target[key], value, full) if key in target else value
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
2214
2245
  elif isinstance(target, list) and isinstance(source, list) and source:
2215
2246
  for i in range(max(len(source), len(target))):
2216
2247
  if i < len(target):
2217
2248
  if i < len(source):
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)
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)
2221
2252
  else:
2222
2253
  target.append(source[i])
2223
- elif source:
2254
+ elif source not in (None, {}, []):
2224
2255
  target = source
2225
2256
  return target
2226
2257
 
@@ -1,6 +1,5 @@
1
1
  from copy import deepcopy
2
2
  from functools import lru_cache
3
- import re
4
3
  from typing import Any, Callable, List, Optional, Tuple, Type, Union
5
4
  from dcicutils.data_readers import RowReader
6
5
  from dcicutils.misc_utils import create_readonly_object
@@ -14,11 +13,9 @@ class PortalObject:
14
13
 
15
14
  _PROPERTY_DELETION_SENTINEL = RowReader.CELL_DELETION_SENTINEL
16
15
 
17
- def __init__(self, data: dict, portal: Portal = None,
18
- schema: Optional[Union[dict, Schema]] = None, type: Optional[str] = None) -> None:
16
+ def __init__(self, data: dict, portal: Optional[Portal] = None, type: Optional[str] = None) -> None:
19
17
  self._data = data if isinstance(data, dict) else {}
20
18
  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)
22
19
  self._type = type if isinstance(type, str) else ""
23
20
 
24
21
  @property
@@ -32,7 +29,7 @@ class PortalObject:
32
29
  @property
33
30
  @lru_cache(maxsize=1)
34
31
  def type(self) -> str:
35
- return self._type or Portal.get_schema_type(self._data) or (Schema(self._schema).type if self._schema else "")
32
+ return self._type or Portal.get_schema_type(self._data) or ""
36
33
 
37
34
  @property
38
35
  @lru_cache(maxsize=1)
@@ -47,7 +44,7 @@ class PortalObject:
47
44
  @property
48
45
  @lru_cache(maxsize=1)
49
46
  def schema(self) -> Optional[dict]:
50
- return self._schema if self._schema else (self._portal.get_schema(self.type) if self._portal else None)
47
+ return self._portal.get_schema(self.type) if self._portal else None
51
48
 
52
49
  def copy(self) -> PortalObject:
53
50
  return PortalObject(deepcopy(self.data), portal=self.portal, type=self.type)
@@ -59,39 +56,29 @@ class PortalObject:
59
56
  Returns the list of all identifying property names of this Portal object which actually have values.
60
57
  Implicitly include "uuid" and "identifier" properties as identifying properties if they are actually
61
58
  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.
62
60
  """
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
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 []
77
63
 
78
64
  @lru_cache(maxsize=8192)
79
65
  def lookup(self, raw: bool = False,
80
66
  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
81
69
  nlookups = 0
82
70
  first_identifying_path = None
83
71
  try:
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
- )
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
+ )
95
82
  except Exception:
96
83
  pass
97
84
  return None, first_identifying_path, nlookups
@@ -159,64 +146,12 @@ class PortalObject:
159
146
 
160
147
  @lru_cache(maxsize=1)
161
148
  def _get_identifying_paths(self, ref_lookup_strategy: Optional[Callable] = None) -> Optional[List[str]]:
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
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
220
155
 
221
156
  def _normalized_refs(self, refs: List[dict]) -> Tuple[PortalObject, int]:
222
157
  """