clear-skies 1.22.30__py3-none-any.whl → 2.0.0__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.

Potentially problematic release.


This version of clear-skies might be problematic. Click here for more details.

Files changed (344) hide show
  1. {clear_skies-1.22.30.dist-info → clear_skies-2.0.0.dist-info}/METADATA +5 -7
  2. clear_skies-2.0.0.dist-info/RECORD +248 -0
  3. {clear_skies-1.22.30.dist-info → clear_skies-2.0.0.dist-info}/WHEEL +1 -1
  4. clearskies/__init__.py +42 -25
  5. clearskies/action.py +7 -0
  6. clearskies/authentication/__init__.py +8 -41
  7. clearskies/authentication/authentication.py +42 -0
  8. clearskies/authentication/authorization.py +4 -9
  9. clearskies/authentication/authorization_pass_through.py +11 -9
  10. clearskies/authentication/jwks.py +128 -58
  11. clearskies/authentication/public.py +3 -38
  12. clearskies/authentication/secret_bearer.py +516 -54
  13. clearskies/autodoc/formats/oai3_json/__init__.py +1 -1
  14. clearskies/autodoc/formats/oai3_json/oai3_json.py +9 -7
  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 +4 -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 +7 -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 +1100 -284
  42. clearskies/backends/backend.py +40 -84
  43. clearskies/backends/cursor_backend.py +236 -186
  44. clearskies/backends/memory_backend.py +519 -226
  45. clearskies/backends/secrets_backend.py +75 -31
  46. clearskies/column.py +1232 -0
  47. clearskies/columns/__init__.py +71 -0
  48. clearskies/columns/audit.py +205 -0
  49. clearskies/columns/belongs_to_id.py +483 -0
  50. clearskies/columns/belongs_to_model.py +128 -0
  51. clearskies/columns/belongs_to_self.py +105 -0
  52. clearskies/columns/boolean.py +109 -0
  53. clearskies/columns/category_tree.py +275 -0
  54. clearskies/columns/category_tree_ancestors.py +51 -0
  55. clearskies/columns/category_tree_children.py +127 -0
  56. clearskies/columns/category_tree_descendants.py +48 -0
  57. clearskies/columns/created.py +94 -0
  58. clearskies/columns/created_by_authorization_data.py +116 -0
  59. clearskies/columns/created_by_header.py +99 -0
  60. clearskies/columns/created_by_ip.py +92 -0
  61. clearskies/columns/created_by_routing_data.py +96 -0
  62. clearskies/columns/created_by_user_agent.py +92 -0
  63. clearskies/columns/date.py +230 -0
  64. clearskies/columns/datetime.py +278 -0
  65. clearskies/columns/email.py +76 -0
  66. clearskies/columns/float.py +149 -0
  67. clearskies/columns/has_many.py +505 -0
  68. clearskies/columns/has_many_self.py +56 -0
  69. clearskies/columns/has_one.py +14 -0
  70. clearskies/columns/integer.py +156 -0
  71. clearskies/columns/json.py +122 -0
  72. clearskies/columns/many_to_many_ids.py +333 -0
  73. clearskies/columns/many_to_many_ids_with_data.py +270 -0
  74. clearskies/columns/many_to_many_models.py +154 -0
  75. clearskies/columns/many_to_many_pivots.py +133 -0
  76. clearskies/columns/phone.py +158 -0
  77. clearskies/columns/select.py +91 -0
  78. clearskies/columns/string.py +98 -0
  79. clearskies/columns/timestamp.py +160 -0
  80. clearskies/columns/updated.py +110 -0
  81. clearskies/columns/uuid.py +86 -0
  82. clearskies/configs/README.md +105 -0
  83. clearskies/configs/__init__.py +159 -0
  84. clearskies/configs/actions.py +43 -0
  85. clearskies/configs/any.py +13 -0
  86. clearskies/configs/any_dict.py +22 -0
  87. clearskies/configs/any_dict_or_callable.py +23 -0
  88. clearskies/configs/authentication.py +23 -0
  89. clearskies/configs/authorization.py +23 -0
  90. clearskies/configs/boolean.py +16 -0
  91. clearskies/configs/boolean_or_callable.py +18 -0
  92. clearskies/configs/callable_config.py +18 -0
  93. clearskies/configs/columns.py +34 -0
  94. clearskies/configs/conditions.py +30 -0
  95. clearskies/configs/config.py +21 -0
  96. clearskies/configs/datetime.py +18 -0
  97. clearskies/configs/datetime_or_callable.py +19 -0
  98. clearskies/configs/endpoint.py +23 -0
  99. clearskies/configs/float.py +16 -0
  100. clearskies/configs/float_or_callable.py +18 -0
  101. clearskies/configs/integer.py +16 -0
  102. clearskies/configs/integer_or_callable.py +18 -0
  103. clearskies/configs/joins.py +30 -0
  104. clearskies/configs/list_any_dict.py +30 -0
  105. clearskies/configs/list_any_dict_or_callable.py +31 -0
  106. clearskies/configs/model_class.py +35 -0
  107. clearskies/configs/model_column.py +65 -0
  108. clearskies/configs/model_columns.py +56 -0
  109. clearskies/configs/model_destination_name.py +25 -0
  110. clearskies/configs/model_to_id_column.py +43 -0
  111. clearskies/configs/readable_model_column.py +9 -0
  112. clearskies/configs/readable_model_columns.py +9 -0
  113. clearskies/configs/schema.py +23 -0
  114. clearskies/configs/searchable_model_columns.py +9 -0
  115. clearskies/configs/security_headers.py +39 -0
  116. clearskies/configs/select.py +26 -0
  117. clearskies/configs/select_list.py +47 -0
  118. clearskies/configs/string.py +29 -0
  119. clearskies/configs/string_dict.py +32 -0
  120. clearskies/configs/string_list.py +32 -0
  121. clearskies/configs/string_list_or_callable.py +35 -0
  122. clearskies/configs/string_or_callable.py +18 -0
  123. clearskies/configs/timedelta.py +18 -0
  124. clearskies/configs/timezone.py +18 -0
  125. clearskies/configs/url.py +23 -0
  126. clearskies/configs/validators.py +45 -0
  127. clearskies/configs/writeable_model_column.py +9 -0
  128. clearskies/configs/writeable_model_columns.py +9 -0
  129. clearskies/configurable.py +76 -0
  130. clearskies/contexts/__init__.py +8 -8
  131. clearskies/contexts/cli.py +5 -42
  132. clearskies/contexts/context.py +78 -56
  133. clearskies/contexts/wsgi.py +13 -30
  134. clearskies/contexts/wsgi_ref.py +49 -0
  135. clearskies/di/__init__.py +10 -7
  136. clearskies/di/additional_config.py +115 -4
  137. clearskies/di/additional_config_auto_import.py +12 -0
  138. clearskies/di/di.py +742 -121
  139. clearskies/di/inject/__init__.py +23 -0
  140. clearskies/di/inject/by_class.py +21 -0
  141. clearskies/di/inject/by_name.py +18 -0
  142. clearskies/di/inject/di.py +13 -0
  143. clearskies/di/inject/environment.py +14 -0
  144. clearskies/di/inject/input_output.py +20 -0
  145. clearskies/di/inject/now.py +13 -0
  146. clearskies/di/inject/requests.py +13 -0
  147. clearskies/di/inject/secrets.py +14 -0
  148. clearskies/di/inject/utcnow.py +13 -0
  149. clearskies/di/inject/uuid.py +15 -0
  150. clearskies/di/injectable.py +29 -0
  151. clearskies/di/injectable_properties.py +131 -0
  152. clearskies/end.py +183 -0
  153. clearskies/endpoint.py +1309 -0
  154. clearskies/endpoint_group.py +297 -0
  155. clearskies/endpoints/__init__.py +23 -0
  156. clearskies/endpoints/advanced_search.py +526 -0
  157. clearskies/endpoints/callable.py +387 -0
  158. clearskies/endpoints/create.py +202 -0
  159. clearskies/endpoints/delete.py +139 -0
  160. clearskies/endpoints/get.py +275 -0
  161. clearskies/endpoints/health_check.py +181 -0
  162. clearskies/endpoints/list.py +573 -0
  163. clearskies/endpoints/restful_api.py +427 -0
  164. clearskies/endpoints/simple_search.py +286 -0
  165. clearskies/endpoints/update.py +190 -0
  166. clearskies/environment.py +5 -3
  167. clearskies/exceptions/__init__.py +17 -0
  168. clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
  169. clearskies/exceptions/moved_permanently.py +3 -0
  170. clearskies/exceptions/moved_temporarily.py +3 -0
  171. clearskies/exceptions/not_found.py +2 -0
  172. clearskies/functional/__init__.py +2 -2
  173. clearskies/functional/routing.py +92 -0
  174. clearskies/functional/string.py +19 -11
  175. clearskies/functional/validations.py +61 -9
  176. clearskies/input_outputs/__init__.py +9 -7
  177. clearskies/input_outputs/cli.py +130 -142
  178. clearskies/input_outputs/exceptions/__init__.py +1 -1
  179. clearskies/input_outputs/headers.py +45 -0
  180. clearskies/input_outputs/input_output.py +91 -122
  181. clearskies/input_outputs/programmatic.py +69 -0
  182. clearskies/input_outputs/wsgi.py +23 -38
  183. clearskies/model.py +489 -184
  184. clearskies/parameters_to_properties.py +31 -0
  185. clearskies/query/__init__.py +12 -0
  186. clearskies/query/condition.py +223 -0
  187. clearskies/query/join.py +136 -0
  188. clearskies/query/query.py +196 -0
  189. clearskies/query/sort.py +27 -0
  190. clearskies/schema.py +82 -0
  191. clearskies/secrets/__init__.py +3 -31
  192. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
  193. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
  194. clearskies/secrets/akeyless.py +88 -147
  195. clearskies/secrets/secrets.py +8 -8
  196. clearskies/security_header.py +8 -0
  197. clearskies/security_headers/__init__.py +8 -8
  198. clearskies/security_headers/cache_control.py +47 -110
  199. clearskies/security_headers/cors.py +40 -95
  200. clearskies/security_headers/csp.py +76 -151
  201. clearskies/security_headers/hsts.py +14 -16
  202. clearskies/test_base.py +8 -0
  203. clearskies/typing.py +11 -0
  204. clearskies/validator.py +25 -0
  205. clearskies/validators/__init__.py +33 -0
  206. clearskies/validators/after_column.py +62 -0
  207. clearskies/validators/before_column.py +13 -0
  208. clearskies/validators/in_the_future.py +32 -0
  209. clearskies/validators/in_the_future_at_least.py +11 -0
  210. clearskies/validators/in_the_future_at_most.py +10 -0
  211. clearskies/validators/in_the_past.py +32 -0
  212. clearskies/validators/in_the_past_at_least.py +10 -0
  213. clearskies/validators/in_the_past_at_most.py +10 -0
  214. clearskies/validators/maximum_length.py +26 -0
  215. clearskies/validators/maximum_value.py +29 -0
  216. clearskies/validators/minimum_length.py +26 -0
  217. clearskies/validators/minimum_value.py +29 -0
  218. clearskies/validators/required.py +35 -0
  219. clearskies/validators/timedelta.py +59 -0
  220. clearskies/validators/unique.py +31 -0
  221. clear_skies-1.22.30.dist-info/RECORD +0 -214
  222. clearskies/application.py +0 -29
  223. clearskies/authentication/auth0_jwks.py +0 -118
  224. clearskies/authentication/auth_exception.py +0 -2
  225. clearskies/authentication/jwks_jwcrypto.py +0 -51
  226. clearskies/backends/api_get_only_backend.py +0 -48
  227. clearskies/backends/example_backend.py +0 -43
  228. clearskies/backends/file_backend.py +0 -48
  229. clearskies/backends/json_backend.py +0 -7
  230. clearskies/backends/restful_api_advanced_search_backend.py +0 -103
  231. clearskies/binding_config.py +0 -16
  232. clearskies/column_types/__init__.py +0 -203
  233. clearskies/column_types/audit.py +0 -249
  234. clearskies/column_types/belongs_to.py +0 -271
  235. clearskies/column_types/boolean.py +0 -60
  236. clearskies/column_types/category_tree.py +0 -304
  237. clearskies/column_types/column.py +0 -373
  238. clearskies/column_types/created.py +0 -26
  239. clearskies/column_types/created_by_authorization_data.py +0 -26
  240. clearskies/column_types/created_by_header.py +0 -24
  241. clearskies/column_types/created_by_ip.py +0 -17
  242. clearskies/column_types/created_by_routing_data.py +0 -25
  243. clearskies/column_types/created_by_user_agent.py +0 -17
  244. clearskies/column_types/created_micro.py +0 -26
  245. clearskies/column_types/datetime.py +0 -109
  246. clearskies/column_types/datetime_micro.py +0 -12
  247. clearskies/column_types/email.py +0 -18
  248. clearskies/column_types/float.py +0 -43
  249. clearskies/column_types/has_many.py +0 -179
  250. clearskies/column_types/has_one.py +0 -60
  251. clearskies/column_types/integer.py +0 -41
  252. clearskies/column_types/json.py +0 -25
  253. clearskies/column_types/many_to_many.py +0 -278
  254. clearskies/column_types/many_to_many_with_data.py +0 -162
  255. clearskies/column_types/phone.py +0 -48
  256. clearskies/column_types/select.py +0 -11
  257. clearskies/column_types/string.py +0 -24
  258. clearskies/column_types/timestamp.py +0 -73
  259. clearskies/column_types/updated.py +0 -26
  260. clearskies/column_types/updated_micro.py +0 -26
  261. clearskies/column_types/uuid.py +0 -25
  262. clearskies/columns.py +0 -123
  263. clearskies/condition_parser.py +0 -172
  264. clearskies/contexts/build_context.py +0 -54
  265. clearskies/contexts/convert_to_application.py +0 -190
  266. clearskies/contexts/extract_handler.py +0 -37
  267. clearskies/contexts/test.py +0 -94
  268. clearskies/decorators/__init__.py +0 -41
  269. clearskies/decorators/allow_non_json_bodies.py +0 -9
  270. clearskies/decorators/auth0_jwks.py +0 -22
  271. clearskies/decorators/authorization.py +0 -10
  272. clearskies/decorators/binding_classes.py +0 -9
  273. clearskies/decorators/binding_modules.py +0 -9
  274. clearskies/decorators/bindings.py +0 -9
  275. clearskies/decorators/create.py +0 -10
  276. clearskies/decorators/delete.py +0 -10
  277. clearskies/decorators/docs.py +0 -14
  278. clearskies/decorators/get.py +0 -10
  279. clearskies/decorators/jwks.py +0 -26
  280. clearskies/decorators/merge.py +0 -124
  281. clearskies/decorators/patch.py +0 -10
  282. clearskies/decorators/post.py +0 -10
  283. clearskies/decorators/public.py +0 -11
  284. clearskies/decorators/response_headers.py +0 -10
  285. clearskies/decorators/return_raw_response.py +0 -9
  286. clearskies/decorators/schema.py +0 -10
  287. clearskies/decorators/secret_bearer.py +0 -24
  288. clearskies/decorators/security_headers.py +0 -10
  289. clearskies/di/standard_dependencies.py +0 -151
  290. clearskies/handlers/__init__.py +0 -41
  291. clearskies/handlers/advanced_search.py +0 -271
  292. clearskies/handlers/base.py +0 -479
  293. clearskies/handlers/callable.py +0 -192
  294. clearskies/handlers/create.py +0 -35
  295. clearskies/handlers/crud_by_method.py +0 -18
  296. clearskies/handlers/database_connector.py +0 -32
  297. clearskies/handlers/delete.py +0 -61
  298. clearskies/handlers/exceptions/__init__.py +0 -5
  299. clearskies/handlers/exceptions/not_found.py +0 -3
  300. clearskies/handlers/get.py +0 -156
  301. clearskies/handlers/health_check.py +0 -59
  302. clearskies/handlers/input_processing.py +0 -79
  303. clearskies/handlers/list.py +0 -530
  304. clearskies/handlers/mygrations.py +0 -82
  305. clearskies/handlers/request_method_routing.py +0 -47
  306. clearskies/handlers/restful_api.py +0 -218
  307. clearskies/handlers/routing.py +0 -62
  308. clearskies/handlers/schema_helper.py +0 -128
  309. clearskies/handlers/simple_routing.py +0 -206
  310. clearskies/handlers/simple_routing_route.py +0 -197
  311. clearskies/handlers/simple_search.py +0 -136
  312. clearskies/handlers/update.py +0 -102
  313. clearskies/handlers/write.py +0 -193
  314. clearskies/input_requirements/__init__.py +0 -78
  315. clearskies/input_requirements/after.py +0 -36
  316. clearskies/input_requirements/before.py +0 -36
  317. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  318. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  319. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  320. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  321. clearskies/input_requirements/maximum_length.py +0 -19
  322. clearskies/input_requirements/maximum_value.py +0 -19
  323. clearskies/input_requirements/minimum_length.py +0 -22
  324. clearskies/input_requirements/minimum_value.py +0 -19
  325. clearskies/input_requirements/required.py +0 -23
  326. clearskies/input_requirements/requirement.py +0 -25
  327. clearskies/input_requirements/time_delta.py +0 -38
  328. clearskies/input_requirements/unique.py +0 -18
  329. clearskies/mocks/__init__.py +0 -7
  330. clearskies/mocks/input_output.py +0 -124
  331. clearskies/mocks/models.py +0 -142
  332. clearskies/models.py +0 -350
  333. clearskies/security_headers/base.py +0 -12
  334. clearskies/tests/simple_api/models/__init__.py +0 -2
  335. clearskies/tests/simple_api/models/status.py +0 -23
  336. clearskies/tests/simple_api/models/user.py +0 -21
  337. clearskies/tests/simple_api/users_api.py +0 -64
  338. {clear_skies-1.22.30.dist-info → clear_skies-2.0.0.dist-info}/LICENSE +0 -0
  339. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  340. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  341. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  342. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  343. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  344. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -1,265 +1,313 @@
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 typing import Any, Callable
2
+
3
+ import clearskies.model
4
+ import clearskies.query
5
+ from clearskies.autodoc.schema import Integer as AutoDocInteger
6
+ from clearskies.autodoc.schema import Schema as AutoDocSchema
7
+ from clearskies.backends.backend import Backend
8
+ from clearskies.di import InjectableProperties, inject
9
+
10
+
11
+ class CursorBackend(Backend, InjectableProperties):
12
+ """
13
+ The cursor backend connects your models to a MySQL or MariaDB database.
14
+
15
+ ## Installing Dependencies
16
+
17
+ clearskies uses PyMySQL to manage the database connection and make queries. This is not installed by default,
18
+ but is a named extra that you can install when needed via:
19
+
20
+ ```bash
21
+ pip install clear-skies[mysql]
22
+ ```
23
+
24
+ ## Connecting to your server
25
+
26
+ By default, database credentials are expected in environment variables:
27
+
28
+ | Name | Default | Value |
29
+ |-------------|---------|---------------------------------------------------------------|
30
+ | db_host | | The hostname where the database can be found |
31
+ | db_username | | The username to connect as |
32
+ | db_password | | The password to connect with |
33
+ | db_database | | The name of the database to use |
34
+ | db_port | 3306 | The network port to connect to |
35
+ | db_ssl_ca | | Path to a certificate to use: enables SSL over the connection |
36
+
37
+ However, you can fully control the credential provisioning process by declaring a dependency named `connection_details` and
38
+ setting it to a dictionary with the above keys, minus the `db_` prefix:
39
+
40
+ ```python
41
+ class ConnectionDetails(clearskies.di.AdditionalConfig):
42
+ provide_connection_details(self, secrets):
43
+ return {
44
+ "host": secrets.get("database_host"),
45
+ "username": secrets.get("db_username"),
46
+ "password": secrets.get("db_password"),
47
+ "database": secrets.get("db_database"),
48
+ "port": 3306,
49
+ "ssl_ca": "/path/to/ca",
50
+ }
51
+
52
+ wsgi = clearskies.contexts.Wsgi(
53
+ some_application,
54
+ additional_configs=[ConnectionDetails()],
55
+ bindings={
56
+ "secrets": "" # some configuration here to point to your secret manager
57
+ }
58
+ )
59
+ ```
60
+
61
+ Similarly, some alternate credential provisioning schemes are built into clearskies. See the
62
+ clearskies.secrets.additional_configs module for those options.
63
+
64
+ ## Connecting models to tables
65
+
66
+ The table name for your model comes from calling the `destination_name` class method of the model class. By
67
+ default, this takes the class name, converts it to snake case, and then pluralizes it. So, if you have a model
68
+ class named `UserPreference` then the cursor backend will look for a table called `user_preferences`. If this
69
+ isn't what you want, then you can simply override `destination_name` to return whatever table you want:
70
+
71
+ ```python
72
+ class UserPreference(clearskies.Model):
73
+ @classmethod
74
+ def destination_name(cls):
75
+ return "some_other_table_name"
76
+ ```
77
+
78
+ Additionally, the cursor backend accepts an argument called `table_prefix` which, if provided, will be prefixed
79
+ to your table name. Finally, you can declare a dependency called `global_table_prefix` which will automatically
80
+ be added to every table name. In the following example, the table name will be `user_configuration_preferences`
81
+ due to:
82
+
83
+ 1. The `destination_name` method sets the table name to `preferences`
84
+ 2. The `table_prefix` argument to the CursorBackend constructor adds a prefix of `configuration_`
85
+ 3. The `global_table_prefix` binding sets a prefix of `user_`, wihch goes before everything else.
86
+
87
+ ```python
88
+ import clearskies
89
+
90
+
91
+ class UserPreference(clearskies.Model):
92
+ id_column_name = "id"
93
+ backend = clearskies.backends.CursorBackend(table_prefix="configuration_")
94
+ id = clearskies.columns.Uuid()
95
+
96
+ @classmethod
97
+ def destination_name(cls):
98
+ return "preferences"
99
+
100
+
101
+ cli = clearskies.contexts.Cli(
102
+ clearskies.endpoints.Callable(
103
+ lambda user_preferences: user_preferences.create(no_data=True).id,
104
+ ),
105
+ classes=[UserPreference],
106
+ bindings={
107
+ "global_table_prefix": "user_",
108
+ },
109
+ )
110
+ ```
111
+
112
+ """
5
113
 
6
-
7
- class CursorBackend(Backend):
8
114
  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
115
+ cursor = inject.ByName("cursor")
116
+ global_table_prefix = inject.ByName("global_table_prefix")
117
+ table_escape_character = "`"
118
+ column_escape_character = "`"
119
+ table_prefix = ""
120
+
121
+ def __init__(self, table_escape_character="`", column_escape_character="`", table_prefix=""):
122
+ self.table_escape_character = table_escape_character
123
+ self.column_escape_character = column_escape_character
124
+ self.table_prefix = table_prefix
44
125
 
45
126
  def _finalize_table_name(self, table_name):
46
- escape = self._table_escape_character()
127
+ table_name = f"{self.global_table_prefix}{self.table_prefix}{table_name}"
47
128
  if "." not in table_name:
48
- return f"{escape}{table_name}{escape}"
49
- return escape + f"{escape}.{escape}".join(table_name.split(".")) + escape
129
+ return f"{self.table_escape_character}{table_name}{self.table_escape_character}"
130
+ return (
131
+ self.table_escape_character
132
+ + f"{self.table_escape_character}.{self.table_escape_character}".join(table_name.split("."))
133
+ + self.table_escape_character
134
+ )
50
135
 
51
- def update(self, id, data, model):
136
+ def update(self, id: int | str, data: dict[str, Any], model: clearskies.model.Model) -> dict[str, Any]:
52
137
  query_parts = []
53
138
  parameters = []
54
- escape = self._column_escape_character()
139
+ escape = self.column_escape_character
55
140
  for key, val in data.items():
56
141
  query_parts.append(f"{escape}{key}{escape}=%s")
57
142
  parameters.append(val)
58
143
  updates = ", ".join(query_parts)
59
144
 
60
- table_name = self._finalize_table_name(model.table_name())
61
- self._cursor.execute(
145
+ # update the record
146
+ table_name = self._finalize_table_name(model.destination_name())
147
+ self.cursor.execute(
62
148
  f"UPDATE {table_name} SET {updates} WHERE {model.id_column_name}=%s", tuple([*parameters, id])
63
149
  )
64
150
 
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]
151
+ # and now query again to fetch the updated record.
152
+ return self.records(
153
+ clearskies.query.Query(
154
+ model.__class__, conditions=[clearskies.query.Condition(f"{model.id_column_name}={id}")]
155
+ )
156
+ )[0]
81
157
 
82
- def create(self, data, model):
83
- escape = self._column_escape_character()
158
+ def create(self, data: dict[str, Any], model: clearskies.model.Model) -> dict[str, Any]:
159
+ escape = self.column_escape_character
84
160
  columns = escape + f"{escape}, {escape}".join(data.keys()) + escape
85
161
  placeholders = ", ".join(["%s" for i in range(len(data))])
86
162
 
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()))
163
+ table_name = self._finalize_table_name(model.destination_name())
164
+ self.cursor.execute(f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})", tuple(data.values()))
89
165
  new_id = data.get(model.id_column_name)
90
166
  if not new_id:
91
- new_id = self._cursor.lastrowid
167
+ new_id = self.cursor.lastrowid
92
168
  if not new_id:
93
169
  raise ValueError("I can't figure out what the id is for a newly created record :(")
94
170
 
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]
171
+ return self.records(
172
+ clearskies.query.Query(
173
+ model.__class__, conditions=[clearskies.query.Condition(f"{model.id_column_name}={new_id}")]
174
+ )
175
+ )[0]
111
176
 
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,))
177
+ def delete(self, id: int | str, model: clearskies.model.Model) -> bool:
178
+ table_name = self._finalize_table_name(model.destination_name())
179
+ self.cursor.execute(f"DELETE FROM {table_name} WHERE {model.id_column_name}=%s", (id,))
115
180
  return True
116
181
 
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:
182
+ def count(self, query: clearskies.query.Query) -> int:
183
+ (sql, parameters) = self.as_count_sql(query)
184
+ self.cursor.execute(sql, parameters)
185
+ for row in self.cursor:
122
186
  return row[0] if type(row) == tuple else row["count"]
123
187
  return 0
124
188
 
125
189
  def records(
126
- self, configuration: Dict[str, Any], model: model.Model, next_page_data: Dict[str, str] = None
127
- ) -> List[Dict[str, Any]]:
190
+ self, query: clearskies.query.Query, next_page_data: dict[str, str | int] | None = None
191
+ ) -> list[dict[str, Any]]:
128
192
  # I was going to get fancy and have this return an iterator, but since I'm going to load up
129
193
  # 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]
194
+ (sql, parameters) = self.as_sql(query)
195
+ self.cursor.execute(sql, parameters)
196
+ records = [row for row in self.cursor]
134
197
  if type(next_page_data) == dict:
135
- limit = configuration.get("limit", None)
136
- start = configuration.get("pagination", {}).get("start", 0)
198
+ limit = query.limit
199
+ start = query.pagination.get("start", 0)
137
200
  if limit and len(records) == limit:
138
201
  next_page_data["start"] = int(start) + int(limit)
139
202
  return records
140
203
 
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"]
204
+ def as_sql(self, query: clearskies.query.Query) -> tuple[str, tuple[Any]]:
205
+ escape = self.column_escape_character
206
+ table_name = query.model_class.destination_name()
207
+ (wheres, parameters) = self.conditions_as_wheres_and_parameters(
208
+ query.conditions, query.model_class.destination_name()
156
209
  )
157
210
  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"])
211
+ if query.select_all:
212
+ select_parts.append(self._finalize_table_name(table_name) + ".*")
213
+ if query.selects:
214
+ select_parts.extend(query.selects)
162
215
  select = ", ".join(select_parts)
163
- if configuration["joins"]:
164
- joins = " " + " ".join([join["raw"] for join in configuration["joins"]])
216
+ if query.joins:
217
+ joins = " " + " ".join([join._raw_join for join in query.joins])
165
218
  else:
166
219
  joins = ""
167
- if configuration["sorts"]:
220
+ if query.sorts:
168
221
  sort_parts = []
169
- for sort in configuration["sorts"]:
170
- table_name = sort.get("table")
171
- column_name = sort["column"]
172
- direction = sort["direction"]
222
+ for sort in query.sorts:
223
+ table_name = sort.table_name
224
+ column_name = sort.column_name
225
+ direction = sort.direction
173
226
  prefix = self._finalize_table_name(table_name) + "." if table_name else ""
174
227
  sort_parts.append(f"{prefix}{escape}{column_name}{escape} {direction}")
175
228
  order_by = " ORDER BY " + ", ".join(sort_parts)
176
229
  else:
177
230
  order_by = ""
178
- group_by = self.group_by_clause(configuration["group_by_column"])
231
+ group_by = self.group_by_clause(query.group_by)
179
232
  limit = ""
180
- if configuration["limit"]:
233
+ if query.limit:
181
234
  start = 0
182
- if configuration["pagination"].get("start"):
183
- start = int(configuration["pagination"]["start"])
184
- limit = f' LIMIT {start}, {configuration["limit"]}'
235
+ limit_size = int(query.limit)
236
+ if "start" in query.pagination:
237
+ start = int(query.pagination["start"])
238
+ limit = f" LIMIT {start}, {limit_size}"
185
239
 
186
- table_name = self._finalize_table_name(configuration["table_name"])
187
- return [
240
+ table_name = self._finalize_table_name(table_name)
241
+ return (
188
242
  f"SELECT {select} FROM {table_name}{joins}{wheres}{group_by}{order_by}{limit}".strip(),
189
243
  parameters,
190
- ]
244
+ )
191
245
 
192
- def as_count_sql(self, configuration):
193
- escape = self._column_escape_character()
246
+ def as_count_sql(self, query: clearskies.query.Query) -> tuple[str, tuple[Any]]:
247
+ escape = self.column_escape_character
194
248
  # 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"]
249
+ (wheres, parameters) = self.conditions_as_wheres_and_parameters(
250
+ query.conditions, query.model_class.destination_name()
197
251
  )
198
252
  # we also don't currently support parameters in the join clause - I'll probably need that though
199
- if configuration["joins"]:
253
+ if query.joins:
200
254
  # 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"]])
255
+ join_sections = filter(lambda join: join.type != "LEFT", query.joins) # type: ignore
256
+ joins = " " + " ".join([join._raw_join for join in join_sections])
203
257
  else:
204
258
  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}"
259
+ table_name = self._finalize_table_name(query.model_class.destination_name())
260
+ if not query.group_by:
261
+ query_string = f"SELECT COUNT(*) AS count FROM {table_name}{joins}{wheres}"
208
262
  else:
209
- group_by = self.group_by_clause(configuration["group_by_column"])
210
- query = (
263
+ group_by = self.group_by_clause(query.group_by)
264
+ query_string = (
211
265
  f"SELECT COUNT(*) AS count FROM (SELECT 1 FROM {table_name}{joins}{wheres}{group_by}) AS count_inner"
212
266
  )
213
- return [query, parameters]
267
+ return (query_string, parameters)
214
268
 
215
- def _conditions_as_wheres_and_parameters(self, conditions, default_table_name):
269
+ def conditions_as_wheres_and_parameters(
270
+ self, conditions: list[clearskies.query.Condition], default_table_name: str
271
+ ) -> tuple[str, tuple[Any]]:
216
272
  if not conditions:
217
- return ["", []]
273
+ return ("", ()) # type: ignore
218
274
 
219
275
  parameters = []
220
276
  where_parts = []
221
277
  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}"
278
+ parameters.extend(condition.values)
279
+ table = condition.table_name if condition.table_name else self._finalize_table_name(default_table_name)
280
+ column = condition.column_name
228
281
  where_parts.append(
229
- self.condition_parser._with_placeholders(
230
- column_with_table,
231
- condition["operator"],
232
- condition["values"],
282
+ condition._with_placeholders(
283
+ f"{table}.{column}",
284
+ condition.operator,
285
+ condition.values,
233
286
  escape=False,
234
287
  )
235
288
  )
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())
289
+ return (" WHERE " + " AND ".join(where_parts), tuple(parameters)) # type: ignore
290
+
291
+ def group_by_clause(self, group_by: str) -> str:
292
+ if not group_by:
293
+ return ""
294
+ escape = self.column_escape_character
295
+ if "." not in group_by:
296
+ return f" GROUP BY {escape}{group_by}{escape}"
297
+ parts = group_by.split(".", 1)
298
+ table = parts[0]
299
+ column = parts[1]
300
+ return f" GROUP BY {escape}{table}{escape}.{escape}{column}{escape}"
301
+
302
+ def validate_pagination_data(self, data: dict[str, Any], case_mapping: Callable) -> str:
303
+ extra_keys = set(data.keys()) - set(self.allowed_pagination_keys())
256
304
  if len(extra_keys):
257
305
  key_name = case_mapping("start")
258
306
  return "Invalid pagination key(s): '" + "','".join(extra_keys) + f"'. Only '{key_name}' is allowed"
259
- if "start" not in kwargs:
307
+ if "start" not in data:
260
308
  key_name = case_mapping("start")
261
309
  return f"You must specify '{key_name}' when setting pagination"
262
- start = kwargs["start"]
310
+ start = data["start"]
263
311
  try:
264
312
  start = int(start)
265
313
  except:
@@ -267,16 +315,18 @@ class CursorBackend(Backend):
267
315
  return f"Invalid pagination data: '{key_name}' must be a number"
268
316
  return ""
269
317
 
270
- def allowed_pagination_keys(self) -> List[str]:
318
+ def allowed_pagination_keys(self) -> list[str]:
271
319
  return ["start"]
272
320
 
273
- def documentation_pagination_next_page_response(self, case_mapping: Callable) -> List[Any]:
321
+ def documentation_pagination_next_page_response(self, case_mapping: Callable[[str], str]) -> list[Any]:
274
322
  return [AutoDocInteger(case_mapping("start"), example=0)]
275
323
 
276
- def documentation_pagination_next_page_example(self, case_mapping: Callable) -> Dict[str, Any]:
324
+ def documentation_pagination_next_page_example(self, case_mapping: Callable[[str], str]) -> dict[str, Any]:
277
325
  return {case_mapping("start"): 0}
278
326
 
279
- def documentation_pagination_parameters(self, case_mapping: Callable) -> List[Tuple[Any]]:
327
+ def documentation_pagination_parameters(
328
+ self, case_mapping: Callable[[str], str]
329
+ ) -> list[tuple[AutoDocSchema, str]]:
280
330
  return [
281
331
  (
282
332
  AutoDocInteger(case_mapping("start"), example=0),