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 +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
|