clear-skies 1.22.30__py3-none-any.whl → 2.0.0__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 (344) hide show
  1. {clear_skies-1.22.30.dist-info → clear_skies-2.0.0.dist-info}/METADATA +5 -7
  2. clear_skies-2.0.0.dist-info/RECORD +248 -0
  3. {clear_skies-1.22.30.dist-info → clear_skies-2.0.0.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 +42 -0
  8. clearskies/authentication/authorization.py +4 -9
  9. clearskies/authentication/authorization_pass_through.py +11 -9
  10. clearskies/authentication/jwks.py +128 -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 +40 -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 +1232 -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 +159 -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 +21 -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/float.py +16 -0
  100. clearskies/configs/float_or_callable.py +18 -0
  101. clearskies/configs/integer.py +16 -0
  102. clearskies/configs/integer_or_callable.py +18 -0
  103. clearskies/configs/joins.py +30 -0
  104. clearskies/configs/list_any_dict.py +30 -0
  105. clearskies/configs/list_any_dict_or_callable.py +31 -0
  106. clearskies/configs/model_class.py +35 -0
  107. clearskies/configs/model_column.py +65 -0
  108. clearskies/configs/model_columns.py +56 -0
  109. clearskies/configs/model_destination_name.py +25 -0
  110. clearskies/configs/model_to_id_column.py +43 -0
  111. clearskies/configs/readable_model_column.py +9 -0
  112. clearskies/configs/readable_model_columns.py +9 -0
  113. clearskies/configs/schema.py +23 -0
  114. clearskies/configs/searchable_model_columns.py +9 -0
  115. clearskies/configs/security_headers.py +39 -0
  116. clearskies/configs/select.py +26 -0
  117. clearskies/configs/select_list.py +47 -0
  118. clearskies/configs/string.py +29 -0
  119. clearskies/configs/string_dict.py +32 -0
  120. clearskies/configs/string_list.py +32 -0
  121. clearskies/configs/string_list_or_callable.py +35 -0
  122. clearskies/configs/string_or_callable.py +18 -0
  123. clearskies/configs/timedelta.py +18 -0
  124. clearskies/configs/timezone.py +18 -0
  125. clearskies/configs/url.py +23 -0
  126. clearskies/configs/validators.py +45 -0
  127. clearskies/configs/writeable_model_column.py +9 -0
  128. clearskies/configs/writeable_model_columns.py +9 -0
  129. clearskies/configurable.py +76 -0
  130. clearskies/contexts/__init__.py +8 -8
  131. clearskies/contexts/cli.py +5 -42
  132. clearskies/contexts/context.py +78 -56
  133. clearskies/contexts/wsgi.py +13 -30
  134. clearskies/contexts/wsgi_ref.py +49 -0
  135. clearskies/di/__init__.py +10 -7
  136. clearskies/di/additional_config.py +115 -4
  137. clearskies/di/additional_config_auto_import.py +12 -0
  138. clearskies/di/di.py +742 -121
  139. clearskies/di/inject/__init__.py +23 -0
  140. clearskies/di/inject/by_class.py +21 -0
  141. clearskies/di/inject/by_name.py +18 -0
  142. clearskies/di/inject/di.py +13 -0
  143. clearskies/di/inject/environment.py +14 -0
  144. clearskies/di/inject/input_output.py +20 -0
  145. clearskies/di/inject/now.py +13 -0
  146. clearskies/di/inject/requests.py +13 -0
  147. clearskies/di/inject/secrets.py +14 -0
  148. clearskies/di/inject/utcnow.py +13 -0
  149. clearskies/di/inject/uuid.py +15 -0
  150. clearskies/di/injectable.py +29 -0
  151. clearskies/di/injectable_properties.py +131 -0
  152. clearskies/end.py +183 -0
  153. clearskies/endpoint.py +1309 -0
  154. clearskies/endpoint_group.py +297 -0
  155. clearskies/endpoints/__init__.py +23 -0
  156. clearskies/endpoints/advanced_search.py +526 -0
  157. clearskies/endpoints/callable.py +387 -0
  158. clearskies/endpoints/create.py +202 -0
  159. clearskies/endpoints/delete.py +139 -0
  160. clearskies/endpoints/get.py +275 -0
  161. clearskies/endpoints/health_check.py +181 -0
  162. clearskies/endpoints/list.py +573 -0
  163. clearskies/endpoints/restful_api.py +427 -0
  164. clearskies/endpoints/simple_search.py +286 -0
  165. clearskies/endpoints/update.py +190 -0
  166. clearskies/environment.py +5 -3
  167. clearskies/exceptions/__init__.py +17 -0
  168. clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
  169. clearskies/exceptions/moved_permanently.py +3 -0
  170. clearskies/exceptions/moved_temporarily.py +3 -0
  171. clearskies/exceptions/not_found.py +2 -0
  172. clearskies/functional/__init__.py +2 -2
  173. clearskies/functional/routing.py +92 -0
  174. clearskies/functional/string.py +19 -11
  175. clearskies/functional/validations.py +61 -9
  176. clearskies/input_outputs/__init__.py +9 -7
  177. clearskies/input_outputs/cli.py +130 -142
  178. clearskies/input_outputs/exceptions/__init__.py +1 -1
  179. clearskies/input_outputs/headers.py +45 -0
  180. clearskies/input_outputs/input_output.py +91 -122
  181. clearskies/input_outputs/programmatic.py +69 -0
  182. clearskies/input_outputs/wsgi.py +23 -38
  183. clearskies/model.py +489 -184
  184. clearskies/parameters_to_properties.py +31 -0
  185. clearskies/query/__init__.py +12 -0
  186. clearskies/query/condition.py +223 -0
  187. clearskies/query/join.py +136 -0
  188. clearskies/query/query.py +196 -0
  189. clearskies/query/sort.py +27 -0
  190. clearskies/schema.py +82 -0
  191. clearskies/secrets/__init__.py +3 -31
  192. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
  193. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
  194. clearskies/secrets/akeyless.py +88 -147
  195. clearskies/secrets/secrets.py +8 -8
  196. clearskies/security_header.py +8 -0
  197. clearskies/security_headers/__init__.py +8 -8
  198. clearskies/security_headers/cache_control.py +47 -110
  199. clearskies/security_headers/cors.py +40 -95
  200. clearskies/security_headers/csp.py +76 -151
  201. clearskies/security_headers/hsts.py +14 -16
  202. clearskies/test_base.py +8 -0
  203. clearskies/typing.py +11 -0
  204. clearskies/validator.py +25 -0
  205. clearskies/validators/__init__.py +33 -0
  206. clearskies/validators/after_column.py +62 -0
  207. clearskies/validators/before_column.py +13 -0
  208. clearskies/validators/in_the_future.py +32 -0
  209. clearskies/validators/in_the_future_at_least.py +11 -0
  210. clearskies/validators/in_the_future_at_most.py +10 -0
  211. clearskies/validators/in_the_past.py +32 -0
  212. clearskies/validators/in_the_past_at_least.py +10 -0
  213. clearskies/validators/in_the_past_at_most.py +10 -0
  214. clearskies/validators/maximum_length.py +26 -0
  215. clearskies/validators/maximum_value.py +29 -0
  216. clearskies/validators/minimum_length.py +26 -0
  217. clearskies/validators/minimum_value.py +29 -0
  218. clearskies/validators/required.py +35 -0
  219. clearskies/validators/timedelta.py +59 -0
  220. clearskies/validators/unique.py +31 -0
  221. clear_skies-1.22.30.dist-info/RECORD +0 -214
  222. clearskies/application.py +0 -29
  223. clearskies/authentication/auth0_jwks.py +0 -118
  224. clearskies/authentication/auth_exception.py +0 -2
  225. clearskies/authentication/jwks_jwcrypto.py +0 -51
  226. clearskies/backends/api_get_only_backend.py +0 -48
  227. clearskies/backends/example_backend.py +0 -43
  228. clearskies/backends/file_backend.py +0 -48
  229. clearskies/backends/json_backend.py +0 -7
  230. clearskies/backends/restful_api_advanced_search_backend.py +0 -103
  231. clearskies/binding_config.py +0 -16
  232. clearskies/column_types/__init__.py +0 -203
  233. clearskies/column_types/audit.py +0 -249
  234. clearskies/column_types/belongs_to.py +0 -271
  235. clearskies/column_types/boolean.py +0 -60
  236. clearskies/column_types/category_tree.py +0 -304
  237. clearskies/column_types/column.py +0 -373
  238. clearskies/column_types/created.py +0 -26
  239. clearskies/column_types/created_by_authorization_data.py +0 -26
  240. clearskies/column_types/created_by_header.py +0 -24
  241. clearskies/column_types/created_by_ip.py +0 -17
  242. clearskies/column_types/created_by_routing_data.py +0 -25
  243. clearskies/column_types/created_by_user_agent.py +0 -17
  244. clearskies/column_types/created_micro.py +0 -26
  245. clearskies/column_types/datetime.py +0 -109
  246. clearskies/column_types/datetime_micro.py +0 -12
  247. clearskies/column_types/email.py +0 -18
  248. clearskies/column_types/float.py +0 -43
  249. clearskies/column_types/has_many.py +0 -179
  250. clearskies/column_types/has_one.py +0 -60
  251. clearskies/column_types/integer.py +0 -41
  252. clearskies/column_types/json.py +0 -25
  253. clearskies/column_types/many_to_many.py +0 -278
  254. clearskies/column_types/many_to_many_with_data.py +0 -162
  255. clearskies/column_types/phone.py +0 -48
  256. clearskies/column_types/select.py +0 -11
  257. clearskies/column_types/string.py +0 -24
  258. clearskies/column_types/timestamp.py +0 -73
  259. clearskies/column_types/updated.py +0 -26
  260. clearskies/column_types/updated_micro.py +0 -26
  261. clearskies/column_types/uuid.py +0 -25
  262. clearskies/columns.py +0 -123
  263. clearskies/condition_parser.py +0 -172
  264. clearskies/contexts/build_context.py +0 -54
  265. clearskies/contexts/convert_to_application.py +0 -190
  266. clearskies/contexts/extract_handler.py +0 -37
  267. clearskies/contexts/test.py +0 -94
  268. clearskies/decorators/__init__.py +0 -41
  269. clearskies/decorators/allow_non_json_bodies.py +0 -9
  270. clearskies/decorators/auth0_jwks.py +0 -22
  271. clearskies/decorators/authorization.py +0 -10
  272. clearskies/decorators/binding_classes.py +0 -9
  273. clearskies/decorators/binding_modules.py +0 -9
  274. clearskies/decorators/bindings.py +0 -9
  275. clearskies/decorators/create.py +0 -10
  276. clearskies/decorators/delete.py +0 -10
  277. clearskies/decorators/docs.py +0 -14
  278. clearskies/decorators/get.py +0 -10
  279. clearskies/decorators/jwks.py +0 -26
  280. clearskies/decorators/merge.py +0 -124
  281. clearskies/decorators/patch.py +0 -10
  282. clearskies/decorators/post.py +0 -10
  283. clearskies/decorators/public.py +0 -11
  284. clearskies/decorators/response_headers.py +0 -10
  285. clearskies/decorators/return_raw_response.py +0 -9
  286. clearskies/decorators/schema.py +0 -10
  287. clearskies/decorators/secret_bearer.py +0 -24
  288. clearskies/decorators/security_headers.py +0 -10
  289. clearskies/di/standard_dependencies.py +0 -151
  290. clearskies/handlers/__init__.py +0 -41
  291. clearskies/handlers/advanced_search.py +0 -271
  292. clearskies/handlers/base.py +0 -479
  293. clearskies/handlers/callable.py +0 -192
  294. clearskies/handlers/create.py +0 -35
  295. clearskies/handlers/crud_by_method.py +0 -18
  296. clearskies/handlers/database_connector.py +0 -32
  297. clearskies/handlers/delete.py +0 -61
  298. clearskies/handlers/exceptions/__init__.py +0 -5
  299. clearskies/handlers/exceptions/not_found.py +0 -3
  300. clearskies/handlers/get.py +0 -156
  301. clearskies/handlers/health_check.py +0 -59
  302. clearskies/handlers/input_processing.py +0 -79
  303. clearskies/handlers/list.py +0 -530
  304. clearskies/handlers/mygrations.py +0 -82
  305. clearskies/handlers/request_method_routing.py +0 -47
  306. clearskies/handlers/restful_api.py +0 -218
  307. clearskies/handlers/routing.py +0 -62
  308. clearskies/handlers/schema_helper.py +0 -128
  309. clearskies/handlers/simple_routing.py +0 -206
  310. clearskies/handlers/simple_routing_route.py +0 -197
  311. clearskies/handlers/simple_search.py +0 -136
  312. clearskies/handlers/update.py +0 -102
  313. clearskies/handlers/write.py +0 -193
  314. clearskies/input_requirements/__init__.py +0 -78
  315. clearskies/input_requirements/after.py +0 -36
  316. clearskies/input_requirements/before.py +0 -36
  317. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  318. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  319. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  320. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  321. clearskies/input_requirements/maximum_length.py +0 -19
  322. clearskies/input_requirements/maximum_value.py +0 -19
  323. clearskies/input_requirements/minimum_length.py +0 -22
  324. clearskies/input_requirements/minimum_value.py +0 -19
  325. clearskies/input_requirements/required.py +0 -23
  326. clearskies/input_requirements/requirement.py +0 -25
  327. clearskies/input_requirements/time_delta.py +0 -38
  328. clearskies/input_requirements/unique.py +0 -18
  329. clearskies/mocks/__init__.py +0 -7
  330. clearskies/mocks/input_output.py +0 -124
  331. clearskies/mocks/models.py +0 -142
  332. clearskies/models.py +0 -350
  333. clearskies/security_headers/base.py +0 -12
  334. clearskies/tests/simple_api/models/__init__.py +0 -2
  335. clearskies/tests/simple_api/models/status.py +0 -23
  336. clearskies/tests/simple_api/models/user.py +0 -21
  337. clearskies/tests/simple_api/users_api.py +0 -64
  338. {clear_skies-1.22.30.dist-info → clear_skies-2.0.0.dist-info}/LICENSE +0 -0
  339. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  340. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  341. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  342. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  343. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  344. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -0,0 +1,158 @@
1
+ import re
2
+ from typing import Any, Callable
3
+
4
+ import clearskies.typing
5
+ from clearskies import configs, parameters_to_properties
6
+ from clearskies.columns.string import String
7
+
8
+
9
+ class Phone(String):
10
+ """
11
+ A string column that stores a phone number.
12
+
13
+ The main difference between this and a plain string column is that this will validate that the string contains
14
+ a phone number (containing only digits, dashes, spaces, plus sign, and parenthesis) of the appropriate length.
15
+ When persisting the value to the backend, this column removes all non-digit characters.
16
+
17
+ If you also set the usa_only flag to true then it will also validate that it is a valid US number containing
18
+ 9 digits and, optionally, a leading `1`. Example:
19
+
20
+ ```python
21
+ import clearskies
22
+
23
+
24
+ class User(clearskies.Model):
25
+ id_column_name = "id"
26
+ backend = clearskies.backends.MemoryBackend()
27
+
28
+ id = clearskies.columns.Uuid()
29
+ name = clearskies.columns.String()
30
+ phone = clearskies.columns.Phone(usa_only=True)
31
+
32
+
33
+ wsgi = clearskies.contexts.WsgiRef(
34
+ clearskies.endpoints.Create(
35
+ User,
36
+ writeable_column_names=["name", "phone"],
37
+ readable_column_names=["id", "name", "phone"],
38
+ ),
39
+ )
40
+ wsgi()
41
+ ```
42
+
43
+ Which you can invoke:
44
+
45
+ ```bash
46
+ $ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "+1 (555) 451-1234"}' | jq
47
+ {
48
+ "status": "success",
49
+ "error": "",
50
+ "data": {
51
+ "id": "e2b4bdad-b70f-4d44-a94c-0e265868b4d2",
52
+ "name": "John Doe",
53
+ "phone": "15554511234"
54
+ },
55
+ "pagination": {},
56
+ "input_errors": {}
57
+ }
58
+
59
+ $ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "555 451-1234"}' | jq
60
+ {
61
+ "status": "success",
62
+ "error": "",
63
+ "data": {
64
+ "id": "aea34022-4b75-4eed-ac92-65fa4f4511ae",
65
+ "name": "John Doe",
66
+ "phone": "5554511234"
67
+ },
68
+ "pagination": {},
69
+ "input_errors": {}
70
+ }
71
+
72
+
73
+ $ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "555 451-12341"}' | jq
74
+ {
75
+ "status": "input_errors",
76
+ "error": "",
77
+ "data": [],
78
+ "pagination": {},
79
+ "input_errors": {
80
+ "phone": "Invalid phone number"
81
+ }
82
+ }
83
+
84
+ $ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "1-2-3-4 asdf"}' | jq
85
+ {
86
+ "status": "input_errors",
87
+ "error": "",
88
+ "data": [],
89
+ "pagination": {},
90
+ "input_errors": {
91
+ "phone": "Invalid phone number"
92
+ }
93
+ }
94
+ ```
95
+ """
96
+
97
+ """ Whether or not to allow non-USA numbers. """
98
+ usa_only = configs.Boolean(default=True)
99
+ _descriptor_config_map = None
100
+
101
+ @parameters_to_properties.parameters_to_properties
102
+ def __init__(
103
+ self,
104
+ usa_only: bool = True,
105
+ default: str | None = None,
106
+ setable: str | Callable[..., str] | None = None,
107
+ is_readable: bool = True,
108
+ is_writeable: bool = True,
109
+ is_searchable: bool = True,
110
+ is_temporary: bool = False,
111
+ validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
112
+ on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
113
+ on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
114
+ on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
115
+ created_by_source_type: str = "",
116
+ created_by_source_key: str = "",
117
+ created_by_source_strict: bool = True,
118
+ ):
119
+ pass
120
+
121
+ def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
122
+ if not data.get(self.name):
123
+ return data
124
+
125
+ # phone numbers are stored as only digits.
126
+ return {**data, **{self.name: re.sub(r"\D", "", data[self.name])}}
127
+
128
+ def input_error_for_value(self, value: str, operator: str | None = None) -> str:
129
+ if type(value) != str:
130
+ return f"Value must be a string for {self.name}"
131
+
132
+ # we'll allow spaces, dashes, parenthesis, dashes, and plus signs.
133
+ # if there is anything else then it's not a valid phone number.
134
+ # However, we don't do more detailed validation, because I'm too lazy to
135
+ # figure out what is and is not a valid phone number, especially when
136
+ # you get to the world of international numbers.
137
+ if re.search(r"[^\d \-()+]", value):
138
+ return "Invalid phone number"
139
+
140
+ # for some final validation (especially US numbers) work only with the digits.
141
+ value = re.sub(r"\D", "", value)
142
+
143
+ if len(value) > 15:
144
+ return "Invalid phone number"
145
+
146
+ # we can't be too short unless we're doing a fuzzy search
147
+ if len(value) < 10 and operator and operator.lower() != "like":
148
+ return "Invalid phone number"
149
+
150
+ if self.usa_only:
151
+ if len(value) > 11:
152
+ return "Invalid phone number"
153
+ if value[0] == "1" and len(value) != 11:
154
+ return "Invalid phone number"
155
+ if value[0] != "1" and len(value) != 10:
156
+ return "Invalid phone number"
157
+
158
+ return ""
@@ -0,0 +1,91 @@
1
+ from typing import Callable
2
+
3
+ import clearskies.typing
4
+ from clearskies import configs, parameters_to_properties
5
+ from clearskies.columns.string import String
6
+
7
+
8
+ class Select(String):
9
+ """
10
+ A string column but, when writeable via an endpoint, only specific values are allowed.
11
+
12
+ Note: the allowed values are case sensitive.
13
+
14
+ ```python
15
+ import clearskies
16
+
17
+
18
+ class Order(clearskies.Model):
19
+ id_column_name = "id"
20
+ backend = clearskies.backends.MemoryBackend()
21
+
22
+ id = clearskies.columns.Uuid()
23
+ total = clearskies.columns.Float()
24
+ status = clearskies.columns.Select(["Open", "Processing", "Shipped", "Complete"])
25
+
26
+
27
+ wsgi = clearskies.contexts.WsgiRef(
28
+ clearskies.endpoints.Create(
29
+ Order,
30
+ writeable_column_names=["total", "status"],
31
+ readable_column_names=["id", "total", "status"],
32
+ ),
33
+ )
34
+ wsgi()
35
+ ```
36
+
37
+ And when invoked:
38
+
39
+ ```bash
40
+ $ curl http://localhost:8080 -d '{"total": 125, "status": "Open"}' | jq
41
+ {
42
+ "status": "success",
43
+ "error": "",
44
+ "data": {
45
+ "id": "22f2c950-6519-4d8e-9084-013455449b07",
46
+ "total": 125.0,
47
+ "status": "Open"
48
+ },
49
+ "pagination": {},
50
+ "input_errors": {}
51
+ }
52
+
53
+ $ curl http://localhost:8080 -d '{"total": 125, "status": "huh"}' | jq
54
+ {
55
+ "status": "input_errors",
56
+ "error": "",
57
+ "data": [],
58
+ "pagination": {},
59
+ "input_errors": {
60
+ "status": "Invalid value for status"
61
+ }
62
+ }
63
+ ```
64
+ """
65
+
66
+ """ The allowed values. """
67
+ allowed_values = configs.StringList(required=True)
68
+ _descriptor_config_map = None
69
+
70
+ @parameters_to_properties.parameters_to_properties
71
+ def __init__(
72
+ self,
73
+ allowed_values: list[str],
74
+ default: str | None = None,
75
+ setable: str | Callable[..., str] | None = None,
76
+ is_readable: bool = True,
77
+ is_writeable: bool = True,
78
+ is_searchable: bool = True,
79
+ is_temporary: bool = False,
80
+ validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
81
+ on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
82
+ on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
83
+ on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
84
+ created_by_source_type: str = "",
85
+ created_by_source_key: str = "",
86
+ created_by_source_strict: bool = True,
87
+ ):
88
+ pass
89
+
90
+ def input_error_for_value(self, value: str, operator: str | None = None) -> str:
91
+ return f"Invalid value for {self.name}" if value not in self.allowed_values else ""
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Self, overload
4
+
5
+ from clearskies.column import Column
6
+
7
+ if TYPE_CHECKING:
8
+ from clearskies import Model
9
+
10
+
11
+ class String(Column):
12
+ """
13
+ A simple string column.
14
+
15
+ ```python
16
+ import clearskies
17
+
18
+
19
+ class Pet(clearskies.Model):
20
+ id_column_name = "id"
21
+ backend = clearskies.backends.MemoryBackend()
22
+
23
+ id = clearskies.columns.Uuid()
24
+ name = clearskies.columns.String()
25
+
26
+
27
+ wsgi = clearskies.contexts.WsgiRef(
28
+ clearskies.endpoints.Create(
29
+ Pet,
30
+ writeable_column_names=["name"],
31
+ readable_column_names=["id", "name"],
32
+ ),
33
+ )
34
+ wsgi()
35
+ ```
36
+
37
+ And when invoked:
38
+
39
+ ```bash
40
+ $ curl http://localhost:8080 -d '{"name": "Spot"}' | jq
41
+ {
42
+ "status": "success",
43
+ "error": "",
44
+ "data": {
45
+ "id": "e5b8417f-91bc-4fe5-9b64-04f571a7b10a",
46
+ "name": "Spot"
47
+ },
48
+ "pagination": {},
49
+ "input_errors": {}
50
+ }
51
+
52
+ $ curl http://localhost:8080 -d '{"name": 10}' | jq
53
+ {
54
+ "status": "input_errors",
55
+ "error": "",
56
+ "data": [],
57
+ "pagination": {},
58
+ "input_errors": {
59
+ "name": "value should be a string"
60
+ }
61
+ }
62
+
63
+ ```
64
+ """
65
+
66
+ _allowed_search_operators = ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null", "like"]
67
+ _descriptor_config_map = None
68
+
69
+ @overload
70
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
71
+ pass
72
+
73
+ @overload
74
+ def __get__(self, instance: Model, cls: type[Model]) -> str:
75
+ pass
76
+
77
+ def __get__(self, instance, cls):
78
+ if instance is None:
79
+ self.model_class = cls
80
+ return self
81
+
82
+ # this makes sure we're initialized
83
+ if "name" not in self._config: # type: ignore
84
+ instance.get_columns()
85
+
86
+ if self.name not in instance._data:
87
+ return None # type: ignore
88
+
89
+ if self.name not in instance._transformed_data:
90
+ instance._transformed_data[self.name] = self.from_backend(instance._data[self.name])
91
+
92
+ return instance._transformed_data[self.name]
93
+
94
+ def __set__(self, instance: Model, value: str) -> None:
95
+ instance._next_data[self.name] = value
96
+
97
+ def input_error_for_value(self, value: str, operator: str | None = None) -> str:
98
+ return "value should be a string" if type(value) != str else ""
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ from typing import TYPE_CHECKING, Any, Callable, Self, Type, overload
5
+
6
+ import clearskies.parameters_to_properties
7
+ import clearskies.typing
8
+ from clearskies import configs
9
+ from clearskies.columns.datetime import Datetime
10
+
11
+ if TYPE_CHECKING:
12
+ from clearskies import Model
13
+
14
+
15
+ class Timestamp(Datetime):
16
+ """
17
+ A timestamp column.
18
+
19
+ The difference between this and the datetime column is that this stores the datetime
20
+ as a standard unix timestamp - the number of seconds since the unix epoch.
21
+
22
+ Also, this **always** assumes the timezone for the timestamp is UTC
23
+
24
+ ```python
25
+ import datetime
26
+ import clearskies
27
+
28
+
29
+ class Pet(clearskies.Model):
30
+ id_column_name = "id"
31
+ backend = clearskies.backends.MemoryBackend()
32
+
33
+ id = clearskies.columns.Uuid()
34
+ name = clearskies.columns.String()
35
+ last_fed = clearskies.columns.Timestamp()
36
+
37
+
38
+ def demo_timestamp(utcnow: datetime.datetime, pets: Pet) -> dict[str, str | int]:
39
+ pet = pets.create({
40
+ "name": "Spot",
41
+ "last_fed": utcnow,
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
+ @clearskies.parameters_to_properties.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: clearskies.typing.validator | list[clearskies.typing.validator] = [],
90
+ on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
91
+ on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
92
+ on_change_save_finished: clearskies.typing.action | list[clearskies.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) or isinstance(value, 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
+ instance._next_data[self.name] = value
152
+
153
+ def input_error_for_value(self, value: str, operator: str | None = None) -> str:
154
+ if not isinstance(value, int):
155
+ return f"'{self.name}' must be an integer"
156
+ return ""
157
+
158
+ def values_match(self, value_1, value_2):
159
+ """Compare two values to see if they are the same."""
160
+ return value_1 == value_2
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ import clearskies.di
7
+ import clearskies.parameters_to_properties
8
+ import clearskies.typing
9
+ from clearskies import configs
10
+ from clearskies.columns.datetime import Datetime
11
+
12
+ if TYPE_CHECKING:
13
+ from clearskies import Model
14
+
15
+
16
+ class Updated(Datetime):
17
+ """
18
+ The updated column records the time that a record is created or updated.
19
+
20
+ Note that this will always populate the column anytime the model is created or updated.
21
+ You don't have to provide the timestamp yourself and you should never expose it as
22
+ a writeable column through an endpoint (in fact, you can't).
23
+
24
+ ```python
25
+ import clearskies
26
+ import time
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
+ created = clearskies.columns.Created()
36
+ updated = clearskies.columns.Updated()
37
+
38
+
39
+ def test_updated(my_models: MyModel) -> MyModel:
40
+ my_model = my_models.create({"name": "Jane"})
41
+ updated_column_after_create = my_model.updated
42
+
43
+ time.sleep(2)
44
+
45
+ my_model.save({"name": "Susan"})
46
+
47
+ return {
48
+ "updated_column_after_create": updated_column_after_create.isoformat(),
49
+ "updated_column_at_end": my_model.updated.isoformat(),
50
+ "difference_in_seconds": (my_model.updated - updated_column_after_create).total_seconds(),
51
+ }
52
+
53
+
54
+ cli = clearskies.contexts.Cli(clearskies.endpoints.Callable(test_updated), classes=[MyModel])
55
+ cli()
56
+ ```
57
+
58
+ And when invoked:
59
+
60
+ ```bash
61
+ $ ./test.py | jq
62
+ {
63
+ "status": "success",
64
+ "error": "",
65
+ "data": {
66
+ "updated_column_after_create": "2025-05-18T19:28:46+00:00",
67
+ "updated_column_at_end": "2025-05-18T19:28:48+00:00",
68
+ "difference_in_seconds": 2.0
69
+ },
70
+ "pagination": {},
71
+ "input_errors": {}
72
+ }
73
+ ```
74
+
75
+ Note that the `updated` column was set both when the record was first created and when it was updated,
76
+ so there is a two second difference between them (since we slept for two seconds).
77
+
78
+ """
79
+
80
+ """
81
+ Created fields are never writeable because they always set the created time automatically.
82
+ """
83
+ is_writeable = configs.Boolean(default=False)
84
+ _descriptor_config_map = None
85
+
86
+ now = clearskies.di.inject.Now()
87
+
88
+ @clearskies.parameters_to_properties.parameters_to_properties
89
+ def __init__(
90
+ self,
91
+ in_utc: bool = True,
92
+ date_format: str = "%Y-%m-%d %H:%M:%S",
93
+ backend_default: str = "0000-00-00 00:00:00",
94
+ is_readable: bool = True,
95
+ is_searchable: bool = True,
96
+ is_temporary: bool = False,
97
+ on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
98
+ on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
99
+ on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
100
+ ):
101
+ pass
102
+
103
+ def pre_save(self, data: dict[str, Any], model: Model) -> dict[str, Any]:
104
+ now = self.now
105
+ if self.timezone_aware:
106
+ now = now.astimezone(self.timezone)
107
+ data = {**data, self.name: now}
108
+ if self.on_change_pre_save:
109
+ data = self.execute_actions_with_data(self.on_change_pre_save, model, data)
110
+ return data
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ import clearskies.di
6
+ import clearskies.parameters_to_properties
7
+ import clearskies.typing
8
+ from clearskies import configs
9
+ from clearskies.columns.string import String
10
+
11
+ if TYPE_CHECKING:
12
+ from clearskies import Model
13
+
14
+
15
+ class Uuid(String):
16
+ """
17
+ Populates the column with a UUID upon record creation.
18
+
19
+ This column really just has a very specific purpose: ids!
20
+
21
+ When used, it will automatically populate the column with a random UUID upon record creation.
22
+ It is not a writeable column, which means that you cannot expose it for write operations via an endpoint.
23
+
24
+ ```python
25
+ import clearskies
26
+
27
+
28
+ class MyModel(clearskies.Model):
29
+ backend = clearskies.backends.MemoryBackend()
30
+ id_column_name = "id"
31
+
32
+ id = clearskies.columns.Uuid()
33
+ name = clearskies.columns.String()
34
+
35
+
36
+ wsgi = clearskies.contexts.WsgiRef(
37
+ clearskies.endpoints.Create(
38
+ MyModel,
39
+ writeable_column_names=["name"],
40
+ readable_column_names=["id", "name"],
41
+ ),
42
+ )
43
+ wsgi()
44
+ ```
45
+
46
+ and when invoked:
47
+
48
+ ```bash
49
+ $ curl http://localhost:8080 -d '{"name": "John Doe"}' | jq
50
+ {
51
+ "status": "success",
52
+ "error": "",
53
+ "data": {
54
+ "id": "d4f23106-b48a-4dc5-9bf6-df61f6ca54f7",
55
+ "name": "John Doe"
56
+ },
57
+ "pagination": {},
58
+ "input_errors": {}
59
+ }
60
+ ```
61
+ """
62
+
63
+ is_writeable = configs.Boolean(default=False)
64
+ _descriptor_config_map = None
65
+
66
+ uuid = clearskies.di.inject.Uuid()
67
+
68
+ @clearskies.parameters_to_properties.parameters_to_properties
69
+ def __init__(
70
+ self,
71
+ is_readable: bool = True,
72
+ is_searchable: bool = True,
73
+ is_temporary: bool = False,
74
+ on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
75
+ on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
76
+ on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
77
+ ):
78
+ pass
79
+
80
+ def pre_save(self, data: dict[str, Any], model: Model) -> dict[str, Any]:
81
+ if model:
82
+ return data
83
+ data = {**data, self.name: str(self.uuid.uuid4())}
84
+ if self.on_change_pre_save:
85
+ data = self.execute_actions_with_data(self.on_change_pre_save, model, data)
86
+ return data