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
@@ -1,278 +0,0 @@
1
- from .string import String
2
- import re
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 ManyToMany(String):
10
- """
11
- Controls a many-to-many relationship.
12
-
13
- This column connects to models via a many-to-many relationship, meaning that a record in either table can
14
- be associated with multiple records in the other table. Image you had two models: users and teams, where
15
- a user can be on more than one team. To keep track of the mapping, a "pivot" table is required which tracks
16
- relationships. In the case of users/teams, you might imagine a table called "users_teams" which has 3 columns:
17
-
18
- - id
19
- - user_id
20
- - team_id
21
-
22
- You would then create a many-to-many relationship in both your users model and your teams model that would
23
- look something like:
24
-
25
- ```
26
- class User:
27
- def columns_configuration(self):
28
- return OrderedDict([
29
- clearskies.column_types.has_many('teams', related_models_class=Teams, pivot_models_class=UsersTeams),
30
- ')
31
-
32
- class Team:
33
- def columns_configuration(self):
34
- return OrderedDict([
35
- clearskies.column_types.has_many('users', related_models_class=Users, pivot_models_class=UsersTeams),
36
- ')
37
- ```
38
-
39
- Note that `related_models_class` and pivot_models_class receive the model*s* class, not the _model_ class.
40
-
41
- You can attach records to eachother by saving a list of ids via the column name, i.e.:
42
-
43
- ```
44
- user.save({'teams': [1, 2, 3]})
45
- team.save({'users': [4, 5, 6]})
46
- ```
47
-
48
- The many_to_many column will let you easily pull out the related models:
49
-
50
- ```
51
- print(user.teams)
52
- # prints [<__main__.Team object>, <__main__.Team object>, <__main__.Team object>]
53
- ```
54
-
55
- as well as their ids:
56
-
57
- ```
58
- print(user.teams_ids)
59
- # prints [1, 2, 3]
60
- ```
61
- """
62
-
63
- required_configs = [
64
- "pivot_models_class",
65
- "related_models_class",
66
- ]
67
-
68
- my_configs = [
69
- "foreign_column_name_in_pivot",
70
- "own_column_name_in_pivot",
71
- "pivot_table",
72
- "readable_related_columns",
73
- "is_readable",
74
- ]
75
-
76
- def __init__(self, di):
77
- super().__init__(di)
78
-
79
- @property
80
- def is_readable(self):
81
- is_readable = self.config("is_readable", True)
82
- # default is_readable to False
83
- return True if (is_readable and is_readable is not None) else False
84
-
85
- def _check_configuration(self, configuration):
86
- super()._check_configuration(configuration)
87
- self.validate_models_class(configuration["pivot_models_class"])
88
- self.validate_models_class(configuration["related_models_class"])
89
- if self.name[-3:] == "_id" or self.name[-4:] == "_ids":
90
- raise ValueError(
91
- f"Invalid name for column '{self.name}' in '{self.model_class.__name__}' - "
92
- + "ManyToMany column should not end in '_id' or '_ids'"
93
- )
94
-
95
- if configuration.get("is_readable"):
96
- related_columns = self.di.build(
97
- configuration["related_models_class"], cache=True
98
- ).raw_columns_configuration()
99
- error_prefix = f"Configuration error for '{self.name}' in '{self.model_class.__name__}':"
100
- if not "readable_related_columns" in configuration:
101
- raise ValueError(f"{error_prefix} must provide 'readable_related_columns' if is_readable is set")
102
- readable_related_columns = configuration["readable_related_columns"]
103
- if not hasattr(readable_related_columns, "__iter__"):
104
- raise ValueError(
105
- f"{error_prefix} 'readable_related_columns' should be an iterable "
106
- + "with the list of child columns to output."
107
- )
108
- if isinstance(readable_related_columns, str):
109
- raise ValueError(
110
- f"{error_prefix} 'readable_related_columns' should be an iterable "
111
- + "with the list of child columns to output."
112
- )
113
- for column_name in readable_related_columns:
114
- if column_name not in related_columns:
115
- raise ValueError(
116
- f"{error_prefix} 'readable_related_columns' references column named '{column_name}' but this"
117
- + "column does not exist in the model class."
118
- )
119
-
120
- def _finalize_configuration(self, configuration):
121
- pivot_models = self.di.build(configuration["pivot_models_class"], cache=True)
122
- related_models = self.di.build(configuration["related_models_class"], cache=True)
123
-
124
- if not configuration.get("foreign_column_name_in_pivot"):
125
- model_class = related_models.model_class()
126
- foreign_column_name = re.sub(r"(?<!^)(?=[A-Z])", "_", model_class.__name__.replace("_", "")).lower() + "_id"
127
- else:
128
- foreign_column_name = configuration["foreign_column_name_in_pivot"]
129
-
130
- if not configuration.get("own_column_name_in_pivot"):
131
- own_column_name = (
132
- re.sub(r"(?<!^)(?=[A-Z])", "_", self.model_class.__name__.replace("_", "")).lower() + "_id"
133
- )
134
- else:
135
- own_column_name = configuration["own_column_name_in_pivot"]
136
-
137
- return {
138
- **super()._finalize_configuration(configuration),
139
- **{
140
- "foreign_column_name_in_pivot": foreign_column_name,
141
- "own_column_name_in_pivot": own_column_name,
142
- "pivot_table": pivot_models.get_table_name(),
143
- "own_id_column_name": self.model_class.id_column_name,
144
- "related_id_column_name": related_models.get_id_column_name(),
145
- },
146
- }
147
-
148
- def input_error_for_value(self, value, operator=None):
149
- if type(value) != list:
150
- return f"{self.name} should be a list of ids"
151
- related_models = self.related_models
152
- related_id_column_name = self.config("related_id_column_name")
153
- for id_to_check in value:
154
- if type(id_to_check) != str:
155
- return f"Invalid selection for {self.name}: all values must be strings"
156
- if not len(related_models.where(f"{related_id_column_name}={id_to_check}")):
157
- return f"Invalid selection for {self.name}: record {id_to_check} does not exist"
158
- return ""
159
-
160
- def can_provide(self, column_name):
161
- return column_name == self.name or column_name == f"{self.name}_ids"
162
-
163
- def provide(self, data, column_name):
164
- foreign_column_name_in_pivot = self.config("foreign_column_name_in_pivot")
165
- own_column_name_in_pivot = self.config("own_column_name_in_pivot")
166
- own_id_column_name = self.config("own_id_column_name")
167
- pivot_table = self.config("pivot_table")
168
- related_id_column_name = self.config("related_id_column_name")
169
- models = self.related_models
170
- join = f"JOIN {pivot_table} ON {pivot_table}.{foreign_column_name_in_pivot}={models.get_table_name()}.{related_id_column_name}"
171
- related_models = models.join(join).where(f"{pivot_table}.{own_column_name_in_pivot}={data[own_id_column_name]}")
172
- if column_name == self.name:
173
- return [model for model in related_models]
174
- return [model.__getattr__(related_id_column_name) for model in related_models]
175
-
176
- def to_backend(self, data):
177
- # we can't persist our mapping data to the database directly, so remove anything here
178
- # and take care of things in post_save
179
- if self.name in data:
180
- del data[self.name]
181
- return data
182
-
183
- def post_save(self, data, model, id):
184
- # if our incoming data is not in the data array or is None, then nothing has been set and we do not want
185
- # to make any changes
186
- if self.name not in data or data[self.name] is None:
187
- return data
188
-
189
- # figure out what ids need to be created or deleted from the pivot table.
190
- if not model.exists:
191
- old_ids = set()
192
- else:
193
- old_ids = set(getattr(model, f"{self.name}_ids"))
194
-
195
- new_ids = set(data[self.name])
196
- to_delete = old_ids - new_ids
197
- to_create = new_ids - old_ids
198
- if to_delete:
199
- pivot_models = self.pivot_models
200
- foreign_column_name = self.config("foreign_column_name_in_pivot")
201
- for model_to_delete in pivot_models.where(
202
- f"{foreign_column_name} IN (" + ",".join(map(str, to_delete)) + ")"
203
- ):
204
- model_to_delete.delete()
205
- if to_create:
206
- pivot_models = self.pivot_models
207
- foreign_column_name = self.config("foreign_column_name_in_pivot")
208
- own_column_name = self.config("own_column_name_in_pivot")
209
- for to_insert in new_ids - old_ids:
210
- pivot_models.create(
211
- {
212
- foreign_column_name: to_insert,
213
- own_column_name: id,
214
- }
215
- )
216
-
217
- return data
218
-
219
- @property
220
- def pivot_models(self):
221
- return self.di.build(self.config("pivot_models_class"), cache=True)
222
-
223
- @property
224
- def related_models(self):
225
- return self.di.build(self.config("related_models_class"), cache=True)
226
-
227
- @property
228
- def related_columns(self):
229
- return self.related_models.model_columns
230
-
231
- def add_search(self, models, value, operator=None, relationship_reference=None):
232
- foreign_column_name_in_pivot = self.config("foreign_column_name_in_pivot")
233
- own_column_name_in_pivot = self.config("own_column_name_in_pivot")
234
- own_id_column_name = self.config("own_id_column_name")
235
- pivot_table = self.config("pivot_table")
236
- my_table_name = self.model_class.table_name()
237
- related_table_name = self.related_models.get_table_name()
238
- join_pivot = (
239
- f"JOIN {pivot_table} ON {pivot_table}.{own_column_name_in_pivot}={my_table_name}.{own_id_column_name}"
240
- )
241
- # no reason we can't support searching by both an id or a list of ids
242
- values = value if type(value) == list else [value]
243
- search = " IN (" + ", ".join([str(val) for val in value]) + ")"
244
- return models.join(join_pivot).where(f"{pivot_table}.{foreign_column_name_in_pivot}{search}")
245
-
246
- def to_json(self, model):
247
- records = []
248
- columns = self.related_columns
249
- related_id_column_name = self.config("related_id_column_name")
250
- for related in model.__getattr__(self.name):
251
- json = OrderedDict()
252
- if related_id_column_name not in self.config("readable_related_columns"):
253
- json[related_id_column_name] = columns[related_id_column_name].to_json(related)
254
- for column_name in self.config("readable_related_columns"):
255
- column_data = columns[column_name].to_json(related)
256
- if type(column_data) == dict:
257
- json = {**json, **column_data}
258
- else:
259
- json[column_name] = column_data
260
- records.append(json)
261
- return {self.name: records}
262
-
263
- def documentation(self, name=None, example=None, value=None):
264
- columns = self.related_columns
265
- related_id_column_name = self.config("related_id_column_name")
266
- related_properties = [columns[related_id_column_name].documentation()]
267
-
268
- for column_name in self.config("readable_related_columns"):
269
- related_docs = columns[column_name].documentation()
270
- if type(related_docs) != list:
271
- related_docs = [related_docs]
272
- related_properties.extend(child_docs)
273
-
274
- related_object = AutoDocObject(
275
- self.camel_to_nice(self.related_models.model_class().__name__),
276
- related_properties,
277
- )
278
- return AutoDocArray(name if name is not None else self.name, related_object, value=value)
@@ -1,162 +0,0 @@
1
- from .many_to_many import ManyToMany
2
-
3
-
4
- class ManyToManyWithData(ManyToMany):
5
- """
6
- Controls a many-to-many relationship where additional data is stored in the pivot table.
7
- """
8
-
9
- required_configs = [
10
- "pivot_models_class",
11
- "related_models_class",
12
- ]
13
-
14
- my_configs = [
15
- "foreign_column_name_in_pivot",
16
- "own_column_name_in_pivot",
17
- "pivot_table",
18
- "readable_related_columns",
19
- "is_readable",
20
- "setable_columns",
21
- "persist_unique_lookup_column_to_pivot_table",
22
- ]
23
-
24
- def __init__(self, di):
25
- super().__init__(di)
26
-
27
- @property
28
- def is_readable(self):
29
- is_readable = self.config("is_readable", True)
30
- # default is_readable to False
31
- return True if (is_readable and is_readable is not None) else False
32
-
33
- def _check_configuration(self, configuration):
34
- super()._check_configuration(configuration)
35
- setable_columns = configuration.get("setable_columns")
36
- if setable_columns is None:
37
- return
38
- pivot_columns = self.di.build(configuration["pivot_models_class"], cache=True).raw_columns_configuration()
39
- if not hasattr(setable_columns, "__iter__"):
40
- raise ValueError(
41
- f"{error_prefix} 'setable_columns' should be None or an iterable "
42
- + "with the list of pivot columns that can be set."
43
- )
44
- if isinstance(setable_columns, str):
45
- raise ValueError(
46
- f"{error_prefix} 'setable_columns' should be None or an iterable "
47
- + "with the list of pivot columns that can be set."
48
- )
49
- for column_name in setable_columns:
50
- if column_name not in pivot_columns:
51
- raise ValueError(
52
- f"{error_prefix} 'setable_columns' references column named '{column_name}' but this"
53
- + "column does not exist in the pivot model class."
54
- )
55
-
56
- def _finalize_configuration(self, configuration):
57
- configuration = super()._finalize_configuration(configuration)
58
- if "persist_unique_lookup_column_to_pivot_table" not in configuration:
59
- configuration["persist_unique_lookup_column_to_pivot_table"] = False
60
- return configuration
61
-
62
- def post_save(self, data, model, id):
63
- # if our incoming data is not in the data array or is None, then nothing has been set and we do not want
64
- # to make any changes
65
- if self.name not in data or data[self.name] is None:
66
- return data
67
-
68
- # figure out what ids need to be created or deleted from the pivot table.
69
- if not model.exists:
70
- old_ids = set()
71
- else:
72
- old_ids = set(getattr(model, f"{self.name}_ids"))
73
-
74
- # this is trickier for many-to-many-with-data compared to many-to-many. We're generally
75
- # expecting data[self.name] to be a list of dictionaries. For each entry, we need to find
76
- # the corresponding entry in the pivot table to decide if we need to delete, create, or update.
77
- # However, since we have a dictionary there are a variety of ways that we can connect to
78
- # an entry in the related table - either related id or any unique column from the related
79
- # table. Technically we might also specify a pivot id, but we're generally trying to be
80
- # transparent to those, so let's ignore that one.
81
-
82
- # unfortunately I'm using related_models and foreign_models interchangeably - this is likely
83
- # an accident due to the slow inheritence from he belongs to class, to the many to many class,
84
- # and now this. Keep in mind that "foreign" and "related" refer to the same thing
85
- foreign_column_name_in_pivot = self.config("foreign_column_name_in_pivot")
86
- own_column_name_in_pivot = self.config("own_column_name_in_pivot")
87
- unique_foreign_columns = {
88
- column.name: column.name for column in self.related_columns.values() if column.is_unique
89
- }
90
- related_models = self.related_models
91
- pivot_models = self.pivot_models
92
- new_ids = set()
93
- for pivot_record in data[self.name]:
94
- # first we need to identify which foreign column this belongs to.
95
- foreign_column_id = None
96
- # if they provide the foreign column id in the pivot data then we're good
97
- if foreign_column_name_in_pivot in pivot_record:
98
- foreign_column_id = pivot_record[foreign_column_name_in_pivot]
99
- elif len(unique_foreign_columns):
100
- for pivot_column, pivot_value in pivot_record.items():
101
- if pivot_column not in unique_foreign_columns:
102
- continue
103
- foreign_model = related_models.find(f"{pivot_column}={pivot_value}")
104
- foreign_column_id = foreign_model.id
105
- if foreign_column_id:
106
- # remove this column from the data - it was used to lookup the right
107
- # record, but mostly won't exist in the model, unless we've been instructed
108
- # to keep it
109
- if not self.config("persist_unique_lookup_column_to_pivot_table"):
110
- del pivot_record[pivot_column]
111
- break
112
- if not foreign_column_id:
113
- column_list = "'" + "', '".join([column for column in unique_foreign_columns.key()]) + "'"
114
- raise ValueError(
115
- f"Missing data for {self.name}: Unable to match foreign record for a record in the many-to-many relationship: you must provide either '{foreign_column_name_in_pivot}' with the id column for the foreign table, or a value from one of the unique columns: {column_list}"
116
- )
117
- pivot_model = (
118
- pivot_models.where(f"{foreign_column_name_in_pivot}={foreign_column_id}")
119
- .where(f"{own_column_name_in_pivot}={id}")
120
- .first()
121
- )
122
- new_ids.add(foreign_column_id)
123
- # this will either update or create accordingly
124
- pivot_model.save(
125
- {
126
- **pivot_record,
127
- foreign_column_name_in_pivot: foreign_column_id,
128
- own_column_name_in_pivot: id,
129
- }
130
- )
131
-
132
- # the above took care of isnerting and updating active records. Now we need to delete
133
- # records that are no longer needed.
134
- to_delete = old_ids - new_ids
135
- if to_delete:
136
- pivot_models = self.pivot_models
137
- foreign_column_name = self.config("foreign_column_name_in_pivot")
138
- for model_to_delete in pivot_models.where(
139
- f"{foreign_column_name} IN (" + ",".join(map(str, to_delete)) + ")"
140
- ):
141
- model_to_delete.delete()
142
-
143
- return data
144
-
145
- def can_provide(self, column_name):
146
- if column_name == self.name:
147
- return True
148
- if column_name == f"{self.name}_ids":
149
- return True
150
- if column_name == f"{self.name}_pivots":
151
- return True
152
-
153
- def provide(self, data, column_name):
154
- # the base class handles most of this: returning the list of matching
155
- # ids or returning the list of related models
156
- if column_name == self.name or column_name == f"{self.name}_ids":
157
- return super().provide(data, column_name)
158
-
159
- # so if we get here then we need to provide the pivot models for this record
160
- own_column_name_in_pivot = self.config("own_column_name_in_pivot")
161
- my_id = data[self.config("own_id_column_name")]
162
- return [model for model in self.pivot_models.where(f"{own_column_name_in_pivot}={my_id}")]
@@ -1,11 +0,0 @@
1
- from .string import String
2
-
3
-
4
- class Select(String):
5
- required_configs = ["values"]
6
-
7
- def __init__(self, di):
8
- super().__init__(di)
9
-
10
- def input_error_for_value(self, value, operator=None):
11
- return f"Invalid value for {self.name}" if value not in self.config("values") else ""
@@ -1,24 +0,0 @@
1
- from .column import Column
2
-
3
-
4
- class String(Column):
5
- def __init__(self, di):
6
- super().__init__(di)
7
-
8
- def build_condition(self, value, operator=None, column_prefix=""):
9
- if not operator:
10
- operator = "="
11
- if operator.lower() == "like":
12
- return f"{column_prefix}{self.name} LIKE '%{value}%'"
13
- return f"{column_prefix}{self.name}{operator}{value}"
14
-
15
- def is_allowed_operator(self, operator, relationship_reference=None):
16
- """
17
- This is called when processing user data to decide if the end-user is specifying an allowed operator
18
- """
19
- if operator in ["=", "<", ">", "<=", ">=", "in"]:
20
- return True
21
- return operator.lower() == "like"
22
-
23
- def input_error_for_value(self, value, operator=None):
24
- return "value should be a string" if type(value) != str else ""
@@ -1,24 +0,0 @@
1
- from .datetime import DateTime
2
-
3
-
4
- class Updated(DateTime):
5
- my_configs = [
6
- "date_format",
7
- "default_date",
8
- "utc",
9
- ]
10
-
11
- def __init__(self, di, datetime):
12
- super().__init__(di)
13
- self.datetime = datetime
14
-
15
- @property
16
- def is_writeable(self):
17
- return False
18
-
19
- def pre_save(self, data, model):
20
- if self.config("utc", silent=True):
21
- now = self.datetime.datetime.now(self.datetime.timezone.utc)
22
- else:
23
- now = self.datetime.datetime.now()
24
- return {**data, self.name: now}
@@ -1,24 +0,0 @@
1
- from .datetime_micro import DateTimeMicro
2
-
3
-
4
- class UpdatedMicro(DateTimeMicro):
5
- my_configs = [
6
- "date_format",
7
- "default_date",
8
- "utc",
9
- ]
10
-
11
- def __init__(self, di, datetime):
12
- super().__init__(di)
13
- self.datetime = datetime
14
-
15
- @property
16
- def is_writeable(self):
17
- return False
18
-
19
- def pre_save(self, data, model):
20
- if self.config("utc", silent=True):
21
- now = self.datetime.datetime.now(self.datetime.timezone.utc)
22
- else:
23
- now = self.datetime.datetime.now()
24
- return {**data, self.name: now}
@@ -1,25 +0,0 @@
1
- from .string import String
2
-
3
-
4
- class UUID(String):
5
- def __init__(self, di, uuid):
6
- super().__init__(di)
7
- self.uuid = uuid
8
-
9
- @property
10
- def is_writeable(self):
11
- return False
12
-
13
- def build_condition(self, value, operator=None, column_prefix=""):
14
- return f"{column_prefix}{self.name}={value}"
15
-
16
- def is_allowed_operator(self, operator, relationship_reference=None):
17
- """
18
- This is called when processing user data to decide if the end-user is specifying an allowed operator
19
- """
20
- return operator == "="
21
-
22
- def pre_save(self, data, model):
23
- if model.exists:
24
- return data
25
- return {**data, self.name: str(self.uuid.uuid4())}
clearskies/columns.py DELETED
@@ -1,123 +0,0 @@
1
- from collections import OrderedDict
2
- from collections.abc import Sequence
3
- import inspect
4
- from .binding_config import BindingConfig
5
-
6
-
7
- class Columns:
8
- def __init__(self, di):
9
- self.di = di
10
-
11
- def configure(self, definitions, model_class, overrides=None):
12
- columns = OrderedDict()
13
- for name, configuration in definitions.items():
14
- name = name.strip()
15
- if not name:
16
- raise ValueError(f"Missing name for column in '{model_class.__name__}'")
17
- if name in columns:
18
- raise ValueError(f"Duplicate column '{name}' found for model '{model_class.__name__}'")
19
- column_overrides = overrides[name] if (overrides is not None and name in overrides) else {}
20
- # if the overrides changes the class then we need to completely replace the column definition
21
- # with what is in the overrides.
22
- if "class" in column_overrides and id(column_overrides["class"]) != id(configuration["class"]):
23
- configuration = column_overrides
24
- else:
25
- configuration = {
26
- **configuration,
27
- **column_overrides,
28
- "input_requirements": self._resolve_input_requirements(
29
- self._merge_input_requirements(
30
- configuration.get("input_requirements"),
31
- column_overrides.get("input_requirements"),
32
- ),
33
- name,
34
- model_class.__name__,
35
- ),
36
- }
37
- columns[name] = self.build_column(name, configuration, model_class)
38
-
39
- # overrides can add columns too - need to handle those separately
40
- if overrides is not None:
41
- for name, configuration in overrides.items():
42
- if name in columns:
43
- continue
44
- configuration["input_requirements"] = (
45
- self._resolve_input_requirements(configuration["input_requirements"], name, model_class.__name__)
46
- if "input_requirements" in configuration
47
- else []
48
- )
49
- columns[name] = self.build_column(name, configuration, model_class)
50
-
51
- return columns
52
-
53
- def build_column(self, name, configuration, model_class):
54
- if not "class" in configuration:
55
- raise ValueError(f"Missing column class for column {name} in {model_class.__name__}")
56
- column = self.di.build(configuration["class"], cache=False)
57
- column.configure(name, configuration, model_class)
58
- return column
59
-
60
- def _merge_input_requirements(self, config_requirements, override_requirements):
61
- if config_requirements is None and override_requirements is None:
62
- return []
63
- if config_requirements is None:
64
- return override_requirements
65
- if override_requirements is None:
66
- return config_requirements
67
-
68
- # if we have more than one of the same class then use the one from the overrides
69
- requirements = []
70
- used_classes = {}
71
- for requirement in override_requirements:
72
- requirements.append(requirement)
73
- [requirement_class, args, kwargs] = self._input_requirement_args_and_class(requirement)
74
- used_classes[requirement_class.__name__] = True
75
- for requirement in config_requirements:
76
- [requirement_class, args, kwargs] = self._input_requirement_args_and_class(requirement)
77
- if requirement_class.__name__ in used_classes:
78
- continue
79
- requirements.append(requirement)
80
- used_classes[requirement_class.__name__] = True
81
-
82
- return requirements
83
-
84
- def _input_requirement_args_and_class(self, requirement):
85
- """
86
- This takes the input requirement data provided by the developer and returns the things we need to build it.
87
-
88
- An input requirement can be:
89
-
90
- 1. An InputRequirement class (aka `Required`)
91
- 2. A tuple with the class and then configuration parameters for the class (aka `(MaxLength, 255)`)
92
- 3. A BindingConfig
93
-
94
- This normalizes these three options and returns a list with `[Class, [args], {kwargs}]` for building
95
- """
96
- if inspect.isclass(requirement):
97
- return [requirement, [], {}]
98
- elif isinstance(requirement, BindingConfig):
99
- return [requirement.object_class, requirement.args, requirement.kwargs]
100
- elif isinstance(requirement, Sequence) and type(requirement) != str:
101
- if not inspect.isclass(requirement[0]):
102
- raise ValueError(
103
- f"{error_prefix} incorrect value for input_requirement. First element should "
104
- + f"be the Requirement class, but instead {type(requirement[0])} was found"
105
- )
106
- return [requirement[0], requirement[1:], {}]
107
- else:
108
- raise ValueError("Unrecognized value for input_requirement")
109
-
110
- def _resolve_input_requirements(self, input_requirements, column_name, model_class_name):
111
- error_prefix = f"Configuration error for column '{column_name}' in model '{model_class_name}':"
112
- if not hasattr(input_requirements, "__iter__"):
113
- raise ValueError(
114
- f"{error_prefix} 'input_requirements' should be an iterable but is {type(input_requirements)}"
115
- )
116
- resolved_requirements = []
117
- for requirement in input_requirements:
118
- [requirement_class, args, kwargs] = self._input_requirement_args_and_class(requirement)
119
- requirement_instance = self.di.build(requirement_class, cache=False)
120
- requirement_instance.column_name = column_name
121
- requirement_instance.configure(*args, **kwargs)
122
- resolved_requirements.append(requirement_instance)
123
- return resolved_requirements