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
@@ -1,271 +0,0 @@
1
- import re
2
- from .string import String
3
- from ..autodoc.schema import Array as AutoDocArray
4
- from ..autodoc.schema import Object as AutoDocObject
5
- from ..autodoc.schema import String as AutoDocString
6
- from collections import OrderedDict
7
-
8
-
9
- class BelongsTo(String):
10
- """
11
- Controls a belongs to relationship.
12
-
13
- This column should be named something like 'parent_id', e.g. user_id, column_id, etc... It expects the actual
14
- database column to be an integer. It also provides an additional property on the model which returns the
15
- related model, instead of the id, with a name given by dropping `_id` from the column name. In other words,
16
- if you have a column called user_id and a particular model has a user_id of 5, then:
17
-
18
- ```
19
- print(model.user_id)
20
- # prints 5
21
- print(model.user.id)
22
- # prints 5
23
- print(model.user.name)
24
- # prints the name of the user with an id of 5.
25
- ```
26
- """
27
-
28
- wants_n_plus_one = True
29
- required_configs = [
30
- "parent_models_class",
31
- ]
32
-
33
- my_configs = [
34
- "model_column_name",
35
- "readable_parent_columns",
36
- "join_type",
37
- "where",
38
- ]
39
-
40
- def __init__(self, di):
41
- super().__init__(di)
42
-
43
- def _check_configuration(self, configuration):
44
- super()._check_configuration(configuration)
45
- self.validate_models_class(configuration["parent_models_class"])
46
-
47
- error_prefix = f"Configuration error for '{self.name}' in '{self.model_class.__name__}':"
48
- if not configuration.get("model_column_name") and self.name[-3:] != "_id":
49
- raise ValueError(
50
- f"Invalid name for column '{self.name}' in '{self.model_class.__name__}' - "
51
- + "BelongsTo column names must end in '_id', or you must set 'model_column_name' to specify the name of the column "
52
- + "that the parent model can be fetched from."
53
- )
54
- if configuration.get("model_column_name") and type(configuration.get("model_column_name")) != str:
55
- raise ValueError(f"{error_prefix} 'model_column_name' must be a string.")
56
-
57
- join_type = configuration.get("join_type")
58
- if join_type and join_type.upper() not in ["LEFT", "INNER"]:
59
- raise ValueError(f"{error_prefix} join_type must be INNER or LEFT")
60
-
61
- if configuration.get("readable_parent_columns"):
62
- parent_columns = self.di.build(configuration["parent_models_class"], cache=True).raw_columns_configuration()
63
- readable_parent_columns = configuration["readable_parent_columns"]
64
- if not hasattr(readable_parent_columns, "__iter__"):
65
- raise ValueError(
66
- f"{error_prefix} 'readable_parent_columns' should be an iterable "
67
- + "with the list of child columns to output."
68
- )
69
- if isinstance(readable_parent_columns, str):
70
- raise ValueError(
71
- f"{error_prefix} 'readable_parent_columns' should be an iterable "
72
- + "with the list of child columns to output."
73
- )
74
- for column_name in readable_parent_columns:
75
- if column_name not in parent_columns:
76
- raise ValueError(
77
- f"{error_prefix} 'readable_parent_columns' references column named '{column_name}' but this"
78
- + "column does not exist in the model class."
79
- )
80
-
81
- wheres = configuration.get("where")
82
- if wheres:
83
- if not isinstance(wheres, list):
84
- raise ValueError(
85
- f"{error_prefix} 'where' must be a list of where conditions or callables that return where conditions"
86
- )
87
- for index, where in enumerate(wheres):
88
- if callable(where) or isinstance(where, str):
89
- continue
90
- raise ValueError(
91
- f"{error_prefix} 'where' must be a list of where conditions or callables that return where conditions, but the item in entry #${index+1} was neither a string nor a callable"
92
- )
93
-
94
- def _finalize_configuration(self, configuration):
95
- return {
96
- **super()._finalize_configuration(configuration),
97
- **{
98
- "model_column_name": configuration.get("model_column_name")
99
- if configuration.get("model_column_name")
100
- else self.name[:-3],
101
- "join_type": configuration.get("join_type", "INNER").upper(),
102
- "where": configuration.get("where", []),
103
- },
104
- }
105
-
106
- def input_error_for_value(self, value, operator=None):
107
- integer_check = super().input_error_for_value(value)
108
- if integer_check:
109
- return integer_check
110
- parent_models = self.parent_models
111
- id_column_name = parent_models.get_id_column_name()
112
- matching_parents = parent_models.where(f"{id_column_name}={value}")
113
- input_output = self.di.build("input_output", cache=True)
114
- matching_parents = matching_parents.where_for_request(
115
- matching_parents,
116
- input_output.routing_data(),
117
- input_output.get_authorization_data(),
118
- input_output,
119
- )
120
- if not len(matching_parents):
121
- return f"Invalid selection for {self.name}: record does not exist"
122
- return ""
123
-
124
- def can_provide(self, column_name):
125
- return column_name == self.config("model_column_name")
126
-
127
- def provide(self, data, column_name):
128
- # did we have data parent data loaded up with a query?
129
- alias = self.join_table_alias()
130
- parent_id_column_name = self.parent_models.get_id_column_name()
131
- if f"{alias}_{parent_id_column_name}" in data:
132
- parent_data = {parent_id_column_name: data[f"{alias}_{parent_id_column_name}"]}
133
- for column_name in self.parent_columns.keys():
134
- select_alias = f"{alias}_{column_name}"
135
- parent_data[column_name] = data[select_alias] if select_alias in data else None
136
- return self.parent_models.model(parent_data)
137
-
138
- # if not, just look it up from the id
139
- parent_id = data.get(self.name)
140
- if parent_id:
141
- parent_id_column_name = self.parent_models.get_id_column_name()
142
- return self.parent_models.where(f"{parent_id_column_name}={parent_id}").first()
143
- return self.parent_models.empty_model()
144
-
145
- def join_table_alias(self):
146
- return self.parent_models.table_name() + "_" + self.name
147
-
148
- def configure_n_plus_one(self, models, columns=None):
149
- if columns is None:
150
- columns = self.config("readable_parent_columns", silent=True)
151
- if not columns:
152
- return models
153
-
154
- models = self.add_join(models)
155
- alias = self.join_table_alias()
156
- parent_id_column_name = self.parent_models.get_id_column_name()
157
- select_parts = [f"{alias}.{column_name} AS {alias}_{column_name}" for column_name in columns]
158
- select_parts.append(f"{alias}.{parent_id_column_name} AS {alias}_{parent_id_column_name}")
159
- return models.select(", ".join(select_parts))
160
-
161
- @property
162
- def parent_models(self):
163
- parents = self.di.build(self.config("parent_models_class"), cache=True)
164
- for where in self.config("where"):
165
- if callable(where):
166
- parents = self.di.call_function(where, model=parents)
167
- if not parents:
168
- raise ValueError(
169
- f"Configuration error for column '{self.name}' in model '{self.model_class.__name__}': when 'where' is a callable, it must return a models class, but when the callable in where entry #{index+1} was called, it did not return the models class"
170
- )
171
- else:
172
- parents = parents.where(where)
173
- return parents
174
-
175
- @property
176
- def parent_columns(self):
177
- return self.parent_models.model_columns
178
-
179
- def to_json(self, model):
180
- # if we don't have readable parent columns specified, then just return the id
181
- if not self.config("readable_parent_columns", silent=True):
182
- return super().to_json(model)
183
-
184
- # otherwise return an object with the readable parent columns
185
- columns = self.parent_columns
186
- parent = model.__getattr__(self.config("model_column_name"))
187
- json = OrderedDict()
188
- if parent.id_column_name not in self.config("readable_parent_columns"):
189
- json[parent.id_column_name] = list(columns[parent.id_column_name].to_json(parent).values())[0]
190
- for column_name in self.config("readable_parent_columns"):
191
- json = {**json, **columns[column_name].to_json(parent)}
192
- id_less_name = self.config("model_column_name")
193
- return {
194
- **super().to_json(model),
195
- id_less_name: json,
196
- }
197
-
198
- def documentation(self, name=None, example=None, value=None):
199
- columns = self.parent_columns
200
- parent_id_column_name = self.parent_models.get_id_column_name()
201
- parent_properties = [columns[parent_id_column_name].documentation()]
202
-
203
- parent_columns = self.config("readable_parent_columns", silent=True)
204
- parent_id_doc = AutoDocString(name if name is not None else self.name)
205
- if not parent_columns:
206
- return parent_id_doc
207
-
208
- for column_name in self.config("readable_parent_columns"):
209
- if column_name == parent_id_column_name:
210
- continue
211
- parent_properties.append(columns[column_name].documentation())
212
-
213
- return [
214
- parent_id_doc,
215
- AutoDocObject(
216
- self.config("model_column_name"),
217
- parent_properties,
218
- ),
219
- ]
220
-
221
- def is_allowed_operator(self, operator, relationship_reference=None):
222
- """
223
- This is called when processing user data to decide if the end-user is specifying an allowed operator
224
- """
225
- if not relationship_reference:
226
- return "="
227
- parent_columns = self.parent_columns
228
- if relationship_reference not in self.parent_columns:
229
- raise ValueError(
230
- "I was asked to search on a related column that doens't exist. This shouldn't have happened :("
231
- )
232
- return self.parent_columns[relationship_reference].is_allowed_operator(operator)
233
-
234
- def check_search_value(self, value, operator=None, relationship_reference=None):
235
- if not relationship_reference:
236
- return self.input_error_for_value(value, operator=operator)
237
- parent_columns = self.parent_columns
238
- if relationship_reference not in self.parent_columns:
239
- raise ValueError(
240
- "I was asked to search on a related column that doens't exist. This shouldn't have happened :("
241
- )
242
- return self.parent_columns[relationship_reference].check_search_value(value, operator=operator)
243
-
244
- def add_join(self, models):
245
- parent_table = self.parent_models.table_name()
246
- alias = self.join_table_alias()
247
-
248
- if models.is_joined(parent_table, alias=alias):
249
- return models
250
-
251
- join_type = "LEFT " if self.config("join_type") == "LEFT" else ""
252
- own_table_name = models.table_name()
253
- parent_id_column_name = self.parent_models.get_id_column_name()
254
- return models.join(
255
- f"{join_type}JOIN {parent_table} as {alias} on {alias}.{parent_id_column_name}={own_table_name}.{self.name}"
256
- )
257
-
258
- def add_search(self, models, value, operator=None, relationship_reference=None):
259
- if not relationship_reference:
260
- return super().add_search(models, value, operator=operator)
261
-
262
- parent_columns = self.parent_columns
263
- if relationship_reference not in self.parent_columns:
264
- raise ValueError(
265
- "I was asked to search on a related column that doens't exist. This shouldn't have happened :("
266
- )
267
-
268
- models = self.add_join(models)
269
- related_column = self.parent_columns[relationship_reference]
270
- alias = self.join_table_alias()
271
- return models.where(related_column.build_condition(value, operator=operator, column_prefix=f"{alias}."))
@@ -1,60 +0,0 @@
1
- from .column import Column
2
- from ..autodoc.schema import Boolean as AutoDocBoolean
3
-
4
-
5
- class Boolean(Column):
6
- _auto_doc_class = AutoDocBoolean
7
-
8
- my_configs = {
9
- "on_true": None,
10
- "on_false": None,
11
- }
12
-
13
- def __init__(self, di):
14
- super().__init__(di)
15
-
16
- def _check_configuration(self, configuration):
17
- """Check the configuration and throw exceptions as needed"""
18
- super()._check_configuration(configuration)
19
- for trigger in ["on_true", "on_false"]:
20
- if configuration.get(trigger):
21
- self._check_actions(configuration[trigger], trigger)
22
-
23
- def to_backend(self, data):
24
- if self.name not in data or data[self.name] is None:
25
- return data
26
-
27
- return {
28
- **data,
29
- self.name: bool(data[self.name]),
30
- }
31
-
32
- def from_backend(self, value):
33
- return bool(value)
34
-
35
- def input_error_for_value(self, value, operator=None):
36
- return f"{self.name} must be a boolean" if type(value) != bool else ""
37
-
38
- def build_condition(self, value, operator=None, column_prefix=""):
39
- condition_value = "1" if value else "0"
40
- if not operator:
41
- operator = "="
42
- return f"{column_prefix}{self.name}{operator}{condition_value}"
43
-
44
- def save_finished(self, model):
45
- """
46
- Make any necessary changes needed after a save has completely finished.
47
- """
48
- super().save_finished(model)
49
-
50
- on_true = self.config("on_true", silent=True)
51
- on_false = self.config("on_false", silent=True)
52
- if not on_true and not on_false:
53
- return
54
- if not model.was_changed(self.name):
55
- return
56
-
57
- if model.get(self.name) and on_true:
58
- self.execute_actions(on_true, model)
59
- if not model.get(self.name) and on_false:
60
- self.execute_actions(on_false, model)
@@ -1,304 +0,0 @@
1
- import re
2
- from .belongs_to import BelongsTo
3
- from ..autodoc.schema import Array as AutoDocArray
4
- from ..autodoc.schema import Object as AutoDocObject
5
- from ..autodoc.schema import String as AutoDocString
6
- from collections import OrderedDict
7
-
8
-
9
- class CategoryTree(BelongsTo):
10
- """
11
- Builds a tree table for quick lookups in a category heirarchy.
12
-
13
- Imagine you have a model that represents a category heirarchy:
14
-
15
- ```
16
- CREATE TABLE categories (
17
- id varchar(255),
18
- parent_id varchar(255),
19
- name varchar(255)
20
- )
21
- ```
22
-
23
- Where `parent_id` references a record in the same categories table - a category tree! This works
24
- fine but it gets tricky when you want to answer the question "what are all the parent categories
25
- of X category?" or "what are all the child categories of Y category?". This column class solves that
26
- by building a tree table that caches this data as the categories are updated. That table should look
27
- like this:
28
-
29
- ```
30
- CREATE TABLE category_tree (
31
- id varchar(255),
32
- parent_id varchar(255),
33
- child_id varchar(255),
34
- is_parent tinyint(1),
35
- level tinyint(1),
36
- )
37
- ```
38
-
39
- (add indexes as desired). You then you have your corresponding models:
40
-
41
- ```
42
- import clearskies
43
-
44
- class CategoryTree(clearskies.Model):
45
- def __init__(self, cursor_backend, columns):
46
- super().__init__(cursor_backend, columns)
47
-
48
- def columns_configuration(self):
49
- return OrderedDict([
50
- clearskies.column_types.string('parent_id'),
51
- clearskies.column_types.string('child_id'),
52
- clearskies.column_types.integer('is_parent'),
53
- clearskies.column_types.integer('level'),
54
- ])
55
-
56
- class Category(clearskies.Model):
57
- def __init__(self, cursor_backend, columns):
58
- super().__init__(cursor_backend, columns)
59
-
60
- def columns_configuration(self):
61
- return OrderedDict([
62
- clearskies.column_types.string('name'),
63
- clearskies.column_types.category_tree('parent_id', tree_models_class=CategoryTree),
64
- ])
65
- ```
66
-
67
- You would then build your cateogry tree normally:
68
-
69
- ```
70
- # categories object comes in from dependency injection
71
- root_category = categories.create({'name': 'my root category'})
72
- sub_category = categories.create({'name': 'my sub category', parent_id=root_category.id})
73
- alt_sub_category = categories.create({'name': 'my alternate sub category', parent_id=root_category.id})
74
- sub_sub_category = categories.create({'name': 'my sub-sub category', parent_id=sub_category.id})
75
- sub_sub_sub_category = categories.create({'name': 'my sub-sub-sub category', parent_id=sub_sub_category.id})
76
- ```
77
-
78
- and your database would look like this (using auto-incrementing ids for hopeful clarity)
79
-
80
- ```
81
- $ SELECT * FROM category_tree
82
-
83
- parent_id | child_id | is_parent | level
84
- 1 | 2 | 1 | 0 # sub category
85
- 1 | 3 | 1 | 0 # alt sub category
86
- 1 | 4 | 0 | 0 # sub sub category referencing the root category
87
- 2 | 4 | 1 | 1 # sub sub category referencing the sub category
88
- 1 | 5 | 0 | 0 # sub sub sub category referencing the root category
89
- 2 | 5 | 0 | 1 # sub sub sub category referencing the sub category
90
- 4 | 5 | 1 | 2 # sub sub sub category referencing the sub sub category
91
- ```
92
-
93
- You can then use various SQL statements to efficiently fetch various pieces of data.
94
-
95
- ```
96
- # the category tree for a specific category
97
- SELECT parent_id FROM category_tree WHERE child_id=5 ORDER BY level DESC;
98
- # all the children of a parent (excluding sub-children)
99
- SELECT child_id FROM category_tree WHERE parent_id=1 AND is_parent=1
100
- # All the children of a parent (including sub-children)
101
- SELECT child_id FROM category_tree WHERE parent_id=1;
102
- ```
103
-
104
- Of course other kinds of databases are better at this (such as graph databases), but it isn't
105
- always worth managing another database unless performance is becoming a problem.
106
- """
107
-
108
- required_configs = [
109
- "tree_models_class",
110
- ]
111
-
112
- my_configs = [
113
- "model_column_name",
114
- "readable_parent_columns",
115
- "join_type",
116
- "tree_parent_id_column_name",
117
- "tree_child_id_column_name",
118
- "tree_is_parent_column_name",
119
- "tree_level_column_name",
120
- "max_iterations",
121
- "parent_models_class",
122
- "children_column_name",
123
- "descendents_column_name",
124
- "ancestors_column_name",
125
- "load_relatives_strategy",
126
- ]
127
-
128
- def __init__(self, di):
129
- super().__init__(di)
130
-
131
- def _check_configuration(self, configuration):
132
- # our parent class is the BelongsTo which needs to know the parent model class.
133
- # with a category tree, we _are_ our own parent model class, so no need to ask for it.
134
- super()._check_configuration(
135
- {
136
- **configuration,
137
- "parent_models_class": configuration.get("parent_models_class", self.model_class),
138
- }
139
- )
140
- self.validate_models_class(
141
- configuration["tree_models_class"],
142
- config_name="tree_models_class",
143
- )
144
- load_relatives_strategy = configuration.get("load_relatives_strategy", None)
145
- if load_relatives_strategy and load_relatives_strategy not in ["join", "where_in", "individual"]:
146
- raise ValueError(
147
- f"Configuration error for category_tree column '{self.name} in model class '{self.model_class.__name__}': load_relatives_strategy must be one of ['join', 'where_in', or 'individual']"
148
- )
149
-
150
- def _finalize_configuration(self, configuration):
151
- return {
152
- **super()._finalize_configuration(
153
- {
154
- **configuration,
155
- "parent_models_class": configuration.get("parent_models_class", self.model_class),
156
- }
157
- ),
158
- **{
159
- "tree_parent_id_column_name": configuration.get("tree_parent_id_column_name", "parent_id"),
160
- "tree_child_id_column_name": configuration.get("tree_child_id_column_name", "child_id"),
161
- "tree_is_parent_column_name": configuration.get("tree_is_parent_column_name", "is_parent"),
162
- "tree_level_column_name": configuration.get("tree_level_column_name", "level"),
163
- "max_iterations": configuration.get("max_iterations", 100),
164
- "children_column_name": configuration.get("children_column_name", "children"),
165
- "descendents_column_name": configuration.get("descendents_column_name", "descendents"),
166
- "ancestors_column_name": configuration.get("ancestors_column_name", "ancestors"),
167
- "load_relatives_strategy": configuration.get("load_relatives_strategy", "join"),
168
- },
169
- }
170
-
171
- @property
172
- def tree_models(self):
173
- return self.di.build(self.config("tree_models_class"), cache=True)
174
-
175
- def post_save(self, data, model, id):
176
- if not model.is_changing(self.name, data):
177
- return data
178
-
179
- self.update_tree_table(model, id, model.latest(self.name, data))
180
- return data
181
-
182
- def force_tree_update(self, model):
183
- self.update_tree_table(model, model.get(self.id_column_name), model.__getattr__(self.name))
184
-
185
- def update_tree_table(self, model, child_id, direct_parent_id):
186
- tree_models = self.tree_models
187
- parent_models = self.parent_models
188
- model_column_name = self.config("model_column_name")
189
- tree_parent_id_column_name = self.config("tree_parent_id_column_name")
190
- tree_child_id_column_name = self.config("tree_child_id_column_name")
191
- tree_is_parent_column_name = self.config("tree_is_parent_column_name")
192
- tree_level_column_name = self.config("tree_level_column_name")
193
- max_iterations = self.config("max_iterations")
194
-
195
- # we're going to be lazy and just delete the data for the current record in the tree table,
196
- # and then re-insert everything (but we can skip this if creating a new record)
197
- if model.exists:
198
- for tree in tree_models.where(f"{tree_child_id_column_name}={child_id}"):
199
- tree.delete()
200
-
201
- # if we are a root category then we don't have a tree
202
- if not direct_parent_id:
203
- return
204
-
205
- is_root = False
206
- id_column_name = parent_models.id_column_name
207
- next_parent = parent_models.find(f"{id_column_name}={direct_parent_id}")
208
- tree = []
209
- c = 0
210
- while not is_root:
211
- c += 1
212
- if c > max_iterations:
213
- self._circular(max_iterations)
214
-
215
- tree.append(next_parent.__getattr__(next_parent.id_column_name))
216
- if not next_parent.__getattr__(self.name):
217
- is_root = True
218
- else:
219
- next_next_parent_id = next_parent.__getattr__(self.name)
220
- next_parent = parent_models.find(f"{id_column_name}={next_next_parent_id}")
221
-
222
- tree.reverse()
223
- for index, parent_id in enumerate(tree):
224
- tree_models.create(
225
- {
226
- tree_parent_id_column_name: parent_id,
227
- tree_child_id_column_name: child_id,
228
- tree_is_parent_column_name: 1 if parent_id == direct_parent_id else 0,
229
- tree_level_column_name: index,
230
- }
231
- )
232
-
233
- def _circular(self, max_iterations):
234
- raise ValueError(
235
- f"Error for column '{self.name}' for model class '{self.model_class.__name__}': "
236
- + f"I've climbed through {max_iterations} parents and haven't found the root yet."
237
- + "You may have accidentally created a circular cateogry tree. If not, and your category tree "
238
- + "really _is_ that deep, then adjust the 'max_iterations' configuration for this column accordingly. "
239
- )
240
-
241
- def can_provide(self, column_name):
242
- return column_name in [
243
- self.config("model_column_name"),
244
- self.config("children_column_name"),
245
- self.config("descendents_column_name"),
246
- self.config("ancestors_column_name"),
247
- ]
248
-
249
- def provide(self, data, column_name):
250
- if column_name == self.config("model_column_name"):
251
- return super().provide(data, column_name)
252
-
253
- if column_name == self.config("children_column_name"):
254
- return self.relatives(data)
255
-
256
- if column_name == self.config("descendents_column_name"):
257
- return self.relatives(data, include_all=True)
258
-
259
- if column_name == self.config("ancestors_column_name"):
260
- return self.relatives(data, find_parents=True, include_all=True)
261
-
262
- def relatives(self, data, include_all=False, find_parents=False):
263
- id_column_name = self.model_class.id_column_name
264
- model_id = data[self.model_class.id_column_name]
265
- model_table_name = self.model_class.table_name()
266
- tree_table_name = self.config("tree_models_class").table_name()
267
- parent_id_column_name = self.config("tree_parent_id_column_name")
268
- child_id_column_name = self.config("tree_child_id_column_name")
269
- is_parent_column_name = self.config("tree_is_parent_column_name")
270
- level_column_name = self.config("tree_level_column_name")
271
-
272
- if find_parents:
273
- join_on = parent_id_column_name
274
- search_on = child_id_column_name
275
- else:
276
- join_on = child_id_column_name
277
- search_on = parent_id_column_name
278
-
279
- # if we can join then use a join.
280
- if self.config("load_relatives_strategy") == "join":
281
- relatives = self.parent_models.join(
282
- f"{tree_table_name} as tree on tree.{join_on}={model_table_name}.{id_column_name}"
283
- )
284
- relatives = relatives.where(f"tree.{search_on}={model_id}")
285
- if not include_all:
286
- relatives = relatives.where(f"tree.{is_parent_column_name}=1")
287
- if find_parents:
288
- relatives = relatives.sort_by(level_column_name, "asc")
289
- return relatives
290
-
291
- # joins only work for SQL-like backends. Otherwise, we have to pull out our list of ids
292
- branches = self.tree_models.where(f"{search_on}={model_id}")
293
- if not include_all:
294
- branches = branches.where(f"{is_parent_column_name}=1")
295
- if find_parents:
296
- branches = branches.sort_by(level_column_name, "asc")
297
- ids = [str(branch.get(join_on)) for branch in branches]
298
-
299
- # Can we search with a WHERE IN() clause? If the backend supports it, it is probably faster
300
- if self.config("load_relatives_strategy") == "where_in":
301
- return self.parent_models.where(f"{id_column_name} IN ('" + "','".join(ids) + "')")
302
-
303
- # otherwise we have to load each model individually which is SLOW....
304
- return [self.parent_models.find(f"{id_column_name}={id}") for id in ids]