clear-skies 1.22.31__py3-none-any.whl → 2.0.1__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 (345) hide show
  1. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/METADATA +12 -14
  2. clear_skies-2.0.1.dist-info/RECORD +249 -0
  3. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/WHEEL +1 -1
  4. clearskies/__init__.py +42 -25
  5. clearskies/action.py +7 -0
  6. clearskies/authentication/__init__.py +8 -41
  7. clearskies/authentication/authentication.py +46 -0
  8. clearskies/authentication/authorization.py +8 -9
  9. clearskies/authentication/authorization_pass_through.py +11 -9
  10. clearskies/authentication/jwks.py +133 -58
  11. clearskies/authentication/public.py +3 -38
  12. clearskies/authentication/secret_bearer.py +516 -54
  13. clearskies/autodoc/formats/oai3_json/__init__.py +1 -1
  14. clearskies/autodoc/formats/oai3_json/oai3_json.py +9 -7
  15. clearskies/autodoc/formats/oai3_json/parameter.py +6 -3
  16. clearskies/autodoc/formats/oai3_json/request.py +7 -5
  17. clearskies/autodoc/formats/oai3_json/response.py +7 -4
  18. clearskies/autodoc/formats/oai3_json/schema/object.py +4 -1
  19. clearskies/autodoc/request/__init__.py +2 -0
  20. clearskies/autodoc/request/header.py +4 -6
  21. clearskies/autodoc/request/json_body.py +4 -6
  22. clearskies/autodoc/request/parameter.py +8 -0
  23. clearskies/autodoc/request/request.py +7 -4
  24. clearskies/autodoc/request/url_parameter.py +4 -6
  25. clearskies/autodoc/request/url_path.py +4 -6
  26. clearskies/autodoc/schema/__init__.py +4 -2
  27. clearskies/autodoc/schema/array.py +5 -6
  28. clearskies/autodoc/schema/boolean.py +4 -10
  29. clearskies/autodoc/schema/date.py +0 -3
  30. clearskies/autodoc/schema/datetime.py +1 -4
  31. clearskies/autodoc/schema/double.py +0 -3
  32. clearskies/autodoc/schema/enum.py +4 -2
  33. clearskies/autodoc/schema/integer.py +4 -9
  34. clearskies/autodoc/schema/long.py +0 -3
  35. clearskies/autodoc/schema/number.py +4 -9
  36. clearskies/autodoc/schema/object.py +5 -7
  37. clearskies/autodoc/schema/password.py +0 -3
  38. clearskies/autodoc/schema/schema.py +11 -0
  39. clearskies/autodoc/schema/string.py +4 -10
  40. clearskies/backends/__init__.py +55 -20
  41. clearskies/backends/api_backend.py +1100 -284
  42. clearskies/backends/backend.py +53 -84
  43. clearskies/backends/cursor_backend.py +236 -186
  44. clearskies/backends/memory_backend.py +519 -226
  45. clearskies/backends/secrets_backend.py +75 -31
  46. clearskies/column.py +1229 -0
  47. clearskies/columns/__init__.py +71 -0
  48. clearskies/columns/audit.py +205 -0
  49. clearskies/columns/belongs_to_id.py +483 -0
  50. clearskies/columns/belongs_to_model.py +128 -0
  51. clearskies/columns/belongs_to_self.py +105 -0
  52. clearskies/columns/boolean.py +109 -0
  53. clearskies/columns/category_tree.py +275 -0
  54. clearskies/columns/category_tree_ancestors.py +51 -0
  55. clearskies/columns/category_tree_children.py +127 -0
  56. clearskies/columns/category_tree_descendants.py +48 -0
  57. clearskies/columns/created.py +94 -0
  58. clearskies/columns/created_by_authorization_data.py +116 -0
  59. clearskies/columns/created_by_header.py +99 -0
  60. clearskies/columns/created_by_ip.py +92 -0
  61. clearskies/columns/created_by_routing_data.py +96 -0
  62. clearskies/columns/created_by_user_agent.py +92 -0
  63. clearskies/columns/date.py +230 -0
  64. clearskies/columns/datetime.py +278 -0
  65. clearskies/columns/email.py +76 -0
  66. clearskies/columns/float.py +149 -0
  67. clearskies/columns/has_many.py +505 -0
  68. clearskies/columns/has_many_self.py +56 -0
  69. clearskies/columns/has_one.py +14 -0
  70. clearskies/columns/integer.py +156 -0
  71. clearskies/columns/json.py +122 -0
  72. clearskies/columns/many_to_many_ids.py +333 -0
  73. clearskies/columns/many_to_many_ids_with_data.py +270 -0
  74. clearskies/columns/many_to_many_models.py +154 -0
  75. clearskies/columns/many_to_many_pivots.py +133 -0
  76. clearskies/columns/phone.py +158 -0
  77. clearskies/columns/select.py +91 -0
  78. clearskies/columns/string.py +98 -0
  79. clearskies/columns/timestamp.py +160 -0
  80. clearskies/columns/updated.py +110 -0
  81. clearskies/columns/uuid.py +86 -0
  82. clearskies/configs/README.md +105 -0
  83. clearskies/configs/__init__.py +162 -0
  84. clearskies/configs/actions.py +43 -0
  85. clearskies/configs/any.py +13 -0
  86. clearskies/configs/any_dict.py +22 -0
  87. clearskies/configs/any_dict_or_callable.py +23 -0
  88. clearskies/configs/authentication.py +23 -0
  89. clearskies/configs/authorization.py +23 -0
  90. clearskies/configs/boolean.py +16 -0
  91. clearskies/configs/boolean_or_callable.py +18 -0
  92. clearskies/configs/callable_config.py +18 -0
  93. clearskies/configs/columns.py +34 -0
  94. clearskies/configs/conditions.py +30 -0
  95. clearskies/configs/config.py +24 -0
  96. clearskies/configs/datetime.py +18 -0
  97. clearskies/configs/datetime_or_callable.py +19 -0
  98. clearskies/configs/endpoint.py +23 -0
  99. clearskies/configs/endpoint_list.py +28 -0
  100. clearskies/configs/float.py +16 -0
  101. clearskies/configs/float_or_callable.py +18 -0
  102. clearskies/configs/integer.py +16 -0
  103. clearskies/configs/integer_or_callable.py +18 -0
  104. clearskies/configs/joins.py +30 -0
  105. clearskies/configs/list_any_dict.py +30 -0
  106. clearskies/configs/list_any_dict_or_callable.py +31 -0
  107. clearskies/configs/model_class.py +35 -0
  108. clearskies/configs/model_column.py +65 -0
  109. clearskies/configs/model_columns.py +56 -0
  110. clearskies/configs/model_destination_name.py +25 -0
  111. clearskies/configs/model_to_id_column.py +43 -0
  112. clearskies/configs/readable_model_column.py +9 -0
  113. clearskies/configs/readable_model_columns.py +9 -0
  114. clearskies/configs/schema.py +23 -0
  115. clearskies/configs/searchable_model_columns.py +9 -0
  116. clearskies/configs/security_headers.py +39 -0
  117. clearskies/configs/select.py +26 -0
  118. clearskies/configs/select_list.py +47 -0
  119. clearskies/configs/string.py +29 -0
  120. clearskies/configs/string_dict.py +32 -0
  121. clearskies/configs/string_list.py +32 -0
  122. clearskies/configs/string_list_or_callable.py +35 -0
  123. clearskies/configs/string_or_callable.py +18 -0
  124. clearskies/configs/timedelta.py +18 -0
  125. clearskies/configs/timezone.py +18 -0
  126. clearskies/configs/url.py +23 -0
  127. clearskies/configs/validators.py +45 -0
  128. clearskies/configs/writeable_model_column.py +9 -0
  129. clearskies/configs/writeable_model_columns.py +9 -0
  130. clearskies/configurable.py +76 -0
  131. clearskies/contexts/__init__.py +8 -8
  132. clearskies/contexts/cli.py +8 -41
  133. clearskies/contexts/context.py +91 -56
  134. clearskies/contexts/wsgi.py +16 -29
  135. clearskies/contexts/wsgi_ref.py +53 -0
  136. clearskies/di/__init__.py +10 -7
  137. clearskies/di/additional_config.py +115 -4
  138. clearskies/di/additional_config_auto_import.py +12 -0
  139. clearskies/di/di.py +742 -121
  140. clearskies/di/inject/__init__.py +23 -0
  141. clearskies/di/inject/by_class.py +21 -0
  142. clearskies/di/inject/by_name.py +18 -0
  143. clearskies/di/inject/di.py +13 -0
  144. clearskies/di/inject/environment.py +14 -0
  145. clearskies/di/inject/input_output.py +20 -0
  146. clearskies/di/inject/now.py +13 -0
  147. clearskies/di/inject/requests.py +13 -0
  148. clearskies/di/inject/secrets.py +14 -0
  149. clearskies/di/inject/utcnow.py +13 -0
  150. clearskies/di/inject/uuid.py +15 -0
  151. clearskies/di/injectable.py +29 -0
  152. clearskies/di/injectable_properties.py +131 -0
  153. clearskies/end.py +183 -0
  154. clearskies/endpoint.py +1310 -0
  155. clearskies/endpoint_group.py +310 -0
  156. clearskies/endpoints/__init__.py +23 -0
  157. clearskies/endpoints/advanced_search.py +526 -0
  158. clearskies/endpoints/callable.py +388 -0
  159. clearskies/endpoints/create.py +202 -0
  160. clearskies/endpoints/delete.py +139 -0
  161. clearskies/endpoints/get.py +275 -0
  162. clearskies/endpoints/health_check.py +181 -0
  163. clearskies/endpoints/list.py +573 -0
  164. clearskies/endpoints/restful_api.py +427 -0
  165. clearskies/endpoints/simple_search.py +286 -0
  166. clearskies/endpoints/update.py +190 -0
  167. clearskies/environment.py +5 -3
  168. clearskies/exceptions/__init__.py +17 -0
  169. clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
  170. clearskies/exceptions/moved_permanently.py +3 -0
  171. clearskies/exceptions/moved_temporarily.py +3 -0
  172. clearskies/exceptions/not_found.py +2 -0
  173. clearskies/functional/__init__.py +2 -2
  174. clearskies/functional/routing.py +92 -0
  175. clearskies/functional/string.py +19 -11
  176. clearskies/functional/validations.py +61 -9
  177. clearskies/input_outputs/__init__.py +9 -7
  178. clearskies/input_outputs/cli.py +130 -142
  179. clearskies/input_outputs/exceptions/__init__.py +1 -1
  180. clearskies/input_outputs/headers.py +45 -0
  181. clearskies/input_outputs/input_output.py +91 -122
  182. clearskies/input_outputs/programmatic.py +69 -0
  183. clearskies/input_outputs/wsgi.py +23 -38
  184. clearskies/model.py +984 -183
  185. clearskies/parameters_to_properties.py +31 -0
  186. clearskies/query/__init__.py +12 -0
  187. clearskies/query/condition.py +223 -0
  188. clearskies/query/join.py +136 -0
  189. clearskies/query/query.py +196 -0
  190. clearskies/query/sort.py +27 -0
  191. clearskies/schema.py +82 -0
  192. clearskies/secrets/__init__.py +3 -31
  193. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
  194. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
  195. clearskies/secrets/akeyless.py +88 -147
  196. clearskies/secrets/secrets.py +8 -8
  197. clearskies/security_header.py +15 -0
  198. clearskies/security_headers/__init__.py +8 -8
  199. clearskies/security_headers/cache_control.py +47 -110
  200. clearskies/security_headers/cors.py +40 -95
  201. clearskies/security_headers/csp.py +76 -151
  202. clearskies/security_headers/hsts.py +14 -16
  203. clearskies/test_base.py +8 -0
  204. clearskies/typing.py +11 -0
  205. clearskies/validator.py +37 -0
  206. clearskies/validators/__init__.py +33 -0
  207. clearskies/validators/after_column.py +62 -0
  208. clearskies/validators/before_column.py +13 -0
  209. clearskies/validators/in_the_future.py +32 -0
  210. clearskies/validators/in_the_future_at_least.py +11 -0
  211. clearskies/validators/in_the_future_at_most.py +10 -0
  212. clearskies/validators/in_the_past.py +32 -0
  213. clearskies/validators/in_the_past_at_least.py +10 -0
  214. clearskies/validators/in_the_past_at_most.py +10 -0
  215. clearskies/validators/maximum_length.py +26 -0
  216. clearskies/validators/maximum_value.py +29 -0
  217. clearskies/validators/minimum_length.py +26 -0
  218. clearskies/validators/minimum_value.py +29 -0
  219. clearskies/validators/required.py +35 -0
  220. clearskies/validators/timedelta.py +59 -0
  221. clearskies/validators/unique.py +31 -0
  222. clear_skies-1.22.31.dist-info/RECORD +0 -214
  223. clearskies/application.py +0 -29
  224. clearskies/authentication/auth0_jwks.py +0 -118
  225. clearskies/authentication/auth_exception.py +0 -2
  226. clearskies/authentication/jwks_jwcrypto.py +0 -51
  227. clearskies/backends/api_get_only_backend.py +0 -48
  228. clearskies/backends/example_backend.py +0 -43
  229. clearskies/backends/file_backend.py +0 -48
  230. clearskies/backends/json_backend.py +0 -7
  231. clearskies/backends/restful_api_advanced_search_backend.py +0 -103
  232. clearskies/binding_config.py +0 -16
  233. clearskies/column_types/__init__.py +0 -203
  234. clearskies/column_types/audit.py +0 -249
  235. clearskies/column_types/belongs_to.py +0 -271
  236. clearskies/column_types/boolean.py +0 -60
  237. clearskies/column_types/category_tree.py +0 -304
  238. clearskies/column_types/column.py +0 -373
  239. clearskies/column_types/created.py +0 -26
  240. clearskies/column_types/created_by_authorization_data.py +0 -26
  241. clearskies/column_types/created_by_header.py +0 -24
  242. clearskies/column_types/created_by_ip.py +0 -17
  243. clearskies/column_types/created_by_routing_data.py +0 -25
  244. clearskies/column_types/created_by_user_agent.py +0 -17
  245. clearskies/column_types/created_micro.py +0 -26
  246. clearskies/column_types/datetime.py +0 -109
  247. clearskies/column_types/datetime_micro.py +0 -12
  248. clearskies/column_types/email.py +0 -18
  249. clearskies/column_types/float.py +0 -43
  250. clearskies/column_types/has_many.py +0 -179
  251. clearskies/column_types/has_one.py +0 -60
  252. clearskies/column_types/integer.py +0 -41
  253. clearskies/column_types/json.py +0 -25
  254. clearskies/column_types/many_to_many.py +0 -278
  255. clearskies/column_types/many_to_many_with_data.py +0 -162
  256. clearskies/column_types/phone.py +0 -48
  257. clearskies/column_types/select.py +0 -11
  258. clearskies/column_types/string.py +0 -24
  259. clearskies/column_types/timestamp.py +0 -73
  260. clearskies/column_types/updated.py +0 -26
  261. clearskies/column_types/updated_micro.py +0 -26
  262. clearskies/column_types/uuid.py +0 -25
  263. clearskies/columns.py +0 -123
  264. clearskies/condition_parser.py +0 -172
  265. clearskies/contexts/build_context.py +0 -54
  266. clearskies/contexts/convert_to_application.py +0 -190
  267. clearskies/contexts/extract_handler.py +0 -37
  268. clearskies/contexts/test.py +0 -94
  269. clearskies/decorators/__init__.py +0 -41
  270. clearskies/decorators/allow_non_json_bodies.py +0 -9
  271. clearskies/decorators/auth0_jwks.py +0 -22
  272. clearskies/decorators/authorization.py +0 -10
  273. clearskies/decorators/binding_classes.py +0 -9
  274. clearskies/decorators/binding_modules.py +0 -9
  275. clearskies/decorators/bindings.py +0 -9
  276. clearskies/decorators/create.py +0 -10
  277. clearskies/decorators/delete.py +0 -10
  278. clearskies/decorators/docs.py +0 -14
  279. clearskies/decorators/get.py +0 -10
  280. clearskies/decorators/jwks.py +0 -26
  281. clearskies/decorators/merge.py +0 -124
  282. clearskies/decorators/patch.py +0 -10
  283. clearskies/decorators/post.py +0 -10
  284. clearskies/decorators/public.py +0 -11
  285. clearskies/decorators/response_headers.py +0 -10
  286. clearskies/decorators/return_raw_response.py +0 -9
  287. clearskies/decorators/schema.py +0 -10
  288. clearskies/decorators/secret_bearer.py +0 -24
  289. clearskies/decorators/security_headers.py +0 -10
  290. clearskies/di/standard_dependencies.py +0 -151
  291. clearskies/handlers/__init__.py +0 -41
  292. clearskies/handlers/advanced_search.py +0 -271
  293. clearskies/handlers/base.py +0 -479
  294. clearskies/handlers/callable.py +0 -192
  295. clearskies/handlers/create.py +0 -35
  296. clearskies/handlers/crud_by_method.py +0 -18
  297. clearskies/handlers/database_connector.py +0 -32
  298. clearskies/handlers/delete.py +0 -61
  299. clearskies/handlers/exceptions/__init__.py +0 -5
  300. clearskies/handlers/exceptions/not_found.py +0 -3
  301. clearskies/handlers/get.py +0 -156
  302. clearskies/handlers/health_check.py +0 -59
  303. clearskies/handlers/input_processing.py +0 -79
  304. clearskies/handlers/list.py +0 -530
  305. clearskies/handlers/mygrations.py +0 -82
  306. clearskies/handlers/request_method_routing.py +0 -47
  307. clearskies/handlers/restful_api.py +0 -218
  308. clearskies/handlers/routing.py +0 -62
  309. clearskies/handlers/schema_helper.py +0 -128
  310. clearskies/handlers/simple_routing.py +0 -206
  311. clearskies/handlers/simple_routing_route.py +0 -197
  312. clearskies/handlers/simple_search.py +0 -136
  313. clearskies/handlers/update.py +0 -102
  314. clearskies/handlers/write.py +0 -193
  315. clearskies/input_requirements/__init__.py +0 -78
  316. clearskies/input_requirements/after.py +0 -36
  317. clearskies/input_requirements/before.py +0 -36
  318. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  319. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  320. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  321. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  322. clearskies/input_requirements/maximum_length.py +0 -19
  323. clearskies/input_requirements/maximum_value.py +0 -19
  324. clearskies/input_requirements/minimum_length.py +0 -22
  325. clearskies/input_requirements/minimum_value.py +0 -19
  326. clearskies/input_requirements/required.py +0 -23
  327. clearskies/input_requirements/requirement.py +0 -25
  328. clearskies/input_requirements/time_delta.py +0 -38
  329. clearskies/input_requirements/unique.py +0 -18
  330. clearskies/mocks/__init__.py +0 -7
  331. clearskies/mocks/input_output.py +0 -124
  332. clearskies/mocks/models.py +0 -142
  333. clearskies/models.py +0 -350
  334. clearskies/security_headers/base.py +0 -12
  335. clearskies/tests/simple_api/models/__init__.py +0 -2
  336. clearskies/tests/simple_api/models/status.py +0 -23
  337. clearskies/tests/simple_api/models/user.py +0 -21
  338. clearskies/tests/simple_api/users_api.py +0 -64
  339. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/LICENSE +0 -0
  340. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  341. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  342. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  343. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  344. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  345. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ from typing import TYPE_CHECKING, Any, Callable, Self, overload
5
+
6
+ import dateparser # type: ignore
7
+
8
+ import clearskies.parameters_to_properties
9
+ import clearskies.typing
10
+ from clearskies import configs
11
+ from clearskies.autodoc.schema import Datetime as AutoDocDatetime
12
+ from clearskies.autodoc.schema import Schema as AutoDocSchema
13
+ from clearskies.columns.datetime import Datetime
14
+ from clearskies.query import Condition
15
+
16
+ if TYPE_CHECKING:
17
+ from clearskies import Model
18
+
19
+
20
+ class Date(Datetime):
21
+ """
22
+ Stores date data in a column.
23
+
24
+ This is specifically for a column that only stores date information - not time information. When processing user input,
25
+ this value is passed through `dateparser.parse()` to decide if it is a proper date string. This makes for relatively
26
+ flexible input validation. Example:
27
+
28
+ ```python
29
+ import clearskies
30
+
31
+
32
+ class MyModel(clearskies.Model):
33
+ backend = clearskies.backends.MemoryBackend()
34
+ id_column_name = "id"
35
+
36
+ id = clearskies.columns.Uuid()
37
+ name = clearskies.columns.String()
38
+ my_date = clearskies.columns.Date()
39
+
40
+
41
+ wsgi = clearskies.contexts.WsgiRef(
42
+ clearskies.endpoints.Create(
43
+ MyModel,
44
+ writeable_column_names=["name", "my_date"],
45
+ readable_column_names=["id", "name", "my_date"],
46
+ ),
47
+ classes=[MyModel],
48
+ )
49
+ wsgi()
50
+ ```
51
+
52
+ And when invoked:
53
+
54
+ ```bash
55
+ $ curl 'http://localhost:8080' -d '{"name":"Bob", "my_date":"May 5th 2025"}' | jq
56
+ {
57
+ "status": "success",
58
+ "error": "",
59
+ "data": {
60
+ "id": "a8c8ac79-bc28-4b24-9728-e85f13fc4104",
61
+ "name": "Bob",
62
+ "my_date": "2025-05-05"
63
+ },
64
+ "pagination": {},
65
+ "input_errors": {}
66
+ }
67
+
68
+ $ curl 'http://localhost:8080' -d '{"name":"Bob", "my_date":"2025-05-03"}' | jq
69
+ {
70
+ "status": "success",
71
+ "error": "",
72
+ "data": {
73
+ "id": "21376ae7-4090-4c2b-a50b-8d932ad5dac1",
74
+ "name": "Bob",
75
+ "my_date": "2025-05-03"
76
+ },
77
+ "pagination": {},
78
+ "input_errors": {}
79
+ }
80
+
81
+ $ curl 'http://localhost:8080' -d '{"name":"Bob", "my_date":"not a date"}' | jq
82
+ {
83
+ "status": "input_errors",
84
+ "error": "",
85
+ "data": [],
86
+ "pagination": {},
87
+ "input_errors": {
88
+ "my_date": "given value did not appear to be a valid date"
89
+ }
90
+ }
91
+ ```
92
+ """
93
+
94
+ date_format = configs.String(default="%Y-%m-%d")
95
+ backend_default = configs.String(default="0000-00-00")
96
+
97
+ default = configs.Datetime() # type: ignore
98
+ setable = configs.DatetimeOrCallable(default=None) # type: ignore
99
+
100
+ _allowed_search_operators = ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null"]
101
+
102
+ auto_doc_class: type[AutoDocSchema] = AutoDocDatetime
103
+ _descriptor_config_map = None
104
+
105
+ @clearskies.parameters_to_properties.parameters_to_properties
106
+ def __init__(
107
+ self,
108
+ date_format: str = "%Y-%m-%d",
109
+ backend_default: str = "0000-00-00",
110
+ default: datetime.datetime | None = None,
111
+ setable: datetime.datetime | Callable[..., datetime.datetime] | None = None,
112
+ is_readable: bool = True,
113
+ is_writeable: bool = True,
114
+ is_searchable: bool = True,
115
+ is_temporary: bool = False,
116
+ validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
117
+ on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
118
+ on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
119
+ on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
120
+ created_by_source_type: str = "",
121
+ created_by_source_key: str = "",
122
+ created_by_source_strict: bool = True,
123
+ ):
124
+ pass
125
+
126
+ def from_backend(self, value) -> datetime.date | None: # type: ignore
127
+ if not value or value == self.backend_default:
128
+ return None
129
+ if isinstance(value, str):
130
+ value = dateparser.parse(value)
131
+ if not isinstance(value, datetime.datetime):
132
+ raise TypeError(
133
+ f"I was expecting to get a datetime from the backend but I didn't get anything recognizable. I have a value of type '{value.__class__.__name__}'. I need either a datetime object or a datetime serialized as a string."
134
+ )
135
+
136
+ return datetime.date(value.year, value.month, value.day)
137
+
138
+ def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
139
+ if self.name not in data or isinstance(data[self.name], str) or data[self.name] is None:
140
+ return data
141
+
142
+ value = data[self.name]
143
+ if not isinstance(data[self.name], datetime.datetime) and not isinstance(data[self.name], datetime.date):
144
+ raise TypeError(
145
+ f"I was expecting a stringified-date or a datetime object to send to the backend, but instead I found a value of {value.__class__.__name__}"
146
+ )
147
+
148
+ return {
149
+ **data,
150
+ self.name: value.strftime(self.date_format),
151
+ }
152
+
153
+ @overload # type: ignore
154
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
155
+ pass
156
+
157
+ @overload
158
+ def __get__(self, instance: Model, cls: type[Model]) -> datetime.date:
159
+ pass
160
+
161
+ def __get__(self, instance, cls):
162
+ return super().__get__(instance, cls)
163
+
164
+ def __set__(self, instance, value: datetime.datetime | datetime.date) -> None:
165
+ instance._next_data[self.name] = value
166
+
167
+ def equals(self, value: str | datetime.datetime | datetime.date) -> Condition:
168
+ return super().equals(value) # type: ignore
169
+
170
+ def spaceship(self, value: str | datetime.datetime | datetime.date) -> Condition:
171
+ return super().spaceship(value) # type: ignore
172
+
173
+ def not_equals(self, value: str | datetime.datetime | datetime.date) -> Condition:
174
+ return super().not_equals(value) # type: ignore
175
+
176
+ def less_than_equals(self, value: str | datetime.datetime | datetime.date) -> Condition:
177
+ return super().less_than_equals(value) # type: ignore
178
+
179
+ def greater_than_equals(self, value: str | datetime.datetime | datetime.date) -> Condition:
180
+ return super().greater_than_equals(value) # type: ignore
181
+
182
+ def less_than(self, value: str | datetime.datetime | datetime.date) -> Condition:
183
+ return super().less_than(value) # type: ignore
184
+
185
+ def greater_than(self, value: str | datetime.datetime | datetime.date) -> Condition:
186
+ return super().greater_than(value) # type: ignore
187
+
188
+ def is_in(self, values: list[str | datetime.datetime | datetime.date]) -> Condition: # type: ignore
189
+ return super().is_in(values) # type: ignore
190
+
191
+ def input_error_for_value(self, value, operator=None):
192
+ value = dateparser.parse(value)
193
+ if not value:
194
+ return "given value did not appear to be a valid date"
195
+ return ""
196
+
197
+ def values_match(self, value_1, value_2):
198
+ """Compare two values to see if they are the same."""
199
+ # in this function we deal with data directly out of the backend, so our date is likely
200
+ # to be string-ified and we want to look for default (e.g. null) values in string form.
201
+ if type(value_1) == str and ("0000-00-00" in value_1 or value_1 == self.backend_default):
202
+ value_1 = None
203
+ if type(value_2) == str and ("0000-00-00" in value_2 or value_2 == self.backend_default):
204
+ value_2 = None
205
+ number_values = 0
206
+ if value_1:
207
+ number_values += 1
208
+ if value_2:
209
+ number_values += 1
210
+ if number_values == 0:
211
+ return True
212
+ if number_values == 1:
213
+ return False
214
+
215
+ if type(value_1) == str:
216
+ value_1 = dateparser.parse(value_1)
217
+ value_1 = datetime.date(value_1.year, value_1.month, value_1.day)
218
+ if type(value_2) == str:
219
+ value_2 = dateparser.parse(value_2)
220
+ value_2 = datetime.date(value_2.year, value_2.month, value_2.day)
221
+
222
+ # two times can be the same but if one is datetime-aware and one is not, python will treat them as not equal.
223
+ # we want to treat such times as being the same. Therefore, check for equality but ignore the timezone.
224
+ for to_check in ["year", "month", "day"]:
225
+ if getattr(value_1, to_check) != getattr(value_2, to_check):
226
+ return False
227
+
228
+ # and since we already converted the timezones to match (or one has a timezone and one doesn't), we're good to go.
229
+ # if we passed the above loop then the times are the same.
230
+ return True
@@ -0,0 +1,278 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ from typing import TYPE_CHECKING, Any, Callable, Self, overload
5
+
6
+ import dateparser # type: ignore
7
+
8
+ import clearskies.parameters_to_properties
9
+ import clearskies.typing
10
+ from clearskies import configs
11
+ from clearskies.autodoc.schema import Datetime as AutoDocDatetime
12
+ from clearskies.autodoc.schema import Schema as AutoDocSchema
13
+ from clearskies.column import Column
14
+ from clearskies.query import Condition
15
+
16
+ if TYPE_CHECKING:
17
+ from clearskies import Model
18
+
19
+
20
+ class Datetime(Column):
21
+ """
22
+ Stores date+time data in a column.
23
+
24
+ When processing user input, this value is passed through `dateparser.parse()` to decide if it is a proper date string.
25
+ This makes for relatively flexible input validation. Example:
26
+
27
+ ```python
28
+ import clearskies
29
+
30
+
31
+ class MyModel(clearskies.Model):
32
+ backend = clearskies.backends.MemoryBackend()
33
+ id_column_name = "id"
34
+
35
+ id = clearskies.columns.Uuid()
36
+ name = clearskies.columns.String()
37
+ my_datetime = clearskies.columns.Datetime()
38
+
39
+
40
+ wsgi = clearskies.contexts.WsgiRef(
41
+ clearskies.endpoints.Create(
42
+ MyModel,
43
+ writeable_column_names=["name", "my_datetime"],
44
+ readable_column_names=["id", "name", "my_datetime"],
45
+ ),
46
+ classes=[MyModel],
47
+ )
48
+ wsgi()
49
+ ```
50
+
51
+ And when invoked:
52
+
53
+ ```bash
54
+ $ curl 'http://localhost:8080' -d '{"name":"Bob", "my_datetime":"2025-05-13 12:35:45+00:00"}' | jq
55
+ {
56
+ "status": "success",
57
+ "error": "",
58
+ "data": {
59
+ "id": "68095d0d-c909-4ab3-8c15-bd2667b7b074",
60
+ "name": "Bob",
61
+ "my_datetime": "2025-05-13T12:35:45+00:00"
62
+ },
63
+ "pagination": {},
64
+ "input_errors": {}
65
+ }
66
+
67
+ $ curl 'http://localhost:8080' -d '{"name":"Bob", "my_datetime":"May 13th 2025 2:35:45UTC"}' | jq
68
+ {
69
+ "status": "success",
70
+ "error": "",
71
+ "data": {
72
+ "id": "9fea6933-86ac-4dd1-b9e0-a9fa50608410",
73
+ "name": "Bob",
74
+ "my_datetime": "2025-05-13T12:35:45+00:00"
75
+ },
76
+ "pagination": {},
77
+ "input_errors": {}
78
+ }
79
+
80
+ $ curl 'http://localhost:8080' -d '{"name":"Bob", "my_datetime":"not a date"}' | jq
81
+ {
82
+ "status": "input_errors",
83
+ "error": "",
84
+ "data": [],
85
+ "pagination": {},
86
+ "input_errors": {
87
+ "my_datetime": "given value did not appear to be a valid date"
88
+ }
89
+ }
90
+ ```
91
+ """
92
+
93
+ """
94
+ Whether or not to make datetime objects timezone-aware
95
+ """
96
+ timezone_aware = configs.Boolean(default=True)
97
+
98
+ """
99
+ The timezone to use for the datetime object (if it is timezone aware)
100
+ """
101
+ timezone = configs.Timezone(default=datetime.timezone.utc)
102
+
103
+ """
104
+ The format string to use when sending to the backend (default: %Y-%m-%d %H:%M:%S)
105
+ """
106
+ date_format = configs.String(default="%Y-%m-%d %H:%M:%S")
107
+
108
+ """
109
+ A default value to set for this column.
110
+
111
+ The default is only used when creating a record for the first time, and only if
112
+ a value for this column has not been set.
113
+ """
114
+ default = configs.Datetime() # type: ignore
115
+
116
+ """
117
+ Sets a default date that the backend is going to provide.
118
+
119
+ Some backends, depending on configuration, may provide a default value for the column
120
+ instead of null. By setting this equal to that default value, clearskies can detect
121
+ when a given value is actually a non-value.
122
+ """
123
+ backend_default = configs.String(default="0000-00-00 00:00:00")
124
+
125
+ setable = configs.DatetimeOrCallable(default=None) # type: ignore
126
+ _allowed_search_operators = ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null"]
127
+ auto_doc_class: type[AutoDocSchema] = AutoDocDatetime
128
+ _descriptor_config_map = None
129
+
130
+ @clearskies.parameters_to_properties.parameters_to_properties
131
+ def __init__(
132
+ self,
133
+ date_format: str = "%Y-%m-%d %H:%M:%S",
134
+ backend_default: str = "0000-00-00 00:00:00",
135
+ timezone_aware: bool = True,
136
+ timezone: datetime.timezone = datetime.timezone.utc,
137
+ default: datetime.datetime | None = None,
138
+ setable: datetime.datetime | Callable[..., datetime.datetime] | None = None,
139
+ is_readable: bool = True,
140
+ is_writeable: bool = True,
141
+ is_searchable: bool = True,
142
+ is_temporary: bool = False,
143
+ validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
144
+ on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
145
+ on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
146
+ on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
147
+ created_by_source_type: str = "",
148
+ created_by_source_key: str = "",
149
+ created_by_source_strict: bool = True,
150
+ ):
151
+ pass
152
+
153
+ def from_backend(self, value) -> datetime.datetime | None:
154
+ if not value or value == self.backend_default:
155
+ return None
156
+ if isinstance(value, str):
157
+ value = dateparser.parse(value)
158
+ if not isinstance(value, datetime.datetime):
159
+ raise TypeError(
160
+ f"I was expecting to get a datetime from the backend but I didn't get anything recognizable. I have a value of type '{value.__class__.__name__}'. I need either a datetime object or a datetime serialized as a string."
161
+ )
162
+ if self.timezone_aware:
163
+ if not value.tzinfo:
164
+ value = value.replace(tzinfo=self.timezone)
165
+ elif value.tzinfo != self.timezone:
166
+ value = value.astimezone(self.timezone)
167
+ else:
168
+ value = value.replace(tzinfo=None)
169
+
170
+ return value
171
+
172
+ def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
173
+ if self.name not in data or isinstance(data[self.name], str) or data[self.name] is None:
174
+ return data
175
+
176
+ value = data[self.name]
177
+ if not isinstance(data[self.name], datetime.datetime):
178
+ raise TypeError(
179
+ f"I was expecting a stringified-date or a datetime object to send to the backend, but instead I found a value of {value.__class__.__name__}"
180
+ )
181
+
182
+ return {
183
+ **data,
184
+ self.name: value.strftime(self.date_format),
185
+ }
186
+
187
+ def to_json(self, model: clearskies.model.Model) -> dict[str, Any]:
188
+ """Grabs the column out of the model and converts it into a representation that can be turned into JSON."""
189
+ value = self.__get__(model, model.__class__)
190
+ if value and (isinstance(value, datetime.datetime) or isinstance(value, datetime.date)):
191
+ value = value.isoformat() # type: ignore
192
+
193
+ return {self.name: value}
194
+
195
+ @overload
196
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
197
+ pass
198
+
199
+ @overload
200
+ def __get__(self, instance: Model, cls: type[Model]) -> datetime.datetime:
201
+ pass
202
+
203
+ def __get__(self, instance, cls):
204
+ return super().__get__(instance, cls)
205
+
206
+ def __set__(self, instance, value: datetime.datetime) -> None:
207
+ instance._next_data[self.name] = value
208
+
209
+ def equals(self, value: str | datetime.datetime) -> Condition:
210
+ return super().equals(value)
211
+
212
+ def spaceship(self, value: str | datetime.datetime) -> Condition:
213
+ return super().spaceship(value)
214
+
215
+ def not_equals(self, value: str | datetime.datetime) -> Condition:
216
+ return super().not_equals(value)
217
+
218
+ def less_than_equals(self, value: str | datetime.datetime) -> Condition:
219
+ return super().less_than_equals(value)
220
+
221
+ def greater_than_equals(self, value: str | datetime.datetime) -> Condition:
222
+ return super().greater_than_equals(value)
223
+
224
+ def less_than(self, value: str | datetime.datetime) -> Condition:
225
+ return super().less_than(value)
226
+
227
+ def greater_than(self, value: str | datetime.datetime) -> Condition:
228
+ return super().greater_than(value)
229
+
230
+ def is_in(self, values: list[str | datetime.datetime]) -> Condition:
231
+ return super().is_in(values)
232
+
233
+ def input_error_for_value(self, value, operator=None):
234
+ value = dateparser.parse(value)
235
+ if not value:
236
+ return "given value did not appear to be a valid date"
237
+ if not value.tzinfo and self.timezone_aware:
238
+ return "date is missing timezone information"
239
+ return ""
240
+
241
+ def values_match(self, value_1, value_2):
242
+ """Compare two values to see if they are the same."""
243
+ # in this function we deal with data directly out of the backend, so our date is likely
244
+ # to be string-ified and we want to look for default (e.g. null) values in string form.
245
+ if type(value_1) == str and ("0000-00-00" in value_1 or value_1 == self.backend_default):
246
+ value_1 = None
247
+ if type(value_2) == str and ("0000-00-00" in value_2 or value_2 == self.backend_default):
248
+ value_2 = None
249
+ number_values = 0
250
+ if value_1:
251
+ number_values += 1
252
+ if value_2:
253
+ number_values += 1
254
+ if number_values == 0:
255
+ return True
256
+ if number_values == 1:
257
+ return False
258
+
259
+ if type(value_1) == str:
260
+ value_1 = dateparser.parse(value_1)
261
+ if type(value_2) == str:
262
+ value_2 = dateparser.parse(value_2)
263
+
264
+ # we need to make sure we're comparing in the same timezones. For our purposes, a difference in timezone
265
+ # is fine as long as they represent the same time (e.g. 16:00EST == 20:00UTC). For python, same time in different
266
+ # timezones is treated as different datetime objects.
267
+ if value_1.tzinfo is not None and value_2.tzinfo is not None:
268
+ value_1 = value_1.astimezone(value_2.tzinfo)
269
+
270
+ # two times can be the same but if one is datetime-aware and one is not, python will treat them as not equal.
271
+ # we want to treat such times as being the same. Therefore, check for equality but ignore the timezone.
272
+ for to_check in ["year", "month", "day", "hour", "minute", "second", "microsecond"]:
273
+ if getattr(value_1, to_check) != getattr(value_2, to_check):
274
+ return False
275
+
276
+ # and since we already converted the timezones to match (or one has a timezone and one doesn't), we're good to go.
277
+ # if we passed the above loop then the times are the same.
278
+ return True
@@ -0,0 +1,76 @@
1
+ import re
2
+
3
+ from clearskies.columns.string import String
4
+
5
+
6
+ class Email(String):
7
+ """
8
+ A string column that specifically expects an email.
9
+
10
+ ```python
11
+ import clearskies
12
+
13
+
14
+ class MyModel(clearskies.Model):
15
+ backend = clearskies.backends.MemoryBackend()
16
+ id_column_name = "id"
17
+
18
+ id = clearskies.columns.Uuid()
19
+ email = clearskies.columns.Email()
20
+
21
+
22
+ wsgi = clearskies.contexts.WsgiRef(
23
+ clearskies.endpoints.Create(
24
+ MyModel,
25
+ writeable_column_names=["email"],
26
+ readable_column_names=["id", "email"],
27
+ ),
28
+ classes=[MyModel],
29
+ )
30
+ wsgi()
31
+ ```
32
+
33
+ And when invoked:
34
+
35
+ ```bash
36
+ $ curl 'http://localhost:8080' -d '{"email":"test@example.com"}' | jq
37
+ {
38
+ "status": "success",
39
+ "error": "",
40
+ "data": {
41
+ "id": "2a72a895-c469-45b0-b5cd-5a3cbb3a6e99",
42
+ "email": "test@example.com"
43
+ },
44
+ "pagination": {},
45
+ "input_errors": {}
46
+ }
47
+
48
+ $ curl 'http://localhost:8080' -d '{"email":"asdf"}' | jq
49
+ {
50
+ "status": "input_errors",
51
+ "error": "",
52
+ "data": [],
53
+ "pagination": {},
54
+ "input_errors": {
55
+ "email": "Invalid email address"
56
+ }
57
+ }
58
+ ```
59
+ """
60
+
61
+ _descriptor_config_map = None
62
+
63
+ """
64
+ A column that always requires an email address.
65
+ """
66
+
67
+ def input_error_for_value(self, value: str, operator: str | None = None) -> str:
68
+ if type(value) != str:
69
+ return f"Value must be a string for {self.name}"
70
+ if operator and operator.lower() == "like":
71
+ # don't check for an email if doing a fuzzy search, since we may be searching
72
+ # for a partial email
73
+ return ""
74
+ if re.search(r"^[^@\s]+@[^@]+\.[^@]+$", value):
75
+ return ""
76
+ return "Invalid email address"