clear-skies 1.22.10__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 (368) 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.22.10.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 +8 -39
  7. clearskies/authentication/authentication.py +44 -0
  8. clearskies/authentication/authorization.py +14 -8
  9. clearskies/authentication/authorization_pass_through.py +14 -10
  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 +55 -20
  41. clearskies/backends/api_backend.py +1118 -280
  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 +115 -4
  154. clearskies/di/additional_config_auto_import.py +12 -0
  155. clearskies/di/di.py +714 -125
  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 -160
  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 +1874 -193
  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.22.10.dist-info/METADATA +0 -47
  243. clear_skies-1.22.10.dist-info/RECORD +0 -213
  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 -51
  248. clearskies/backends/api_get_only_backend.py +0 -48
  249. clearskies/backends/example_backend.py +0 -43
  250. clearskies/backends/file_backend.py +0 -48
  251. clearskies/backends/json_backend.py +0 -7
  252. clearskies/backends/restful_api_advanced_search_backend.py +0 -103
  253. clearskies/binding_config.py +0 -16
  254. clearskies/column_types/__init__.py +0 -203
  255. clearskies/column_types/audit.py +0 -249
  256. clearskies/column_types/belongs_to.py +0 -271
  257. clearskies/column_types/boolean.py +0 -60
  258. clearskies/column_types/category_tree.py +0 -304
  259. clearskies/column_types/column.py +0 -373
  260. clearskies/column_types/created.py +0 -26
  261. clearskies/column_types/created_by_authorization_data.py +0 -26
  262. clearskies/column_types/created_by_header.py +0 -24
  263. clearskies/column_types/created_by_ip.py +0 -17
  264. clearskies/column_types/created_by_routing_data.py +0 -25
  265. clearskies/column_types/created_by_user_agent.py +0 -17
  266. clearskies/column_types/created_micro.py +0 -26
  267. clearskies/column_types/datetime.py +0 -109
  268. clearskies/column_types/datetime_micro.py +0 -13
  269. clearskies/column_types/email.py +0 -18
  270. clearskies/column_types/float.py +0 -43
  271. clearskies/column_types/has_many.py +0 -179
  272. clearskies/column_types/has_one.py +0 -58
  273. clearskies/column_types/integer.py +0 -41
  274. clearskies/column_types/json.py +0 -25
  275. clearskies/column_types/many_to_many.py +0 -278
  276. clearskies/column_types/many_to_many_with_data.py +0 -162
  277. clearskies/column_types/phone.py +0 -48
  278. clearskies/column_types/select.py +0 -11
  279. clearskies/column_types/string.py +0 -24
  280. clearskies/column_types/timestamp.py +0 -73
  281. clearskies/column_types/updated.py +0 -26
  282. clearskies/column_types/updated_micro.py +0 -26
  283. clearskies/column_types/uuid.py +0 -25
  284. clearskies/columns.py +0 -123
  285. clearskies/condition_parser.py +0 -172
  286. clearskies/contexts/build_context.py +0 -54
  287. clearskies/contexts/convert_to_application.py +0 -190
  288. clearskies/contexts/extract_handler.py +0 -37
  289. clearskies/contexts/test.py +0 -94
  290. clearskies/decorators/__init__.py +0 -39
  291. clearskies/decorators/auth0_jwks.py +0 -22
  292. clearskies/decorators/authorization.py +0 -10
  293. clearskies/decorators/binding_classes.py +0 -9
  294. clearskies/decorators/binding_modules.py +0 -9
  295. clearskies/decorators/bindings.py +0 -9
  296. clearskies/decorators/create.py +0 -10
  297. clearskies/decorators/delete.py +0 -10
  298. clearskies/decorators/docs.py +0 -14
  299. clearskies/decorators/get.py +0 -10
  300. clearskies/decorators/jwks.py +0 -26
  301. clearskies/decorators/merge.py +0 -124
  302. clearskies/decorators/patch.py +0 -10
  303. clearskies/decorators/post.py +0 -10
  304. clearskies/decorators/public.py +0 -11
  305. clearskies/decorators/response_headers.py +0 -10
  306. clearskies/decorators/return_raw_response.py +0 -9
  307. clearskies/decorators/schema.py +0 -10
  308. clearskies/decorators/secret_bearer.py +0 -24
  309. clearskies/decorators/security_headers.py +0 -10
  310. clearskies/di/standard_dependencies.py +0 -151
  311. clearskies/di/test_module/__init__.py +0 -6
  312. clearskies/di/test_module/another_module/__init__.py +0 -2
  313. clearskies/di/test_module/module_class.py +0 -5
  314. clearskies/handlers/__init__.py +0 -41
  315. clearskies/handlers/advanced_search.py +0 -271
  316. clearskies/handlers/base.py +0 -479
  317. clearskies/handlers/callable.py +0 -191
  318. clearskies/handlers/create.py +0 -35
  319. clearskies/handlers/crud_by_method.py +0 -18
  320. clearskies/handlers/database_connector.py +0 -32
  321. clearskies/handlers/delete.py +0 -61
  322. clearskies/handlers/exceptions/__init__.py +0 -5
  323. clearskies/handlers/exceptions/not_found.py +0 -3
  324. clearskies/handlers/get.py +0 -156
  325. clearskies/handlers/health_check.py +0 -59
  326. clearskies/handlers/input_processing.py +0 -79
  327. clearskies/handlers/list.py +0 -530
  328. clearskies/handlers/mygrations.py +0 -82
  329. clearskies/handlers/request_method_routing.py +0 -47
  330. clearskies/handlers/restful_api.py +0 -218
  331. clearskies/handlers/routing.py +0 -62
  332. clearskies/handlers/schema_helper.py +0 -128
  333. clearskies/handlers/simple_routing.py +0 -206
  334. clearskies/handlers/simple_routing_route.py +0 -192
  335. clearskies/handlers/simple_search.py +0 -136
  336. clearskies/handlers/update.py +0 -96
  337. clearskies/handlers/write.py +0 -193
  338. clearskies/input_requirements/__init__.py +0 -78
  339. clearskies/input_requirements/after.py +0 -36
  340. clearskies/input_requirements/before.py +0 -36
  341. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  342. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  343. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  344. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  345. clearskies/input_requirements/maximum_length.py +0 -19
  346. clearskies/input_requirements/maximum_value.py +0 -19
  347. clearskies/input_requirements/minimum_length.py +0 -22
  348. clearskies/input_requirements/minimum_value.py +0 -19
  349. clearskies/input_requirements/requirement.py +0 -25
  350. clearskies/input_requirements/time_delta.py +0 -38
  351. clearskies/input_requirements/unique.py +0 -18
  352. clearskies/mocks/__init__.py +0 -7
  353. clearskies/mocks/input_output.py +0 -124
  354. clearskies/mocks/models.py +0 -142
  355. clearskies/models.py +0 -350
  356. clearskies/security_headers/base.py +0 -12
  357. clearskies/tests/simple_api/models/__init__.py +0 -2
  358. clearskies/tests/simple_api/models/status.py +0 -23
  359. clearskies/tests/simple_api/models/user.py +0 -21
  360. clearskies/tests/simple_api/users_api.py +0 -64
  361. {clear_skies-1.22.10.dist-info → clear_skies-2.0.23.dist-info/licenses}/LICENSE +0 -0
  362. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  363. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  364. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  365. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  366. /clearskies/{secrets/exceptions → exceptions}/not_found.py +0 -0
  367. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  368. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -1,479 +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
- except exceptions.Authentication as auth_error:
199
- return self.error(input_output, str(auth_error), 401)
200
- except exceptions.Authorization as auth_error:
201
- return self.error(input_output, str(auth_error), 403)
202
- except exceptions.NotFound as auth_error:
203
- return self.error(input_output, str(auth_error), 404)
204
-
205
- return response
206
-
207
- def input_errors(self, input_output, errors, status_code=200):
208
- return self.respond(input_output, {"status": "input_errors", "input_errors": errors}, status_code)
209
-
210
- def error(self, input_output, message, status_code):
211
- return self.respond(input_output, {"status": "client_error", "error": message}, status_code)
212
-
213
- def success(self, input_output, data, number_results=None, limit=None, next_page=None):
214
- response_data = {"status": "success", "data": data, "pagination": {}}
215
-
216
- if number_results is not None:
217
- for value in [number_results, limit]:
218
- if value is not None and type(value) != int:
219
- raise ValueError("number_results and limit must all be integers")
220
-
221
- response_data["pagination"] = {
222
- "number_results": number_results,
223
- "limit": limit,
224
- "next_page": next_page,
225
- }
226
-
227
- return self.respond(input_output, response_data, 200)
228
-
229
- def respond(self, input_output, response_data, status_code):
230
- response_headers = self.configuration("response_headers")
231
- if response_headers:
232
- input_output.set_headers(response_headers)
233
- for security_header in self.configuration("security_headers"):
234
- security_header.set_headers_for_input_output(input_output)
235
- return input_output.respond(self._normalize_response(response_data), status_code)
236
-
237
- def _normalize_response(self, response_data):
238
- if not "status" in response_data:
239
- raise ValueError("Huh, status got left out somehow")
240
- return {
241
- self.auto_case_internal_column_name("status"): self.auto_case_internal_column_name(response_data["status"]),
242
- self.auto_case_internal_column_name("error"): response_data.get("error", ""),
243
- self.auto_case_internal_column_name("data"): response_data.get("data", []),
244
- self.auto_case_internal_column_name("pagination"): self._normalize_pagination(
245
- response_data.get("pagination", {})
246
- ),
247
- self.auto_case_internal_column_name("input_errors"): response_data.get("input_errors", {}),
248
- }
249
-
250
- def _normalize_pagination(self, pagination):
251
- # pagination isn't always relevant so if it is completely empty then leave it that way
252
- if not pagination:
253
- return pagination
254
- return {
255
- self.auto_case_internal_column_name("number_results"): pagination.get("number_results", 0),
256
- self.auto_case_internal_column_name("limit"): pagination.get("limit", 0),
257
- self.auto_case_internal_column_name("next_page"): {
258
- self.auto_case_internal_column_name(key): value
259
- for (key, value) in pagination.get("next_page", {}).items()
260
- },
261
- }
262
-
263
- def _model_as_json(self, model, input_output):
264
- if self.configuration("output_map"):
265
- return self._di.call_function(self.configuration("output_map"), model=model)
266
-
267
- if self._as_json_map is None:
268
- self._as_json_map = self._build_as_json_map(model)
269
-
270
- json = OrderedDict()
271
- for output_name, column in self._as_json_map.items():
272
- column_data = column.to_json(model)
273
- if len(column_data) == 1:
274
- json[output_name] = list(column_data.values())[0]
275
- else:
276
- for key, value in column_data.items():
277
- json[self.auto_case_column_name(key, True)] = value
278
- return json
279
-
280
- def _build_as_json_map(self, model):
281
- conversion_map = {}
282
- if self.configuration("id_column_name"):
283
- conversion_map[self.auto_case_internal_column_name("id")] = model.columns()[self.id_column_name]
284
-
285
- for column in self._get_readable_columns().values():
286
- conversion_map[self.auto_case_column_name(column.name, True)] = column
287
- return conversion_map
288
-
289
- def auto_case_internal_column_name(self, column_name):
290
- if self._configuration["external_casing"]:
291
- return string.swap_casing(column_name, "snake_case", self._configuration["external_casing"])
292
- return column_name
293
-
294
- def auto_case_to_internal_column_name(self, column_name):
295
- if self._configuration["external_casing"]:
296
- return string.swap_casing(column_name, self._configuration["external_casing"], "snake_case")
297
- return column_name
298
-
299
- def auto_case_column_name(self, column_name, internal_to_external):
300
- if not self._configuration["internal_casing"]:
301
- return column_name
302
- if internal_to_external:
303
- return string.swap_casing(
304
- column_name,
305
- self._configuration["internal_casing"],
306
- self._configuration["external_casing"],
307
- )
308
- return string.swap_casing(
309
- column_name,
310
- self._configuration["external_casing"],
311
- self._configuration["internal_casing"],
312
- )
313
-
314
- @property
315
- def id_column_name(self) -> str:
316
- """
317
- This returns the name of the id column to use for requests
318
-
319
- There are three ways to determine the id column:
320
-
321
- 1. It may be defined in the handler configuration.
322
- 2. It may be overridden in the model class
323
- 3. It defaults to 'id'
324
-
325
- The first happens if the developer wants to expose a different "id" column to the client.
326
- The second happens if the developer wants to use a different id column internally.
327
- The third is the clearskies default.
328
-
329
- The first is easy to detect because the dev will set `id_column_name` in the handler config.
330
- The second happens if the model class defines a different `id_column_name` property in the model class.
331
- However, this is tricky because there is nothing in this base case that allows us to pull up the model.
332
- In fact, not all handlers use a model, or they may use multiple models, etc... Still, it's pretty
333
- common for the handler to have a configuration named `model_class` or `model`, so let's check for that and assume
334
- the handler will only ask for the id_column_name() if the handler has a `self.configuration('model_class')`
335
- """
336
- id_column_name = self.configuration("id_column_name")
337
- if id_column_name is not None:
338
- return id_column_name
339
- if not self._configuration.get("model_class", False) and not self._configuration.get("model", False):
340
- raise KeyError(
341
- "To properly use handler.id_column_name, the handler must have a 'model_class' or 'model' configuration key"
342
- )
343
- if self._configuration.get("model_class", False):
344
- return self._configuration.get("model_class").id_column_name
345
- return self._configuration.get("model").id_column_name
346
-
347
- def cors(self, input_output):
348
- cors = self._cors_header
349
- if not cors:
350
- return self.error(input_output, "not found", 404)
351
- authentication = self._configuration.get("authentication")
352
- if authentication:
353
- authentication.set_headers_for_cors(cors)
354
- cors.set_headers_for_input_output(input_output)
355
- return input_output.respond("", 200)
356
-
357
- def documentation(self):
358
- return []
359
-
360
- def documentation_components(self):
361
- return {
362
- "models": self.documentation_models(),
363
- "securitySchemes": self.documentation_security_schemes(),
364
- }
365
-
366
- def documentation_security_schemes(self):
367
- authentication = self._configuration.get("authentication")
368
- if not authentication or not authentication.documentation_security_scheme_name():
369
- return {}
370
-
371
- return {
372
- authentication.documentation_security_scheme_name(): authentication.documentation_security_scheme(),
373
- }
374
-
375
- def documentation_models(self):
376
- return {}
377
-
378
- def documentation_pagination_response(self, include_pagination=True):
379
- if not include_pagination:
380
- return AutoDocObject(self.auto_case_internal_column_name("pagination"), [], value={})
381
- return AutoDocObject(
382
- self.auto_case_internal_column_name("pagination"),
383
- [
384
- AutoDocInteger(self.auto_case_internal_column_name("number_results"), example=10),
385
- AutoDocInteger(self.auto_case_internal_column_name("limit"), example=100),
386
- AutoDocObject(
387
- self.auto_case_internal_column_name("next_page"),
388
- self._model.documentation_pagination_next_page_response(self.auto_case_internal_column_name),
389
- self._model.documentation_pagination_next_page_example(self.auto_case_internal_column_name),
390
- ),
391
- ],
392
- )
393
-
394
- def documentation_success_response(self, data_schema, description="", include_pagination=False):
395
- return AutoDocResponse(
396
- 200,
397
- AutoDocObject(
398
- "body",
399
- [
400
- AutoDocString(self.auto_case_internal_column_name("status"), value="success"),
401
- data_schema,
402
- self.documentation_pagination_response(include_pagination=include_pagination),
403
- AutoDocString(self.auto_case_internal_column_name("error"), value=""),
404
- AutoDocObject(self.auto_case_internal_column_name("input_errors"), [], value={}),
405
- ],
406
- ),
407
- description=description,
408
- )
409
-
410
- def documentation_generic_error_response(self, description="Invalid Call", status=400):
411
- return AutoDocResponse(
412
- status,
413
- AutoDocObject(
414
- "body",
415
- [
416
- AutoDocString(self.auto_case_internal_column_name("status"), value="error"),
417
- AutoDocObject(self.auto_case_internal_column_name("data"), [], value={}),
418
- self.documentation_pagination_response(include_pagination=False),
419
- AutoDocString(self.auto_case_internal_column_name("error"), example="User readable error message"),
420
- AutoDocObject(self.auto_case_internal_column_name("input_errors"), [], value={}),
421
- ],
422
- ),
423
- description=description,
424
- )
425
-
426
- def documentation_input_error_response(self, description="Invalid client-side input"):
427
- email_example = self.auto_case_internal_column_name("email")
428
- return AutoDocResponse(
429
- 200,
430
- AutoDocObject(
431
- "body",
432
- [
433
- AutoDocString(self.auto_case_internal_column_name("status"), value="input_errors"),
434
- AutoDocObject(self.auto_case_internal_column_name("data"), [], value={}),
435
- self.documentation_pagination_response(include_pagination=False),
436
- AutoDocString(self.auto_case_internal_column_name("error"), value=""),
437
- AutoDocObject(
438
- self.auto_case_internal_column_name("input_errors"),
439
- [AutoDocString("[COLUMN_NAME]", example="User friendly error message")],
440
- example={email_example: f"{email_example} was not a valid email address"},
441
- ),
442
- ],
443
- ),
444
- description=description,
445
- )
446
-
447
- def documentation_access_denied_response(self):
448
- return self.documentation_generic_error_response(description="Access Denied", status=401)
449
-
450
- def documentation_unauthorized_response(self):
451
- return self.documentation_generic_error_response(description="Unauthorized", status=403)
452
-
453
- def documentation_not_found(self):
454
- return self.documentation_generic_error_response(description="Not Found", status=404)
455
-
456
- def documentation_request_security(self):
457
- authentication = self.configuration("authentication")
458
- name = authentication.documentation_security_scheme_name()
459
- return [{name: []}] if name else []
460
-
461
- def documentation_data_schema(self):
462
- id_column_name = self.id_column_name
463
- properties = []
464
- if self.configuration("id_column_name"):
465
- properties.append(
466
- self._columns[id_column_name].documentation(name=self.auto_case_internal_column_name("id"))
467
- if id_column_name in self._columns
468
- else AutoDocString(self.auto_case_internal_column_name("id"))
469
- )
470
-
471
- for column in self._get_readable_columns().values():
472
- column_doc = column.documentation()
473
- if type(column_doc) != list:
474
- column_doc = [column_doc]
475
- for doc in column_doc:
476
- doc.name = self.auto_case_internal_column_name(doc.name)
477
- properties.append(doc)
478
-
479
- return properties
@@ -1,191 +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
- **input_output.routing_data(),
51
- **input_output.context_specifics(),
52
- request_data=request_data,
53
- authorization_data=input_output.get_authorization_data(),
54
- )
55
- else:
56
- response = self._di.call_function(
57
- self.configuration("callable"),
58
- **input_output.routing_data(),
59
- **input_output.context_specifics(),
60
- request_data=self.request_data(input_output, required=False),
61
- authorization_data=input_output.get_authorization_data(),
62
- )
63
- if response:
64
- return self.success(input_output, response)
65
- return
66
- except InputError as e:
67
- if e.errors:
68
- return self.input_errors(input_output, e.errors)
69
- else:
70
- return self.input_errors(input_output, str(e))
71
- except ClientError as e:
72
- return self.error(input_output, str(e), 400)
73
- except NotFound as e:
74
- return self.error(input_output, str(e), 404)
75
-
76
- def _check_configuration(self, configuration):
77
- super()._check_configuration(configuration)
78
- error_prefix = "Configuration error for %s:" % (self.__class__.__name__)
79
- if not "callable" in configuration:
80
- raise KeyError(f"{error_prefix} you must specify 'callable'")
81
- if not callable(configuration["callable"]):
82
- raise ValueError(f"{error_prefix} the provided callable is not actually callable")
83
- if configuration.get("schema") is not None:
84
- self._check_schema(configuration["schema"], configuration.get("writeable_columns"), error_prefix)
85
-
86
- def _finalize_configuration(self, configuration):
87
- configuration = super()._finalize_configuration(configuration)
88
- if configuration.get("schema") is not None:
89
- if validations.is_model(configuration["schema"]):
90
- configuration["doc_model_name"] = configuration["schema"].__class__.__name__
91
- elif validations.is_model_class(configuration["schema"]):
92
- configuration["doc_model_name"] = configuration["schema"].__name__
93
- configuration["schema"] = self._schema_to_columns(
94
- configuration["schema"], columns_to_keep=configuration.get("writeable_columns")
95
- )
96
- return configuration
97
-
98
- def request_data(self, input_output, required=True):
99
- if not self.configuration("schema"):
100
- return input_output.request_data(required=required)
101
- # we have to map from internal names to external names, because case mapping
102
- # isn't always one-to-one, so we want to do it exactly the same way that the documentation
103
- # is built.
104
- key_map = {self.auto_case_column_name(key, True): key for key in self.configuration("schema").keys()}
105
- # in case the id comes up in the request body
106
- key_map[self.auto_case_internal_column_name("id")] = "id"
107
-
108
- # and make sure we don't drop any data along the way, because the input validation
109
- # needs to return an error for unexpected data.
110
- request_data = {
111
- key_map.get(key, key): value for (key, value) in input_output.request_data(required=required).items()
112
- }
113
- # the parent handler should provide our resource id (we don't do any routing ourselves)
114
- # However, our update/etc handlers need to find the id easily, so I'm going to be lazy and
115
- # just dump it into the request. I'll probably regret that.
116
- routing_data = input_output.routing_data()
117
- # we don't have to worry about casing on the 'id' in routing_data because it doesn't come in from the
118
- # route with a name. Rather, it is populated by clearskies, so will always just be 'id'
119
- if "id" in routing_data:
120
- request_data["id"] = routing_data["id"]
121
- return request_data
122
-
123
- def success(self, input_output, data, number_results=None, limit=None, next_page=None):
124
- if self.configuration("return_raw_response"):
125
- return input_output.respond(data, 200)
126
-
127
- return super().success(input_output, data, number_results=number_results, limit=limit, next_page=next_page)
128
-
129
- def documentation(self):
130
- schema = self.configuration("schema")
131
-
132
- # our request parameters
133
- parameters = []
134
- if schema:
135
- parameters = [
136
- autodoc.request.JSONBody(
137
- column.documentation(name=self.auto_case_column_name(column.name, True)),
138
- description=f"Set '{column.name}'",
139
- required=column.is_required,
140
- )
141
- for column in schema.values()
142
- ]
143
-
144
- authentication = self.configuration("authentication")
145
- standard_error_responses = []
146
- if not getattr(authentication, "is_public", False):
147
- standard_error_responses.append(self.documentation_access_denied_response())
148
- if getattr(authentication, "can_authorize", False):
149
- standard_error_responses.append(self.documentation_unauthorized_response())
150
-
151
- response_data_schema = self.configuration("doc_response_data_schema")
152
- if not response_data_schema:
153
- response_data_schema = []
154
-
155
- return [
156
- autodoc.request.Request(
157
- self.configuration("doc_description"),
158
- [
159
- self.documentation_success_response(
160
- autodoc.schema.Object(
161
- self.auto_case_internal_column_name("data"),
162
- children=response_data_schema,
163
- model_name=self.configuration("doc_model_name"),
164
- ),
165
- include_pagination=False,
166
- ),
167
- *standard_error_responses,
168
- self.documentation_not_found(),
169
- ],
170
- request_methods="POST" if schema else "GET",
171
- relative_path=self.configuration("base_url"),
172
- parameters=[
173
- *parameters,
174
- ],
175
- root_properties={
176
- "security": self.documentation_request_security(),
177
- },
178
- )
179
- ]
180
-
181
- def documentation_models(self):
182
- if not self.configuration("doc_model_name") or not self.configuration("doc_response_data_schema"):
183
- return {}
184
-
185
- schema_model_name = self.configuration("doc_model_name")
186
- return {
187
- schema_model_name: autodoc.schema.Object(
188
- "data",
189
- children=self.configuration("doc_response_data_schema"),
190
- ),
191
- }