dcicutils 8.5.0.1b6__tar.gz → 8.6.0.0b0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/PKG-INFO +1 -1
  2. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/misc_utils.py +7 -16
  3. dcicutils-8.6.0.0b0/dcicutils/portal_utils.py +278 -0
  4. dcicutils-8.6.0.0b0/dcicutils/schema_utils.py +142 -0
  5. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/structured_data.py +34 -47
  6. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/pyproject.toml +1 -1
  7. dcicutils-8.5.0.1b6/dcicutils/portal_utils.py +0 -375
  8. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/LICENSE.txt +0 -0
  9. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/README.rst +0 -0
  10. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/__init__.py +0 -0
  11. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/base.py +0 -0
  12. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/beanstalk_utils.py +0 -0
  13. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/bundle_utils.py +0 -0
  14. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/cloudformation_utils.py +0 -0
  15. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/codebuild_utils.py +0 -0
  16. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/command_utils.py +0 -0
  17. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/common.py +0 -0
  18. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/contribution_scripts.py +0 -0
  19. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/contribution_utils.py +0 -0
  20. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/creds_utils.py +0 -0
  21. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/data_readers.py +0 -0
  22. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/data_utils.py +0 -0
  23. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/deployment_utils.py +0 -0
  24. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/diff_utils.py +0 -0
  25. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/docker_utils.py +0 -0
  26. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/ecr_scripts.py +0 -0
  27. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/ecr_utils.py +0 -0
  28. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/ecs_utils.py +0 -0
  29. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/env_base.py +0 -0
  30. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/env_manager.py +0 -0
  31. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/env_scripts.py +0 -0
  32. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/env_utils.py +0 -0
  33. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/env_utils_legacy.py +0 -0
  34. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/es_utils.py +0 -0
  35. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/exceptions.py +0 -0
  36. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/ff_mocks.py +0 -0
  37. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/ff_utils.py +0 -0
  38. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/function_cache_decorator.py +0 -0
  39. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/glacier_utils.py +0 -0
  40. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/jh_utils.py +0 -0
  41. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/kibana/dashboards.json +0 -0
  42. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/kibana/readme.md +0 -0
  43. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/lang_utils.py +0 -0
  44. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/license_policies/c4-infrastructure.jsonc +0 -0
  45. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/license_policies/c4-python-infrastructure.jsonc +0 -0
  46. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/license_policies/park-lab-common-server.jsonc +0 -0
  47. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/license_policies/park-lab-common.jsonc +0 -0
  48. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/license_policies/park-lab-gpl-pipeline.jsonc +0 -0
  49. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/license_policies/park-lab-pipeline.jsonc +0 -0
  50. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/license_utils.py +0 -0
  51. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/log_utils.py +0 -0
  52. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/obfuscation_utils.py +0 -0
  53. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/opensearch_utils.py +0 -0
  54. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/project_utils.py +0 -0
  55. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/qa_checkers.py +0 -0
  56. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/qa_utils.py +0 -0
  57. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/redis_tools.py +0 -0
  58. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/redis_utils.py +0 -0
  59. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/s3_utils.py +0 -0
  60. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/scripts/publish_to_pypi.py +0 -0
  61. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/scripts/run_license_checker.py +0 -0
  62. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/secrets_utils.py +0 -0
  63. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/sheet_utils.py +0 -0
  64. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/snapshot_utils.py +0 -0
  65. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/ssl_certificate_utils.py +0 -0
  66. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/task_utils.py +0 -0
  67. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/trace_utils.py +0 -0
  68. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/validation_utils.py +0 -0
  69. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/dcicutils/variant_utils.py +0 -0
  70. {dcicutils-8.5.0.1b6 → dcicutils-8.6.0.0b0}/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.1b6
3
+ Version: 8.6.0.0b0
4
4
  Summary: Utility package for interacting with the 4DN Data Portal and other 4DN resources
5
5
  Home-page: https://github.com/4dn-dcic/utils
6
6
  License: MIT
@@ -982,11 +982,7 @@ def to_integer(value: str, fallback: Optional[Any] = None) -> Optional[Any]:
982
982
  try:
983
983
  return int(value)
984
984
  except Exception:
985
- try:
986
- return int(float(value))
987
- except Exception:
988
- pass
989
- return fallback
985
+ return fallback
990
986
 
991
987
 
992
988
  def to_float(value: str, fallback: Optional[Any] = None) -> Optional[Any]:
@@ -1469,33 +1465,28 @@ def string_list(s):
1469
1465
  return [p for p in [part.strip() for part in s.split(",")] if p]
1470
1466
 
1471
1467
 
1472
- def split_string(value: str, delimiter: str, escape: Optional[str] = None, unique: bool = False) -> List[str]:
1468
+ def split_string(value: str, delimiter: str, escape: Optional[str] = None) -> List[str]:
1473
1469
  """
1474
1470
  Splits the given string into an array of string based on the given delimiter, and an optional escape character.
1475
1471
  """
1476
1472
  if not isinstance(value, str) or not (value := value.strip()):
1477
1473
  return []
1478
- result = []
1479
1474
  if not isinstance(escape, str) or not escape:
1480
- for item in value.split(delimiter):
1481
- if (item := item.strip()) and (unique is not True or item not in result):
1482
- result.append(item)
1483
- return result
1475
+ return [item.strip() for item in value.split(delimiter)]
1476
+ result = []
1484
1477
  item = r""
1485
1478
  escaped = False
1486
1479
  for c in value:
1487
1480
  if c == delimiter and not escaped:
1488
- if (item := item.strip()) and (unique is not True or item not in result):
1489
- result.append(item)
1481
+ result.append(item.strip())
1490
1482
  item = r""
1491
1483
  elif c == escape and not escaped:
1492
1484
  escaped = True
1493
1485
  else:
1494
1486
  item += c
1495
1487
  escaped = False
1496
- if (item := item.strip()) and (unique is not True or item not in result):
1497
- result.append(item)
1498
- return result
1488
+ result.append(item.strip())
1489
+ return [item for item in result if item]
1499
1490
 
1500
1491
 
1501
1492
  def right_trim(list_or_tuple: Union[List[Any], Tuple[Any]],
@@ -0,0 +1,278 @@
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_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
98
+
99
+ @property
100
+ def env(self):
101
+ return self._env
102
+
103
+ @property
104
+ def app(self):
105
+ return self._app
106
+
107
+ @property
108
+ def server(self):
109
+ return self._server
110
+
111
+ @property
112
+ def key(self):
113
+ return self._key
114
+
115
+ @property
116
+ def key_pair(self):
117
+ return self._key_pair
118
+
119
+ @property
120
+ def key_file(self):
121
+ return self._key_file
122
+
123
+ @property
124
+ def vapp(self):
125
+ return self._vapp
126
+
127
+ def get_metadata(self, object_id: str) -> Optional[dict]:
128
+ return get_metadata(obj_id=object_id, vapp=self._vapp, key=self._key)
129
+
130
+ def patch_metadata(self, object_id: str, data: str) -> Optional[dict]:
131
+ if self._key:
132
+ return patch_metadata(obj_id=object_id, patch_item=data, key=self._key)
133
+ return self.patch(f"/{object_id}", data)
134
+
135
+ def post_metadata(self, object_type: str, data: str) -> Optional[dict]:
136
+ if self._key:
137
+ return post_metadata(schema_name=object_type, post_item=data, key=self._key)
138
+ return self.post(f"/{object_type}", data)
139
+
140
+ def get(self, uri: str, follow: bool = True, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
141
+ if isinstance(self._vapp, (VirtualApp, TestApp)):
142
+ response = self._vapp.get(self._uri(uri), **self._kwargs(**kwargs))
143
+ if response and response.status_code in [301, 302, 303, 307, 308] and follow:
144
+ response = response.follow()
145
+ return self._response(response)
146
+ return requests.get(self._uri(uri), allow_redirects=follow, **self._kwargs(**kwargs))
147
+
148
+ def patch(self, uri: str, data: Optional[dict] = None,
149
+ json: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
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))
153
+
154
+ def post(self, uri: str, data: Optional[dict] = None, json: Optional[dict] = None,
155
+ files: Optional[dict] = None, **kwargs) -> Optional[Union[RequestResponse, TestResponse]]:
156
+ if isinstance(self._vapp, (VirtualApp, TestApp)):
157
+ if files:
158
+ return self._vapp.post(self._uri(uri), json or data, upload_files=files, **self._kwargs(**kwargs))
159
+ else:
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))
162
+
163
+ def get_schema(self, schema_name: str) -> Optional[dict]:
164
+ return get_schema(self.schema_name(schema_name), portal_vapp=self._vapp, key=self._key)
165
+
166
+ def get_schemas(self) -> dict:
167
+ return self.get("/profiles/").json()
168
+
169
+ @staticmethod
170
+ def schema_name(name: str) -> str:
171
+ return to_camel_case(name)
172
+
173
+ def is_file_schema(self, schema_name: str) -> bool:
174
+ if super_type_map := self.get_schemas_super_type_map():
175
+ if file_super_type := super_type_map.get(FILE_SCHEMA_NAME):
176
+ return self.schema_name(schema_name) in file_super_type
177
+ return False
178
+
179
+ def get_schemas_super_type_map(self) -> dict:
180
+ """
181
+ Returns the "super type map" for all of the known schemas (via /profiles).
182
+ This is a dictionary of all types which have (one or more) sub-types whose value is
183
+ an array of all of those sub-types (direct and all descendents), in breadth first order.
184
+ """
185
+ def breadth_first(super_type_map: dict, super_type_name: str) -> dict:
186
+ result = []
187
+ queue = deque(super_type_map.get(super_type_name, []))
188
+ while queue:
189
+ result.append(sub_type_name := queue.popleft())
190
+ if sub_type_name in super_type_map:
191
+ queue.extend(super_type_map[sub_type_name])
192
+ return result
193
+ if not (schemas := self.get_schemas()):
194
+ return {}
195
+ super_type_map = {}
196
+ for type_name in schemas:
197
+ if super_type_name := schemas[type_name].get("rdfs:subClassOf"):
198
+ super_type_name = super_type_name.replace("/profiles/", "").replace(".json", "")
199
+ if super_type_name != "Item":
200
+ if not super_type_map.get(super_type_name):
201
+ super_type_map[super_type_name] = [type_name]
202
+ elif type_name not in super_type_map[super_type_name]:
203
+ super_type_map[super_type_name].append(type_name)
204
+ super_type_map_flattened = {}
205
+ for super_type_name in super_type_map:
206
+ super_type_map_flattened[super_type_name] = breadth_first(super_type_map, super_type_name)
207
+ return super_type_map_flattened
208
+
209
+ def _uri(self, uri: str) -> str:
210
+ if not isinstance(uri, str) or not uri:
211
+ return "/"
212
+ if uri.lower().startswith("http://") or uri.lower().startswith("https://"):
213
+ return uri
214
+ uri = re.sub(r"/+", "/", uri)
215
+ return (self._server + ("/" if uri.startswith("/") else "") + uri) if self._server else uri
216
+
217
+ def _kwargs(self, **kwargs) -> dict:
218
+ result_kwargs = {"headers":
219
+ kwargs.get("headers", {"Content-type": "application/json", "Accept": "application/json"})}
220
+ if self._key_pair:
221
+ result_kwargs["auth"] = self._key_pair
222
+ if isinstance(timeout := kwargs.get("timeout"), int):
223
+ result_kwargs["timeout"] = timeout
224
+ return result_kwargs
225
+
226
+ def _response(self, response) -> Optional[RequestResponse]:
227
+ if response and isinstance(getattr(response.__class__, "json"), property):
228
+ class RequestResponseWrapper: # For consistency change json property to method.
229
+ def __init__(self, response, **kwargs):
230
+ super().__init__(**kwargs)
231
+ self._response = response
232
+ def __getattr__(self, attr): # noqa
233
+ return getattr(self._response, attr)
234
+ def json(self): # noqa
235
+ return self._response.json
236
+ response = RequestResponseWrapper(response)
237
+ return response
238
+
239
+ @staticmethod
240
+ def create_for_testing(ini_file: Optional[str] = None) -> Portal:
241
+ if isinstance(ini_file, str):
242
+ return Portal(Portal._create_vapp(ini_file))
243
+ minimal_ini_for_unit_testing = "[app:app]\nuse = egg:encoded\nsqlalchemy.url = postgresql://dummy\n"
244
+ with temporary_file(content=minimal_ini_for_unit_testing, suffix=".ini") as ini_file:
245
+ return Portal(Portal._create_vapp(ini_file))
246
+
247
+ @staticmethod
248
+ def create_for_testing_local(ini_file: Optional[str] = None) -> Portal:
249
+ if isinstance(ini_file, str) and ini_file:
250
+ return Portal(Portal._create_vapp(ini_file))
251
+ minimal_ini_for_testing_local = "\n".join([
252
+ "[app:app]\nuse = egg:encoded\nfile_upload_bucket = dummy",
253
+ "sqlalchemy.url = postgresql://postgres@localhost:5441/postgres?host=/tmp/snovault/pgdata",
254
+ "multiauth.groupfinder = encoded.authorization.smaht_groupfinder",
255
+ "multiauth.policies = auth0 session remoteuser accesskey",
256
+ "multiauth.policy.session.namespace = mailto",
257
+ "multiauth.policy.session.use = encoded.authentication.NamespacedAuthenticationPolicy",
258
+ "multiauth.policy.session.base = pyramid.authentication.SessionAuthenticationPolicy",
259
+ "multiauth.policy.remoteuser.namespace = remoteuser",
260
+ "multiauth.policy.remoteuser.use = encoded.authentication.NamespacedAuthenticationPolicy",
261
+ "multiauth.policy.remoteuser.base = pyramid.authentication.RemoteUserAuthenticationPolicy",
262
+ "multiauth.policy.accesskey.namespace = accesskey",
263
+ "multiauth.policy.accesskey.use = encoded.authentication.NamespacedAuthenticationPolicy",
264
+ "multiauth.policy.accesskey.base = encoded.authentication.BasicAuthAuthenticationPolicy",
265
+ "multiauth.policy.accesskey.check = encoded.authentication.basic_auth_check",
266
+ "multiauth.policy.auth0.use = encoded.authentication.NamespacedAuthenticationPolicy",
267
+ "multiauth.policy.auth0.namespace = auth0",
268
+ "multiauth.policy.auth0.base = encoded.authentication.Auth0AuthenticationPolicy"
269
+ ])
270
+ with temporary_file(content=minimal_ini_for_testing_local, suffix=".ini") as minimal_ini_file:
271
+ return Portal(Portal._create_vapp(minimal_ini_file))
272
+
273
+ @staticmethod
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)