clear-skies 1.19.22__py3-none-any.whl → 2.0.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. clear_skies-2.0.23.dist-info/METADATA +76 -0
  2. clear_skies-2.0.23.dist-info/RECORD +265 -0
  3. {clear_skies-1.19.22.dist-info → clear_skies-2.0.23.dist-info}/WHEEL +1 -1
  4. clearskies/__init__.py +37 -21
  5. clearskies/action.py +7 -0
  6. clearskies/authentication/__init__.py +9 -38
  7. clearskies/authentication/authentication.py +44 -0
  8. clearskies/authentication/authorization.py +14 -8
  9. clearskies/authentication/authorization_pass_through.py +22 -0
  10. clearskies/authentication/jwks.py +135 -58
  11. clearskies/authentication/public.py +3 -26
  12. clearskies/authentication/secret_bearer.py +515 -44
  13. clearskies/autodoc/formats/oai3_json/__init__.py +2 -2
  14. clearskies/autodoc/formats/oai3_json/oai3_json.py +11 -9
  15. clearskies/autodoc/formats/oai3_json/parameter.py +6 -3
  16. clearskies/autodoc/formats/oai3_json/request.py +7 -5
  17. clearskies/autodoc/formats/oai3_json/response.py +7 -4
  18. clearskies/autodoc/formats/oai3_json/schema/object.py +10 -1
  19. clearskies/autodoc/request/__init__.py +2 -0
  20. clearskies/autodoc/request/header.py +4 -6
  21. clearskies/autodoc/request/json_body.py +4 -6
  22. clearskies/autodoc/request/parameter.py +8 -0
  23. clearskies/autodoc/request/request.py +16 -4
  24. clearskies/autodoc/request/url_parameter.py +4 -6
  25. clearskies/autodoc/request/url_path.py +4 -6
  26. clearskies/autodoc/schema/__init__.py +4 -2
  27. clearskies/autodoc/schema/array.py +5 -6
  28. clearskies/autodoc/schema/boolean.py +4 -10
  29. clearskies/autodoc/schema/date.py +0 -3
  30. clearskies/autodoc/schema/datetime.py +1 -4
  31. clearskies/autodoc/schema/double.py +0 -3
  32. clearskies/autodoc/schema/enum.py +4 -2
  33. clearskies/autodoc/schema/integer.py +4 -9
  34. clearskies/autodoc/schema/long.py +0 -3
  35. clearskies/autodoc/schema/number.py +4 -9
  36. clearskies/autodoc/schema/object.py +5 -7
  37. clearskies/autodoc/schema/password.py +0 -3
  38. clearskies/autodoc/schema/schema.py +11 -0
  39. clearskies/autodoc/schema/string.py +4 -10
  40. clearskies/backends/__init__.py +56 -17
  41. clearskies/backends/api_backend.py +1128 -166
  42. clearskies/backends/backend.py +54 -85
  43. clearskies/backends/cursor_backend.py +246 -191
  44. clearskies/backends/memory_backend.py +514 -208
  45. clearskies/backends/secrets_backend.py +68 -31
  46. clearskies/column.py +1221 -0
  47. clearskies/columns/__init__.py +71 -0
  48. clearskies/columns/audit.py +306 -0
  49. clearskies/columns/belongs_to_id.py +478 -0
  50. clearskies/columns/belongs_to_model.py +129 -0
  51. clearskies/columns/belongs_to_self.py +109 -0
  52. clearskies/columns/boolean.py +110 -0
  53. clearskies/columns/category_tree.py +273 -0
  54. clearskies/columns/category_tree_ancestors.py +51 -0
  55. clearskies/columns/category_tree_children.py +126 -0
  56. clearskies/columns/category_tree_descendants.py +48 -0
  57. clearskies/columns/created.py +92 -0
  58. clearskies/columns/created_by_authorization_data.py +114 -0
  59. clearskies/columns/created_by_header.py +103 -0
  60. clearskies/columns/created_by_ip.py +90 -0
  61. clearskies/columns/created_by_routing_data.py +102 -0
  62. clearskies/columns/created_by_user_agent.py +89 -0
  63. clearskies/columns/date.py +232 -0
  64. clearskies/columns/datetime.py +284 -0
  65. clearskies/columns/email.py +78 -0
  66. clearskies/columns/float.py +149 -0
  67. clearskies/columns/has_many.py +529 -0
  68. clearskies/columns/has_many_self.py +62 -0
  69. clearskies/columns/has_one.py +21 -0
  70. clearskies/columns/integer.py +158 -0
  71. clearskies/columns/json.py +126 -0
  72. clearskies/columns/many_to_many_ids.py +335 -0
  73. clearskies/columns/many_to_many_ids_with_data.py +274 -0
  74. clearskies/columns/many_to_many_models.py +156 -0
  75. clearskies/columns/many_to_many_pivots.py +132 -0
  76. clearskies/columns/phone.py +162 -0
  77. clearskies/columns/select.py +95 -0
  78. clearskies/columns/string.py +102 -0
  79. clearskies/columns/timestamp.py +164 -0
  80. clearskies/columns/updated.py +107 -0
  81. clearskies/columns/uuid.py +83 -0
  82. clearskies/configs/README.md +105 -0
  83. clearskies/configs/__init__.py +170 -0
  84. clearskies/configs/actions.py +43 -0
  85. clearskies/configs/any.py +15 -0
  86. clearskies/configs/any_dict.py +24 -0
  87. clearskies/configs/any_dict_or_callable.py +25 -0
  88. clearskies/configs/authentication.py +23 -0
  89. clearskies/configs/authorization.py +23 -0
  90. clearskies/configs/boolean.py +18 -0
  91. clearskies/configs/boolean_or_callable.py +20 -0
  92. clearskies/configs/callable_config.py +20 -0
  93. clearskies/configs/columns.py +34 -0
  94. clearskies/configs/conditions.py +30 -0
  95. clearskies/configs/config.py +26 -0
  96. clearskies/configs/datetime.py +20 -0
  97. clearskies/configs/datetime_or_callable.py +21 -0
  98. clearskies/configs/email.py +10 -0
  99. clearskies/configs/email_list.py +17 -0
  100. clearskies/configs/email_list_or_callable.py +17 -0
  101. clearskies/configs/email_or_email_list_or_callable.py +59 -0
  102. clearskies/configs/endpoint.py +23 -0
  103. clearskies/configs/endpoint_list.py +29 -0
  104. clearskies/configs/float.py +18 -0
  105. clearskies/configs/float_or_callable.py +20 -0
  106. clearskies/configs/headers.py +28 -0
  107. clearskies/configs/integer.py +18 -0
  108. clearskies/configs/integer_or_callable.py +20 -0
  109. clearskies/configs/joins.py +30 -0
  110. clearskies/configs/list_any_dict.py +32 -0
  111. clearskies/configs/list_any_dict_or_callable.py +33 -0
  112. clearskies/configs/model_class.py +35 -0
  113. clearskies/configs/model_column.py +67 -0
  114. clearskies/configs/model_columns.py +58 -0
  115. clearskies/configs/model_destination_name.py +26 -0
  116. clearskies/configs/model_to_id_column.py +45 -0
  117. clearskies/configs/readable_model_column.py +11 -0
  118. clearskies/configs/readable_model_columns.py +11 -0
  119. clearskies/configs/schema.py +23 -0
  120. clearskies/configs/searchable_model_columns.py +11 -0
  121. clearskies/configs/security_headers.py +39 -0
  122. clearskies/configs/select.py +28 -0
  123. clearskies/configs/select_list.py +49 -0
  124. clearskies/configs/string.py +31 -0
  125. clearskies/configs/string_dict.py +34 -0
  126. clearskies/configs/string_list.py +47 -0
  127. clearskies/configs/string_list_or_callable.py +48 -0
  128. clearskies/configs/string_or_callable.py +18 -0
  129. clearskies/configs/timedelta.py +20 -0
  130. clearskies/configs/timezone.py +20 -0
  131. clearskies/configs/url.py +25 -0
  132. clearskies/configs/validators.py +45 -0
  133. clearskies/configs/writeable_model_column.py +11 -0
  134. clearskies/configs/writeable_model_columns.py +11 -0
  135. clearskies/configurable.py +78 -0
  136. clearskies/contexts/__init__.py +8 -8
  137. clearskies/contexts/cli.py +129 -43
  138. clearskies/contexts/context.py +93 -56
  139. clearskies/contexts/wsgi.py +79 -33
  140. clearskies/contexts/wsgi_ref.py +87 -0
  141. clearskies/cursors/__init__.py +7 -0
  142. clearskies/cursors/cursor.py +166 -0
  143. clearskies/cursors/from_environment/__init__.py +5 -0
  144. clearskies/cursors/from_environment/mysql.py +51 -0
  145. clearskies/cursors/from_environment/postgresql.py +49 -0
  146. clearskies/cursors/from_environment/sqlite.py +35 -0
  147. clearskies/cursors/mysql.py +61 -0
  148. clearskies/cursors/postgresql.py +61 -0
  149. clearskies/cursors/sqlite.py +62 -0
  150. clearskies/decorators.py +33 -0
  151. clearskies/decorators.pyi +10 -0
  152. clearskies/di/__init__.py +11 -7
  153. clearskies/di/additional_config.py +117 -3
  154. clearskies/di/additional_config_auto_import.py +12 -0
  155. clearskies/di/di.py +717 -126
  156. clearskies/di/inject/__init__.py +23 -0
  157. clearskies/di/inject/akeyless_sdk.py +16 -0
  158. clearskies/di/inject/by_class.py +24 -0
  159. clearskies/di/inject/by_name.py +22 -0
  160. clearskies/di/inject/di.py +16 -0
  161. clearskies/di/inject/environment.py +15 -0
  162. clearskies/di/inject/input_output.py +19 -0
  163. clearskies/di/inject/now.py +16 -0
  164. clearskies/di/inject/requests.py +16 -0
  165. clearskies/di/inject/secrets.py +15 -0
  166. clearskies/di/inject/utcnow.py +16 -0
  167. clearskies/di/inject/uuid.py +16 -0
  168. clearskies/di/injectable.py +32 -0
  169. clearskies/di/injectable_properties.py +131 -0
  170. clearskies/end.py +219 -0
  171. clearskies/endpoint.py +1303 -0
  172. clearskies/endpoint_group.py +333 -0
  173. clearskies/endpoints/__init__.py +25 -0
  174. clearskies/endpoints/advanced_search.py +519 -0
  175. clearskies/endpoints/callable.py +382 -0
  176. clearskies/endpoints/create.py +201 -0
  177. clearskies/endpoints/delete.py +133 -0
  178. clearskies/endpoints/get.py +267 -0
  179. clearskies/endpoints/health_check.py +181 -0
  180. clearskies/endpoints/list.py +567 -0
  181. clearskies/endpoints/restful_api.py +417 -0
  182. clearskies/endpoints/schema.py +185 -0
  183. clearskies/endpoints/simple_search.py +279 -0
  184. clearskies/endpoints/update.py +188 -0
  185. clearskies/environment.py +7 -3
  186. clearskies/exceptions/__init__.py +19 -0
  187. clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
  188. clearskies/exceptions/missing_dependency.py +2 -0
  189. clearskies/exceptions/moved_permanently.py +3 -0
  190. clearskies/exceptions/moved_temporarily.py +3 -0
  191. clearskies/functional/__init__.py +2 -2
  192. clearskies/functional/json.py +47 -0
  193. clearskies/functional/routing.py +92 -0
  194. clearskies/functional/string.py +19 -11
  195. clearskies/functional/validations.py +61 -9
  196. clearskies/input_outputs/__init__.py +9 -7
  197. clearskies/input_outputs/cli.py +135 -152
  198. clearskies/input_outputs/exceptions/__init__.py +6 -1
  199. clearskies/input_outputs/headers.py +54 -0
  200. clearskies/input_outputs/input_output.py +77 -123
  201. clearskies/input_outputs/programmatic.py +62 -0
  202. clearskies/input_outputs/wsgi.py +36 -48
  203. clearskies/model.py +1894 -199
  204. clearskies/query/__init__.py +12 -0
  205. clearskies/query/condition.py +228 -0
  206. clearskies/query/join.py +136 -0
  207. clearskies/query/query.py +193 -0
  208. clearskies/query/sort.py +27 -0
  209. clearskies/schema.py +82 -0
  210. clearskies/secrets/__init__.py +4 -31
  211. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
  212. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
  213. clearskies/secrets/akeyless.py +421 -155
  214. clearskies/secrets/exceptions/__init__.py +7 -1
  215. clearskies/secrets/exceptions/not_found_error.py +2 -0
  216. clearskies/secrets/exceptions/permissions_error.py +2 -0
  217. clearskies/secrets/secrets.py +12 -11
  218. clearskies/security_header.py +17 -0
  219. clearskies/security_headers/__init__.py +8 -8
  220. clearskies/security_headers/cache_control.py +47 -109
  221. clearskies/security_headers/cors.py +38 -92
  222. clearskies/security_headers/csp.py +76 -150
  223. clearskies/security_headers/hsts.py +14 -15
  224. clearskies/typing.py +11 -0
  225. clearskies/validator.py +36 -0
  226. clearskies/validators/__init__.py +33 -0
  227. clearskies/validators/after_column.py +61 -0
  228. clearskies/validators/before_column.py +15 -0
  229. clearskies/validators/in_the_future.py +29 -0
  230. clearskies/validators/in_the_future_at_least.py +13 -0
  231. clearskies/validators/in_the_future_at_most.py +12 -0
  232. clearskies/validators/in_the_past.py +29 -0
  233. clearskies/validators/in_the_past_at_least.py +12 -0
  234. clearskies/validators/in_the_past_at_most.py +12 -0
  235. clearskies/validators/maximum_length.py +25 -0
  236. clearskies/validators/maximum_value.py +28 -0
  237. clearskies/validators/minimum_length.py +25 -0
  238. clearskies/validators/minimum_value.py +28 -0
  239. clearskies/{input_requirements → validators}/required.py +18 -9
  240. clearskies/validators/timedelta.py +58 -0
  241. clearskies/validators/unique.py +28 -0
  242. clear_skies-1.19.22.dist-info/METADATA +0 -46
  243. clear_skies-1.19.22.dist-info/RECORD +0 -206
  244. clearskies/application.py +0 -29
  245. clearskies/authentication/auth0_jwks.py +0 -118
  246. clearskies/authentication/auth_exception.py +0 -2
  247. clearskies/authentication/jwks_jwcrypto.py +0 -39
  248. clearskies/backends/example_backend.py +0 -43
  249. clearskies/backends/file_backend.py +0 -48
  250. clearskies/backends/json_backend.py +0 -7
  251. clearskies/backends/restful_api_advanced_search_backend.py +0 -138
  252. clearskies/binding_config.py +0 -16
  253. clearskies/column_types/__init__.py +0 -184
  254. clearskies/column_types/audit.py +0 -235
  255. clearskies/column_types/belongs_to.py +0 -250
  256. clearskies/column_types/boolean.py +0 -60
  257. clearskies/column_types/category_tree.py +0 -226
  258. clearskies/column_types/column.py +0 -373
  259. clearskies/column_types/created.py +0 -26
  260. clearskies/column_types/created_by_authorization_data.py +0 -26
  261. clearskies/column_types/created_by_header.py +0 -24
  262. clearskies/column_types/created_by_ip.py +0 -17
  263. clearskies/column_types/created_by_routing_data.py +0 -25
  264. clearskies/column_types/created_by_user_agent.py +0 -17
  265. clearskies/column_types/created_micro.py +0 -26
  266. clearskies/column_types/datetime.py +0 -108
  267. clearskies/column_types/datetime_micro.py +0 -12
  268. clearskies/column_types/email.py +0 -18
  269. clearskies/column_types/float.py +0 -43
  270. clearskies/column_types/has_many.py +0 -139
  271. clearskies/column_types/integer.py +0 -41
  272. clearskies/column_types/json.py +0 -25
  273. clearskies/column_types/many_to_many.py +0 -278
  274. clearskies/column_types/many_to_many_with_data.py +0 -162
  275. clearskies/column_types/select.py +0 -11
  276. clearskies/column_types/string.py +0 -24
  277. clearskies/column_types/updated.py +0 -24
  278. clearskies/column_types/updated_micro.py +0 -24
  279. clearskies/column_types/uuid.py +0 -25
  280. clearskies/columns.py +0 -123
  281. clearskies/condition_parser.py +0 -172
  282. clearskies/contexts/build_context.py +0 -54
  283. clearskies/contexts/convert_to_application.py +0 -190
  284. clearskies/contexts/extract_handler.py +0 -37
  285. clearskies/contexts/test.py +0 -94
  286. clearskies/decorators/__init__.py +0 -39
  287. clearskies/decorators/auth0_jwks.py +0 -22
  288. clearskies/decorators/authorization.py +0 -10
  289. clearskies/decorators/binding_classes.py +0 -9
  290. clearskies/decorators/binding_modules.py +0 -9
  291. clearskies/decorators/bindings.py +0 -9
  292. clearskies/decorators/create.py +0 -10
  293. clearskies/decorators/delete.py +0 -10
  294. clearskies/decorators/docs.py +0 -14
  295. clearskies/decorators/get.py +0 -10
  296. clearskies/decorators/jwks.py +0 -26
  297. clearskies/decorators/merge.py +0 -124
  298. clearskies/decorators/patch.py +0 -10
  299. clearskies/decorators/post.py +0 -10
  300. clearskies/decorators/public.py +0 -11
  301. clearskies/decorators/response_headers.py +0 -10
  302. clearskies/decorators/return_raw_response.py +0 -9
  303. clearskies/decorators/schema.py +0 -10
  304. clearskies/decorators/secret_bearer.py +0 -24
  305. clearskies/decorators/security_headers.py +0 -10
  306. clearskies/di/standard_dependencies.py +0 -140
  307. clearskies/di/test_module/__init__.py +0 -6
  308. clearskies/di/test_module/another_module/__init__.py +0 -2
  309. clearskies/di/test_module/module_class.py +0 -5
  310. clearskies/handlers/__init__.py +0 -41
  311. clearskies/handlers/advanced_search.py +0 -271
  312. clearskies/handlers/base.py +0 -473
  313. clearskies/handlers/callable.py +0 -189
  314. clearskies/handlers/create.py +0 -35
  315. clearskies/handlers/crud_by_method.py +0 -18
  316. clearskies/handlers/database_connector.py +0 -32
  317. clearskies/handlers/delete.py +0 -61
  318. clearskies/handlers/exceptions/__init__.py +0 -5
  319. clearskies/handlers/exceptions/not_found.py +0 -3
  320. clearskies/handlers/get.py +0 -156
  321. clearskies/handlers/health_check.py +0 -59
  322. clearskies/handlers/input_processing.py +0 -79
  323. clearskies/handlers/list.py +0 -530
  324. clearskies/handlers/mygrations.py +0 -82
  325. clearskies/handlers/request_method_routing.py +0 -47
  326. clearskies/handlers/restful_api.py +0 -218
  327. clearskies/handlers/routing.py +0 -62
  328. clearskies/handlers/schema_helper.py +0 -128
  329. clearskies/handlers/simple_routing.py +0 -204
  330. clearskies/handlers/simple_routing_route.py +0 -192
  331. clearskies/handlers/simple_search.py +0 -136
  332. clearskies/handlers/update.py +0 -96
  333. clearskies/handlers/write.py +0 -193
  334. clearskies/input_requirements/__init__.py +0 -68
  335. clearskies/input_requirements/after.py +0 -36
  336. clearskies/input_requirements/before.py +0 -36
  337. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  338. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  339. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  340. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  341. clearskies/input_requirements/maximum_length.py +0 -19
  342. clearskies/input_requirements/minimum_length.py +0 -22
  343. clearskies/input_requirements/requirement.py +0 -25
  344. clearskies/input_requirements/time_delta.py +0 -38
  345. clearskies/input_requirements/unique.py +0 -18
  346. clearskies/mocks/__init__.py +0 -7
  347. clearskies/mocks/input_output.py +0 -124
  348. clearskies/mocks/models.py +0 -142
  349. clearskies/models.py +0 -345
  350. clearskies/security_headers/base.py +0 -12
  351. clearskies/tests/simple_api/models/__init__.py +0 -2
  352. clearskies/tests/simple_api/models/status.py +0 -23
  353. clearskies/tests/simple_api/models/user.py +0 -21
  354. clearskies/tests/simple_api/users_api.py +0 -64
  355. {clear_skies-1.19.22.dist-info → clear_skies-2.0.23.dist-info/licenses}/LICENSE +0 -0
  356. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  357. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  358. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  359. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  360. /clearskies/{secrets/exceptions → exceptions}/not_found.py +0 -0
  361. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  362. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -1,473 +0,0 @@
1
- from abc import ABC, abstractmethod
2
- from . import exceptions
3
- from collections import OrderedDict
4
- import inspect
5
- import re
6
- from ..autodoc.schema import Integer as AutoDocInteger
7
- from ..autodoc.schema import String as AutoDocString
8
- from ..autodoc.schema import Object as AutoDocObject
9
- from ..autodoc.response import Response as AutoDocResponse
10
- from ..functional import string
11
- from typing import List, Dict
12
-
13
-
14
- class Base(ABC):
15
- _configuration = None
16
- _configuration_defaults = {}
17
- _as_json_map = None
18
- _global_configuration_defaults = {
19
- "base_url": "",
20
- "response_headers": None,
21
- "authentication": None,
22
- "authorization": None,
23
- "output_map": None,
24
- "column_overrides": None,
25
- "id_column_name": None,
26
- "doc_description": "",
27
- "internal_casing": "",
28
- "external_casing": "",
29
- "security_headers": None,
30
- }
31
- _di = None
32
- _configuration = None
33
- _cors_header = None
34
- has_cors = False
35
-
36
- def __init__(self, di):
37
- self._di = di
38
- self._configuration = None
39
-
40
- @abstractmethod
41
- def handle(self):
42
- pass
43
-
44
- def configure(self, configuration):
45
- for key in configuration.keys():
46
- if key not in self._configuration_defaults and key not in self._global_configuration_defaults:
47
- class_name = self.__class__.__name__
48
- raise KeyError(f"Attempt to set unknown configuration setting '{key}' for handler '{class_name}'")
49
-
50
- self._check_configuration(configuration)
51
- self._configuration = self._finalize_configuration(self.apply_default_configuration(configuration))
52
-
53
- def _check_configuration(self, configuration):
54
- if not "authentication" in configuration:
55
- raise KeyError(
56
- f"You must provide authentication in the configuration for handler '{self.__class__.__name__}'"
57
- )
58
- if configuration.get("authorization", None):
59
- # authorization can be a function (in which case we'll just call it for gating) or it can be an object
60
- # with 'gate' and 'filter_models' attributes, per the authentication.authorization base class
61
- # or it can be a binding config
62
- authorization = configuration["authorization"]
63
- if type(authorization) == str:
64
- # if we have a binding name then we need to build the authorization object
65
- authorization = self._di.build(authorization, cache=True)
66
- elif hasattr(authorization, "object_class"):
67
- # if it's a binding config then pull out the target class, since we just need to check attributes here
68
- authorization = authorization.object_class
69
- is_callable = callable(authorization)
70
- gates_or_filters = hasattr(authorization, "gate") or hasattr(authorization, "filter_models")
71
- if not is_callable and not gates_or_filters:
72
- raise ValueError("'authorization' should be a callable or a provide 'gate' or 'filter_models' methods")
73
- if configuration.get("output_map") is not None:
74
- if not callable(configuration["output_map"]):
75
- raise ValueError("'output_map' should be a callable")
76
- number_casings = 0
77
- internal_casing = configuration.get("internal_casing")
78
- if internal_casing and internal_casing not in string.casings:
79
- raise ValueError(
80
- f"Invalid internal_casing config for handler '{self.__class__.__name__}': expected one of "
81
- + "'"
82
- + ", '".join(string.casings)
83
- + f"' but found '{internal_casing}'"
84
- )
85
- number_casings += 1
86
- external_casing = configuration.get("external_casing")
87
- if external_casing and external_casing not in string.casings:
88
- raise ValueError(
89
- f"Invalid external_casing config for handler '{self.__class__.__name__}': expected one of "
90
- + "'"
91
- + ", '".join(string.casings)
92
- + f"' but found '{external_casing}'"
93
- )
94
- number_casings += 1
95
- if number_casings == 1:
96
- raise ValueError(
97
- f"Configuration error for handler '{self.__class__.__name__}': external_casing and internal_casing"
98
- + " must be specified together, but only one was found"
99
- )
100
- if "base_url" in configuration and configuration["base_url"] != None and type(configuration["base_url"]) != str:
101
- raise ValueError(
102
- f"Configuration error for handler '{self.__class__.__name__}': if provided, base_url must be a string"
103
- )
104
-
105
- def apply_default_configuration(self, configuration):
106
- return {
107
- **self._global_configuration_defaults,
108
- **self._configuration_defaults,
109
- **configuration,
110
- }
111
-
112
- def configuration(self, key):
113
- if self._configuration is None:
114
- raise ValueError("Cannot fetch configuration values before setting the configuration")
115
- if key not in self._configuration:
116
- class_name = self.__class__.__name__
117
- raise KeyError(f"Configuration key '{key}' does not exist for handler '{class_name}'")
118
- return self._configuration[key]
119
-
120
- def _finalize_configuration(self, configuration):
121
- configuration["authentication"] = self._di.build(configuration["authentication"], cache=True)
122
- authorization = configuration.get("authorization")
123
- if authorization and (hasattr(authorization, "object_class") or type(authorization) == str):
124
- configuration["authorization"] = self._di.build(configuration["authorization"], cache=True)
125
- if configuration.get("base_url") is None:
126
- configuration["base_url"] = "/"
127
- if not configuration["base_url"] or configuration["base_url"][0] != "/":
128
- configuration["base_url"] = "/" + configuration["base_url"]
129
- security_headers = configuration.get("security_headers")
130
- if not security_headers:
131
- configuration["security_headers"] = []
132
- else:
133
- # should be a list or a binding config. If it's a binding config, convert it to a list
134
- if hasattr(security_headers, "object_class"):
135
- security_headers = [security_headers]
136
- if type(security_headers) != list:
137
- raise ValueError(
138
- f"Configuration error for handler '{self.__class__.__name__}': if provided, security_headers must be a list or binding config"
139
- )
140
- final_security_headers = []
141
- for index, security_header in enumerate(security_headers):
142
- if hasattr(security_header, "object_class"):
143
- security_header = self._di.build(security_header, cache=True)
144
- if not hasattr(security_header, "set_headers_for_input_output"):
145
- raise ValueError(
146
- f"Configuration error for handler '{self.__class__.__name__}': security header #{index+1} did not resolve to a security header"
147
- )
148
- if security_header.is_cors:
149
- self._cors_header = security_header
150
- self.has_cors = True
151
- final_security_headers.append(security_header)
152
- configuration["security_headers"] = final_security_headers
153
- return configuration
154
-
155
- def top_level_authentication_and_authorization(self, input_output, authentication=None):
156
- if authentication is None:
157
- authentication = self._configuration.get("authentication")
158
- if not authentication:
159
- return
160
- try:
161
- if not authentication.authenticate(input_output):
162
- raise exceptions.Authentication("Not Authenticated")
163
- except exceptions.ClientError as client_error:
164
- raise exceptions.Authentication(str(client_error))
165
- authorization = self._configuration.get("authorization")
166
- if authorization:
167
- authorization_data = input_output.get_authorization_data()
168
- try:
169
- allowed = True
170
- if hasattr(authorization, "gate"):
171
- allowed = authorization.gate(authorization_data, input_output)
172
- elif callable(authorization):
173
- allowed = authorization(authorization_data, input_output)
174
- if not allowed:
175
- raise exceptions.Authorization("Not Authorized")
176
- except exceptions.ClientError as client_error:
177
- raise exception.Authorization(str(client_error))
178
-
179
- def __call__(self, input_output):
180
- self._di.bind("input_output", input_output)
181
- if self._configuration is None:
182
- raise ValueError("Must configure handler before calling")
183
- try:
184
- self.top_level_authentication_and_authorization(input_output)
185
- except exceptions.Authentication as auth_error:
186
- return self.error(input_output, str(auth_error), 401)
187
- except exceptions.Authorization as auth_error:
188
- return self.error(input_output, str(auth_error), 403)
189
- except exceptions.NotFound as auth_error:
190
- return self.error(input_output, str(auth_error), 404)
191
-
192
- try:
193
- response = self.handle(input_output)
194
- except exceptions.ClientError as client_error:
195
- return self.error(input_output, str(client_error), 400)
196
- except exceptions.InputError as input_error:
197
- return self.input_errors(input_output, input_error.errors)
198
-
199
- return response
200
-
201
- def input_errors(self, input_output, errors, status_code=200):
202
- return self.respond(input_output, {"status": "input_errors", "input_errors": errors}, status_code)
203
-
204
- def error(self, input_output, message, status_code):
205
- return self.respond(input_output, {"status": "client_error", "error": message}, status_code)
206
-
207
- def success(self, input_output, data, number_results=None, limit=None, next_page=None):
208
- response_data = {"status": "success", "data": data, "pagination": {}}
209
-
210
- if number_results is not None:
211
- for value in [number_results, limit]:
212
- if value is not None and type(value) != int:
213
- raise ValueError("number_results and limit must all be integers")
214
-
215
- response_data["pagination"] = {
216
- "number_results": number_results,
217
- "limit": limit,
218
- "next_page": next_page,
219
- }
220
-
221
- return self.respond(input_output, response_data, 200)
222
-
223
- def respond(self, input_output, response_data, status_code):
224
- response_headers = self.configuration("response_headers")
225
- if response_headers:
226
- input_output.set_headers(response_headers)
227
- for security_header in self.configuration("security_headers"):
228
- security_header.set_headers_for_input_output(input_output)
229
- return input_output.respond(self._normalize_response(response_data), status_code)
230
-
231
- def _normalize_response(self, response_data):
232
- if not "status" in response_data:
233
- raise ValueError("Huh, status got left out somehow")
234
- return {
235
- self.auto_case_internal_column_name("status"): self.auto_case_internal_column_name(response_data["status"]),
236
- self.auto_case_internal_column_name("error"): response_data.get("error", ""),
237
- self.auto_case_internal_column_name("data"): response_data.get("data", []),
238
- self.auto_case_internal_column_name("pagination"): self._normalize_pagination(
239
- response_data.get("pagination", {})
240
- ),
241
- self.auto_case_internal_column_name("input_errors"): response_data.get("input_errors", {}),
242
- }
243
-
244
- def _normalize_pagination(self, pagination):
245
- # pagination isn't always relevant so if it is completely empty then leave it that way
246
- if not pagination:
247
- return pagination
248
- return {
249
- self.auto_case_internal_column_name("number_results"): pagination.get("number_results", 0),
250
- self.auto_case_internal_column_name("limit"): pagination.get("limit", 0),
251
- self.auto_case_internal_column_name("next_page"): {
252
- self.auto_case_internal_column_name(key): value
253
- for (key, value) in pagination.get("next_page", {}).items()
254
- },
255
- }
256
-
257
- def _model_as_json(self, model, input_output):
258
- if self.configuration("output_map"):
259
- return self._di.call_function(self.configuration("output_map"), model=model)
260
-
261
- if self._as_json_map is None:
262
- self._as_json_map = self._build_as_json_map(model)
263
-
264
- json = OrderedDict()
265
- for output_name, column in self._as_json_map.items():
266
- column_data = column.to_json(model)
267
- if len(column_data) == 1:
268
- json[output_name] = list(column_data.values())[0]
269
- else:
270
- for key, value in column_data.items():
271
- json[self.auto_case_column_name(key, True)] = value
272
- return json
273
-
274
- def _build_as_json_map(self, model):
275
- conversion_map = {}
276
- if self.configuration("id_column_name"):
277
- conversion_map[self.auto_case_internal_column_name("id")] = model.columns()[self.id_column_name]
278
-
279
- for column in self._get_readable_columns().values():
280
- conversion_map[self.auto_case_column_name(column.name, True)] = column
281
- return conversion_map
282
-
283
- def auto_case_internal_column_name(self, column_name):
284
- if self._configuration["external_casing"]:
285
- return string.swap_casing(column_name, "snake_case", self._configuration["external_casing"])
286
- return column_name
287
-
288
- def auto_case_to_internal_column_name(self, column_name):
289
- if self._configuration["external_casing"]:
290
- return string.swap_casing(column_name, self._configuration["external_casing"], "snake_case")
291
- return column_name
292
-
293
- def auto_case_column_name(self, column_name, internal_to_external):
294
- if not self._configuration["internal_casing"]:
295
- return column_name
296
- if internal_to_external:
297
- return string.swap_casing(
298
- column_name,
299
- self._configuration["internal_casing"],
300
- self._configuration["external_casing"],
301
- )
302
- return string.swap_casing(
303
- column_name,
304
- self._configuration["external_casing"],
305
- self._configuration["internal_casing"],
306
- )
307
-
308
- @property
309
- def id_column_name(self) -> str:
310
- """
311
- This returns the name of the id column to use for requests
312
-
313
- There are three ways to determine the id column:
314
-
315
- 1. It may be defined in the handler configuration.
316
- 2. It may be overridden in the model class
317
- 3. It defaults to 'id'
318
-
319
- The first happens if the developer wants to expose a different "id" column to the client.
320
- The second happens if the developer wants to use a different id column internally.
321
- The third is the clearskies default.
322
-
323
- The first is easy to detect because the dev will set `id_column_name` in the handler config.
324
- The second happens if the model class defines a different `id_column_name` property in the model class.
325
- However, this is tricky because there is nothing in this base case that allows us to pull up the model.
326
- In fact, not all handlers use a model, or they may use multiple models, etc... Still, it's pretty
327
- common for the handler to have a configuration named `model_class` or `model`, so let's check for that and assume
328
- the handler will only ask for the id_column_name() if the handler has a `self.configuration('model_class')`
329
- """
330
- id_column_name = self.configuration("id_column_name")
331
- if id_column_name is not None:
332
- return id_column_name
333
- if not self._configuration.get("model_class", False) and not self._configuration.get("model", False):
334
- raise KeyError(
335
- "To properly use handler.id_column_name, the handler must have a 'model_class' or 'model' configuration key"
336
- )
337
- if self._configuration.get("model_class", False):
338
- return self._configuration.get("model_class").id_column_name
339
- return self._configuration.get("model").id_column_name
340
-
341
- def cors(self, input_output):
342
- cors = self._cors_header
343
- if not cors:
344
- return self.error(input_output, "not found", 404)
345
- authentication = self._configuration.get("authentication")
346
- if authentication:
347
- authentication.set_headers_for_cors(cors)
348
- cors.set_headers_for_input_output(input_output)
349
- return input_output.respond("", 200)
350
-
351
- def documentation(self):
352
- return []
353
-
354
- def documentation_components(self):
355
- return {
356
- "models": self.documentation_models(),
357
- "securitySchemes": self.documentation_security_schemes(),
358
- }
359
-
360
- def documentation_security_schemes(self):
361
- authentication = self._configuration.get("authentication")
362
- if not authentication or not authentication.documentation_security_scheme_name():
363
- return {}
364
-
365
- return {
366
- authentication.documentation_security_scheme_name(): authentication.documentation_security_scheme(),
367
- }
368
-
369
- def documentation_models(self):
370
- return {}
371
-
372
- def documentation_pagination_response(self, include_pagination=True):
373
- if not include_pagination:
374
- return AutoDocObject(self.auto_case_internal_column_name("pagination"), [], value={})
375
- return AutoDocObject(
376
- self.auto_case_internal_column_name("pagination"),
377
- [
378
- AutoDocInteger(self.auto_case_internal_column_name("number_results"), example=10),
379
- AutoDocInteger(self.auto_case_internal_column_name("limit"), example=100),
380
- AutoDocObject(
381
- self.auto_case_internal_column_name("next_page"),
382
- self._model.documentation_pagination_next_page_response(self.auto_case_internal_column_name),
383
- self._model.documentation_pagination_next_page_example(self.auto_case_internal_column_name),
384
- ),
385
- ],
386
- )
387
-
388
- def documentation_success_response(self, data_schema, description="", include_pagination=False):
389
- return AutoDocResponse(
390
- 200,
391
- AutoDocObject(
392
- "body",
393
- [
394
- AutoDocString(self.auto_case_internal_column_name("status"), value="success"),
395
- data_schema,
396
- self.documentation_pagination_response(include_pagination=include_pagination),
397
- AutoDocString(self.auto_case_internal_column_name("error"), value=""),
398
- AutoDocObject(self.auto_case_internal_column_name("input_errors"), [], value={}),
399
- ],
400
- ),
401
- description=description,
402
- )
403
-
404
- def documentation_generic_error_response(self, description="Invalid Call", status=400):
405
- return AutoDocResponse(
406
- status,
407
- AutoDocObject(
408
- "body",
409
- [
410
- AutoDocString(self.auto_case_internal_column_name("status"), value="error"),
411
- AutoDocObject(self.auto_case_internal_column_name("data"), [], value={}),
412
- self.documentation_pagination_response(include_pagination=False),
413
- AutoDocString(self.auto_case_internal_column_name("error"), example="User readable error message"),
414
- AutoDocObject(self.auto_case_internal_column_name("input_errors"), [], value={}),
415
- ],
416
- ),
417
- description=description,
418
- )
419
-
420
- def documentation_input_error_response(self, description="Invalid client-side input"):
421
- email_example = self.auto_case_internal_column_name("email")
422
- return AutoDocResponse(
423
- 200,
424
- AutoDocObject(
425
- "body",
426
- [
427
- AutoDocString(self.auto_case_internal_column_name("status"), value="input_errors"),
428
- AutoDocObject(self.auto_case_internal_column_name("data"), [], value={}),
429
- self.documentation_pagination_response(include_pagination=False),
430
- AutoDocString(self.auto_case_internal_column_name("error"), value=""),
431
- AutoDocObject(
432
- self.auto_case_internal_column_name("input_errors"),
433
- [AutoDocString("[COLUMN_NAME]", example="User friendly error message")],
434
- example={email_example: f"{email_example} was not a valid email address"},
435
- ),
436
- ],
437
- ),
438
- description=description,
439
- )
440
-
441
- def documentation_access_denied_response(self):
442
- return self.documentation_generic_error_response(description="Access Denied", status=401)
443
-
444
- def documentation_unauthorized_response(self):
445
- return self.documentation_generic_error_response(description="Unauthorized", status=403)
446
-
447
- def documentation_not_found(self):
448
- return self.documentation_generic_error_response(description="Not Found", status=404)
449
-
450
- def documentation_request_security(self):
451
- authentication = self.configuration("authentication")
452
- name = authentication.documentation_security_scheme_name()
453
- return [{name: []}] if name else []
454
-
455
- def documentation_data_schema(self):
456
- id_column_name = self.id_column_name
457
- properties = []
458
- if self.configuration("id_column_name"):
459
- properties.append(
460
- self._columns[id_column_name].documentation(name=self.auto_case_internal_column_name("id"))
461
- if id_column_name in self._columns
462
- else AutoDocString(self.auto_case_internal_column_name("id"))
463
- )
464
-
465
- for column in self._get_readable_columns().values():
466
- column_doc = column.documentation()
467
- if type(column_doc) != list:
468
- column_doc = [column_doc]
469
- for doc in column_doc:
470
- doc.name = self.auto_case_internal_column_name(doc.name)
471
- properties.append(doc)
472
-
473
- return properties
@@ -1,189 +0,0 @@
1
- from .base import Base
2
- from .schema_helper import SchemaHelper
3
- from .exceptions import InputError, ClientError, NotFound
4
- import inspect
5
- import json
6
- from ..functional import validations, string
7
- from .. import autodoc
8
-
9
-
10
- class Callable(Base, SchemaHelper):
11
- _columns = None
12
-
13
- _global_configuration_defaults = {
14
- "response_headers": None,
15
- "authentication": None,
16
- "authorization": None,
17
- "callable": None,
18
- "id_column_name": None,
19
- "doc_description": "",
20
- "internal_casing": "",
21
- "external_casing": "",
22
- "security_headers": None,
23
- }
24
-
25
- _configuration_defaults = {
26
- "base_url": "",
27
- "return_raw_response": False,
28
- "schema": None,
29
- "writeable_columns": None,
30
- "doc_model_name": "",
31
- "doc_response_data_schema": None,
32
- }
33
-
34
- def __init__(self, di):
35
- super().__init__(di)
36
-
37
- def handle(self, input_output):
38
- self._di.bind("input_output", input_output)
39
- try:
40
- if self.configuration("schema"):
41
- request_data = self.request_data(input_output)
42
- input_errors = {
43
- **self._extra_column_errors(request_data),
44
- **self._find_input_errors(request_data),
45
- }
46
- if input_errors:
47
- return self.input_errors(input_output, input_errors)
48
- response = self._di.call_function(
49
- self.configuration("callable"),
50
- request_data=request_data,
51
- **input_output.routing_data(),
52
- **input_output.context_specifics(),
53
- )
54
- else:
55
- response = self._di.call_function(
56
- self.configuration("callable"),
57
- request_data=self.request_data(input_output, required=False),
58
- **input_output.routing_data(),
59
- **input_output.context_specifics(),
60
- )
61
- if response:
62
- return self.success(input_output, response)
63
- return
64
- except InputError as e:
65
- if e.errors:
66
- return self.input_errors(input_output, e.errors)
67
- else:
68
- return self.input_errors(input_output, str(e))
69
- except ClientError as e:
70
- return self.error(input_output, str(e), 400)
71
- except NotFound as e:
72
- return self.error(input_output, str(e), 404)
73
-
74
- def _check_configuration(self, configuration):
75
- super()._check_configuration(configuration)
76
- error_prefix = "Configuration error for %s:" % (self.__class__.__name__)
77
- if not "callable" in configuration:
78
- raise KeyError(f"{error_prefix} you must specify 'callable'")
79
- if not callable(configuration["callable"]):
80
- raise ValueError(f"{error_prefix} the provided callable is not actually callable")
81
- if configuration.get("schema") is not None:
82
- self._check_schema(configuration["schema"], configuration.get("writeable_columns"), error_prefix)
83
-
84
- def _finalize_configuration(self, configuration):
85
- configuration = super()._finalize_configuration(configuration)
86
- if configuration.get("schema") is not None:
87
- if validations.is_model(configuration["schema"]):
88
- configuration["doc_model_name"] = configuration["schema"].__class__.__name__
89
- elif validations.is_model_class(configuration["schema"]):
90
- configuration["doc_model_name"] = configuration["schema"].__name__
91
- configuration["schema"] = self._schema_to_columns(
92
- configuration["schema"], columns_to_keep=configuration.get("writeable_columns")
93
- )
94
- return configuration
95
-
96
- def request_data(self, input_output, required=True):
97
- if not self.configuration("schema"):
98
- return input_output.request_data(required=required)
99
- # we have to map from internal names to external names, because case mapping
100
- # isn't always one-to-one, so we want to do it exactly the same way that the documentation
101
- # is built.
102
- key_map = {self.auto_case_column_name(key, True): key for key in self.configuration("schema").keys()}
103
- # in case the id comes up in the request body
104
- key_map[self.auto_case_internal_column_name("id")] = "id"
105
-
106
- # and make sure we don't drop any data along the way, because the input validation
107
- # needs to return an error for unexpected data.
108
- request_data = {
109
- key_map.get(key, key): value for (key, value) in input_output.request_data(required=required).items()
110
- }
111
- # the parent handler should provide our resource id (we don't do any routing ourselves)
112
- # However, our update/etc handlers need to find the id easily, so I'm going to be lazy and
113
- # just dump it into the request. I'll probably regret that.
114
- routing_data = input_output.routing_data()
115
- # we don't have to worry about casing on the 'id' in routing_data because it doesn't come in from the
116
- # route with a name. Rather, it is populated by clearskies, so will always just be 'id'
117
- if "id" in routing_data:
118
- request_data["id"] = routing_data["id"]
119
- return request_data
120
-
121
- def success(self, input_output, data, number_results=None, limit=None, next_page=None):
122
- if self.configuration("return_raw_response"):
123
- return input_output.respond(data, 200)
124
-
125
- return super().success(input_output, data, number_results=number_results, limit=limit, next_page=next_page)
126
-
127
- def documentation(self):
128
- schema = self.configuration("schema")
129
-
130
- # our request parameters
131
- parameters = []
132
- if schema:
133
- parameters = [
134
- autodoc.request.JSONBody(
135
- column.documentation(name=self.auto_case_column_name(column.name, True)),
136
- description=f"Set '{column.name}'",
137
- required=column.is_required,
138
- )
139
- for column in schema.values()
140
- ]
141
-
142
- authentication = self.configuration("authentication")
143
- standard_error_responses = []
144
- if not getattr(authentication, "is_public", False):
145
- standard_error_responses.append(self.documentation_access_denied_response())
146
- if getattr(authentication, "can_authorize", False):
147
- standard_error_responses.append(self.documentation_unauthorized_response())
148
-
149
- response_data_schema = self.configuration("doc_response_data_schema")
150
- if not response_data_schema:
151
- response_data_schema = []
152
-
153
- return [
154
- autodoc.request.Request(
155
- self.configuration("doc_description"),
156
- [
157
- self.documentation_success_response(
158
- autodoc.schema.Object(
159
- self.auto_case_internal_column_name("data"),
160
- children=response_data_schema,
161
- model_name=self.configuration("doc_model_name"),
162
- ),
163
- include_pagination=False,
164
- ),
165
- *standard_error_responses,
166
- self.documentation_not_found(),
167
- ],
168
- request_methods="POST" if schema else "GET",
169
- relative_path=self.configuration("base_url"),
170
- parameters=[
171
- *parameters,
172
- ],
173
- root_properties={
174
- "security": self.documentation_request_security(),
175
- },
176
- )
177
- ]
178
-
179
- def documentation_models(self):
180
- if not self.configuration("doc_model_name") or not self.configuration("doc_response_data_schema"):
181
- return {}
182
-
183
- schema_model_name = self.configuration("doc_model_name")
184
- return {
185
- schema_model_name: autodoc.schema.Object(
186
- "data",
187
- children=self.configuration("doc_response_data_schema"),
188
- ),
189
- }