clear-skies 1.22.10__py3-none-any.whl → 2.0.23__py3-none-any.whl

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