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,530 +0,0 @@
1
- from .base import Base
2
- from collections import OrderedDict
3
- from .. import autodoc
4
- from ..functional import string
5
- import inspect
6
- from ..column_types import BelongsTo
7
-
8
-
9
- class List(Base):
10
- _model = None
11
- _columns = None
12
- _searchable_columns = None
13
- _readable_columns = None
14
- _prepared_models = None
15
- expected_request_methods = "GET"
16
-
17
- _configuration_defaults = {
18
- "model": None,
19
- "model_class": None,
20
- "readable_columns": None,
21
- "searchable_columns": None,
22
- "sortable_columns": [],
23
- "where": [],
24
- "join": [],
25
- "group_by": "",
26
- "default_sort_column": "",
27
- "default_sort_direction": "asc",
28
- "default_limit": 100,
29
- "max_limit": 200,
30
- }
31
-
32
- def __init__(self, di):
33
- super().__init__(di)
34
-
35
- def handle(self, input_output):
36
- models = self._prepared_models.clone()
37
- for where in self.configuration("where"):
38
- if callable(where):
39
- models = self._di.call_function(
40
- where, models=models, input_output=input_output, routing_data=input_output.routing_data()
41
- )
42
- models = models.where_for_request(
43
- models,
44
- input_output.routing_data(),
45
- input_output.get_authorization_data(),
46
- input_output,
47
- )
48
- limit = self.configuration("default_limit")
49
- authorization = self._configuration.get("authorization", None)
50
- if authorization and hasattr(authorization, "filter_models"):
51
- models = authorization.filter_models(models, input_output.get_authorization_data(), input_output)
52
- request_data = self.map_input_to_internal_names(input_output.request_data(False))
53
- query_parameters = self.map_input_to_internal_names(input_output.get_query_parameters())
54
- pagination_data = {}
55
- for key in self._model.allowed_pagination_keys():
56
- if key in request_data and key in query_parameters:
57
- original_name = self.auto_case_internal_column_name(key)
58
- return self.error(
59
- input_output,
60
- f"Ambiguous request: key '{original_name}' is present in both the JSON body and URL data",
61
- )
62
- if key in request_data:
63
- pagination_data[key] = request_data[key]
64
- del request_data[key]
65
- if key in query_parameters:
66
- pagination_data[key] = query_parameters[key]
67
- del query_parameters[key]
68
- if request_data or query_parameters or pagination_data:
69
- error = self.check_request_data(request_data, query_parameters, pagination_data)
70
- if error:
71
- return self.error(input_output, error, 400)
72
- [models, limit] = self.configure_models_from_request_data(
73
- models, request_data, query_parameters, pagination_data
74
- )
75
- if not models.query_sorts:
76
- models = models.sort_by(
77
- self.configuration("default_sort_column"),
78
- self.configuration("default_sort_direction"),
79
- primary_table=models.table_name(),
80
- )
81
-
82
- return self.success(
83
- input_output,
84
- [self._model_as_json(model, input_output) for model in models],
85
- number_results=len(models),
86
- limit=limit,
87
- next_page=models.next_page_data(),
88
- )
89
-
90
- def configure_models_from_request_data(self, models, request_data, query_parameters, pagination_data):
91
- limit = int(query_parameters.get("limit", self.configuration("default_limit")))
92
- models = models.limit(limit)
93
- if pagination_data:
94
- models = models.pagination(**pagination_data)
95
- sort = query_parameters.get("sort")
96
- direction = query_parameters.get("direction")
97
- if sort and direction:
98
- models = self._add_join(sort, models)
99
- [sort_column, sort_table] = self._resolve_references_for_query(sort)
100
- models = models.sort_by(sort_column, direction, primary_table=sort_table)
101
-
102
- return [models, limit]
103
-
104
- @property
105
- def allowed_request_keys(self):
106
- return ["sort", "direction", "limit"]
107
-
108
- @property
109
- def internal_request_keys(self):
110
- return ["sort", "direction", "limit"]
111
-
112
- def map_input_to_internal_names(self, input):
113
- internal_request_keys = [*self.internal_request_keys, *self._model.allowed_pagination_keys()]
114
- for key in internal_request_keys:
115
- mapped_key = self.auto_case_internal_column_name(key)
116
- if mapped_key != key and mapped_key in input:
117
- input[key] = input[mapped_key]
118
- del input[mapped_key]
119
- # any non-internal fields are assumed to be column names and need to go
120
- # through the full mapping
121
- for key in set(self.allowed_request_keys) - set(internal_request_keys):
122
- mapped_key = self.auto_case_column_name(key, True)
123
- if mapped_key != key and mapped_key in input:
124
- input[key] = input[mapped_key]
125
- del input[mapped_key]
126
-
127
- # finally, if we have a sort key set then convert the value to the properly cased column name
128
- if "sort" in input:
129
- # we can't just take the sort value and convert it to internal casing because camel/title case
130
- # to snake_case can be ambiguous (while snake_case to camel/title is not)
131
- sort_column_map = {}
132
- for internal_name in self.configuration("sortable_columns"):
133
- external_name = self.auto_case_column_name(internal_name, True)
134
- sort_column_map[external_name] = internal_name
135
- # sometimes the sort may be a list of directives
136
- if type(input["sort"]) == list:
137
- for index, sort_entry in enumerate(input["sort"]):
138
- if input["sort"][index]["column"] in sort_column_map:
139
- input["sort"][index]["column"] = sort_column_map[input["sort"][index]["column"]]
140
- else:
141
- if input["sort"] in sort_column_map:
142
- input["sort"] = sort_column_map[input["sort"]]
143
-
144
- return input
145
-
146
- def check_request_data(self, request_data, query_parameters, pagination_data):
147
- if pagination_data:
148
- error = self._model.validate_pagination_kwargs(pagination_data, self.auto_case_internal_column_name)
149
- if error:
150
- return error
151
- for key in request_data.keys():
152
- if key not in self.allowed_request_keys or key in ["sort", "direction", "limit"]:
153
- return f"Invalid request parameter found in request body: '{key}'"
154
- for key in query_parameters.keys():
155
- if key not in self.allowed_request_keys:
156
- return f"Invalid request parameter found in URL data: '{key}'"
157
- if key in request_data:
158
- return f"Ambiguous request: '{key}' was found in both the request body and URL data"
159
- limit = query_parameters.get("limit")
160
- if limit is not None and type(limit) != int and type(limit) != float and type(limit) != str:
161
- return "Invalid request: 'limit' should be an integer"
162
- if limit:
163
- try:
164
- limit = int(limit)
165
- except ValueError:
166
- return "Invalid request: 'limit' should be an integer"
167
- if limit and limit > self.configuration("max_limit"):
168
- return f"Invalid request: 'limit' must be at most {self.configuration('max_limit')}"
169
- allowed_sort_columns = self.configuration("sortable_columns")
170
- if not allowed_sort_columns:
171
- allowed_sort_columns = self._columns
172
- sort = self._from_either(request_data, query_parameters, "sort")
173
- direction = self._from_either(request_data, query_parameters, "direction")
174
- if sort and type(sort) != str:
175
- return "Invalid request: 'sort' should be a string"
176
- if direction and type(direction) != str:
177
- return "Invalid request: 'direction' should be a string"
178
- if sort or direction:
179
- if (sort and not direction) or (direction and not sort):
180
- return "You must specify 'sort' and 'direction' together in the request - not just one of them"
181
- if sort not in allowed_sort_columns:
182
- return f"Invalid request: invalid sort column"
183
- if direction.lower() not in ["asc", "desc"]:
184
- return "Invalid request: direction must be 'asc' or 'desc'"
185
- return self.check_search_in_request_data(request_data, query_parameters)
186
-
187
- def check_search_in_request_data(self, request_data, query_parameters):
188
- return None
189
-
190
- def _unpack_column_name_with_reference(self, column_name):
191
- if "." not in column_name:
192
- return [column_name, ""]
193
- return column_name.split(".", 1)
194
-
195
- def configure(self, configuration):
196
- super().configure(configuration)
197
- # performance optimizations! First, take any of our configuration options that affect
198
- # the search results and create a models class with those built in
199
- self._prepared_models = self._model
200
- for where in self.configuration("where"):
201
- if type(where) == str:
202
- self._prepared_models = self._prepared_models.where(where)
203
- for join in self.configuration("join"):
204
- self._prepared_models = self._prepared_models.join(join)
205
- if self.configuration("group_by"):
206
- self._prepared_models = self._prepared_models.group_by(self.configuration("group_by"))
207
- self._prepared_models = self._prepared_models.limit(self.configuration("default_limit"))
208
-
209
- if self._prepared_models.supports_n_plus_one():
210
- for column in self._get_readable_columns().values():
211
- self._prepared_models = column.configure_n_plus_one(self._prepared_models)
212
-
213
- def _check_configuration(self, configuration):
214
- super()._check_configuration(configuration)
215
- error_prefix = "Configuration error for %s:" % (self.__class__.__name__)
216
- has_model_class = ("model_class" in configuration) and configuration["model_class"] is not None
217
- has_model = ("model" in configuration) and configuration["model"] is not None
218
- if not has_model and not has_model_class:
219
- raise KeyError(f"{error_prefix} you must specify 'model' or 'model_class'")
220
- if has_model and has_model_class:
221
- raise KeyError(f"{error_prefix} you specified both 'model' and 'model_class', but can only provide one")
222
- if has_model and inspect.isclass(configuration["model"]):
223
- raise ValueError(
224
- "{error_prefix} you must provide a model instance in the 'model' configuration setting, but a class was provided instead"
225
- )
226
- self._model = self._di.build(configuration["model_class"]) if has_model_class else configuration["model"]
227
- self._columns = self._model.columns(overrides=configuration.get("column_overrides"))
228
- model_class_name = self._model.__class__.__name__
229
- # checks for searchable_columns and readable_columns
230
- self._check_columns_in_configuration(configuration, "readable_columns")
231
- # the List base class doesn't use searchable columns so just ignore this check for List
232
- if type(self) != List:
233
- self._check_columns_in_configuration(configuration, "searchable_columns")
234
-
235
- if "default_sort_column" not in configuration:
236
- raise ValueError(f"{error_prefix} missing required configuration 'default_sort_column'")
237
-
238
- # sortable_columns, wheres, and joins should all be iterables
239
- for config_name, contents in {
240
- "sortable_columns": "column names",
241
- "where": "conditions",
242
- "join": "joins",
243
- }.items():
244
- if config_name not in configuration:
245
- continue
246
- if not hasattr(configuration[config_name], "__iter__") or type(configuration[config_name]) == str:
247
- raise ValueError(
248
- f"{error_prefix} '{config_name}' should be an iterable of {contents} "
249
- + f", not {str(type(configuration[config_name]))}"
250
- )
251
-
252
- # checks for sortable_columns
253
- if configuration.get("sortable_columns"):
254
- self._check_columns_in_configuration(configuration, "searchable_columns")
255
-
256
- # common checks for group_by and default_sort_column
257
- for config_name in ["group_by", "default_sort_column"]:
258
- value = configuration.get(config_name)
259
- if not value:
260
- continue
261
- # we're being lazy for now and not checking complicated values
262
- if "." in value:
263
- continue
264
- if value not in self._columns:
265
- raise ValueError(
266
- f"{error_prefix} '{config_name}' references column named {column_name} "
267
- + f"but this column does not exist for model '{model_class_name}'"
268
- )
269
-
270
- for config_name in ["default_page_length", "max_page_length"]:
271
- if config_name in configuration and type(configuration[config_name]) != int:
272
- raise ValueError(
273
- f"{error_prefix} '{config_name}' should be an int, not {str(type(configuration[config_name]))}"
274
- )
275
-
276
- def _check_columns_in_configuration(self, configuration, config_name):
277
- error_prefix = "Configuration error for %s:" % (self.__class__.__name__)
278
- model_class_name = self._model.__class__.__name__
279
- if not configuration.get(config_name):
280
- raise ValueError(f"{error_prefix} missing required configuration '{config_name}'")
281
- if not hasattr(configuration[config_name], "__iter__"):
282
- raise ValueError(
283
- f"{error_prefix} '{config_name}' should be an iterable of column names "
284
- + f", not {str(type(configuration[config_name]))}"
285
- )
286
- for column_name in configuration[config_name]:
287
- relationship_reference = None
288
- if config_name == "searchable_columns" or config_name == "sortable_columns":
289
- [column_name, relationship_reference] = self._unpack_column_name_with_reference(column_name)
290
- if column_name not in self._columns:
291
- raise ValueError(
292
- f"{error_prefix} '{config_name}' references column named {column_name} "
293
- + f"but this column does not exist for model '{model_class_name}'"
294
- )
295
- column = self._columns[column_name]
296
- if config_name == "readable_columns" and not column.is_readable:
297
- raise ValueError(
298
- f"{error_prefix} '{config_name}' references column named {column_name} "
299
- + f"but this column does not exist for model '{model_class_name}'"
300
- )
301
- if relationship_reference:
302
- if not isinstance(column, BelongsTo):
303
- raise ValueError(
304
- f"{error_prefix} '{config_name}' references {column_name}.{relationship_reference}. "
305
- + f"For this to work, {column_name} must be a belongs to relatiionship, but it isn't."
306
- )
307
- if relationship_reference not in column.parent_columns:
308
- parent_class = column.config("parent_models_class").__name__
309
- raise ValueError(
310
- f"{error_prefix} '{config_name}' references {column_name}.{relationship_reference}, "
311
- + f"but {relationship_reference} is not a valid column in the BelongsTo model class, {parent_class}."
312
- )
313
-
314
- def _resolve_references_for_query(self, column_name):
315
- """
316
- Takes the column name and returns the name and table.
317
-
318
- If it's just a column name, we assume the table is the table for our model class.
319
- If it's something like `belongs_to_column.column_name`, then it will find the appropriate
320
- table reference.
321
- """
322
- if not column_name:
323
- return [None, None]
324
- [column_name, relationship_reference] = self._unpack_column_name_with_reference(column_name)
325
- if not relationship_reference:
326
- return [column_name, self._model.table_name()]
327
-
328
- belongs_to_column = self._columns[column_name]
329
- return [relationship_reference, belongs_to_column.join_table_alias()]
330
-
331
- def _add_join(self, column_name, models):
332
- """
333
- Adds a join to the query for the given column name in the case where it references a column in a belongs to.
334
-
335
- If column_name is something like `belongs_to_column.column_name`, this will add have the belongs to column
336
- add it's typical join condition, so that further sorting/searching can work.
337
-
338
- If column_name is empty, or doesn't contain a period, then this does nothing.
339
- """
340
- if not column_name:
341
- return models
342
- [column_name, relationship_reference] = self._unpack_column_name_with_reference(column_name)
343
- if not relationship_reference:
344
- return models
345
- return self._columns[column_name].add_join(models)
346
-
347
- def _from_either(self, request_data, query_parameters, key, default=None, ignore_none=True):
348
- """
349
- Returns the key from either object. Assumes it is not present in both
350
- """
351
- if key in request_data:
352
- if request_data[key] is not None or not ignore_none:
353
- return request_data[key]
354
- if key in query_parameters:
355
- if query_parameters[key] is not None or not ignore_none:
356
- return query_parameters[key]
357
- return default
358
-
359
- def _get_columns(self, column_type):
360
- resolved_columns = OrderedDict()
361
- for column_name in self.configuration(f"{column_type}_columns"):
362
- if column_type == "searchable":
363
- [column_name, relationship_reference] = self._unpack_column_name_with_reference(column_name)
364
- if column_name not in self._columns:
365
- class_name = self.__class__.__name__
366
- model_class = self._model.__class__.__name__
367
- raise ValueError(
368
- f"Handler {class_name} was configured with {column_type} column '{column_name}' but this "
369
- + f"column doesn't exist for model {model_class}"
370
- )
371
- resolved_columns[column_name] = self._columns[column_name]
372
- return resolved_columns
373
-
374
- def _get_readable_columns(self):
375
- if self._readable_columns is None:
376
- self._readable_columns = self._get_columns("readable")
377
- return self._readable_columns
378
-
379
- def _get_searchable_columns(self):
380
- if self._searchable_columns is None:
381
- self._searchable_columns = self._get_columns("searchable")
382
- return self._searchable_columns
383
-
384
- def documentation(self):
385
- nice_model = string.camel_case_to_words(self._model.__class__.__name__)
386
- schema_model_name = string.camel_case_to_snake_case(self._model.__class__.__name__)
387
- data_schema = self.documentation_data_schema()
388
-
389
- authentication = self.configuration("authentication")
390
- standard_error_responses = []
391
- if not getattr(authentication, "is_public", False):
392
- standard_error_responses.append(self.documentation_access_denied_response())
393
- if getattr(authentication, "can_authorize", False):
394
- standard_error_responses.append(self.documentation_unauthorized_response())
395
-
396
- return [
397
- autodoc.request.Request(
398
- f"Fetch the list of current {nice_model} records",
399
- [
400
- self.documentation_success_response(
401
- autodoc.schema.Array(
402
- self.auto_case_internal_column_name("data"),
403
- autodoc.schema.Object(nice_model, children=data_schema, model_name=schema_model_name),
404
- ),
405
- description=f"The matching {nice_model} records",
406
- include_pagination=True,
407
- ),
408
- *standard_error_responses,
409
- self.documentation_generic_error_response(),
410
- ],
411
- relative_path=self.configuration("base_url"),
412
- request_methods=self.expected_request_methods,
413
- parameters=self.documentation_request_parameters(),
414
- root_properties={
415
- "security": self.documentation_request_security(),
416
- },
417
- ),
418
- ]
419
-
420
- def documentation_request_parameters(self):
421
- return [
422
- *self.documentation_url_pagination_parameters(),
423
- *self.documentation_url_sort_parameters(),
424
- ]
425
-
426
- def documentation_models(self):
427
- schema_model_name = string.camel_case_to_snake_case(self._model.__class__.__name__)
428
-
429
- return {
430
- schema_model_name: autodoc.schema.Object(
431
- self.auto_case_internal_column_name("data"),
432
- children=self.documentation_data_schema(),
433
- ),
434
- }
435
-
436
- def documentation_id_url_parameter(self):
437
- id_column_name = self.id_column_name
438
- if id_column_name in self._columns:
439
- id_column_schema = self._columns[id_column_name].documentation()
440
- else:
441
- id_column_schema = autodoc.schema.Integer(self.auto_case_internal_column_name("id"))
442
- return autodoc.request.URLPath(
443
- id_column_schema,
444
- description="The id of the record to fetch",
445
- required=True,
446
- )
447
-
448
- def documentation_url_pagination_parameters(self):
449
- url_parameters = [
450
- autodoc.request.URLParameter(
451
- autodoc.schema.Integer(self.auto_case_internal_column_name("limit")),
452
- description="The number of records to return",
453
- ),
454
- ]
455
-
456
- for parameter in self._model.documentation_pagination_parameters(self.auto_case_internal_column_name):
457
- (schema, description) = parameter
458
- url_parameters.append(autodoc.request.URLParameter(schema, description=description))
459
-
460
- return url_parameters
461
-
462
- def documentation_url_sort_parameters(self):
463
- sort_columns = self.configuration("sortable_columns")
464
- if not sort_columns:
465
- sort_columns = self._columns.keys()
466
- sort_columns = [self.auto_case_column_name(internal_name, True) for internal_name in sort_columns]
467
- directions = [self.auto_case_column_name(internal_name, True) for internal_name in ["asc", "desc"]]
468
-
469
- return [
470
- autodoc.request.URLParameter(
471
- autodoc.schema.Enum(
472
- self.auto_case_internal_column_name("sort"),
473
- sort_columns,
474
- autodoc.schema.String(self.auto_case_internal_column_name("sort")),
475
- example=self.auto_case_column_name("name", True),
476
- ),
477
- description=f"Column to sort by",
478
- ),
479
- autodoc.request.URLParameter(
480
- autodoc.schema.Enum(
481
- self.auto_case_internal_column_name("direction"),
482
- directions,
483
- autodoc.schema.String(self.auto_case_internal_column_name("direction")),
484
- example=self.auto_case_column_name("asc", True),
485
- ),
486
- description=f"Direction to sort",
487
- ),
488
- ]
489
-
490
- def documentation_json_pagination_parameters(self):
491
- json_parameters = [
492
- autodoc.request.JSONBody(
493
- autodoc.schema.Integer(self.auto_case_internal_column_name("limit")),
494
- description="The number of records to return",
495
- ),
496
- ]
497
-
498
- for parameter in self._model.documentation_pagination_parameters(self.auto_case_internal_column_name):
499
- (schema, description) = parameter
500
- json_parameters.append(autodoc.request.JSONBody(schema, description=description))
501
-
502
- return json_parameters
503
-
504
- def documentation_json_sort_parameters(self):
505
- sort_columns = self.configuration("sortable_columns")
506
- if not sort_columns:
507
- sort_columns = self._columns.keys()
508
- sort_columns = [self.auto_case_column_name(internal_name, True) for internal_name in sort_columns]
509
- directions = [self.auto_case_column_name(internal_name, True) for internal_name in ["asc", "desc"]]
510
-
511
- return [
512
- autodoc.request.JSONBody(
513
- autodoc.schema.Enum(
514
- self.auto_case_internal_column_name("sort"),
515
- sort_columns,
516
- autodoc.schema.String(self.auto_case_internal_column_name("sort")),
517
- example=self.auto_case_column_name("name", True),
518
- ),
519
- description=f"Column to sort by",
520
- ),
521
- autodoc.request.JSONBody(
522
- autodoc.schema.Enum(
523
- self.auto_case_internal_column_name("direction"),
524
- directions,
525
- autodoc.schema.String(self.auto_case_internal_column_name("direction")),
526
- example=self.auto_case_column_name("asc", True),
527
- ),
528
- description=f"Direction to sort",
529
- ),
530
- ]
@@ -1,82 +0,0 @@
1
- from .base import Base
2
- from io import StringIO
3
- import sys
4
-
5
-
6
- class Capturing:
7
- def __init__(self, sys):
8
- self.sys = sys
9
- self.output = []
10
-
11
- def __enter__(self):
12
- self._stdout = sys.stdout
13
- sys.stdout = self._stringio = StringIO()
14
- return self
15
-
16
- def __exit__(self, *args):
17
- self.output.extend(self._stringio.getvalue().splitlines())
18
- del self._stringio
19
- sys.stdout = self._stdout
20
-
21
-
22
- class Mygrations(Base):
23
- _connection = None
24
- _sys = None
25
-
26
- _configuration_defaults = {
27
- "command": None,
28
- "allow_input": False,
29
- "sql": None,
30
- }
31
-
32
- def __init__(self, di, connection_no_autocommit, sys):
33
- super().__init__(di)
34
- self._connection = connection_no_autocommit
35
- self._sys = sys
36
-
37
- def handle(self, input_output):
38
- from mygrations.core.commands import execute, commands
39
-
40
- command = self._from_input_or_config("command", input_output)
41
- if not command:
42
- return self.error(
43
- input_output,
44
- "Must provide 'command' in handler configuration, or in user input after setting the allow_input flag in the handler configuration",
45
- 400,
46
- )
47
- sql = self._from_input_or_config("sql", input_output)
48
- if not sql:
49
- return self.error(
50
- input_output,
51
- "Must provide 'sql' in handler configuration, or in user input after setting the allow_input flag in the handler configuration",
52
- 400,
53
- )
54
-
55
- if command not in commands:
56
- return self.error(
57
- input_output,
58
- "Invalid mygrations command. See allowed list: https://github.com/cmancone/mygrations#command-line-usage",
59
- 400,
60
- )
61
-
62
- [output, success] = execute(command, {"connection": self._connection, "sql_files": sql}, print_results=False)
63
- if success:
64
- return self.success(input_output, output)
65
- else:
66
- return self.error(input_output, "\n".join(output), 400)
67
-
68
- def _from_input_or_config(self, key, input_output):
69
- if self.configuration("allow_input"):
70
- input_data = input_output.json_body(required=False)
71
- if input_data is not None and key in input_data:
72
- return input_data[key]
73
- return self.configuration(key)
74
-
75
- def _check_configuration(self, configuration):
76
- super()._check_configuration(configuration)
77
- try:
78
- import mygrations
79
- except ModuleNotFoundError:
80
- raise ModuleNotFoundError(
81
- "You must install mygrations to use the mygrations handler. See https://github.com/cmancone/mygrations#installation"
82
- )
@@ -1,47 +0,0 @@
1
- from .routing import Routing
2
- from abc import abstractmethod
3
-
4
-
5
- class RequestMethodRouting(Routing):
6
- def __init__(self, di):
7
- super().__init__(di)
8
-
9
- @abstractmethod
10
- def method_handler_map(self):
11
- pass
12
-
13
- def handler_classes(self, configuration):
14
- return self.method_handler_map().values()
15
-
16
- def handle(self, input_output):
17
- request_method = input_output.get_request_method()
18
- method_handler_map = self.method_handler_map()
19
- if not request_method in method_handler_map:
20
- return self.error(input_output, "Invalid request method", 400)
21
- handler = self.build_handler(method_handler_map[request_method])
22
- return handler(input_output)
23
-
24
- def documentation(self):
25
- docs = []
26
- for method, handler in self.method_handler_map.items():
27
- doc = self.build_handler(method_handler_map[request_method]).documentation()
28
- if not doc:
29
- continue
30
- doc.set_request_methods(method)
31
- docs.append(doc)
32
- return docs
33
-
34
- def documentation_security_schemes(self):
35
- schemes = {}
36
- for method, handler in self.method_handler_map.items():
37
- schemes = {
38
- **schemes,
39
- **self.build_handler(method_handler_map[request_method]).documentation_security_schemes(),
40
- }
41
- return schemes
42
-
43
- def documentation_models(self):
44
- models = {}
45
- for method, handler in self.method_handler_map.items():
46
- models = {**models, **self.build_handler(method_handler_map[request_method]).documentation_models()}
47
- return models