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