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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. clear_skies-2.0.23.dist-info/METADATA +76 -0
  2. clear_skies-2.0.23.dist-info/RECORD +265 -0
  3. {clear_skies-1.22.10.dist-info → clear_skies-2.0.23.dist-info}/WHEEL +1 -1
  4. clearskies/__init__.py +37 -21
  5. clearskies/action.py +7 -0
  6. clearskies/authentication/__init__.py +8 -39
  7. clearskies/authentication/authentication.py +44 -0
  8. clearskies/authentication/authorization.py +14 -8
  9. clearskies/authentication/authorization_pass_through.py +14 -10
  10. clearskies/authentication/jwks.py +135 -58
  11. clearskies/authentication/public.py +3 -26
  12. clearskies/authentication/secret_bearer.py +515 -44
  13. clearskies/autodoc/formats/oai3_json/__init__.py +2 -2
  14. clearskies/autodoc/formats/oai3_json/oai3_json.py +11 -9
  15. clearskies/autodoc/formats/oai3_json/parameter.py +6 -3
  16. clearskies/autodoc/formats/oai3_json/request.py +7 -5
  17. clearskies/autodoc/formats/oai3_json/response.py +7 -4
  18. clearskies/autodoc/formats/oai3_json/schema/object.py +10 -1
  19. clearskies/autodoc/request/__init__.py +2 -0
  20. clearskies/autodoc/request/header.py +4 -6
  21. clearskies/autodoc/request/json_body.py +4 -6
  22. clearskies/autodoc/request/parameter.py +8 -0
  23. clearskies/autodoc/request/request.py +16 -4
  24. clearskies/autodoc/request/url_parameter.py +4 -6
  25. clearskies/autodoc/request/url_path.py +4 -6
  26. clearskies/autodoc/schema/__init__.py +4 -2
  27. clearskies/autodoc/schema/array.py +5 -6
  28. clearskies/autodoc/schema/boolean.py +4 -10
  29. clearskies/autodoc/schema/date.py +0 -3
  30. clearskies/autodoc/schema/datetime.py +1 -4
  31. clearskies/autodoc/schema/double.py +0 -3
  32. clearskies/autodoc/schema/enum.py +4 -2
  33. clearskies/autodoc/schema/integer.py +4 -9
  34. clearskies/autodoc/schema/long.py +0 -3
  35. clearskies/autodoc/schema/number.py +4 -9
  36. clearskies/autodoc/schema/object.py +5 -7
  37. clearskies/autodoc/schema/password.py +0 -3
  38. clearskies/autodoc/schema/schema.py +11 -0
  39. clearskies/autodoc/schema/string.py +4 -10
  40. clearskies/backends/__init__.py +55 -20
  41. clearskies/backends/api_backend.py +1118 -280
  42. clearskies/backends/backend.py +54 -85
  43. clearskies/backends/cursor_backend.py +246 -191
  44. clearskies/backends/memory_backend.py +514 -208
  45. clearskies/backends/secrets_backend.py +68 -31
  46. clearskies/column.py +1221 -0
  47. clearskies/columns/__init__.py +71 -0
  48. clearskies/columns/audit.py +306 -0
  49. clearskies/columns/belongs_to_id.py +478 -0
  50. clearskies/columns/belongs_to_model.py +129 -0
  51. clearskies/columns/belongs_to_self.py +109 -0
  52. clearskies/columns/boolean.py +110 -0
  53. clearskies/columns/category_tree.py +273 -0
  54. clearskies/columns/category_tree_ancestors.py +51 -0
  55. clearskies/columns/category_tree_children.py +126 -0
  56. clearskies/columns/category_tree_descendants.py +48 -0
  57. clearskies/columns/created.py +92 -0
  58. clearskies/columns/created_by_authorization_data.py +114 -0
  59. clearskies/columns/created_by_header.py +103 -0
  60. clearskies/columns/created_by_ip.py +90 -0
  61. clearskies/columns/created_by_routing_data.py +102 -0
  62. clearskies/columns/created_by_user_agent.py +89 -0
  63. clearskies/columns/date.py +232 -0
  64. clearskies/columns/datetime.py +284 -0
  65. clearskies/columns/email.py +78 -0
  66. clearskies/columns/float.py +149 -0
  67. clearskies/columns/has_many.py +529 -0
  68. clearskies/columns/has_many_self.py +62 -0
  69. clearskies/columns/has_one.py +21 -0
  70. clearskies/columns/integer.py +158 -0
  71. clearskies/columns/json.py +126 -0
  72. clearskies/columns/many_to_many_ids.py +335 -0
  73. clearskies/columns/many_to_many_ids_with_data.py +274 -0
  74. clearskies/columns/many_to_many_models.py +156 -0
  75. clearskies/columns/many_to_many_pivots.py +132 -0
  76. clearskies/columns/phone.py +162 -0
  77. clearskies/columns/select.py +95 -0
  78. clearskies/columns/string.py +102 -0
  79. clearskies/columns/timestamp.py +164 -0
  80. clearskies/columns/updated.py +107 -0
  81. clearskies/columns/uuid.py +83 -0
  82. clearskies/configs/README.md +105 -0
  83. clearskies/configs/__init__.py +170 -0
  84. clearskies/configs/actions.py +43 -0
  85. clearskies/configs/any.py +15 -0
  86. clearskies/configs/any_dict.py +24 -0
  87. clearskies/configs/any_dict_or_callable.py +25 -0
  88. clearskies/configs/authentication.py +23 -0
  89. clearskies/configs/authorization.py +23 -0
  90. clearskies/configs/boolean.py +18 -0
  91. clearskies/configs/boolean_or_callable.py +20 -0
  92. clearskies/configs/callable_config.py +20 -0
  93. clearskies/configs/columns.py +34 -0
  94. clearskies/configs/conditions.py +30 -0
  95. clearskies/configs/config.py +26 -0
  96. clearskies/configs/datetime.py +20 -0
  97. clearskies/configs/datetime_or_callable.py +21 -0
  98. clearskies/configs/email.py +10 -0
  99. clearskies/configs/email_list.py +17 -0
  100. clearskies/configs/email_list_or_callable.py +17 -0
  101. clearskies/configs/email_or_email_list_or_callable.py +59 -0
  102. clearskies/configs/endpoint.py +23 -0
  103. clearskies/configs/endpoint_list.py +29 -0
  104. clearskies/configs/float.py +18 -0
  105. clearskies/configs/float_or_callable.py +20 -0
  106. clearskies/configs/headers.py +28 -0
  107. clearskies/configs/integer.py +18 -0
  108. clearskies/configs/integer_or_callable.py +20 -0
  109. clearskies/configs/joins.py +30 -0
  110. clearskies/configs/list_any_dict.py +32 -0
  111. clearskies/configs/list_any_dict_or_callable.py +33 -0
  112. clearskies/configs/model_class.py +35 -0
  113. clearskies/configs/model_column.py +67 -0
  114. clearskies/configs/model_columns.py +58 -0
  115. clearskies/configs/model_destination_name.py +26 -0
  116. clearskies/configs/model_to_id_column.py +45 -0
  117. clearskies/configs/readable_model_column.py +11 -0
  118. clearskies/configs/readable_model_columns.py +11 -0
  119. clearskies/configs/schema.py +23 -0
  120. clearskies/configs/searchable_model_columns.py +11 -0
  121. clearskies/configs/security_headers.py +39 -0
  122. clearskies/configs/select.py +28 -0
  123. clearskies/configs/select_list.py +49 -0
  124. clearskies/configs/string.py +31 -0
  125. clearskies/configs/string_dict.py +34 -0
  126. clearskies/configs/string_list.py +47 -0
  127. clearskies/configs/string_list_or_callable.py +48 -0
  128. clearskies/configs/string_or_callable.py +18 -0
  129. clearskies/configs/timedelta.py +20 -0
  130. clearskies/configs/timezone.py +20 -0
  131. clearskies/configs/url.py +25 -0
  132. clearskies/configs/validators.py +45 -0
  133. clearskies/configs/writeable_model_column.py +11 -0
  134. clearskies/configs/writeable_model_columns.py +11 -0
  135. clearskies/configurable.py +78 -0
  136. clearskies/contexts/__init__.py +8 -8
  137. clearskies/contexts/cli.py +129 -43
  138. clearskies/contexts/context.py +93 -56
  139. clearskies/contexts/wsgi.py +79 -33
  140. clearskies/contexts/wsgi_ref.py +87 -0
  141. clearskies/cursors/__init__.py +7 -0
  142. clearskies/cursors/cursor.py +166 -0
  143. clearskies/cursors/from_environment/__init__.py +5 -0
  144. clearskies/cursors/from_environment/mysql.py +51 -0
  145. clearskies/cursors/from_environment/postgresql.py +49 -0
  146. clearskies/cursors/from_environment/sqlite.py +35 -0
  147. clearskies/cursors/mysql.py +61 -0
  148. clearskies/cursors/postgresql.py +61 -0
  149. clearskies/cursors/sqlite.py +62 -0
  150. clearskies/decorators.py +33 -0
  151. clearskies/decorators.pyi +10 -0
  152. clearskies/di/__init__.py +11 -7
  153. clearskies/di/additional_config.py +115 -4
  154. clearskies/di/additional_config_auto_import.py +12 -0
  155. clearskies/di/di.py +714 -125
  156. clearskies/di/inject/__init__.py +23 -0
  157. clearskies/di/inject/akeyless_sdk.py +16 -0
  158. clearskies/di/inject/by_class.py +24 -0
  159. clearskies/di/inject/by_name.py +22 -0
  160. clearskies/di/inject/di.py +16 -0
  161. clearskies/di/inject/environment.py +15 -0
  162. clearskies/di/inject/input_output.py +19 -0
  163. clearskies/di/inject/now.py +16 -0
  164. clearskies/di/inject/requests.py +16 -0
  165. clearskies/di/inject/secrets.py +15 -0
  166. clearskies/di/inject/utcnow.py +16 -0
  167. clearskies/di/inject/uuid.py +16 -0
  168. clearskies/di/injectable.py +32 -0
  169. clearskies/di/injectable_properties.py +131 -0
  170. clearskies/end.py +219 -0
  171. clearskies/endpoint.py +1303 -0
  172. clearskies/endpoint_group.py +333 -0
  173. clearskies/endpoints/__init__.py +25 -0
  174. clearskies/endpoints/advanced_search.py +519 -0
  175. clearskies/endpoints/callable.py +382 -0
  176. clearskies/endpoints/create.py +201 -0
  177. clearskies/endpoints/delete.py +133 -0
  178. clearskies/endpoints/get.py +267 -0
  179. clearskies/endpoints/health_check.py +181 -0
  180. clearskies/endpoints/list.py +567 -0
  181. clearskies/endpoints/restful_api.py +417 -0
  182. clearskies/endpoints/schema.py +185 -0
  183. clearskies/endpoints/simple_search.py +279 -0
  184. clearskies/endpoints/update.py +188 -0
  185. clearskies/environment.py +7 -3
  186. clearskies/exceptions/__init__.py +19 -0
  187. clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
  188. clearskies/exceptions/missing_dependency.py +2 -0
  189. clearskies/exceptions/moved_permanently.py +3 -0
  190. clearskies/exceptions/moved_temporarily.py +3 -0
  191. clearskies/functional/__init__.py +2 -2
  192. clearskies/functional/json.py +47 -0
  193. clearskies/functional/routing.py +92 -0
  194. clearskies/functional/string.py +19 -11
  195. clearskies/functional/validations.py +61 -9
  196. clearskies/input_outputs/__init__.py +9 -7
  197. clearskies/input_outputs/cli.py +135 -160
  198. clearskies/input_outputs/exceptions/__init__.py +6 -1
  199. clearskies/input_outputs/headers.py +54 -0
  200. clearskies/input_outputs/input_output.py +77 -123
  201. clearskies/input_outputs/programmatic.py +62 -0
  202. clearskies/input_outputs/wsgi.py +36 -48
  203. clearskies/model.py +1874 -193
  204. clearskies/query/__init__.py +12 -0
  205. clearskies/query/condition.py +228 -0
  206. clearskies/query/join.py +136 -0
  207. clearskies/query/query.py +193 -0
  208. clearskies/query/sort.py +27 -0
  209. clearskies/schema.py +82 -0
  210. clearskies/secrets/__init__.py +4 -31
  211. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
  212. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
  213. clearskies/secrets/akeyless.py +421 -155
  214. clearskies/secrets/exceptions/__init__.py +7 -1
  215. clearskies/secrets/exceptions/not_found_error.py +2 -0
  216. clearskies/secrets/exceptions/permissions_error.py +2 -0
  217. clearskies/secrets/secrets.py +12 -11
  218. clearskies/security_header.py +17 -0
  219. clearskies/security_headers/__init__.py +8 -8
  220. clearskies/security_headers/cache_control.py +47 -109
  221. clearskies/security_headers/cors.py +38 -92
  222. clearskies/security_headers/csp.py +76 -150
  223. clearskies/security_headers/hsts.py +14 -15
  224. clearskies/typing.py +11 -0
  225. clearskies/validator.py +36 -0
  226. clearskies/validators/__init__.py +33 -0
  227. clearskies/validators/after_column.py +61 -0
  228. clearskies/validators/before_column.py +15 -0
  229. clearskies/validators/in_the_future.py +29 -0
  230. clearskies/validators/in_the_future_at_least.py +13 -0
  231. clearskies/validators/in_the_future_at_most.py +12 -0
  232. clearskies/validators/in_the_past.py +29 -0
  233. clearskies/validators/in_the_past_at_least.py +12 -0
  234. clearskies/validators/in_the_past_at_most.py +12 -0
  235. clearskies/validators/maximum_length.py +25 -0
  236. clearskies/validators/maximum_value.py +28 -0
  237. clearskies/validators/minimum_length.py +25 -0
  238. clearskies/validators/minimum_value.py +28 -0
  239. clearskies/{input_requirements → validators}/required.py +18 -9
  240. clearskies/validators/timedelta.py +58 -0
  241. clearskies/validators/unique.py +28 -0
  242. clear_skies-1.22.10.dist-info/METADATA +0 -47
  243. clear_skies-1.22.10.dist-info/RECORD +0 -213
  244. clearskies/application.py +0 -29
  245. clearskies/authentication/auth0_jwks.py +0 -118
  246. clearskies/authentication/auth_exception.py +0 -2
  247. clearskies/authentication/jwks_jwcrypto.py +0 -51
  248. clearskies/backends/api_get_only_backend.py +0 -48
  249. clearskies/backends/example_backend.py +0 -43
  250. clearskies/backends/file_backend.py +0 -48
  251. clearskies/backends/json_backend.py +0 -7
  252. clearskies/backends/restful_api_advanced_search_backend.py +0 -103
  253. clearskies/binding_config.py +0 -16
  254. clearskies/column_types/__init__.py +0 -203
  255. clearskies/column_types/audit.py +0 -249
  256. clearskies/column_types/belongs_to.py +0 -271
  257. clearskies/column_types/boolean.py +0 -60
  258. clearskies/column_types/category_tree.py +0 -304
  259. clearskies/column_types/column.py +0 -373
  260. clearskies/column_types/created.py +0 -26
  261. clearskies/column_types/created_by_authorization_data.py +0 -26
  262. clearskies/column_types/created_by_header.py +0 -24
  263. clearskies/column_types/created_by_ip.py +0 -17
  264. clearskies/column_types/created_by_routing_data.py +0 -25
  265. clearskies/column_types/created_by_user_agent.py +0 -17
  266. clearskies/column_types/created_micro.py +0 -26
  267. clearskies/column_types/datetime.py +0 -109
  268. clearskies/column_types/datetime_micro.py +0 -13
  269. clearskies/column_types/email.py +0 -18
  270. clearskies/column_types/float.py +0 -43
  271. clearskies/column_types/has_many.py +0 -179
  272. clearskies/column_types/has_one.py +0 -58
  273. clearskies/column_types/integer.py +0 -41
  274. clearskies/column_types/json.py +0 -25
  275. clearskies/column_types/many_to_many.py +0 -278
  276. clearskies/column_types/many_to_many_with_data.py +0 -162
  277. clearskies/column_types/phone.py +0 -48
  278. clearskies/column_types/select.py +0 -11
  279. clearskies/column_types/string.py +0 -24
  280. clearskies/column_types/timestamp.py +0 -73
  281. clearskies/column_types/updated.py +0 -26
  282. clearskies/column_types/updated_micro.py +0 -26
  283. clearskies/column_types/uuid.py +0 -25
  284. clearskies/columns.py +0 -123
  285. clearskies/condition_parser.py +0 -172
  286. clearskies/contexts/build_context.py +0 -54
  287. clearskies/contexts/convert_to_application.py +0 -190
  288. clearskies/contexts/extract_handler.py +0 -37
  289. clearskies/contexts/test.py +0 -94
  290. clearskies/decorators/__init__.py +0 -39
  291. clearskies/decorators/auth0_jwks.py +0 -22
  292. clearskies/decorators/authorization.py +0 -10
  293. clearskies/decorators/binding_classes.py +0 -9
  294. clearskies/decorators/binding_modules.py +0 -9
  295. clearskies/decorators/bindings.py +0 -9
  296. clearskies/decorators/create.py +0 -10
  297. clearskies/decorators/delete.py +0 -10
  298. clearskies/decorators/docs.py +0 -14
  299. clearskies/decorators/get.py +0 -10
  300. clearskies/decorators/jwks.py +0 -26
  301. clearskies/decorators/merge.py +0 -124
  302. clearskies/decorators/patch.py +0 -10
  303. clearskies/decorators/post.py +0 -10
  304. clearskies/decorators/public.py +0 -11
  305. clearskies/decorators/response_headers.py +0 -10
  306. clearskies/decorators/return_raw_response.py +0 -9
  307. clearskies/decorators/schema.py +0 -10
  308. clearskies/decorators/secret_bearer.py +0 -24
  309. clearskies/decorators/security_headers.py +0 -10
  310. clearskies/di/standard_dependencies.py +0 -151
  311. clearskies/di/test_module/__init__.py +0 -6
  312. clearskies/di/test_module/another_module/__init__.py +0 -2
  313. clearskies/di/test_module/module_class.py +0 -5
  314. clearskies/handlers/__init__.py +0 -41
  315. clearskies/handlers/advanced_search.py +0 -271
  316. clearskies/handlers/base.py +0 -479
  317. clearskies/handlers/callable.py +0 -191
  318. clearskies/handlers/create.py +0 -35
  319. clearskies/handlers/crud_by_method.py +0 -18
  320. clearskies/handlers/database_connector.py +0 -32
  321. clearskies/handlers/delete.py +0 -61
  322. clearskies/handlers/exceptions/__init__.py +0 -5
  323. clearskies/handlers/exceptions/not_found.py +0 -3
  324. clearskies/handlers/get.py +0 -156
  325. clearskies/handlers/health_check.py +0 -59
  326. clearskies/handlers/input_processing.py +0 -79
  327. clearskies/handlers/list.py +0 -530
  328. clearskies/handlers/mygrations.py +0 -82
  329. clearskies/handlers/request_method_routing.py +0 -47
  330. clearskies/handlers/restful_api.py +0 -218
  331. clearskies/handlers/routing.py +0 -62
  332. clearskies/handlers/schema_helper.py +0 -128
  333. clearskies/handlers/simple_routing.py +0 -206
  334. clearskies/handlers/simple_routing_route.py +0 -192
  335. clearskies/handlers/simple_search.py +0 -136
  336. clearskies/handlers/update.py +0 -96
  337. clearskies/handlers/write.py +0 -193
  338. clearskies/input_requirements/__init__.py +0 -78
  339. clearskies/input_requirements/after.py +0 -36
  340. clearskies/input_requirements/before.py +0 -36
  341. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  342. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  343. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  344. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  345. clearskies/input_requirements/maximum_length.py +0 -19
  346. clearskies/input_requirements/maximum_value.py +0 -19
  347. clearskies/input_requirements/minimum_length.py +0 -22
  348. clearskies/input_requirements/minimum_value.py +0 -19
  349. clearskies/input_requirements/requirement.py +0 -25
  350. clearskies/input_requirements/time_delta.py +0 -38
  351. clearskies/input_requirements/unique.py +0 -18
  352. clearskies/mocks/__init__.py +0 -7
  353. clearskies/mocks/input_output.py +0 -124
  354. clearskies/mocks/models.py +0 -142
  355. clearskies/models.py +0 -350
  356. clearskies/security_headers/base.py +0 -12
  357. clearskies/tests/simple_api/models/__init__.py +0 -2
  358. clearskies/tests/simple_api/models/status.py +0 -23
  359. clearskies/tests/simple_api/models/user.py +0 -21
  360. clearskies/tests/simple_api/users_api.py +0 -64
  361. {clear_skies-1.22.10.dist-info → clear_skies-2.0.23.dist-info/licenses}/LICENSE +0 -0
  362. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  363. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  364. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  365. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  366. /clearskies/{secrets/exceptions → exceptions}/not_found.py +0 -0
  367. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  368. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -0,0 +1,478 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Callable
4
+
5
+ from clearskies import configs, decorators
6
+ from clearskies.autodoc.schema import Object as AutoDocObject
7
+ from clearskies.autodoc.schema import Schema as AutoDocSchema
8
+ from clearskies.autodoc.schema import String as AutoDocString
9
+ from clearskies.columns.string import String
10
+ from clearskies.di.inject import InputOutput
11
+ from clearskies.functional import validations
12
+
13
+ if TYPE_CHECKING:
14
+ from clearskies import Model, typing
15
+
16
+
17
+ class BelongsToId(String):
18
+ """
19
+ Declares that this model belongs to another - that it has a parent.
20
+
21
+ ## Usage
22
+
23
+ The way that a belongs to relationship works is that the child model (e.g. the one with
24
+ the BelongsToId column) needs to have a column that stores the id of the parent it is related
25
+ to. Then you can attach the BelongsToModel class and point it to the column containing the
26
+ id. If you allow the end-user to set the parent id in a save action, the belongs to column
27
+ will automatically verify that the given id corresponds to an actual record. Here's a simple
28
+ usage example:
29
+
30
+ ```python
31
+ import clearskies
32
+
33
+
34
+ class Category(clearskies.Model):
35
+ id_column_name = "id"
36
+ backend = clearskies.backends.MemoryBackend()
37
+
38
+ id = clearskies.columns.Uuid()
39
+ name = clearskies.columns.String()
40
+
41
+
42
+ class Product(clearskies.Model):
43
+ id_column_name = "id"
44
+ backend = clearskies.backends.MemoryBackend()
45
+
46
+ id = clearskies.columns.Uuid()
47
+ name = clearskies.columns.String()
48
+ category_id = clearskies.columns.BelongsToId(Category)
49
+ category = clearskies.columns.BelongsToModel("category_id")
50
+
51
+
52
+ def test_belongs_to(products: Product, categories: Category):
53
+ toys = categories.create({"name": "Toys"})
54
+ auto = categories.create({"name": "Auto"})
55
+
56
+ # Note: we set the cateogry by setting "category_id"
57
+ ball = products.create({"name": "ball", "category_id": toys.id})
58
+
59
+ # note: we set the category by saving a category model to "category"
60
+ fidget_spinner = products.create({"name": "Fidget Spinner", "category": toys})
61
+
62
+ return {
63
+ "ball_category": ball.category.name,
64
+ "fidget_spinner_category": fidget_spinner.category.name,
65
+ "ball_id_check": ball.category_id == ball.category.id,
66
+ "ball_fidget_id_check": fidget_spinner.category_id == ball.category.id,
67
+ }
68
+
69
+
70
+ cli = clearskies.contexts.Cli(
71
+ clearskies.endpoints.Callable(test_belongs_to),
72
+ classes=[Category, Product],
73
+ )
74
+
75
+ if __name__ == "__main__":
76
+ cli()
77
+ ```
78
+
79
+ ## Circular Dependency Trees
80
+
81
+ The opposite of a BelongsToId relationship is a HasMany relationship. It's common
82
+ for the child model to contain a BelonsToId column to point to the parent, and then
83
+ have the parent contain a HasMany column to point to the child. This creates circular
84
+ depenency errors in python. To work around this, clearskies requires the addition of
85
+ a "model reference" class that looks like this:
86
+
87
+ ```python
88
+ import some_model
89
+
90
+
91
+ class SomeModelReference:
92
+ def get_model_class(self):
93
+ return some_model.SomeModel
94
+ ```
95
+
96
+ These have to live in their own file, should use relative imports to import the file containing
97
+ the model, and should not be imported into the module they live in. So, sticking with the example
98
+ of categories and products, you would have the following directory structure:
99
+
100
+ ```
101
+ ├── models
102
+ │ ├── category.py
103
+ │ ├── category_reference.py
104
+ │ ├── product.py
105
+ │ └── product_reference.py
106
+
107
+ └── app.py
108
+ ```
109
+
110
+ The files would then contain:
111
+
112
+ category.py
113
+ ```python
114
+ import clearskies
115
+ import models.product_reference
116
+
117
+
118
+ class Category(clearskies.Model):
119
+ id_column_name = "id"
120
+ backend = clearskies.backends.MemoryBackend()
121
+
122
+ id = clearskies.columns.Uuid()
123
+ name = clearskies.columns.String()
124
+ products = clearskies.columns.HasMany(product_reference.ProductReference)
125
+ ```
126
+
127
+ category_reference.py
128
+ ```python
129
+ from clearskies.model import ModelClassReference
130
+ from . import cateogry
131
+
132
+
133
+ class CategoryReference(ModelClassReference):
134
+ def get_model_class(self):
135
+ return category.Category
136
+ ```
137
+
138
+ product.py
139
+ ```python
140
+ import clearskies
141
+ import models.category_reference
142
+
143
+
144
+ class Product(clearskies.Model):
145
+ id_column_name = "id"
146
+ backend = clearskies.backends.MemoryBackend()
147
+
148
+ id = clearskies.columns.Uuid()
149
+ name = clearskies.columns.String()
150
+ category_id = clearskies.columns.BelongsToId(CategoryReference)
151
+ category = clearskies.columns.BelongsToModel("category_id")
152
+ ```
153
+
154
+ product_reference.py
155
+ ```python
156
+ from clearskies.model import ModelClassReference
157
+ from . import product
158
+
159
+
160
+ class ProductReference(ModelClassReference):
161
+ def get_model_class(self):
162
+ return product.Product
163
+ ```
164
+ """
165
+
166
+ """ The model class we belong to. """
167
+ parent_model_class = configs.ModelClass(required=True)
168
+
169
+ """
170
+ The name of the property used to fetch the parent model itself.
171
+
172
+ Note that this isn't set explicitly, but by adding a BelongsToModel column to the model.
173
+ """
174
+ model_column_name = configs.String()
175
+
176
+ """
177
+ The list of columns from the parent that should be included when converting this column to JSON.
178
+
179
+ When configuring readable columns for an endpoint, you can specify the BelongsToModel column.
180
+ If you do this, you must set readable_parent_columns on the BelongsToId column to specify which
181
+ columns from the parent model should be returned in the response. See this example:
182
+
183
+ ```python
184
+ import clearskies
185
+
186
+ class Owner(clearskies.Model):
187
+ id_column_name = "id"
188
+ backend = clearskies.backends.MemoryBackend()
189
+
190
+ id = clearskies.columns.Uuid()
191
+ name = clearskies.columns.String()
192
+
193
+ class Pet(clearskies.Model):
194
+ id_column_name = "id"
195
+ backend = clearskies.backends.MemoryBackend()
196
+
197
+ id = clearskies.columns.Uuid()
198
+ name = clearskies.columns.String()
199
+ owner_id = clearskies.columns.BelongsToId(
200
+ Owner,
201
+ readable_parent_columns=["id", "name"],
202
+ )
203
+ owner = clearskies.columns.BelongsToModel("owner_id")
204
+
205
+ cli = clearskies.contexts.Cli(
206
+ clearskies.endpoints.List(
207
+ Pet,
208
+ sortable_column_names=["id", "name"],
209
+ readable_column_names=["id", "name", "owner"],
210
+ default_sort_column_name="name",
211
+ ),
212
+ classes=[Owner, Pet],
213
+ bindings={
214
+ "memory_backend_default_data": [
215
+ {
216
+ "model_class": Owner,
217
+ "records": [
218
+ {"id": "1-2-3-4", "name": "John Doe"},
219
+ {"id": "5-6-7-8", "name": "Jane Doe"},
220
+ ],
221
+ },
222
+ {
223
+ "model_class": Pet,
224
+ "records": [
225
+ {"id": "a-b-c-d", "name": "Fido", "owner_id": "1-2-3-4"},
226
+ {"id": "e-f-g-h", "name": "Spot", "owner_id": "1-2-3-4"},
227
+ {"id": "i-j-k-l", "name": "Puss in Boots", "owner_id": "5-6-7-8"},
228
+ ],
229
+ },
230
+ ],
231
+ }
232
+ )
233
+
234
+ if __name__ == "__main__":
235
+ cli()
236
+ ```
237
+
238
+ With readable_parent_columns set in the Pet.owner_id column, and owner set in the list configuration,
239
+ The owner id and name are included in the `owner` key of the returned Pet dictionary:
240
+
241
+ ```bash
242
+ $ ./test.py | jq
243
+ {
244
+ "status": "success",
245
+ "error": "",
246
+ "data": [
247
+ {
248
+ "id": "a-b-c-d",
249
+ "name": "Fido",
250
+ "owner": {
251
+ "id": "1-2-3-4",
252
+ "name": "John Doe"
253
+ }
254
+ },
255
+ {
256
+ "id": "i-j-k-l",
257
+ "name": "Puss in Boots",
258
+ "owner": {
259
+ "id": "5-6-7-8",
260
+ "name": "Jane Doe"
261
+ }
262
+ },
263
+ {
264
+ "id": "e-f-g-h",
265
+ "name": "Spot",
266
+ "owner": {
267
+ "id": "1-2-3-4",
268
+ "name": "John Doe"
269
+ }
270
+ }
271
+ ],
272
+ "pagination": {},
273
+ "input_errors": {}
274
+ }
275
+ ```
276
+
277
+ """
278
+ readable_parent_columns = configs.ReadableModelColumns("parent_model_class")
279
+
280
+ """
281
+ The type of join to use when searching on the parent.
282
+ """
283
+ join_type = configs.Select(["LEFT", "INNER", "RIGHT"], default="LEFT")
284
+
285
+ """
286
+ Any additional conditions to place on the parent table when finding related records.
287
+
288
+ where should be a list containing a combination of conditions-as-strings, queries built from the columns
289
+ themselves, or callable functions which accept the model and apply filters. This is primarily used in
290
+ input validation to exclude values as allowed parents.
291
+ """
292
+ where = configs.Conditions()
293
+
294
+ input_output = InputOutput()
295
+ wants_n_plus_one = True
296
+ _allowed_search_operators = ["="]
297
+ _descriptor_config_map = None
298
+
299
+ @decorators.parameters_to_properties
300
+ def __init__(
301
+ self,
302
+ parent_model_class,
303
+ readable_parent_columns: list[str] = [],
304
+ join_type: str | None = None,
305
+ where: typing.condition | list[typing.condition] = [],
306
+ default: str | None = None,
307
+ setable: str | Callable | None = None,
308
+ is_readable: bool = True,
309
+ is_writeable: bool = True,
310
+ is_searchable: bool = True,
311
+ is_temporary: bool = False,
312
+ validators: typing.validator | list[typing.validator] = [],
313
+ on_change_pre_save: typing.action | list[typing.action] = [],
314
+ on_change_post_save: typing.action | list[typing.action] = [],
315
+ on_change_save_finished: typing.action | list[typing.action] = [],
316
+ created_by_source_type: str = "",
317
+ created_by_source_key: str = "",
318
+ created_by_source_strict: bool = True,
319
+ ):
320
+ pass
321
+
322
+ @property
323
+ def parent_model(self) -> Model:
324
+ parents = self.di.build(self.parent_model_class, cache=True)
325
+ if not self.where:
326
+ return parents
327
+
328
+ return self.apply_wheres(parents)
329
+
330
+ def apply_wheres(self, parents: Model) -> Model:
331
+ if not self.where:
332
+ return parents
333
+
334
+ for index, where in enumerate(self.where):
335
+ if callable(where):
336
+ parents = self.di.call_function(where, model=parents, **self.input_output.get_context_for_callables())
337
+ if not validations.is_model(parents):
338
+ raise ValueError(
339
+ f"Configuration error for {self.model_class.__name__}.{self.name}: when 'where' is a callable, it must return a model class, but when the callable in where entry #{index + 1} was called, it returned something else."
340
+ )
341
+ else:
342
+ parents = parents.where(where)
343
+ return parents
344
+
345
+ @property
346
+ def parent_columns(self) -> dict[str, Any]:
347
+ return self.parent_model_class.get_columns()
348
+
349
+ def input_error_for_value(self, value: str, operator: str | None = None) -> str:
350
+ parent_check = super().input_error_for_value(value)
351
+ if parent_check:
352
+ return parent_check
353
+ parent_model = self.parent_model
354
+ matching_parents = parent_model.where(f"{parent_model.id_column_name}={value}")
355
+ matching_parents = self.apply_wheres(matching_parents)
356
+ matching_parents = matching_parents.where_for_request_all(
357
+ matching_parents,
358
+ self.input_output,
359
+ routing_data=self.input_output.routing_data,
360
+ authorization_data=self.input_output.authorization_data,
361
+ )
362
+ if not len(matching_parents):
363
+ return f"Invalid selection for {self.name}: record does not exist"
364
+ return ""
365
+
366
+ def n_plus_one_add_joins(self, model: Model, column_names: list[str] = []) -> Model:
367
+ """Add any additional joins to solve the N+1 problem."""
368
+ if not column_names:
369
+ column_names = self.readable_parent_columns
370
+ if not column_names:
371
+ return model
372
+
373
+ model = self.add_join(model)
374
+ alias = self.join_table_alias()
375
+ parent_id_column_name = self.parent_model.id_column_name
376
+ select_parts = [f"{alias}.{column_name} AS {alias}_{column_name}" for column_name in column_names]
377
+ if parent_id_column_name not in column_names:
378
+ select_parts.append(f"{alias}.{parent_id_column_name} AS {alias}_{parent_id_column_name}")
379
+ return model.select(", ".join(select_parts))
380
+
381
+ def add_join(self, model: Model) -> Model:
382
+ parent_table = self.parent_model.destination_name()
383
+ alias = self.join_table_alias()
384
+
385
+ if model.is_joined(parent_table, alias=alias):
386
+ return model
387
+
388
+ join_type = "LEFT " if self.join_type == "LEFT" else ""
389
+ own_table_name = model.destination_name()
390
+ parent_id_column_name = self.parent_model.id_column_name
391
+ return model.join(
392
+ f"{join_type}JOIN {parent_table} as {alias} on {alias}.{parent_id_column_name}={own_table_name}.{self.name}"
393
+ )
394
+
395
+ def join_table_alias(self) -> str:
396
+ return self.parent_model.destination_name() + "_" + self.name
397
+
398
+ def is_allowed_operator(self, operator, relationship_reference=None):
399
+ """Proces user data to decide if the end-user is specifying an allowed operator."""
400
+ if not relationship_reference:
401
+ return "="
402
+ parent_columns = self.parent_columns
403
+ if relationship_reference not in self.parent_columns:
404
+ raise ValueError(
405
+ "I was asked to search on a related column that doens't exist. This shouldn't have happened :("
406
+ )
407
+ return self.parent_columns[relationship_reference].is_allowed_operator(operator)
408
+
409
+ def check_search_value(self, value, operator=None, relationship_reference=None):
410
+ if not relationship_reference:
411
+ return self.input_error_for_value(value, operator=operator)
412
+ parent_columns = self.parent_columns
413
+ if relationship_reference not in self.parent_columns:
414
+ raise ValueError(
415
+ "I was asked to search on a related column that doens't exist. This shouldn't have happened :("
416
+ )
417
+ return self.parent_columns[relationship_reference].check_search_value(value, operator=operator)
418
+
419
+ def is_allowed_search_operator(self, operator: str, relationship_reference: str = "") -> bool:
420
+ if not relationship_reference:
421
+ return operator in self._allowed_search_operators
422
+ parent_columns = self.parent_columns
423
+ if relationship_reference not in self.parent_columns:
424
+ raise ValueError(
425
+ "I was asked to check search operators on a related column that doens't exist. This shouldn't have happened :("
426
+ )
427
+ return self.parent_columns[relationship_reference].is_allowed_search_operator(
428
+ operator, relationship_reference=relationship_reference
429
+ )
430
+
431
+ def allowed_search_operators(self, relationship_reference: str = ""):
432
+ if not relationship_reference:
433
+ return self._allowed_search_operators
434
+ parent_columns = self.parent_columns
435
+ if relationship_reference not in self.parent_columns:
436
+ raise ValueError(
437
+ "I was asked for allowed search operators on a related column that doens't exist. This shouldn't have happened :("
438
+ )
439
+ return self.parent_columns[relationship_reference].allowed_search_operators()
440
+
441
+ def add_search(self, model: Model, value: str, operator: str = "", relationship_reference: str = "") -> Model:
442
+ if not relationship_reference:
443
+ return super().add_search(model, value, operator=operator)
444
+
445
+ if relationship_reference not in self.parent_columns:
446
+ raise ValueError(
447
+ "I was asked to search on a related column that doens't exist. This shouldn't have happened :("
448
+ )
449
+
450
+ model = self.add_join(model)
451
+ related_column = self.parent_columns[relationship_reference]
452
+ alias = self.join_table_alias()
453
+ return model.where(related_column.build_condition(value, operator=operator, column_prefix=f"{alias}."))
454
+
455
+ def documentation(
456
+ self, name: str | None = None, example: str | None = None, value: str | None = None
457
+ ) -> list[AutoDocSchema]:
458
+ columns = self.parent_columns
459
+ parent_id_column_name = self.parent_model.id_column_name
460
+ parent_properties = [columns[parent_id_column_name].documentation()]
461
+ parent_id_doc = AutoDocString(name if name is not None else self.name)
462
+
463
+ readable_parent_columns = self.readable_parent_columns
464
+ if not readable_parent_columns:
465
+ return [parent_id_doc]
466
+
467
+ for column_name in readable_parent_columns:
468
+ if column_name == parent_id_column_name:
469
+ continue
470
+ parent_properties.append(columns[column_name].documentation())
471
+
472
+ return [
473
+ parent_id_doc,
474
+ AutoDocObject(
475
+ self.model_column_name,
476
+ parent_properties,
477
+ ),
478
+ ]
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import OrderedDict
4
+ from typing import TYPE_CHECKING, Any, Self, overload
5
+
6
+ from clearskies import configs, decorators
7
+ from clearskies.column import Column
8
+ from clearskies.columns.belongs_to_id import BelongsToId
9
+ from clearskies.functional import validations
10
+
11
+ if TYPE_CHECKING:
12
+ from clearskies import Model
13
+
14
+
15
+ class BelongsToModel(Column):
16
+ """Return the model object for a belongs to relationship."""
17
+
18
+ """ The name of the belongs to column we are connected to. """
19
+ belongs_to_column_name = configs.ModelColumn(required=True)
20
+
21
+ is_temporary = configs.boolean.Boolean(default=True)
22
+ _descriptor_config_map = None
23
+
24
+ @decorators.parameters_to_properties
25
+ def __init__(
26
+ self,
27
+ belongs_to_column_name: str,
28
+ ):
29
+ pass
30
+
31
+ def finalize_configuration(self, model_class: type, name: str) -> None:
32
+ """Finalize and check the configuration."""
33
+ getattr(self.__class__, "belongs_to_column_name").set_model_class(model_class)
34
+ self.model_class = model_class
35
+ self.name = name
36
+ self.finalize_and_validate_configuration()
37
+
38
+ # finally, let the belongs to column know about us and make sure it's the right thing.
39
+ belongs_to_column = getattr(model_class, self.belongs_to_column_name)
40
+ if not isinstance(belongs_to_column, BelongsToId):
41
+ raise ValueError(
42
+ f"Error with configuration for {model_class.__name__}.{name}, which is a BelongsToModel. It needs to point to a belongs to column, and it was told to use {model_class.__name__}.{self.belongs_to_column_name}, but this is not a BelongsToId column."
43
+ )
44
+ belongs_to_column.model_column_name = name
45
+
46
+ @overload
47
+ def __get__(self, model: None, cls: type[Model]) -> Self:
48
+ pass
49
+
50
+ @overload
51
+ def __get__(self, model: Model, cls: type[Model]) -> Model:
52
+ pass
53
+
54
+ def __get__(self, model, cls):
55
+ if model is None:
56
+ self.model_class = cls
57
+ return self # type: ignore
58
+
59
+ # this makes sure we're initialized
60
+ if "name" not in self._config: # type: ignore
61
+ model.get_columns()
62
+
63
+ belongs_to_column = getattr(model.__class__, self.belongs_to_column_name)
64
+ parent_id = getattr(model, self.belongs_to_column_name)
65
+ parent_class = belongs_to_column.parent_model_class
66
+ parent_model = self.di.build(parent_class, cache=False)
67
+ if not parent_id:
68
+ return parent_model.empty()
69
+
70
+ parent_id_column_name = parent_model.id_column_name
71
+ join_alias = belongs_to_column.join_table_alias()
72
+ raw_data = model.get_raw_data()
73
+
74
+ # sometimes the model is loaded via the N+1 functionality, in which case the data will already exist
75
+ # in model.data but hiding under a different name.
76
+ if raw_data.get(f"{join_alias}.{parent_id_column_name}"):
77
+ parent_data = {parent_id_column_name: raw_data[f"{join_alias}_{parent_id_column_name}"]}
78
+ for column_name in belongs_to_column.readable_parent_columns:
79
+ select_alias = f"{join_alias}_{column_name}"
80
+ parent_data[column_name] = raw_data[select_alias] if select_alias in raw_data else None
81
+ return parent_model.model(parent_data)
82
+
83
+ return parent_model.find(f"{parent_id_column_name}={parent_id}")
84
+
85
+ def __set__(self, model: Model, value: Model) -> None:
86
+ # this makes sure we're initialized
87
+ if "name" not in self._config: # type: ignore
88
+ model.get_columns()
89
+
90
+ setattr(model, self.belongs_to_column_name, getattr(value, value.id_column_name))
91
+
92
+ def pre_save(self, data: dict[str, Any], model: Model) -> dict[str, Any]:
93
+ # if we have a model coming in then we want to extract the id. Either way, the model id needs to go to the
94
+ # belongs_to_id column, which is the only one that is actually saved.
95
+ if self.name in data:
96
+ value = data[self.name]
97
+ data[self.belongs_to_column_name] = (
98
+ getattr(value, value.id_column_name) if validations.is_model(value) else value
99
+ )
100
+ return super().pre_save(data, model)
101
+
102
+ def add_join(self, model: Model) -> Model:
103
+ return getattr(model.__class__, self.belongs_to_column_name).add_join(model)
104
+
105
+ def join_table_alias(self) -> str:
106
+ return getattr(self.model_class, self.belongs_to_column_name).join_table_alias()
107
+
108
+ def add_search(self, model: Model, value: str, operator: str = "", relationship_reference: str = "") -> Model:
109
+ return getattr(self.model_class, self.belongs_to_column_name).add_search(
110
+ model, value, operator, relationship_reference=relationship_reference
111
+ )
112
+
113
+ def to_json(self, model: Model) -> dict[str, Any]:
114
+ """Convert the column into a json-friendly representation."""
115
+ belongs_to_column = getattr(model.__class__, self.belongs_to_column_name)
116
+ if not belongs_to_column.readable_parent_columns:
117
+ raise ValueError(
118
+ f"Configuration error for {model.__class__.__name__}: I can't convert to JSON unless you set readable_parent_columns on my parent attribute, {model.__class__.__name__}.{self.belongs_to_column_name}."
119
+ )
120
+
121
+ # otherwise return an object with the readable parent columns
122
+ columns = belongs_to_column.parent_columns
123
+ parent = getattr(model, self.name)
124
+ json: dict[str, Any] = OrderedDict()
125
+ for column_name in belongs_to_column.readable_parent_columns:
126
+ json = {**json, **columns[column_name].to_json(parent)} # type: ignore
127
+ return {
128
+ self.name: json,
129
+ }