dcicutils 8.7.0.1b33__tar.gz → 8.7.0.1b35__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/PKG-INFO +1 -1
  2. dcicutils-8.7.0.1b35/dcicutils/portal_object_utils.py +165 -0
  3. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/schema_utils.py +79 -1
  4. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/structured_data.py +13 -5
  5. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/pyproject.toml +1 -1
  6. dcicutils-8.7.0.1b33/dcicutils/portal_object_utils.py +0 -119
  7. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/LICENSE.txt +0 -0
  8. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/README.rst +0 -0
  9. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/__init__.py +0 -0
  10. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/base.py +0 -0
  11. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/beanstalk_utils.py +0 -0
  12. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/bundle_utils.py +0 -0
  13. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/cloudformation_utils.py +0 -0
  14. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/codebuild_utils.py +0 -0
  15. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/command_utils.py +0 -0
  16. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/common.py +0 -0
  17. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/contribution_scripts.py +0 -0
  18. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/contribution_utils.py +0 -0
  19. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/creds_utils.py +0 -0
  20. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/data_readers.py +0 -0
  21. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/data_utils.py +0 -0
  22. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/deployment_utils.py +0 -0
  23. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/diff_utils.py +0 -0
  24. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/docker_utils.py +0 -0
  25. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/ecr_scripts.py +0 -0
  26. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/ecr_utils.py +0 -0
  27. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/ecs_utils.py +0 -0
  28. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/env_base.py +0 -0
  29. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/env_manager.py +0 -0
  30. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/env_scripts.py +0 -0
  31. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/env_utils.py +0 -0
  32. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/env_utils_legacy.py +0 -0
  33. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/es_utils.py +0 -0
  34. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/exceptions.py +0 -0
  35. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/ff_mocks.py +0 -0
  36. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/ff_utils.py +0 -0
  37. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/file_utils.py +0 -0
  38. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/function_cache_decorator.py +0 -0
  39. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/glacier_utils.py +0 -0
  40. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/jh_utils.py +0 -0
  41. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/kibana/dashboards.json +0 -0
  42. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/kibana/readme.md +0 -0
  43. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/lang_utils.py +0 -0
  44. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/license_policies/c4-infrastructure.jsonc +0 -0
  45. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/license_policies/c4-python-infrastructure.jsonc +0 -0
  46. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/license_policies/park-lab-common-server.jsonc +0 -0
  47. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/license_policies/park-lab-common.jsonc +0 -0
  48. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/license_policies/park-lab-gpl-pipeline.jsonc +0 -0
  49. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/license_policies/park-lab-pipeline.jsonc +0 -0
  50. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/license_utils.py +0 -0
  51. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/log_utils.py +0 -0
  52. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/misc_utils.py +0 -0
  53. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/obfuscation_utils.py +0 -0
  54. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/opensearch_utils.py +0 -0
  55. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/portal_utils.py +0 -0
  56. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/project_utils.py +0 -0
  57. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/qa_checkers.py +0 -0
  58. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/qa_utils.py +0 -0
  59. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/redis_tools.py +0 -0
  60. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/redis_utils.py +0 -0
  61. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/s3_utils.py +0 -0
  62. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/scripts/publish_to_pypi.py +0 -0
  63. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/scripts/run_license_checker.py +0 -0
  64. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/secrets_utils.py +0 -0
  65. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/sheet_utils.py +0 -0
  66. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/snapshot_utils.py +0 -0
  67. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/ssl_certificate_utils.py +0 -0
  68. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/task_utils.py +0 -0
  69. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/tmpfile_utils.py +0 -0
  70. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/trace_utils.py +0 -0
  71. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/validation_utils.py +0 -0
  72. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/variant_utils.py +0 -0
  73. {dcicutils-8.7.0.1b33 → dcicutils-8.7.0.1b35}/dcicutils/zip_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcicutils
3
- Version: 8.7.0.1b33
3
+ Version: 8.7.0.1b35
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
@@ -0,0 +1,165 @@
1
+ from functools import lru_cache
2
+ import re
3
+ from typing import Any, Callable, List, Optional, Tuple, Type, Union
4
+ from dcicutils.portal_utils import Portal
5
+ from dcicutils.schema_utils import Schema
6
+
7
+ PortalObject = Type["PortalObject"] # Forward type reference for type hints.
8
+
9
+
10
+ class PortalObject:
11
+
12
+ def __init__(self, portal: Portal, portal_object: dict, portal_object_type: Optional[str] = None) -> None:
13
+ self._portal = portal
14
+ self._data = portal_object
15
+ self._type = portal_object_type if isinstance(portal_object_type, str) and portal_object_type else None
16
+
17
+ @property
18
+ def data(self):
19
+ return self._data
20
+
21
+ @property
22
+ @lru_cache(maxsize=1)
23
+ def type(self):
24
+ return self._type or Portal.get_schema_type(self._data)
25
+
26
+ @property
27
+ @lru_cache(maxsize=1)
28
+ def types(self):
29
+ return self._type or Portal.get_schema_types(self._data)
30
+
31
+ @property
32
+ @lru_cache(maxsize=1)
33
+ def uuid(self) -> Optional[str]:
34
+ return self._data.get("uuid") if isinstance(self._data, dict) else None
35
+
36
+ @property
37
+ @lru_cache(maxsize=1)
38
+ def schema(self):
39
+ return self._portal.get_schema(self.type)
40
+
41
+ @property
42
+ @lru_cache(maxsize=1)
43
+ def identifying_properties(self) -> List[str]:
44
+ """
45
+ Returns the list of all identifying property names of this Portal object which actually have values.
46
+ Implicitly include "uuid" and "identifier" properties as identifying properties if they are actually
47
+ properties in the object schema, and favor these (first); defavor "aliases"; no other ordering defined.
48
+ """
49
+ if not (schema := self.schema) or not (schema_identifying_properties := schema.get("identifyingProperties")):
50
+ return []
51
+ identifying_properties = []
52
+ for identifying_property in schema_identifying_properties:
53
+ if identifying_property not in ["uuid", "identifier", "aliases"]:
54
+ if self._data.get(identifying_property):
55
+ identifying_properties.append(identifying_property)
56
+ if self._data.get("identifier"):
57
+ identifying_properties.insert(0, "identifier")
58
+ if self._data.get("uuid"):
59
+ identifying_properties.insert(0, "uuid")
60
+ if "aliases" in schema_identifying_properties and self._data.get("aliases"):
61
+ identifying_properties.append("aliases")
62
+ return identifying_properties
63
+
64
+ @property
65
+ @lru_cache(maxsize=1)
66
+ def identifying_paths(self) -> List[str]:
67
+ """
68
+ Returns a list of the possible Portal URL paths identifying this Portal object.
69
+ """
70
+ if not (identifying_properties := self.identifying_properties):
71
+ return []
72
+ identifying_paths = []
73
+ for identifying_property in identifying_properties:
74
+ if (identifying_value := self._data.get(identifying_property)):
75
+ if identifying_property == "uuid":
76
+ identifying_paths.append(f"/{identifying_value}")
77
+ # For now at least we include the path both with and without the schema type component,
78
+ # as for some identifying values, it works (only) with, and some, it works (only) without.
79
+ # For example: If we have FileSet with "accession", an identifying property, with value
80
+ # SMAFSFXF1RO4 then /SMAFSFXF1RO4 works but /FileSet/SMAFSFXF1RO4 does not; and
81
+ # conversely using "submitted_id", also an identifying property, with value
82
+ # UW_FILE-SET_COLO-829BL_HI-C_1 then /UW_FILE-SET_COLO-829BL_HI-C_1 does
83
+ # not work but /FileSet/UW_FILE-SET_COLO-829BL_HI-C_1 does work.
84
+ elif isinstance(identifying_value, list):
85
+ for identifying_value_item in identifying_value:
86
+ identifying_paths.append(f"/{self.type}/{identifying_value_item}")
87
+ identifying_paths.append(f"/{identifying_value_item}")
88
+ else:
89
+ identifying_paths.append(f"/{self.type}/{identifying_value}")
90
+ identifying_paths.append(f"/{identifying_value}")
91
+ return identifying_paths
92
+
93
+ @property
94
+ @lru_cache(maxsize=1)
95
+ def identifying_path(self) -> Optional[str]:
96
+ if identifying_paths := self.identifying_paths:
97
+ return identifying_paths[0]
98
+
99
+ def lookup(self, include_identifying_path: bool = False,
100
+ raw: bool = False) -> Optional[Union[Tuple[PortalObject, str], PortalObject]]:
101
+ return self._lookup(raw=raw) if include_identifying_path else self._lookup(raw=raw)[0]
102
+
103
+ def lookup_identifying_path(self) -> Optional[str]:
104
+ return self._lookup()[1]
105
+
106
+ def _lookup(self, raw: bool = False) -> Tuple[Optional[PortalObject], Optional[str]]:
107
+ try:
108
+ for identifying_path in self.identifying_paths:
109
+ if (value := self._portal.get(identifying_path, raw=raw)) and (value.status_code == 200):
110
+ return PortalObject(self._portal, value.json(), self.type if raw else None), identifying_path
111
+ except Exception:
112
+ pass
113
+ return None, self.identifying_path
114
+
115
+ def compare(self, value: Union[dict, PortalObject], consider_link_to: bool = False) -> dict:
116
+ """
117
+ Compares this Portal object against the given Portal object value; noting differences values of properites
118
+ which they have in common; and properties which are in this Portal object and not in the given Portal object;
119
+ we do NOT check the converse, i.e. properties in the given Portal object which are not in this Portal object.
120
+ Returns a dictionary with a description of the differences.
121
+ """
122
+ def are_properties_equal(property_path: str, property_value_a: Any, property_value_b: Any) -> bool:
123
+ if property_value_a == property_value_b:
124
+ return True
125
+ nonlocal self
126
+ if (schema := self.schema) and (property_type := Schema.get_property_by_path(schema, property_path)):
127
+ if link_to := property_type.get("linkTo"):
128
+ if a := self._portal.get(f"/{link_to}/{property_value_a}", raw=True):
129
+ if (a.status_code == 200) and (a := a.json()):
130
+ if b := self._portal.get(f"/{link_to}/{property_value_b}", raw=True):
131
+ if (b.status_code == 200) and (b := b.json()):
132
+ return a == b
133
+ return False
134
+ return PortalObject._compare(self._data, value.data if isinstance(value, PortalObject) else value,
135
+ compare=are_properties_equal if consider_link_to else None)
136
+
137
+ _ARRAY_KEY_REGULAR_EXPRESSION = re.compile(rf"^({Schema._ARRAY_NAME_SUFFIX_CHAR}\d+)$")
138
+
139
+ @staticmethod
140
+ def _compare(a: dict, b: dict, compare: Optional[Callable] = None, _path: Optional[str] = None) -> dict:
141
+ def key_to_path(key: str) -> Optional[str]: # noqa
142
+ nonlocal _path
143
+ if match := PortalObject._ARRAY_KEY_REGULAR_EXPRESSION.search(key):
144
+ return f"{_path}{match.group(1)}" if _path else match.group(1)
145
+ return f"{_path}.{key}" if _path else key
146
+ def list_to_dictionary(value: list) -> dict: # noqa
147
+ result = {}
148
+ for index, item in enumerate(sorted(value)): # ignore array order
149
+ result[f"#{index}"] = item
150
+ return result
151
+ diffs = {}
152
+ for key in a:
153
+ path = key_to_path(key)
154
+ if key not in b:
155
+ diffs[path] = {"value": a[key], "missing_value": True}
156
+ else:
157
+ if isinstance(a[key], dict) and isinstance(b[key], dict):
158
+ diffs.update(PortalObject._compare(a[key], b[key], compare=compare, _path=path))
159
+ elif isinstance(a[key], list) and isinstance(b[key], list):
160
+ diffs.update(PortalObject._compare(list_to_dictionary(a[key]),
161
+ list_to_dictionary(b[key]), compare=compare, _path=path))
162
+ elif a[key] != b[key]:
163
+ if not callable(compare) or not compare(path, a[key], b[key]):
164
+ diffs[path] = {"value": a[key], "differing_value": b[key]}
165
+ return diffs
@@ -1,4 +1,6 @@
1
- from typing import Any, Dict, List
1
+ import os
2
+ from typing import Any, Dict, List, Optional, Tuple
3
+ from dcicutils.misc_utils import to_camel_case
2
4
 
3
5
 
4
6
  class JsonSchemaConstants:
@@ -183,3 +185,79 @@ def get_one_of_formats(schema: Dict[str, Any]) -> List[str]:
183
185
  for one_of_schema in get_one_of(schema)
184
186
  if get_format(one_of_schema)
185
187
  ]
188
+
189
+
190
+ class Schema:
191
+
192
+ def __init__(self, schema: dict, schema_type: Optional[str] = None) -> None:
193
+ self._data = schema if isinstance(schema, dict) else (schema.data if isinstance(schema, Schema) else {})
194
+ self._type = (isinstance(schema_type, str) and schema_type) or Schema.type_name(self._data.get("title", ""))
195
+
196
+ @property
197
+ def data(self) -> dict:
198
+ return self._data
199
+
200
+ @property
201
+ def type(self) -> str:
202
+ return self._type
203
+
204
+ @staticmethod
205
+ def type_name(value: str) -> Optional[str]: # File or other name.
206
+ if isinstance(value, str) and (value := os.path.basename(value.replace(" ", ""))):
207
+ return to_camel_case(value[0:dot] if (dot := value.rfind(".")) >= 0 else value)
208
+
209
+ def property_by_path(self, property_path: str) -> Optional[dict]:
210
+ """
211
+ TODO
212
+ """
213
+ return Schema.get_property_by_path(self._data, property_path)
214
+
215
+ _ARRAY_NAME_SUFFIX_CHAR = "#"
216
+ _DOTTED_NAME_DELIMITER_CHAR = "."
217
+
218
+ @staticmethod
219
+ def get_property_by_path(schema: dict, property_path: str) -> Optional[dict]:
220
+ if not isinstance(schema, dict) or not isinstance(property_path, str):
221
+ return None
222
+ elif not (schema_properties := schema.get("properties")):
223
+ return None
224
+ property_paths = property_path.split(Schema._DOTTED_NAME_DELIMITER_CHAR)
225
+ for property_index, property_name in enumerate(property_paths):
226
+ property_name, array_specifiers = Schema._unarrayize_property_name(property_name)
227
+ if not (property_value := schema_properties.get(property_name)):
228
+ return None
229
+ elif (property_type := property_value.get("type")) == "object":
230
+ property_paths_tail = Schema._DOTTED_NAME_DELIMITER_CHAR.join(property_paths[property_index + 1:])
231
+ return Schema.get_property_by_path(property_value, property_paths_tail)
232
+ elif (property_type := property_value.get("type")) == "array":
233
+ if not array_specifiers:
234
+ if property_index == len(property_paths) - 1:
235
+ return property_value
236
+ return None
237
+ for array_index in range(len(array_specifiers)):
238
+ if property_type != "array":
239
+ return None
240
+ elif not (array_items := property_value.get("items")):
241
+ return None
242
+ property_type = (property_value := array_items).get("type")
243
+ if property_type == "object":
244
+ if property_index == len(property_paths) - 1:
245
+ return property_value
246
+ property_paths_tail = Schema._DOTTED_NAME_DELIMITER_CHAR.join(property_paths[property_index + 1:])
247
+ return Schema.get_property_by_path(property_value, property_paths_tail)
248
+ return property_value
249
+
250
+ @staticmethod
251
+ def _unarrayize_property_name(property_name: str) -> Tuple[str, Optional[List[int]]]:
252
+ if len(components := (property_name := property_name.strip()).split(Schema._ARRAY_NAME_SUFFIX_CHAR)) < 2:
253
+ return property_name, None
254
+ unarrayized_property_name = components[0].strip()
255
+ array_specifiers = []
256
+ for component in components[1:]:
257
+ if component.isdigit():
258
+ array_specifiers.append(int(component))
259
+ elif component == "":
260
+ array_specifiers.append(0)
261
+ else:
262
+ return property_name, None
263
+ return unarrayized_property_name, array_specifiers
@@ -74,7 +74,7 @@ class StructuredDataSet:
74
74
  row_number += 1
75
75
  if (validation_errors := schema.validate(data)) is not None:
76
76
  for validation_error in validation_errors:
77
- self._note_error({"src": create_dict(type=schema.name, row=row_number),
77
+ self._note_error({"src": create_dict(type=schema.type, row=row_number),
78
78
  "error": validation_error}, "validation")
79
79
 
80
80
  @property
@@ -168,7 +168,7 @@ class StructuredDataSet:
168
168
  if not structured_row_template: # Delay creation just so we don't reference schema if there are no rows.
169
169
  if not schema and not noschema and not (schema := Schema.load_by_name(type_name, portal=self._portal)):
170
170
  noschema = True
171
- elif schema and (schema_name := schema.name):
171
+ elif schema and (schema_name := schema.type):
172
172
  type_name = schema_name
173
173
  structured_row_template = _StructuredRowTemplate(reader.header, schema)
174
174
  structured_row = structured_row_template.create_row()
@@ -222,7 +222,7 @@ class _StructuredRowTemplate:
222
222
 
223
223
  def set_value(self, data: dict, column_name: str, value: str, file: Optional[str], row_number: int = -1) -> None:
224
224
  if (set_value_function := self._set_value_functions.get(column_name)):
225
- src = create_dict(type=self._schema.name if self._schema else None,
225
+ src = create_dict(type=self._schema.type if self._schema else None,
226
226
  column=column_name, file=file, row=row_number)
227
227
  set_value_function(data, value, src)
228
228
 
@@ -319,8 +319,8 @@ class _StructuredRowTemplate:
319
319
  class Schema:
320
320
 
321
321
  def __init__(self, schema_json: dict, portal: Optional[Portal] = None) -> None:
322
- self.data = schema_json
323
- self.name = Schema.type_name(schema_json.get("title", "")) if schema_json else ""
322
+ self._data = schema_json if isinstance(schema_json, dict) else {}
323
+ self._type = Schema.type_name(schema_json.get("title", ""))
324
324
  self._portal = portal # Needed only to resolve linkTo references.
325
325
  self._map_value_functions = {
326
326
  "boolean": self._map_function_boolean,
@@ -333,6 +333,14 @@ class Schema:
333
333
  self._unresolved_refs = []
334
334
  self._typeinfo = self._create_typeinfo(schema_json)
335
335
 
336
+ @property
337
+ def data(self) -> dict:
338
+ return self._data
339
+
340
+ @property
341
+ def type(self) -> str:
342
+ return self._type
343
+
336
344
  @staticmethod
337
345
  def load_by_name(name: str, portal: Portal) -> Optional[dict]:
338
346
  schema_json = portal.get_schema(Schema.type_name(name)) if portal else None
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dcicutils"
3
- version = "8.7.0.1b33" # TODO: To become 8.7.1
3
+ version = "8.7.0.1b35" # TODO: To become 8.7.1
4
4
  description = "Utility package for interacting with the 4DN Data Portal and other 4DN resources"
5
5
  authors = ["4DN-DCIC Team <support@4dnucleome.org>"]
6
6
  license = "MIT"
@@ -1,119 +0,0 @@
1
- from dcicutils.schema_utils import get_identifying_properties
2
- from dcicutils.portal_utils import Portal
3
- from functools import lru_cache
4
- from typing import List, Optional, Tuple, Union
5
-
6
-
7
- class PortalObject:
8
-
9
- def __init__(self, portal: Portal, portal_object: dict, portal_object_type: Optional[str] = None) -> None:
10
- self._portal = portal
11
- self._data = portal_object
12
- self._type = portal_object_type if isinstance(portal_object_type, str) and portal_object_type else None
13
-
14
- @property
15
- def data(self):
16
- return self._data
17
-
18
- @property
19
- @lru_cache(maxsize=1)
20
- def schema(self):
21
- return self._portal.get_schema(self.schema_type)
22
-
23
- @property
24
- @lru_cache(maxsize=1)
25
- def schema_type(self):
26
- return self._type or Portal.get_schema_type(self._data)
27
-
28
- @property
29
- @lru_cache(maxsize=1)
30
- def schema_types(self):
31
- return self._type or Portal.get_schema_types(self._data)
32
-
33
- @property
34
- @lru_cache(maxsize=1)
35
- def schema_identifying_properties(self) -> list:
36
- if not (schema := self.schema):
37
- return []
38
- return get_identifying_properties(schema)
39
-
40
- @property
41
- @lru_cache(maxsize=1)
42
- def uuid(self) -> Optional[str]:
43
- return PortalObject.get_uuid(self._data)
44
-
45
- @staticmethod
46
- def get_uuid(portal_object: dict) -> Optional[str]:
47
- return portal_object.get("uuid") if isinstance(portal_object, dict) else None
48
-
49
- @property
50
- @lru_cache(maxsize=1)
51
- def identifying_properties(self) -> List[str]:
52
- """
53
- Returns the list of all identifying property names of this Portal object which actually have values.
54
- Implicitly include "uuid" and "identifier" properties as identifying properties if they are actually
55
- properties in the object schema, and favor these (first); defavor "aliases"; no other ordering defined.
56
- """
57
- identifying_properties = []
58
- for identifying_property in self.schema_identifying_properties:
59
- if identifying_property not in ["uuid", "identifier", "aliases"]:
60
- if self._data.get(identifying_property):
61
- identifying_properties.append(identifying_property)
62
- if self._data.get("identifier"):
63
- identifying_properties.insert(0, "identifier")
64
- if self._data.get("uuid"):
65
- identifying_properties.insert(0, "uuid")
66
- if "aliases" in self.schema_identifying_properties and self._data.get("aliases"):
67
- identifying_properties.append("aliases")
68
- return identifying_properties
69
-
70
- @property
71
- @lru_cache(maxsize=1)
72
- def identifying_paths(self) -> List[str]:
73
- """
74
- Returns a list of the possible Portal URL paths identifying this Portal object.
75
- """
76
- if not (identifying_properties := self.identifying_properties):
77
- return []
78
- identifying_paths = []
79
- for identifying_property in identifying_properties:
80
- if (identifying_value := self._data.get(identifying_property)):
81
- if identifying_property == "uuid":
82
- identifying_paths.append(f"/{identifying_value}")
83
- # For now at least we include the path both with and without the schema type component
84
- # as for some identifying values it works (only) with and some it works (only) without.
85
- # For example: If we have FileSet with "accession", an identifying property, with value
86
- # SMAFSFXF1RO4 then /SMAFSFXF1RO4 works but /FileSet/SMAFSFXF1RO4 does not; and
87
- # conversely using "submitted_id", also an identifying property, with value
88
- # UW_FILE-SET_COLO-829BL_HI-C_1 then /UW_FILE-SET_COLO-829BL_HI-C_1 does
89
- # not work but /FileSet/UW_FILE-SET_COLO-829BL_HI-C_1 does work.
90
- elif isinstance(identifying_value, list):
91
- for identifying_value_item in identifying_value:
92
- identifying_paths.append(f"/{self.schema_type}/{identifying_value_item}")
93
- identifying_paths.append(f"/{identifying_value_item}")
94
- else:
95
- identifying_paths.append(f"/{self.schema_type}/{identifying_value}")
96
- identifying_paths.append(f"/{identifying_value}")
97
- return identifying_paths
98
-
99
- @property
100
- @lru_cache(maxsize=1)
101
- def identifying_path(self) -> Optional[str]:
102
- if identifying_paths := self.identifying_paths:
103
- return identifying_paths[0]
104
-
105
- def lookup(self, include_identifying_path: bool = False,
106
- raw: bool = False) -> Optional[Union[Tuple[dict, str], dict]]:
107
- return self._lookup(raw=raw) if include_identifying_path else self._lookup(raw=raw)[0]
108
-
109
- def lookup_identifying_path(self) -> Optional[str]:
110
- return self._lookup()[1]
111
-
112
- def _lookup(self, raw: bool = False) -> Tuple[Optional[dict], Optional[str]]:
113
- try:
114
- for identifying_path in self.identifying_paths:
115
- if (value := self._portal.get(identifying_path, raw=raw)) and (value.status_code == 200):
116
- return value.json(), identifying_path
117
- except Exception:
118
- pass
119
- return None, self.identifying_path