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,56 @@
1
+ import clearskies.typing
2
+ from clearskies import parameters_to_properties
3
+ from clearskies.columns.has_many import HasMany
4
+
5
+
6
+ class HasManySelf(HasMany):
7
+ """
8
+ This is just like the HasMany column, but is used when the model references itself.
9
+
10
+ This exists because a model can't refer to itself inside it's own class definition. There are
11
+ workarounds, but having this class is usually quicker for the developer.
12
+
13
+ The main difference between this and HasMany is that you don't have to provide the child class.
14
+ Also, the name of the column that contains the id of the parent becomes `parent_id` by default,
15
+ rather than basing it on the name of the model. This is done because, since the model is also
16
+ the child, using the name of the model in the name of the column id is often ambiguous.
17
+
18
+ See also BelongsToSelf.
19
+ """
20
+
21
+ _descriptor_config_map = None
22
+
23
+ @parameters_to_properties.parameters_to_properties
24
+ def __init__(
25
+ self,
26
+ foreign_column_name: str | None = None,
27
+ readable_child_columns: list[str] = [],
28
+ where: clearskies.typing.condition | list[clearskies.typing.condition] = [],
29
+ is_readable: bool = True,
30
+ on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
31
+ on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
32
+ on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
33
+ ):
34
+ pass
35
+
36
+ def finalize_configuration(self, model_class, name) -> None:
37
+ """
38
+ Finalize and check the configuration.
39
+
40
+ This is an external trigger called by the model class when the model class is ready.
41
+ The reason it exists here instead of in the constructor is because some columns are tightly
42
+ connected to the model class, and can't validate configuration until they know what the model is.
43
+ Therefore, we need the model involved, and the only way for a property to know what class it is
44
+ in is if the parent class checks in (which is what happens here).
45
+ """
46
+ self.child_model_class = model_class
47
+ has_value = False
48
+ try:
49
+ has_value = bool(self.foreign_column_name)
50
+ except KeyError:
51
+ pass
52
+
53
+ if not has_value:
54
+ self.foreign_column_name = "parent_id"
55
+
56
+ super().finalize_configuration(model_class, name)
@@ -0,0 +1,14 @@
1
+ from clearskies.columns.has_many import HasMany
2
+
3
+
4
+ class HasOne(HasMany):
5
+ """
6
+ This operates exactly like the HasMany relationship, except it assumes there is only ever one child.
7
+
8
+ The only real difference between this and HasMany is that the HasMany column type will return a list
9
+ of models, while this returns the first model.
10
+ """
11
+
12
+ _descriptor_config_map = None
13
+
14
+ pass
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Callable, Self, overload
4
+
5
+ import clearskies.parameters_to_properties
6
+ import clearskies.typing
7
+ from clearskies import configs
8
+ from clearskies.autodoc.schema import Integer as AutoDocInteger
9
+ from clearskies.autodoc.schema import Schema as AutoDocSchema
10
+ from clearskies.column import Column
11
+ from clearskies.query import Condition
12
+
13
+ if TYPE_CHECKING:
14
+ from clearskies import Model
15
+
16
+
17
+ class Integer(Column):
18
+ """
19
+ A column that stores integer data.
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
+ age = clearskies.columns.Integer()
31
+
32
+
33
+ wsgi = clearskies.contexts.WsgiRef(
34
+ clearskies.endpoints.Create(
35
+ MyModel,
36
+ writeable_column_names=["age"],
37
+ readable_column_names=["id", "age"],
38
+ ),
39
+ classes=[MyModel],
40
+ )
41
+ wsgi()
42
+ ```
43
+
44
+ And when invoked:
45
+
46
+ ```bash
47
+ $ curl 'http://localhost:8080' -d '{"age":20}' | jq
48
+ {
49
+ "status": "success",
50
+ "error": "",
51
+ "data": {
52
+ "id": "6ea74719-a65f-45ae-b6a3-641ce682ed25",
53
+ "age": 20
54
+ },
55
+ "pagination": {},
56
+ "input_errors": {}
57
+ }
58
+
59
+ $ curl 'http://localhost:8080' -d '{"age":"asdf"}' | jq
60
+ {
61
+ "status": "input_errors",
62
+ "error": "",
63
+ "data": [],
64
+ "pagination": {},
65
+ "input_errors": {
66
+ "age": "value should be an integer"
67
+ }
68
+ }
69
+ ```
70
+ """
71
+
72
+ default = configs.Integer(default=None) # type: ignore
73
+ setable = configs.IntegerOrCallable(default=None) # type: ignore
74
+ _allowed_search_operators = ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null"]
75
+
76
+ auto_doc_class: type[AutoDocSchema] = AutoDocInteger
77
+
78
+ _descriptor_config_map = None
79
+
80
+ @clearskies.parameters_to_properties.parameters_to_properties
81
+ def __init__(
82
+ self,
83
+ default: int | None = None,
84
+ setable: int | Callable[..., int] | 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
+ @overload
100
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
101
+ pass
102
+
103
+ @overload
104
+ def __get__(self, instance: Model, cls: type[Model]) -> int:
105
+ pass
106
+
107
+ def __get__(self, instance, cls):
108
+ if instance is None:
109
+ self.model_class = cls
110
+ return self
111
+
112
+ value = super().__get__(instance, cls)
113
+ return None if value is None else int(value)
114
+
115
+ def __set__(self, instance, value: int) -> None:
116
+ instance._next_data[self.name] = value
117
+
118
+ def from_backend(self, value) -> int | None:
119
+ return None if value is None else int(value)
120
+
121
+ def to_backend(self, data):
122
+ if self.name not in data or data[self.name] is None:
123
+ return data
124
+
125
+ return {**data, self.name: int(data[self.name])}
126
+
127
+ def input_error_for_value(self, value, operator=None):
128
+ try:
129
+ int(value)
130
+ except ValueError:
131
+ return "value should be an integer"
132
+ return ""
133
+
134
+ def equals(self, value: int) -> Condition:
135
+ return super().equals(value)
136
+
137
+ def spaceship(self, value: int) -> Condition:
138
+ return super().spaceship(value)
139
+
140
+ def not_equals(self, value: int) -> Condition:
141
+ return super().not_equals(value)
142
+
143
+ def less_than_equals(self, value: int) -> Condition:
144
+ return super().less_than_equals(value)
145
+
146
+ def greater_than_equals(self, value: int) -> Condition:
147
+ return super().greater_than_equals(value)
148
+
149
+ def less_than(self, value: int) -> Condition:
150
+ return super().less_than(value)
151
+
152
+ def greater_than(self, value: int) -> Condition:
153
+ return super().greater_than(value)
154
+
155
+ def is_in(self, values: list[int]) -> Condition:
156
+ return super().is_in(values)
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import TYPE_CHECKING, Any, Callable, Self, overload
5
+
6
+ import clearskies.parameters_to_properties
7
+ import clearskies.typing
8
+ from clearskies import configs
9
+ from clearskies.column import Column
10
+
11
+ if TYPE_CHECKING:
12
+ from clearskies import Model
13
+
14
+
15
+ class Json(Column):
16
+ """
17
+ A column to store generic data.
18
+
19
+ ```python
20
+ import clearskies
21
+
22
+
23
+ class MyModel(clearskies.Model):
24
+ backend = clearskies.backends.MemoryBackend()
25
+ id_column_name = "id"
26
+
27
+ id = clearskies.columns.Uuid()
28
+ my_data = clearskies.columns.Json()
29
+
30
+
31
+ wsgi = clearskies.contexts.WsgiRef(
32
+ clearskies.endpoints.Create(
33
+ MyModel,
34
+ writeable_column_names=["my_data"],
35
+ readable_column_names=["id", "my_data"],
36
+ ),
37
+ classes=[MyModel],
38
+ )
39
+ wsgi()
40
+ ```
41
+
42
+ And when invoked:
43
+
44
+ ```bash
45
+ $ curl 'http://localhost:8080' -d '{"my_data":{"count":[1,2,3,4,{"thing":true}]}}' | jq
46
+ {
47
+ "status": "success",
48
+ "error": "",
49
+ "data": {
50
+ "id": "63cbd5e7-a198-4424-bd35-3890075a2a5e",
51
+ "my_data": {
52
+ "count": [
53
+ 1,
54
+ 2,
55
+ 3,
56
+ 4,
57
+ {
58
+ "thing": true
59
+ }
60
+ ]
61
+ }
62
+ },
63
+ "pagination": {},
64
+ "input_errors": {}
65
+ }
66
+ ```
67
+
68
+ Note that there is no attempt to check the shape of the input passed into a JSON column.
69
+
70
+ """
71
+
72
+ is_searchable = configs.Boolean(default=False)
73
+ _descriptor_config_map = None
74
+
75
+ @clearskies.parameters_to_properties.parameters_to_properties
76
+ def __init__(
77
+ self,
78
+ default: dict[str, Any] | None = None,
79
+ setable: dict[str, Any] | Callable[..., dict[str, Any]] | None = None,
80
+ is_readable: bool = True,
81
+ is_writeable: bool = True,
82
+ is_temporary: bool = False,
83
+ validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
84
+ on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
85
+ on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
86
+ on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
87
+ created_by_source_type: str = "",
88
+ created_by_source_key: str = "",
89
+ created_by_source_strict: bool = True,
90
+ ):
91
+ pass
92
+
93
+ @overload
94
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
95
+ pass
96
+
97
+ @overload
98
+ def __get__(self, instance: Model, cls: type[Model]) -> dict[str, Any]:
99
+ pass
100
+
101
+ def __get__(self, instance, cls):
102
+ return super().__get__(instance, cls)
103
+
104
+ def __set__(self, instance, value: dict[str, Any]) -> None:
105
+ instance._next_data[self.name] = value
106
+
107
+ def from_backend(self, value) -> dict[str, Any] | list[Any] | None:
108
+ if type(value) == list or type(value) == dict:
109
+ return value
110
+ if not value:
111
+ return None
112
+ try:
113
+ return json.loads(value)
114
+ except json.JSONDecodeError:
115
+ return None
116
+
117
+ def to_backend(self, data):
118
+ if self.name not in data or data[self.name] is None:
119
+ return data
120
+
121
+ value = data[self.name]
122
+ return {**data, self.name: value if isinstance(value, str) else json.dumps(value)}
@@ -0,0 +1,333 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import OrderedDict
4
+ from typing import TYPE_CHECKING, Any, Callable, Self, overload
5
+
6
+ import clearskies.parameters_to_properties
7
+ import clearskies.typing
8
+ from clearskies import configs
9
+ from clearskies.autodoc.schema import Array as AutoDocArray
10
+ from clearskies.autodoc.schema import String as AutoDocString
11
+ from clearskies.column import Column
12
+ from clearskies.functional import string
13
+
14
+ if TYPE_CHECKING:
15
+ from clearskies import Column, Model
16
+
17
+
18
+ class ManyToManyIds(Column):
19
+ """
20
+ A column that represents a many-to-many relationship.
21
+
22
+ This is different from belongs to/has many because with those, every child has only one parent. With a many-to-many
23
+ relationship, both models can have multiple relatives from the other model class. In order to support this, it's necessary
24
+ to have a third model (the pivot model) that records the relationships. In general this table just needs three
25
+ columns: it's own id, and then one column for each other model to store the id of the related records.
26
+ You can specify the names of these columns but it also follows the standard naming convention by default:
27
+ take the class name, convert it to snake case, and append `_id`.
28
+
29
+ Note, there is a variation on this (`ManyToManyIdsWithData`) where additional data is stored in the pivot table
30
+ to record information about the relationship.
31
+
32
+ This column is writeable. You would set it to a list of ids from the related model that denotes which
33
+ records it is related to.
34
+
35
+ The following example shows usage. Normally the many-to-many column exists for both related models, but in this
36
+ specific example it only exists for one of the models. This is done so that the example can fit in a single file
37
+ and therefore be easy to demonstrate. In order to have both models reference eachother, you have to use model
38
+ references to avoid circular imports. There are examples of doing this in the `BelongsTo` column class.
39
+
40
+ ```python
41
+ import clearskies
42
+
43
+
44
+ class ThingyToWidget(clearskies.Model):
45
+ id_column_name = "id"
46
+ backend = clearskies.backends.MemoryBackend()
47
+
48
+ id = clearskies.columns.Uuid()
49
+ # these could also be belongs to relationships, but the pivot model
50
+ # is rarely used directly, so I'm being lazy to avoid having to use
51
+ # model references.
52
+ thingy_id = clearskies.columns.String()
53
+ widget_id = clearskies.columns.String()
54
+
55
+
56
+ class Thingy(clearskies.Model):
57
+ id_column_name = "id"
58
+ backend = clearskies.backends.MemoryBackend()
59
+
60
+ id = clearskies.columns.Uuid()
61
+ name = clearskies.columns.String()
62
+
63
+
64
+ class Widget(clearskies.Model):
65
+ id_column_name = "id"
66
+ backend = clearskies.backends.MemoryBackend()
67
+
68
+ id = clearskies.columns.Uuid()
69
+ name = clearskies.columns.String()
70
+ thingy_ids = clearskies.columns.ManyToManyIds(
71
+ related_model_class=Thingy,
72
+ pivot_model_class=ThingyToWidget,
73
+ )
74
+ thingies = clearskies.columns.ManyToManyModels("thingy_ids")
75
+
76
+
77
+ def my_application(widgets: Widget, thingies: Thingy):
78
+ thing_1 = thingies.create({"name": "Thing 1"})
79
+ thing_2 = thingies.create({"name": "Thing 2"})
80
+ thing_3 = thingies.create({"name": "Thing 3"})
81
+ widget = widgets.create({
82
+ "name": "Widget 1",
83
+ "thingy_ids": [thing_1.id, thing_2.id],
84
+ })
85
+
86
+ # remove an item by saving without it's id in place
87
+ widget.save({"thingy_ids": [thing.id for thing in widget.thingies if thing.id != thing_1.id]})
88
+
89
+ # add an item by saving and adding the new id
90
+ widget.save({"thingy_ids": [*widget.thingy_ids, thing_3.id]})
91
+
92
+ return widget.thingies
93
+
94
+
95
+ cli = clearskies.contexts.Cli(
96
+ clearskies.endpoints.Callable(
97
+ my_application,
98
+ model_class=Thingy,
99
+ return_records=True,
100
+ readable_column_names=["id", "name"],
101
+ ),
102
+ classes=[Widget, Thingy, ThingyToWidget],
103
+ )
104
+
105
+ if __name__ == "__main__":
106
+ cli()
107
+ ```
108
+
109
+ And when executed:
110
+
111
+ ```json
112
+ {
113
+ "status": "success",
114
+ "error": "",
115
+ "data": [
116
+ {"id": "741bc838-c694-4624-9fc2-e9032f6cb962", "name": "Thing 2"},
117
+ {"id": "1808a8ef-e288-44e6-9fed-46e3b0df057f", "name": "Thing 3"},
118
+ ],
119
+ "pagination": {},
120
+ "input_errors": {},
121
+ }
122
+ ```
123
+
124
+ Of course, you can also create or remove individual relationships by using the pivot model directly,
125
+ as shown in these partial code snippets:
126
+
127
+ ```python
128
+ def add_items(thingy_to_widgets):
129
+ thingy_to_widgets.create({
130
+ "thingy_id": "some_id",
131
+ "widget_id": "other_id",
132
+ })
133
+
134
+
135
+ def remove_item(thingy_to_widgets):
136
+ thingy_to_widgets.where("thingy_id=some_id").where("widget_id=other_id").first().delete()
137
+ ```
138
+ """
139
+
140
+ """ The model class for the model that we are related to. """
141
+ related_model_class = configs.ModelClass(required=True)
142
+
143
+ """ The model class for the pivot table - the table used to record connections between ourselves and our related table. """
144
+ pivot_model_class = configs.ModelClass(required=True)
145
+
146
+ """
147
+ The name of the column in the pivot table that contains the id of records from the model with this column.
148
+
149
+ A default name is created by taking the model class name, converting it to snake case, and then appending `_id`.
150
+ If you name your columns according to this standard then you don't have to specify this column name.
151
+ """
152
+ own_column_name_in_pivot = configs.ModelToIdColumn(model_column_config_name="pivot_model_class")
153
+
154
+ """
155
+ The name of the column in the pivot table that contains the id of records from the related table.
156
+
157
+ A default name is created by taking the name of the related model class, converting it to snake case, and then
158
+ appending `_id`. If you name your columns according to this standard then you don't have to specify this column
159
+ name.
160
+ """
161
+ related_column_name_in_pivot = configs.ModelToIdColumn(
162
+ model_column_config_name="pivot_model_class", source_model_class_config_name="related_model_class"
163
+ )
164
+
165
+ """ The name of the pivot table."""
166
+ pivot_table_name = configs.ModelDestinationName("pivot_model_class")
167
+
168
+ """ The list of columns to be loaded from the related models when we are converted to JSON. """
169
+ readable_related_column_names = configs.ReadableModelColumns("related_model_class")
170
+
171
+ default = configs.StringList(default=None) # type: ignore
172
+ setable = configs.StringListOrCallable(default=None) # type: ignore
173
+ is_searchable = configs.Boolean(default=False)
174
+ _descriptor_config_map = None
175
+
176
+ @clearskies.parameters_to_properties.parameters_to_properties
177
+ def __init__(
178
+ self,
179
+ related_model_class,
180
+ pivot_model_class,
181
+ own_column_name_in_pivot: str = "",
182
+ related_column_name_in_pivot: str = "",
183
+ readable_related_column_names: list[str] = [],
184
+ default: list[str] = [],
185
+ setable: list[str] | Callable[..., list[str]] = [],
186
+ is_readable: bool = True,
187
+ is_writeable: bool = True,
188
+ is_temporary: bool = False,
189
+ validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
190
+ on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
191
+ on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
192
+ on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
193
+ created_by_source_type: str = "",
194
+ created_by_source_key: str = "",
195
+ created_by_source_strict: bool = True,
196
+ ):
197
+ pass
198
+
199
+ def finalize_configuration(self, model_class: type, name: str) -> None:
200
+ """
201
+ Finalize and check the configuration.
202
+
203
+ This is an external trigger called by the model class when the model class is ready.
204
+ The reason it exists here instead of in the constructor is because some columns are tightly
205
+ connected to the model class, and can't validate configuration until they know what the model is.
206
+ Therefore, we need the model involved, and the only way for a property to know what class it is
207
+ in is if the parent class checks in (which is what happens here).
208
+ """
209
+ self.model_class = model_class
210
+ self.name = name
211
+ getattr(self.__class__, "pivot_table_name").finalize_and_validate_configuration(self)
212
+ own_column_name_in_pivot_config = getattr(self.__class__, "own_column_name_in_pivot")
213
+ own_column_name_in_pivot_config.source_model_class = model_class
214
+ own_column_name_in_pivot_config.finalize_and_validate_configuration(self)
215
+ self.finalize_and_validate_configuration()
216
+
217
+ def to_backend(self, data):
218
+ # we can't persist our mapping data to the database directly, so remove anything here
219
+ # and take care of things in post_save
220
+ if self.name in data:
221
+ del data[self.name]
222
+ return data
223
+
224
+ @property
225
+ def pivot_model(self) -> Model:
226
+ return self.di.build(self.pivot_model_class, cache=True)
227
+
228
+ @property
229
+ def related_model(self) -> Model:
230
+ return self.di.build(self.related_model_class, cache=True)
231
+
232
+ @property
233
+ def related_columns(self) -> dict[str, Column]:
234
+ return self.related_model.get_columns()
235
+
236
+ @property
237
+ def pivot_columns(self) -> dict[str, Column]:
238
+ return self.pivot_model.get_columns()
239
+
240
+ @overload
241
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
242
+ pass
243
+
244
+ @overload
245
+ def __get__(self, instance: Model, cls: type[Model]) -> list[str | int]:
246
+ pass
247
+
248
+ def __get__(self, instance, cls):
249
+ if instance is None:
250
+ self.model_class = cls
251
+ return self
252
+
253
+ # this makes sure we're initialized
254
+ if "name" not in self._config: # type: ignore
255
+ instance.get_columns()
256
+
257
+ related_id_column_name = self.related_model_class.id_column_name
258
+ return [getattr(model, related_id_column_name) for model in self.get_related_models(instance)]
259
+
260
+ def __set__(self, instance, value: list[str | int]) -> None:
261
+ instance._next_data[self.name] = value
262
+
263
+ def get_related_models(self, model: Model) -> Model:
264
+ related_column_name_in_pivot = self.related_column_name_in_pivot
265
+ own_column_name_in_pivot = self.own_column_name_in_pivot
266
+ pivot_table_name = self.pivot_table_name
267
+ related_id_column_name = self.related_model_class.id_column_name
268
+ model_id = getattr(model, self.model_class.id_column_name)
269
+ model = self.related_model
270
+ join = f"JOIN {pivot_table_name} ON {pivot_table_name}.{related_column_name_in_pivot}={model.destination_name()}.{related_id_column_name}"
271
+ related_models = model.join(join).where(f"{pivot_table_name}.{own_column_name_in_pivot}={model_id}")
272
+ return related_models
273
+
274
+ def get_pivot_models(self, model: Model) -> Model:
275
+ return self.pivot_model.where(
276
+ f"{self.own_column_name_in_pivot}=" + getattr(model, self.model_class.id_column_name)
277
+ )
278
+
279
+ def post_save(self, data: dict[str, Any], model: clearskies.model.Model, id: int | str) -> None:
280
+ # if our incoming data is not in the data array or is None, then nothing has been set and we do not want
281
+ # to make any changes
282
+ if self.name not in data or data[self.name] is None:
283
+ return
284
+
285
+ # figure out what ids need to be created or deleted from the pivot table.
286
+ if not model:
287
+ old_ids = set()
288
+ else:
289
+ old_ids = set(self.__get__(model, model.__class__))
290
+
291
+ new_ids = set(data[self.name])
292
+ to_delete = old_ids - new_ids
293
+ to_create = new_ids - old_ids
294
+ pivot_model = self.pivot_model
295
+ related_column_name_in_pivot = self.related_column_name_in_pivot
296
+ if to_delete:
297
+ for model_to_delete in pivot_model.where(
298
+ f"{related_column_name_in_pivot} IN ({','.join(map(str, to_delete))})"
299
+ ):
300
+ model_to_delete.delete()
301
+ if to_create:
302
+ own_column_name_in_pivot = self.own_column_name_in_pivot
303
+ for id_to_create in to_create:
304
+ pivot_model.create(
305
+ {
306
+ related_column_name_in_pivot: id_to_create,
307
+ own_column_name_in_pivot: id,
308
+ }
309
+ )
310
+
311
+ super().post_save(data, model, id)
312
+
313
+ def add_search(self, model: Model, value: str, operator: str = "", relationship_reference: str = "") -> Model:
314
+ related_column_name_in_pivot = self.related_column_name_in_pivot
315
+ own_column_name_in_pivot = self.own_column_name_in_pivot
316
+ own_id_column_name = self.model_class.id_column_name
317
+ pivot_table_name = self.pivot_table_name
318
+ my_table_name = self.model_class.destination_name()
319
+ related_table_name = self.related_model.destination_name()
320
+ join_pivot = f"JOIN {pivot_table_name} ON {pivot_table_name}.{own_column_name_in_pivot}={my_table_name}.{own_id_column_name}"
321
+ # no reason we can't support searching by both an id or a list of ids
322
+ values = value if type(value) == list else [value]
323
+ search = " IN (" + ", ".join([str(val) for val in value]) + ")"
324
+ return model.join(join_pivot).where(f"{pivot_table_name}.{related_column_name_in_pivot}{search}")
325
+
326
+ def to_json(self, model: Model) -> dict[str, Any]:
327
+ related_id_column_name = self.related_model_class.id_column_name
328
+ records = [getattr(related, related_id_column_name) for related in self.get_related_models(model)]
329
+ return {self.name: records}
330
+
331
+ def documentation(self, name: str | None = None, example: str | None = None, value: str | None = None):
332
+ related_id_column_name = self.related_model_class.id_column_name
333
+ return AutoDocArray(name if name is not None else self.name, AutoDocString(related_id_column_name))