dcicutils 8.4.0.1b10__tar.gz → 8.4.0.1b12__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/PKG-INFO +1 -1
  2. dcicutils-8.4.0.1b12/dcicutils/portal_utils.py +267 -0
  3. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/structured_data.py +11 -215
  4. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/pyproject.toml +1 -1
  5. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/LICENSE.txt +0 -0
  6. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/README.rst +0 -0
  7. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/__init__.py +0 -0
  8. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/base.py +0 -0
  9. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/beanstalk_utils.py +0 -0
  10. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/bundle_utils.py +0 -0
  11. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/cloudformation_utils.py +0 -0
  12. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/codebuild_utils.py +0 -0
  13. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/command_utils.py +0 -0
  14. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/common.py +0 -0
  15. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/contribution_scripts.py +0 -0
  16. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/contribution_utils.py +0 -0
  17. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/creds_utils.py +0 -0
  18. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/data_readers.py +0 -0
  19. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/data_utils.py +0 -0
  20. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/deployment_utils.py +0 -0
  21. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/diff_utils.py +0 -0
  22. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/docker_utils.py +0 -0
  23. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/ecr_scripts.py +0 -0
  24. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/ecr_utils.py +0 -0
  25. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/ecs_utils.py +0 -0
  26. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/env_base.py +0 -0
  27. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/env_manager.py +0 -0
  28. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/env_scripts.py +0 -0
  29. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/env_utils.py +0 -0
  30. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/env_utils_legacy.py +0 -0
  31. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/es_utils.py +0 -0
  32. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/exceptions.py +0 -0
  33. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/ff_mocks.py +0 -0
  34. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/ff_utils.py +0 -0
  35. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/function_cache_decorator.py +0 -0
  36. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/glacier_utils.py +0 -0
  37. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/jh_utils.py +0 -0
  38. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/kibana/dashboards.json +0 -0
  39. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/kibana/readme.md +0 -0
  40. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/lang_utils.py +0 -0
  41. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/license_policies/c4-infrastructure.jsonc +0 -0
  42. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/license_policies/c4-python-infrastructure.jsonc +0 -0
  43. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/license_policies/park-lab-common-server.jsonc +0 -0
  44. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/license_policies/park-lab-common.jsonc +0 -0
  45. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/license_policies/park-lab-gpl-pipeline.jsonc +0 -0
  46. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/license_policies/park-lab-pipeline.jsonc +0 -0
  47. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/license_utils.py +0 -0
  48. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/log_utils.py +0 -0
  49. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/misc_utils.py +0 -0
  50. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/obfuscation_utils.py +0 -0
  51. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/opensearch_utils.py +0 -0
  52. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/project_utils.py +0 -0
  53. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/qa_checkers.py +0 -0
  54. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/qa_utils.py +0 -0
  55. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/redis_tools.py +0 -0
  56. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/redis_utils.py +0 -0
  57. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/s3_utils.py +0 -0
  58. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/scripts/publish_to_pypi.py +0 -0
  59. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/scripts/run_license_checker.py +0 -0
  60. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/secrets_utils.py +0 -0
  61. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/sheet_utils.py +0 -0
  62. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/snapshot_utils.py +0 -0
  63. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/ssl_certificate_utils.py +0 -0
  64. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/task_utils.py +0 -0
  65. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/trace_utils.py +0 -0
  66. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/validation_utils.py +0 -0
  67. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/variant_utils.py +0 -0
  68. {dcicutils-8.4.0.1b10 → dcicutils-8.4.0.1b12}/dcicutils/zip_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcicutils
3
- Version: 8.4.0.1b10
3
+ Version: 8.4.0.1b12
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,267 @@
1
+ from collections import deque
2
+ from pyramid.paster import get_app
3
+ from pyramid.router import Router
4
+ import re
5
+ import requests
6
+ from requests.models import Response as RequestResponse
7
+ from typing import Optional, Type, Union
8
+ from webtest.app import TestApp, TestResponse
9
+ from dcicutils.common import OrchestratedApp, APP_CGAP, APP_FOURFRONT, APP_SMAHT, ORCHESTRATED_APPS
10
+ from dcicutils.creds_utils import CGAPKeyManager, FourfrontKeyManager, SMaHTKeyManager
11
+ from dcicutils.ff_utils import get_metadata, get_schema, patch_metadata, post_metadata
12
+ from dcicutils.misc_utils import to_camel_case, VirtualApp
13
+ from dcicutils.zip_utils import temporary_file
14
+
15
+ Portal = Type["Portal"] # Forward type reference for type hints.
16
+ FILE_SCHEMA_NAME = "File"
17
+
18
+
19
+ class Portal:
20
+
21
+ def __init__(self,
22
+ arg: Optional[Union[VirtualApp, TestApp, Router, Portal, dict, tuple, str]] = None,
23
+ env: Optional[str] = None, app: Optional[OrchestratedApp] = None, server: Optional[str] = None,
24
+ key: Optional[Union[dict, tuple]] = None,
25
+ vapp: Optional[Union[VirtualApp, TestApp, Router, Portal, str]] = None,
26
+ portal: Optional[Union[VirtualApp, TestApp, Router, Portal, str]] = None) -> Portal:
27
+ if vapp and not portal:
28
+ portal = vapp
29
+ if ((isinstance(arg, (VirtualApp, TestApp, Router, Portal)) or
30
+ isinstance(arg, str) and arg.endswith(".ini")) and not portal):
31
+ portal = arg
32
+ elif isinstance(arg, str) and not env:
33
+ env = arg
34
+ elif (isinstance(arg, dict) or isinstance(arg, tuple)) and not key:
35
+ key = arg
36
+ if not app and env:
37
+ if env.startswith(APP_SMAHT):
38
+ app = APP_SMAHT
39
+ elif env.startswith(APP_CGAP):
40
+ app = APP_CGAP
41
+ elif env.startswith(APP_FOURFRONT):
42
+ app = APP_FOURFRONT
43
+ if isinstance(portal, Portal):
44
+ self._vapp = portal._vapp
45
+ self._env = portal._env
46
+ self._app = portal._app
47
+ self._server = portal._server
48
+ self._key = portal._key
49
+ self._key_pair = portal._key_pair
50
+ self._key_file = portal._key_file
51
+ return
52
+ self._vapp = None
53
+ self._env = env
54
+ self._app = app
55
+ self._server = server
56
+ self._key = None
57
+ self._key_pair = None
58
+ self._key_file = None
59
+ if isinstance(portal, (VirtualApp, TestApp)):
60
+ self._vapp = portal
61
+ elif isinstance(portal, (Router, str)):
62
+ self._vapp = Portal._create_testapp(portal)
63
+ elif isinstance(key, dict):
64
+ self._key = key
65
+ self._key_pair = (key.get("key"), key.get("secret")) if key else None
66
+ if key_server := key.get("server"):
67
+ self._server = key_server
68
+ elif isinstance(key, tuple) and len(key) >= 2:
69
+ self._key = {"key": key[0], "secret": key[1]}
70
+ self._key_pair = key
71
+ elif isinstance(env, str):
72
+ key_managers = {APP_CGAP: CGAPKeyManager, APP_FOURFRONT: FourfrontKeyManager, APP_SMAHT: SMaHTKeyManager}
73
+ if not (key_manager := key_managers.get(self._app)) or not (key_manager := key_manager()):
74
+ raise Exception(f"Invalid app name: {self._app} (valid: {', '.join(ORCHESTRATED_APPS)}).")
75
+ if isinstance(env, str):
76
+ self._key = key_manager.get_keydict_for_env(env)
77
+ if key_server := self._key.get("server"):
78
+ self._server = key_server
79
+ elif isinstance(self._server, str):
80
+ self._key = key_manager.get_keydict_for_server(self._server)
81
+ self._key_pair = key_manager.keydict_to_keypair(self._key) if self._key else None
82
+ self._key_file = key_manager.keys_file
83
+
84
+ @property
85
+ def env(self):
86
+ return self._env
87
+
88
+ @property
89
+ def app(self):
90
+ return self._app
91
+
92
+ @property
93
+ def server(self):
94
+ return self._server
95
+
96
+ @property
97
+ def key(self):
98
+ return self._key
99
+
100
+ @property
101
+ def key_pair(self):
102
+ return self._key_pair
103
+
104
+ @property
105
+ def key_file(self):
106
+ return self._key_file
107
+
108
+ @property
109
+ def vapp(self):
110
+ return self._vapp
111
+
112
+ def get_metadata(self, object_id: str) -> Optional[dict]:
113
+ return get_metadata(obj_id=object_id, vapp=self._vapp, key=self._key)
114
+
115
+ def patch_metadata(self, object_id: str, data: str) -> Optional[dict]:
116
+ if self._key:
117
+ return patch_metadata(obj_id=object_id, patch_item=data, key=self._key)
118
+ return self.patch(f"/{object_id}", data)
119
+
120
+ def post_metadata(self, object_type: str, data: str) -> Optional[dict]:
121
+ if self._key:
122
+ return post_metadata(schema_name=object_type, post_item=data, key=self._key)
123
+ return self.post(f"/{object_type}", data)
124
+
125
+ def get(self, uri: str, follow: bool = True, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
126
+ if isinstance(self._vapp, (VirtualApp, TestApp)):
127
+ response = self._vapp.get(self._uri(uri), **self._kwargs(**kwargs))
128
+ if response and response.status_code in [301, 302, 303, 307, 308] and follow:
129
+ response = response.follow()
130
+ return self._response(response)
131
+ return requests.get(self._uri(uri), allow_redirects=follow, **self._kwargs(**kwargs))
132
+
133
+ def patch(self, uri: str, data: Optional[dict] = None,
134
+ json: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
135
+ if isinstance(self._vapp, (VirtualApp, TestApp)):
136
+ return self._vapp.patch_json(self._uri(uri), json or data, **self._kwargs(**kwargs))
137
+ return requests.patch(self._uri(uri), json=json or data, **self._kwargs(**kwargs))
138
+
139
+ def post(self, uri: str, data: Optional[dict] = None, json: Optional[dict] = None,
140
+ files: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
141
+ if isinstance(self._vapp, (VirtualApp, TestApp)):
142
+ if files:
143
+ return self._vapp.post(self._uri(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
144
+ else:
145
+ return self._vapp.post_json(self._uri(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
146
+ return requests.post(self._uri(uri), json=json or data, files=files, **self._kwargs(**kwargs))
147
+
148
+ def get_schema(self, schema_name: str) -> Optional[dict]:
149
+ return get_schema(self.schema_name(schema_name), portal_vapp=self._vapp, key=self._key)
150
+
151
+ def get_schemas(self) -> dict:
152
+ return self.get("/profiles/").json()
153
+
154
+ @staticmethod
155
+ def schema_name(name: str) -> str:
156
+ return to_camel_case(name)
157
+
158
+ def is_file_schema(self, schema_name: str) -> bool:
159
+ if super_type_map := self.get_schemas_super_type_map():
160
+ if file_super_type := super_type_map.get(FILE_SCHEMA_NAME):
161
+ return self.schema_name(schema_name) in file_super_type
162
+ return False
163
+
164
+ def get_schemas_super_type_map(self) -> dict:
165
+ """
166
+ Returns the "super type map" for all of the known schemas (via /profiles).
167
+ This is a dictionary of all types which have (one or more) sub-types whose value is
168
+ an array of all of those sub-types (direct and all descendents), in breadth first order.
169
+ """
170
+ def breadth_first(super_type_map: dict, super_type_name: str) -> dict:
171
+ result = []
172
+ queue = deque(super_type_map.get(super_type_name, []))
173
+ while queue:
174
+ result.append(sub_type_name := queue.popleft())
175
+ if sub_type_name in super_type_map:
176
+ queue.extend(super_type_map[sub_type_name])
177
+ return result
178
+ if not (schemas := self.get_schemas()):
179
+ return {}
180
+ super_type_map = {}
181
+ for type_name in schemas:
182
+ if super_type_name := schemas[type_name].get("rdfs:subClassOf"):
183
+ super_type_name = super_type_name.replace("/profiles/", "").replace(".json", "")
184
+ if super_type_name != "Item":
185
+ if not super_type_map.get(super_type_name):
186
+ super_type_map[super_type_name] = [type_name]
187
+ elif type_name not in super_type_map[super_type_name]:
188
+ super_type_map[super_type_name].append(type_name)
189
+ super_type_map_flattened = {}
190
+ for super_type_name in super_type_map:
191
+ super_type_map_flattened[super_type_name] = breadth_first(super_type_map, super_type_name)
192
+ return super_type_map_flattened
193
+
194
+ def _uri(self, uri: str) -> str:
195
+ if not isinstance(uri, str) or not uri:
196
+ return "/"
197
+ if uri.lower().startswith("http://") or uri.lower().startswith("https://"):
198
+ return uri
199
+ uri = re.sub(r"/+", "/", uri)
200
+ return (self._server + ("/" if uri.startswith("/") else "") + uri) if self._server else uri
201
+
202
+ def _kwargs(self, **kwargs) -> dict:
203
+ result_kwargs = {"headers":
204
+ kwargs.get("headers", {"Content-type": "application/json", "Accept": "application/json"})}
205
+ if self._key_pair:
206
+ result_kwargs["auth"] = self._key_pair
207
+ if isinstance(timeout := kwargs.get("timeout"), int):
208
+ result_kwargs["timeout"] = timeout
209
+ return result_kwargs
210
+
211
+ def _response(self, response) -> Optional[RequestResponse]:
212
+ if response and isinstance(getattr(response.__class__, "json"), property):
213
+ class RequestResponseWrapper: # For consistency change json property to method.
214
+ def __init__(self, response, **kwargs):
215
+ super().__init__(**kwargs)
216
+ self._response = response
217
+ def __getattr__(self, attr): # noqa
218
+ return getattr(self._response, attr)
219
+ def json(self): # noqa
220
+ return self._response.json
221
+ response = RequestResponseWrapper(response)
222
+ return response
223
+
224
+ @staticmethod
225
+ def create_for_testing(ini_file: Optional[str] = None) -> Portal:
226
+ if isinstance(ini_file, str):
227
+ return Portal(Portal._create_testapp(ini_file))
228
+ minimal_ini_for_unit_testing = "[app:app]\nuse = egg:encoded\nsqlalchemy.url = postgresql://dummy\n"
229
+ with temporary_file(content=minimal_ini_for_unit_testing, suffix=".ini") as ini_file:
230
+ return Portal(Portal._create_testapp(ini_file))
231
+
232
+ @staticmethod
233
+ def create_for_testing_local(ini_file: Optional[str] = None) -> Portal:
234
+ if isinstance(ini_file, str) and ini_file:
235
+ return Portal(Portal._create_testapp(ini_file))
236
+ minimal_ini_for_testing_local = "\n".join([
237
+ "[app:app]\nuse = egg:encoded\nfile_upload_bucket = dummy",
238
+ "sqlalchemy.url = postgresql://postgres@localhost:5441/postgres?host=/tmp/snovault/pgdata",
239
+ "multiauth.groupfinder = encoded.authorization.smaht_groupfinder",
240
+ "multiauth.policies = auth0 session remoteuser accesskey",
241
+ "multiauth.policy.session.namespace = mailto",
242
+ "multiauth.policy.session.use = encoded.authentication.NamespacedAuthenticationPolicy",
243
+ "multiauth.policy.session.base = pyramid.authentication.SessionAuthenticationPolicy",
244
+ "multiauth.policy.remoteuser.namespace = remoteuser",
245
+ "multiauth.policy.remoteuser.use = encoded.authentication.NamespacedAuthenticationPolicy",
246
+ "multiauth.policy.remoteuser.base = pyramid.authentication.RemoteUserAuthenticationPolicy",
247
+ "multiauth.policy.accesskey.namespace = accesskey",
248
+ "multiauth.policy.accesskey.use = encoded.authentication.NamespacedAuthenticationPolicy",
249
+ "multiauth.policy.accesskey.base = encoded.authentication.BasicAuthAuthenticationPolicy",
250
+ "multiauth.policy.accesskey.check = encoded.authentication.basic_auth_check",
251
+ "multiauth.policy.auth0.use = encoded.authentication.NamespacedAuthenticationPolicy",
252
+ "multiauth.policy.auth0.namespace = auth0",
253
+ "multiauth.policy.auth0.base = encoded.authentication.Auth0AuthenticationPolicy"
254
+ ])
255
+ with temporary_file(content=minimal_ini_for_testing_local, suffix=".ini") as minimal_ini_file:
256
+ return Portal(Portal._create_testapp(minimal_ini_file))
257
+
258
+ @staticmethod
259
+ def _create_testapp(value: Union[str, Router, TestApp] = "development.ini") -> TestApp:
260
+ """
261
+ Creates and returns a TestApp. Refactored out of above loadxl code to consolidate at a
262
+ single point; also for use by the generate_local_access_key and view_local_object scripts.
263
+ """
264
+ if isinstance(value, TestApp):
265
+ return value
266
+ app = value if isinstance(value, Router) else get_app(value, "app")
267
+ return TestApp(app, {"HTTP_ACCEPT": "application/json", "REMOTE_USER": "TEST"})
@@ -1,24 +1,19 @@
1
- from collections import deque
2
1
  import copy
3
2
  from functools import lru_cache
4
3
  import json
5
4
  from jsonschema import Draft7Validator as SchemaValidator
6
5
  import os
7
- from pyramid.paster import get_app
8
6
  from pyramid.router import Router
9
7
  import re
10
- import requests
11
- from requests.models import Response as RequestResponse
12
8
  import sys
13
9
  from typing import Any, Callable, List, Optional, Tuple, Type, Union
14
- from webtest.app import TestApp, TestResponse
15
- from dcicutils.common import OrchestratedApp, APP_CGAP, APP_FOURFRONT, APP_SMAHT, ORCHESTRATED_APPS
16
- from dcicutils.creds_utils import CGAPKeyManager, FourfrontKeyManager, SMaHTKeyManager
10
+ from webtest.app import TestApp
11
+ from dcicutils.common import OrchestratedApp
17
12
  from dcicutils.data_readers import CsvReader, Excel, RowReader
18
- from dcicutils.ff_utils import get_metadata, get_schema, patch_metadata, post_metadata
19
13
  from dcicutils.misc_utils import (create_object, load_json_if, merge_objects, remove_empty_properties, right_trim,
20
- split_string, to_boolean, to_camel_case, to_enum, to_float, to_integer, VirtualApp)
21
- from dcicutils.zip_utils import temporary_file, unpack_gz_file_to_temporary_file, unpack_files
14
+ split_string, to_boolean, to_enum, to_float, to_integer, VirtualApp)
15
+ from dcicutils.portal_utils import Portal as PortalBase
16
+ from dcicutils.zip_utils import unpack_gz_file_to_temporary_file, unpack_files
22
17
 
23
18
 
24
19
  # Classes/functions to parse a CSV or Excel Spreadsheet into structured data, using a specialized
@@ -36,12 +31,10 @@ ARRAY_VALUE_DELIMITER_ESCAPE_CHAR = "\\"
36
31
  ARRAY_NAME_SUFFIX_CHAR = "#"
37
32
  ARRAY_NAME_SUFFIX_REGEX = re.compile(rf"{ARRAY_NAME_SUFFIX_CHAR}\d+")
38
33
  DOTTED_NAME_DELIMITER_CHAR = "."
39
- FILE_SCHEMA_NAME = "File"
40
34
  FILE_SCHEMA_NAME_PROPERTY = "filename"
41
35
 
42
36
  # Forward type references for type hints.
43
37
  Portal = Type["Portal"]
44
- PortalBase = Type["PortalBase"]
45
38
  Schema = Type["Schema"]
46
39
  StructuredDataSet = Type["StructuredDataSet"]
47
40
 
@@ -526,7 +519,7 @@ class Schema:
526
519
  @staticmethod
527
520
  def type_name(value: str) -> str: # File or other name.
528
521
  name = os.path.basename(value).replace(" ", "") if isinstance(value, str) else ""
529
- return to_camel_case(name[0:dot] if (dot := name.rfind(".")) > 0 else name)
522
+ return PortalBase.schema_name(name[0:dot] if (dot := name.rfind(".")) > 0 else name)
530
523
 
531
524
  @staticmethod
532
525
  def array_indices(name: str) -> Tuple[Optional[str], Optional[List[int]]]:
@@ -540,179 +533,15 @@ class Schema:
540
533
  return (name, indices) if indices else (None, None)
541
534
 
542
535
 
543
- class PortalBase:
544
-
545
- def __init__(self,
546
- arg: Optional[Union[VirtualApp, TestApp, Router, Portal, dict, tuple, str]] = None,
547
- env: Optional[str] = None, app: OrchestratedApp = APP_SMAHT, server: Optional[str] = None,
548
- key: Optional[Union[dict, tuple]] = None,
549
- portal: Optional[Union[VirtualApp, TestApp, Router, Portal, str]] = None) -> PortalBase:
550
- if ((isinstance(arg, (VirtualApp, TestApp, Router, Portal)) or
551
- isinstance(arg, str) and arg.endswith(".ini")) and not portal):
552
- portal = arg
553
- elif isinstance(arg, str) and not env:
554
- env = arg
555
- elif (isinstance(arg, dict) or isinstance(arg, tuple)) and not key:
556
- key = arg
557
- self._vapp = None
558
- self._key = None
559
- self._key_pair = None
560
- self._server = None
561
- if isinstance(portal, Portal):
562
- self._vapp = portal._vapp
563
- self._key = portal._key
564
- self._key_pair = portal._key_pair
565
- self._server = portal._server
566
- elif isinstance(portal, (VirtualApp, TestApp)):
567
- self._vapp = portal
568
- elif isinstance(portal, (Router, str)):
569
- self._vapp = PortalBase._create_testapp(portal)
570
- elif isinstance(key, dict):
571
- self._key = key
572
- self._key_pair = (key.get("key"), key.get("secret")) if key else None
573
- self._server = key.get("server")
574
- elif isinstance(key, tuple) and len(key) >= 2:
575
- self._key = {"key": key[0], "secret": key[1]}
576
- self._key_pair = key
577
- elif isinstance(env, str):
578
- key_managers = {APP_CGAP: CGAPKeyManager, APP_FOURFRONT: FourfrontKeyManager, APP_SMAHT: SMaHTKeyManager}
579
- if not (key_manager := key_managers.get(app)) or not (key_manager := key_manager()):
580
- raise Exception(f"Invalid app name: {app} (valid: {', '.join(ORCHESTRATED_APPS)}).")
581
- if isinstance(env, str):
582
- self._key = key_manager.get_keydict_for_env(env)
583
- self._server = self._key.get("server") if self._key else None
584
- elif isinstance(server, str):
585
- self._key = key_manager.get_keydict_for_server(server)
586
- self._server = server
587
- self._key_pair = key_manager.keydict_to_keypair(self._key) if self._key else None
588
-
589
- def get_metadata(self, object_id: str) -> Optional[dict]:
590
- return get_metadata(obj_id=object_id, vapp=self._vapp, key=self._key)
591
-
592
- def patch_metadata(self, object_id: str, data: str) -> Optional[dict]:
593
- if self._key:
594
- return patch_metadata(obj_id=object_id, patch_item=data, key=self._key)
595
- return self.patch(f"/{object_id}", data)
596
-
597
- def post_metadata(self, object_type: str, data: str) -> Optional[dict]:
598
- if self._key:
599
- return post_metadata(schema_name=object_type, post_item=data, key=self._key)
600
- return self.post(f"/{object_type}", data)
601
-
602
- def get(self, uri: str, follow: bool = True, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
603
- if isinstance(self._vapp, (VirtualApp, TestApp)):
604
- response = self._vapp.get(self._uri(uri), **self._kwargs(**kwargs))
605
- if response and response.status_code in [301, 302, 303, 307, 308] and follow:
606
- response = response.follow()
607
- return self._response(response)
608
- return requests.get(self._uri(uri), allow_redirects=follow, **self._kwargs(**kwargs))
609
-
610
- def patch(self, uri: str, data: Optional[dict] = None,
611
- json: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
612
- if isinstance(self._vapp, (VirtualApp, TestApp)):
613
- return self._vapp.patch_json(self._uri(uri), json or data, **self._kwargs(**kwargs))
614
- return requests.patch(self._uri(uri), json=json or data, **self._kwargs(**kwargs))
615
-
616
- def post(self, uri: str, data: Optional[dict] = None, json: Optional[dict] = None,
617
- files: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
618
- if isinstance(self._vapp, (VirtualApp, TestApp)):
619
- if files:
620
- return self._vapp.post(self._uri(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
621
- else:
622
- return self._vapp.post_json(self._uri(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
623
- return requests.post(self._uri(uri), json=json or data, files=files, **self._kwargs(**kwargs))
624
-
625
- def get_schema(self, schema_name: str) -> Optional[dict]:
626
- return get_schema(schema_name, portal_vapp=self._vapp, key=self._key)
627
-
628
- def get_schemas(self) -> dict:
629
- return self.get("/profiles/").json()
630
-
631
- def _uri(self, uri: str) -> str:
632
- if not isinstance(uri, str) or not uri:
633
- return "/"
634
- if uri.lower().startswith("http://") or uri.lower().startswith("https://"):
635
- return uri
636
- uri = re.sub(r"/+", "/", uri)
637
- return (self._server + ("/" if uri.startswith("/") else "") + uri) if self._server else uri
638
-
639
- def _kwargs(self, **kwargs) -> dict:
640
- result_kwargs = {"headers":
641
- kwargs.get("headers", {"Content-type": "application/json", "Accept": "application/json"})}
642
- if self._key_pair:
643
- result_kwargs["auth"] = self._key_pair
644
- if isinstance(timeout := kwargs.get("timeout"), int):
645
- result_kwargs["timeout"] = timeout
646
- return result_kwargs
647
-
648
- def _response(self, response) -> Optional[RequestResponse]:
649
- if response and isinstance(getattr(response.__class__, "json"), property):
650
- class RequestResponseWrapper: # For consistency change json property to method.
651
- def __init__(self, response, **kwargs):
652
- super().__init__(**kwargs)
653
- self._response = response
654
- def __getattr__(self, attr): # noqa
655
- return getattr(self._response, attr)
656
- def json(self): # noqa
657
- return self._response.json
658
- response = RequestResponseWrapper(response)
659
- return response
660
-
661
- @staticmethod
662
- def create_for_testing(ini_file: Optional[str] = None) -> PortalBase:
663
- if isinstance(ini_file, str):
664
- return Portal(Portal._create_testapp(ini_file))
665
- minimal_ini_for_unit_testing = "[app:app]\nuse = egg:encoded\nsqlalchemy.url = postgresql://dummy\n"
666
- with temporary_file(content=minimal_ini_for_unit_testing, suffix=".ini") as ini_file:
667
- return Portal(Portal._create_testapp(ini_file))
668
-
669
- @staticmethod
670
- def create_for_testing_local(ini_file: Optional[str] = None) -> Portal:
671
- if isinstance(ini_file, str) and ini_file:
672
- return Portal(Portal._create_testapp(ini_file))
673
- minimal_ini_for_testing_local = "\n".join([
674
- "[app:app]\nuse = egg:encoded\nfile_upload_bucket = dummy",
675
- "sqlalchemy.url = postgresql://postgres@localhost:5441/postgres?host=/tmp/snovault/pgdata",
676
- "multiauth.groupfinder = encoded.authorization.smaht_groupfinder",
677
- "multiauth.policies = auth0 session remoteuser accesskey",
678
- "multiauth.policy.session.namespace = mailto",
679
- "multiauth.policy.session.use = encoded.authentication.NamespacedAuthenticationPolicy",
680
- "multiauth.policy.session.base = pyramid.authentication.SessionAuthenticationPolicy",
681
- "multiauth.policy.remoteuser.namespace = remoteuser",
682
- "multiauth.policy.remoteuser.use = encoded.authentication.NamespacedAuthenticationPolicy",
683
- "multiauth.policy.remoteuser.base = pyramid.authentication.RemoteUserAuthenticationPolicy",
684
- "multiauth.policy.accesskey.namespace = accesskey",
685
- "multiauth.policy.accesskey.use = encoded.authentication.NamespacedAuthenticationPolicy",
686
- "multiauth.policy.accesskey.base = encoded.authentication.BasicAuthAuthenticationPolicy",
687
- "multiauth.policy.accesskey.check = encoded.authentication.basic_auth_check",
688
- "multiauth.policy.auth0.use = encoded.authentication.NamespacedAuthenticationPolicy",
689
- "multiauth.policy.auth0.namespace = auth0",
690
- "multiauth.policy.auth0.base = encoded.authentication.Auth0AuthenticationPolicy"
691
- ])
692
- with temporary_file(content=minimal_ini_for_testing_local, suffix=".ini") as minimal_ini_file:
693
- return Portal(Portal._create_testapp(minimal_ini_file))
694
-
695
- @staticmethod
696
- def _create_testapp(value: Union[str, Router, TestApp] = "development.ini") -> TestApp:
697
- """
698
- Creates and returns a TestApp. Refactored out of above loadxl code to consolidate at a
699
- single point; also for use by the generate_local_access_key and view_local_object scripts.
700
- """
701
- if isinstance(value, TestApp):
702
- return value
703
- app = value if isinstance(value, Router) else get_app(value, "app")
704
- return TestApp(app, {"HTTP_ACCEPT": "application/json", "REMOTE_USER": "TEST"})
705
-
706
-
707
536
  class Portal(PortalBase):
708
537
 
709
538
  def __init__(self,
710
539
  arg: Optional[Union[VirtualApp, TestApp, Router, Portal, dict, tuple, str]] = None,
711
- env: Optional[str] = None, app: OrchestratedApp = APP_SMAHT, server: Optional[str] = None,
540
+ env: Optional[str] = None, app: OrchestratedApp = None, server: Optional[str] = None,
712
541
  key: Optional[Union[dict, tuple]] = None,
713
542
  portal: Optional[Union[VirtualApp, TestApp, Router, Portal, str]] = None,
714
543
  data: Optional[dict] = None, schemas: Optional[List[dict]] = None) -> Optional[Portal]:
715
- super(Portal, self).__init__(arg, env=env, app=app, server=server, key=key, portal=portal)
544
+ super().__init__(arg, env=env, app=app, server=server, key=key, portal=portal)
716
545
  if isinstance(arg, Portal) and not portal:
717
546
  portal = arg
718
547
  if isinstance(portal, Portal):
@@ -725,7 +554,7 @@ class Portal(PortalBase):
725
554
  @lru_cache(maxsize=256)
726
555
  def get_metadata(self, object_name: str) -> Optional[dict]:
727
556
  try:
728
- return super(Portal, self).get_metadata(object_name)
557
+ return super().get_metadata(object_name)
729
558
  except Exception:
730
559
  return None
731
560
 
@@ -740,7 +569,7 @@ class Portal(PortalBase):
740
569
 
741
570
  @lru_cache(maxsize=1)
742
571
  def get_schemas(self) -> dict:
743
- schemas = super(Portal, self).get_schemas()
572
+ schemas = super().get_schemas()
744
573
  if self._schemas:
745
574
  schemas = copy.deepcopy(schemas)
746
575
  for user_specified_schema in self._schemas:
@@ -748,42 +577,9 @@ class Portal(PortalBase):
748
577
  schemas[user_specified_schema["title"]] = user_specified_schema
749
578
  return schemas
750
579
 
751
- def is_file_schema(self, schema_name: str) -> bool:
752
- if super_type_map := self.get_schemas_super_type_map():
753
- if file_super_type := super_type_map.get(FILE_SCHEMA_NAME):
754
- return Schema.type_name(schema_name) in file_super_type
755
- return False
756
-
757
580
  @lru_cache(maxsize=1)
758
581
  def get_schemas_super_type_map(self) -> dict:
759
- """
760
- Returns the "super type map" for all of the known schemas (via /profiles).
761
- This is a dictionary of all types which have (one or more) sub-types whose value is
762
- an array of all of those sub-types (direct and all descendents), in breadth first order.
763
- """
764
- def breadth_first(super_type_map: dict, super_type_name: str) -> dict:
765
- result = []
766
- queue = deque(super_type_map.get(super_type_name, []))
767
- while queue:
768
- result.append(sub_type_name := queue.popleft())
769
- if sub_type_name in super_type_map:
770
- queue.extend(super_type_map[sub_type_name])
771
- return result
772
- if not (schemas := self.get_schemas()):
773
- return {}
774
- super_type_map = {}
775
- for type_name in schemas:
776
- if super_type_name := schemas[type_name].get("rdfs:subClassOf"):
777
- super_type_name = super_type_name.replace("/profiles/", "").replace(".json", "")
778
- if super_type_name != "Item":
779
- if not super_type_map.get(super_type_name):
780
- super_type_map[super_type_name] = [type_name]
781
- elif type_name not in super_type_map[super_type_name]:
782
- super_type_map[super_type_name].append(type_name)
783
- super_type_map_flattened = {}
784
- for super_type_name in super_type_map:
785
- super_type_map_flattened[super_type_name] = breadth_first(super_type_map, super_type_name)
786
- return super_type_map_flattened
582
+ return super().get_schemas_super_type_map()
787
583
 
788
584
  def ref_exists(self, type_name: str, value: str) -> List[str]:
789
585
  resolved = []
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dcicutils"
3
- version = "8.4.0.1b10" # TODO: To become 8.4.1
3
+ version = "8.4.0.1b12" # TODO: To become 8.4.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"