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,265 +1,318 @@
1
- from .backend import Backend
2
- from typing import Any, Callable, Dict, List, Tuple
3
- from ..autodoc.schema import Integer as AutoDocInteger
4
- from .. import model
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from clearskies.autodoc.schema import Integer as AutoDocInteger
7
+ from clearskies.autodoc.schema import Schema as AutoDocSchema
8
+ from clearskies.backends.backend import Backend
9
+ from clearskies.di import InjectableProperties, inject
10
+ from clearskies.query import Condition, Query
11
+
12
+ if TYPE_CHECKING:
13
+ from clearskies import Model
14
+ from clearskies.di import Di
15
+
16
+
17
+ class CursorBackend(Backend, InjectableProperties):
18
+ """
19
+ The cursor backend connects your models to a MySQL or MariaDB database.
20
+
21
+ ## Installing Dependencies
22
+
23
+ clearskies uses PyMySQL to manage the database connection and make queries. This is not installed by default,
24
+ but is a named extra that you can install when needed via:
25
+
26
+ ```bash
27
+ pip install clear-skies[mysql]
28
+ ```
29
+
30
+ ## Connecting to your server
31
+
32
+ By default, database credentials are expected in environment variables:
33
+
34
+ | Name | Default | Value |
35
+ |-------------|---------|---------------------------------------------------------------|
36
+ | db_host | | The hostname where the database can be found |
37
+ | db_username | | The username to connect as |
38
+ | db_password | | The password to connect with |
39
+ | db_database | | The name of the database to use |
40
+ | db_port | 3306 | The network port to connect to |
41
+ | db_ssl_ca | | Path to a certificate to use: enables SSL over the connection |
42
+
43
+ However, you can fully control the credential provisioning process by declaring a dependency named `connection_details` and
44
+ setting it to a dictionary with the above keys, minus the `db_` prefix:
45
+
46
+ ```python
47
+ class ConnectionDetails(clearskies.di.AdditionalConfig):
48
+ provide_connection_details(self, secrets):
49
+ return {
50
+ "host": secrets.get("database_host"),
51
+ "username": secrets.get("db_username"),
52
+ "password": secrets.get("db_password"),
53
+ "database": secrets.get("db_database"),
54
+ "port": 3306,
55
+ "ssl_ca": "/path/to/ca",
56
+ }
57
+
58
+ wsgi = clearskies.contexts.Wsgi(
59
+ some_application,
60
+ additional_configs=[ConnectionDetails()],
61
+ bindings={
62
+ "secrets": "" # some configuration here to point to your secret manager
63
+ }
64
+ )
65
+ ```
66
+
67
+ Similarly, some alternate credential provisioning schemes are built into clearskies. See the
68
+ clearskies.secrets.additional_configs module for those options.
69
+
70
+ ## Connecting models to tables
71
+
72
+ The table name for your model comes from calling the `destination_name` class method of the model class. By
73
+ default, this takes the class name, converts it to snake case, and then pluralizes it. So, if you have a model
74
+ class named `UserPreference` then the cursor backend will look for a table called `user_preferences`. If this
75
+ isn't what you want, then you can simply override `destination_name` to return whatever table you want:
76
+
77
+ ```python
78
+ class UserPreference(clearskies.Model):
79
+ @classmethod
80
+ def destination_name(cls):
81
+ return "some_other_table_name"
82
+ ```
83
+
84
+ Additionally, the cursor backend accepts an argument called `table_prefix` which, if provided, will be prefixed
85
+ to your table name. Finally, you can declare a dependency called `global_table_prefix` which will automatically
86
+ be added to every table name. In the following example, the table name will be `user_configuration_preferences`
87
+ due to:
88
+
89
+ 1. The `destination_name` method sets the table name to `preferences`
90
+ 2. The `table_prefix` argument to the CursorBackend constructor adds a prefix of `configuration_`
91
+ 3. The `global_table_prefix` binding sets a prefix of `user_`, wihch goes before everything else.
92
+
93
+ ```python
94
+ import clearskies
95
+
96
+
97
+ class UserPreference(clearskies.Model):
98
+ id_column_name = "id"
99
+ backend = clearskies.backends.CursorBackend(table_prefix="configuration_")
100
+ id = clearskies.columns.Uuid()
101
+
102
+ @classmethod
103
+ def destination_name(cls):
104
+ return "preferences"
105
+
106
+
107
+ cli = clearskies.contexts.Cli(
108
+ clearskies.endpoints.Callable(
109
+ lambda user_preferences: user_preferences.create(no_data=True).id,
110
+ ),
111
+ classes=[UserPreference],
112
+ bindings={
113
+ "global_table_prefix": "user_",
114
+ },
115
+ )
116
+ ```
117
+
118
+ """
5
119
 
6
-
7
- class CursorBackend(Backend):
8
120
  supports_n_plus_one = True
9
- _cursor = None
10
-
11
- _allowed_configs = [
12
- "table_name",
13
- "wheres",
14
- "sorts",
15
- "group_by_column",
16
- "limit",
17
- "pagination",
18
- "selects",
19
- "select_all",
20
- "joins",
21
- "model_columns",
22
- ]
23
-
24
- _required_configs = [
25
- "table_name",
26
- ]
27
-
28
- def __init__(self, cursor):
29
- self._cursor = cursor
30
- from .. import ConditionParser
31
-
32
- self.condition_parser = ConditionParser()
33
-
34
- def _table_escape_character(self) -> str:
35
- """Return the character to use to escape table names in queries."""
36
- return "`"
37
-
38
- def _column_escape_character(self) -> str:
39
- """Return the character to use to escape column names in queries."""
40
- return "`"
41
-
42
- def configure(self):
43
- pass
121
+ global_table_prefix = inject.ByName("global_table_prefix")
122
+ di: Di = inject.Di() # type: ignore[assignment]
123
+
124
+ def __init__(self, cursor_dependency_name="cursor", table_prefix=""):
125
+ self.cursor_dependency_name = cursor_dependency_name
126
+ self.table_prefix = table_prefix
127
+
128
+ @property
129
+ def cursor(self):
130
+ """
131
+ Lazily inject and return the database cursor instance.
132
+
133
+ Returns
134
+ -------
135
+ The cursor object used for executing database queries.
136
+ """
137
+ if not hasattr(self, "_cursor"):
138
+ self._cursor = self.di.build(self.cursor_dependency_name)
139
+ return self._cursor
44
140
 
45
141
  def _finalize_table_name(self, table_name):
46
- escape = self._table_escape_character()
142
+ table_name = f"{self.global_table_prefix}{self.table_prefix}{table_name}"
47
143
  if "." not in table_name:
48
- return f"{escape}{table_name}{escape}"
49
- return escape + f"{escape}.{escape}".join(table_name.split(".")) + escape
144
+ return f"{self.cursor.table_escape_character}{table_name}{self.cursor.table_escape_character}"
145
+ return (
146
+ self.cursor.table_escape_character
147
+ + f"{self.cursor.table_escape_character}.{self.cursor.table_escape_character}".join(table_name.split("."))
148
+ + self.cursor.table_escape_character
149
+ )
50
150
 
51
- def update(self, id, data, model):
151
+ def update(self, id: int | str, data: dict[str, Any], model: Model) -> dict[str, Any]:
52
152
  query_parts = []
53
153
  parameters = []
54
- escape = self._column_escape_character()
55
154
  for key, val in data.items():
56
- query_parts.append(f"{escape}{key}{escape}=%s")
155
+ query_parts.append(self.cursor.column_equals_with_placeholder(key))
57
156
  parameters.append(val)
58
157
  updates = ", ".join(query_parts)
59
158
 
60
- table_name = self._finalize_table_name(model.table_name())
61
- self._cursor.execute(
62
- f"UPDATE {table_name} SET {updates} WHERE {model.id_column_name}=%s", tuple([*parameters, id])
63
- )
159
+ # update the record
160
+ table_name = self._finalize_table_name(model.destination_name())
161
+ id_equals = self.cursor.column_equals_with_placeholder(model.id_column_name)
162
+ self.cursor.execute(f"UPDATE {table_name} SET {updates} WHERE {id_equals}", tuple([*parameters, id]))
64
163
 
65
- results = self.records(
66
- {
67
- "table_name": model.table_name(),
68
- "select_all": True,
69
- "wheres": [
70
- {
71
- "column": model.id_column_name,
72
- "operator": "=",
73
- "parsed": f"{model.id_column_name}=%s",
74
- "values": [id],
75
- }
76
- ],
77
- },
78
- model,
79
- )
80
- return results[0]
164
+ # and now query again to fetch the updated record.
165
+ return self.records(Query(model.__class__, conditions=[Condition(f"{model.id_column_name}={id}")]))[0]
81
166
 
82
- def create(self, data, model):
83
- escape = self._column_escape_character()
167
+ def create(self, data: dict[str, Any], model: Model) -> dict[str, Any]:
168
+ escape = self.cursor.column_escape_character
84
169
  columns = escape + f"{escape}, {escape}".join(data.keys()) + escape
85
- placeholders = ", ".join(["%s" for i in range(len(data))])
170
+ placeholders = ", ".join([self.cursor.value_placeholder for i in range(len(data))])
86
171
 
87
- table_name = self._finalize_table_name(model.table_name())
88
- self._cursor.execute(f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})", tuple(data.values()))
172
+ table_name = self._finalize_table_name(model.destination_name())
173
+ self.cursor.execute(f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})", tuple(data.values()))
89
174
  new_id = data.get(model.id_column_name)
90
175
  if not new_id:
91
- new_id = self._cursor.lastrowid
176
+ new_id = self.cursor.lastrowid
92
177
  if not new_id:
93
178
  raise ValueError("I can't figure out what the id is for a newly created record :(")
94
179
 
95
- results = self.records(
96
- {
97
- "table_name": model.table_name(),
98
- "select_all": True,
99
- "wheres": [
100
- {
101
- "column": model.id_column_name,
102
- "operator": "=",
103
- "parsed": f"{model.id_column_name}=%s",
104
- "values": [new_id],
105
- }
106
- ],
107
- },
108
- model,
109
- )
110
- return results[0]
180
+ return self.records(Query(model.__class__, conditions=[Condition(f"{model.id_column_name}={new_id}")]))[0]
111
181
 
112
- def delete(self, id, model):
113
- table_name = self._finalize_table_name(model.table_name())
114
- self._cursor.execute(f"DELETE FROM {table_name} WHERE {model.id_column_name}=%s", (id,))
182
+ def delete(self, id: int | str, model: Model) -> bool:
183
+ table_name = self._finalize_table_name(model.destination_name())
184
+ id_equals = self.cursor.column_equals_with_placeholder(model.id_column_name)
185
+ self.cursor.execute(f"DELETE FROM {table_name} WHERE {id_equals}", (id,))
115
186
  return True
116
187
 
117
- def count(self, configuration, model):
118
- configuration = self._check_query_configuration(configuration)
119
- [query, parameters] = self.as_count_sql(configuration)
120
- self._cursor.execute(query, tuple(parameters))
121
- for row in self._cursor:
188
+ def count(self, query: Query) -> int:
189
+ (sql, parameters) = self.as_count_sql(query)
190
+ self.cursor.execute(sql, parameters)
191
+ for row in self.cursor:
122
192
  return row[0] if type(row) == tuple else row["count"]
123
193
  return 0
124
194
 
125
- def records(
126
- self, configuration: Dict[str, Any], model: model.Model, next_page_data: Dict[str, str] = None
127
- ) -> List[Dict[str, Any]]:
195
+ def records(self, query: Query, next_page_data: dict[str, str | int] | None = None) -> list[dict[str, Any]]:
128
196
  # I was going to get fancy and have this return an iterator, but since I'm going to load up
129
197
  # everything into a list anyway, I may as well just return the list, right?
130
- configuration = self._check_query_configuration(configuration)
131
- [query, parameters] = self.as_sql(configuration)
132
- self._cursor.execute(query, tuple(parameters))
133
- records = [row for row in self._cursor]
198
+ (sql, parameters) = self.as_sql(query)
199
+ self.cursor.execute(sql, parameters)
200
+ records = [row for row in self.cursor]
134
201
  if type(next_page_data) == dict:
135
- limit = configuration.get("limit", None)
136
- start = configuration.get("pagination", {}).get("start", 0)
202
+ limit = query.limit
203
+ start = query.pagination.get("start", 0)
137
204
  if limit and len(records) == limit:
138
205
  next_page_data["start"] = int(start) + int(limit)
139
206
  return records
140
207
 
141
- def group_by_clause(self, group_by):
142
- if not group_by:
143
- return ""
144
- escape = self._column_escape_character()
145
- if "." not in group_by:
146
- return f" GROUP BY {escape}{group_by}{escape}"
147
- parts = group_by.split(".", 1)
148
- table = parts[0]
149
- column = parts[1]
150
- return f" GROUP BY {escape}{table}{escape}.{escape}{column}{escape}"
151
-
152
- def as_sql(self, configuration):
153
- escape = self._column_escape_character()
154
- [wheres, parameters] = self._conditions_as_wheres_and_parameters(
155
- configuration["wheres"], configuration["table_name"]
208
+ def as_sql(self, query: Query) -> tuple[str, tuple[Any]]:
209
+ escape = self.cursor.column_escape_character
210
+ table_name = query.model_class.destination_name()
211
+ (wheres, parameters) = self.conditions_as_wheres_and_parameters(
212
+ query.conditions, query.model_class.destination_name()
156
213
  )
157
214
  select_parts = []
158
- if configuration["select_all"]:
159
- select_parts.append(self._finalize_table_name(configuration["table_name"]) + ".*")
160
- if configuration["selects"]:
161
- select_parts.extend(configuration["selects"])
215
+ if query.select_all:
216
+ select_parts.append(self._finalize_table_name(table_name) + ".*")
217
+ if query.selects:
218
+ select_parts.extend(query.selects)
162
219
  select = ", ".join(select_parts)
163
- if configuration["joins"]:
164
- joins = " " + " ".join([join["raw"] for join in configuration["joins"]])
220
+ if query.joins:
221
+ joins = " " + " ".join([join._raw_join for join in query.joins])
165
222
  else:
166
223
  joins = ""
167
- if configuration["sorts"]:
224
+ if query.sorts:
168
225
  sort_parts = []
169
- for sort in configuration["sorts"]:
170
- table_name = sort.get("table")
171
- column_name = sort["column"]
172
- direction = sort["direction"]
226
+ for sort in query.sorts:
227
+ table_name = sort.table_name
228
+ column_name = sort.column_name
229
+ direction = sort.direction
173
230
  prefix = self._finalize_table_name(table_name) + "." if table_name else ""
174
231
  sort_parts.append(f"{prefix}{escape}{column_name}{escape} {direction}")
175
232
  order_by = " ORDER BY " + ", ".join(sort_parts)
176
233
  else:
177
234
  order_by = ""
178
- group_by = self.group_by_clause(configuration["group_by_column"])
235
+ group_by = self.group_by_clause(query.group_by)
179
236
  limit = ""
180
- if configuration["limit"]:
237
+ if query.limit:
181
238
  start = 0
182
- if configuration["pagination"].get("start"):
183
- start = int(configuration["pagination"]["start"])
184
- limit = f' LIMIT {start}, {configuration["limit"]}'
239
+ limit_size = int(query.limit)
240
+ if "start" in query.pagination:
241
+ start = int(query.pagination["start"])
242
+ limit = f" LIMIT {start}, {limit_size}"
185
243
 
186
- table_name = self._finalize_table_name(configuration["table_name"])
187
- return [
244
+ table_name = self._finalize_table_name(table_name)
245
+ return (
188
246
  f"SELECT {select} FROM {table_name}{joins}{wheres}{group_by}{order_by}{limit}".strip(),
189
247
  parameters,
190
- ]
248
+ )
191
249
 
192
- def as_count_sql(self, configuration):
193
- escape = self._column_escape_character()
250
+ def as_count_sql(self, query: Query) -> tuple[str, tuple[Any]]:
251
+ escape = self.cursor.column_escape_character
194
252
  # note that this won't work if we start including a HAVING clause
195
- [wheres, parameters] = self._conditions_as_wheres_and_parameters(
196
- configuration["wheres"], configuration["table_name"]
253
+ (wheres, parameters) = self.conditions_as_wheres_and_parameters(
254
+ query.conditions, query.model_class.destination_name()
197
255
  )
198
256
  # we also don't currently support parameters in the join clause - I'll probably need that though
199
- if configuration["joins"]:
257
+ if query.joins:
200
258
  # We can ignore left joins because they don't change the count
201
- join_sections = filter(lambda join: join["type"] != "LEFT", configuration["joins"])
202
- joins = " " + " ".join([join["raw"] for join in configuration["joins"]])
259
+ join_sections = filter(lambda join: join.type != "LEFT", query.joins) # type: ignore
260
+ joins = " " + " ".join([join._raw_join for join in join_sections])
203
261
  else:
204
262
  joins = ""
205
- table_name = self._finalize_table_name(configuration["table_name"])
206
- if not configuration["group_by_column"]:
207
- query = f"SELECT COUNT(*) AS count FROM {table_name}{joins}{wheres}"
263
+ table_name = self._finalize_table_name(query.model_class.destination_name())
264
+ if not query.group_by:
265
+ query_string = f"SELECT COUNT(*) AS count FROM {table_name}{joins}{wheres}"
208
266
  else:
209
- group_by = self.group_by_clause(configuration["group_by_column"])
210
- query = (
267
+ group_by = self.group_by_clause(query.group_by)
268
+ query_string = (
211
269
  f"SELECT COUNT(*) AS count FROM (SELECT 1 FROM {table_name}{joins}{wheres}{group_by}) AS count_inner"
212
270
  )
213
- return [query, parameters]
271
+ return (query_string, parameters)
214
272
 
215
- def _conditions_as_wheres_and_parameters(self, conditions, default_table_name):
273
+ def conditions_as_wheres_and_parameters(
274
+ self, conditions: list[Condition], default_table_name: str
275
+ ) -> tuple[str, tuple[Any]]:
216
276
  if not conditions:
217
- return ["", []]
277
+ return ("", ()) # type: ignore
218
278
 
219
279
  parameters = []
220
280
  where_parts = []
221
281
  for condition in conditions:
222
- parameters.extend(condition["values"])
223
- table = condition.get("table", default_table_name)
224
- if not table:
225
- table = default_table_name
226
- column = condition["column"]
227
- column_with_table = f"{table}.{column}"
282
+ parameters.extend(condition.values)
283
+ table = condition.table_name if condition.table_name else self._finalize_table_name(default_table_name)
284
+ column = condition.column_name
228
285
  where_parts.append(
229
- self.condition_parser._with_placeholders(
230
- column_with_table,
231
- condition["operator"],
232
- condition["values"],
286
+ condition._with_placeholders(
287
+ f"{table}.{column}",
288
+ condition.operator,
289
+ condition.values,
233
290
  escape=False,
291
+ placeholder=self.cursor.value_placeholder,
234
292
  )
235
293
  )
236
- return [" WHERE " + " AND ".join(where_parts), parameters]
237
-
238
- def _check_query_configuration(self, configuration):
239
- for key in configuration.keys():
240
- if key not in self._allowed_configs:
241
- raise KeyError(f"CursorBackend does not support config '{key}'. You may be using the wrong backend")
242
-
243
- for key in self._required_configs:
244
- if key not in configuration:
245
- raise KeyError(f"Missing required configuration key {key}")
246
-
247
- if "pagination" not in configuration:
248
- configuration["pagination"] = {"start": 0}
249
- for key in self._allowed_configs:
250
- if not key in configuration:
251
- configuration[key] = [] if key[-1] == "s" else ""
252
- return configuration
253
-
254
- def validate_pagination_kwargs(self, kwargs: Dict[str, Any], case_mapping: Callable) -> str:
255
- extra_keys = set(kwargs.keys()) - set(self.allowed_pagination_keys())
294
+ return (" WHERE " + " AND ".join(where_parts), tuple(parameters)) # type: ignore
295
+
296
+ def group_by_clause(self, group_by: str) -> str:
297
+ if not group_by:
298
+ return ""
299
+ escape = self.cursor.column_escape_character
300
+ if "." not in group_by:
301
+ return f" GROUP BY {escape}{group_by}{escape}"
302
+ parts = group_by.split(".", 1)
303
+ table = parts[0]
304
+ column = parts[1]
305
+ return f" GROUP BY {escape}{table}{escape}.{escape}{column}{escape}"
306
+
307
+ def validate_pagination_data(self, data: dict[str, Any], case_mapping: Callable) -> str:
308
+ extra_keys = set(data.keys()) - set(self.allowed_pagination_keys())
256
309
  if len(extra_keys):
257
310
  key_name = case_mapping("start")
258
311
  return "Invalid pagination key(s): '" + "','".join(extra_keys) + f"'. Only '{key_name}' is allowed"
259
- if "start" not in kwargs:
312
+ if "start" not in data:
260
313
  key_name = case_mapping("start")
261
314
  return f"You must specify '{key_name}' when setting pagination"
262
- start = kwargs["start"]
315
+ start = data["start"]
263
316
  try:
264
317
  start = int(start)
265
318
  except:
@@ -267,16 +320,18 @@ class CursorBackend(Backend):
267
320
  return f"Invalid pagination data: '{key_name}' must be a number"
268
321
  return ""
269
322
 
270
- def allowed_pagination_keys(self) -> List[str]:
323
+ def allowed_pagination_keys(self) -> list[str]:
271
324
  return ["start"]
272
325
 
273
- def documentation_pagination_next_page_response(self, case_mapping: Callable) -> List[Any]:
326
+ def documentation_pagination_next_page_response(self, case_mapping: Callable[[str], str]) -> list[Any]:
274
327
  return [AutoDocInteger(case_mapping("start"), example=0)]
275
328
 
276
- def documentation_pagination_next_page_example(self, case_mapping: Callable) -> Dict[str, Any]:
329
+ def documentation_pagination_next_page_example(self, case_mapping: Callable[[str], str]) -> dict[str, Any]:
277
330
  return {case_mapping("start"): 0}
278
331
 
279
- def documentation_pagination_parameters(self, case_mapping: Callable) -> List[Tuple[Any]]:
332
+ def documentation_pagination_parameters(
333
+ self, case_mapping: Callable[[str], str]
334
+ ) -> list[tuple[AutoDocSchema, str]]:
280
335
  return [
281
336
  (
282
337
  AutoDocInteger(case_mapping("start"), example=0),