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
@@ -0,0 +1,164 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ from typing import TYPE_CHECKING, Any, Callable, Self, overload
5
+
6
+ from clearskies import configs, decorators
7
+ from clearskies.columns.datetime import Datetime
8
+
9
+ if TYPE_CHECKING:
10
+ from clearskies import Model, typing
11
+
12
+
13
+ class Timestamp(Datetime):
14
+ """
15
+ A timestamp column.
16
+
17
+ The difference between this and the datetime column is that this stores the datetime
18
+ as a standard unix timestamp - the number of seconds since the unix epoch.
19
+
20
+ Also, this **always** assumes the timezone for the timestamp is UTC
21
+
22
+ ```python
23
+ import datetime
24
+ import clearskies
25
+
26
+
27
+ class Pet(clearskies.Model):
28
+ id_column_name = "id"
29
+ backend = clearskies.backends.MemoryBackend()
30
+
31
+ id = clearskies.columns.Uuid()
32
+ name = clearskies.columns.String()
33
+ last_fed = clearskies.columns.Timestamp()
34
+
35
+
36
+ def demo_timestamp(utcnow: datetime.datetime, pets: Pet) -> dict[str, str | int]:
37
+ pet = pets.create(
38
+ {
39
+ "name": "Spot",
40
+ "last_fed": utcnow,
41
+ }
42
+ )
43
+ return {
44
+ "last_fed": pet.last_fed.isoformat(),
45
+ "raw_data": pet.get_raw_data()["last_fed"],
46
+ }
47
+
48
+
49
+ cli = clearskies.contexts.Cli(
50
+ clearskies.endpoints.Callable(
51
+ demo_timestamp,
52
+ ),
53
+ classes=[Pet],
54
+ )
55
+ cli()
56
+ ```
57
+
58
+ And when invoked it returns:
59
+
60
+ ```json
61
+ {
62
+ "status": "success",
63
+ "error": "",
64
+ "data": {"last_fed": "2025-05-18T19:14:56+00:00", "raw_data": 1747595696},
65
+ "pagination": {},
66
+ "input_errors": {},
67
+ }
68
+ ```
69
+
70
+ Note that if you pull the column from the model in the usual way (e.g. `pet.last_fed` you get a timestamp,
71
+ but if you check the raw data straight out of the backend (e.g. `pet.get_raw_data()["last_fed"]`) it's an
72
+ integer.
73
+ """
74
+
75
+ # whether or not to include the microseconds in the timestamp
76
+ include_microseconds = configs.Boolean(default=False)
77
+ _descriptor_config_map = None
78
+
79
+ @decorators.parameters_to_properties
80
+ def __init__(
81
+ self,
82
+ include_microseconds: bool = False,
83
+ default: datetime.datetime | None = None,
84
+ setable: datetime.datetime | Callable[..., datetime.datetime] | None = None,
85
+ is_readable: bool = True,
86
+ is_writeable: bool = True,
87
+ is_searchable: bool = True,
88
+ is_temporary: bool = False,
89
+ validators: typing.validator | list[typing.validator] = [],
90
+ on_change_pre_save: typing.action | list[typing.action] = [],
91
+ on_change_post_save: typing.action | list[typing.action] = [],
92
+ on_change_save_finished: typing.action | list[typing.action] = [],
93
+ created_by_source_type: str = "",
94
+ created_by_source_key: str = "",
95
+ created_by_source_strict: bool = True,
96
+ ):
97
+ pass
98
+
99
+ def from_backend(self, value) -> datetime.datetime | None:
100
+ mult = 1000 if self.include_microseconds else 1
101
+ if not value:
102
+ date = None
103
+ elif isinstance(value, str):
104
+ if not value.isdigit():
105
+ raise ValueError(
106
+ f"Invalid data was found in the backend for model {self.model_class.__name__} and column {self.name}: a string value was found that is not a timestamp. It was '{value}'"
107
+ )
108
+ date = datetime.datetime.fromtimestamp(int(value) / mult, datetime.timezone.utc)
109
+ elif isinstance(value, (int, float)):
110
+ date = datetime.datetime.fromtimestamp(value / mult, datetime.timezone.utc)
111
+ else:
112
+ if not isinstance(value, datetime.datetime):
113
+ raise ValueError(
114
+ f"Invalid data was found in the backend for model {self.model_class.__name__} and column {self.name}: the value was neither an integer, float, string, or datetime object"
115
+ )
116
+ date = value
117
+ return date.replace(tzinfo=datetime.timezone.utc) if date else None
118
+
119
+ def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
120
+ if not self.name in data or isinstance(data[self.name], int) or data[self.name] == None:
121
+ return data
122
+
123
+ value = data[self.name]
124
+ if isinstance(value, str):
125
+ if not value.isdigit():
126
+ raise ValueError(
127
+ f"Invalid data was sent to the backend for model {self.model_class.__name__} and column {self.name}: a string value was found that is not a timestamp. It was '{value}'"
128
+ )
129
+ value = int(value)
130
+ elif isinstance(value, datetime.datetime):
131
+ value = value.timestamp()
132
+ else:
133
+ raise ValueError(
134
+ f"Invalid data was sent to the backend for model {self.model_class.__name__} and column {self.name}: the value was neither an integer, a string, nor a datetime object"
135
+ )
136
+
137
+ return {**data, self.name: int(value)}
138
+
139
+ @overload
140
+ def __get__(self, instance: None, cls: type) -> Self:
141
+ pass
142
+
143
+ @overload
144
+ def __get__(self, instance: Model, cls: type) -> datetime.datetime:
145
+ pass
146
+
147
+ def __get__(self, instance, cls):
148
+ return super().__get__(instance, cls)
149
+
150
+ def __set__(self, instance, value: datetime.datetime) -> None:
151
+ # this makes sure we're initialized
152
+ if "name" not in self._config: # type: ignore
153
+ instance.get_columns()
154
+
155
+ instance._next_data[self.name] = value
156
+
157
+ def input_error_for_value(self, value: str, operator: str | None = None) -> str:
158
+ if not isinstance(value, int):
159
+ return f"'{self.name}' must be an integer"
160
+ return ""
161
+
162
+ def values_match(self, value_1, value_2):
163
+ """Compare two values to see if they are the same."""
164
+ return value_1 == value_2
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from clearskies import configs, decorators
6
+ from clearskies.columns.datetime import Datetime
7
+ from clearskies.di import inject
8
+
9
+ if TYPE_CHECKING:
10
+ from clearskies import Model, typing
11
+
12
+
13
+ class Updated(Datetime):
14
+ """
15
+ The updated column records the time that a record is created or updated.
16
+
17
+ Note that this will always populate the column anytime the model is created or updated.
18
+ You don't have to provide the timestamp yourself and you should never expose it as
19
+ a writeable column through an endpoint (in fact, you can't).
20
+
21
+ ```python
22
+ import clearskies
23
+ import time
24
+
25
+
26
+ class MyModel(clearskies.Model):
27
+ backend = clearskies.backends.MemoryBackend()
28
+ id_column_name = "id"
29
+
30
+ id = clearskies.columns.Uuid()
31
+ name = clearskies.columns.String()
32
+ created = clearskies.columns.Created()
33
+ updated = clearskies.columns.Updated()
34
+
35
+
36
+ def test_updated(my_models: MyModel) -> MyModel:
37
+ my_model = my_models.create({"name": "Jane"})
38
+ updated_column_after_create = my_model.updated
39
+
40
+ time.sleep(2)
41
+
42
+ my_model.save({"name": "Susan"})
43
+
44
+ return {
45
+ "updated_column_after_create": updated_column_after_create.isoformat(),
46
+ "updated_column_at_end": my_model.updated.isoformat(),
47
+ "difference_in_seconds": (my_model.updated - updated_column_after_create).total_seconds(),
48
+ }
49
+
50
+
51
+ cli = clearskies.contexts.Cli(clearskies.endpoints.Callable(test_updated), classes=[MyModel])
52
+ cli()
53
+ ```
54
+
55
+ And when invoked:
56
+
57
+ ```bash
58
+ $ ./test.py | jq
59
+ {
60
+ "status": "success",
61
+ "error": "",
62
+ "data": {
63
+ "updated_column_after_create": "2025-05-18T19:28:46+00:00",
64
+ "updated_column_at_end": "2025-05-18T19:28:48+00:00",
65
+ "difference_in_seconds": 2.0
66
+ },
67
+ "pagination": {},
68
+ "input_errors": {}
69
+ }
70
+ ```
71
+
72
+ Note that the `updated` column was set both when the record was first created and when it was updated,
73
+ so there is a two second difference between them (since we slept for two seconds).
74
+
75
+ """
76
+
77
+ """
78
+ Created fields are never writeable because they always set the created time automatically.
79
+ """
80
+ is_writeable = configs.Boolean(default=False)
81
+ _descriptor_config_map = None
82
+
83
+ now = inject.Now()
84
+
85
+ @decorators.parameters_to_properties
86
+ def __init__(
87
+ self,
88
+ in_utc: bool = True,
89
+ date_format: str = "%Y-%m-%d %H:%M:%S",
90
+ backend_default: str = "0000-00-00 00:00:00",
91
+ is_readable: bool = True,
92
+ is_searchable: bool = True,
93
+ is_temporary: bool = False,
94
+ on_change_pre_save: typing.action | list[typing.action] = [],
95
+ on_change_post_save: typing.action | list[typing.action] = [],
96
+ on_change_save_finished: typing.action | list[typing.action] = [],
97
+ ):
98
+ pass
99
+
100
+ def pre_save(self, data: dict[str, Any], model: Model) -> dict[str, Any]:
101
+ now = self.now
102
+ if self.timezone_aware:
103
+ now = now.astimezone(self.timezone)
104
+ data = {**data, self.name: now}
105
+ if self.on_change_pre_save:
106
+ data = self.execute_actions_with_data(self.on_change_pre_save, model, data)
107
+ return data
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from clearskies import configs, decorators, di
6
+ from clearskies.columns.string import String
7
+
8
+ if TYPE_CHECKING:
9
+ from clearskies import Model, typing
10
+
11
+
12
+ class Uuid(String):
13
+ """
14
+ Populates the column with a UUID upon record creation.
15
+
16
+ This column really just has a very specific purpose: ids!
17
+
18
+ When used, it will automatically populate the column with a random UUID upon record creation.
19
+ It is not a writeable column, which means that you cannot expose it for write operations via an endpoint.
20
+
21
+ ```python
22
+ import clearskies
23
+
24
+
25
+ class MyModel(clearskies.Model):
26
+ backend = clearskies.backends.MemoryBackend()
27
+ id_column_name = "id"
28
+
29
+ id = clearskies.columns.Uuid()
30
+ name = clearskies.columns.String()
31
+
32
+
33
+ wsgi = clearskies.contexts.WsgiRef(
34
+ clearskies.endpoints.Create(
35
+ MyModel,
36
+ writeable_column_names=["name"],
37
+ readable_column_names=["id", "name"],
38
+ ),
39
+ )
40
+ wsgi()
41
+ ```
42
+
43
+ and when invoked:
44
+
45
+ ```bash
46
+ $ curl http://localhost:8080 -d '{"name": "John Doe"}' | jq
47
+ {
48
+ "status": "success",
49
+ "error": "",
50
+ "data": {
51
+ "id": "d4f23106-b48a-4dc5-9bf6-df61f6ca54f7",
52
+ "name": "John Doe"
53
+ },
54
+ "pagination": {},
55
+ "input_errors": {}
56
+ }
57
+ ```
58
+ """
59
+
60
+ is_writeable = configs.Boolean(default=False)
61
+ _descriptor_config_map = None
62
+
63
+ uuid = di.inject.Uuid()
64
+
65
+ @decorators.parameters_to_properties
66
+ def __init__(
67
+ self,
68
+ is_readable: bool = True,
69
+ is_searchable: bool = True,
70
+ is_temporary: bool = False,
71
+ on_change_pre_save: typing.action | list[typing.action] = [],
72
+ on_change_post_save: typing.action | list[typing.action] = [],
73
+ on_change_save_finished: typing.action | list[typing.action] = [],
74
+ ):
75
+ pass
76
+
77
+ def pre_save(self, data: dict[str, Any], model: Model) -> dict[str, Any]:
78
+ if model:
79
+ return data
80
+ data = {**data, self.name: str(self.uuid.uuid4())}
81
+ if self.on_change_pre_save:
82
+ data = self.execute_actions_with_data(self.on_change_pre_save, model, data)
83
+ return data
@@ -0,0 +1,105 @@
1
+ # About
2
+
3
+ There are all sorts of things in clearskies that need to be configured - handlers, columns, models, etc... `configurable.Configurable` works together with the config classes to make this happen. The idea is that something that needs to be configured extends `Configurable` and then declares configs as properties. A simple example:
4
+
5
+ ```python
6
+ class ConfigurableThing(configurable.Configurable):
7
+ my_name = config.String(required=True)
8
+ is_required = config.Boolean(default=False)
9
+ some_option = config.Select(allowed_values=["option 1", "option 2", "option 3"])
10
+ ```
11
+
12
+ We've declared three configuration options for our `ConfigurableThing` class:
13
+
14
+ 1. `my_name` which is a string and must be set
15
+ 2. `is_required` which is a boolean and defaults to `False`
16
+ 3. `some_option` which is a string and must be one of `[None, "option 1", "option 2", "option 3"]`
17
+
18
+ However, our example above is missing one important thing: actually setting these values. They act like standard descriptors, so with just the above code you could:
19
+
20
+ ```python
21
+ configruable_thing = ConfigurableThing()
22
+ configruable_thing.my_name = "Jane Doe"
23
+ configruable_thing.is_required = True
24
+ configruable_thing.some_option = "option 2"
25
+ ```
26
+
27
+ Typically though you need a well defined way to set these values **AND** the class must call `super().finalize_and_validate_configuration()` once the configuration is set. This is because many of the validations are only possible after all the configs are set, so the configurable class treats the process of setting the configuration as a one-time, monolithic process: you set the configs, validate everything, and then use the config. It's *NOT* the goal to continually change the configuration for an object after creation. The simplest way to do this would be in the constructor:
28
+
29
+ ```python
30
+ class ConfigurableThing(configurable.Configurable):
31
+ my_name = config.String(required=True)
32
+ is_required = config.Boolean(default=False)
33
+ some_option = config.Select(allowed_values=["option 1", "option 2", "option 3"])
34
+
35
+ def __init__(self,
36
+ my_name: str,
37
+ is_required: bool=False,
38
+ some_option: str=None,
39
+ ):
40
+ self.my_name = my_name
41
+ self.is_required = is_required
42
+ self.some_option = some_option
43
+
44
+ super().finalize_and_validate_configuration()
45
+ ```
46
+
47
+ However, this doesn't always work because your class may be constructed via the dependency injection system. In this case, the constructor must be reserved for injecting the necessary dependencies. In addition, the object won't be constructed directly via code, so it's not possible to specify the configuration options there. In this case, config values can be shifted to the `configure` method and you can use a binding config:
48
+
49
+ ```python
50
+ class ConfigurableThing(configurable.Configurable):
51
+ my_name = config.String(required=True)
52
+ is_required = config.Boolean(default=False)
53
+ some_option = config.Select(allowed_values=["option 1", "option 2", "option 3"])
54
+
55
+ def __init__(self, some_dependency, other_dependency):
56
+ self.some_dependency = some_dependency
57
+ self.other_dependency = other_dependency
58
+
59
+ def configure(self,
60
+ my_name: str,
61
+ is_required: bool=False,
62
+ some_option: str=None,
63
+ ):
64
+ self.my_name = my_name
65
+ self.is_required = is_required
66
+ self.some_option = some_option
67
+
68
+ super().finalize_and_validate_configuration()
69
+
70
+ context = clearskies.contexts.cli(
71
+ SomeApplication,
72
+ bindings={
73
+ "configurable_thing": clearskies.bindings.Binding(ConfigurableThing, my_name="hey", is_required=Flase, some_option="option 2"),
74
+ }
75
+ )
76
+ ```
77
+
78
+ Note that we've lost our strong typing when creating the binding, but that can be fixed by extending the binding config:
79
+
80
+ ```python
81
+ class ConfigurableThing:
82
+ """ See above """
83
+
84
+ class ConfigurableThingBinding(clearskies.bindings.Binding):
85
+ def __init__(
86
+ self,
87
+ my_name: str,
88
+ is_required: bool=False,
89
+ some_option: str=None,
90
+ ):
91
+ self.object_class = ConfigurableThing
92
+ self.args = [my_name]
93
+ self.kwargs = {"is_required": is_required, "some_option": some_option}
94
+
95
+ context = clearskies.contexts.cli(
96
+ SomeApplication,
97
+ bindings={
98
+ "configurable_thing": ConfigurableThingBinding("hey", some_option="option 3")
99
+ }
100
+ )
101
+ ```
102
+
103
+ The primary example of classes that implement this pattern are the column config classes (`clearskies.columns.*`, but excluding `clearskies.columns.implementors`). In this case the config and implementation are completely separated, so the configuration is set in the constructor instead of a separate `configure` method.
104
+
105
+ Validators, actions, and handlers also use the above config pattern, but those use the `Binding` pattern and so make use of a `configure` method.
@@ -0,0 +1,170 @@
1
+ """
2
+ This module helps classes declare their configuration parameters via properties.
3
+
4
+ To use it, the class needs to include the clearskies.configs.Confirgurable in its parent chain and
5
+ then create properties as needed using the various classes in the clearskies.configs module. Each
6
+ class represents a specific "kind" of configuration, has typing declared to help while writing code,
7
+ and runtime checks to verify configs while the application is (preferably) booting.
8
+
9
+ Each config accepts a `required` and `default` kwarg to assist with validation/construction of
10
+ the configuration
11
+
12
+ Data is stored in the `_config` property on the instance.
13
+
14
+ Usage:
15
+
16
+ ```python
17
+ from clearskies import configs
18
+
19
+
20
+ class MyConfigurableClass(configs.Configurable):
21
+ name = configs.String()
22
+ age = configs.Integer(required=True)
23
+ property_with_default = configs.String(default="some value")
24
+
25
+ def __init__(self, name, age, optional=None):
26
+ self.name = name
27
+ self.age = age
28
+
29
+ # always call this after saving the confiuration values to the properties.
30
+ # It will fill in default values for any properties that have a default and
31
+ # are none, and it will raise a ValueError if there is a required property
32
+ # that does not have a value.
33
+ self.finalize_and_validate_configuration()
34
+
35
+
36
+ configured_thingie = MyConfigurableClass("Bob", 18)
37
+ print(configured_thingie.age) # prints: 18
38
+ print(configured_thingie.property_with_default) # prints: some value
39
+
40
+ invalid_thingie = MyConfigurableClass(18, 20) # raises a TypeError
41
+
42
+ also_invalid = MyConfigurableClass("", 18) # raises a ValueError
43
+ ```
44
+
45
+ Finally, parameters_to_properties is a decorator that will take any parameters passed into the
46
+ decorated function and assign them as instance properties. You can use this to skip some code,
47
+ especially if you have a lot of configuration parameters. In the above example, you could simplify
48
+ it as:
49
+
50
+ ```python
51
+ from clearskies import configs
52
+
53
+
54
+ class MyConfigurableClass(configs.Configurable):
55
+ name = configs.String()
56
+ age = configs.Integer(required=True)
57
+ property_with_default = configs.String(default="some value")
58
+
59
+ @clearskies.decorators()
60
+ def __init__(self, name: str, age: int, optional: string = None):
61
+ self.finalize_and_validate_configuration()
62
+ ```
63
+
64
+ """
65
+
66
+ from .actions import Actions
67
+ from .any import Any
68
+ from .any_dict import AnyDict
69
+ from .any_dict_or_callable import AnyDictOrCallable
70
+ from .authentication import Authentication
71
+ from .authorization import Authorization
72
+ from .boolean import Boolean
73
+ from .boolean_or_callable import BooleanOrCallable
74
+ from .callable_config import Callable
75
+ from .columns import Columns
76
+ from .conditions import Conditions
77
+ from .config import Config
78
+ from .datetime import Datetime
79
+ from .datetime_or_callable import DatetimeOrCallable
80
+ from .email import Email
81
+ from .email_list import EmailList
82
+ from .email_list_or_callable import EmailListOrCallable
83
+ from .email_or_email_list_or_callable import EmailOrEmailListOrCallable
84
+ from .endpoint import Endpoint
85
+ from .endpoint_list import EndpointList
86
+ from .float import Float
87
+ from .float_or_callable import FloatOrCallable
88
+ from .headers import Headers
89
+ from .integer import Integer
90
+ from .integer_or_callable import IntegerOrCallable
91
+ from .joins import Joins
92
+ from .list_any_dict import ListAnyDict
93
+ from .list_any_dict_or_callable import ListAnyDictOrCallable
94
+ from .model_class import ModelClass
95
+ from .model_column import ModelColumn
96
+ from .model_columns import ModelColumns
97
+ from .model_destination_name import ModelDestinationName
98
+ from .model_to_id_column import ModelToIdColumn
99
+ from .readable_model_column import ReadableModelColumn
100
+ from .readable_model_columns import ReadableModelColumns
101
+ from .schema import Schema
102
+ from .searchable_model_columns import SearchableModelColumns
103
+ from .security_headers import SecurityHeaders
104
+ from .select import Select
105
+ from .select_list import SelectList
106
+ from .string import String
107
+ from .string_dict import StringDict
108
+ from .string_list import StringList
109
+ from .string_list_or_callable import StringListOrCallable
110
+ from .string_or_callable import StringOrCallable
111
+ from .timedelta import Timedelta
112
+ from .timezone import Timezone
113
+ from .url import Url
114
+ from .validators import Validators
115
+ from .writeable_model_column import WriteableModelColumn
116
+ from .writeable_model_columns import WriteableModelColumns
117
+
118
+ __all__ = [
119
+ "Actions",
120
+ "Any",
121
+ "AnyDict",
122
+ "AnyDictOrCallable",
123
+ "Authentication",
124
+ "Authorization",
125
+ "Boolean",
126
+ "BooleanOrCallable",
127
+ "Callable",
128
+ "Columns",
129
+ "Conditions",
130
+ "Config",
131
+ "Datetime",
132
+ "DatetimeOrCallable",
133
+ "Email",
134
+ "EmailList",
135
+ "EmailListOrCallable",
136
+ "EmailOrEmailListOrCallable",
137
+ "Endpoint",
138
+ "EndpointList",
139
+ "Float",
140
+ "FloatOrCallable",
141
+ "Headers",
142
+ "Joins",
143
+ "Integer",
144
+ "IntegerOrCallable",
145
+ "ListAnyDict",
146
+ "ListAnyDictOrCallable",
147
+ "ModelClass",
148
+ "ModelColumn",
149
+ "ModelColumns",
150
+ "ModelToIdColumn",
151
+ "ModelDestinationName",
152
+ "ReadableModelColumn",
153
+ "ReadableModelColumns",
154
+ "Schema",
155
+ "SearchableModelColumns",
156
+ "SecurityHeaders",
157
+ "Select",
158
+ "SelectList",
159
+ "String",
160
+ "StringDict",
161
+ "StringList",
162
+ "StringListOrCallable",
163
+ "StringOrCallable",
164
+ "Timedelta",
165
+ "Timezone",
166
+ "Url",
167
+ "Validators",
168
+ "WriteableModelColumn",
169
+ "WriteableModelColumns",
170
+ ]
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from clearskies import action
6
+ from clearskies.configs import config
7
+
8
+ if TYPE_CHECKING:
9
+ from clearskies import typing
10
+
11
+
12
+ class Actions(config.Config):
13
+ """
14
+ Action config.
15
+
16
+ A config that accepts various things that are accepted as actions in model lifecycle hooks:
17
+
18
+ 1. A callable (which should accept `model` as a parameter)
19
+ 2. An instance of clearskies.actions.Action
20
+ 3. A list containing any combination of the above
21
+
22
+ Incoming values are normalized to a list so that a list always comes out even if a non-list is provided.
23
+ """
24
+
25
+ def __set__(self, instance, value: typing.action | list[typing.action]):
26
+ if not isinstance(value, list):
27
+ value = [value]
28
+
29
+ for index, item in enumerate(value):
30
+ if callable(item) or isinstance(item, action.Action):
31
+ continue
32
+
33
+ error_prefix = self._error_prefix(instance)
34
+ raise TypeError(
35
+ f"{error_prefix} attempt to set a value of type '{item.__class__.__name__}' for item #{index + 1} when a callable or Action is required"
36
+ )
37
+
38
+ instance._set_config(self, [*value])
39
+
40
+ def __get__(self, instance, parent) -> list[typing.action]:
41
+ if not instance:
42
+ return self # type: ignore
43
+ return instance._get_config(self)
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any as AnyType
4
+
5
+ from clearskies.configs import config
6
+
7
+
8
+ class Any(config.Config):
9
+ def __set__(self, instance, value: AnyType):
10
+ instance._set_config(self, value)
11
+
12
+ def __get__(self, instance, parent) -> AnyType:
13
+ if not instance:
14
+ return self # type: ignore
15
+ return instance._get_config(self)