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
clearskies/model.py CHANGED
@@ -1,42 +1,88 @@
1
- from abc import abstractmethod
2
- from collections import OrderedDict
3
- from .column_types import UUID
4
- from .functional import string
1
+ from __future__ import annotations
2
+
5
3
  import re
6
- from .models import Models
4
+ from abc import abstractmethod
5
+ from typing import TYPE_CHECKING, Any, Callable, Iterator, Self
6
+
7
+ from clearskies.autodoc.schema import Schema as AutoDocSchema
8
+ from clearskies.di import InjectableProperties, inject
9
+ from clearskies.functional import string
10
+ from clearskies.query import Condition, Join, Query, Sort
11
+ from clearskies.schema import Schema
12
+
13
+ if TYPE_CHECKING:
14
+ from clearskies import Column
15
+ from clearskies.backends import Backend
16
+
17
+
18
+ class Model(Schema, InjectableProperties):
19
+ """
20
+ A clearskies model.
21
+
22
+ To be useable, a model class needs three things:
7
23
 
8
- try:
9
- from typing_extensions import Self
10
- except ModuleNotFoundError:
11
- from typing import Self
24
+ 1. Column definitions
25
+ 2. The name of the id column
26
+ 3. A backend
27
+ 4. A destination name (equivalent to a table name for SQL backends)
12
28
 
29
+ """
13
30
 
14
- class Model(Models):
15
- _configured_columns = None
16
- _data = None
17
- _previous_data = None
18
- _touched_columns = None
19
- _transformed = None
20
- id_column_name = "id"
31
+ _previous_data: dict[str, Any] = {}
32
+ _data: dict[str, Any] = {}
33
+ _next_data: dict[str, Any] = {}
34
+ _transformed_data: dict[str, Any] = {}
35
+ _touched_columns: dict[str, bool] = {}
36
+ _query: Query | None = None
37
+ _query_executed: bool = False
38
+ _count: int | None = None
39
+ _next_page_data: dict[str, Any] | None = None
21
40
 
22
- def __init__(self: Self, backend, columns):
23
- super().__init__(backend, columns)
24
- self._transformed = {}
41
+ id_column_name: str = ""
42
+ backend: Backend = None # type: ignore
43
+
44
+ _di = inject.Di()
45
+
46
+ def __init__(self):
47
+ if not self.id_column_name:
48
+ raise ValueError(
49
+ f"You must define the 'id_column_name' property for every model class, but this is missing for model '{self.__class__.__name__}'"
50
+ )
51
+ if not isinstance(self.id_column_name, str):
52
+ raise TypeError(
53
+ f"The 'id_column_name' property of a model must be a string that specifies the name of the id column, but that is not the case for model '{self.__class__.__name__}'."
54
+ )
55
+ if not self.backend:
56
+ raise ValueError(
57
+ f"You must define the 'backend' property for every model class, but this is missing for model '{self.__class__.__name__}'"
58
+ )
59
+ if not hasattr(self.backend, "documentation_pagination_parameters"):
60
+ raise TypeError(
61
+ f"The 'backend' property of a model must be an object that extends the clearskies.Backend class, but that is not the case for model '{self.__class__.__name__}'."
62
+ )
63
+ self._previous_data = {}
25
64
  self._data = {}
26
- self._previous_data = None
27
- self._touched_columns = None
65
+ self._next_data = {}
66
+ self._transformed_data = {}
67
+ self._touched_columns = {}
68
+ self._query = None
69
+ self._query_executed = False
70
+ self._count = None
71
+ self._next_page_data = None
28
72
 
29
- def model_class(self: Self) -> type[Self]:
73
+ @classmethod
74
+ def destination_name(cls: type[Self]) -> str:
30
75
  """
31
- Return the model class that this models object will find/return instances of
76
+ Return the name of the destination that the model uses for data storage.
32
77
 
33
- This is needed by the models class
34
- """
35
- return self.__class__
78
+ For SQL backends, this would return the table name. Other backends will use this
79
+ same function but interpret it in whatever way it makes sense. For instance, an
80
+ API backend may treat it as a URL (or URL path), an SQS backend may expect a queue
81
+ URL, etc...
36
82
 
37
- @classmethod
38
- def table_name(cls: type[Self]) -> str:
39
- """Return the name of the table that the model uses for data storage"""
83
+ By default this takes the class name, converts from title case to snake case, and then
84
+ makes it plural.
85
+ """
40
86
  singular = string.camel_case_to_snake_case(cls.__name__)
41
87
  if singular[-1] == "y":
42
88
  return singular[:-1] + "ies"
@@ -44,103 +90,67 @@ class Model(Models):
44
90
  return singular + "es"
45
91
  return f"{singular}s"
46
92
 
47
- @abstractmethod
48
- def columns_configuration(self: Self):
49
- """Returns an ordered dictionary with the configuration for the columns"""
50
- pass
51
-
52
- def all_columns(self: Self):
53
- default = OrderedDict([(self.id_column_name, {"class": UUID})])
54
- default.update(self.columns_configuration())
55
- return default
56
-
57
- def columns(self: Self, overrides=None):
58
- # no caching if we have overrides
59
- if overrides is not None:
60
- return self._columns.configure(self.all_columns(), self.__class__, overrides=overrides)
61
-
62
- if self._configured_columns is None:
63
- self._configured_columns = self._columns.configure(self.all_columns(), self.__class__)
64
- return self._configured_columns
65
-
66
93
  def supports_n_plus_one(self: Self):
67
- return self._backend.supports_n_plus_one
68
-
69
- def __getitem__(self: Self, column_name):
70
- return self.__getattr__(column_name)
71
-
72
- def __getattr__(self: Self, column_name):
73
- # this should be adjusted to only return None for empty records if the column name corresponds
74
- # to an actual column in the table.
75
- if not self.exists:
76
- return None
77
-
78
- return self.get_transformed_from_data(column_name, self._data)
79
-
80
- def get(self: Self, column_name, silent=False):
81
- if not self.exists:
82
- return None
83
-
84
- return self.get_transformed_from_data(column_name, self._data, silent=silent)
85
-
86
- def get_transformed_from_data(self: Self, column_name, data, cache=True, check_providers=True, silent=False):
87
- if cache and column_name in self._transformed:
88
- return self._transformed[column_name]
89
-
90
- # everything in self._data came directly out of the database, but we don't want to send that off.
91
- # instead, the corresponding column has an opportunity to make changes as needed. Moreover,
92
- # it could be that the requested column_name doesn't even exist directly in self._data, but
93
- # can be provided by a column. Therefore, we're going to do some work to fulfill the request,
94
- # raise an Error if we *really* can't fulfill it, and store the results in self._transformed
95
- # as a simple local cache (self._transformed is cleared during a save operation)
96
- columns = self.columns()
97
- value = None
98
- if (column_name not in data or data[column_name] is None) and check_providers:
99
- for column in columns.values():
100
- if column.can_provide(column_name):
101
- value = column.provide(data, column_name)
102
- break
103
- if column_name not in data and value is None:
104
- if not silent:
105
- raise KeyError(f"Unknown column '{column_name}' requested from model '{self.__class__.__name__}'")
106
- return None
107
- else:
108
- value = (
109
- self._backend.column_from_backend(self.columns()[column_name], data[column_name])
110
- if column_name in self.columns()
111
- else data[column_name]
112
- )
94
+ return self.backend.supports_n_plus_one # type: ignore
113
95
 
114
- if cache:
115
- self._transformed[column_name] = value
116
- return value
96
+ def __bool__(self: Self) -> bool: # noqa: D105
97
+ if self._query:
98
+ return bool(self.__len__())
117
99
 
118
- @property
119
- def exists(self: Self) -> bool:
120
- return True if (self.id_column_name in self._data and self._data[self.id_column_name]) else False
100
+ return True if self._data else False
121
101
 
122
- @property
123
- def data(self: Self):
102
+ def get_raw_data(self: Self) -> dict[str, Any]:
103
+ self.no_queries()
124
104
  return self._data
125
105
 
126
- @data.setter
127
- def data(self: Self, data) -> None:
106
+ def set_raw_data(self: Self, data: dict[str, Any]) -> None:
107
+ self.no_queries()
128
108
  self._data = {} if data is None else data
109
+ self._transformed_data = {}
129
110
 
130
- def save(self: Self, data, columns=None) -> bool:
111
+ def save(self: Self, data: dict[str, Any] | None = None, columns: dict[str, Column] = {}, no_data=False) -> bool:
131
112
  """
132
- Save data to the database and update the model!
113
+ Save data to the database and update the model.
114
+
115
+ Executes an update if the model corresponds to a record already, or an insert if not.
116
+
117
+ There are two supported flows. One is to pass in a dictionary of data to save:
118
+
119
+ ```python
120
+ model.save({
121
+ "some_column": "New Value",
122
+ "another_column": 5,
123
+ })
124
+ ```
133
125
 
134
- Executes an update if the model corresponds to a record already, or an insert if not
126
+ And the other is to set new values on the columns attributes and then call save without data:
127
+
128
+ ```python
129
+ model.some_column = "New Value"
130
+ model.another_column = 5
131
+ model.save()
132
+ ```
133
+
134
+ You cannot combine these methods. If you set a value on a column attribute and also pass
135
+ in a dictionary of data to the save, then an exception will be raised.
135
136
  """
136
- if not len(data):
137
- raise ValueError("You have to pass in something to save!")
138
- save_columns = self.columns()
137
+ self.no_queries()
138
+ if not data and not self._next_data and not no_data:
139
+ raise ValueError("You have to pass in something to save, or set no_data=True in your call to save/create.")
140
+ if data and self._next_data:
141
+ raise ValueError(
142
+ "Save data was provided to the model class by both passing in a dictionary and setting new values on the column attributes. This is not allowed. You will have to use just one method of specifying save data."
143
+ )
144
+ if not data:
145
+ data = {**self._next_data}
146
+ self._next_data = {}
147
+
148
+ save_columns = self.get_columns()
139
149
  if columns is not None:
140
150
  for column in columns.values():
141
151
  save_columns[column.name] = column
142
152
 
143
- old_data = self.data
153
+ old_data = self.get_raw_data()
144
154
  data = self.columns_pre_save(data, save_columns)
145
155
  data = self.pre_save(data)
146
156
  if data is None:
@@ -148,11 +158,11 @@ class Model(Models):
148
158
 
149
159
  [to_save, temporary_data] = self.columns_to_backend(data, save_columns)
150
160
  to_save = self.to_backend(to_save, save_columns)
151
- if self.exists:
152
- new_data = self._backend.update(self._data[self.id_column_name], to_save, self)
161
+ if self:
162
+ new_data = self.backend.update(self._data[self.id_column_name], to_save, self) # type: ignore
153
163
  else:
154
- new_data = self._backend.create(to_save, self)
155
- id = self._backend.column_from_backend(save_columns[self.id_column_name], new_data[self.id_column_name])
164
+ new_data = self.backend.create(to_save, self) # type: ignore
165
+ id = self.backend.column_from_backend(save_columns[self.id_column_name], new_data[self.id_column_name]) # type: ignore
156
166
 
157
167
  # if we had any temporary columns add them back in
158
168
  new_data = {
@@ -163,22 +173,23 @@ class Model(Models):
163
173
  data = self.columns_post_save(data, id, save_columns)
164
174
  self.post_save(data, id)
165
175
 
166
- self.data = new_data
167
- self._transformed = {}
176
+ self.set_raw_data(new_data)
177
+ self._transformed_data = {}
168
178
  self._previous_data = old_data
169
- self._touched_columns = list(data.keys())
179
+ self._touched_columns = {key: True for key in data.keys()}
170
180
 
171
181
  self.columns_save_finished(save_columns)
172
182
  self.save_finished()
173
183
 
174
184
  return True
175
185
 
176
- def is_changing(self: Self, key, data) -> bool:
186
+ def is_changing(self: Self, key: str, data: dict[str, Any]) -> bool:
177
187
  """
178
- Returns True/False to denote if the given column is being modified by the active save operation
188
+ Return True/False to denote if the given column is being modified by the active save operation.
179
189
 
180
190
  Pass in the name of the column to check and the data dictionary from the save in progress
181
191
  """
192
+ self.no_queries()
182
193
  has_old_value = key in self._data
183
194
  has_new_value = key in data
184
195
 
@@ -187,24 +198,26 @@ class Model(Models):
187
198
  if not has_old_value:
188
199
  return True
189
200
 
190
- return self.__getattr__(key) != data[key]
201
+ return getattr(self, key) != data[key]
191
202
 
192
- def latest(self: Self, key, data):
203
+ def latest(self: Self, key: str, data: dict[str, Any]) -> Any:
193
204
  """
194
- Returns the 'latest' value for a column during the save operation
205
+ Return the 'latest' value for a column during the save operation.
195
206
 
196
- Returns either the column value from the data dictionary or the current value stored in the model
207
+ Return either the column value from the data dictionary or the current value stored in the model
197
208
  Basically, shorthand for the optimized version of: `data.get(key, default=getattr(self, key))` (which is
198
209
  less than ideal because it always builds the default value, even when not necessary)
199
210
 
200
211
  Pass in the name of the column to check and the data dictionary from the save in progress
201
212
  """
213
+ self.no_queries()
202
214
  if key in data:
203
215
  return data[key]
204
- return self.__getattr__(key)
216
+ return getattr(self, key)
205
217
 
206
- def was_changed(self: Self, key) -> bool:
207
- """Returns True/False to denote if a column was changed in the last save"""
218
+ def was_changed(self: Self, key: str) -> bool:
219
+ """Return True/False to denote if a column was changed in the last save."""
220
+ self.no_queries()
208
221
  if self._previous_data is None:
209
222
  raise ValueError("was_changed was called before a save was finished - you must save something first")
210
223
  if key not in self._touched_columns:
@@ -219,51 +232,54 @@ class Model(Models):
219
232
  if not has_old_value:
220
233
  return False
221
234
 
222
- columns = self.columns()
223
- new_value = self.__getattr__(key)
235
+ columns = self.get_columns()
236
+ new_value = self._data[key]
224
237
  old_value = self._previous_data[key]
225
238
  if key not in columns:
226
239
  return old_value != new_value
227
240
  return not columns[key].values_match(old_value, new_value)
228
241
 
229
- def previous_value(self: Self, key):
230
- return self.get_transformed_from_data(key, self._previous_data, cache=False, check_providers=False, silent=True)
242
+ def previous_value(self: Self, key: str):
243
+ self.no_queries()
244
+ return getattr(self.__class__, key).transform(self._previous_data.get(key))
231
245
 
232
246
  def delete(self: Self, except_if_not_exists=True) -> bool:
233
- if not self.exists:
247
+ self.no_queries()
248
+ if not self:
234
249
  if except_if_not_exists:
235
250
  raise ValueError("Cannot delete model that already exists")
236
251
  return True
237
252
 
238
- columns = self.columns()
253
+ columns = self.get_columns()
239
254
  self.columns_pre_delete(columns)
240
255
  self.pre_delete()
241
256
 
242
- self._backend.delete(self._data[self.id_column_name], self)
257
+ self.backend.delete(self._data[self.id_column_name], self) # type: ignore
243
258
 
244
259
  self.columns_post_delete(columns)
245
260
  self.post_delete()
246
261
  return True
247
262
 
248
- def columns_pre_save(self: Self, data, columns):
249
- """Uses the column information present in the model to make any necessary changes before saving"""
250
- for column in columns.values():
251
- data = column.pre_save(data, self)
252
- if data is None:
253
- raise ValueError(
254
- f"Column {column.name} of type {column.__class__.__name__} did not return any data for pre_save"
255
- )
256
- return data
257
-
258
- def pre_save(self, data):
259
- """
260
- A hook to extend so you can provide additional pre-save logic as needed
261
-
262
- It is passed in the data being saved and it should return the same data with adjustments as needed
263
- """
263
+ def columns_pre_save(self: Self, data: dict[str, Any], columns) -> dict[str, Any]:
264
+ """Use the column information present in the model to make any necessary changes before saving."""
265
+ iterate = True
266
+ changed = {}
267
+ while iterate:
268
+ iterate = False
269
+ for column in columns.values():
270
+ data = column.pre_save(data, self)
271
+ if data is None:
272
+ raise ValueError(
273
+ f"Column {column.name} of type {column.__class__.__name__} did not return any data for pre_save"
274
+ )
275
+
276
+ # if we have newly chnaged data then we want to loop through the pre-saves again
277
+ if data and column.name not in changed:
278
+ changed[column.name] = True
279
+ iterate = True
264
280
  return data
265
281
 
266
- def columns_to_backend(self: Self, data, columns):
282
+ def columns_to_backend(self: Self, data: dict[str, Any], columns) -> Any:
267
283
  backend_data = {**data}
268
284
  temporary_data = {}
269
285
  for column in columns.values():
@@ -273,7 +289,7 @@ class Model(Models):
273
289
  del backend_data[column.name]
274
290
  continue
275
291
 
276
- backend_data = self._backend.column_to_backend(column, backend_data)
292
+ backend_data = self.backend.column_to_backend(column, backend_data) # type: ignore
277
293
  if backend_data is None:
278
294
  raise ValueError(
279
295
  f"Column {column.name} of type {column.__class__.__name__} did not return any data for to_database"
@@ -281,44 +297,40 @@ class Model(Models):
281
297
 
282
298
  return [backend_data, temporary_data]
283
299
 
284
- def to_backend(self: Self, data, columns):
300
+ def to_backend(self: Self, data: dict[str, Any], columns) -> dict[str, Any]:
285
301
  return data
286
302
 
287
- def columns_post_save(self: Self, data, id, columns):
288
- """Uses the column information present in the model to make additional changes as needed after saving"""
303
+ def columns_post_save(self: Self, data: dict[str, Any], id: str | int, columns) -> dict[str, Any]:
304
+ """Use the column information present in the model to make additional changes as needed after saving."""
289
305
  for column in columns.values():
290
- data = column.post_save(data, self, id)
291
- if data is None:
292
- raise ValueError(
293
- f"Column {column.name} of type {column.__class__.__name__} did not return any data for post_save"
294
- )
306
+ column.post_save(data, self, id)
295
307
  return data
296
308
 
297
- def columns_save_finished(self: Self, columns):
298
- """Calls the save_finished method on all of our columns"""
309
+ def columns_save_finished(self: Self, columns) -> None:
310
+ """Call the save_finished method on all of our columns."""
299
311
  for column in columns.values():
300
312
  column.save_finished(self)
301
313
 
302
- def post_save(self: Self, data, id):
314
+ def post_save(self: Self, data: dict[str, Any], id: str | int) -> None:
303
315
  """
304
- A hook to extend so you can provide additional pre-save logic as needed
316
+ Create a hook to extend so you can provide additional pre-save logic as needed.
305
317
 
306
318
  It is passed in the data being saved as well as the id. It should take action as needed and then return
307
319
  either the original data array or an adjusted one if appropriate.
308
320
  """
309
321
  pass
310
322
 
311
- def pre_save(self: Self, data):
323
+ def pre_save(self: Self, data: dict[str, Any]) -> dict[str, Any]:
312
324
  """
313
- A hook to extend so you can provide additional pre-save logic as needed
325
+ Create a hook to extend so you can provide additional pre-save logic as needed.
314
326
 
315
327
  It is passed in the data being saved and it should return the same data with adjustments as needed
316
328
  """
317
329
  return data
318
330
 
319
- def save_finished(self: Self):
331
+ def save_finished(self: Self) -> None:
320
332
  """
321
- A hook to extend so you can provide additional logic after a save operation has fully completed
333
+ Create a hook to extend so you can provide additional logic after a save operation has fully completed.
322
334
 
323
335
  It has no retrun value and is passed no data. By the time this fires the model has already been
324
336
  updated with the new data. You can decide on the necessary actions using the `was_changed` and
@@ -326,32 +338,325 @@ class Model(Models):
326
338
  """
327
339
  pass
328
340
 
329
- def columns_pre_delete(self: Self, columns):
330
- """Uses the column information present in the model to make any necessary changes before deleting"""
341
+ def columns_pre_delete(self: Self, columns: dict[str, Column]) -> None:
342
+ """Use the column information present in the model to make any necessary changes before deleting."""
331
343
  for column in columns.values():
332
344
  column.pre_delete(self)
333
345
 
334
- def pre_delete(self: Self):
335
- """
336
- A hook to extend so you can provide additional pre-delete logic as needed
337
- """
346
+ def pre_delete(self: Self) -> None:
347
+ """Create a hook to extend so you can provide additional pre-delete logic as needed."""
338
348
  pass
339
349
 
340
- def columns_post_delete(self: Self, columns):
341
- """Uses the column information present in the model to make any necessary changes after deleting"""
350
+ def columns_post_delete(self: Self, columns: dict[str, Column]) -> None:
351
+ """Use the column information present in the model to make any necessary changes after deleting."""
342
352
  for column in columns.values():
343
353
  column.post_delete(self)
344
354
 
345
- def post_delete(self: Self):
355
+ def post_delete(self: Self) -> None:
356
+ """Create a hook to extend so you can provide additional post-delete logic as needed."""
357
+ pass
358
+
359
+ def where_for_request(
360
+ self: Self,
361
+ models: Self,
362
+ routing_data: dict[str, str],
363
+ authorization_data: dict[str, Any],
364
+ input_output: Any,
365
+ overrides: dict[str, Column] = {},
366
+ ) -> Self:
367
+ """Create a hook to automatically apply filtering whenever the model makes an appearance in a get/update/list/search handler."""
368
+ for column in self.get_columns(overrides=overrides).values():
369
+ models = column.where_for_request(models, routing_data, authorization_data, input_output) # type: ignore
370
+ return models
371
+
372
+ ##############################################################
373
+ ### From here down is functionality related to list/search ###
374
+ ##############################################################
375
+ def has_query(self) -> bool:
376
+ """Whether or not this model instance represents a query."""
377
+ return bool(self._query)
378
+
379
+ def get_query(self) -> Query:
380
+ """Fetch the query object in the model."""
381
+ return self._query if self._query else Query(self.__class__)
382
+
383
+ def as_query(self) -> Self:
346
384
  """
347
- A hook to extend so you can provide additional post-delete logic as needed
385
+ Make the model queryable.
386
+
387
+ This is used to remove the ambiguity of attempting execute a query against a model object that stores a record.
388
+
389
+ The reason this exists is because the model class is used both to query as well as to operate on single records, which can cause
390
+ subtle bugs if a developer accidentally confuses the two usages. Consider the following (partial) example:
391
+
392
+ ```python
393
+ def some_function(models):
394
+ model = models.find("id=5")
395
+ if model:
396
+ models.save({"test": "example"})
397
+ other_record = model.find("id=6")
398
+ ```
399
+
400
+ In the above example it seems likely that the intention was to use `model.save()`, not `models.save()`. Similarly, the last line
401
+ should be `models.find()`, not `model.find()`. To minimize these kinds of issues, clearskies won't let you execute a query against
402
+ an individual model record, nor will it let you execute a save against a model being used to make a query. In both cases, you'll
403
+ get an exception from clearskies, as the models track exactly how they are being used.
404
+
405
+ In some rare cases though, you may want to start a new query aginst a model that represents a single record. This is most common
406
+ if you have a function that was passed an individual model, and you'd like to use it to fetch more records without having to
407
+ inject the model class more generally. That's where the `as_query()` method comes in. It's basically just a way of telling clearskies
408
+ "yes, I really do want to start a query using a model that represents a record". So, for example:
409
+
410
+ ```python
411
+ def some_function(models):
412
+ model = models.find("id=5")
413
+ more_models = model.where("test=example") # throws an exception.
414
+ more_models = model.as_query().where("test=example") # works as expected.
415
+ ```
348
416
  """
349
- pass
417
+ new_model = self._di.build(self.__class__, cache=False)
418
+ new_model.set_query(Query(self.__class__))
419
+ return new_model
420
+
421
+ def set_query(self, query: Query) -> Self:
422
+ """Set the query object."""
423
+ self._query = query
424
+ self._query_executed = False
425
+ return self
426
+
427
+ def with_query(self, query: Query) -> Self:
428
+ return self._di.build(self.__class__, cache=False).set_query(query)
429
+
430
+ def select(self: Self, select: str) -> Self:
431
+ """
432
+ Add some additional columns to the select part of the query.
433
+
434
+ This method returns a new object with the updated query. The original model object is unmodified.
435
+ Multiple calls to this method add together. The following:
350
436
 
351
- def where_for_request(self: Self, models, routing_data, authorization_data, input_output, overrides=None):
437
+ ```python
438
+ models.select("column_1 column_2").select("column_3")
439
+ ```
440
+
441
+ will select column_1, column_2, column_3 in the final query.
352
442
  """
353
- A hook to automatically apply filtering whenever the model makes an appearance in a get/update/list/search handler.
443
+ self.no_single_model()
444
+ return self.with_query(self.get_query().add_select(select))
445
+
446
+ def select_all(self: Self, select_all=True) -> Self:
354
447
  """
355
- for column in self.columns(overrides=overrides).values():
356
- models = column.where_for_request(models, routing_data, authorization_data, input_output)
357
- return models
448
+ Set whether or not to select all columns with the query.
449
+
450
+ This method returns a new object with the updated query. The original model object is unmodified.
451
+ """
452
+ self.no_single_model()
453
+ return self.with_query(self.get_query().set_select_all(select_all))
454
+
455
+ def where(self: Self, where: str | Condition) -> Self:
456
+ """
457
+ Add the given condition to the query.
458
+
459
+ This method returns a new object with the updated query. The original model object is unmodified.
460
+
461
+ Conditions should be an SQL-like string of the form [column][operator][value] with an optional table prefix.
462
+ You can safely inject user input into the value. The column name will also be checked against the searchable
463
+ columns for the model class, and an exception will be thrown if the column doesn't exist or is not searchable.
464
+
465
+ Multiple conditions are always joined with AND. There is no explicit option for OR. The closest is using an
466
+ IN condition.
467
+
468
+ Examples:
469
+ ```python
470
+ for record in (
471
+ models.where("order_id=5").where("status IN ('ACTIVE','PENDING')").where("other_table.id=asdf")
472
+ ):
473
+ print(record.id)
474
+ ```
475
+ """
476
+ self.no_single_model()
477
+ return self.with_query(self.get_query().add_where(where if isinstance(where, Condition) else Condition(where)))
478
+
479
+ def join(self: Self, join: str) -> Self:
480
+ """Add a join clause to the query."""
481
+ self.no_single_model()
482
+ return self.with_query(self.get_query().add_join(Join(join)))
483
+
484
+ def is_joined(self: Self, table_name: str, alias: str = "") -> bool:
485
+ """
486
+ Check if a given table was already joined.
487
+
488
+ If you provide an alias then it will also verify if the table was joined with the specific alias name.
489
+ """
490
+ for join in self.get_query().joins:
491
+ if join.unaliased_table_name != table_name:
492
+ continue
493
+
494
+ if alias and join.alias != alias:
495
+ continue
496
+
497
+ return True
498
+ return False
499
+
500
+ def group_by(self: Self, group_by_column_name: str) -> Self:
501
+ self.no_single_model()
502
+ return self.with_query(self.get_query().set_group_by(group_by_column_name))
503
+
504
+ def sort_by(
505
+ self: Self,
506
+ primary_column_name: str,
507
+ primary_direction: str,
508
+ primary_table_name: str = "",
509
+ secondary_column_name: str = "",
510
+ secondary_direction: str = "",
511
+ secondary_table_name: str = "",
512
+ ) -> Self:
513
+ self.no_single_model()
514
+ sort = Sort(primary_table_name, primary_column_name, primary_direction)
515
+ secondary_sort = None
516
+ if secondary_column_name and secondary_direction:
517
+ secondary_sort = Sort(secondary_table_name, secondary_column_name, secondary_direction)
518
+ return self.with_query(self.get_query().set_sort(sort, secondary_sort))
519
+
520
+ def limit(self: Self, limit: int) -> Self:
521
+ self.no_single_model()
522
+ return self.with_query(self.get_query().set_limit(limit))
523
+
524
+ def pagination(self: Self, **pagination_data) -> Self:
525
+ self.no_single_model()
526
+ error = self.backend.validate_pagination_data(pagination_data, str)
527
+ if error:
528
+ raise ValueError(
529
+ f"Invalid pagination data for model {self.__class__.__name__} with backend "
530
+ + f"{self.backend.__class__.__name__}. {error}"
531
+ )
532
+ return self.with_query(self.get_query().set_pagination(pagination_data))
533
+
534
+ def find(self: Self, where: str | Condition) -> Self:
535
+ """
536
+ Return the first model matching a given where condition.
537
+
538
+ This is just shorthand for `models.where("column=value").find()`. Example:
539
+
540
+ ```python
541
+ model = models.find("column=value")
542
+ print(model.id)
543
+ ```
544
+ """
545
+ self.no_single_model()
546
+ return self.where(where).first()
547
+
548
+ def __len__(self: Self): # noqa: D105
549
+ self.no_single_model()
550
+ if self._count is None:
551
+ self._count = self.backend.count(self.get_query())
552
+ return self._count
553
+
554
+ def __iter__(self: Self) -> Iterator[Self]: # noqa: D105
555
+ self.no_single_model()
556
+ self._next_page_data = {}
557
+ raw_rows = self.backend.records(
558
+ self.get_query(),
559
+ next_page_data=self._next_page_data,
560
+ )
561
+ return iter([self.model(row) for row in raw_rows])
562
+
563
+ def paginate_all(self: Self) -> list[Self]:
564
+ """
565
+ Loop through all available pages of results and returns a list of all models that match the query.
566
+
567
+ NOTE: this loads up all records in memory before returning (e.g. it isn't using generators yet), so
568
+ expect delays for large record sets.
569
+
570
+ ```python
571
+ for model in models.where("column=value").paginate_all():
572
+ print(model.id)
573
+ ```
574
+ """
575
+ self.no_single_model()
576
+ next_models = self.with_query(self.get_query())
577
+ results = list(next_models.__iter__())
578
+ next_page_data = next_models.next_page_data()
579
+ while next_page_data:
580
+ next_models = self.pagination(**next_page_data)
581
+ results.extend(next_models.__iter__())
582
+ next_page_data = next_models.next_page_data()
583
+ return results
584
+
585
+ def model(self: Self, data: dict[str, Any] = {}) -> Self:
586
+ """
587
+ Create a new model object and populates it with the data in `data`.
588
+
589
+ NOTE: the difference between this and `model.create` is that model.create() actually saves a record in the backend,
590
+ while this method just creates a model object populated with the given data.
591
+ """
592
+ model = self._di.build(self.__class__, cache=False)
593
+ model.set_raw_data(data)
594
+ return model
595
+
596
+ def empty(self: Self) -> Self:
597
+ """
598
+ An alias for self.model({})
599
+ """
600
+ return self.model({})
601
+
602
+ def create(self: Self, data: dict[str, Any] = {}, columns: dict[str, Column] = {}, no_data=False) -> Self:
603
+ """
604
+ Create a new record in the backend using the information in `data`.
605
+
606
+ new_model = models.create({"column": "value"})
607
+ """
608
+ empty = self.model()
609
+ empty.save(data, columns=columns, no_data=no_data)
610
+ return empty
611
+
612
+ def first(self: Self) -> Self:
613
+ """
614
+ Return the first model matching the given query.
615
+
616
+ ```python
617
+ model = models.where("column=value").sort_by("age", "DESC").first()
618
+ print(model.id)
619
+ ```
620
+ """
621
+ self.no_single_model()
622
+ iter = self.__iter__()
623
+ try:
624
+ return iter.__next__()
625
+ except StopIteration:
626
+ return self.model()
627
+
628
+ def allowed_pagination_keys(self: Self) -> list[str]:
629
+ return self.backend.allowed_pagination_keys()
630
+
631
+ def validate_pagination_data(self, kwargs: dict[str, Any], case_mapping: Callable[[str], str]) -> str:
632
+ return self.backend.validate_pagination_data(kwargs, case_mapping)
633
+
634
+ def next_page_data(self: Self):
635
+ return self._next_page_data
636
+
637
+ def documentation_pagination_next_page_response(self: Self, case_mapping: Callable) -> list[Any]:
638
+ return self.backend.documentation_pagination_next_page_response(case_mapping)
639
+
640
+ def documentation_pagination_next_page_example(self: Self, case_mapping: Callable) -> dict[str, Any]:
641
+ return self.backend.documentation_pagination_next_page_example(case_mapping)
642
+
643
+ def documentation_pagination_parameters(self: Self, case_mapping: Callable) -> list[tuple[AutoDocSchema, str]]:
644
+ return self.backend.documentation_pagination_parameters(case_mapping)
645
+
646
+ def no_queries(self) -> None:
647
+ if self._query:
648
+ raise ValueError(
649
+ "You attempted to save/read record data for a model being used to make a query. This is not allowed, as it is typically a sign of a bug in your application code."
650
+ )
651
+
652
+ def no_single_model(self):
653
+ if self._data:
654
+ raise ValueError(
655
+ "You have attempted to execute a query against a model that represents an individual record. This is not allowed, as it is typically a sign of a bug in your application code. If this is intentional, call model.as_query() before executing your query."
656
+ )
657
+
658
+
659
+ class ModelClassReference:
660
+ @abstractmethod
661
+ def get_model_class(self) -> type[Model]:
662
+ pass