dcicutils 8.5.0.1b6__py3-none-any.whl → 8.6.0.0b0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
dcicutils/misc_utils.py CHANGED
@@ -982,11 +982,7 @@ def to_integer(value: str, fallback: Optional[Any] = None) -> Optional[Any]:
982
982
  try:
983
983
  return int(value)
984
984
  except Exception:
985
- try:
986
- return int(float(value))
987
- except Exception:
988
- pass
989
- return fallback
985
+ return fallback
990
986
 
991
987
 
992
988
  def to_float(value: str, fallback: Optional[Any] = None) -> Optional[Any]:
@@ -1469,33 +1465,28 @@ def string_list(s):
1469
1465
  return [p for p in [part.strip() for part in s.split(",")] if p]
1470
1466
 
1471
1467
 
1472
- def split_string(value: str, delimiter: str, escape: Optional[str] = None, unique: bool = False) -> List[str]:
1468
+ def split_string(value: str, delimiter: str, escape: Optional[str] = None) -> List[str]:
1473
1469
  """
1474
1470
  Splits the given string into an array of string based on the given delimiter, and an optional escape character.
1475
1471
  """
1476
1472
  if not isinstance(value, str) or not (value := value.strip()):
1477
1473
  return []
1478
- result = []
1479
1474
  if not isinstance(escape, str) or not escape:
1480
- for item in value.split(delimiter):
1481
- if (item := item.strip()) and (unique is not True or item not in result):
1482
- result.append(item)
1483
- return result
1475
+ return [item.strip() for item in value.split(delimiter)]
1476
+ result = []
1484
1477
  item = r""
1485
1478
  escaped = False
1486
1479
  for c in value:
1487
1480
  if c == delimiter and not escaped:
1488
- if (item := item.strip()) and (unique is not True or item not in result):
1489
- result.append(item)
1481
+ result.append(item.strip())
1490
1482
  item = r""
1491
1483
  elif c == escape and not escaped:
1492
1484
  escaped = True
1493
1485
  else:
1494
1486
  item += c
1495
1487
  escaped = False
1496
- if (item := item.strip()) and (unique is not True or item not in result):
1497
- result.append(item)
1498
- return result
1488
+ result.append(item.strip())
1489
+ return [item for item in result if item]
1499
1490
 
1500
1491
 
1501
1492
  def right_trim(list_or_tuple: Union[List[Any], Tuple[Any]],
dcicutils/portal_utils.py CHANGED
@@ -1,20 +1,19 @@
1
1
  from collections import deque
2
- import io
3
- import json
4
2
  from pyramid.paster import get_app
5
3
  from pyramid.router import Router
6
- import os
7
4
  import re
8
5
  import requests
9
6
  from requests.models import Response as RequestResponse
10
7
  from typing import Optional, Type, Union
11
8
  from webtest.app import TestApp, TestResponse
12
- from dcicutils.common import OrchestratedApp, ORCHESTRATED_APPS
9
+ from dcicutils.common import OrchestratedApp, APP_CGAP, APP_FOURFRONT, APP_SMAHT, ORCHESTRATED_APPS
10
+ from dcicutils.creds_utils import CGAPKeyManager, FourfrontKeyManager, SMaHTKeyManager
13
11
  from dcicutils.ff_utils import get_metadata, get_schema, patch_metadata, post_metadata
14
12
  from dcicutils.misc_utils import to_camel_case, VirtualApp
15
13
  from dcicutils.zip_utils import temporary_file
16
14
 
17
15
  Portal = Type["Portal"] # Forward type reference for type hints.
16
+ FILE_SCHEMA_NAME = "File"
18
17
 
19
18
 
20
19
  class Portal:
@@ -24,175 +23,105 @@ class Portal:
24
23
  2. From a key dictionary, containing "key" and "secret" property values.
25
24
  3. From a key tuple, containing (in order) a key and secret values.
26
25
  4. From a keys file assumed to reside in ~/.{app}-keys.json where the given "app" value is either "smaht", "cgap",
27
- or "fourfront"; where is assumed to contain a dictionary with a key for the given "env" value, e.g. smaht-local;
28
- and with a dictionary value containing "key" and "secret" property values, and an optional "server" property;
29
- if an "app" value is not specified but the given "env" value begins with one of the app values then that value
30
- will be used, i.e. e.g. if "env" is "smaht-local" and app is unspecified than it is assumed to be "smaht".
26
+ or "fourfront"; and where this file is assumed to contain a dictionary with a key equal to the given "env"
27
+ value (e.g. smaht-localhost) and with a dictionary value containing "key" and "secret" property values; if
28
+ an "app" value is not specified but the given "env" value begins with one of the app values then that value
29
+ will be used, i.e. e.g. if env is "smaht-localhost" and app is unspecified than it is assumed to be "smaht".
31
30
  5. From a keys file as described above (#4) but rather than be identified by the given "env" value it
32
- is looked up via the given "server" name and the "server" key dictionary value in the key file.
31
+ is looked up by the given "server" name and the "server" key dictionary value in the key file.
33
32
  6. From a given "vapp" value (which is assumed to be a TestApp or VirtualApp).
34
- 7. From another Portal object; or from a a pyramid Router object.
33
+ 7. From another Portal object.
34
+ 8. From a a pyramid Router object.
35
35
  """
36
- FILE_SCHEMA_NAME = "File"
37
- KEYS_FILE_DIRECTORY = os.path.expanduser(f"~")
38
-
39
36
  def __init__(self,
40
- arg: Optional[Union[Portal, TestApp, VirtualApp, Router, dict, tuple, str]] = None,
41
- env: Optional[str] = None, server: Optional[str] = None,
42
- app: Optional[OrchestratedApp] = None) -> None:
43
-
44
- def init(unspecified: Optional[list] = []) -> None:
45
- self._ini_file = None
46
- self._key = None
47
- self._key_pair = None
48
- self._key_id = None
49
- self._secret = None
50
- self._keys_file = None
51
- self._env = None
52
- self._server = None
53
- self._app = None
54
- self._vapp = None
55
- for arg in unspecified:
56
- if arg is not None:
57
- raise Exception("Portal init error; extraneous args.")
58
-
59
- def init_from_portal(portal: Portal, unspecified: Optional[list] = None) -> None:
60
- init(unspecified)
61
- self._ini_file = portal._ini_file
62
- self._key = portal._key
63
- self._key_pair = portal._key_pair
64
- self._key_id = portal._key_id
65
- self._secret = portal._secret
66
- self._keys_file = portal._keys_file
37
+ arg: Optional[Union[VirtualApp, TestApp, Router, Portal, dict, tuple, str]] = None,
38
+ env: Optional[str] = None, app: Optional[OrchestratedApp] = None, server: Optional[str] = None,
39
+ key: Optional[Union[dict, tuple]] = None,
40
+ vapp: Optional[Union[VirtualApp, TestApp, Router, Portal, str]] = None,
41
+ portal: Optional[Union[VirtualApp, TestApp, Router, Portal, str]] = None) -> Portal:
42
+ if vapp and not portal:
43
+ portal = vapp
44
+ if ((isinstance(arg, (VirtualApp, TestApp, Router, Portal)) or
45
+ isinstance(arg, str) and arg.endswith(".ini")) and not portal):
46
+ portal = arg
47
+ elif isinstance(arg, str) and not env:
48
+ env = arg
49
+ elif (isinstance(arg, dict) or isinstance(arg, tuple)) and not key:
50
+ key = arg
51
+ if not app and env:
52
+ if env.startswith(APP_SMAHT):
53
+ app = APP_SMAHT
54
+ elif env.startswith(APP_CGAP):
55
+ app = APP_CGAP
56
+ elif env.startswith(APP_FOURFRONT):
57
+ app = APP_FOURFRONT
58
+ if isinstance(portal, Portal):
59
+ self._vapp = portal._vapp
67
60
  self._env = portal._env
68
- self._server = portal._server
69
61
  self._app = portal._app
70
- self._vapp = portal._vapp
71
-
72
- def init_from_vapp(vapp: Union[TestApp, VirtualApp, Router], unspecified: Optional[list] = []) -> None:
73
- init(unspecified)
74
- self._vapp = Portal._create_testapp(vapp)
75
-
76
- def init_from_ini_file(ini_file: str, unspecified: Optional[list] = []) -> None:
77
- init(unspecified)
78
- self._ini_file = ini_file
79
- self._vapp = Portal._create_testapp(ini_file)
80
-
81
- def init_from_key(key: dict, server: Optional[str], unspecified: Optional[list] = []) -> None:
82
- init(unspecified)
83
- if (isinstance(key_id := key.get("key"), str) and key_id and
84
- isinstance(secret := key.get("secret"), str) and secret): # noqa
85
- self._key = {"key": key_id, "secret": secret}
86
- self._key_id = key_id
87
- self._secret = secret
88
- self._key_pair = (key_id, secret)
89
- if ((isinstance(server, str) and server) or (isinstance(server := key.get("server"), str) and server)):
90
- if server := normalize_server(server):
91
- self._key["server"] = self._server = server
92
- if not self._key:
93
- raise Exception("Portal init error; from key.")
94
-
95
- def init_from_key_pair(key_pair: tuple, server: Optional[str], unspecified: Optional[list] = []) -> None:
96
- if len(key_pair) == 2:
97
- init_from_key({"key": key_pair[0], "secret": key_pair[1]}, server, unspecified)
98
- else:
99
- raise Exception("Portal init error; from key-pair.")
100
-
101
- def init_from_keys_file(keys_file: str, env: Optional[str], server: Optional[str],
102
- unspecified: Optional[list] = []) -> None:
103
- try:
104
- with io.open(keys_file) as f:
105
- keys = json.load(f)
106
- except Exception:
107
- raise Exception(f"Portal init error; cannot open keys-file: {keys_file}")
108
- if isinstance(env, str) and env and isinstance(key := keys.get(env), dict):
109
- init_from_key(key, server)
110
- self._keys_file = keys_file
111
- self._env = env
112
- elif isinstance(server, str) and server and (key := [k for k in keys if keys[k].get("server") == server]):
113
- init_from_key(key, server)
114
- self._keys_file = keys_file
115
- elif len(keys) == 1 and (env := next(iter(keys))) and isinstance(key := keys[env], dict) and key:
116
- init_from_key(key, server)
117
- self._keys_file = keys_file
118
- self._env = env
119
- else:
120
- raise Exception(f"Portal init error; {env or server or None} not found in keys-file: {keys_file}")
121
-
122
- def init_from_env_server_app(env: str, server: str, app: Optional[str],
123
- unspecified: Optional[list] = None) -> None:
124
- return init_from_keys_file(self._default_keys_file(app, env), env, server, unspecified)
125
-
126
- def normalize_server(server: str) -> Optional[str]:
127
- prefix = ""
128
- if (lserver := server.lower()).startswith("http://"):
129
- prefix = "http://"
130
- elif lserver.startswith("https://"):
131
- prefix = "https://"
132
- if prefix:
133
- if (server := re.sub(r"/+", "/", server[len(prefix):])).startswith("/"):
134
- server = server[1:]
135
- if len(server) > 1 and server.endswith("/"):
136
- server = server[:-1]
137
- return prefix + server if server else None
138
-
139
- if isinstance(arg, Portal):
140
- init_from_portal(arg, unspecified=[env, server, app])
141
- elif isinstance(arg, (TestApp, VirtualApp, Router)):
142
- init_from_vapp(arg, unspecified=[env, server, app])
143
- elif isinstance(arg, str) and arg.endswith(".ini"):
144
- init_from_ini_file(arg, unspecified=[env, server, app])
145
- elif isinstance(arg, dict):
146
- init_from_key(arg, server, unspecified=[env, app])
147
- elif isinstance(arg, tuple):
148
- init_from_key_pair(arg, server, unspecified=[env, app])
149
- elif isinstance(arg, str) and arg.endswith(".json"):
150
- init_from_keys_file(arg, env, server, unspecified=[app])
151
- elif isinstance(arg, str) and arg:
152
- init_from_env_server_app(arg, server, app, unspecified=[env])
153
- elif isinstance(env, str) and env:
154
- init_from_env_server_app(env, server, app, unspecified=[arg])
155
- else:
156
- raise Exception("Portal init error; invalid args.")
157
-
158
- @property
159
- def ini_file(self) -> Optional[str]:
160
- return self._ini_file
161
-
162
- @property
163
- def key(self) -> Optional[dict]:
164
- return self._key
165
-
166
- @property
167
- def key_pair(self) -> Optional[tuple]:
168
- return self._key_pair
62
+ self._server = portal._server
63
+ self._key = portal._key
64
+ self._key_pair = portal._key_pair
65
+ self._key_file = portal._key_file
66
+ return
67
+ self._vapp = None
68
+ self._env = env
69
+ self._app = app
70
+ self._server = server
71
+ self._key = None
72
+ self._key_pair = None
73
+ self._key_file = None
74
+ if isinstance(portal, (VirtualApp, TestApp)):
75
+ self._vapp = portal
76
+ elif isinstance(portal, (Router, str)):
77
+ self._vapp = Portal._create_vapp(portal)
78
+ elif isinstance(key, dict):
79
+ self._key = key
80
+ self._key_pair = (key.get("key"), key.get("secret")) if key else None
81
+ if key_server := key.get("server"):
82
+ self._server = key_server
83
+ elif isinstance(key, tuple) and len(key) >= 2:
84
+ self._key = {"key": key[0], "secret": key[1]}
85
+ self._key_pair = key
86
+ elif isinstance(env, str):
87
+ key_managers = {APP_CGAP: CGAPKeyManager, APP_FOURFRONT: FourfrontKeyManager, APP_SMAHT: SMaHTKeyManager}
88
+ if not (key_manager := key_managers.get(self._app)) or not (key_manager := key_manager()):
89
+ raise Exception(f"Invalid app name: {self._app} (valid: {', '.join(ORCHESTRATED_APPS)}).")
90
+ if isinstance(env, str):
91
+ self._key = key_manager.get_keydict_for_env(env)
92
+ if key_server := self._key.get("server"):
93
+ self._server = key_server
94
+ elif isinstance(self._server, str):
95
+ self._key = key_manager.get_keydict_for_server(self._server)
96
+ self._key_pair = key_manager.keydict_to_keypair(self._key) if self._key else None
97
+ self._key_file = key_manager.keys_file
169
98
 
170
99
  @property
171
- def key_id(self) -> Optional[str]:
172
- return self._key_id
100
+ def env(self):
101
+ return self._env
173
102
 
174
103
  @property
175
- def secret(self) -> Optional[str]:
176
- return self._secret
104
+ def app(self):
105
+ return self._app
177
106
 
178
107
  @property
179
- def keys_file(self) -> Optional[str]:
180
- return self._keys_file
108
+ def server(self):
109
+ return self._server
181
110
 
182
111
  @property
183
- def env(self) -> Optional[str]:
184
- return self._env
112
+ def key(self):
113
+ return self._key
185
114
 
186
115
  @property
187
- def server(self) -> Optional[str]:
188
- return self._server
116
+ def key_pair(self):
117
+ return self._key_pair
189
118
 
190
119
  @property
191
- def app(self) -> Optional[str]:
192
- return self._app
120
+ def key_file(self):
121
+ return self._key_file
193
122
 
194
123
  @property
195
- def vapp(self) -> Optional[TestApp]:
124
+ def vapp(self):
196
125
  return self._vapp
197
126
 
198
127
  def get_metadata(self, object_id: str) -> Optional[dict]:
@@ -209,27 +138,27 @@ class Portal:
209
138
  return self.post(f"/{object_type}", data)
210
139
 
211
140
  def get(self, uri: str, follow: bool = True, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
212
- if self._vapp:
213
- response = self._vapp.get(self.url(uri), **self._kwargs(**kwargs))
141
+ if isinstance(self._vapp, (VirtualApp, TestApp)):
142
+ response = self._vapp.get(self._uri(uri), **self._kwargs(**kwargs))
214
143
  if response and response.status_code in [301, 302, 303, 307, 308] and follow:
215
144
  response = response.follow()
216
145
  return self._response(response)
217
- return requests.get(self.url(uri), allow_redirects=follow, **self._kwargs(**kwargs))
146
+ return requests.get(self._uri(uri), allow_redirects=follow, **self._kwargs(**kwargs))
218
147
 
219
148
  def patch(self, uri: str, data: Optional[dict] = None,
220
149
  json: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
221
- if self._vapp:
222
- return self._vapp.patch_json(self.url(uri), json or data, **self._kwargs(**kwargs))
223
- return requests.patch(self.url(uri), data=data, json=json, **self._kwargs(**kwargs))
150
+ if isinstance(self._vapp, (VirtualApp, TestApp)):
151
+ return self._vapp.patch_json(self._uri(uri), json or data, **self._kwargs(**kwargs))
152
+ return requests.patch(self._uri(uri), json=json or data, **self._kwargs(**kwargs))
224
153
 
225
154
  def post(self, uri: str, data: Optional[dict] = None, json: Optional[dict] = None,
226
155
  files: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
227
- if self._vapp:
156
+ if isinstance(self._vapp, (VirtualApp, TestApp)):
228
157
  if files:
229
- return self._vapp.post(self.url(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
158
+ return self._vapp.post(self._uri(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
230
159
  else:
231
- return self._vapp.post_json(self.url(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
232
- return requests.post(self.url(uri), data=data, json=json, files=files, **self._kwargs(**kwargs))
160
+ return self._vapp.post_json(self._uri(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
161
+ return requests.post(self._uri(uri), json=json or data, files=files, **self._kwargs(**kwargs))
233
162
 
234
163
  def get_schema(self, schema_name: str) -> Optional[dict]:
235
164
  return get_schema(self.schema_name(schema_name), portal_vapp=self._vapp, key=self._key)
@@ -243,7 +172,7 @@ class Portal:
243
172
 
244
173
  def is_file_schema(self, schema_name: str) -> bool:
245
174
  if super_type_map := self.get_schemas_super_type_map():
246
- if file_super_type := super_type_map.get(Portal.FILE_SCHEMA_NAME):
175
+ if file_super_type := super_type_map.get(FILE_SCHEMA_NAME):
247
176
  return self.schema_name(schema_name) in file_super_type
248
177
  return False
249
178
 
@@ -277,20 +206,13 @@ class Portal:
277
206
  super_type_map_flattened[super_type_name] = breadth_first(super_type_map, super_type_name)
278
207
  return super_type_map_flattened
279
208
 
280
- def ping(self) -> bool:
281
- try:
282
- return self.get("/health").status_code == 200
283
- except Exception:
284
- return False
285
-
286
- def url(self, uri: str) -> str:
209
+ def _uri(self, uri: str) -> str:
287
210
  if not isinstance(uri, str) or not uri:
288
211
  return "/"
289
- if (luri := uri.lower()).startswith("http://") or luri.startswith("https://"):
212
+ if uri.lower().startswith("http://") or uri.lower().startswith("https://"):
290
213
  return uri
291
- if not (uri := re.sub(r"/+", "/", uri)).startswith("/"):
292
- uri = "/"
293
- return self._server + uri if self._server else uri
214
+ uri = re.sub(r"/+", "/", uri)
215
+ return (self._server + ("/" if uri.startswith("/") else "") + uri) if self._server else uri
294
216
 
295
217
  def _kwargs(self, **kwargs) -> dict:
296
218
  result_kwargs = {"headers":
@@ -301,16 +223,6 @@ class Portal:
301
223
  result_kwargs["timeout"] = timeout
302
224
  return result_kwargs
303
225
 
304
- def _default_keys_file(self, app: Optional[str], env: Optional[str] = None) -> Optional[str]:
305
- def is_valid_app(app: Optional[str]) -> bool: # noqa
306
- return app and app.lower() in [name.lower() for name in ORCHESTRATED_APPS]
307
- def infer_app_from_env(env: str) -> Optional[str]: # noqa
308
- if isinstance(env, str) and (lenv := env.lower()):
309
- if app := [app for app in ORCHESTRATED_APPS if lenv.startswith(app.lower())]:
310
- return app[0]
311
- if is_valid_app(app) or (app := infer_app_from_env(env)):
312
- return os.path.join(Portal.KEYS_FILE_DIRECTORY, f".{app.lower()}-keys.json")
313
-
314
226
  def _response(self, response) -> Optional[RequestResponse]:
315
227
  if response and isinstance(getattr(response.__class__, "json"), property):
316
228
  class RequestResponseWrapper: # For consistency change json property to method.
@@ -327,15 +239,15 @@ class Portal:
327
239
  @staticmethod
328
240
  def create_for_testing(ini_file: Optional[str] = None) -> Portal:
329
241
  if isinstance(ini_file, str):
330
- return Portal(Portal._create_testapp(ini_file))
242
+ return Portal(Portal._create_vapp(ini_file))
331
243
  minimal_ini_for_unit_testing = "[app:app]\nuse = egg:encoded\nsqlalchemy.url = postgresql://dummy\n"
332
244
  with temporary_file(content=minimal_ini_for_unit_testing, suffix=".ini") as ini_file:
333
- return Portal(Portal._create_testapp(ini_file))
245
+ return Portal(Portal._create_vapp(ini_file))
334
246
 
335
247
  @staticmethod
336
248
  def create_for_testing_local(ini_file: Optional[str] = None) -> Portal:
337
249
  if isinstance(ini_file, str) and ini_file:
338
- return Portal(Portal._create_testapp(ini_file))
250
+ return Portal(Portal._create_vapp(ini_file))
339
251
  minimal_ini_for_testing_local = "\n".join([
340
252
  "[app:app]\nuse = egg:encoded\nfile_upload_bucket = dummy",
341
253
  "sqlalchemy.url = postgresql://postgres@localhost:5441/postgres?host=/tmp/snovault/pgdata",
@@ -356,20 +268,11 @@ class Portal:
356
268
  "multiauth.policy.auth0.base = encoded.authentication.Auth0AuthenticationPolicy"
357
269
  ])
358
270
  with temporary_file(content=minimal_ini_for_testing_local, suffix=".ini") as minimal_ini_file:
359
- return Portal(Portal._create_testapp(minimal_ini_file))
271
+ return Portal(Portal._create_vapp(minimal_ini_file))
360
272
 
361
273
  @staticmethod
362
- def _create_testapp(arg: Union[TestApp, VirtualApp, Router, str] = None, app_name: Optional[str] = None) -> TestApp:
363
- if isinstance(arg, TestApp):
364
- return arg
365
- elif isinstance(arg, VirtualApp):
366
- if not isinstance(arg.wrapped_app, TestApp):
367
- raise Exception("Portal._create_testapp VirtualApp argument error.")
368
- return arg.wrapped_app
369
- if isinstance(arg, Router):
370
- router = arg
371
- elif isinstance(arg, str) or arg is None:
372
- router = get_app(arg or "development.ini", app_name or "app")
373
- else:
374
- raise Exception("Portal._create_testapp argument error.")
375
- return TestApp(router, {"HTTP_ACCEPT": "application/json", "REMOTE_USER": "TEST"})
274
+ def _create_vapp(value: Union[str, Router, TestApp] = "development.ini", app_name: str = "app") -> TestApp:
275
+ if isinstance(value, TestApp):
276
+ return value
277
+ app = value if isinstance(value, Router) else get_app(value, app_name)
278
+ return TestApp(app, {"HTTP_ACCEPT": "application/json", "REMOTE_USER": "TEST"})
@@ -0,0 +1,142 @@
1
+ from typing import Any, Dict, List
2
+
3
+
4
+ class JsonSchemaConstants:
5
+ ANY_OF = "anyOf"
6
+ ARRAY = "array"
7
+ BOOLEAN = "boolean"
8
+ ENUM = "enum"
9
+ INTEGER = "integer"
10
+ ITEMS = "items"
11
+ NUMBER = "number"
12
+ OBJECT = "object"
13
+ ONE_OF = "oneOf"
14
+ PATTERN = "pattern"
15
+ PROPERTIES = "properties"
16
+ REQUIRED = "required"
17
+ STRING = "string"
18
+ TYPE = "type"
19
+
20
+
21
+ class EncodedSchemaConstants:
22
+ DEFAULT = "default"
23
+ FORMAT = "format"
24
+ IDENTIFYING_PROPERTIES = "identifyingProperties"
25
+ LINK_TO = "linkTo"
26
+ MERGE_REF = "$merge"
27
+ MIXIN_PROPERTIES = "mixinProperties"
28
+ REF = "$ref"
29
+ UNIQUE_KEY = "uniqueKey"
30
+
31
+
32
+ class SchemaConstants(JsonSchemaConstants, EncodedSchemaConstants):
33
+ pass
34
+
35
+
36
+ def get_properties(schema: Dict[str, Any]) -> Dict[str, Any]:
37
+ """Return the properties of a schema."""
38
+ return schema.get(SchemaConstants.PROPERTIES, {})
39
+
40
+
41
+ def get_required(schema: Dict[str, Any]) -> List[str]:
42
+ """Return the required properties of a schema."""
43
+ return schema.get(SchemaConstants.REQUIRED, [])
44
+
45
+
46
+ def get_any_of(schema: Dict[str, Any]) -> List[Dict[str, Any]]:
47
+ """Return the anyOf properties of a schema."""
48
+ return schema.get(SchemaConstants.ANY_OF, [])
49
+
50
+
51
+ def get_one_of(schema: Dict[str, Any]) -> List[Dict[str, Any]]:
52
+ """Return the oneOf properties of a schema."""
53
+ return schema.get(SchemaConstants.ONE_OF, [])
54
+
55
+
56
+ def get_conditionally_required_properties(schema: Dict[str, Any]) -> List[str]:
57
+ """Get required + possibly required properties.
58
+
59
+ Using heuristics here; update as needed.
60
+ """
61
+ return sorted(
62
+ list(
63
+ set(
64
+ get_required(schema)
65
+ + get_any_of_required_properties(schema)
66
+ + get_one_of_required_properties(schema)
67
+ )
68
+ )
69
+ )
70
+
71
+
72
+ def get_any_of_required_properties(schema: Dict[str, Any]) -> List[str]:
73
+ """Get required properties from anyOf."""
74
+ return [
75
+ property_name
76
+ for any_of_schema in get_any_of(schema)
77
+ for property_name in get_required(any_of_schema)
78
+ ]
79
+
80
+
81
+ def get_one_of_required_properties(schema: Dict[str, Any]) -> List[str]:
82
+ """Get required properties from oneOf."""
83
+ return [
84
+ property_name
85
+ for one_of_schema in get_one_of(schema)
86
+ for property_name in get_required(one_of_schema)
87
+ ]
88
+
89
+
90
+ def get_mixin_properties(schema: Dict[str, Any]) -> List[Dict[str, Any]]:
91
+ """Return the mixin properties of a schema."""
92
+ return schema.get(EncodedSchemaConstants.MIXIN_PROPERTIES, [])
93
+
94
+
95
+ def get_identifying_properties(schema: Dict[str, Any]) -> List[str]:
96
+ """Return the identifying properties of a schema."""
97
+ return schema.get(EncodedSchemaConstants.IDENTIFYING_PROPERTIES, [])
98
+
99
+
100
+ def get_schema_type(schema: Dict[str, Any]) -> str:
101
+ """Return the type of a schema."""
102
+ return schema.get(SchemaConstants.TYPE, "")
103
+
104
+
105
+ def is_array_schema(schema: Dict[str, Any]) -> bool:
106
+ """Return True if the schema is an array."""
107
+ return get_schema_type(schema) == SchemaConstants.ARRAY
108
+
109
+
110
+ def is_object_schema(schema: Dict[str, Any]) -> bool:
111
+ """Return True if the schema is an object."""
112
+ return get_schema_type(schema) == SchemaConstants.OBJECT
113
+
114
+
115
+ def is_string_schema(schema: Dict[str, Any]) -> bool:
116
+ """Return True if the schema is a string."""
117
+ return get_schema_type(schema) == SchemaConstants.STRING
118
+
119
+
120
+ def is_number_schema(schema: Dict[str, Any]) -> bool:
121
+ """Return True if the schema is a number."""
122
+ return get_schema_type(schema) == SchemaConstants.NUMBER
123
+
124
+
125
+ def is_integer_schema(schema: Dict[str, Any]) -> bool:
126
+ """Return True if the schema is an integer."""
127
+ return get_schema_type(schema) == SchemaConstants.INTEGER
128
+
129
+
130
+ def is_boolean_schema(schema: Dict[str, Any]) -> bool:
131
+ """Return True if the schema is a boolean."""
132
+ return get_schema_type(schema) == SchemaConstants.BOOLEAN
133
+
134
+
135
+ def get_items(schema: Dict[str, Any]) -> Dict[str, Any]:
136
+ """Return the items of a schema."""
137
+ return schema.get(SchemaConstants.ITEMS, {})
138
+
139
+
140
+ def has_property(schema: Dict[str, Any], property_name: str) -> bool:
141
+ """Return True if the schema has the given property."""
142
+ return property_name in get_properties(schema)
@@ -42,24 +42,23 @@ StructuredDataSet = Type["StructuredDataSet"]
42
42
  class StructuredDataSet:
43
43
 
44
44
  def __init__(self, file: Optional[str] = None, portal: Optional[Union[VirtualApp, TestApp, Portal]] = None,
45
- schemas: Optional[List[dict]] = None, autoadd: Optional[dict] = None,
45
+ schemas: Optional[List[dict]] = None, data: Optional[List[dict]] = None,
46
46
  order: Optional[List[str]] = None, prune: bool = True) -> None:
47
- self.data = {}
47
+ self.data = {} if not data else data # If portal is None then no schemas nor refs.
48
48
  self._portal = Portal(portal, data=self.data, schemas=schemas) if portal else None
49
49
  self._order = order
50
50
  self._prune = prune
51
51
  self._warnings = {}
52
52
  self._errors = {}
53
- self._resolved_refs = set()
53
+ self._resolved_refs = []
54
54
  self._validated = False
55
- self._autoadd_properties = autoadd if isinstance(autoadd, dict) and autoadd else None
56
55
  self._load_file(file) if file else None
57
56
 
58
57
  @staticmethod
59
58
  def load(file: str, portal: Optional[Union[VirtualApp, TestApp, Portal]] = None,
60
- schemas: Optional[List[dict]] = None, autoadd: Optional[dict] = None,
59
+ schemas: Optional[List[dict]] = None,
61
60
  order: Optional[List[str]] = None, prune: bool = True) -> StructuredDataSet:
62
- return StructuredDataSet(file=file, portal=portal, schemas=schemas, autoadd=autoadd, order=order, prune=prune)
61
+ return StructuredDataSet(file=file, portal=portal, schemas=schemas, order=order, prune=prune)
63
62
 
64
63
  def validate(self, force: bool = False) -> None:
65
64
  if self._validated and not force:
@@ -97,7 +96,7 @@ class StructuredDataSet:
97
96
 
98
97
  @property
99
98
  def resolved_refs(self) -> List[str]:
100
- return list(self._resolved_refs)
99
+ return self._resolved_refs
101
100
 
102
101
  @property
103
102
  def upload_files(self) -> List[str]:
@@ -113,7 +112,7 @@ class StructuredDataSet:
113
112
  def _load_file(self, file: str) -> None:
114
113
  # Returns a dictionary where each property is the name (i.e. the type) of the data,
115
114
  # and the value is array of dictionaries for the data itself. Handle these kinds of files:
116
- # 1. Single CSV, TSV, or JSON file, where the (base) name of the file is the data type name.
115
+ # 1. Single CSV of JSON file, where the (base) name of the file is the data type name.
117
116
  # 2. Single Excel file containing one or more sheets, where each sheet
118
117
  # represents (i.e. is named for, and contains data for) a different type.
119
118
  # 3. Zip file (.zip or .tar.gz or .tgz or .tar), containing data files to load, where the
@@ -164,13 +163,11 @@ class StructuredDataSet:
164
163
  structured_row = structured_row_template.create_row()
165
164
  for column_name, value in row.items():
166
165
  structured_row_template.set_value(structured_row, column_name, value, reader.file, reader.row_number)
167
- if self._autoadd_properties:
168
- self._add_properties(structured_row, self._autoadd_properties, schema)
169
166
  self._add(type_name, structured_row)
170
167
  self._note_warning(reader.warnings, "reader")
171
168
  if schema:
172
169
  self._note_error(schema._unresolved_refs, "ref")
173
- self._resolved_refs.update(schema._resolved_refs)
170
+ self._resolved_refs = schema._resolved_refs
174
171
 
175
172
  def _add(self, type_name: str, data: Union[dict, List[dict]]) -> None:
176
173
  if self._prune:
@@ -180,11 +177,6 @@ class StructuredDataSet:
180
177
  else:
181
178
  self.data[type_name] = [data] if isinstance(data, dict) else data
182
179
 
183
- def _add_properties(self, structured_row: dict, properties: dict, schema: Optional[dict] = None) -> None:
184
- for name in properties:
185
- if name not in structured_row and (not schema or schema.data.get("properties", {}).get(name)):
186
- structured_row[name] = properties[name]
187
-
188
180
  def _note_warning(self, item: Optional[Union[dict, List[dict]]], group: str) -> None:
189
181
  self._note_issue(self._warnings, item, group)
190
182
 
@@ -245,7 +237,7 @@ class _StructuredRowTemplate:
245
237
  return {array_name: array} if array_name else {column_component: value}
246
238
 
247
239
  def set_value_internal(data: Union[dict, list], value: Optional[Any], src: Optional[str],
248
- path: List[Union[str, int]], typeinfo: Optional[dict], mapv: Optional[Callable]) -> None:
240
+ path: List[Union[str, int]], mapv: Optional[Callable]) -> None:
249
241
 
250
242
  def set_value_backtrack_object(path_index: int, path_element: str) -> None:
251
243
  nonlocal data, path, original_data
@@ -265,7 +257,7 @@ class _StructuredRowTemplate:
265
257
  set_value_backtrack_object(i, p)
266
258
  data = data[p]
267
259
  if (p := path[-1]) == -1 and isinstance(value, str):
268
- values = _split_array_string(value, unique=typeinfo.get("unique") if typeinfo else False)
260
+ values = _split_array_string(value)
269
261
  if mapv:
270
262
  values = [mapv(value, src) for value in values]
271
263
  merge_objects(data, values)
@@ -296,13 +288,11 @@ class _StructuredRowTemplate:
296
288
  for column_name in column_names or []:
297
289
  ensure_column_consistency(column_name)
298
290
  rational_column_name = self._schema.rationalize_column_name(column_name) if self._schema else column_name
299
- column_typeinfo = self._schema.get_typeinfo(rational_column_name) if self._schema else None
300
- map_value_function = column_typeinfo.get("map") if column_typeinfo else None
291
+ map_value_function = self._schema.get_map_value_function(rational_column_name) if self._schema else None
301
292
  if (column_components := _split_dotted_string(rational_column_name)):
302
293
  merge_objects(structured_row_template, parse_components(column_components, path := []), True)
303
- self._set_value_functions[column_name] = (
304
- lambda data, value, src, path=path, typeinfo=column_typeinfo, mapv=map_value_function:
305
- set_value_internal(data, value, src, path, typeinfo, mapv))
294
+ self._set_value_functions[column_name] = (lambda data, value, src, path=path, mapv=map_value_function:
295
+ set_value_internal(data, value, src, path, mapv))
306
296
  return structured_row_template
307
297
 
308
298
 
@@ -325,8 +315,7 @@ class Schema:
325
315
 
326
316
  @staticmethod
327
317
  def load_by_name(name: str, portal: Portal) -> Optional[dict]:
328
- schema_json = portal.get_schema(Schema.type_name(name)) if portal else None
329
- return Schema(schema_json, portal) if schema_json else None
318
+ return Schema(portal.get_schema(Schema.type_name(name)), portal) if portal else None
330
319
 
331
320
  def validate(self, data: dict) -> List[str]:
332
321
  errors = []
@@ -342,7 +331,10 @@ class Schema:
342
331
  def resolved_refs(self) -> List[str]:
343
332
  return list(self._resolved_refs)
344
333
 
345
- def get_typeinfo(self, column_name: str) -> Optional[dict]:
334
+ def get_map_value_function(self, column_name: str) -> Optional[Any]:
335
+ return (self._get_typeinfo(column_name) or {}).get("map")
336
+
337
+ def _get_typeinfo(self, column_name: str) -> Optional[dict]:
346
338
  if isinstance(info := self._typeinfo.get(column_name), str):
347
339
  info = self._typeinfo.get(info)
348
340
  if not info and isinstance(info := self._typeinfo.get(self.unadorn_column_name(column_name)), str):
@@ -475,14 +467,9 @@ class Schema:
475
467
  raise Exception(f"Array of undefined or multiple types in JSON schema NOT supported: {key}")
476
468
  raise Exception(f"Invalid array type specifier in JSON schema: {key}")
477
469
  key = key + ARRAY_NAME_SUFFIX_CHAR
478
- if unique := (property_value.get("uniqueItems") is True):
479
- pass
480
470
  property_value = array_property_items
481
471
  property_value_type = property_value.get("type")
482
- typeinfo = self._create_typeinfo(array_property_items, parent_key=key)
483
- if unique:
484
- typeinfo[key]["unique"] = True
485
- result.update(typeinfo)
472
+ result.update(self._create_typeinfo(array_property_items, parent_key=key))
486
473
  continue
487
474
  result[key] = {"type": property_value_type, "map": self._map_function({**property_value, "column": key})}
488
475
  if ARRAY_NAME_SUFFIX_CHAR in key:
@@ -550,13 +537,16 @@ class Portal(PortalBase):
550
537
 
551
538
  def __init__(self,
552
539
  arg: Optional[Union[VirtualApp, TestApp, Router, Portal, dict, tuple, str]] = None,
553
- env: Optional[str] = None, server: Optional[str] = None,
554
- app: Optional[OrchestratedApp] = None,
555
- data: Optional[dict] = None, schemas: Optional[List[dict]] = None) -> None:
556
- super().__init__(arg, env=env, server=server, app=app)
557
- if isinstance(arg, Portal):
558
- self._schemas = schemas if schemas is not None else arg._schemas # Explicitly specified/known schemas.
559
- self._data = data if data is not None else arg._data # Data set being loaded; e.g. by StructuredDataSet.
540
+ env: Optional[str] = None, app: OrchestratedApp = None, server: Optional[str] = None,
541
+ key: Optional[Union[dict, tuple]] = None,
542
+ portal: Optional[Union[VirtualApp, TestApp, Router, Portal, str]] = None,
543
+ data: Optional[dict] = None, schemas: Optional[List[dict]] = None) -> Optional[Portal]:
544
+ super().__init__(arg, env=env, app=app, server=server, key=key, portal=portal)
545
+ if isinstance(arg, Portal) and not portal:
546
+ portal = arg
547
+ if isinstance(portal, Portal):
548
+ self._schemas = schemas if schemas is not None else portal._schemas # Explicitly specified/known schemas.
549
+ self._data = data if data is not None else portal._data # Data set being loaded; e.g. by StructuredDataSet.
560
550
  else:
561
551
  self._schemas = schemas
562
552
  self._data = data
@@ -570,9 +560,7 @@ class Portal(PortalBase):
570
560
 
571
561
  @lru_cache(maxsize=256)
572
562
  def get_schema(self, schema_name: str) -> Optional[dict]:
573
- if not (schemas := self.get_schemas()):
574
- return None
575
- if schema := schemas.get(schema_name := Schema.type_name(schema_name)):
563
+ if (schemas := self.get_schemas()) and (schema := schemas.get(schema_name := Schema.type_name(schema_name))):
576
564
  return schema
577
565
  if schema_name == schema_name.upper() and (schema := schemas.get(schema_name.lower().title())):
578
566
  return schema
@@ -580,9 +568,8 @@ class Portal(PortalBase):
580
568
  return schema
581
569
 
582
570
  @lru_cache(maxsize=1)
583
- def get_schemas(self) -> Optional[dict]:
584
- if not (schemas := super().get_schemas()) or (schemas.get("status") == "error"):
585
- return None
571
+ def get_schemas(self) -> dict:
572
+ schemas = super().get_schemas()
586
573
  if self._schemas:
587
574
  schemas = copy.deepcopy(schemas)
588
575
  for user_specified_schema in self._schemas:
@@ -628,5 +615,5 @@ def _split_dotted_string(value: str):
628
615
  return split_string(value, DOTTED_NAME_DELIMITER_CHAR)
629
616
 
630
617
 
631
- def _split_array_string(value: str, unique: bool = False):
632
- return split_string(value, ARRAY_VALUE_DELIMITER_CHAR, ARRAY_VALUE_DELIMITER_ESCAPE_CHAR, unique=unique)
618
+ def _split_array_string(value: str):
619
+ return split_string(value, ARRAY_VALUE_DELIMITER_CHAR, ARRAY_VALUE_DELIMITER_ESCAPE_CHAR)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcicutils
3
- Version: 8.5.0.1b6
3
+ Version: 8.6.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
@@ -40,30 +40,31 @@ dcicutils/license_policies/park-lab-gpl-pipeline.jsonc,sha256=vLZkwm3Js-kjV44nug
40
40
  dcicutils/license_policies/park-lab-pipeline.jsonc,sha256=9qlY0ASy3iUMQlr3gorVcXrSfRHnVGbLhkS427UaRy4,283
41
41
  dcicutils/license_utils.py,sha256=d1cq6iwv5Ju-VjdoINi6q7CPNNL7Oz6rcJdLMY38RX0,46978
42
42
  dcicutils/log_utils.py,sha256=7pWMc6vyrorUZQf-V-M3YC6zrPgNhuV_fzm9xqTPph0,10883
43
- dcicutils/misc_utils.py,sha256=nRjLEORY35YmJwTjO0fnauBPznaI_bkVasIW8PccDYM,100179
43
+ dcicutils/misc_utils.py,sha256=slnJM87VrPj2gCiQR8Yrl8bwTjeynXpM1yKIjJg8NN4,99816
44
44
  dcicutils/obfuscation_utils.py,sha256=fo2jOmDRC6xWpYX49u80bVNisqRRoPskFNX3ymFAmjw,5963
45
45
  dcicutils/opensearch_utils.py,sha256=V2exmFYW8Xl2_pGFixF4I2Cc549Opwe4PhFi5twC0M8,1017
46
- dcicutils/portal_utils.py,sha256=PLIKONo_BhVrf-r5fkXAaxP5IQZRjvPjf83YxvRx0ZE,18393
46
+ dcicutils/portal_utils.py,sha256=OF1JPoDgEbvCvPKz0egcnmqGEOYWkamuBLRN_RpZFtk,14073
47
47
  dcicutils/project_utils.py,sha256=qPdCaFmWUVBJw4rw342iUytwdQC0P-XKpK4mhyIulMM,31250
48
48
  dcicutils/qa_checkers.py,sha256=cdXjeL0jCDFDLT8VR8Px78aS10hwNISOO5G_Zv2TZ6M,20534
49
49
  dcicutils/qa_utils.py,sha256=TT0SiJWiuxYvbsIyhK9VO4uV_suxhB6CpuC4qPacCzQ,160208
50
50
  dcicutils/redis_tools.py,sha256=qkcSNMtvqkpvts-Cm9gWhneK523Q_oHwhNUud1be1qk,7055
51
51
  dcicutils/redis_utils.py,sha256=VJ-7g8pOZqR1ZCtdcjKz3-6as2DMUcs1b1zG6wSprH4,6462
52
52
  dcicutils/s3_utils.py,sha256=eN8lfDI8Yxqga7iuy_eOlmUJUEjewHho0szLmj2YmYI,28852
53
+ dcicutils/schema_utils.py,sha256=xlvY7BuIhjJYk7GKYamhbHDw1751rmYcEM2pOaEH1BY,4203
53
54
  dcicutils/scripts/publish_to_pypi.py,sha256=qmWyjrg5bNQNfpNKFTZdyMXpRmrECnRV9VmNQddUPQA,13576
54
55
  dcicutils/scripts/run_license_checker.py,sha256=z2keYnRDZsHQbTeo1XORAXSXNJK5axVzL5LjiNqZ7jE,4184
55
56
  dcicutils/secrets_utils.py,sha256=8dppXAsiHhJzI6NmOcvJV5ldvKkQZzh3Fl-cb8Wm7MI,19745
56
57
  dcicutils/sheet_utils.py,sha256=VlmzteONW5VF_Q4vo0yA5vesz1ViUah1MZ_yA1rwZ0M,33629
57
58
  dcicutils/snapshot_utils.py,sha256=ymP7PXH6-yEiXAt75w0ldQFciGNqWBClNxC5gfX2FnY,22961
58
59
  dcicutils/ssl_certificate_utils.py,sha256=F0ifz_wnRRN9dfrfsz7aCp4UDLgHEY8LaK7PjnNvrAQ,9707
59
- dcicutils/structured_data.py,sha256=mWjH6h9ARUvYMHI4ZiwbaoKtoeM0CAOz1UxCHh4HIHE,32874
60
+ dcicutils/structured_data.py,sha256=bOzashl_cy5c8NWewcPpRPknQDbSPstL_3Cw2-KUSl0,32112
60
61
  dcicutils/task_utils.py,sha256=MF8ujmTD6-O2AC2gRGPHyGdUrVKgtr8epT5XU8WtNjk,8082
61
62
  dcicutils/trace_utils.py,sha256=g8kwV4ebEy5kXW6oOrEAUsurBcCROvwtZqz9fczsGRE,1769
62
63
  dcicutils/validation_utils.py,sha256=cMZIU2cY98FYtzK52z5WUYck7urH6JcqOuz9jkXpqzg,14797
63
64
  dcicutils/variant_utils.py,sha256=2H9azNx3xAj-MySg-uZ2SFqbWs4kZvf61JnK6b-h4Qw,4343
64
65
  dcicutils/zip_utils.py,sha256=0OXR0aLNwyLIZOzIFTM_5DOun7dxIv6TIZbFiithkO0,3276
65
- dcicutils-8.5.0.1b6.dist-info/LICENSE.txt,sha256=t0_-jIjqxNnymZoNJe-OltRIuuF8qfhN0ATlHyrUJPk,1102
66
- dcicutils-8.5.0.1b6.dist-info/METADATA,sha256=wWiZixknI6Pne33sGS-Lxeh6hSKSScj44tPcctlpHJw,3314
67
- dcicutils-8.5.0.1b6.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
68
- dcicutils-8.5.0.1b6.dist-info/entry_points.txt,sha256=8wbw5csMIgBXhkwfgsgJeuFcoUc0WsucUxmOyml2aoA,209
69
- dcicutils-8.5.0.1b6.dist-info/RECORD,,
66
+ dcicutils-8.6.0.0b0.dist-info/LICENSE.txt,sha256=t0_-jIjqxNnymZoNJe-OltRIuuF8qfhN0ATlHyrUJPk,1102
67
+ dcicutils-8.6.0.0b0.dist-info/METADATA,sha256=Wbkp0zSdhveaLePg7P-jFS06WfiHGMn30IQq2RxhN6g,3314
68
+ dcicutils-8.6.0.0b0.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
69
+ dcicutils-8.6.0.0b0.dist-info/entry_points.txt,sha256=8wbw5csMIgBXhkwfgsgJeuFcoUc0WsucUxmOyml2aoA,209
70
+ dcicutils-8.6.0.0b0.dist-info/RECORD,,