clear-skies 1.19.22__py3-none-any.whl → 2.0.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. clear_skies-2.0.23.dist-info/METADATA +76 -0
  2. clear_skies-2.0.23.dist-info/RECORD +265 -0
  3. {clear_skies-1.19.22.dist-info → clear_skies-2.0.23.dist-info}/WHEEL +1 -1
  4. clearskies/__init__.py +37 -21
  5. clearskies/action.py +7 -0
  6. clearskies/authentication/__init__.py +9 -38
  7. clearskies/authentication/authentication.py +44 -0
  8. clearskies/authentication/authorization.py +14 -8
  9. clearskies/authentication/authorization_pass_through.py +22 -0
  10. clearskies/authentication/jwks.py +135 -58
  11. clearskies/authentication/public.py +3 -26
  12. clearskies/authentication/secret_bearer.py +515 -44
  13. clearskies/autodoc/formats/oai3_json/__init__.py +2 -2
  14. clearskies/autodoc/formats/oai3_json/oai3_json.py +11 -9
  15. clearskies/autodoc/formats/oai3_json/parameter.py +6 -3
  16. clearskies/autodoc/formats/oai3_json/request.py +7 -5
  17. clearskies/autodoc/formats/oai3_json/response.py +7 -4
  18. clearskies/autodoc/formats/oai3_json/schema/object.py +10 -1
  19. clearskies/autodoc/request/__init__.py +2 -0
  20. clearskies/autodoc/request/header.py +4 -6
  21. clearskies/autodoc/request/json_body.py +4 -6
  22. clearskies/autodoc/request/parameter.py +8 -0
  23. clearskies/autodoc/request/request.py +16 -4
  24. clearskies/autodoc/request/url_parameter.py +4 -6
  25. clearskies/autodoc/request/url_path.py +4 -6
  26. clearskies/autodoc/schema/__init__.py +4 -2
  27. clearskies/autodoc/schema/array.py +5 -6
  28. clearskies/autodoc/schema/boolean.py +4 -10
  29. clearskies/autodoc/schema/date.py +0 -3
  30. clearskies/autodoc/schema/datetime.py +1 -4
  31. clearskies/autodoc/schema/double.py +0 -3
  32. clearskies/autodoc/schema/enum.py +4 -2
  33. clearskies/autodoc/schema/integer.py +4 -9
  34. clearskies/autodoc/schema/long.py +0 -3
  35. clearskies/autodoc/schema/number.py +4 -9
  36. clearskies/autodoc/schema/object.py +5 -7
  37. clearskies/autodoc/schema/password.py +0 -3
  38. clearskies/autodoc/schema/schema.py +11 -0
  39. clearskies/autodoc/schema/string.py +4 -10
  40. clearskies/backends/__init__.py +56 -17
  41. clearskies/backends/api_backend.py +1128 -166
  42. clearskies/backends/backend.py +54 -85
  43. clearskies/backends/cursor_backend.py +246 -191
  44. clearskies/backends/memory_backend.py +514 -208
  45. clearskies/backends/secrets_backend.py +68 -31
  46. clearskies/column.py +1221 -0
  47. clearskies/columns/__init__.py +71 -0
  48. clearskies/columns/audit.py +306 -0
  49. clearskies/columns/belongs_to_id.py +478 -0
  50. clearskies/columns/belongs_to_model.py +129 -0
  51. clearskies/columns/belongs_to_self.py +109 -0
  52. clearskies/columns/boolean.py +110 -0
  53. clearskies/columns/category_tree.py +273 -0
  54. clearskies/columns/category_tree_ancestors.py +51 -0
  55. clearskies/columns/category_tree_children.py +126 -0
  56. clearskies/columns/category_tree_descendants.py +48 -0
  57. clearskies/columns/created.py +92 -0
  58. clearskies/columns/created_by_authorization_data.py +114 -0
  59. clearskies/columns/created_by_header.py +103 -0
  60. clearskies/columns/created_by_ip.py +90 -0
  61. clearskies/columns/created_by_routing_data.py +102 -0
  62. clearskies/columns/created_by_user_agent.py +89 -0
  63. clearskies/columns/date.py +232 -0
  64. clearskies/columns/datetime.py +284 -0
  65. clearskies/columns/email.py +78 -0
  66. clearskies/columns/float.py +149 -0
  67. clearskies/columns/has_many.py +529 -0
  68. clearskies/columns/has_many_self.py +62 -0
  69. clearskies/columns/has_one.py +21 -0
  70. clearskies/columns/integer.py +158 -0
  71. clearskies/columns/json.py +126 -0
  72. clearskies/columns/many_to_many_ids.py +335 -0
  73. clearskies/columns/many_to_many_ids_with_data.py +274 -0
  74. clearskies/columns/many_to_many_models.py +156 -0
  75. clearskies/columns/many_to_many_pivots.py +132 -0
  76. clearskies/columns/phone.py +162 -0
  77. clearskies/columns/select.py +95 -0
  78. clearskies/columns/string.py +102 -0
  79. clearskies/columns/timestamp.py +164 -0
  80. clearskies/columns/updated.py +107 -0
  81. clearskies/columns/uuid.py +83 -0
  82. clearskies/configs/README.md +105 -0
  83. clearskies/configs/__init__.py +170 -0
  84. clearskies/configs/actions.py +43 -0
  85. clearskies/configs/any.py +15 -0
  86. clearskies/configs/any_dict.py +24 -0
  87. clearskies/configs/any_dict_or_callable.py +25 -0
  88. clearskies/configs/authentication.py +23 -0
  89. clearskies/configs/authorization.py +23 -0
  90. clearskies/configs/boolean.py +18 -0
  91. clearskies/configs/boolean_or_callable.py +20 -0
  92. clearskies/configs/callable_config.py +20 -0
  93. clearskies/configs/columns.py +34 -0
  94. clearskies/configs/conditions.py +30 -0
  95. clearskies/configs/config.py +26 -0
  96. clearskies/configs/datetime.py +20 -0
  97. clearskies/configs/datetime_or_callable.py +21 -0
  98. clearskies/configs/email.py +10 -0
  99. clearskies/configs/email_list.py +17 -0
  100. clearskies/configs/email_list_or_callable.py +17 -0
  101. clearskies/configs/email_or_email_list_or_callable.py +59 -0
  102. clearskies/configs/endpoint.py +23 -0
  103. clearskies/configs/endpoint_list.py +29 -0
  104. clearskies/configs/float.py +18 -0
  105. clearskies/configs/float_or_callable.py +20 -0
  106. clearskies/configs/headers.py +28 -0
  107. clearskies/configs/integer.py +18 -0
  108. clearskies/configs/integer_or_callable.py +20 -0
  109. clearskies/configs/joins.py +30 -0
  110. clearskies/configs/list_any_dict.py +32 -0
  111. clearskies/configs/list_any_dict_or_callable.py +33 -0
  112. clearskies/configs/model_class.py +35 -0
  113. clearskies/configs/model_column.py +67 -0
  114. clearskies/configs/model_columns.py +58 -0
  115. clearskies/configs/model_destination_name.py +26 -0
  116. clearskies/configs/model_to_id_column.py +45 -0
  117. clearskies/configs/readable_model_column.py +11 -0
  118. clearskies/configs/readable_model_columns.py +11 -0
  119. clearskies/configs/schema.py +23 -0
  120. clearskies/configs/searchable_model_columns.py +11 -0
  121. clearskies/configs/security_headers.py +39 -0
  122. clearskies/configs/select.py +28 -0
  123. clearskies/configs/select_list.py +49 -0
  124. clearskies/configs/string.py +31 -0
  125. clearskies/configs/string_dict.py +34 -0
  126. clearskies/configs/string_list.py +47 -0
  127. clearskies/configs/string_list_or_callable.py +48 -0
  128. clearskies/configs/string_or_callable.py +18 -0
  129. clearskies/configs/timedelta.py +20 -0
  130. clearskies/configs/timezone.py +20 -0
  131. clearskies/configs/url.py +25 -0
  132. clearskies/configs/validators.py +45 -0
  133. clearskies/configs/writeable_model_column.py +11 -0
  134. clearskies/configs/writeable_model_columns.py +11 -0
  135. clearskies/configurable.py +78 -0
  136. clearskies/contexts/__init__.py +8 -8
  137. clearskies/contexts/cli.py +129 -43
  138. clearskies/contexts/context.py +93 -56
  139. clearskies/contexts/wsgi.py +79 -33
  140. clearskies/contexts/wsgi_ref.py +87 -0
  141. clearskies/cursors/__init__.py +7 -0
  142. clearskies/cursors/cursor.py +166 -0
  143. clearskies/cursors/from_environment/__init__.py +5 -0
  144. clearskies/cursors/from_environment/mysql.py +51 -0
  145. clearskies/cursors/from_environment/postgresql.py +49 -0
  146. clearskies/cursors/from_environment/sqlite.py +35 -0
  147. clearskies/cursors/mysql.py +61 -0
  148. clearskies/cursors/postgresql.py +61 -0
  149. clearskies/cursors/sqlite.py +62 -0
  150. clearskies/decorators.py +33 -0
  151. clearskies/decorators.pyi +10 -0
  152. clearskies/di/__init__.py +11 -7
  153. clearskies/di/additional_config.py +117 -3
  154. clearskies/di/additional_config_auto_import.py +12 -0
  155. clearskies/di/di.py +717 -126
  156. clearskies/di/inject/__init__.py +23 -0
  157. clearskies/di/inject/akeyless_sdk.py +16 -0
  158. clearskies/di/inject/by_class.py +24 -0
  159. clearskies/di/inject/by_name.py +22 -0
  160. clearskies/di/inject/di.py +16 -0
  161. clearskies/di/inject/environment.py +15 -0
  162. clearskies/di/inject/input_output.py +19 -0
  163. clearskies/di/inject/now.py +16 -0
  164. clearskies/di/inject/requests.py +16 -0
  165. clearskies/di/inject/secrets.py +15 -0
  166. clearskies/di/inject/utcnow.py +16 -0
  167. clearskies/di/inject/uuid.py +16 -0
  168. clearskies/di/injectable.py +32 -0
  169. clearskies/di/injectable_properties.py +131 -0
  170. clearskies/end.py +219 -0
  171. clearskies/endpoint.py +1303 -0
  172. clearskies/endpoint_group.py +333 -0
  173. clearskies/endpoints/__init__.py +25 -0
  174. clearskies/endpoints/advanced_search.py +519 -0
  175. clearskies/endpoints/callable.py +382 -0
  176. clearskies/endpoints/create.py +201 -0
  177. clearskies/endpoints/delete.py +133 -0
  178. clearskies/endpoints/get.py +267 -0
  179. clearskies/endpoints/health_check.py +181 -0
  180. clearskies/endpoints/list.py +567 -0
  181. clearskies/endpoints/restful_api.py +417 -0
  182. clearskies/endpoints/schema.py +185 -0
  183. clearskies/endpoints/simple_search.py +279 -0
  184. clearskies/endpoints/update.py +188 -0
  185. clearskies/environment.py +7 -3
  186. clearskies/exceptions/__init__.py +19 -0
  187. clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
  188. clearskies/exceptions/missing_dependency.py +2 -0
  189. clearskies/exceptions/moved_permanently.py +3 -0
  190. clearskies/exceptions/moved_temporarily.py +3 -0
  191. clearskies/functional/__init__.py +2 -2
  192. clearskies/functional/json.py +47 -0
  193. clearskies/functional/routing.py +92 -0
  194. clearskies/functional/string.py +19 -11
  195. clearskies/functional/validations.py +61 -9
  196. clearskies/input_outputs/__init__.py +9 -7
  197. clearskies/input_outputs/cli.py +135 -152
  198. clearskies/input_outputs/exceptions/__init__.py +6 -1
  199. clearskies/input_outputs/headers.py +54 -0
  200. clearskies/input_outputs/input_output.py +77 -123
  201. clearskies/input_outputs/programmatic.py +62 -0
  202. clearskies/input_outputs/wsgi.py +36 -48
  203. clearskies/model.py +1894 -199
  204. clearskies/query/__init__.py +12 -0
  205. clearskies/query/condition.py +228 -0
  206. clearskies/query/join.py +136 -0
  207. clearskies/query/query.py +193 -0
  208. clearskies/query/sort.py +27 -0
  209. clearskies/schema.py +82 -0
  210. clearskies/secrets/__init__.py +4 -31
  211. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
  212. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
  213. clearskies/secrets/akeyless.py +421 -155
  214. clearskies/secrets/exceptions/__init__.py +7 -1
  215. clearskies/secrets/exceptions/not_found_error.py +2 -0
  216. clearskies/secrets/exceptions/permissions_error.py +2 -0
  217. clearskies/secrets/secrets.py +12 -11
  218. clearskies/security_header.py +17 -0
  219. clearskies/security_headers/__init__.py +8 -8
  220. clearskies/security_headers/cache_control.py +47 -109
  221. clearskies/security_headers/cors.py +38 -92
  222. clearskies/security_headers/csp.py +76 -150
  223. clearskies/security_headers/hsts.py +14 -15
  224. clearskies/typing.py +11 -0
  225. clearskies/validator.py +36 -0
  226. clearskies/validators/__init__.py +33 -0
  227. clearskies/validators/after_column.py +61 -0
  228. clearskies/validators/before_column.py +15 -0
  229. clearskies/validators/in_the_future.py +29 -0
  230. clearskies/validators/in_the_future_at_least.py +13 -0
  231. clearskies/validators/in_the_future_at_most.py +12 -0
  232. clearskies/validators/in_the_past.py +29 -0
  233. clearskies/validators/in_the_past_at_least.py +12 -0
  234. clearskies/validators/in_the_past_at_most.py +12 -0
  235. clearskies/validators/maximum_length.py +25 -0
  236. clearskies/validators/maximum_value.py +28 -0
  237. clearskies/validators/minimum_length.py +25 -0
  238. clearskies/validators/minimum_value.py +28 -0
  239. clearskies/{input_requirements → validators}/required.py +18 -9
  240. clearskies/validators/timedelta.py +58 -0
  241. clearskies/validators/unique.py +28 -0
  242. clear_skies-1.19.22.dist-info/METADATA +0 -46
  243. clear_skies-1.19.22.dist-info/RECORD +0 -206
  244. clearskies/application.py +0 -29
  245. clearskies/authentication/auth0_jwks.py +0 -118
  246. clearskies/authentication/auth_exception.py +0 -2
  247. clearskies/authentication/jwks_jwcrypto.py +0 -39
  248. clearskies/backends/example_backend.py +0 -43
  249. clearskies/backends/file_backend.py +0 -48
  250. clearskies/backends/json_backend.py +0 -7
  251. clearskies/backends/restful_api_advanced_search_backend.py +0 -138
  252. clearskies/binding_config.py +0 -16
  253. clearskies/column_types/__init__.py +0 -184
  254. clearskies/column_types/audit.py +0 -235
  255. clearskies/column_types/belongs_to.py +0 -250
  256. clearskies/column_types/boolean.py +0 -60
  257. clearskies/column_types/category_tree.py +0 -226
  258. clearskies/column_types/column.py +0 -373
  259. clearskies/column_types/created.py +0 -26
  260. clearskies/column_types/created_by_authorization_data.py +0 -26
  261. clearskies/column_types/created_by_header.py +0 -24
  262. clearskies/column_types/created_by_ip.py +0 -17
  263. clearskies/column_types/created_by_routing_data.py +0 -25
  264. clearskies/column_types/created_by_user_agent.py +0 -17
  265. clearskies/column_types/created_micro.py +0 -26
  266. clearskies/column_types/datetime.py +0 -108
  267. clearskies/column_types/datetime_micro.py +0 -12
  268. clearskies/column_types/email.py +0 -18
  269. clearskies/column_types/float.py +0 -43
  270. clearskies/column_types/has_many.py +0 -139
  271. clearskies/column_types/integer.py +0 -41
  272. clearskies/column_types/json.py +0 -25
  273. clearskies/column_types/many_to_many.py +0 -278
  274. clearskies/column_types/many_to_many_with_data.py +0 -162
  275. clearskies/column_types/select.py +0 -11
  276. clearskies/column_types/string.py +0 -24
  277. clearskies/column_types/updated.py +0 -24
  278. clearskies/column_types/updated_micro.py +0 -24
  279. clearskies/column_types/uuid.py +0 -25
  280. clearskies/columns.py +0 -123
  281. clearskies/condition_parser.py +0 -172
  282. clearskies/contexts/build_context.py +0 -54
  283. clearskies/contexts/convert_to_application.py +0 -190
  284. clearskies/contexts/extract_handler.py +0 -37
  285. clearskies/contexts/test.py +0 -94
  286. clearskies/decorators/__init__.py +0 -39
  287. clearskies/decorators/auth0_jwks.py +0 -22
  288. clearskies/decorators/authorization.py +0 -10
  289. clearskies/decorators/binding_classes.py +0 -9
  290. clearskies/decorators/binding_modules.py +0 -9
  291. clearskies/decorators/bindings.py +0 -9
  292. clearskies/decorators/create.py +0 -10
  293. clearskies/decorators/delete.py +0 -10
  294. clearskies/decorators/docs.py +0 -14
  295. clearskies/decorators/get.py +0 -10
  296. clearskies/decorators/jwks.py +0 -26
  297. clearskies/decorators/merge.py +0 -124
  298. clearskies/decorators/patch.py +0 -10
  299. clearskies/decorators/post.py +0 -10
  300. clearskies/decorators/public.py +0 -11
  301. clearskies/decorators/response_headers.py +0 -10
  302. clearskies/decorators/return_raw_response.py +0 -9
  303. clearskies/decorators/schema.py +0 -10
  304. clearskies/decorators/secret_bearer.py +0 -24
  305. clearskies/decorators/security_headers.py +0 -10
  306. clearskies/di/standard_dependencies.py +0 -140
  307. clearskies/di/test_module/__init__.py +0 -6
  308. clearskies/di/test_module/another_module/__init__.py +0 -2
  309. clearskies/di/test_module/module_class.py +0 -5
  310. clearskies/handlers/__init__.py +0 -41
  311. clearskies/handlers/advanced_search.py +0 -271
  312. clearskies/handlers/base.py +0 -473
  313. clearskies/handlers/callable.py +0 -189
  314. clearskies/handlers/create.py +0 -35
  315. clearskies/handlers/crud_by_method.py +0 -18
  316. clearskies/handlers/database_connector.py +0 -32
  317. clearskies/handlers/delete.py +0 -61
  318. clearskies/handlers/exceptions/__init__.py +0 -5
  319. clearskies/handlers/exceptions/not_found.py +0 -3
  320. clearskies/handlers/get.py +0 -156
  321. clearskies/handlers/health_check.py +0 -59
  322. clearskies/handlers/input_processing.py +0 -79
  323. clearskies/handlers/list.py +0 -530
  324. clearskies/handlers/mygrations.py +0 -82
  325. clearskies/handlers/request_method_routing.py +0 -47
  326. clearskies/handlers/restful_api.py +0 -218
  327. clearskies/handlers/routing.py +0 -62
  328. clearskies/handlers/schema_helper.py +0 -128
  329. clearskies/handlers/simple_routing.py +0 -204
  330. clearskies/handlers/simple_routing_route.py +0 -192
  331. clearskies/handlers/simple_search.py +0 -136
  332. clearskies/handlers/update.py +0 -96
  333. clearskies/handlers/write.py +0 -193
  334. clearskies/input_requirements/__init__.py +0 -68
  335. clearskies/input_requirements/after.py +0 -36
  336. clearskies/input_requirements/before.py +0 -36
  337. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  338. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  339. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  340. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  341. clearskies/input_requirements/maximum_length.py +0 -19
  342. clearskies/input_requirements/minimum_length.py +0 -22
  343. clearskies/input_requirements/requirement.py +0 -25
  344. clearskies/input_requirements/time_delta.py +0 -38
  345. clearskies/input_requirements/unique.py +0 -18
  346. clearskies/mocks/__init__.py +0 -7
  347. clearskies/mocks/input_output.py +0 -124
  348. clearskies/mocks/models.py +0 -142
  349. clearskies/models.py +0 -345
  350. clearskies/security_headers/base.py +0 -12
  351. clearskies/tests/simple_api/models/__init__.py +0 -2
  352. clearskies/tests/simple_api/models/status.py +0 -23
  353. clearskies/tests/simple_api/models/user.py +0 -21
  354. clearskies/tests/simple_api/users_api.py +0 -64
  355. {clear_skies-1.19.22.dist-info → clear_skies-2.0.23.dist-info/licenses}/LICENSE +0 -0
  356. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  357. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  358. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  359. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  360. /clearskies/{secrets/exceptions → exceptions}/not_found.py +0 -0
  361. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  362. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Callable
4
+
5
+ from clearskies import decorators
6
+ from clearskies.columns.belongs_to_id import BelongsToId
7
+
8
+ if TYPE_CHECKING:
9
+ from clearskies import typing
10
+
11
+
12
+ class BelongsToSelf(BelongsToId):
13
+ """
14
+ This is a standard BelongsToId column except it's used in cases where the model relates to itself.
15
+
16
+ This exists because a model can't refer to itself inside it's own class definition. There are
17
+ workarounds, but having this class is usually quicker for the developer.
18
+
19
+ The only difference between this and BelongsToId is that you don't have to provide the parent class.
20
+
21
+ See also HasManySelf
22
+
23
+ ```python
24
+ from typing import Any
25
+
26
+ import clearskies
27
+
28
+
29
+ class Category(clearskies.Model):
30
+ id_column_name = "id"
31
+ backend = clearskies.backends.MemoryBackend()
32
+
33
+ id = clearskies.columns.Uuid()
34
+ name = clearskies.columns.String()
35
+ parent_id = clearskies.columns.BelongsToSelf()
36
+ parent = clearskies.columns.BelongsToModel("parent_id")
37
+ children = clearskies.columns.HasManySelf()
38
+
39
+
40
+ def test_self_relationship(categories: Category) -> dict[str, Any]:
41
+ root = categories.create({"name": "Root"})
42
+ sub = categories.create({"name": "Sub", "parent": root})
43
+ subsub_1 = categories.create({"name": "Sub Sub 1", "parent": sub})
44
+ subsub_2 = categories.create({"name": "Sub Sub 2", "parent_id": sub.id})
45
+
46
+ return {
47
+ "root_from_child": subsub_1.parent.parent.name,
48
+ "subsubs_from_sub": [subsub.name for subsub in sub.children],
49
+ }
50
+
51
+
52
+ cli = clearskies.contexts.Cli(
53
+ clearskies.endpoints.Callable(test_self_relationship),
54
+ classes=[Category],
55
+ )
56
+
57
+ if __name__ == "__main__":
58
+ cli()
59
+ ```
60
+
61
+ Which when invoked returns:
62
+
63
+ ```json
64
+ {
65
+ "status": "success",
66
+ "error": "",
67
+ "data": {"root_from_child": "Root", "subsubs_from_sub": ["Sub Sub 1", "Sub Sub 2"]},
68
+ "pagination": {},
69
+ "input_errors": {},
70
+ }
71
+ ```
72
+ """
73
+
74
+ _descriptor_config_map = None
75
+
76
+ @decorators.parameters_to_properties
77
+ def __init__(
78
+ self,
79
+ readable_parent_columns: list[str] = [],
80
+ join_type: str | None = None,
81
+ where: typing.condition | list[typing.condition] = [],
82
+ default: str | None = None,
83
+ setable: str | Callable | None = None,
84
+ is_readable: bool = True,
85
+ is_writeable: bool = True,
86
+ is_searchable: bool = True,
87
+ is_temporary: bool = False,
88
+ validators: typing.validator | list[typing.validator] = [],
89
+ on_change_pre_save: typing.action | list[typing.action] = [],
90
+ on_change_post_save: typing.action | list[typing.action] = [],
91
+ on_change_save_finished: typing.action | list[typing.action] = [],
92
+ created_by_source_type: str = "",
93
+ created_by_source_key: str = "",
94
+ created_by_source_strict: bool = True,
95
+ ):
96
+ pass
97
+
98
+ def finalize_configuration(self, model_class, name) -> None:
99
+ """
100
+ Finalize and check the configuration.
101
+
102
+ This is an external trigger called by the model class when the model class is ready.
103
+ The reason it exists here instead of in the constructor is because some columns are tightly
104
+ connected to the model class, and can't validate configuration until they know what the model is.
105
+ Therefore, we need the model involved, and the only way for a property to know what class it is
106
+ in is if the parent class checks in (which is what happens here).
107
+ """
108
+ self.parent_model_class = model_class
109
+ super().finalize_configuration(model_class, name)
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Callable, Self, overload
4
+
5
+ from clearskies import configs, decorators
6
+ from clearskies.autodoc.schema import Boolean as AutoDocBoolean
7
+ from clearskies.column import Column
8
+
9
+ if TYPE_CHECKING:
10
+ from clearskies import Model, typing
11
+ from clearskies.autodoc.schema import Schema as AutoDocSchema
12
+ from clearskies.query import Condition
13
+
14
+
15
+ class Boolean(Column):
16
+ """Represents a column with a true/false type."""
17
+
18
+ """
19
+ Actions to trigger when the column changes to True
20
+ """
21
+ on_true = configs.actions.Actions(default=[])
22
+
23
+ """
24
+ Actions to trigger when the column changes to False
25
+ """
26
+ on_false = configs.actions.Actions(default=[])
27
+
28
+ """
29
+ The class to use when documenting this column
30
+ """
31
+ auto_doc_class: type[AutoDocSchema] = AutoDocBoolean
32
+
33
+ _allowed_search_operators = ["="]
34
+ default = configs.Boolean() # type: ignore
35
+ setable = configs.BooleanOrCallable() # type: ignore
36
+ _descriptor_config_map = None
37
+
38
+ @decorators.parameters_to_properties
39
+ def __init__(
40
+ self,
41
+ default: bool | None = None,
42
+ setable: bool | Callable[..., bool] | None = None,
43
+ is_readable: bool = True,
44
+ is_writeable: bool = True,
45
+ is_searchable: bool = True,
46
+ is_temporary: bool = False,
47
+ validators: typing.validator | list[typing.validator] = [],
48
+ on_change_pre_save: typing.action | list[typing.action] = [],
49
+ on_change_post_save: typing.action | list[typing.action] = [],
50
+ on_change_save_finished: typing.action | list[typing.action] = [],
51
+ on_true: typing.action | list[typing.action] = [],
52
+ on_false: typing.action | list[typing.action] = [],
53
+ created_by_source_type: str = "",
54
+ created_by_source_key: str = "",
55
+ created_by_source_strict: bool = True,
56
+ ):
57
+ pass
58
+
59
+ def from_backend(self, value) -> bool:
60
+ if value == "0":
61
+ return False
62
+ return bool(value)
63
+
64
+ def to_backend(self, data):
65
+ if self.name not in data:
66
+ return data
67
+
68
+ return {**data, self.name: bool(data[self.name])}
69
+
70
+ @overload
71
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
72
+ pass
73
+
74
+ @overload
75
+ def __get__(self, instance: Model, cls: type[Model]) -> bool:
76
+ pass
77
+
78
+ def __get__(self, instance, cls):
79
+ return super().__get__(instance, cls)
80
+
81
+ def __set__(self, instance, value: bool) -> None:
82
+ # this makes sure we're initialized
83
+ if "name" not in self._config: # type: ignore
84
+ instance.get_columns()
85
+
86
+ instance._next_data[self.name] = value
87
+
88
+ def input_error_for_value(self, value: str, operator: str | None = None) -> str:
89
+ return f"{self.name} must be a boolean" if type(value) != bool else ""
90
+
91
+ def build_condition(self, value: str, operator: str | None = None, column_prefix: str = ""):
92
+ condition_value = "1" if value else "0"
93
+ if not operator:
94
+ operator = "="
95
+ return f"{column_prefix}{self.name}{operator}{condition_value}"
96
+
97
+ def save_finished(self, model: Model) -> None:
98
+ """Make any necessary changes needed after a save has completely finished."""
99
+ super().save_finished(model)
100
+
101
+ if (not self.on_true and not self.on_false) or not model.was_changed(self.name):
102
+ return
103
+
104
+ if getattr(model, self.name) and self.on_true:
105
+ self.execute_actions(self.on_true, model)
106
+ if not getattr(model, self.name) and self.on_false:
107
+ self.execute_actions(self.on_false, model)
108
+
109
+ def equals(self, value: bool) -> Condition:
110
+ return super().equals(value)
@@ -0,0 +1,273 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Callable
4
+
5
+ from clearskies import configs, decorators
6
+ from clearskies.columns.belongs_to_id import BelongsToId
7
+
8
+ if TYPE_CHECKING:
9
+ from clearskies import Model, typing
10
+
11
+
12
+ class CategoryTree(BelongsToId):
13
+ """
14
+ The category tree helps you do quick lookups on a typical category tree.
15
+
16
+ It's a very niche tool. In general, graph databases solve this problem better, but
17
+ it's not always worth the effort of spinning up a new kind of database.
18
+
19
+ This column needs a special tree table where it will pre-compute and store the
20
+ necessary information to perform quick lookups about relationships in a cateogry
21
+ tree. So, imagine you have a table that represents a standard category heirarchy:
22
+
23
+ ```sql
24
+ CREATE TABLE categories (
25
+ id varchar(255),
26
+ parent_id varchar(255),
27
+ name varchar(255)
28
+ )
29
+
30
+ `parent_id`, in this case, would be a reference to the `categories` table itself -
31
+ hence the heirarchy. This works fine as a starting point but it gets tricky when you want to answer questions like
32
+ "what are all the parent categories of category X?" or "what are all the child categories of category Y?".
33
+ This column class solves that by building a tree table that caches this data as the categories are updated.
34
+ That table should look like this:
35
+
36
+ ```sql
37
+ CREATE TABLE category_tree (
38
+ id varchar(255),
39
+ parent_id varchar(255),
40
+ child_id varchar(255),
41
+ is_parent tinyint(1),
42
+ level tinyint(1),
43
+ )
44
+ ```
45
+
46
+ Then you would attach this column to your category model as a replacement for a typical BelongsToId relationship:
47
+
48
+ ```python
49
+ import clearskies
50
+
51
+ class Tree(clearskies.Model):
52
+ id_column_name = "id"
53
+ backend = clearskies.backends.MemoryBackend(silent_on_missing_tables=True)
54
+
55
+ id = clearskies.columns.Uuid()
56
+ parent_id = clearskies.columns.String()
57
+ child_id = clearskies.columns.String()
58
+ is_parent = clearskies.columns.Boolean()
59
+ level = clearskies.columns.Integer()
60
+
61
+ class Category(clearskies.Model):
62
+ id_column_name = "id"
63
+ backend = clearskies.backends.MemoryBackend(silent_on_missing_tables=True)
64
+
65
+ id = clearskies.columns.Uuid()
66
+ name = clearskies.columns.String()
67
+ parent_id = clearskies.columns.CategoryTree(Tree)
68
+ parent = clearskies.columns.BelongsToModel("parent_id")
69
+ children = clearskies.columns.CategoryTreeChildren("parent_id")
70
+ descendants = clearskies.columns.CategoryTreeDescendants("parent_id")
71
+ ancestors = clearskies.columns.CategoryTreeAncestors("parent_id")
72
+
73
+ def test_category_tree(category: Category):
74
+ root_1 = category.create({"name": "Root 1"})
75
+ root_2 = category.create({"name": "Root 2"})
76
+ sub_1_root_1 = category.create({"name": "Sub 1 of Root 1", "parent_id": root_1.id})
77
+ sub_2_root_1 = category.create({"name": "Sub 2 of Root 1", "parent_id": root_1.id})
78
+ sub_sub = category.create({"name": "Sub Sub", "parent_id": sub_1_root_1.id})
79
+ sub_1_root_2 = category.create({"name": "Sub 1 of Root 2", "parent_id": root_2.id})
80
+
81
+ return {
82
+ "descendants_of_root_1": [descendant.name for descendant in root_1.descendants],
83
+ "children_of_root_1": [child.name for child in root_1.children],
84
+ "descendants_of_root_2": [descendant.name for descendant in root_2.descendants],
85
+ "ancestors_of_sub_sub": [ancestor.name for ancestor in sub_sub.ancestors],
86
+ }
87
+
88
+ cli = clearskies.contexts.Cli(
89
+ clearskies.endpoints.Callable(test_category_tree),
90
+ classes=[Category, Tree],
91
+ )
92
+ cli()
93
+ ```
94
+
95
+ And if you invoke the above you will get:
96
+
97
+ ```json
98
+ {
99
+ "status": "success",
100
+ "error": "",
101
+ "data": {
102
+ "descendants_of_root_1": ["Sub 1 of Root 1", "Sub 2 of Root 1", "Sub Sub"],
103
+ "children_of_root_1": ["Sub 1 of Root 1", "Sub 2 of Root 1"],
104
+ "descendants_of_root_2": ["Sub 1 of Root 2"],
105
+ "ancestors_of_sub_sub": ["Root 1", "Sub 1 of Root 1"],
106
+ },
107
+ "pagination": {},
108
+ "input_errors": {},
109
+ }
110
+ ```
111
+
112
+ In case it's not clear, the definition of these things are:
113
+
114
+ 1. Descendants: All children under a given category (recursively).
115
+ 2. Children: The direct descendants of a given category.
116
+ 3. Ancestors: The parents of a given category, starting from the root category.
117
+ 4. Parent: the immediate parent of the category.
118
+
119
+ """
120
+
121
+ """
122
+ The model class that will persist our tree data
123
+ """
124
+ tree_model_class = configs.ModelClass(required=True)
125
+
126
+ """
127
+ The column in the tree model that references the parent in the relationship
128
+ """
129
+ tree_parent_id_column_name = configs.ModelColumn("tree_model_class", default="parent_id")
130
+
131
+ """
132
+ The column in the tree model that references the child in the relationship
133
+ """
134
+ tree_child_id_column_name = configs.ModelColumn("tree_model_class", default="child_id")
135
+
136
+ """
137
+ The column in the tree model that denotes which node in the relationship represents the tree
138
+ """
139
+ tree_is_parent_column_name = configs.ModelColumn("tree_model_class", default="is_parent")
140
+
141
+ """
142
+ The column in the tree model that references the parent in a relationship
143
+ """
144
+ tree_level_column_name = configs.ModelColumn("tree_model_class", default="level")
145
+
146
+ """
147
+ The maximum expected depth of the tree
148
+ """
149
+ max_iterations = configs.Integer(default=100)
150
+
151
+ """
152
+ The strategy for loading relatives.
153
+
154
+ Choose whatever one actually works for your backend
155
+
156
+ * JOIN: use an actual `JOIN` (e.g. quick and efficient, but mostly only works for SQL backends).
157
+ * WHERE IN: Use a `WHERE IN` condition.
158
+ * INDIVIDUAL: Load each record separately. Works for any backend but is also the slowest.
159
+ """
160
+ load_relatives_strategy = configs.Select(["join", "where_in", "individual"], default="join")
161
+
162
+ _descriptor_config_map = None
163
+
164
+ @decorators.parameters_to_properties
165
+ def __init__(
166
+ self,
167
+ tree_model_class,
168
+ tree_parent_id_column_name: str = "parent_id",
169
+ tree_child_id_column_name: str = "child_id",
170
+ tree_is_parent_column_name: str = "is_parent",
171
+ tree_level_column_name: str = "level",
172
+ max_iterations: int = 100,
173
+ load_relatives_strategy: str = "join",
174
+ readable_parent_columns: list[str] = [],
175
+ join_type: str | None = None,
176
+ where: typing.condition | list[typing.condition] = [],
177
+ default: str | None = None,
178
+ setable: str | Callable | None = None,
179
+ is_readable: bool = True,
180
+ is_writeable: bool = True,
181
+ is_searchable: bool = True,
182
+ is_temporary: bool = False,
183
+ validators: typing.validator | list[typing.validator] = [],
184
+ on_change_pre_save: typing.action | list[typing.action] = [],
185
+ on_change_post_save: typing.action | list[typing.action] = [],
186
+ on_change_save_finished: typing.action | list[typing.action] = [],
187
+ created_by_source_type: str = "",
188
+ created_by_source_key: str = "",
189
+ created_by_source_strict: bool = True,
190
+ ):
191
+ pass
192
+
193
+ def finalize_configuration(self, model_class, name) -> None:
194
+ """
195
+ Finalize and check the configuration.
196
+
197
+ This is an external trigger called by the model class when the model class is ready.
198
+ The reason it exists here instead of in the constructor is because some columns are tightly
199
+ connected to the model class, and can't validate configuration until they know what the model is.
200
+ Therefore, we need the model involved, and the only way for a property to know what class it is
201
+ in is if the parent class checks in (which is what happens here).
202
+ """
203
+ self.parent_model_class = model_class
204
+ super().finalize_configuration(model_class, name)
205
+
206
+ @property
207
+ def tree_model(self):
208
+ return self.di.build(self.tree_model_class, cache=True)
209
+
210
+ def post_save(self, data: dict[str, Any], model: Model, id: int | str) -> None:
211
+ if not model.is_changing(self.name, data):
212
+ return
213
+
214
+ self.update_tree_table(model, id, model.latest(self.name, data))
215
+ return
216
+
217
+ def force_tree_update(self, model: Model) -> None:
218
+ self.update_tree_table(model, getattr(model, model.id_column_name), getattr(model, self.name))
219
+
220
+ def update_tree_table(self, model: Model, child_id: int | str, direct_parent_id: int | str) -> None:
221
+ tree_model = self.tree_model
222
+ parent_model = self.parent_model
223
+ tree_parent_id_column_name = self.tree_parent_id_column_name
224
+ tree_child_id_column_name = self.tree_child_id_column_name
225
+ tree_is_parent_column_name = self.tree_is_parent_column_name
226
+ tree_level_column_name = self.tree_level_column_name
227
+ max_iterations = self.max_iterations
228
+
229
+ # we're going to be lazy and just delete the data for the current record in the tree table,
230
+ # and then re-insert everything (but we can skip this if creating a new record)
231
+ if model:
232
+ for tree in tree_model.where(f"{tree_child_id_column_name}={child_id}"):
233
+ tree.delete()
234
+
235
+ # if we are a root category then we don't have a tree
236
+ if not direct_parent_id:
237
+ return
238
+
239
+ is_root = False
240
+ id_column_name = parent_model.id_column_name
241
+ next_parent = parent_model.find(f"{id_column_name}={direct_parent_id}")
242
+ tree = []
243
+ c = 0
244
+ while not is_root:
245
+ c += 1
246
+ if c > max_iterations:
247
+ self._circular(max_iterations)
248
+
249
+ tree.append(getattr(next_parent, next_parent.id_column_name))
250
+ if not getattr(next_parent, self.name):
251
+ is_root = True
252
+ else:
253
+ next_next_parent_id = getattr(next_parent, self.name)
254
+ next_parent = model.find(f"{id_column_name}={next_next_parent_id}")
255
+
256
+ tree.reverse()
257
+ for index, parent_id in enumerate(tree):
258
+ tree_model.create(
259
+ {
260
+ tree_parent_id_column_name: parent_id,
261
+ tree_child_id_column_name: child_id,
262
+ tree_is_parent_column_name: 1 if parent_id == direct_parent_id else 0,
263
+ tree_level_column_name: index,
264
+ }
265
+ )
266
+
267
+ def _circular(self, max_iterations):
268
+ raise ValueError(
269
+ f"Error for column {self.model_class.__name__}.{self.name}: "
270
+ + f"I've climbed through {max_iterations} parents and haven't found the root yet."
271
+ + "You may have accidentally created a circular cateogry tree. If not, and your category tree "
272
+ + "really _is_ that deep, then adjust the 'max_iterations' configuration for this column accordingly. "
273
+ )
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Self, overload
4
+
5
+ from clearskies.columns.category_tree_children import CategoryTreeChildren
6
+
7
+ if TYPE_CHECKING:
8
+ from clearskies import Model
9
+
10
+
11
+ class CategoryTreeAncestors(CategoryTreeChildren):
12
+ """
13
+ A column to fetch the ancestors from a category tree column.
14
+
15
+ See the CategoryTree column for usage examples.
16
+
17
+ The ancestors are all parents of a given category, starting from the root category and working
18
+ down to the direct parent. So, given the following category tree:
19
+
20
+ ```
21
+ Root/
22
+ ├─ Sub/
23
+ │ ├─ Sub Sub/
24
+ │ │ ├─ Sub Sub Sub/
25
+ ├─ Another Child/
26
+ ```
27
+
28
+ The ancesotrs of `Sub Sub Sub` are `["Root", "Sub", "Sub Sub"]` while the ancestors of `Another Child`
29
+ are `["Root"]`
30
+ """
31
+
32
+ _descriptor_config_map = None
33
+
34
+ @overload
35
+ def __get__(self, instance: None, cls: type) -> Self:
36
+ pass
37
+
38
+ @overload
39
+ def __get__(self, instance: Model, cls: type) -> Model:
40
+ pass
41
+
42
+ def __get__(self, model, cls):
43
+ if model is None:
44
+ self.model_class = cls
45
+ return self # type: ignore
46
+
47
+ # this makes sure we're initialized
48
+ if "name" not in self._config: # type: ignore
49
+ model.get_columns()
50
+
51
+ return self.relatives(model, find_parents=True, include_all=True)
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Self, overload
4
+
5
+ from clearskies import configs, decorators
6
+ from clearskies.column import Column
7
+ from clearskies.columns import CategoryTree
8
+
9
+ if TYPE_CHECKING:
10
+ from clearskies import Model
11
+
12
+
13
+ class CategoryTreeChildren(Column):
14
+ """
15
+ Return the child categories from a category tree column.
16
+
17
+ See the CategoryTree column for usage examples.
18
+
19
+ The ancestors are all direct descendants of a given category. So, given the following tree:
20
+
21
+ ```
22
+ Root/
23
+ ├─ Sub/
24
+ │ ├─ Sub Sub/
25
+ │ │ ├─ Sub Sub Sub/
26
+ ├─ Another Child/
27
+
28
+ The children of `Root` are `["Sub", "Another Child"]`. The children of `Sub Sub` are `["Sub Sub Sub"]`.
29
+ """
30
+
31
+ """ The name of the category tree column we are connected to. """
32
+ category_tree_column_name = configs.ModelColumn(required=True)
33
+
34
+ is_writeable = configs.Boolean(default=False)
35
+ is_searchable = configs.Boolean(default=False)
36
+ _descriptor_config_map = None
37
+
38
+ @decorators.parameters_to_properties
39
+ def __init__(
40
+ self,
41
+ category_tree_column_name: str,
42
+ ):
43
+ pass
44
+
45
+ def finalize_configuration(self, model_class: type, name: str) -> None:
46
+ """Finalize and check the configuration."""
47
+ getattr(self.__class__, "category_tree_column_name").set_model_class(model_class)
48
+ self.model_class = model_class
49
+ self.name = name
50
+ self.finalize_and_validate_configuration()
51
+
52
+ # double check that we are pointed to a category tree column
53
+ category_tree_column = getattr(model_class, self.category_tree_column_name)
54
+ if not isinstance(category_tree_column, CategoryTree):
55
+ raise ValueError(
56
+ f"Error with configuration for {model_class.__name__}.{name}, which is a {self.__class__.__name__}. It needs to point to a category tree column, and it was told to use {model_class.__name__}.{self.category_tree_column_name}, but this is not a CategoryTree column."
57
+ )
58
+
59
+ @overload
60
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
61
+ pass
62
+
63
+ @overload
64
+ def __get__(self, instance: Model, cls: type[Model]) -> Model:
65
+ pass
66
+
67
+ def __get__(self, model, cls):
68
+ if model is None:
69
+ self.model_class = cls
70
+ return self # type: ignore
71
+
72
+ # this makes sure we're initialized
73
+ if "name" not in self._config: # type: ignore
74
+ model.get_columns()
75
+
76
+ return self.relatives(model)
77
+
78
+ def __set__(self, model: Model, value: Model) -> None:
79
+ raise ValueError(
80
+ f"Attempt to set a value to '{model.__class__.__name__}.{self.name}, but this column is not writeable"
81
+ )
82
+
83
+ def relatives(self, model: Model, include_all: bool = False, find_parents: bool = False) -> Model | list[Model]:
84
+ id_column_name = model.id_column_name
85
+ model_id = getattr(model, id_column_name)
86
+ model_table_name = model.destination_name()
87
+ category_tree_column = getattr(self.model_class, self.category_tree_column_name)
88
+ tree_table_name = category_tree_column.tree_model_class.destination_name()
89
+ parent_id_column_name = category_tree_column.tree_parent_id_column_name
90
+ child_id_column_name = category_tree_column.tree_child_id_column_name
91
+ is_parent_column_name = category_tree_column.tree_is_parent_column_name
92
+ level_column_name = category_tree_column.tree_level_column_name
93
+
94
+ if find_parents:
95
+ join_on = parent_id_column_name
96
+ search_on = child_id_column_name
97
+ else:
98
+ join_on = child_id_column_name
99
+ search_on = parent_id_column_name
100
+
101
+ # if we can join then use a join.
102
+ if category_tree_column.load_relatives_strategy == "join":
103
+ relatives = category_tree_column.parent_model.join(
104
+ f"JOIN {tree_table_name} as tree on tree.{join_on}={model_table_name}.{id_column_name}"
105
+ )
106
+ relatives = relatives.where(f"tree.{search_on}={model_id}")
107
+ if not include_all:
108
+ relatives = relatives.where(f"tree.{is_parent_column_name}=1")
109
+ if find_parents:
110
+ relatives = relatives.sort_by(level_column_name, "asc", "tree")
111
+ return relatives
112
+
113
+ # joins only work for SQL-like backends. Otherwise, we have to pull out our list of ids
114
+ branches = category_tree_column.tree_model.where(f"{search_on}={model_id}")
115
+ if not include_all:
116
+ branches = branches.where(f"{is_parent_column_name}=1")
117
+ if find_parents:
118
+ branches = branches.sort_by(level_column_name, "asc")
119
+ ids = [str(getattr(branch, join_on)) for branch in branches]
120
+
121
+ # Can we search with a WHERE IN() clause? If the backend supports it, it is probably faster
122
+ if category_tree_column.load_relatives_strategy == "where_in":
123
+ return category_tree_column.parent_model.where(f"{id_column_name} IN ('" + "','".join(ids) + "')")
124
+
125
+ # otherwise we have to load each model individually which is SLOW....
126
+ return [category_tree_column.parent_model.find(f"{id_column_name}={id}") for id in ids]