dcicutils 8.5.0.1b6__py3-none-any.whl → 8.6.0.0b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dcicutils/misc_utils.py +7 -16
- dcicutils/portal_utils.py +106 -203
- dcicutils/schema_utils.py +142 -0
- dcicutils/structured_data.py +34 -47
- {dcicutils-8.5.0.1b6.dist-info → dcicutils-8.6.0.0b0.dist-info}/METADATA +1 -1
- {dcicutils-8.5.0.1b6.dist-info → dcicutils-8.6.0.0b0.dist-info}/RECORD +9 -8
- {dcicutils-8.5.0.1b6.dist-info → dcicutils-8.6.0.0b0.dist-info}/LICENSE.txt +0 -0
- {dcicutils-8.5.0.1b6.dist-info → dcicutils-8.6.0.0b0.dist-info}/WHEEL +0 -0
- {dcicutils-8.5.0.1b6.dist-info → dcicutils-8.6.0.0b0.dist-info}/entry_points.txt +0 -0
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
1497
|
-
|
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
|
28
|
-
and with a dictionary value containing "key" and "secret" property values
|
29
|
-
|
30
|
-
will be used, i.e. e.g. if
|
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
|
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
|
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[
|
41
|
-
env: Optional[str] = None, server: Optional[str] = None,
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
self.
|
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.
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
if
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
172
|
-
return self.
|
100
|
+
def env(self):
|
101
|
+
return self._env
|
173
102
|
|
174
103
|
@property
|
175
|
-
def
|
176
|
-
return self.
|
104
|
+
def app(self):
|
105
|
+
return self._app
|
177
106
|
|
178
107
|
@property
|
179
|
-
def
|
180
|
-
return self.
|
108
|
+
def server(self):
|
109
|
+
return self._server
|
181
110
|
|
182
111
|
@property
|
183
|
-
def
|
184
|
-
return self.
|
112
|
+
def key(self):
|
113
|
+
return self._key
|
185
114
|
|
186
115
|
@property
|
187
|
-
def
|
188
|
-
return self.
|
116
|
+
def key_pair(self):
|
117
|
+
return self._key_pair
|
189
118
|
|
190
119
|
@property
|
191
|
-
def
|
192
|
-
return self.
|
120
|
+
def key_file(self):
|
121
|
+
return self._key_file
|
193
122
|
|
194
123
|
@property
|
195
|
-
def vapp(self)
|
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.
|
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.
|
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.
|
223
|
-
return requests.patch(self.
|
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.
|
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.
|
232
|
-
return requests.post(self.
|
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(
|
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
|
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
|
212
|
+
if uri.lower().startswith("http://") or uri.lower().startswith("https://"):
|
290
213
|
return uri
|
291
|
-
|
292
|
-
|
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.
|
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.
|
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.
|
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.
|
271
|
+
return Portal(Portal._create_vapp(minimal_ini_file))
|
360
272
|
|
361
273
|
@staticmethod
|
362
|
-
def
|
363
|
-
if isinstance(
|
364
|
-
return
|
365
|
-
|
366
|
-
|
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)
|
dcicutils/structured_data.py
CHANGED
@@ -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,
|
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 =
|
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,
|
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,
|
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
|
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
|
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
|
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]],
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
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
|
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) ->
|
584
|
-
|
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
|
632
|
-
return split_string(value, ARRAY_VALUE_DELIMITER_CHAR, ARRAY_VALUE_DELIMITER_ESCAPE_CHAR
|
618
|
+
def _split_array_string(value: str):
|
619
|
+
return split_string(value, ARRAY_VALUE_DELIMITER_CHAR, ARRAY_VALUE_DELIMITER_ESCAPE_CHAR)
|
@@ -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=
|
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=
|
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=
|
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.
|
66
|
-
dcicutils-8.
|
67
|
-
dcicutils-8.
|
68
|
-
dcicutils-8.
|
69
|
-
dcicutils-8.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|