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,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,48 +0,0 @@
1
- from .string import String
2
- import re
3
-
4
-
5
- class Phone(String):
6
- my_configs = [
7
- "usa_only",
8
- ]
9
-
10
- def __init__(self, di):
11
- super().__init__(di)
12
-
13
- def to_backend(self, data):
14
- if self.name not in data:
15
- return data
16
-
17
- # phone numbers are stored as only digits.
18
- return {**data, **{self.name: re.sub(r"\D", "", data[self.name])}}
19
-
20
- def input_error_for_value(self, value, operator=None):
21
- if type(value) != str:
22
- return f"Value must be a string for {self.name}"
23
-
24
- # we'll allow spaces, dashes, parenthesis, dashes, and plus signs.
25
- # if there is anything else then it's not a valid phone number.
26
- # However, we don't do more detailed validation, because I'm too lazy to
27
- # figure out what is and is not a valid phone number, especially when
28
- # you get to the world of international numbers.
29
- if re.search(r"[^\d \-()+]", value):
30
- return "Invalid phone number"
31
-
32
- # for some final validation (especially US numbers) work only with the digits.
33
- value = re.sub(r"\D", "", value)
34
-
35
- if len(value) > 15:
36
- return "Invalid phone number"
37
-
38
- # we can't be too short unless we're doing a fuzzy search
39
- if len(value) < 10 and operator and operator.lower() != "like":
40
- return "Invalid phone number"
41
-
42
- if self.config("usa_only", silent=True):
43
- if len(value) > 11:
44
- return "Invalid phone number"
45
- if value[0] == "1" and len(value) != 11:
46
- return "Invalid phone number"
47
-
48
- return ""
@@ -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,73 +0,0 @@
1
- import time
2
- from .datetime import DateTime
3
- from datetime import datetime, timezone
4
- import dateparser
5
- from ..autodoc.schema import DateTime as AutoDocDateTime
6
-
7
-
8
- class Timestamp(DateTime):
9
- my_configs = [
10
- "date_format",
11
- "milliseconds",
12
- ]
13
-
14
- def _finalize_configuration(self, configuration):
15
- return {
16
- **{
17
- "date_format": self._date_format,
18
- "milliseconds": False,
19
- },
20
- **super()._finalize_configuration(configuration),
21
- }
22
-
23
- def from_backend(self, value):
24
- mult = 1000 if self.config("milliseconds") else 1
25
- if not value:
26
- date = None
27
- elif isinstance(value, str):
28
- if not value.isdigit():
29
- raise ValueError(
30
- f"Invalid data was found in the backend for model {self.model_class.__name__} and column {self.name}: a string value was found that is not a timestamp. It was '{value}'"
31
- )
32
- date = datetime.fromtimestamp(int(value) / mult, self._timezone)
33
- elif isinstance(value, int):
34
- date = datetime.fromtimestamp(value / mult, self._timezone)
35
- else:
36
- if not isinstance(value, datetime):
37
- raise ValueError(
38
- f"Invalid data was found in the backend for model {self.model_class.__name__} and column {self.name}: the value was neither an integer, a string, nor a datetime object"
39
- )
40
- date = value
41
- return date.replace(tzinfo=self._timezone) if date else None
42
-
43
- def to_backend(self, data):
44
- if not self.name in data or isinstance(data[self.name], int) or data[self.name] == None:
45
- return data
46
-
47
- value = data[self.name]
48
- if isinstance(value, str):
49
- if not value.isdigit():
50
- raise ValueError(
51
- f"Invalid data was sent to the backend for model {self.model_class.__name__} and column {self.name}: a string value was found that is not a timestamp. It was '{value}'"
52
- )
53
- value = int(value)
54
- elif isinstance(value, datetime):
55
- value = value.timestamp()
56
- else:
57
- raise ValueError(
58
- f"Invalid data was sent to the backend for model {self.model_class.__name__} and column {self.name}: the value was neither an integer, a string, nor a datetime object"
59
- )
60
-
61
- # hopefully this is a Python datetime object in UTC timezone...
62
- return {**data, **{self.name: value}}
63
-
64
- def input_error_for_value(self, value, operator=None):
65
- if not isinstance(value, int):
66
- return f"'{self.name}' must be an integer"
67
- return ""
68
-
69
- def values_match(self, value_1, value_2):
70
- """
71
- Compares two values to see if they are the same
72
- """
73
- return value_1 == value_2
@@ -1,26 +0,0 @@
1
- import datetime
2
-
3
- from .datetime import DateTime
4
-
5
-
6
- class Updated(DateTime):
7
- my_configs = [
8
- "date_format",
9
- "default_date",
10
- "utc",
11
- ]
12
-
13
- def __init__(self, di, datetime, timezone: datetime.tzinfo):
14
- super().__init__(di, timezone)
15
- self.datetime = datetime
16
-
17
- @property
18
- def is_writeable(self):
19
- return False
20
-
21
- def pre_save(self, data, model):
22
- if self.config("utc", silent=True):
23
- now = self.datetime.datetime.now(self.datetime.timezone.utc)
24
- else:
25
- now = self.datetime.datetime.now(self._timezone)
26
- return {**data, self.name: now}
@@ -1,26 +0,0 @@
1
- import datetime
2
-
3
- from .datetime_micro import DateTimeMicro
4
-
5
-
6
- class UpdatedMicro(DateTimeMicro):
7
- my_configs = [
8
- "date_format",
9
- "default_date",
10
- "utc",
11
- ]
12
-
13
- def __init__(self, di, datetime, timezone: datetime.tzinfo):
14
- super().__init__(di, timezone)
15
- self.datetime = datetime
16
-
17
- @property
18
- def is_writeable(self):
19
- return False
20
-
21
- def pre_save(self, data, model):
22
- if self.config("utc", silent=True):
23
- now = self.datetime.datetime.now(self.datetime.timezone.utc)
24
- else:
25
- now = self.datetime.datetime.now(self._timezone)
26
- 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