clear-skies 1.22.31__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of clear-skies might be problematic. Click here for more details.

Files changed (345) hide show
  1. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/METADATA +12 -14
  2. clear_skies-2.0.1.dist-info/RECORD +249 -0
  3. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/WHEEL +1 -1
  4. clearskies/__init__.py +42 -25
  5. clearskies/action.py +7 -0
  6. clearskies/authentication/__init__.py +8 -41
  7. clearskies/authentication/authentication.py +46 -0
  8. clearskies/authentication/authorization.py +8 -9
  9. clearskies/authentication/authorization_pass_through.py +11 -9
  10. clearskies/authentication/jwks.py +133 -58
  11. clearskies/authentication/public.py +3 -38
  12. clearskies/authentication/secret_bearer.py +516 -54
  13. clearskies/autodoc/formats/oai3_json/__init__.py +1 -1
  14. clearskies/autodoc/formats/oai3_json/oai3_json.py +9 -7
  15. clearskies/autodoc/formats/oai3_json/parameter.py +6 -3
  16. clearskies/autodoc/formats/oai3_json/request.py +7 -5
  17. clearskies/autodoc/formats/oai3_json/response.py +7 -4
  18. clearskies/autodoc/formats/oai3_json/schema/object.py +4 -1
  19. clearskies/autodoc/request/__init__.py +2 -0
  20. clearskies/autodoc/request/header.py +4 -6
  21. clearskies/autodoc/request/json_body.py +4 -6
  22. clearskies/autodoc/request/parameter.py +8 -0
  23. clearskies/autodoc/request/request.py +7 -4
  24. clearskies/autodoc/request/url_parameter.py +4 -6
  25. clearskies/autodoc/request/url_path.py +4 -6
  26. clearskies/autodoc/schema/__init__.py +4 -2
  27. clearskies/autodoc/schema/array.py +5 -6
  28. clearskies/autodoc/schema/boolean.py +4 -10
  29. clearskies/autodoc/schema/date.py +0 -3
  30. clearskies/autodoc/schema/datetime.py +1 -4
  31. clearskies/autodoc/schema/double.py +0 -3
  32. clearskies/autodoc/schema/enum.py +4 -2
  33. clearskies/autodoc/schema/integer.py +4 -9
  34. clearskies/autodoc/schema/long.py +0 -3
  35. clearskies/autodoc/schema/number.py +4 -9
  36. clearskies/autodoc/schema/object.py +5 -7
  37. clearskies/autodoc/schema/password.py +0 -3
  38. clearskies/autodoc/schema/schema.py +11 -0
  39. clearskies/autodoc/schema/string.py +4 -10
  40. clearskies/backends/__init__.py +55 -20
  41. clearskies/backends/api_backend.py +1100 -284
  42. clearskies/backends/backend.py +53 -84
  43. clearskies/backends/cursor_backend.py +236 -186
  44. clearskies/backends/memory_backend.py +519 -226
  45. clearskies/backends/secrets_backend.py +75 -31
  46. clearskies/column.py +1229 -0
  47. clearskies/columns/__init__.py +71 -0
  48. clearskies/columns/audit.py +205 -0
  49. clearskies/columns/belongs_to_id.py +483 -0
  50. clearskies/columns/belongs_to_model.py +128 -0
  51. clearskies/columns/belongs_to_self.py +105 -0
  52. clearskies/columns/boolean.py +109 -0
  53. clearskies/columns/category_tree.py +275 -0
  54. clearskies/columns/category_tree_ancestors.py +51 -0
  55. clearskies/columns/category_tree_children.py +127 -0
  56. clearskies/columns/category_tree_descendants.py +48 -0
  57. clearskies/columns/created.py +94 -0
  58. clearskies/columns/created_by_authorization_data.py +116 -0
  59. clearskies/columns/created_by_header.py +99 -0
  60. clearskies/columns/created_by_ip.py +92 -0
  61. clearskies/columns/created_by_routing_data.py +96 -0
  62. clearskies/columns/created_by_user_agent.py +92 -0
  63. clearskies/columns/date.py +230 -0
  64. clearskies/columns/datetime.py +278 -0
  65. clearskies/columns/email.py +76 -0
  66. clearskies/columns/float.py +149 -0
  67. clearskies/columns/has_many.py +505 -0
  68. clearskies/columns/has_many_self.py +56 -0
  69. clearskies/columns/has_one.py +14 -0
  70. clearskies/columns/integer.py +156 -0
  71. clearskies/columns/json.py +122 -0
  72. clearskies/columns/many_to_many_ids.py +333 -0
  73. clearskies/columns/many_to_many_ids_with_data.py +270 -0
  74. clearskies/columns/many_to_many_models.py +154 -0
  75. clearskies/columns/many_to_many_pivots.py +133 -0
  76. clearskies/columns/phone.py +158 -0
  77. clearskies/columns/select.py +91 -0
  78. clearskies/columns/string.py +98 -0
  79. clearskies/columns/timestamp.py +160 -0
  80. clearskies/columns/updated.py +110 -0
  81. clearskies/columns/uuid.py +86 -0
  82. clearskies/configs/README.md +105 -0
  83. clearskies/configs/__init__.py +162 -0
  84. clearskies/configs/actions.py +43 -0
  85. clearskies/configs/any.py +13 -0
  86. clearskies/configs/any_dict.py +22 -0
  87. clearskies/configs/any_dict_or_callable.py +23 -0
  88. clearskies/configs/authentication.py +23 -0
  89. clearskies/configs/authorization.py +23 -0
  90. clearskies/configs/boolean.py +16 -0
  91. clearskies/configs/boolean_or_callable.py +18 -0
  92. clearskies/configs/callable_config.py +18 -0
  93. clearskies/configs/columns.py +34 -0
  94. clearskies/configs/conditions.py +30 -0
  95. clearskies/configs/config.py +24 -0
  96. clearskies/configs/datetime.py +18 -0
  97. clearskies/configs/datetime_or_callable.py +19 -0
  98. clearskies/configs/endpoint.py +23 -0
  99. clearskies/configs/endpoint_list.py +28 -0
  100. clearskies/configs/float.py +16 -0
  101. clearskies/configs/float_or_callable.py +18 -0
  102. clearskies/configs/integer.py +16 -0
  103. clearskies/configs/integer_or_callable.py +18 -0
  104. clearskies/configs/joins.py +30 -0
  105. clearskies/configs/list_any_dict.py +30 -0
  106. clearskies/configs/list_any_dict_or_callable.py +31 -0
  107. clearskies/configs/model_class.py +35 -0
  108. clearskies/configs/model_column.py +65 -0
  109. clearskies/configs/model_columns.py +56 -0
  110. clearskies/configs/model_destination_name.py +25 -0
  111. clearskies/configs/model_to_id_column.py +43 -0
  112. clearskies/configs/readable_model_column.py +9 -0
  113. clearskies/configs/readable_model_columns.py +9 -0
  114. clearskies/configs/schema.py +23 -0
  115. clearskies/configs/searchable_model_columns.py +9 -0
  116. clearskies/configs/security_headers.py +39 -0
  117. clearskies/configs/select.py +26 -0
  118. clearskies/configs/select_list.py +47 -0
  119. clearskies/configs/string.py +29 -0
  120. clearskies/configs/string_dict.py +32 -0
  121. clearskies/configs/string_list.py +32 -0
  122. clearskies/configs/string_list_or_callable.py +35 -0
  123. clearskies/configs/string_or_callable.py +18 -0
  124. clearskies/configs/timedelta.py +18 -0
  125. clearskies/configs/timezone.py +18 -0
  126. clearskies/configs/url.py +23 -0
  127. clearskies/configs/validators.py +45 -0
  128. clearskies/configs/writeable_model_column.py +9 -0
  129. clearskies/configs/writeable_model_columns.py +9 -0
  130. clearskies/configurable.py +76 -0
  131. clearskies/contexts/__init__.py +8 -8
  132. clearskies/contexts/cli.py +8 -41
  133. clearskies/contexts/context.py +91 -56
  134. clearskies/contexts/wsgi.py +16 -29
  135. clearskies/contexts/wsgi_ref.py +53 -0
  136. clearskies/di/__init__.py +10 -7
  137. clearskies/di/additional_config.py +115 -4
  138. clearskies/di/additional_config_auto_import.py +12 -0
  139. clearskies/di/di.py +742 -121
  140. clearskies/di/inject/__init__.py +23 -0
  141. clearskies/di/inject/by_class.py +21 -0
  142. clearskies/di/inject/by_name.py +18 -0
  143. clearskies/di/inject/di.py +13 -0
  144. clearskies/di/inject/environment.py +14 -0
  145. clearskies/di/inject/input_output.py +20 -0
  146. clearskies/di/inject/now.py +13 -0
  147. clearskies/di/inject/requests.py +13 -0
  148. clearskies/di/inject/secrets.py +14 -0
  149. clearskies/di/inject/utcnow.py +13 -0
  150. clearskies/di/inject/uuid.py +15 -0
  151. clearskies/di/injectable.py +29 -0
  152. clearskies/di/injectable_properties.py +131 -0
  153. clearskies/end.py +183 -0
  154. clearskies/endpoint.py +1310 -0
  155. clearskies/endpoint_group.py +310 -0
  156. clearskies/endpoints/__init__.py +23 -0
  157. clearskies/endpoints/advanced_search.py +526 -0
  158. clearskies/endpoints/callable.py +388 -0
  159. clearskies/endpoints/create.py +202 -0
  160. clearskies/endpoints/delete.py +139 -0
  161. clearskies/endpoints/get.py +275 -0
  162. clearskies/endpoints/health_check.py +181 -0
  163. clearskies/endpoints/list.py +573 -0
  164. clearskies/endpoints/restful_api.py +427 -0
  165. clearskies/endpoints/simple_search.py +286 -0
  166. clearskies/endpoints/update.py +190 -0
  167. clearskies/environment.py +5 -3
  168. clearskies/exceptions/__init__.py +17 -0
  169. clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
  170. clearskies/exceptions/moved_permanently.py +3 -0
  171. clearskies/exceptions/moved_temporarily.py +3 -0
  172. clearskies/exceptions/not_found.py +2 -0
  173. clearskies/functional/__init__.py +2 -2
  174. clearskies/functional/routing.py +92 -0
  175. clearskies/functional/string.py +19 -11
  176. clearskies/functional/validations.py +61 -9
  177. clearskies/input_outputs/__init__.py +9 -7
  178. clearskies/input_outputs/cli.py +130 -142
  179. clearskies/input_outputs/exceptions/__init__.py +1 -1
  180. clearskies/input_outputs/headers.py +45 -0
  181. clearskies/input_outputs/input_output.py +91 -122
  182. clearskies/input_outputs/programmatic.py +69 -0
  183. clearskies/input_outputs/wsgi.py +23 -38
  184. clearskies/model.py +984 -183
  185. clearskies/parameters_to_properties.py +31 -0
  186. clearskies/query/__init__.py +12 -0
  187. clearskies/query/condition.py +223 -0
  188. clearskies/query/join.py +136 -0
  189. clearskies/query/query.py +196 -0
  190. clearskies/query/sort.py +27 -0
  191. clearskies/schema.py +82 -0
  192. clearskies/secrets/__init__.py +3 -31
  193. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
  194. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
  195. clearskies/secrets/akeyless.py +88 -147
  196. clearskies/secrets/secrets.py +8 -8
  197. clearskies/security_header.py +15 -0
  198. clearskies/security_headers/__init__.py +8 -8
  199. clearskies/security_headers/cache_control.py +47 -110
  200. clearskies/security_headers/cors.py +40 -95
  201. clearskies/security_headers/csp.py +76 -151
  202. clearskies/security_headers/hsts.py +14 -16
  203. clearskies/test_base.py +8 -0
  204. clearskies/typing.py +11 -0
  205. clearskies/validator.py +37 -0
  206. clearskies/validators/__init__.py +33 -0
  207. clearskies/validators/after_column.py +62 -0
  208. clearskies/validators/before_column.py +13 -0
  209. clearskies/validators/in_the_future.py +32 -0
  210. clearskies/validators/in_the_future_at_least.py +11 -0
  211. clearskies/validators/in_the_future_at_most.py +10 -0
  212. clearskies/validators/in_the_past.py +32 -0
  213. clearskies/validators/in_the_past_at_least.py +10 -0
  214. clearskies/validators/in_the_past_at_most.py +10 -0
  215. clearskies/validators/maximum_length.py +26 -0
  216. clearskies/validators/maximum_value.py +29 -0
  217. clearskies/validators/minimum_length.py +26 -0
  218. clearskies/validators/minimum_value.py +29 -0
  219. clearskies/validators/required.py +35 -0
  220. clearskies/validators/timedelta.py +59 -0
  221. clearskies/validators/unique.py +31 -0
  222. clear_skies-1.22.31.dist-info/RECORD +0 -214
  223. clearskies/application.py +0 -29
  224. clearskies/authentication/auth0_jwks.py +0 -118
  225. clearskies/authentication/auth_exception.py +0 -2
  226. clearskies/authentication/jwks_jwcrypto.py +0 -51
  227. clearskies/backends/api_get_only_backend.py +0 -48
  228. clearskies/backends/example_backend.py +0 -43
  229. clearskies/backends/file_backend.py +0 -48
  230. clearskies/backends/json_backend.py +0 -7
  231. clearskies/backends/restful_api_advanced_search_backend.py +0 -103
  232. clearskies/binding_config.py +0 -16
  233. clearskies/column_types/__init__.py +0 -203
  234. clearskies/column_types/audit.py +0 -249
  235. clearskies/column_types/belongs_to.py +0 -271
  236. clearskies/column_types/boolean.py +0 -60
  237. clearskies/column_types/category_tree.py +0 -304
  238. clearskies/column_types/column.py +0 -373
  239. clearskies/column_types/created.py +0 -26
  240. clearskies/column_types/created_by_authorization_data.py +0 -26
  241. clearskies/column_types/created_by_header.py +0 -24
  242. clearskies/column_types/created_by_ip.py +0 -17
  243. clearskies/column_types/created_by_routing_data.py +0 -25
  244. clearskies/column_types/created_by_user_agent.py +0 -17
  245. clearskies/column_types/created_micro.py +0 -26
  246. clearskies/column_types/datetime.py +0 -109
  247. clearskies/column_types/datetime_micro.py +0 -12
  248. clearskies/column_types/email.py +0 -18
  249. clearskies/column_types/float.py +0 -43
  250. clearskies/column_types/has_many.py +0 -179
  251. clearskies/column_types/has_one.py +0 -60
  252. clearskies/column_types/integer.py +0 -41
  253. clearskies/column_types/json.py +0 -25
  254. clearskies/column_types/many_to_many.py +0 -278
  255. clearskies/column_types/many_to_many_with_data.py +0 -162
  256. clearskies/column_types/phone.py +0 -48
  257. clearskies/column_types/select.py +0 -11
  258. clearskies/column_types/string.py +0 -24
  259. clearskies/column_types/timestamp.py +0 -73
  260. clearskies/column_types/updated.py +0 -26
  261. clearskies/column_types/updated_micro.py +0 -26
  262. clearskies/column_types/uuid.py +0 -25
  263. clearskies/columns.py +0 -123
  264. clearskies/condition_parser.py +0 -172
  265. clearskies/contexts/build_context.py +0 -54
  266. clearskies/contexts/convert_to_application.py +0 -190
  267. clearskies/contexts/extract_handler.py +0 -37
  268. clearskies/contexts/test.py +0 -94
  269. clearskies/decorators/__init__.py +0 -41
  270. clearskies/decorators/allow_non_json_bodies.py +0 -9
  271. clearskies/decorators/auth0_jwks.py +0 -22
  272. clearskies/decorators/authorization.py +0 -10
  273. clearskies/decorators/binding_classes.py +0 -9
  274. clearskies/decorators/binding_modules.py +0 -9
  275. clearskies/decorators/bindings.py +0 -9
  276. clearskies/decorators/create.py +0 -10
  277. clearskies/decorators/delete.py +0 -10
  278. clearskies/decorators/docs.py +0 -14
  279. clearskies/decorators/get.py +0 -10
  280. clearskies/decorators/jwks.py +0 -26
  281. clearskies/decorators/merge.py +0 -124
  282. clearskies/decorators/patch.py +0 -10
  283. clearskies/decorators/post.py +0 -10
  284. clearskies/decorators/public.py +0 -11
  285. clearskies/decorators/response_headers.py +0 -10
  286. clearskies/decorators/return_raw_response.py +0 -9
  287. clearskies/decorators/schema.py +0 -10
  288. clearskies/decorators/secret_bearer.py +0 -24
  289. clearskies/decorators/security_headers.py +0 -10
  290. clearskies/di/standard_dependencies.py +0 -151
  291. clearskies/handlers/__init__.py +0 -41
  292. clearskies/handlers/advanced_search.py +0 -271
  293. clearskies/handlers/base.py +0 -479
  294. clearskies/handlers/callable.py +0 -192
  295. clearskies/handlers/create.py +0 -35
  296. clearskies/handlers/crud_by_method.py +0 -18
  297. clearskies/handlers/database_connector.py +0 -32
  298. clearskies/handlers/delete.py +0 -61
  299. clearskies/handlers/exceptions/__init__.py +0 -5
  300. clearskies/handlers/exceptions/not_found.py +0 -3
  301. clearskies/handlers/get.py +0 -156
  302. clearskies/handlers/health_check.py +0 -59
  303. clearskies/handlers/input_processing.py +0 -79
  304. clearskies/handlers/list.py +0 -530
  305. clearskies/handlers/mygrations.py +0 -82
  306. clearskies/handlers/request_method_routing.py +0 -47
  307. clearskies/handlers/restful_api.py +0 -218
  308. clearskies/handlers/routing.py +0 -62
  309. clearskies/handlers/schema_helper.py +0 -128
  310. clearskies/handlers/simple_routing.py +0 -206
  311. clearskies/handlers/simple_routing_route.py +0 -197
  312. clearskies/handlers/simple_search.py +0 -136
  313. clearskies/handlers/update.py +0 -102
  314. clearskies/handlers/write.py +0 -193
  315. clearskies/input_requirements/__init__.py +0 -78
  316. clearskies/input_requirements/after.py +0 -36
  317. clearskies/input_requirements/before.py +0 -36
  318. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  319. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  320. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  321. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  322. clearskies/input_requirements/maximum_length.py +0 -19
  323. clearskies/input_requirements/maximum_value.py +0 -19
  324. clearskies/input_requirements/minimum_length.py +0 -22
  325. clearskies/input_requirements/minimum_value.py +0 -19
  326. clearskies/input_requirements/required.py +0 -23
  327. clearskies/input_requirements/requirement.py +0 -25
  328. clearskies/input_requirements/time_delta.py +0 -38
  329. clearskies/input_requirements/unique.py +0 -18
  330. clearskies/mocks/__init__.py +0 -7
  331. clearskies/mocks/input_output.py +0 -124
  332. clearskies/mocks/models.py +0 -142
  333. clearskies/models.py +0 -350
  334. clearskies/security_headers/base.py +0 -12
  335. clearskies/tests/simple_api/models/__init__.py +0 -2
  336. clearskies/tests/simple_api/models/status.py +0 -23
  337. clearskies/tests/simple_api/models/user.py +0 -21
  338. clearskies/tests/simple_api/users_api.py +0 -64
  339. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/LICENSE +0 -0
  340. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  341. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  342. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  343. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  344. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  345. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -0,0 +1,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