dcicutils 8.5.0.1b4__py3-none-any.whl → 8.5.0.1b7__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
dcicutils/portal_utils.py CHANGED
@@ -1,19 +1,24 @@
1
1
  from collections import deque
2
- from pyramid.paster import get_app
3
- from pyramid.router import Router
2
+ import io
3
+ import json
4
+ from pyramid.config import Configurator as PyramidConfigurator
5
+ from pyramid.paster import get_app as pyramid_get_app
6
+ from pyramid.response import Response as PyramidResponse
7
+ from pyramid.router import Router as PyramidRouter
8
+ import os
4
9
  import re
5
10
  import requests
6
11
  from requests.models import Response as RequestResponse
7
- from typing import Optional, Type, Union
12
+ from typing import Callable, Dict, List, Optional, Type, Union
13
+ from uuid import uuid4 as uuid
14
+ # from waitress import serve
8
15
  from webtest.app import TestApp, TestResponse
9
- from dcicutils.common import OrchestratedApp, APP_CGAP, APP_FOURFRONT, APP_SMAHT, ORCHESTRATED_APPS
10
- from dcicutils.creds_utils import CGAPKeyManager, FourfrontKeyManager, SMaHTKeyManager
16
+ from dcicutils.common import OrchestratedApp, ORCHESTRATED_APPS
11
17
  from dcicutils.ff_utils import get_metadata, get_schema, patch_metadata, post_metadata
12
18
  from dcicutils.misc_utils import to_camel_case, VirtualApp
13
19
  from dcicutils.zip_utils import temporary_file
14
20
 
15
21
  Portal = Type["Portal"] # Forward type reference for type hints.
16
- FILE_SCHEMA_NAME = "File"
17
22
 
18
23
 
19
24
  class Portal:
@@ -32,105 +37,166 @@ class Portal:
32
37
  6. From a given "vapp" value (which is assumed to be a TestApp or VirtualApp).
33
38
  7. From another Portal object; or from a a pyramid Router object.
34
39
  """
40
+ FILE_SCHEMA_NAME = "File"
41
+ KEYS_FILE_DIRECTORY = os.path.expanduser(f"~")
42
+
35
43
  def __init__(self,
36
- arg: Optional[Union[VirtualApp, TestApp, Router, Portal, dict, tuple, str]] = None,
37
- env: Optional[str] = None, app: Optional[OrchestratedApp] = None, server: Optional[str] = None,
38
- key: Optional[Union[dict, tuple]] = None,
39
- vapp: Optional[Union[VirtualApp, TestApp, Router, Portal, str]] = None,
40
- portal: Optional[Union[VirtualApp, TestApp, Router, Portal, str]] = None) -> Portal:
41
- if vapp and not portal:
42
- portal = vapp
43
- if ((isinstance(arg, (VirtualApp, TestApp, Router, Portal)) or
44
- isinstance(arg, str) and arg.endswith(".ini")) and not portal):
45
- portal = arg
46
- elif isinstance(arg, str) and not env:
47
- env = arg
48
- elif (isinstance(arg, dict) or isinstance(arg, tuple)) and not key:
49
- key = arg
50
- if not app and env:
51
- if env.startswith(APP_SMAHT):
52
- app = APP_SMAHT
53
- elif env.startswith(APP_CGAP):
54
- app = APP_CGAP
55
- elif env.startswith(APP_FOURFRONT):
56
- app = APP_FOURFRONT
57
- if isinstance(portal, Portal):
58
- self._vapp = portal._vapp
59
- self._env = portal._env
60
- self._app = portal._app
61
- self._server = portal._server
44
+ arg: Optional[Union[Portal, TestApp, VirtualApp, PyramidRouter, dict, tuple, str]] = None,
45
+ env: Optional[str] = None, server: Optional[str] = None,
46
+ app: Optional[OrchestratedApp] = None) -> None:
47
+
48
+ def init(unspecified: Optional[list] = []) -> None:
49
+ self._ini_file = None
50
+ self._key = None
51
+ self._key_pair = None
52
+ self._key_id = None
53
+ self._secret = None
54
+ self._keys_file = None
55
+ self._env = None
56
+ self._server = None
57
+ self._app = None
58
+ self._vapp = None
59
+ for arg in unspecified:
60
+ if arg is not None:
61
+ raise Exception("Portal init error; extraneous args.")
62
+
63
+ def init_from_portal(portal: Portal, unspecified: Optional[list] = None) -> None:
64
+ init(unspecified)
65
+ self._ini_file = portal._ini_file
62
66
  self._key = portal._key
63
67
  self._key_pair = portal._key_pair
64
68
  self._key_id = portal._key_id
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_id = None
74
- self._key_file = None
75
- if isinstance(portal, (VirtualApp, TestApp)):
76
- self._vapp = portal
77
- elif isinstance(portal, (Router, str)):
78
- self._vapp = Portal._create_vapp(portal)
79
- elif isinstance(key, dict):
80
- self._key = key
81
- self._key_pair = (key.get("key"), key.get("secret")) if key else None
82
- if key_server := key.get("server"):
83
- self._server = key_server
84
- elif isinstance(key, tuple) and len(key) >= 2:
85
- self._key = {"key": key[0], "secret": key[1]}
86
- self._key_pair = key
87
- elif isinstance(env, str):
88
- key_managers = {APP_CGAP: CGAPKeyManager, APP_FOURFRONT: FourfrontKeyManager, APP_SMAHT: SMaHTKeyManager}
89
- if not (key_manager := key_managers.get(self._app)) or not (key_manager := key_manager()):
90
- raise Exception(f"Invalid app name: {self._app} (valid: {', '.join(ORCHESTRATED_APPS)}).")
91
- if isinstance(env, str):
92
- self._key = key_manager.get_keydict_for_env(env)
93
- if key_server := self._key.get("server"):
94
- self._server = key_server
95
- elif isinstance(self._server, str):
96
- self._key = key_manager.get_keydict_for_server(self._server)
97
- self._key_pair = key_manager.keydict_to_keypair(self._key) if self._key else None
98
- self._key_file = key_manager.keys_file
99
- if self._key and (key_id := self._key.get("key")):
100
- self._key_id = key_id
101
- elif self._key_pair and (key_id := self._key_pair[1]):
102
- self._key_id = key_id
69
+ self._secret = portal._secret
70
+ self._keys_file = portal._keys_file
71
+ self._env = portal._env
72
+ self._server = portal._server
73
+ self._app = portal._app
74
+ self._vapp = portal._vapp
103
75
 
104
- @property
105
- def env(self):
106
- return self._env
76
+ def init_from_vapp(vapp: Union[TestApp, VirtualApp, PyramidRouter], unspecified: Optional[list] = []) -> None:
77
+ init(unspecified)
78
+ self._vapp = Portal._create_vapp(vapp)
107
79
 
108
- @property
109
- def app(self):
110
- return self._app
80
+ def init_from_ini_file(ini_file: str, unspecified: Optional[list] = []) -> None:
81
+ init(unspecified)
82
+ self._ini_file = ini_file
83
+ self._vapp = Portal._create_vapp(ini_file)
84
+
85
+ def init_from_key(key: dict, server: Optional[str], unspecified: Optional[list] = []) -> None:
86
+ init(unspecified)
87
+ if (isinstance(key_id := key.get("key"), str) and key_id and
88
+ isinstance(secret := key.get("secret"), str) and secret): # noqa
89
+ self._key = {"key": key_id, "secret": secret}
90
+ self._key_id = key_id
91
+ self._secret = secret
92
+ self._key_pair = (key_id, secret)
93
+ if ((isinstance(server, str) and server) or (isinstance(server := key.get("server"), str) and server)):
94
+ if server := normalize_server(server):
95
+ self._key["server"] = self._server = server
96
+ if not self._key:
97
+ raise Exception("Portal init error; from key.")
98
+
99
+ def init_from_key_pair(key_pair: tuple, server: Optional[str], unspecified: Optional[list] = []) -> None:
100
+ if len(key_pair) == 2:
101
+ init_from_key({"key": key_pair[0], "secret": key_pair[1]}, server, unspecified)
102
+ else:
103
+ raise Exception("Portal init error; from key-pair.")
104
+
105
+ def init_from_keys_file(keys_file: str, env: Optional[str], server: Optional[str],
106
+ unspecified: Optional[list] = []) -> None:
107
+ try:
108
+ with io.open(keys_file) as f:
109
+ keys = json.load(f)
110
+ except Exception:
111
+ raise Exception(f"Portal init error; cannot open keys-file: {keys_file}")
112
+ if isinstance(env, str) and env and isinstance(key := keys.get(env), dict):
113
+ init_from_key(key, server)
114
+ self._keys_file = keys_file
115
+ self._env = env
116
+ elif isinstance(server, str) and server and (key := [k for k in keys if keys[k].get("server") == server]):
117
+ init_from_key(key, server)
118
+ self._keys_file = keys_file
119
+ elif len(keys) == 1 and (env := next(iter(keys))) and isinstance(key := keys[env], dict) and key:
120
+ init_from_key(key, server)
121
+ self._keys_file = keys_file
122
+ self._env = env
123
+ else:
124
+ raise Exception(f"Portal init error; {env or server or None} not found in keys-file: {keys_file}")
125
+
126
+ def init_from_env_server_app(env: str, server: str, app: Optional[str],
127
+ unspecified: Optional[list] = None) -> None:
128
+ return init_from_keys_file(self._default_keys_file(app, env), env, server, unspecified)
129
+
130
+ def normalize_server(server: str) -> Optional[str]:
131
+ prefix = ""
132
+ if (lserver := server.lower()).startswith("http://"):
133
+ prefix = "http://"
134
+ elif lserver.startswith("https://"):
135
+ prefix = "https://"
136
+ if prefix:
137
+ if (server := re.sub(r"/+", "/", server[len(prefix):])).startswith("/"):
138
+ server = server[1:]
139
+ if len(server) > 1 and server.endswith("/"):
140
+ server = server[:-1]
141
+ return prefix + server if server else None
142
+
143
+ if isinstance(arg, Portal):
144
+ init_from_portal(arg, unspecified=[env, server, app])
145
+ elif isinstance(arg, (TestApp, VirtualApp, PyramidRouter)):
146
+ init_from_vapp(arg, unspecified=[env, server, app])
147
+ elif isinstance(arg, str) and arg.endswith(".ini"):
148
+ init_from_ini_file(arg, unspecified=[env, server, app])
149
+ elif isinstance(arg, dict):
150
+ init_from_key(arg, server, unspecified=[env, app])
151
+ elif isinstance(arg, tuple):
152
+ init_from_key_pair(arg, server, unspecified=[env, app])
153
+ elif isinstance(arg, str) and arg.endswith(".json"):
154
+ init_from_keys_file(arg, env, server, unspecified=[app])
155
+ elif isinstance(arg, str) and arg:
156
+ init_from_env_server_app(arg, server, app, unspecified=[env])
157
+ elif isinstance(env, str) and env:
158
+ init_from_env_server_app(env, server, app, unspecified=[arg])
159
+ else:
160
+ raise Exception("Portal init error; invalid args.")
111
161
 
112
162
  @property
113
- def server(self):
114
- return self._server
163
+ def ini_file(self) -> Optional[str]:
164
+ return self._ini_file
115
165
 
116
166
  @property
117
- def key(self):
167
+ def key(self) -> Optional[dict]:
118
168
  return self._key
119
169
 
120
170
  @property
121
- def key_pair(self):
171
+ def key_pair(self) -> Optional[tuple]:
122
172
  return self._key_pair
123
173
 
124
174
  @property
125
- def key_id(self):
175
+ def key_id(self) -> Optional[str]:
126
176
  return self._key_id
127
177
 
128
178
  @property
129
- def key_file(self):
130
- return self._key_file
179
+ def secret(self) -> Optional[str]:
180
+ return self._secret
181
+
182
+ @property
183
+ def keys_file(self) -> Optional[str]:
184
+ return self._keys_file
131
185
 
132
186
  @property
133
- def vapp(self):
187
+ def env(self) -> Optional[str]:
188
+ return self._env
189
+
190
+ @property
191
+ def server(self) -> Optional[str]:
192
+ return self._server
193
+
194
+ @property
195
+ def app(self) -> Optional[str]:
196
+ return self._app
197
+
198
+ @property
199
+ def vapp(self) -> Optional[TestApp]:
134
200
  return self._vapp
135
201
 
136
202
  def get_metadata(self, object_id: str) -> Optional[dict]:
@@ -139,35 +205,36 @@ class Portal:
139
205
  def patch_metadata(self, object_id: str, data: str) -> Optional[dict]:
140
206
  if self._key:
141
207
  return patch_metadata(obj_id=object_id, patch_item=data, key=self._key)
142
- return self.patch(f"/{object_id}", data)
208
+ return self.patch(f"/{object_id}", data).json()
143
209
 
144
210
  def post_metadata(self, object_type: str, data: str) -> Optional[dict]:
145
211
  if self._key:
146
212
  return post_metadata(schema_name=object_type, post_item=data, key=self._key)
147
- return self.post(f"/{object_type}", data)
213
+ return self.post(f"/{object_type}", data).json()
148
214
 
149
215
  def get(self, uri: str, follow: bool = True, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
150
- if isinstance(self._vapp, (VirtualApp, TestApp)):
151
- response = self._vapp.get(self._uri(uri), **self._kwargs(**kwargs))
152
- if response and response.status_code in [301, 302, 303, 307, 308] and follow:
153
- response = response.follow()
154
- return self._response(response)
155
- return requests.get(self._uri(uri), allow_redirects=follow, **self._kwargs(**kwargs))
216
+ if not self._vapp:
217
+ return requests.get(self.url(uri), allow_redirects=follow, **self._kwargs(**kwargs))
218
+ response = self._vapp.get(self.url(uri), **self._kwargs(**kwargs))
219
+ if response and response.status_code in [301, 302, 303, 307, 308] and follow:
220
+ response = response.follow()
221
+ return self._response(response)
156
222
 
157
223
  def patch(self, uri: str, data: Optional[dict] = None,
158
224
  json: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
159
- if isinstance(self._vapp, (VirtualApp, TestApp)):
160
- return self._vapp.patch_json(self._uri(uri), json or data, **self._kwargs(**kwargs))
161
- return requests.patch(self._uri(uri), json=json or data, **self._kwargs(**kwargs))
225
+ if not self._vapp:
226
+ return requests.patch(self.url(uri), data=data, json=json, **self._kwargs(**kwargs))
227
+ return self._response(self._vapp.patch_json(self.url(uri), json or data, **self._kwargs(**kwargs)))
162
228
 
163
229
  def post(self, uri: str, data: Optional[dict] = None, json: Optional[dict] = None,
164
230
  files: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
165
- if isinstance(self._vapp, (VirtualApp, TestApp)):
166
- if files:
167
- return self._vapp.post(self._uri(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
168
- else:
169
- return self._vapp.post_json(self._uri(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
170
- return requests.post(self._uri(uri), json=json or data, files=files, **self._kwargs(**kwargs))
231
+ if not self._vapp:
232
+ return requests.post(self.url(uri), data=data, json=json, files=files, **self._kwargs(**kwargs))
233
+ if files:
234
+ response = self._vapp.post(self.url(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
235
+ else:
236
+ response = self._vapp.post_json(self.url(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
237
+ return self._response(response)
171
238
 
172
239
  def get_schema(self, schema_name: str) -> Optional[dict]:
173
240
  return get_schema(self.schema_name(schema_name), portal_vapp=self._vapp, key=self._key)
@@ -181,7 +248,7 @@ class Portal:
181
248
 
182
249
  def is_file_schema(self, schema_name: str) -> bool:
183
250
  if super_type_map := self.get_schemas_super_type_map():
184
- if file_super_type := super_type_map.get(FILE_SCHEMA_NAME):
251
+ if file_super_type := super_type_map.get(Portal.FILE_SCHEMA_NAME):
185
252
  return self.schema_name(schema_name) in file_super_type
186
253
  return False
187
254
 
@@ -221,13 +288,14 @@ class Portal:
221
288
  except Exception:
222
289
  return False
223
290
 
224
- def _uri(self, uri: str) -> str:
291
+ def url(self, uri: str) -> str:
225
292
  if not isinstance(uri, str) or not uri:
226
293
  return "/"
227
- if uri.lower().startswith("http://") or uri.lower().startswith("https://"):
294
+ if (luri := uri.lower()).startswith("http://") or luri.startswith("https://"):
228
295
  return uri
229
- uri = re.sub(r"/+", "/", uri)
230
- return (self._server + ("/" if not uri.startswith("/") else "") + uri) if self._server else uri
296
+ if not (uri := re.sub(r"/+", "/", uri)).startswith("/"):
297
+ uri = "/"
298
+ return self._server + uri if self._server else uri
231
299
 
232
300
  def _kwargs(self, **kwargs) -> dict:
233
301
  result_kwargs = {"headers":
@@ -238,7 +306,17 @@ class Portal:
238
306
  result_kwargs["timeout"] = timeout
239
307
  return result_kwargs
240
308
 
241
- def _response(self, response) -> Optional[RequestResponse]:
309
+ def _default_keys_file(self, app: Optional[str], env: Optional[str] = None) -> Optional[str]:
310
+ def is_valid_app(app: Optional[str]) -> bool: # noqa
311
+ return app and app.lower() in [name.lower() for name in ORCHESTRATED_APPS]
312
+ def infer_app_from_env(env: str) -> Optional[str]: # noqa
313
+ if isinstance(env, str) and (lenv := env.lower()):
314
+ if app := [app for app in ORCHESTRATED_APPS if lenv.startswith(app.lower())]:
315
+ return app[0]
316
+ if is_valid_app(app) or (app := infer_app_from_env(env)):
317
+ return os.path.join(Portal.KEYS_FILE_DIRECTORY, f".{app.lower()}-keys.json")
318
+
319
+ def _response(self, response: TestResponse) -> Optional[RequestResponse]:
242
320
  if response and isinstance(getattr(response.__class__, "json"), property):
243
321
  class RequestResponseWrapper: # For consistency change json property to method.
244
322
  def __init__(self, response, **kwargs):
@@ -252,42 +330,72 @@ class Portal:
252
330
  return response
253
331
 
254
332
  @staticmethod
255
- def create_for_testing(ini_file: Optional[str] = None) -> Portal:
256
- if isinstance(ini_file, str):
257
- return Portal(Portal._create_vapp(ini_file))
258
- minimal_ini_for_unit_testing = "[app:app]\nuse = egg:encoded\nsqlalchemy.url = postgresql://dummy\n"
259
- with temporary_file(content=minimal_ini_for_unit_testing, suffix=".ini") as ini_file:
333
+ def create_for_testing(arg: Optional[Union[str, bool, List[dict], dict, Callable]] = None) -> Portal:
334
+ if isinstance(arg, list) or isinstance(arg, dict) or isinstance(arg, Callable):
335
+ return Portal(Portal._create_router_for_testing(arg))
336
+ if isinstance(arg, str) and arg.endswith(".ini"):
337
+ return Portal(Portal._create_vapp(arg))
338
+ if arg == "local" or arg is True:
339
+ minimal_ini_for_testing = "\n".join([
340
+ "[app:app]\nuse = egg:encoded\nfile_upload_bucket = dummy",
341
+ "sqlalchemy.url = postgresql://postgres@localhost:5441/postgres?host=/tmp/snovault/pgdata",
342
+ "multiauth.groupfinder = encoded.authorization.smaht_groupfinder",
343
+ "multiauth.policies = auth0 session remoteuser accesskey",
344
+ "multiauth.policy.session.namespace = mailto",
345
+ "multiauth.policy.session.use = encoded.authentication.NamespacedAuthenticationPolicy",
346
+ "multiauth.policy.session.base = pyramid.authentication.SessionAuthenticationPolicy",
347
+ "multiauth.policy.remoteuser.namespace = remoteuser",
348
+ "multiauth.policy.remoteuser.use = encoded.authentication.NamespacedAuthenticationPolicy",
349
+ "multiauth.policy.remoteuser.base = pyramid.authentication.RemoteUserAuthenticationPolicy",
350
+ "multiauth.policy.accesskey.namespace = accesskey",
351
+ "multiauth.policy.accesskey.use = encoded.authentication.NamespacedAuthenticationPolicy",
352
+ "multiauth.policy.accesskey.base = encoded.authentication.BasicAuthAuthenticationPolicy",
353
+ "multiauth.policy.accesskey.check = encoded.authentication.basic_auth_check",
354
+ "multiauth.policy.auth0.use = encoded.authentication.NamespacedAuthenticationPolicy",
355
+ "multiauth.policy.auth0.namespace = auth0",
356
+ "multiauth.policy.auth0.base = encoded.authentication.Auth0AuthenticationPolicy"
357
+ ])
358
+ else:
359
+ minimal_ini_for_testing = "[app:app]\nuse = egg:encoded\nsqlalchemy.url = postgresql://dummy\n"
360
+ with temporary_file(content=minimal_ini_for_testing, suffix=".ini") as ini_file:
260
361
  return Portal(Portal._create_vapp(ini_file))
261
362
 
262
363
  @staticmethod
263
- def create_for_testing_local(ini_file: Optional[str] = None) -> Portal:
264
- if isinstance(ini_file, str) and ini_file:
265
- return Portal(Portal._create_vapp(ini_file))
266
- minimal_ini_for_testing_local = "\n".join([
267
- "[app:app]\nuse = egg:encoded\nfile_upload_bucket = dummy",
268
- "sqlalchemy.url = postgresql://postgres@localhost:5441/postgres?host=/tmp/snovault/pgdata",
269
- "multiauth.groupfinder = encoded.authorization.smaht_groupfinder",
270
- "multiauth.policies = auth0 session remoteuser accesskey",
271
- "multiauth.policy.session.namespace = mailto",
272
- "multiauth.policy.session.use = encoded.authentication.NamespacedAuthenticationPolicy",
273
- "multiauth.policy.session.base = pyramid.authentication.SessionAuthenticationPolicy",
274
- "multiauth.policy.remoteuser.namespace = remoteuser",
275
- "multiauth.policy.remoteuser.use = encoded.authentication.NamespacedAuthenticationPolicy",
276
- "multiauth.policy.remoteuser.base = pyramid.authentication.RemoteUserAuthenticationPolicy",
277
- "multiauth.policy.accesskey.namespace = accesskey",
278
- "multiauth.policy.accesskey.use = encoded.authentication.NamespacedAuthenticationPolicy",
279
- "multiauth.policy.accesskey.base = encoded.authentication.BasicAuthAuthenticationPolicy",
280
- "multiauth.policy.accesskey.check = encoded.authentication.basic_auth_check",
281
- "multiauth.policy.auth0.use = encoded.authentication.NamespacedAuthenticationPolicy",
282
- "multiauth.policy.auth0.namespace = auth0",
283
- "multiauth.policy.auth0.base = encoded.authentication.Auth0AuthenticationPolicy"
284
- ])
285
- with temporary_file(content=minimal_ini_for_testing_local, suffix=".ini") as minimal_ini_file:
286
- return Portal(Portal._create_vapp(minimal_ini_file))
364
+ def _create_vapp(arg: Union[TestApp, VirtualApp, PyramidRouter, str] = None) -> TestApp:
365
+ if isinstance(arg, TestApp):
366
+ return arg
367
+ elif isinstance(arg, VirtualApp):
368
+ if not isinstance(arg.wrapped_app, TestApp):
369
+ raise Exception("Portal._create_vapp VirtualApp argument error.")
370
+ return arg.wrapped_app
371
+ if isinstance(arg, PyramidRouter):
372
+ router = arg
373
+ elif isinstance(arg, str) or not arg:
374
+ router = pyramid_get_app(arg or "development.ini", "app")
375
+ else:
376
+ raise Exception("Portal._create_vapp argument error.")
377
+ return TestApp(router, {"HTTP_ACCEPT": "application/json", "REMOTE_USER": "TEST"})
287
378
 
288
379
  @staticmethod
289
- def _create_vapp(value: Union[str, Router, TestApp] = "development.ini", app_name: str = "app") -> TestApp:
290
- if isinstance(value, TestApp):
291
- return value
292
- app = value if isinstance(value, Router) else get_app(value, app_name)
293
- return TestApp(app, {"HTTP_ACCEPT": "application/json", "REMOTE_USER": "TEST"})
380
+ def _create_router_for_testing(endpoints: Optional[List[Dict[str, Union[str, Callable]]]] = None):
381
+ if isinstance(endpoints, dict):
382
+ endpoints = [endpoints]
383
+ elif isinstance(endpoints, Callable):
384
+ endpoints = [{"path": "/", "method": "GET", "function": endpoints}]
385
+ if not isinstance(endpoints, list) or not endpoints:
386
+ endpoints = [{"path": "/", "method": "GET", "function": lambda request: {"status": "OK"}}]
387
+ with PyramidConfigurator() as config:
388
+ nendpoints = 0
389
+ for endpoint in endpoints:
390
+ if (endpoint_path := endpoint.get("path")) and (endpoint_function := endpoint.get("function")):
391
+ endpoint_method = endpoint.get("method", "GET")
392
+ def endpoint_wrapper(request): # noqa
393
+ response = endpoint_function(request)
394
+ return PyramidResponse(json.dumps(response), content_type="application/json; charset=utf-8")
395
+ endpoint_id = str(uuid())
396
+ config.add_route(endpoint_id, endpoint_path)
397
+ config.add_view(endpoint_wrapper, route_name=endpoint_id, request_method=endpoint_method)
398
+ nendpoints += 1
399
+ if nendpoints == 0:
400
+ return Portal._create_router_for_testing([])
401
+ return config.make_wsgi_app()
@@ -43,8 +43,8 @@ class StructuredDataSet:
43
43
 
44
44
  def __init__(self, file: Optional[str] = None, portal: Optional[Union[VirtualApp, TestApp, Portal]] = None,
45
45
  schemas: Optional[List[dict]] = None, autoadd: Optional[dict] = None,
46
- data: Optional[List[dict]] = None, order: Optional[List[str]] = None, prune: bool = True) -> None:
47
- self.data = {} if not data else data # If portal is None then no schemas nor refs.
46
+ order: Optional[List[str]] = None, prune: bool = True) -> None:
47
+ self.data = {}
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
@@ -113,7 +113,7 @@ class StructuredDataSet:
113
113
  def _load_file(self, file: str) -> None:
114
114
  # Returns a dictionary where each property is the name (i.e. the type) of the data,
115
115
  # and the value is array of dictionaries for the data itself. Handle these kinds of files:
116
- # 1. Single CSV of JSON file, where the (base) name of the file is the data type name.
116
+ # 1. Single CSV, TSV, or JSON file, where the (base) name of the file is the data type name.
117
117
  # 2. Single Excel file containing one or more sheets, where each sheet
118
118
  # represents (i.e. is named for, and contains data for) a different type.
119
119
  # 3. Zip file (.zip or .tar.gz or .tgz or .tar), containing data files to load, where the
@@ -483,7 +483,6 @@ class Schema:
483
483
  if unique:
484
484
  typeinfo[key]["unique"] = True
485
485
  result.update(typeinfo)
486
- # result.update(self._create_typeinfo(array_property_items, parent_key=key))
487
486
  continue
488
487
  result[key] = {"type": property_value_type, "map": self._map_function({**property_value, "column": key})}
489
488
  if ARRAY_NAME_SUFFIX_CHAR in key:
@@ -551,16 +550,13 @@ class Portal(PortalBase):
551
550
 
552
551
  def __init__(self,
553
552
  arg: Optional[Union[VirtualApp, TestApp, Router, Portal, dict, tuple, str]] = None,
554
- env: Optional[str] = None, app: OrchestratedApp = None, server: Optional[str] = None,
555
- key: Optional[Union[dict, tuple]] = None,
556
- portal: Optional[Union[VirtualApp, TestApp, Router, Portal, str]] = None,
557
- data: Optional[dict] = None, schemas: Optional[List[dict]] = None) -> Optional[Portal]:
558
- super().__init__(arg, env=env, app=app, server=server, key=key, portal=portal)
559
- if isinstance(arg, Portal) and not portal:
560
- portal = arg
561
- if isinstance(portal, Portal):
562
- self._schemas = schemas if schemas is not None else portal._schemas # Explicitly specified/known schemas.
563
- self._data = data if data is not None else portal._data # Data set being loaded; e.g. by StructuredDataSet.
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.
564
560
  else:
565
561
  self._schemas = schemas
566
562
  self._data = data
@@ -620,12 +616,9 @@ class Portal(PortalBase):
620
616
  return self.get_metadata(f"/{type_name}/{value}") is not None
621
617
 
622
618
  @staticmethod
623
- def create_for_testing(ini_file: Optional[str] = None, schemas: Optional[List[dict]] = None) -> Portal:
624
- return Portal(PortalBase.create_for_testing(ini_file), schemas=schemas)
625
-
626
- @staticmethod
627
- def create_for_testing_local(ini_file: Optional[str] = None, schemas: Optional[List[dict]] = None) -> Portal:
628
- return Portal(PortalBase.create_for_testing_local(ini_file), schemas=schemas)
619
+ def create_for_testing(arg: Optional[Union[str, bool, List[dict], dict, Callable]] = None,
620
+ schemas: Optional[List[dict]] = None) -> Portal:
621
+ return Portal(PortalBase.create_for_testing(arg), schemas=schemas)
629
622
 
630
623
 
631
624
  def _split_dotted_string(value: str):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcicutils
3
- Version: 8.5.0.1b4
3
+ Version: 8.5.0.1b7
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
@@ -43,7 +43,7 @@ dcicutils/log_utils.py,sha256=7pWMc6vyrorUZQf-V-M3YC6zrPgNhuV_fzm9xqTPph0,10883
43
43
  dcicutils/misc_utils.py,sha256=nRjLEORY35YmJwTjO0fnauBPznaI_bkVasIW8PccDYM,100179
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=Wh918ZKpUJFbt7w6Bn5G7v-0k6ImOUWMFmE9Oxt1ZN4,14560
46
+ dcicutils/portal_utils.py,sha256=IZfwtbkirNiSSvpUzkQxHBedzaSgMiiVbkLLKzHEvVI,20043
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
@@ -56,14 +56,14 @@ dcicutils/secrets_utils.py,sha256=8dppXAsiHhJzI6NmOcvJV5ldvKkQZzh3Fl-cb8Wm7MI,19
56
56
  dcicutils/sheet_utils.py,sha256=VlmzteONW5VF_Q4vo0yA5vesz1ViUah1MZ_yA1rwZ0M,33629
57
57
  dcicutils/snapshot_utils.py,sha256=ymP7PXH6-yEiXAt75w0ldQFciGNqWBClNxC5gfX2FnY,22961
58
58
  dcicutils/ssl_certificate_utils.py,sha256=F0ifz_wnRRN9dfrfsz7aCp4UDLgHEY8LaK7PjnNvrAQ,9707
59
- dcicutils/structured_data.py,sha256=tMKBhq6OuM2t6iDxIPBFcxV66MiwmOHTBeACxhvkbiQ,33307
59
+ dcicutils/structured_data.py,sha256=ziugsIE_FlYRFQIl2GJqwduRa4yIBPbHBQRYPSeUzSk,32713
60
60
  dcicutils/task_utils.py,sha256=MF8ujmTD6-O2AC2gRGPHyGdUrVKgtr8epT5XU8WtNjk,8082
61
61
  dcicutils/trace_utils.py,sha256=g8kwV4ebEy5kXW6oOrEAUsurBcCROvwtZqz9fczsGRE,1769
62
62
  dcicutils/validation_utils.py,sha256=cMZIU2cY98FYtzK52z5WUYck7urH6JcqOuz9jkXpqzg,14797
63
63
  dcicutils/variant_utils.py,sha256=2H9azNx3xAj-MySg-uZ2SFqbWs4kZvf61JnK6b-h4Qw,4343
64
64
  dcicutils/zip_utils.py,sha256=0OXR0aLNwyLIZOzIFTM_5DOun7dxIv6TIZbFiithkO0,3276
65
- dcicutils-8.5.0.1b4.dist-info/LICENSE.txt,sha256=t0_-jIjqxNnymZoNJe-OltRIuuF8qfhN0ATlHyrUJPk,1102
66
- dcicutils-8.5.0.1b4.dist-info/METADATA,sha256=Zpf8fTsgsuRfo8WDGkONHxtcf4SPWOFK_4iCNVAhB78,3314
67
- dcicutils-8.5.0.1b4.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
68
- dcicutils-8.5.0.1b4.dist-info/entry_points.txt,sha256=8wbw5csMIgBXhkwfgsgJeuFcoUc0WsucUxmOyml2aoA,209
69
- dcicutils-8.5.0.1b4.dist-info/RECORD,,
65
+ dcicutils-8.5.0.1b7.dist-info/LICENSE.txt,sha256=t0_-jIjqxNnymZoNJe-OltRIuuF8qfhN0ATlHyrUJPk,1102
66
+ dcicutils-8.5.0.1b7.dist-info/METADATA,sha256=AZPMmFxRxXIpn3-mhRMhuS3mHK9YFWjpsoA3T2TXo-g,3314
67
+ dcicutils-8.5.0.1b7.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
68
+ dcicutils-8.5.0.1b7.dist-info/entry_points.txt,sha256=8wbw5csMIgBXhkwfgsgJeuFcoUc0WsucUxmOyml2aoA,209
69
+ dcicutils-8.5.0.1b7.dist-info/RECORD,,