dcicutils 8.5.0.1b3__tar.gz → 8.5.0.1b6__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/PKG-INFO +1 -1
  2. dcicutils-8.5.0.1b6/dcicutils/portal_utils.py +375 -0
  3. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/structured_data.py +20 -16
  4. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/pyproject.toml +1 -1
  5. dcicutils-8.5.0.1b3/dcicutils/portal_utils.py +0 -294
  6. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/LICENSE.txt +0 -0
  7. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/README.rst +0 -0
  8. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/__init__.py +0 -0
  9. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/base.py +0 -0
  10. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/beanstalk_utils.py +0 -0
  11. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/bundle_utils.py +0 -0
  12. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/cloudformation_utils.py +0 -0
  13. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/codebuild_utils.py +0 -0
  14. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/command_utils.py +0 -0
  15. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/common.py +0 -0
  16. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/contribution_scripts.py +0 -0
  17. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/contribution_utils.py +0 -0
  18. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/creds_utils.py +0 -0
  19. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/data_readers.py +0 -0
  20. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/data_utils.py +0 -0
  21. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/deployment_utils.py +0 -0
  22. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/diff_utils.py +0 -0
  23. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/docker_utils.py +0 -0
  24. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/ecr_scripts.py +0 -0
  25. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/ecr_utils.py +0 -0
  26. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/ecs_utils.py +0 -0
  27. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/env_base.py +0 -0
  28. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/env_manager.py +0 -0
  29. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/env_scripts.py +0 -0
  30. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/env_utils.py +0 -0
  31. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/env_utils_legacy.py +0 -0
  32. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/es_utils.py +0 -0
  33. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/exceptions.py +0 -0
  34. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/ff_mocks.py +0 -0
  35. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/ff_utils.py +0 -0
  36. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/function_cache_decorator.py +0 -0
  37. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/glacier_utils.py +0 -0
  38. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/jh_utils.py +0 -0
  39. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/kibana/dashboards.json +0 -0
  40. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/kibana/readme.md +0 -0
  41. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/lang_utils.py +0 -0
  42. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/license_policies/c4-infrastructure.jsonc +0 -0
  43. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/license_policies/c4-python-infrastructure.jsonc +0 -0
  44. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/license_policies/park-lab-common-server.jsonc +0 -0
  45. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/license_policies/park-lab-common.jsonc +0 -0
  46. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/license_policies/park-lab-gpl-pipeline.jsonc +0 -0
  47. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/license_policies/park-lab-pipeline.jsonc +0 -0
  48. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/license_utils.py +0 -0
  49. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/log_utils.py +0 -0
  50. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/misc_utils.py +0 -0
  51. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/obfuscation_utils.py +0 -0
  52. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/opensearch_utils.py +0 -0
  53. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/project_utils.py +0 -0
  54. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/qa_checkers.py +0 -0
  55. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/qa_utils.py +0 -0
  56. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/redis_tools.py +0 -0
  57. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/redis_utils.py +0 -0
  58. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/s3_utils.py +0 -0
  59. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/scripts/publish_to_pypi.py +0 -0
  60. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/scripts/run_license_checker.py +0 -0
  61. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/secrets_utils.py +0 -0
  62. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/sheet_utils.py +0 -0
  63. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/snapshot_utils.py +0 -0
  64. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/ssl_certificate_utils.py +0 -0
  65. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/task_utils.py +0 -0
  66. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/trace_utils.py +0 -0
  67. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/validation_utils.py +0 -0
  68. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/variant_utils.py +0 -0
  69. {dcicutils-8.5.0.1b3 → dcicutils-8.5.0.1b6}/dcicutils/zip_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcicutils
3
- Version: 8.5.0.1b3
3
+ Version: 8.5.0.1b6
4
4
  Summary: Utility package for interacting with the 4DN Data Portal and other 4DN resources
5
5
  Home-page: https://github.com/4dn-dcic/utils
6
6
  License: MIT
@@ -0,0 +1,375 @@
1
+ from collections import deque
2
+ import io
3
+ import json
4
+ from pyramid.paster import get_app
5
+ from pyramid.router import Router
6
+ import os
7
+ import re
8
+ import requests
9
+ from requests.models import Response as RequestResponse
10
+ from typing import Optional, Type, Union
11
+ from webtest.app import TestApp, TestResponse
12
+ from dcicutils.common import OrchestratedApp, ORCHESTRATED_APPS
13
+ from dcicutils.ff_utils import get_metadata, get_schema, patch_metadata, post_metadata
14
+ from dcicutils.misc_utils import to_camel_case, VirtualApp
15
+ from dcicutils.zip_utils import temporary_file
16
+
17
+ Portal = Type["Portal"] # Forward type reference for type hints.
18
+
19
+
20
+ class Portal:
21
+ """
22
+ This is meant to be an uber wrapper for Portal access. It can be created in a variety of ways:
23
+ 1. From a (Portal) .ini file (e.g. development.ini)
24
+ 2. From a key dictionary, containing "key" and "secret" property values.
25
+ 3. From a key tuple, containing (in order) a key and secret values.
26
+ 4. From a keys file assumed to reside in ~/.{app}-keys.json where the given "app" value is either "smaht", "cgap",
27
+ or "fourfront"; where is assumed to contain a dictionary with a key for the given "env" value, e.g. smaht-local;
28
+ and with a dictionary value containing "key" and "secret" property values, and an optional "server" property;
29
+ if an "app" value is not specified but the given "env" value begins with one of the app values then that value
30
+ will be used, i.e. e.g. if "env" is "smaht-local" and app is unspecified than it is assumed to be "smaht".
31
+ 5. From a keys file as described above (#4) but rather than be identified by the given "env" value it
32
+ is looked up via the given "server" name and the "server" key dictionary value in the key file.
33
+ 6. From a given "vapp" value (which is assumed to be a TestApp or VirtualApp).
34
+ 7. From another Portal object; or from a a pyramid Router object.
35
+ """
36
+ FILE_SCHEMA_NAME = "File"
37
+ KEYS_FILE_DIRECTORY = os.path.expanduser(f"~")
38
+
39
+ def __init__(self,
40
+ arg: Optional[Union[Portal, TestApp, VirtualApp, Router, dict, tuple, str]] = None,
41
+ env: Optional[str] = None, server: Optional[str] = None,
42
+ app: Optional[OrchestratedApp] = None) -> None:
43
+
44
+ def init(unspecified: Optional[list] = []) -> None:
45
+ self._ini_file = None
46
+ self._key = None
47
+ self._key_pair = None
48
+ self._key_id = None
49
+ self._secret = None
50
+ self._keys_file = None
51
+ self._env = None
52
+ self._server = None
53
+ self._app = None
54
+ self._vapp = None
55
+ for arg in unspecified:
56
+ if arg is not None:
57
+ raise Exception("Portal init error; extraneous args.")
58
+
59
+ def init_from_portal(portal: Portal, unspecified: Optional[list] = None) -> None:
60
+ init(unspecified)
61
+ self._ini_file = portal._ini_file
62
+ self._key = portal._key
63
+ self._key_pair = portal._key_pair
64
+ self._key_id = portal._key_id
65
+ self._secret = portal._secret
66
+ self._keys_file = portal._keys_file
67
+ self._env = portal._env
68
+ self._server = portal._server
69
+ self._app = portal._app
70
+ self._vapp = portal._vapp
71
+
72
+ def init_from_vapp(vapp: Union[TestApp, VirtualApp, Router], unspecified: Optional[list] = []) -> None:
73
+ init(unspecified)
74
+ self._vapp = Portal._create_testapp(vapp)
75
+
76
+ def init_from_ini_file(ini_file: str, unspecified: Optional[list] = []) -> None:
77
+ init(unspecified)
78
+ self._ini_file = ini_file
79
+ self._vapp = Portal._create_testapp(ini_file)
80
+
81
+ def init_from_key(key: dict, server: Optional[str], unspecified: Optional[list] = []) -> None:
82
+ init(unspecified)
83
+ if (isinstance(key_id := key.get("key"), str) and key_id and
84
+ isinstance(secret := key.get("secret"), str) and secret): # noqa
85
+ self._key = {"key": key_id, "secret": secret}
86
+ self._key_id = key_id
87
+ self._secret = secret
88
+ self._key_pair = (key_id, secret)
89
+ if ((isinstance(server, str) and server) or (isinstance(server := key.get("server"), str) and server)):
90
+ if server := normalize_server(server):
91
+ self._key["server"] = self._server = server
92
+ if not self._key:
93
+ raise Exception("Portal init error; from key.")
94
+
95
+ def init_from_key_pair(key_pair: tuple, server: Optional[str], unspecified: Optional[list] = []) -> None:
96
+ if len(key_pair) == 2:
97
+ init_from_key({"key": key_pair[0], "secret": key_pair[1]}, server, unspecified)
98
+ else:
99
+ raise Exception("Portal init error; from key-pair.")
100
+
101
+ def init_from_keys_file(keys_file: str, env: Optional[str], server: Optional[str],
102
+ unspecified: Optional[list] = []) -> None:
103
+ try:
104
+ with io.open(keys_file) as f:
105
+ keys = json.load(f)
106
+ except Exception:
107
+ raise Exception(f"Portal init error; cannot open keys-file: {keys_file}")
108
+ if isinstance(env, str) and env and isinstance(key := keys.get(env), dict):
109
+ init_from_key(key, server)
110
+ self._keys_file = keys_file
111
+ self._env = env
112
+ elif isinstance(server, str) and server and (key := [k for k in keys if keys[k].get("server") == server]):
113
+ init_from_key(key, server)
114
+ self._keys_file = keys_file
115
+ elif len(keys) == 1 and (env := next(iter(keys))) and isinstance(key := keys[env], dict) and key:
116
+ init_from_key(key, server)
117
+ self._keys_file = keys_file
118
+ self._env = env
119
+ else:
120
+ raise Exception(f"Portal init error; {env or server or None} not found in keys-file: {keys_file}")
121
+
122
+ def init_from_env_server_app(env: str, server: str, app: Optional[str],
123
+ unspecified: Optional[list] = None) -> None:
124
+ return init_from_keys_file(self._default_keys_file(app, env), env, server, unspecified)
125
+
126
+ def normalize_server(server: str) -> Optional[str]:
127
+ prefix = ""
128
+ if (lserver := server.lower()).startswith("http://"):
129
+ prefix = "http://"
130
+ elif lserver.startswith("https://"):
131
+ prefix = "https://"
132
+ if prefix:
133
+ if (server := re.sub(r"/+", "/", server[len(prefix):])).startswith("/"):
134
+ server = server[1:]
135
+ if len(server) > 1 and server.endswith("/"):
136
+ server = server[:-1]
137
+ return prefix + server if server else None
138
+
139
+ if isinstance(arg, Portal):
140
+ init_from_portal(arg, unspecified=[env, server, app])
141
+ elif isinstance(arg, (TestApp, VirtualApp, Router)):
142
+ init_from_vapp(arg, unspecified=[env, server, app])
143
+ elif isinstance(arg, str) and arg.endswith(".ini"):
144
+ init_from_ini_file(arg, unspecified=[env, server, app])
145
+ elif isinstance(arg, dict):
146
+ init_from_key(arg, server, unspecified=[env, app])
147
+ elif isinstance(arg, tuple):
148
+ init_from_key_pair(arg, server, unspecified=[env, app])
149
+ elif isinstance(arg, str) and arg.endswith(".json"):
150
+ init_from_keys_file(arg, env, server, unspecified=[app])
151
+ elif isinstance(arg, str) and arg:
152
+ init_from_env_server_app(arg, server, app, unspecified=[env])
153
+ elif isinstance(env, str) and env:
154
+ init_from_env_server_app(env, server, app, unspecified=[arg])
155
+ else:
156
+ raise Exception("Portal init error; invalid args.")
157
+
158
+ @property
159
+ def ini_file(self) -> Optional[str]:
160
+ return self._ini_file
161
+
162
+ @property
163
+ def key(self) -> Optional[dict]:
164
+ return self._key
165
+
166
+ @property
167
+ def key_pair(self) -> Optional[tuple]:
168
+ return self._key_pair
169
+
170
+ @property
171
+ def key_id(self) -> Optional[str]:
172
+ return self._key_id
173
+
174
+ @property
175
+ def secret(self) -> Optional[str]:
176
+ return self._secret
177
+
178
+ @property
179
+ def keys_file(self) -> Optional[str]:
180
+ return self._keys_file
181
+
182
+ @property
183
+ def env(self) -> Optional[str]:
184
+ return self._env
185
+
186
+ @property
187
+ def server(self) -> Optional[str]:
188
+ return self._server
189
+
190
+ @property
191
+ def app(self) -> Optional[str]:
192
+ return self._app
193
+
194
+ @property
195
+ def vapp(self) -> Optional[TestApp]:
196
+ return self._vapp
197
+
198
+ def get_metadata(self, object_id: str) -> Optional[dict]:
199
+ return get_metadata(obj_id=object_id, vapp=self._vapp, key=self._key)
200
+
201
+ def patch_metadata(self, object_id: str, data: str) -> Optional[dict]:
202
+ if self._key:
203
+ return patch_metadata(obj_id=object_id, patch_item=data, key=self._key)
204
+ return self.patch(f"/{object_id}", data)
205
+
206
+ def post_metadata(self, object_type: str, data: str) -> Optional[dict]:
207
+ if self._key:
208
+ return post_metadata(schema_name=object_type, post_item=data, key=self._key)
209
+ return self.post(f"/{object_type}", data)
210
+
211
+ def get(self, uri: str, follow: bool = True, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
212
+ if self._vapp:
213
+ response = self._vapp.get(self.url(uri), **self._kwargs(**kwargs))
214
+ if response and response.status_code in [301, 302, 303, 307, 308] and follow:
215
+ response = response.follow()
216
+ return self._response(response)
217
+ return requests.get(self.url(uri), allow_redirects=follow, **self._kwargs(**kwargs))
218
+
219
+ def patch(self, uri: str, data: Optional[dict] = None,
220
+ json: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
221
+ if self._vapp:
222
+ return self._vapp.patch_json(self.url(uri), json or data, **self._kwargs(**kwargs))
223
+ return requests.patch(self.url(uri), data=data, json=json, **self._kwargs(**kwargs))
224
+
225
+ def post(self, uri: str, data: Optional[dict] = None, json: Optional[dict] = None,
226
+ files: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
227
+ if self._vapp:
228
+ if files:
229
+ return self._vapp.post(self.url(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
230
+ else:
231
+ return self._vapp.post_json(self.url(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
232
+ return requests.post(self.url(uri), data=data, json=json, files=files, **self._kwargs(**kwargs))
233
+
234
+ def get_schema(self, schema_name: str) -> Optional[dict]:
235
+ return get_schema(self.schema_name(schema_name), portal_vapp=self._vapp, key=self._key)
236
+
237
+ def get_schemas(self) -> dict:
238
+ return self.get("/profiles/").json()
239
+
240
+ @staticmethod
241
+ def schema_name(name: str) -> str:
242
+ return to_camel_case(name)
243
+
244
+ def is_file_schema(self, schema_name: str) -> bool:
245
+ if super_type_map := self.get_schemas_super_type_map():
246
+ if file_super_type := super_type_map.get(Portal.FILE_SCHEMA_NAME):
247
+ return self.schema_name(schema_name) in file_super_type
248
+ return False
249
+
250
+ def get_schemas_super_type_map(self) -> dict:
251
+ """
252
+ Returns the "super type map" for all of the known schemas (via /profiles).
253
+ This is a dictionary of all types which have (one or more) sub-types whose value is
254
+ an array of all of those sub-types (direct and all descendents), in breadth first order.
255
+ """
256
+ def breadth_first(super_type_map: dict, super_type_name: str) -> dict:
257
+ result = []
258
+ queue = deque(super_type_map.get(super_type_name, []))
259
+ while queue:
260
+ result.append(sub_type_name := queue.popleft())
261
+ if sub_type_name in super_type_map:
262
+ queue.extend(super_type_map[sub_type_name])
263
+ return result
264
+ if not (schemas := self.get_schemas()):
265
+ return {}
266
+ super_type_map = {}
267
+ for type_name in schemas:
268
+ if super_type_name := schemas[type_name].get("rdfs:subClassOf"):
269
+ super_type_name = super_type_name.replace("/profiles/", "").replace(".json", "")
270
+ if super_type_name != "Item":
271
+ if not super_type_map.get(super_type_name):
272
+ super_type_map[super_type_name] = [type_name]
273
+ elif type_name not in super_type_map[super_type_name]:
274
+ super_type_map[super_type_name].append(type_name)
275
+ super_type_map_flattened = {}
276
+ for super_type_name in super_type_map:
277
+ super_type_map_flattened[super_type_name] = breadth_first(super_type_map, super_type_name)
278
+ return super_type_map_flattened
279
+
280
+ def ping(self) -> bool:
281
+ try:
282
+ return self.get("/health").status_code == 200
283
+ except Exception:
284
+ return False
285
+
286
+ def url(self, uri: str) -> str:
287
+ if not isinstance(uri, str) or not uri:
288
+ return "/"
289
+ if (luri := uri.lower()).startswith("http://") or luri.startswith("https://"):
290
+ return uri
291
+ if not (uri := re.sub(r"/+", "/", uri)).startswith("/"):
292
+ uri = "/"
293
+ return self._server + uri if self._server else uri
294
+
295
+ def _kwargs(self, **kwargs) -> dict:
296
+ result_kwargs = {"headers":
297
+ kwargs.get("headers", {"Content-type": "application/json", "Accept": "application/json"})}
298
+ if self._key_pair:
299
+ result_kwargs["auth"] = self._key_pair
300
+ if isinstance(timeout := kwargs.get("timeout"), int):
301
+ result_kwargs["timeout"] = timeout
302
+ return result_kwargs
303
+
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
+ def _response(self, response) -> Optional[RequestResponse]:
315
+ if response and isinstance(getattr(response.__class__, "json"), property):
316
+ class RequestResponseWrapper: # For consistency change json property to method.
317
+ def __init__(self, response, **kwargs):
318
+ super().__init__(**kwargs)
319
+ self._response = response
320
+ def __getattr__(self, attr): # noqa
321
+ return getattr(self._response, attr)
322
+ def json(self): # noqa
323
+ return self._response.json
324
+ response = RequestResponseWrapper(response)
325
+ return response
326
+
327
+ @staticmethod
328
+ def create_for_testing(ini_file: Optional[str] = None) -> Portal:
329
+ if isinstance(ini_file, str):
330
+ return Portal(Portal._create_testapp(ini_file))
331
+ minimal_ini_for_unit_testing = "[app:app]\nuse = egg:encoded\nsqlalchemy.url = postgresql://dummy\n"
332
+ with temporary_file(content=minimal_ini_for_unit_testing, suffix=".ini") as ini_file:
333
+ return Portal(Portal._create_testapp(ini_file))
334
+
335
+ @staticmethod
336
+ def create_for_testing_local(ini_file: Optional[str] = None) -> Portal:
337
+ if isinstance(ini_file, str) and ini_file:
338
+ return Portal(Portal._create_testapp(ini_file))
339
+ minimal_ini_for_testing_local = "\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
+ with temporary_file(content=minimal_ini_for_testing_local, suffix=".ini") as minimal_ini_file:
359
+ return Portal(Portal._create_testapp(minimal_ini_file))
360
+
361
+ @staticmethod
362
+ def _create_testapp(arg: Union[TestApp, VirtualApp, Router, str] = None, app_name: Optional[str] = None) -> TestApp:
363
+ if isinstance(arg, TestApp):
364
+ return arg
365
+ elif isinstance(arg, VirtualApp):
366
+ if not isinstance(arg.wrapped_app, TestApp):
367
+ raise Exception("Portal._create_testapp VirtualApp argument error.")
368
+ return arg.wrapped_app
369
+ if isinstance(arg, Router):
370
+ router = arg
371
+ elif isinstance(arg, str) or arg is None:
372
+ router = get_app(arg or "development.ini", app_name or "app")
373
+ else:
374
+ raise Exception("Portal._create_testapp argument error.")
375
+ return TestApp(router, {"HTTP_ACCEPT": "application/json", "REMOTE_USER": "TEST"})
@@ -42,9 +42,9 @@ 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, data: Optional[List[dict]] = None,
45
+ schemas: Optional[List[dict]] = None, autoadd: Optional[dict] = None,
46
46
  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.
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
@@ -52,13 +52,14 @@ class StructuredDataSet:
52
52
  self._errors = {}
53
53
  self._resolved_refs = set()
54
54
  self._validated = False
55
+ self._autoadd_properties = autoadd if isinstance(autoadd, dict) and autoadd else None
55
56
  self._load_file(file) if file else None
56
57
 
57
58
  @staticmethod
58
59
  def load(file: str, portal: Optional[Union[VirtualApp, TestApp, Portal]] = None,
59
- schemas: Optional[List[dict]] = None,
60
+ schemas: Optional[List[dict]] = None, autoadd: Optional[dict] = None,
60
61
  order: Optional[List[str]] = None, prune: bool = True) -> StructuredDataSet:
61
- return StructuredDataSet(file=file, portal=portal, schemas=schemas, order=order, prune=prune)
62
+ return StructuredDataSet(file=file, portal=portal, schemas=schemas, autoadd=autoadd, order=order, prune=prune)
62
63
 
63
64
  def validate(self, force: bool = False) -> None:
64
65
  if self._validated and not force:
@@ -112,7 +113,7 @@ class StructuredDataSet:
112
113
  def _load_file(self, file: str) -> None:
113
114
  # Returns a dictionary where each property is the name (i.e. the type) of the data,
114
115
  # and the value is array of dictionaries for the data itself. Handle these kinds of files:
115
- # 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.
116
117
  # 2. Single Excel file containing one or more sheets, where each sheet
117
118
  # represents (i.e. is named for, and contains data for) a different type.
118
119
  # 3. Zip file (.zip or .tar.gz or .tgz or .tar), containing data files to load, where the
@@ -163,6 +164,8 @@ class StructuredDataSet:
163
164
  structured_row = structured_row_template.create_row()
164
165
  for column_name, value in row.items():
165
166
  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)
166
169
  self._add(type_name, structured_row)
167
170
  self._note_warning(reader.warnings, "reader")
168
171
  if schema:
@@ -177,6 +180,11 @@ class StructuredDataSet:
177
180
  else:
178
181
  self.data[type_name] = [data] if isinstance(data, dict) else data
179
182
 
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
+
180
188
  def _note_warning(self, item: Optional[Union[dict, List[dict]]], group: str) -> None:
181
189
  self._note_issue(self._warnings, item, group)
182
190
 
@@ -475,7 +483,6 @@ class Schema:
475
483
  if unique:
476
484
  typeinfo[key]["unique"] = True
477
485
  result.update(typeinfo)
478
- # result.update(self._create_typeinfo(array_property_items, parent_key=key))
479
486
  continue
480
487
  result[key] = {"type": property_value_type, "map": self._map_function({**property_value, "column": key})}
481
488
  if ARRAY_NAME_SUFFIX_CHAR in key:
@@ -543,16 +550,13 @@ class Portal(PortalBase):
543
550
 
544
551
  def __init__(self,
545
552
  arg: Optional[Union[VirtualApp, TestApp, Router, Portal, dict, tuple, str]] = None,
546
- env: Optional[str] = None, app: OrchestratedApp = None, server: Optional[str] = None,
547
- key: Optional[Union[dict, tuple]] = None,
548
- portal: Optional[Union[VirtualApp, TestApp, Router, Portal, str]] = None,
549
- data: Optional[dict] = None, schemas: Optional[List[dict]] = None) -> Optional[Portal]:
550
- super().__init__(arg, env=env, app=app, server=server, key=key, portal=portal)
551
- if isinstance(arg, Portal) and not portal:
552
- portal = arg
553
- if isinstance(portal, Portal):
554
- self._schemas = schemas if schemas is not None else portal._schemas # Explicitly specified/known schemas.
555
- 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.
556
560
  else:
557
561
  self._schemas = schemas
558
562
  self._data = data
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dcicutils"
3
- version = "8.5.0.1b3" # TODO: To become 8.6.0
3
+ version = "8.5.0.1b6" # TODO: To become 8.6.0
4
4
  description = "Utility package for interacting with the 4DN Data Portal and other 4DN resources"
5
5
  authors = ["4DN-DCIC Team <support@4dnucleome.org>"]
6
6
  license = "MIT"
@@ -1,294 +0,0 @@
1
- from collections import deque
2
- from pyramid.paster import get_app
3
- from pyramid.router import Router
4
- import re
5
- import requests
6
- from requests.models import Response as RequestResponse
7
- from typing import Optional, Type, Union
8
- from webtest.app import TestApp, TestResponse
9
- from dcicutils.common import OrchestratedApp, APP_CGAP, APP_FOURFRONT, APP_SMAHT, ORCHESTRATED_APPS
10
- from dcicutils.creds_utils import CGAPKeyManager, FourfrontKeyManager, SMaHTKeyManager
11
- from dcicutils.ff_utils import get_metadata, get_schema, patch_metadata, post_metadata
12
- from dcicutils.misc_utils import to_camel_case, VirtualApp
13
- from dcicutils.zip_utils import temporary_file
14
-
15
- Portal = Type["Portal"] # Forward type reference for type hints.
16
- FILE_SCHEMA_NAME = "File"
17
-
18
-
19
- class Portal:
20
- """
21
- This is meant to be an uber wrapper for Portal access. It can be created in a variety of ways:
22
- 1. From a (Portal) .ini file (e.g. development.ini)
23
- 2. From a key dictionary, containing "key" and "secret" property values.
24
- 3. From a key tuple, containing (in order) a key and secret values.
25
- 4. From a keys file assumed to reside in ~/.{app}-keys.json where the given "app" value is either "smaht", "cgap",
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".
30
- 5. From a keys file as described above (#4) but rather than be identified by the given "env" value it
31
- is looked up by the given "server" name and the "server" key dictionary value in the key file.
32
- 6. From a given "vapp" value (which is assumed to be a TestApp or VirtualApp).
33
- 7. From another Portal object.
34
- 8. From a a pyramid Router object.
35
- """
36
- def __init__(self,
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
60
- self._env = portal._env
61
- self._app = portal._app
62
- self._server = portal._server
63
- self._key = portal._key
64
- self._key_pair = portal._key_pair
65
- self._key_id = portal._key_id
66
- self._key_file = portal._key_file
67
- return
68
- self._vapp = None
69
- self._env = env
70
- self._app = app
71
- self._server = server
72
- self._key = None
73
- self._key_pair = None
74
- self._key_id = None
75
- self._key_file = None
76
- if isinstance(portal, (VirtualApp, TestApp)):
77
- self._vapp = portal
78
- elif isinstance(portal, (Router, str)):
79
- self._vapp = Portal._create_vapp(portal)
80
- elif isinstance(key, dict):
81
- self._key = key
82
- self._key_pair = (key.get("key"), key.get("secret")) if key else None
83
- if key_server := key.get("server"):
84
- self._server = key_server
85
- elif isinstance(key, tuple) and len(key) >= 2:
86
- self._key = {"key": key[0], "secret": key[1]}
87
- self._key_pair = key
88
- elif isinstance(env, str):
89
- key_managers = {APP_CGAP: CGAPKeyManager, APP_FOURFRONT: FourfrontKeyManager, APP_SMAHT: SMaHTKeyManager}
90
- if not (key_manager := key_managers.get(self._app)) or not (key_manager := key_manager()):
91
- raise Exception(f"Invalid app name: {self._app} (valid: {', '.join(ORCHESTRATED_APPS)}).")
92
- if isinstance(env, str):
93
- self._key = key_manager.get_keydict_for_env(env)
94
- if key_server := self._key.get("server"):
95
- self._server = key_server
96
- elif isinstance(self._server, str):
97
- self._key = key_manager.get_keydict_for_server(self._server)
98
- self._key_pair = key_manager.keydict_to_keypair(self._key) if self._key else None
99
- self._key_file = key_manager.keys_file
100
- if self._key and (key_id := self._key.get("key")):
101
- self._key_id = key_id
102
- elif self._key_pair and (key_id := self._key_pair[1]):
103
- self._key_id = key_id
104
-
105
- @property
106
- def env(self):
107
- return self._env
108
-
109
- @property
110
- def app(self):
111
- return self._app
112
-
113
- @property
114
- def server(self):
115
- return self._server
116
-
117
- @property
118
- def key(self):
119
- return self._key
120
-
121
- @property
122
- def key_pair(self):
123
- return self._key_pair
124
-
125
- @property
126
- def key_id(self):
127
- return self._key_id
128
-
129
- @property
130
- def key_file(self):
131
- return self._key_file
132
-
133
- @property
134
- def vapp(self):
135
- return self._vapp
136
-
137
- def get_metadata(self, object_id: str) -> Optional[dict]:
138
- return get_metadata(obj_id=object_id, vapp=self._vapp, key=self._key)
139
-
140
- def patch_metadata(self, object_id: str, data: str) -> Optional[dict]:
141
- if self._key:
142
- return patch_metadata(obj_id=object_id, patch_item=data, key=self._key)
143
- return self.patch(f"/{object_id}", data)
144
-
145
- def post_metadata(self, object_type: str, data: str) -> Optional[dict]:
146
- if self._key:
147
- return post_metadata(schema_name=object_type, post_item=data, key=self._key)
148
- return self.post(f"/{object_type}", data)
149
-
150
- def get(self, uri: str, follow: bool = True, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
151
- if isinstance(self._vapp, (VirtualApp, TestApp)):
152
- response = self._vapp.get(self._uri(uri), **self._kwargs(**kwargs))
153
- if response and response.status_code in [301, 302, 303, 307, 308] and follow:
154
- response = response.follow()
155
- return self._response(response)
156
- return requests.get(self._uri(uri), allow_redirects=follow, **self._kwargs(**kwargs))
157
-
158
- def patch(self, uri: str, data: Optional[dict] = None,
159
- json: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
160
- if isinstance(self._vapp, (VirtualApp, TestApp)):
161
- return self._vapp.patch_json(self._uri(uri), json or data, **self._kwargs(**kwargs))
162
- return requests.patch(self._uri(uri), json=json or data, **self._kwargs(**kwargs))
163
-
164
- def post(self, uri: str, data: Optional[dict] = None, json: Optional[dict] = None,
165
- files: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
166
- if isinstance(self._vapp, (VirtualApp, TestApp)):
167
- if files:
168
- return self._vapp.post(self._uri(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
169
- else:
170
- return self._vapp.post_json(self._uri(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
171
- return requests.post(self._uri(uri), json=json or data, files=files, **self._kwargs(**kwargs))
172
-
173
- def get_schema(self, schema_name: str) -> Optional[dict]:
174
- return get_schema(self.schema_name(schema_name), portal_vapp=self._vapp, key=self._key)
175
-
176
- def get_schemas(self) -> dict:
177
- return self.get("/profiles/").json()
178
-
179
- @staticmethod
180
- def schema_name(name: str) -> str:
181
- return to_camel_case(name)
182
-
183
- def is_file_schema(self, schema_name: str) -> bool:
184
- if super_type_map := self.get_schemas_super_type_map():
185
- if file_super_type := super_type_map.get(FILE_SCHEMA_NAME):
186
- return self.schema_name(schema_name) in file_super_type
187
- return False
188
-
189
- def get_schemas_super_type_map(self) -> dict:
190
- """
191
- Returns the "super type map" for all of the known schemas (via /profiles).
192
- This is a dictionary of all types which have (one or more) sub-types whose value is
193
- an array of all of those sub-types (direct and all descendents), in breadth first order.
194
- """
195
- def breadth_first(super_type_map: dict, super_type_name: str) -> dict:
196
- result = []
197
- queue = deque(super_type_map.get(super_type_name, []))
198
- while queue:
199
- result.append(sub_type_name := queue.popleft())
200
- if sub_type_name in super_type_map:
201
- queue.extend(super_type_map[sub_type_name])
202
- return result
203
- if not (schemas := self.get_schemas()):
204
- return {}
205
- super_type_map = {}
206
- for type_name in schemas:
207
- if super_type_name := schemas[type_name].get("rdfs:subClassOf"):
208
- super_type_name = super_type_name.replace("/profiles/", "").replace(".json", "")
209
- if super_type_name != "Item":
210
- if not super_type_map.get(super_type_name):
211
- super_type_map[super_type_name] = [type_name]
212
- elif type_name not in super_type_map[super_type_name]:
213
- super_type_map[super_type_name].append(type_name)
214
- super_type_map_flattened = {}
215
- for super_type_name in super_type_map:
216
- super_type_map_flattened[super_type_name] = breadth_first(super_type_map, super_type_name)
217
- return super_type_map_flattened
218
-
219
- def ping(self) -> bool:
220
- try:
221
- return self.get("/health").status_code == 200
222
- except Exception:
223
- return False
224
-
225
- def _uri(self, uri: str) -> str:
226
- if not isinstance(uri, str) or not uri:
227
- return "/"
228
- if uri.lower().startswith("http://") or uri.lower().startswith("https://"):
229
- return uri
230
- uri = re.sub(r"/+", "/", uri)
231
- return (self._server + ("/" if not uri.startswith("/") else "") + uri) if self._server else uri
232
-
233
- def _kwargs(self, **kwargs) -> dict:
234
- result_kwargs = {"headers":
235
- kwargs.get("headers", {"Content-type": "application/json", "Accept": "application/json"})}
236
- if self._key_pair:
237
- result_kwargs["auth"] = self._key_pair
238
- if isinstance(timeout := kwargs.get("timeout"), int):
239
- result_kwargs["timeout"] = timeout
240
- return result_kwargs
241
-
242
- def _response(self, response) -> Optional[RequestResponse]:
243
- if response and isinstance(getattr(response.__class__, "json"), property):
244
- class RequestResponseWrapper: # For consistency change json property to method.
245
- def __init__(self, response, **kwargs):
246
- super().__init__(**kwargs)
247
- self._response = response
248
- def __getattr__(self, attr): # noqa
249
- return getattr(self._response, attr)
250
- def json(self): # noqa
251
- return self._response.json
252
- response = RequestResponseWrapper(response)
253
- return response
254
-
255
- @staticmethod
256
- def create_for_testing(ini_file: Optional[str] = None) -> Portal:
257
- if isinstance(ini_file, str):
258
- return Portal(Portal._create_vapp(ini_file))
259
- minimal_ini_for_unit_testing = "[app:app]\nuse = egg:encoded\nsqlalchemy.url = postgresql://dummy\n"
260
- with temporary_file(content=minimal_ini_for_unit_testing, suffix=".ini") as ini_file:
261
- return Portal(Portal._create_vapp(ini_file))
262
-
263
- @staticmethod
264
- def create_for_testing_local(ini_file: Optional[str] = None) -> Portal:
265
- if isinstance(ini_file, str) and ini_file:
266
- return Portal(Portal._create_vapp(ini_file))
267
- minimal_ini_for_testing_local = "\n".join([
268
- "[app:app]\nuse = egg:encoded\nfile_upload_bucket = dummy",
269
- "sqlalchemy.url = postgresql://postgres@localhost:5441/postgres?host=/tmp/snovault/pgdata",
270
- "multiauth.groupfinder = encoded.authorization.smaht_groupfinder",
271
- "multiauth.policies = auth0 session remoteuser accesskey",
272
- "multiauth.policy.session.namespace = mailto",
273
- "multiauth.policy.session.use = encoded.authentication.NamespacedAuthenticationPolicy",
274
- "multiauth.policy.session.base = pyramid.authentication.SessionAuthenticationPolicy",
275
- "multiauth.policy.remoteuser.namespace = remoteuser",
276
- "multiauth.policy.remoteuser.use = encoded.authentication.NamespacedAuthenticationPolicy",
277
- "multiauth.policy.remoteuser.base = pyramid.authentication.RemoteUserAuthenticationPolicy",
278
- "multiauth.policy.accesskey.namespace = accesskey",
279
- "multiauth.policy.accesskey.use = encoded.authentication.NamespacedAuthenticationPolicy",
280
- "multiauth.policy.accesskey.base = encoded.authentication.BasicAuthAuthenticationPolicy",
281
- "multiauth.policy.accesskey.check = encoded.authentication.basic_auth_check",
282
- "multiauth.policy.auth0.use = encoded.authentication.NamespacedAuthenticationPolicy",
283
- "multiauth.policy.auth0.namespace = auth0",
284
- "multiauth.policy.auth0.base = encoded.authentication.Auth0AuthenticationPolicy"
285
- ])
286
- with temporary_file(content=minimal_ini_for_testing_local, suffix=".ini") as minimal_ini_file:
287
- return Portal(Portal._create_vapp(minimal_ini_file))
288
-
289
- @staticmethod
290
- def _create_vapp(value: Union[str, Router, TestApp] = "development.ini", app_name: str = "app") -> TestApp:
291
- if isinstance(value, TestApp):
292
- return value
293
- app = value if isinstance(value, Router) else get_app(value, app_name)
294
- return TestApp(app, {"HTTP_ACCEPT": "application/json", "REMOTE_USER": "TEST"})
File without changes
File without changes