clear-skies 2.0.3__py3-none-any.whl → 2.0.5__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 (251) hide show
  1. clear_skies-2.0.5.dist-info/METADATA +74 -0
  2. clear_skies-2.0.5.dist-info/RECORD +4 -0
  3. {clear_skies-2.0.3.dist-info → clear_skies-2.0.5.dist-info}/WHEEL +1 -1
  4. clear_skies-2.0.3.dist-info/METADATA +0 -46
  5. clear_skies-2.0.3.dist-info/RECORD +0 -249
  6. clearskies/__init__.py +0 -59
  7. clearskies/action.py +0 -7
  8. clearskies/authentication/__init__.py +0 -15
  9. clearskies/authentication/authentication.py +0 -46
  10. clearskies/authentication/authorization.py +0 -16
  11. clearskies/authentication/authorization_pass_through.py +0 -20
  12. clearskies/authentication/jwks.py +0 -163
  13. clearskies/authentication/public.py +0 -5
  14. clearskies/authentication/secret_bearer.py +0 -553
  15. clearskies/autodoc/__init__.py +0 -8
  16. clearskies/autodoc/formats/__init__.py +0 -5
  17. clearskies/autodoc/formats/oai3_json/__init__.py +0 -7
  18. clearskies/autodoc/formats/oai3_json/oai3_json.py +0 -87
  19. clearskies/autodoc/formats/oai3_json/oai3_schema_resolver.py +0 -15
  20. clearskies/autodoc/formats/oai3_json/parameter.py +0 -35
  21. clearskies/autodoc/formats/oai3_json/request.py +0 -68
  22. clearskies/autodoc/formats/oai3_json/response.py +0 -28
  23. clearskies/autodoc/formats/oai3_json/schema/__init__.py +0 -11
  24. clearskies/autodoc/formats/oai3_json/schema/array.py +0 -9
  25. clearskies/autodoc/formats/oai3_json/schema/default.py +0 -13
  26. clearskies/autodoc/formats/oai3_json/schema/enum.py +0 -7
  27. clearskies/autodoc/formats/oai3_json/schema/object.py +0 -29
  28. clearskies/autodoc/formats/oai3_json/test.json +0 -1985
  29. clearskies/autodoc/py.typed +0 -0
  30. clearskies/autodoc/request/__init__.py +0 -15
  31. clearskies/autodoc/request/header.py +0 -6
  32. clearskies/autodoc/request/json_body.py +0 -6
  33. clearskies/autodoc/request/parameter.py +0 -8
  34. clearskies/autodoc/request/request.py +0 -38
  35. clearskies/autodoc/request/url_parameter.py +0 -6
  36. clearskies/autodoc/request/url_path.py +0 -6
  37. clearskies/autodoc/response/__init__.py +0 -5
  38. clearskies/autodoc/response/response.py +0 -9
  39. clearskies/autodoc/schema/__init__.py +0 -31
  40. clearskies/autodoc/schema/array.py +0 -10
  41. clearskies/autodoc/schema/base64.py +0 -8
  42. clearskies/autodoc/schema/boolean.py +0 -5
  43. clearskies/autodoc/schema/date.py +0 -5
  44. clearskies/autodoc/schema/datetime.py +0 -5
  45. clearskies/autodoc/schema/double.py +0 -5
  46. clearskies/autodoc/schema/enum.py +0 -17
  47. clearskies/autodoc/schema/integer.py +0 -6
  48. clearskies/autodoc/schema/long.py +0 -5
  49. clearskies/autodoc/schema/number.py +0 -6
  50. clearskies/autodoc/schema/object.py +0 -13
  51. clearskies/autodoc/schema/password.py +0 -5
  52. clearskies/autodoc/schema/schema.py +0 -11
  53. clearskies/autodoc/schema/string.py +0 -5
  54. clearskies/backends/__init__.py +0 -65
  55. clearskies/backends/api_backend.py +0 -1178
  56. clearskies/backends/backend.py +0 -136
  57. clearskies/backends/cursor_backend.py +0 -335
  58. clearskies/backends/memory_backend.py +0 -797
  59. clearskies/backends/secrets_backend.py +0 -106
  60. clearskies/column.py +0 -1233
  61. clearskies/columns/__init__.py +0 -71
  62. clearskies/columns/audit.py +0 -206
  63. clearskies/columns/belongs_to_id.py +0 -483
  64. clearskies/columns/belongs_to_model.py +0 -132
  65. clearskies/columns/belongs_to_self.py +0 -105
  66. clearskies/columns/boolean.py +0 -113
  67. clearskies/columns/category_tree.py +0 -275
  68. clearskies/columns/category_tree_ancestors.py +0 -51
  69. clearskies/columns/category_tree_children.py +0 -127
  70. clearskies/columns/category_tree_descendants.py +0 -48
  71. clearskies/columns/created.py +0 -95
  72. clearskies/columns/created_by_authorization_data.py +0 -116
  73. clearskies/columns/created_by_header.py +0 -99
  74. clearskies/columns/created_by_ip.py +0 -92
  75. clearskies/columns/created_by_routing_data.py +0 -97
  76. clearskies/columns/created_by_user_agent.py +0 -92
  77. clearskies/columns/date.py +0 -234
  78. clearskies/columns/datetime.py +0 -282
  79. clearskies/columns/email.py +0 -76
  80. clearskies/columns/float.py +0 -153
  81. clearskies/columns/has_many.py +0 -505
  82. clearskies/columns/has_many_self.py +0 -56
  83. clearskies/columns/has_one.py +0 -14
  84. clearskies/columns/integer.py +0 -160
  85. clearskies/columns/json.py +0 -126
  86. clearskies/columns/many_to_many_ids.py +0 -337
  87. clearskies/columns/many_to_many_ids_with_data.py +0 -274
  88. clearskies/columns/many_to_many_models.py +0 -158
  89. clearskies/columns/many_to_many_pivots.py +0 -134
  90. clearskies/columns/phone.py +0 -159
  91. clearskies/columns/select.py +0 -92
  92. clearskies/columns/string.py +0 -102
  93. clearskies/columns/timestamp.py +0 -164
  94. clearskies/columns/updated.py +0 -110
  95. clearskies/columns/uuid.py +0 -86
  96. clearskies/configs/README.md +0 -105
  97. clearskies/configs/__init__.py +0 -162
  98. clearskies/configs/actions.py +0 -43
  99. clearskies/configs/any.py +0 -13
  100. clearskies/configs/any_dict.py +0 -22
  101. clearskies/configs/any_dict_or_callable.py +0 -23
  102. clearskies/configs/authentication.py +0 -23
  103. clearskies/configs/authorization.py +0 -23
  104. clearskies/configs/boolean.py +0 -16
  105. clearskies/configs/boolean_or_callable.py +0 -18
  106. clearskies/configs/callable_config.py +0 -18
  107. clearskies/configs/columns.py +0 -34
  108. clearskies/configs/conditions.py +0 -30
  109. clearskies/configs/config.py +0 -24
  110. clearskies/configs/datetime.py +0 -18
  111. clearskies/configs/datetime_or_callable.py +0 -19
  112. clearskies/configs/endpoint.py +0 -23
  113. clearskies/configs/endpoint_list.py +0 -28
  114. clearskies/configs/float.py +0 -16
  115. clearskies/configs/float_or_callable.py +0 -18
  116. clearskies/configs/integer.py +0 -16
  117. clearskies/configs/integer_or_callable.py +0 -18
  118. clearskies/configs/joins.py +0 -30
  119. clearskies/configs/list_any_dict.py +0 -30
  120. clearskies/configs/list_any_dict_or_callable.py +0 -31
  121. clearskies/configs/model_class.py +0 -35
  122. clearskies/configs/model_column.py +0 -65
  123. clearskies/configs/model_columns.py +0 -56
  124. clearskies/configs/model_destination_name.py +0 -25
  125. clearskies/configs/model_to_id_column.py +0 -43
  126. clearskies/configs/readable_model_column.py +0 -9
  127. clearskies/configs/readable_model_columns.py +0 -9
  128. clearskies/configs/schema.py +0 -23
  129. clearskies/configs/searchable_model_columns.py +0 -9
  130. clearskies/configs/security_headers.py +0 -39
  131. clearskies/configs/select.py +0 -26
  132. clearskies/configs/select_list.py +0 -47
  133. clearskies/configs/string.py +0 -29
  134. clearskies/configs/string_dict.py +0 -32
  135. clearskies/configs/string_list.py +0 -32
  136. clearskies/configs/string_list_or_callable.py +0 -35
  137. clearskies/configs/string_or_callable.py +0 -18
  138. clearskies/configs/timedelta.py +0 -18
  139. clearskies/configs/timezone.py +0 -18
  140. clearskies/configs/url.py +0 -23
  141. clearskies/configs/validators.py +0 -45
  142. clearskies/configs/writeable_model_column.py +0 -9
  143. clearskies/configs/writeable_model_columns.py +0 -9
  144. clearskies/configurable.py +0 -76
  145. clearskies/contexts/__init__.py +0 -11
  146. clearskies/contexts/cli.py +0 -117
  147. clearskies/contexts/context.py +0 -98
  148. clearskies/contexts/wsgi.py +0 -76
  149. clearskies/contexts/wsgi_ref.py +0 -82
  150. clearskies/decorators.py +0 -33
  151. clearskies/di/__init__.py +0 -14
  152. clearskies/di/additional_config.py +0 -130
  153. clearskies/di/additional_config_auto_import.py +0 -17
  154. clearskies/di/di.py +0 -968
  155. clearskies/di/inject/__init__.py +0 -23
  156. clearskies/di/inject/by_class.py +0 -21
  157. clearskies/di/inject/by_name.py +0 -18
  158. clearskies/di/inject/di.py +0 -13
  159. clearskies/di/inject/environment.py +0 -14
  160. clearskies/di/inject/input_output.py +0 -20
  161. clearskies/di/inject/now.py +0 -13
  162. clearskies/di/inject/requests.py +0 -13
  163. clearskies/di/inject/secrets.py +0 -14
  164. clearskies/di/inject/utcnow.py +0 -13
  165. clearskies/di/inject/uuid.py +0 -15
  166. clearskies/di/injectable.py +0 -29
  167. clearskies/di/injectable_properties.py +0 -131
  168. clearskies/di/test_module/__init__.py +0 -6
  169. clearskies/di/test_module/another_module/__init__.py +0 -2
  170. clearskies/di/test_module/module_class.py +0 -5
  171. clearskies/end.py +0 -183
  172. clearskies/endpoint.py +0 -1310
  173. clearskies/endpoint_group.py +0 -310
  174. clearskies/endpoints/__init__.py +0 -23
  175. clearskies/endpoints/advanced_search.py +0 -526
  176. clearskies/endpoints/callable.py +0 -388
  177. clearskies/endpoints/create.py +0 -202
  178. clearskies/endpoints/delete.py +0 -139
  179. clearskies/endpoints/get.py +0 -275
  180. clearskies/endpoints/health_check.py +0 -181
  181. clearskies/endpoints/list.py +0 -573
  182. clearskies/endpoints/restful_api.py +0 -427
  183. clearskies/endpoints/simple_search.py +0 -286
  184. clearskies/endpoints/update.py +0 -190
  185. clearskies/environment.py +0 -104
  186. clearskies/exceptions/__init__.py +0 -17
  187. clearskies/exceptions/authentication.py +0 -2
  188. clearskies/exceptions/authorization.py +0 -2
  189. clearskies/exceptions/client_error.py +0 -2
  190. clearskies/exceptions/input_errors.py +0 -4
  191. clearskies/exceptions/moved_permanently.py +0 -3
  192. clearskies/exceptions/moved_temporarily.py +0 -3
  193. clearskies/exceptions/not_found.py +0 -2
  194. clearskies/functional/__init__.py +0 -7
  195. clearskies/functional/routing.py +0 -92
  196. clearskies/functional/string.py +0 -112
  197. clearskies/functional/validations.py +0 -76
  198. clearskies/input_outputs/__init__.py +0 -13
  199. clearskies/input_outputs/cli.py +0 -171
  200. clearskies/input_outputs/exceptions/__init__.py +0 -2
  201. clearskies/input_outputs/exceptions/cli_input_error.py +0 -2
  202. clearskies/input_outputs/exceptions/cli_not_found.py +0 -2
  203. clearskies/input_outputs/headers.py +0 -45
  204. clearskies/input_outputs/input_output.py +0 -138
  205. clearskies/input_outputs/programmatic.py +0 -69
  206. clearskies/input_outputs/py.typed +0 -0
  207. clearskies/input_outputs/wsgi.py +0 -77
  208. clearskies/model.py +0 -1922
  209. clearskies/py.typed +0 -0
  210. clearskies/query/__init__.py +0 -12
  211. clearskies/query/condition.py +0 -223
  212. clearskies/query/join.py +0 -136
  213. clearskies/query/query.py +0 -196
  214. clearskies/query/sort.py +0 -27
  215. clearskies/schema.py +0 -82
  216. clearskies/secrets/__init__.py +0 -6
  217. clearskies/secrets/additional_configs/__init__.py +0 -32
  218. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +0 -61
  219. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +0 -160
  220. clearskies/secrets/akeyless.py +0 -182
  221. clearskies/secrets/exceptions/__init__.py +0 -1
  222. clearskies/secrets/exceptions/not_found.py +0 -2
  223. clearskies/secrets/secrets.py +0 -38
  224. clearskies/security_header.py +0 -15
  225. clearskies/security_headers/__init__.py +0 -11
  226. clearskies/security_headers/cache_control.py +0 -67
  227. clearskies/security_headers/cors.py +0 -50
  228. clearskies/security_headers/csp.py +0 -94
  229. clearskies/security_headers/hsts.py +0 -22
  230. clearskies/security_headers/x_content_type_options.py +0 -0
  231. clearskies/security_headers/x_frame_options.py +0 -0
  232. clearskies/test_base.py +0 -8
  233. clearskies/typing.py +0 -11
  234. clearskies/validator.py +0 -37
  235. clearskies/validators/__init__.py +0 -33
  236. clearskies/validators/after_column.py +0 -62
  237. clearskies/validators/before_column.py +0 -13
  238. clearskies/validators/in_the_future.py +0 -32
  239. clearskies/validators/in_the_future_at_least.py +0 -11
  240. clearskies/validators/in_the_future_at_most.py +0 -10
  241. clearskies/validators/in_the_past.py +0 -32
  242. clearskies/validators/in_the_past_at_least.py +0 -10
  243. clearskies/validators/in_the_past_at_most.py +0 -10
  244. clearskies/validators/maximum_length.py +0 -26
  245. clearskies/validators/maximum_value.py +0 -29
  246. clearskies/validators/minimum_length.py +0 -26
  247. clearskies/validators/minimum_value.py +0 -29
  248. clearskies/validators/required.py +0 -34
  249. clearskies/validators/timedelta.py +0 -59
  250. clearskies/validators/unique.py +0 -30
  251. {clear_skies-2.0.3.dist-info → clear_skies-2.0.5.dist-info/licenses}/LICENSE +0 -0
clearskies/column.py DELETED
@@ -1,1233 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Any, Callable, Self, Type, overload
4
-
5
- import clearskies.configs.actions
6
- import clearskies.configs.boolean
7
- import clearskies.configs.select
8
- import clearskies.configs.string
9
- import clearskies.configs.string_or_callable
10
- import clearskies.configs.validators
11
- import clearskies.configurable
12
- import clearskies.decorators
13
- import clearskies.di
14
- import clearskies.model
15
- import clearskies.typing
16
- from clearskies.autodoc.schema import Schema as AutoDocSchema
17
- from clearskies.autodoc.schema import String as AutoDocString
18
- from clearskies.query.condition import Condition, ParsedCondition
19
- from clearskies.validator import Validator
20
-
21
- if TYPE_CHECKING:
22
- from clearskies import Model, Schema
23
-
24
-
25
- class Column(clearskies.configurable.Configurable, clearskies.di.InjectableProperties):
26
- """
27
- Columns are used to build schemes and enable a variety of levels of automation with clearskies.
28
-
29
- Columns are used to define your schemas in clearskies, especially via models. The column definitions are then used by endpoints
30
- and other aspects of the clearskies framework to automate things like input validation, front-end/backend-transformations, and more.
31
- """
32
-
33
- """
34
- The column class gets the full DI container, because it does a lot of object building itself
35
- """
36
- di = clearskies.di.inject.Di()
37
-
38
- """
39
- A default value to set for this column.
40
-
41
- The default is only used when creating a record for the first time, and only if
42
- a value for this column has not been set.
43
-
44
- ```python
45
- import clearskies
46
-
47
- class Widget(clearskies.Model):
48
- id_column_name = "id"
49
- backend = clearskies.backends.MemoryBackend()
50
-
51
- id = clearskies.columns.Uuid()
52
- name = clearskies.columns.String(default="Jane Doe")
53
-
54
- cli = clearskies.contexts.Cli(
55
- clearskies.endpoints.Callable(
56
- lambda widgets: widgets.create(no_data=True),
57
- model_class=Widget,
58
- readable_column_names=["id", "name"]
59
- ),
60
- classes=[Widget],
61
- )
62
-
63
- if __name__ == "__main__":
64
- cli()
65
- ```
66
-
67
- Which when invoked returns:
68
-
69
- ```json
70
- {
71
- "status": "success",
72
- "error": "",
73
- "data": {
74
- "id": "03806afa-b189-4729-a43c-9da5aa17bf14",
75
- "name": "Jane Doe"
76
- },
77
- "pagination": {},
78
- "input_errors": {}
79
- }
80
- ```
81
- """
82
- default = clearskies.configs.string.String(default=None)
83
-
84
- """
85
- A value to set for this column during a save operation.
86
-
87
- Unlike the default value, a setable value is always set during a save, even on update. It will
88
- even override other values, so it is intended to be used in cases where the value is always controlled
89
- programmatically.
90
-
91
- ```python
92
- import clearskies
93
- import datetime
94
-
95
- class Pet(clearskies.Model):
96
- id_column_name = "id"
97
- backend = clearskies.backends.MemoryBackend()
98
-
99
- id = clearskies.columns.Uuid()
100
- name = clearskies.columns.String(setable="Spot")
101
- date_of_birth = clearskies.columns.Date()
102
- age = clearskies.columns.Integer(
103
- setable=lambda data, model, now:
104
- (now-dateparser.parse(model.latest("date_of_birth", data))).total_seconds()/(86400*365),
105
- )
106
- created = clearskies.columns.Created()
107
-
108
- cli = clearskies.contexts.Cli(
109
- clearskies.endpoints.Callable(
110
- lambda pets: pets.create({"date_of_birth": "2020-05-03"}),
111
- model_class=Pet,
112
- readable_column_names=["id", "name", "date_of_birth", "age"]
113
- ),
114
- classes=[Pet],
115
- )
116
-
117
- if __name__ == "__main__":
118
- cli()
119
- ```
120
-
121
- Note the use of `model.latest()` above. This function returns either the column information from the data array
122
- or, if not present, the latest column value from the model itself. This makes it more flexible as it works
123
- well with both update and create operations.
124
-
125
- If you then execute this it will return something like:
126
-
127
- ```json
128
- {
129
- "status": "success",
130
- "error": "",
131
- "data": {
132
- "id": "ec4993f4-124a-44a2-8313-816d2ad51aae",
133
- "name": "Spot",
134
- "date_of_birth": "2020-05-03",
135
- "age": 5,
136
- "created": "2025-05-03T20:23:32+00:00"
137
- },
138
- "pagination": {},
139
- "input_errors": {}
140
- }
141
- ```
142
-
143
- e.g., `date_of_birth` is `age` years behind the current time (as recorded in the `created` timestamp).
144
-
145
- """
146
- setable = clearskies.configs.string_or_callable.StringOrCallable(default=None)
147
-
148
- """
149
- Whether or not this column can be converted to JSON and included in an API response.
150
-
151
- If this is set to False for a column and you attempt to set that column as a readable_column in an endpoint,
152
- clearskies will throw an exception.
153
- """
154
- is_readable = clearskies.configs.boolean.Boolean(default=True)
155
-
156
- """
157
- Whether or not this column can be set via an API call.
158
-
159
- If this is set to False for a column and you attempt to set the column as a writeable column in an endpoint,
160
- clearskies will throw an exception.
161
- """
162
- is_writeable = clearskies.configs.boolean.Boolean(default=True)
163
-
164
- """
165
- Whether or not it is possible to search by this column
166
-
167
- If this is set to False for a column and you attempt to set the column as a searchable column in an endpoint,
168
- clearskies will throw an exception.
169
- """
170
- is_searchable = clearskies.configs.boolean.Boolean(default=True)
171
-
172
- """
173
- Whether or not this column is temporary. A temporary column is not persisted to the backend.
174
-
175
- Temporary columns are useful when you want the developer or end user to set a value, but you use that value to
176
- trigger additional behavior, rather than actually recording it. Temporary columns often team up with actions
177
- or are used to calculate other values. For instance, in our setable example above, we had both an age and
178
- a date of birth column, with the date of birth calculated from the age. This obviously results in two columns
179
- with similar data. One could be marked as temporary and it will be available during the save operation, but
180
- it will be skipped when saving data to the backend:
181
-
182
- ```python
183
- import clearskies
184
-
185
- class Pet(clearskies.Model):
186
- id_column_name = "id"
187
- backend = clearskies.backends.MemoryBackend()
188
-
189
- id = clearskies.columns.Uuid()
190
- name = clearskies.columns.String()
191
- date_of_birth = clearskies.columns.Date(is_temporary=True)
192
- age = clearskies.columns.Integer(
193
- setable=lambda data, model, now:
194
- (now-dateparser.parse(model.latest("date_of_birth", data))).total_seconds()/(86400*365),
195
- )
196
- created = clearskies.columns.Created()
197
-
198
- cli = clearskies.contexts.Cli(
199
- clearskies.endpoints.Callable(
200
- lambda pets: pets.create({"name": "Spot", "date_of_birth": "2020-05-03"}),
201
- model_class=Pet,
202
- readable_column_names=["id", "age", "date_of_birth"],
203
- ),
204
- classes=[Pet],
205
- )
206
-
207
- if __name__ == "__main__":
208
- cli()
209
- ```
210
-
211
- Which will return:
212
-
213
- ```json
214
- {
215
- "status": "success",
216
- "error": "",
217
- "data": {
218
- "id": "ee532cfa-91cf-4747-b798-3c6dcd79326e",
219
- "age": 5,
220
- "date_of_birth": null
221
- },
222
- "pagination": {},
223
- "input_errors": {}
224
- }
225
- ```
226
-
227
- e.g. the date_of_birth column is empty. To be clear though, it's not just empty - clearskies made no attempt to set it.
228
- If you were using an SQL database, you would not have to put a `date_of_birth` column in your table.
229
-
230
- """
231
- is_temporary = clearskies.configs.boolean.Boolean(default=False)
232
-
233
- """
234
- Validators to use when checking the input for this column during write operations from the API.
235
-
236
- Keep in mind that the validators are only checked when the column is exposed via a supporting endpoint.
237
- You can still set whatever values you want when saving the model directly, e.g. `model.save(...)`
238
-
239
- In the below example, we require a name that is at least 5 characters long, and the date of birth must
240
- be in the past. Note that date of birth is not required, so the end-user can create a record without
241
- a date of birth. In general, only the `Required` validator will reject a non-existent input.
242
-
243
- ```python
244
- import clearskies
245
-
246
- class Pet(clearskies.Model):
247
- id_column_name = "id"
248
- backend = clearskies.backends.MemoryBackend()
249
-
250
- id = clearskies.columns.Uuid()
251
- name = clearskies.columns.String(validators=[
252
- clearskies.validators.Required(),
253
- clearskies.validators.MinimumLength(5),
254
- ])
255
- date_of_birth = clearskies.columns.Date(validators=[
256
- clearskies.validators.InThePast()
257
- ])
258
- created = clearskies.columns.Created()
259
-
260
- wsgi = clearskies.contexts.WsgiRef(
261
- clearskies.endpoints.Create(
262
- model_class=Pet,
263
- writeable_column_names=["name", "date_of_birth"],
264
- readable_column_names=["id", "name", "date_of_birth", "created"],
265
- ),
266
- )
267
- wsgi()
268
- ```
269
-
270
- You can then see the result of calling the endpoint with various kinds of invalid data:
271
-
272
- ```bash
273
- $ curl http://localhost:8080 -d '{"date_of_birth": "asdf"}'
274
- {
275
- "status": "input_errors",
276
- "error": "",
277
- "data": [],
278
- "pagination": {},
279
- "input_errors": {
280
- "name": "'name' is required.",
281
- "date_of_birth": "given value did not appear to be a valid date"
282
- }
283
- }
284
-
285
- $ curl http://localhost:8080 -d '{"name":"asdf"}' | jq
286
- {
287
- "status": "input_errors",
288
- "error": "",
289
- "data": [],
290
- "pagination": {},
291
- "input_errors": {
292
- "name": "'name' must be at least 5 characters long."
293
- }
294
- }
295
-
296
- $ curl http://localhost:8080 -d '{"name":"Longer", "date_of_birth": "2050-01-01"}' | jq
297
- {
298
- "status": "input_errors",
299
- "error": "",
300
- "data": [],
301
- "pagination": {},
302
- "input_errors": {
303
- "date_of_birth": "'date_of_birth' must be in the past"
304
- }
305
- }
306
-
307
- $ curl http://localhost:8080 -d '{"name":"Long Enough"}' | jq
308
- {
309
- "status": "success",
310
- "error": "",
311
- "data": {
312
- "id": "ace16b93-db91-49b3-a8f7-5dc6568d25f6",
313
- "name": "Long Enough",
314
- "date_of_birth": null,
315
- "created": "2025-05-03T19:32:33+00:00"
316
- },
317
- "pagination": {},
318
- "input_errors": {}
319
- }
320
- ```
321
- """
322
- validators = clearskies.configs.validators.Validators(default=[])
323
-
324
- """
325
- Actions to take during the pre-save step of the save process if the column has changed during the active save operation.
326
-
327
- Pre-save happens before the data is persisted to the backend. Actions/callables in
328
- this step must return a dictionary. The data in the dictionary will be included in the save operation.
329
- Since the save hasn't completed, any data in the model itself reflects the model before the save
330
- operation started. Actions in the pre-save step must **NOT** make any changes directly, but should **ONLY**
331
- return modified data for the save operation. In addition, they must be idempotent - they should always return
332
- the same value when called with the same data. This is because clearskies can call them more than once. If
333
- a pre-save hook changes the save data, then clearskies will call all the pre-save hooks again in case this
334
- new data needs to trigger further changes. Stateful changes should be reserved for the post_save or save_finished stages.
335
-
336
- Callables and actions can request any dependencies provided by the DI system. In addition, they can request
337
- two named parameters:
338
-
339
- 1. `model` - the model involved in the save operation
340
- 2. `data` - the new data being saved
341
-
342
- The key here is that the defined actions will be invoked regardless of how the save happens. Whether the
343
- model.save() function is called directly or the model is creatd/modified via an endpoint, your business logic
344
- will always be executed. This makes for easy reusability and consistency throughout your application.
345
-
346
- Here's an example where we want to record a timestamp anytime an order status becomes a particular value:
347
-
348
- ```python
349
- import clearskies
350
-
351
- class Order(clearskies.Model):
352
- id_column_name = "id"
353
- backend = clearskies.backends.MemoryBackend()
354
-
355
- id = clearskies.columns.Uuid()
356
- status = clearskies.columns.Select(
357
- ["Open", "On Hold", "Fulfilled"],
358
- on_change_pre_save=[
359
- lambda data, utcnow: {"fulfilled_at": utcnow} if data["status"] == "Fulfilled" else {},
360
- ],
361
- )
362
- fulfilled_at = clearskies.columns.Datetime()
363
-
364
- wsgi = clearskies.contexts.WsgiRef(
365
- clearskies.endpoints.Create(
366
- model_class=Order,
367
- writeable_column_names=["status"],
368
- readable_column_names=["id", "status", "fulfilled_at"],
369
- ),
370
- )
371
- wsgi()
372
- ```
373
-
374
- You can then see the difference depending on what you set the status to:
375
-
376
- ```bash
377
- $ curl http://localhost:8080 -d '{"status":"Open"}' | jq
378
- {
379
- "status": "success",
380
- "error": "",
381
- "data": {
382
- "id": "a732545f-51b3-4fd0-a6cf-576cf1b2872f",
383
- "status": "Open",
384
- "fulfilled_at": null
385
- },
386
- "pagination": {},
387
- "input_errors": {}
388
- }
389
-
390
- $ curl http://localhost:8080 -d '{"status":"Fulfilled"}' | jq
391
- {
392
- "status": "success",
393
- "error": "",
394
- "data": {
395
- "id": "c288bf43-2246-48e4-b168-f40cbf5376df",
396
- "status": "Fulfilled",
397
- "fulfilled_at": "2025-05-04T02:32:56+00:00"
398
- },
399
- "pagination": {},
400
- "input_errors": {}
401
- }
402
-
403
- ```
404
-
405
- """
406
- on_change_pre_save = clearskies.configs.actions.Actions(default=[])
407
-
408
- """
409
- Actions to take during the post-save step of the process if the column has changed during the active save.
410
-
411
- Post-save happens after the data is persisted to the backend but before the full save process has finished.
412
- Since the data has been persisted to the backend, any data returned by the callables/actions will be ignored.
413
- If you need to make data changes you'll have to execute a separate save operation.
414
- Since the save hasn't finished, the model is not yet updated with the new data, and
415
- any data you fetch out of the model will refelect the data in the model before the save started.
416
-
417
- Callables and actions can request any dependencies provided by the DI system. In addition, they can request
418
- three named parameters:
419
-
420
- 1. `model` - the model involved in the save operation
421
- 2. `data` - the new data being saved
422
- 3. `id` - the id of the record being saved
423
-
424
- Here's an example of using a post-save action to record a simple audit trail when the order status changes:
425
-
426
- ```python
427
- import clearskies
428
-
429
- class Order(clearskies.Model):
430
- id_column_name = "id"
431
- backend = clearskies.backends.MemoryBackend()
432
-
433
- id = clearskies.columns.Uuid()
434
- status = clearskies.columns.Select(
435
- ["Open", "On Hold", "Fulfilled"],
436
- on_change_post_save=[
437
- lambda model, data, order_histories: order_histories.create({
438
- "order_id": model.latest("id", data),
439
- "event": "Order status changed to " + data["status"]
440
- }),
441
- ],
442
- )
443
-
444
- class OrderHistory(clearskies.Model):
445
- id_column_name = "id"
446
- backend = clearskies.backends.MemoryBackend()
447
-
448
- id = clearskies.columns.Uuid()
449
- event = clearskies.columns.String()
450
- order_id = clearskies.columns.BelongsToId(Order)
451
-
452
- # include microseconds in the created_at time so that we can sort our example by created_at
453
- # and they come out in order (since, for our test program, they will all be created in the same second).
454
- created_at = clearskies.columns.Created(date_format="%Y-%m-%d %H:%M:%S.%f")
455
-
456
- def test_post_save(orders: Order, order_histories: OrderHistory):
457
- my_order = orders.create({"status": "Open"})
458
- my_order.status = "On Hold"
459
- my_order.save()
460
- my_order.save({"status": "Open"})
461
- my_order.save({"status": "Fulfilled"})
462
- return order_histories.where(OrderHistory.order_id.equals(my_order.id)).sort_by("created_at", "asc")
463
-
464
- cli = clearskies.contexts.Cli(
465
- clearskies.endpoints.Callable(
466
- test_post_save,
467
- model_class=OrderHistory,
468
- return_records=True,
469
- readable_column_names=["id", "event", "created_at"],
470
- ),
471
- classes=[Order, OrderHistory],
472
- )
473
- cli()
474
- ```
475
-
476
- Note that in our `on_change_post_save` lambda function, we use `model.latest("id", data)`. We can't just use
477
- `data["id"]` because `data` is a dictionary containing the information present in the save. During the create
478
- operation `data["id"]` will be populated, but during the subsequent edit operations it won't be - only the status
479
- column is changing. `model.latest("id", data)` is basically just short hand for: `data.get("id", model.id)`.
480
- On the other hand, we can just use `data["status"]` because the `on_change` hook is attached to the status field,
481
- so it will only fire when status is being changed, which means that the `status` key is guaranteed to be in
482
- the dictionary when the lambda is executed.
483
-
484
- Finally, the post-save action has a named parameter called `id`, so in this specific case we could use:
485
-
486
- ```python
487
- lambda data, id, order_histories: order_histories.create("order_id": id, "event": data["status"])
488
- ```
489
-
490
- When we execute the above script it will return something like:
491
-
492
- ```json
493
- {
494
- "status": "success",
495
- "error": "",
496
- "data": [
497
- {
498
- "id": "c550d714-839b-4f25-a9e1-bd7e977185ff",
499
- "event": "Order status changed to Open",
500
- "created_at": "2025-05-04T14:09:42.960119+00:00"
501
- },
502
- {
503
- "id": "f393d7b0-da21-4117-a7a4-0359fab802bb",
504
- "event": "Order status changed to On Hold",
505
- "created_at": "2025-05-04T14:09:42.960275+00:00"
506
- },
507
- {
508
- "id": "5b528a10-4a08-47ae-938c-fc7067603f8e",
509
- "event": "Order status changed to Open",
510
- "created_at": "2025-05-04T14:09:42.960395+00:00"
511
- },
512
- {
513
- "id": "91f77a88-1c38-49f7-aa1e-7f97bd9f962f",
514
- "event": "Order status changed to Fulfilled",
515
- "created_at": "2025-05-04T14:09:42.960514+00:00"
516
- }
517
- ],
518
- "pagination": {},
519
- "input_errors": {}
520
- }
521
- ```
522
-
523
- """
524
- on_change_post_save = clearskies.configs.actions.Actions(default=[])
525
-
526
- """
527
- Actions to take during the save-finished step of the save process if the column has changed in the save.
528
-
529
- Save-finished happens after the save process has completely finished and the model is updated with
530
- the final data. Any data returned by these actions will be ignored, since the save has already finished.
531
- If you need to make data changes you'll have to execute a separate save operation.
532
-
533
- Callables and actions can request any dependencies provided by the DI system. In addition, they can request
534
- the following parameter:
535
-
536
- 1. `model` - the model involved in the save operation
537
-
538
- Unlike pre_save and post_save, `data` is not provided because this data has already been merged into the
539
- model. If you need some context from the completed save operation, use methods like `was_changed` and `previous_value`.
540
- """
541
- on_change_save_finished = clearskies.configs.actions.Actions(default=[])
542
-
543
- """
544
- Use in conjunction with `created_by_source_type` to have this column automatically populated by data from an HTTP request.
545
-
546
- So, for instance, setting `created_by_source_type` to `authorization_data` and setting this to `email`
547
- will result in the email value from the authorization data being persisted into this column when the
548
- record is saved.
549
-
550
- NOTE: this is sometimes best set as a column override on an endpoint, rather than directly
551
- on the model itself. The reason is because the authorization data and header information is typically
552
- only available during an HTTP request, so if you set this on the model level, you'll get an error
553
- if you try to make saves to the model in a context where authorization data and/or headers don't exist.
554
-
555
- See created_by_source_type for usage examples.
556
- """
557
- created_by_source_key = clearskies.configs.string.String(default="")
558
-
559
- """
560
- Use in conjunction with `created_by_source_key` to have this column automatically populated by data from ann HTTP request.
561
-
562
- So, for instance, setting this to `authorization_data` and setting `created_by_source_key` to `email`
563
- will result in the email value from the authorization data being persisted into this column when the
564
- record is saved.
565
-
566
- NOTE: this is sometimes best set as a column override on an endpoint, rather than directly
567
- on the model itself. The reason is because the authorization data and header information is typically
568
- only available during an HTTP request, so if you set this on the model level, you'll get an error
569
- if you try to make saves to the model in a context where authorization data and/or headers don't exist.
570
-
571
- Here's an example:
572
-
573
- ```python
574
- class User(clearskies.Model):
575
- id_column_name = "id"
576
- backend = clearskies.backends.MemoryBackend()
577
-
578
- id = clearskies.columns.Uuid()
579
- name = clearskies.columns.String()
580
- account_id = clearskies.columns.String(
581
- created_by_source_type="routing_data",
582
- created_by_source_key="account_id",
583
- )
584
-
585
- wsgi = clearskies.contexts.WsgiRef(
586
- clearskies.endpoints.Create(
587
- User,
588
- readable_column_names=["id", "account_id", "name"],
589
- writeable_column_names=["name"],
590
- url="/:account_id",
591
- ),
592
- )
593
- wsgi()
594
- ```
595
-
596
- Note that `created_by_source_type` is `routing_data` and `created_by_source_key` is `account_id`.
597
- This means that the endpoint that creates this record must have a routing parameter named `account_id`.
598
- Naturally, our endpoint has a url of `/:account_id`, and so the parameter provided by the uesr gets
599
- reflected into the save.
600
-
601
- ```bash
602
- $ curl http://localhost:8080/1-2-3-4 -d '{"name":"Bob"}' | jq
603
- {
604
- "status": "success",
605
- "error": "",
606
- "data": {
607
- "id": "250ed725-d940-4823-aa9d-890be800404a",
608
- "account_id": "1-2-3-4",
609
- "name": "Bob"
610
- },
611
- "pagination": {},
612
- "input_errors": {}
613
- }
614
- ```
615
-
616
- """
617
- created_by_source_type = clearskies.configs.select.Select(
618
- ["authorization_data", "http_header", "routing_data", ""], default=""
619
- )
620
-
621
- """
622
- If True, and the key requested via created_by_source_key doesn't exist in the designated source, an error will be raised.
623
- """
624
- created_by_source_strict = clearskies.configs.boolean.Boolean(default=True)
625
-
626
- """ The model class this column is associated with. """
627
- model_class = clearskies.configs.Schema()
628
-
629
- """ The name of this column. """
630
- name = clearskies.configs.string.String()
631
-
632
- """
633
- Simple flag to denote if the column is unique or not.
634
-
635
- This is an internal cache. Use column.is_unique instead.
636
- """
637
- _is_unique = False
638
-
639
- """
640
- Specify if this column has additional functionality to solve the n+1 problem.
641
-
642
- Relationship columns may fetch data from additional tables when outputting results, but by default they
643
- end up making an additional query for every record (in order to grab related data). This is called the
644
- n+1 problem - a query may fetch 10 records, and then make 10 individual additional queries to select
645
- related data for each record (which obviously hampers performance). The solution to this (when using
646
- sql-like backends) is to add additional joins to the original query so that the data can all be fetched
647
- at once. Columns that are subject to this issue can set this flag to True and then define the
648
- `configure_n_plus_one` method to add the necessary joins. This method will be called as needed.
649
- """
650
- wants_n_plus_one = False
651
-
652
- """
653
- Simple flag to denote if the column is required or not.
654
-
655
- This is an internal cache. Use column.is_required instead.
656
- """
657
- _is_required = False
658
-
659
- """
660
- The list of allowed search operators for this column.
661
-
662
- All the various search methods reference this list. The idea is that a column can just fill out this list
663
- instead of having to override all the methods.
664
- """
665
- _allowed_search_operators = ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null", "like"]
666
-
667
- """
668
- The class to use when documenting this column
669
- """
670
- auto_doc_class: type[AutoDocSchema] = AutoDocString
671
-
672
- @clearskies.decorators.parameters_to_properties
673
- def __init__(
674
- self,
675
- default: str | None = None,
676
- setable: str | Callable[..., str] | None = None,
677
- is_readable: bool = True,
678
- is_writeable: bool = True,
679
- is_searchable: bool = True,
680
- is_temporary: bool = False,
681
- validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
682
- on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
683
- on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
684
- on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
685
- created_by_source_type: str = "",
686
- created_by_source_key: str = "",
687
- created_by_source_strict: bool = True,
688
- ):
689
- pass
690
-
691
- def get_model_columns(self):
692
- """Return the columns or the model this column is attached to."""
693
- return self.model_class.get_columns()
694
-
695
- def finalize_configuration(self, model_class: type[Schema], name: str) -> None:
696
- """
697
- Finalize and check the configuration.
698
-
699
- This is an external trigger called by the model class when the model class is ready.
700
- The reason it exists here instead of in the constructor is because some columns are tightly
701
- connected to the model class, and can't validate configuration until they know what the model is.
702
- Therefore, we need the model involved, and the only way for a property to know what class it is
703
- in is if the parent class checks in (which is what happens here).
704
- """
705
- self.model_class = model_class
706
- self.name = name
707
- self.finalize_and_validate_configuration()
708
-
709
- def from_backend(self, value):
710
- """
711
- Take the backend representation and returns a python representation.
712
-
713
- For instance, for an SQL date field, this will return a Python DateTime object
714
- """
715
- return str(value)
716
-
717
- def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
718
- """
719
- Make any changes needed to save the data to the backend.
720
-
721
- This typically means formatting changes - converting DateTime objects to database
722
- date strings, etc...
723
- """
724
- if self.name not in data:
725
- return data
726
-
727
- return {**data, self.name: str(data[self.name])}
728
-
729
- @overload
730
- def __get__(self, instance: None, cls: type) -> Self:
731
- pass
732
-
733
- @overload
734
- def __get__(self, instance: Model, cls: type):
735
- pass
736
-
737
- def __get__(self, instance, cls):
738
- if instance is None:
739
- # Normally this gets filled in when the model is initialized. However, the condition builders (self.equals, etc...)
740
- # can be called from the class directly, before the model is initialized and everything is populated. This
741
- # can cause trouble, but by filling in the model class we can give enough information for them to get the
742
- # job done. They have a special flow for this, we just have to provide the model class (and the __get__
743
- # function is always called, so this fixes it).
744
- self.model_class = cls
745
- return self
746
-
747
- # this makes sure we're initialized
748
- if "name" not in self._config: # type: ignore
749
- instance.get_columns()
750
-
751
- if self.name not in instance._data:
752
- return None # type: ignore
753
-
754
- if self.name not in instance._transformed_data:
755
- instance._transformed_data[self.name] = self.from_backend(instance._data[self.name])
756
-
757
- return instance._transformed_data[self.name]
758
-
759
- def __set__(self, instance: Model, value) -> None:
760
- # this makes sure we're initialized
761
- if "name" not in self._config: # type: ignore
762
- instance.get_columns()
763
-
764
- instance._next_data[self.name] = value
765
-
766
- def finalize_and_validate_configuration(self):
767
- super().finalize_and_validate_configuration()
768
-
769
- if self.setable is not None and self.created_by_source_type:
770
- raise ValueError(
771
- "You attempted to set both 'setable' and 'created_by_source_type', but these configurations are mutually exclusive. You can only set one for a given column"
772
- )
773
-
774
- if (self.created_by_source_type and not self.created_by_source_key) or (
775
- not self.created_by_source_type and self.created_by_source_key
776
- ):
777
- raise ValueError(
778
- "You only set one of 'created_by_source_type' and 'created_by_source_key'. You have to either set both of them (which enables the 'created_by' feature of the column) or you must set neither of them."
779
- )
780
-
781
- @property
782
- def is_unique(self) -> bool:
783
- """Return True/False to denote if this column should always have unique values."""
784
- if self._is_unique is None:
785
- self._is_unique = any([validator.is_unique for validator in self.validators])
786
- return self._is_unique
787
-
788
- @property
789
- def is_required(self):
790
- """Return True/False to denote if this column should is required."""
791
- if self._is_required is None:
792
- self._is_required = any([validator.is_required for validator in self.validators])
793
- return self._is_required
794
-
795
- def additional_write_columns(self, is_create=False) -> dict[str, Self]:
796
- """
797
- Return any additional columns that should be included in write operations.
798
-
799
- Some column types, and some validation requirements, necessitate the presence of additional
800
- columns in the save operation. This function adds those in so they can be included in the
801
- API call.
802
- """
803
- additional_write_columns: dict[str, Self] = {}
804
- for validator in self.validators:
805
- if not isinstance(validator, Validator):
806
- continue
807
- additional_write_columns = {
808
- **additional_write_columns,
809
- **validator.additional_write_columns(is_create=is_create), # type: ignore
810
- }
811
- return additional_write_columns
812
-
813
- def to_json(self, model: clearskies.model.Model) -> dict[str, Any]:
814
- """Grabs the column out of the model and converts it into a representation that can be turned into JSON."""
815
- return {self.name: self.__get__(model, model.__class__)}
816
-
817
- def input_errors(self, model: clearskies.model.Model, data: dict[str, Any]) -> dict[str, Any]:
818
- """
819
- Check the given dictionary of data for any possible input errors.
820
-
821
- This accepts all the data being saved, and not just the value for this column. The reason is because
822
- some input valdiation flows require more than one piece of data. For instance, a user may be asked
823
- to type a specific piece of input more than once to minimize the chance of typos, or a user may
824
- have to provide their password when changing security-related columns.
825
-
826
- This also returns a dictionary, rather than an error message, so that a column can also return an error
827
- message for more than one column at a time if needed.
828
-
829
- If there are no input errors then this should simply return an empty dictionary.
830
-
831
- This method calls `self.input_error_for_value` and then also calls all the validators attached to the
832
- column so, if you're building your own column and have some specific input validation you need to do,
833
- you probably want to extend `input_error_for_value` as that is the one intended checks for a column type.
834
-
835
- Note: this is not called when you directly invoke the `save`/`create` method of a model. This is only
836
- used by handlers when processing user input (e.g. API calls).
837
- """
838
- if self.name in data and data[self.name]:
839
- error = self.input_error_for_value(data[self.name])
840
- if error:
841
- return {self.name: error}
842
-
843
- for validator in self.validators:
844
- if hasattr(validator, "injectable_properties"):
845
- validator.injectable_properties(self.di)
846
-
847
- error = validator(model, self.name, data)
848
- if error:
849
- return {self.name: error}
850
-
851
- return {}
852
-
853
- def check_search_value(
854
- self, value: str, operator: str | None = None, relationship_reference: str | None = None
855
- ) -> str:
856
- """
857
- Check if the given value is an allowed value.
858
-
859
- This is called by the search operation in the various API-related handlers to validate a search value.
860
-
861
- Generally, this just defers to self.input_error_for_value, but it is a separate method in case you
862
- need to change your input validation logic specifically when checking a search value.
863
- """
864
- return self.input_error_for_value(value, operator=operator)
865
-
866
- def input_error_for_value(self, value: str, operator: str | None = None) -> str:
867
- """
868
- Check if the given value is an allowed value.
869
-
870
- This method is intended for checks that are specific to the column type (e.g. this is where an
871
- email column checks that the value is an actual email, or a datetime column checks for a valid
872
- datetime). The `input_errors` method does a bit more, so in general it's easier to extend this one.
873
-
874
- This method is passed in the value to check. It should return a string. If the data is valid,
875
- then return an empty string. Otherwise return a human-readable error message.
876
-
877
- At times an operator will be passed in. This is used when the user is searching instead of saving.
878
- In this case, the check can vary depending on the operator. For instance, if it's a wildcard search
879
- then an email field only has to verify the type is a string (since the user may have only entered
880
- the beginning of an email address), but if it's an exact search then you would expect the value to be
881
- an actual email.
882
-
883
- Note: this is not called when you directly invoke the `save`/`create` method of a model. This is only
884
- used by handlers when processing user input (e.g. API calls).
885
- """
886
- return ""
887
-
888
- def pre_save(self, data: dict[str, Any], model: clearskies.model.Model) -> dict[str, Any]:
889
- """
890
- Make any necessary changes to the data before starting the save process.
891
-
892
- The difference between this and to_backend is that to_backend only affects
893
- the data as it is going into the database, while this affects the data that will get persisted
894
- in the object as well. So for instance, for a "created" field, pre_save may fill in the current
895
- date with a Python DateTime object when the record is being saved, and then to_backend may
896
- turn that into an SQL-compatible date string.
897
-
898
- Note: this is called during the `pre_save` step in the lifecycle of the save process. See the
899
- model class for more details.
900
- """
901
- if not model and self.created_by_source_type:
902
- data[self.name] = self._extract_value_from_source_type()
903
- if self.setable:
904
- if callable(self.setable):
905
- input_output = self.di.build("input_output", cache=True)
906
- data[self.name] = self.di.call_function(
907
- self.setable, data=data, model=model, **input_output.get_context_for_callables()
908
- )
909
- else:
910
- data[self.name] = self.setable
911
- if not model and self.default and self.name not in data:
912
- data[self.name] = self.default
913
- if self.on_change_pre_save and model.is_changing(self.name, data):
914
- data = self.execute_actions_with_data(self.on_change_pre_save, model, data)
915
- return data
916
-
917
- def post_save(self, data: dict[str, Any], model: clearskies.model.Model, id: int | str) -> None:
918
- """
919
- Make any changes needed after persisting data to the backend.
920
-
921
- This lives in the `post_save` hook of the save lifecycle. See the model class for more details.
922
- `data` is the data dictionary being saved. `model` is obviously the model object that initiated the
923
- save.
924
-
925
- This happens after the backend is updated but before the model is updated. Therefore, You can tell the
926
- difference between a create operation and an update operation by checking if the model exists: `if model`.
927
- For a create operation, the model will be empty (it evaluates to False). The opposite is true for an update
928
- operation.
929
-
930
- Any return value will be ignored. If you need to make additional changes in the backend, you
931
- have to execute a new save operation.
932
- """
933
- if self.on_change_post_save and model.is_changing(self.name, data):
934
- self.execute_actions_with_data(
935
- self.on_change_post_save,
936
- model,
937
- data,
938
- id=id,
939
- context="on_change_post_save",
940
- require_dict_return_value=False,
941
- )
942
-
943
- def save_finished(self, model: clearskies.model.Model) -> None:
944
- """
945
- Make any necessary changes needed after a save has completely finished.
946
-
947
- This is typically used for actions set by the developer. Column-specific behavior usually lives in
948
- `pre_save` or `post_save`. See the model class for more details about the various lifecycle hooks during
949
- a save.
950
- """
951
- if self.on_change_save_finished and model.was_changed(self.name):
952
- self.execute_actions(self.on_change_save_finished, model)
953
-
954
- def pre_delete(self, model):
955
- """Make any changes needed to the data before starting the delete process."""
956
- pass
957
-
958
- def post_delete(self, model):
959
- """Make any changes needed to the data before finishing the delete process."""
960
- pass
961
-
962
- def _extract_value_from_source_type(self) -> Any:
963
- """For columns with `created_by_source_type` set, this fetches the appropriate value from the request."""
964
- input_output = self.di.build("input_output", cache=True)
965
- source_type = self.created_by_source_type
966
- if source_type == "authorization_data":
967
- data = input_output.authorization_data
968
- elif source_type == "http_header":
969
- data = input_output.request_headers
970
- elif source_type == "routing_data":
971
- data = input_output.routing_data
972
-
973
- if self.created_by_source_key not in data and self.created_by_source_strict:
974
- raise ValueError(
975
- f"Column '{self.name}' is configured to load the key named '{self.created_by_source_key}' from "
976
- + f"the {self.created_by_source_type}', but this key was not present in the request."
977
- )
978
-
979
- return data.get(self.created_by_source_key, "N/A")
980
-
981
- def execute_actions_with_data(
982
- self,
983
- actions: list[clearskies.typing.action],
984
- model: clearskies.model.Model,
985
- data: dict[str, Any],
986
- id: int | str | None = None,
987
- context: str = "on_change_pre_save",
988
- require_dict_return_value: bool = True,
989
- ) -> dict[str, Any]:
990
- """Execute a given set of actions and expects data to be both provided and returned."""
991
- input_output = self.di.build("input_output", cache=True)
992
- for index, action in enumerate(actions):
993
- new_data = self.di.call_function(
994
- action,
995
- **{
996
- **input_output.get_context_for_callables(),
997
- **{
998
- "model": model,
999
- "data": data,
1000
- "id": id,
1001
- },
1002
- },
1003
- )
1004
- if not isinstance(new_data, dict):
1005
- if require_dict_return_value:
1006
- raise ValueError(
1007
- f"Return error for action #{index + 1} in 'on_change_pre_save' for column '{self.name}' in model '{self.model_class.__name__}': this action must return a dictionary but returned an object of type '{new_data.__class__.__name__}' instead"
1008
- )
1009
- else:
1010
- return new_data
1011
- data = {
1012
- **data,
1013
- **new_data,
1014
- }
1015
- return data
1016
-
1017
- def execute_actions(
1018
- self,
1019
- actions: list[clearskies.typing.action],
1020
- model: clearskies.model.Model,
1021
- ) -> None:
1022
- """Execute a given set of actions."""
1023
- input_output = self.di.build("input_output", cache=True)
1024
- for action in actions:
1025
- self.di.call_function(action, model=model, **input_output.get_context_for_callables())
1026
-
1027
- def values_match(self, value_1, value_2):
1028
- """
1029
- Compare two values to see if they are the same.
1030
-
1031
- This is mainly used to compare incoming data with old data to determine if a column has changed.
1032
-
1033
- Note that these checks shouldn't make any assumptions about whether or not data has gone through the
1034
- to_backend/from_backend functions. For instance, a datetime field may find one value has a date
1035
- that is formatted as a string, and the other as a DateTime object. Plan appropriately.
1036
- """
1037
- return value_1 == value_2
1038
-
1039
- def add_search(
1040
- self, model: clearskies.model.Model, value: str, operator: str = "", relationship_reference: str = ""
1041
- ) -> clearskies.model.Model:
1042
- return model.where(self.condition(operator, value))
1043
-
1044
- def build_condition(self, value: str, operator: str = "", column_prefix: str = ""):
1045
- """
1046
- Build condition for the read (and related) handlers to turn user input into a condition.
1047
-
1048
- Note that this may look like it is vulnerable to SQLi, but it isn't. These conditions aren't passed directly
1049
- into a query. Rather, they are parsed by the condition parser before being sent into the backend.
1050
- The condition parser can safely reconstruct the original pieces, and the backend can then use the data
1051
- safely (and remember, the backend may not be an SQL anyway)
1052
-
1053
- As a result, this is perfectly safe for any user input, assuming normal system flow.
1054
-
1055
- That being said, this should probably be replaced by self.condition()...
1056
- """
1057
- if not operator:
1058
- operator = "="
1059
- if operator.lower() == "like":
1060
- return f"{column_prefix}{self.name} LIKE '%{value}%'"
1061
- return f"{column_prefix}{self.name}{operator}{value}"
1062
-
1063
- def is_allowed_operator(
1064
- self,
1065
- operator: str,
1066
- relationship_reference: str = "",
1067
- ):
1068
- """Process user data to decide if the end-user is specifying an allowed operator."""
1069
- return operator.lower() in self._allowed_search_operators
1070
-
1071
- def n_plus_one_add_joins(
1072
- self, model: clearskies.model.Model, column_names: list[str] = []
1073
- ) -> clearskies.model.Model:
1074
- """Add any additional joins to solve the N+1 problem."""
1075
- return model
1076
-
1077
- def n_plus_one_join_table_alias_prefix(self):
1078
- """
1079
- Create a table alias to use with joins for n+1 solutions.
1080
-
1081
- When joining tables in for n+1 solutions, you can't just do a SELECT * on the new table, because that
1082
- often results in duplicate column names. A solution that generally works across the board is to select
1083
- specific columns from the joined table and alias them, adding a common prefix. Then, the data from the
1084
- joined table can be reconstructed automatically by finding all columns with that prefix (and then removing
1085
- the prefix). This function returns that prefix for that alias.
1086
-
1087
- Now, technically this isn't function isn't used at all by the base class, so this definition is fairly
1088
- pointless. It isn't marked as an abstract method because most model columns don't need it either.
1089
- Rather, this function is here mostly for documentation so it's easier to understand how to implement
1090
- support for n+1 solutions when needed. See the belongs_to column for a full implementation reference.
1091
- """
1092
- return "join_table_" + self.name
1093
-
1094
- def add_join(self, model: Model) -> Model:
1095
- return model
1096
-
1097
- def where_for_request(
1098
- self,
1099
- model: clearskies.model.Model,
1100
- routing_data: dict[str, str],
1101
- authorization_data: dict[str, Any],
1102
- input_output,
1103
- ) -> clearskies.model.Model:
1104
- """
1105
- Create a hook to automatically apply filtering whenever the column makes an appearance in a get/update/list/search handler.
1106
-
1107
- This hook is called by all the handlers that execute queries, so if your column needs to automatically
1108
- do some filtering whenever the model shows up in an API endpoint, this is the place for it.
1109
- """
1110
- return model
1111
-
1112
- def name_for_building_condition(self) -> str:
1113
- if self._config and "name" in self._config:
1114
- return self.name
1115
-
1116
- if not self._config or not self._config.get("model_class"):
1117
- raise ValueError(
1118
- f"A condition builder was called but the model class isn't set. This means that the __get__ method for column class {self.__class__.__name__} forgot to set `self.model_class = cls`"
1119
- )
1120
-
1121
- for attribute_name in dir(self.model_class):
1122
- if id(getattr(self.model_class, attribute_name)) != id(self):
1123
- continue
1124
- self.name = attribute_name
1125
- break
1126
-
1127
- return self.name
1128
-
1129
- def equals(self, value) -> Condition:
1130
- name = self.name_for_building_condition()
1131
- if "=" not in self._allowed_search_operators:
1132
- raise ValueError(f"An 'equals search' is not allowed for '{self.model_class.__name__}.{name}'.")
1133
- value = self.to_backend({name: value}).get(name)
1134
- return ParsedCondition(name, "=", [value])
1135
-
1136
- def spaceship(self, value) -> Condition:
1137
- name = self.name_for_building_condition()
1138
- if "<=>" not in self._allowed_search_operators:
1139
- raise ValueError(f"A 'spaceship' search is not allowed for '{self.model_class.__name__}.{name}'.")
1140
- value = self.to_backend({name: value}).get(name)
1141
- return ParsedCondition(name, "<=>", [value])
1142
-
1143
- def not_equals(self, value) -> Condition:
1144
- name = self.name_for_building_condition()
1145
- if "!=" not in self._allowed_search_operators:
1146
- raise ValueError(f"A 'not equals' search is not allowed for '{self.model_class.__name__}.{name}'.")
1147
- value = self.to_backend({name: value}).get(name)
1148
- return ParsedCondition(name, "!=", [value])
1149
-
1150
- def less_than_equals(self, value) -> Condition:
1151
- name = self.name_for_building_condition()
1152
- if "<=" not in self._allowed_search_operators:
1153
- raise ValueError(f"A 'less than or equals' search is not allowed for '{self.model_class.__name__}.{name}'.")
1154
- value = self.to_backend({name: value}).get(name)
1155
- return ParsedCondition(name, "<=", [value])
1156
-
1157
- def greater_than_equals(self, value) -> Condition:
1158
- name = self.name_for_building_condition()
1159
- if ">=" not in self._allowed_search_operators:
1160
- raise ValueError(
1161
- f"A 'greater than' or equals search is not allowed for '{self.model_class.__name__}.{name}'."
1162
- )
1163
- value = self.to_backend({name: value}).get(name)
1164
- return ParsedCondition(name, ">=", [value])
1165
-
1166
- def less_than(self, value) -> Condition:
1167
- name = self.name_for_building_condition()
1168
- if "<" not in self._allowed_search_operators:
1169
- raise ValueError(f"A 'less than' search is not allowed for '{self.model_class.__name__}.{name}'.")
1170
- value = self.to_backend({name: value}).get(name)
1171
- return ParsedCondition(name, "<", [value])
1172
-
1173
- def greater_than(self, value) -> Condition:
1174
- name = self.name_for_building_condition()
1175
- if ">" not in self._allowed_search_operators:
1176
- raise ValueError(f"A 'greater than' search is not allowed for '{self.model_class.__name__}.{name}'.")
1177
- value = self.to_backend({name: value}).get(name)
1178
- return ParsedCondition(name, ">", [value])
1179
-
1180
- def is_in(self, values) -> Condition:
1181
- name = self.name_for_building_condition()
1182
- if "in" not in self._allowed_search_operators:
1183
- raise ValueError(f"An 'in' search is not allowed for '{self.model_class.__name__}.{name}'.")
1184
- if not isinstance(values, list):
1185
- raise TypeError("You must pass a list in to column.is_in")
1186
- final_values = []
1187
- for value in values:
1188
- final_values.append(self.to_backend({name: value}).get(name))
1189
- return ParsedCondition(name, "in", final_values) # type: ignore
1190
-
1191
- def is_not_null(self) -> Condition:
1192
- name = self.name_for_building_condition()
1193
- if "is not null" not in self._allowed_search_operators:
1194
- raise ValueError(f"An 'is not null' search is not allowed for '{self.model_class.__name__}.{name}'.")
1195
- return ParsedCondition(name, "is not null", [])
1196
-
1197
- def is_null(self) -> Condition:
1198
- name = self.name_for_building_condition()
1199
- if "is null" not in self._allowed_search_operators:
1200
- raise ValueError(f"An 'is null' search is not allowed for '{self.model_class.__name__}.{name}'.")
1201
- return ParsedCondition(name, "is null", [])
1202
-
1203
- def like(self, value) -> Condition:
1204
- name = self.name_for_building_condition()
1205
- if "like" not in self._allowed_search_operators:
1206
- raise ValueError(f"A 'like' search is not allowed for '{self.model_class.__name__}.{name}'.")
1207
- value = self.to_backend({name: value}).get(name)
1208
- return ParsedCondition(name, "like", [value])
1209
-
1210
- def condition(self, operator: str, value) -> Condition:
1211
- name = self.name_for_building_condition()
1212
- operator = operator.lower()
1213
- if operator not in self._allowed_search_operators:
1214
- raise ValueError(f"The operator '{operator}' is not allowed for '{self.model_class.__name__}.{name}'.")
1215
-
1216
- # the search methods are more or less identical except:
1217
- if operator == "in":
1218
- return self.is_in(value)
1219
-
1220
- value = self.to_backend({name: value}).get(name)
1221
- return ParsedCondition(name, operator, [value])
1222
-
1223
- def is_allowed_search_operator(self, operator: str, relationship_reference: str = "") -> bool:
1224
- return operator in self._allowed_search_operators
1225
-
1226
- def allowed_search_operators(self, relationship_reference: str = ""):
1227
- return self._allowed_search_operators
1228
-
1229
- def join_table_alias(self) -> str:
1230
- raise NotImplementedError("Ooops, I don't support joins")
1231
-
1232
- def documentation(self, name=None, example=None, value=None) -> list[AutoDocSchema]:
1233
- return [self.auto_doc_class(name if name is not None else self.name, example=example, value=value)]