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,43 +0,0 @@
1
- from .column import Column
2
- from ..autodoc.schema import Number as AutoDocNumber
3
-
4
-
5
- class Float(Column):
6
- _auto_doc_class = AutoDocNumber
7
-
8
- def __init__(self, di):
9
- super().__init__(di)
10
-
11
- def from_backend(self, value):
12
- return float(value)
13
-
14
- def to_backend(self, data):
15
- if self.name not in data or data[self.name] is None:
16
- return data
17
-
18
- return {
19
- **data,
20
- self.name: float(data[self.name]),
21
- }
22
-
23
- def check_input(self, model, data):
24
- if not self.name in data:
25
- return ""
26
- if isinstance(data[self.name], int) or isinstance(data[self.name], float) or data[self.name] == None:
27
- return ""
28
- return f"Invalid input: {self.name} must be an integer or float"
29
-
30
- def build_condition(self, value, operator=None, column_prefix=""):
31
- if not operator:
32
- operator = "="
33
- return f"{column_prefix}{self.name}{operator}{value}"
34
-
35
- def is_allowed_operator(self, operator, relationship_reference=None):
36
- return operator in ["=", "<", ">", "<=", ">="]
37
-
38
- def input_error_for_value(self, value, operator=None):
39
- return (
40
- "value should be an integer or float"
41
- if (type(value) != int and type(value) != float and value is not None)
42
- else ""
43
- )
@@ -1,179 +0,0 @@
1
- from .column import Column
2
- import re
3
- from collections import OrderedDict
4
- from ..autodoc.schema import Array as AutoDocArray
5
- from ..autodoc.schema import Object as AutoDocObject
6
- from ..autodoc.schema import String as AutoDocString
7
- from ..functional import validations
8
-
9
-
10
- class HasMany(Column):
11
- """
12
- Controls a has-many relationship.
13
-
14
- This is a readonly column. When used in a model context it will return an iterable with the related child records.
15
- When used in an API context, it will convert the child records into a list of objects.
16
-
17
- It assumes that the foreign id in the child table is `[parent_model_class_name]_id` in all lower case.
18
- e.g., if the parent model class is named Status, then it assumes an id in the child class called `status_id`.
19
- """
20
-
21
- required_configs = [
22
- "child_models_class",
23
- ]
24
-
25
- my_configs = [
26
- "foreign_column_name",
27
- "child_columns",
28
- "is_readable",
29
- "readable_child_columns",
30
- "parent_id_column_name",
31
- "where",
32
- ]
33
-
34
- def __init__(self, di):
35
- super().__init__(di)
36
-
37
- @property
38
- def is_writeable(self):
39
- return False
40
-
41
- @property
42
- def is_readable(self):
43
- is_readable = self.config("is_readable", True)
44
- # default is_readable to False
45
- return True if (is_readable and is_readable is not None) else False
46
-
47
- def configure(self, name, configuration, model_class):
48
- if "child_models_class" not in configuration:
49
- raise KeyError(
50
- f"Missing required configuration 'child_models_class' for column '{name}' in model class "
51
- + f"'{model_class.__name__}'"
52
- )
53
- self.validate_models_class(configuration["child_models_class"])
54
- if not configuration.get("parent_id_column_name"):
55
- configuration["parent_id_column_name"] = model_class.id_column_name
56
-
57
- # if readable_child_columns is set then load up the child models/columns now, because we'll need it in the
58
- # _check_configuration step, but we don't want to load it there because we can't save it back into the config
59
- if "foreign_column_name" not in configuration:
60
- configuration["foreign_column_name"] = (
61
- re.sub(r"(?<!^)(?=[A-Z])", "_", model_class.__name__.replace("_", "")).lower() + "_id"
62
- )
63
-
64
- # continue normally now...
65
- super().configure(name, configuration, model_class)
66
-
67
- def _check_configuration(self, configuration):
68
- super()._check_configuration(configuration)
69
- error_prefix = f"Configuration error for '{self.name}' in '{self.model_class.__name__}':"
70
-
71
- if configuration.get("is_readable"):
72
- child_columns = self.di.build(configuration["child_models_class"], cache=True).raw_columns_configuration()
73
- if not "readable_child_columns" in configuration:
74
- raise ValueError(f"{error_prefix} must provide 'readable_child_columns' if is_readable is set")
75
- readable_child_columns = configuration["readable_child_columns"]
76
- if not hasattr(readable_child_columns, "__iter__"):
77
- raise ValueError(
78
- f"{error_prefix} 'readable_child_columns' should be an iterable "
79
- + "with the list of child columns to output."
80
- )
81
- if isinstance(readable_child_columns, str):
82
- raise ValueError(
83
- f"{error_prefix} 'readable_child_columns' should be an iterable "
84
- + "with the list of child columns to output."
85
- )
86
- for column_name in readable_child_columns:
87
- if column_name not in child_columns:
88
- raise ValueError(
89
- f"{error_prefix} 'readable_child_columns' references column named '{column_name}' but this"
90
- + "column does not exist in the model class."
91
- )
92
-
93
- wheres = configuration.get("where")
94
- if wheres:
95
- if not isinstance(wheres, list):
96
- raise ValueError(
97
- f"{error_prefix} 'where' must be a list of where conditions or callables that return where conditions"
98
- )
99
- for index, where in enumerate(wheres):
100
- if callable(where) or isinstance(where, str):
101
- continue
102
- raise ValueError(
103
- 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"
104
- )
105
-
106
- def _finalize_configuration(self, configuration):
107
- return {
108
- **super()._finalize_configuration(configuration),
109
- **{
110
- "where": configuration.get("where", []),
111
- },
112
- }
113
-
114
- def get_child_columns(self):
115
- if "child_columns" not in self.configuration:
116
- self.configuration["child_columns"] = self.child_models.columns()
117
- return self.configuration["child_columns"]
118
-
119
- def can_provide(self, column_name):
120
- return column_name == self.name
121
-
122
- def provide(self, data, column_name):
123
- foreign_column_name = self.config("foreign_column_name")
124
- id_column_name = self.config("parent_id_column_name")
125
- children = self.child_models.where(f"{foreign_column_name}={data[id_column_name]}")
126
-
127
- wheres = self.config("where", silent=True)
128
- if not wheres:
129
- return children
130
-
131
- for index, where in enumerate(wheres):
132
- if callable(where):
133
- children = self.di.call_function(where, model=children, children=children, parent_data=data)
134
- if not validations.is_model(children):
135
- raise ValueError(
136
- 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"
137
- )
138
- else:
139
- children = children.where(where)
140
- return children
141
-
142
- def to_json(self, model):
143
- children = []
144
- columns = self.get_child_columns()
145
- for child in model.__getattr__(self.name):
146
- json = OrderedDict()
147
- child_id_column_name = child.id_column_name
148
- json = {
149
- **json,
150
- **columns[child_id_column_name].to_json(child),
151
- }
152
- for column_name in self.config("readable_child_columns"):
153
- json = {
154
- **json,
155
- **columns[column_name].to_json(child),
156
- }
157
- children.append(json)
158
- return {self.name: children}
159
-
160
- @property
161
- def child_models(self):
162
- return self.di.build(self.config("child_models_class"), cache=True)
163
-
164
- def documentation(self, name=None, example=None, value=None):
165
- columns = self.get_child_columns()
166
- child_id_column_name = self.child_models.get_id_column_name()
167
- child_properties = [columns[child_id_column_name].documentation()]
168
-
169
- for column_name in self.config("readable_child_columns"):
170
- child_docs = columns[column_name].documentation()
171
- if type(child_docs) != list:
172
- child_docs = [child_docs]
173
- child_properties.extend(child_docs)
174
-
175
- child_object = AutoDocObject(
176
- self.camel_to_nice(self.child_models.model_class().__name__),
177
- child_properties,
178
- )
179
- return AutoDocArray(name if name is not None else self.name, child_object, value=value)
@@ -1,58 +0,0 @@
1
- from .has_many import HasMany
2
- import re
3
- from collections import OrderedDict
4
- from ..autodoc.schema import Array as AutoDocArray
5
- from ..autodoc.schema import Object as AutoDocObject
6
- from ..autodoc.schema import String as AutoDocString
7
-
8
-
9
- class HasOne(HasMany):
10
- """
11
- Controls a has-one relationship.
12
-
13
- This is a readonly column. When used in a model context it will return the related record.
14
- When used in an API context, it will convert the child record into an object.
15
-
16
- It assumes that the foreign id in the child table is `[parent_model_class_name]_id` in all lower case.
17
- e.g., if the parent model class is named Status, then it assumes an id in the child class called `status_id`.
18
- """
19
-
20
- def can_provide(self, column_name):
21
- return column_name == self.name
22
-
23
- def provide(self, data, column_name):
24
- foreign_column_name = self.config("foreign_column_name")
25
- id_column_name = self.config("parent_id_column_name")
26
- return self.child_models.find(f"{foreign_column_name}={data[id_column_name]}")
27
-
28
- def to_json(self, model):
29
- json = OrderedDict()
30
- columns = self.get_child_columns()
31
- child = model.__getattr__(self.name)
32
- child_id_column_name = child.id_column_name
33
- json = {
34
- **json,
35
- **columns[child_id_column_name].to_json(child),
36
- }
37
- for column_name in self.config("readable_child_columns"):
38
- json = {
39
- **json,
40
- **columns[column_name].to_json(child),
41
- }
42
- return {self.name: json}
43
-
44
- def documentation(self, name=None, example=None, value=None):
45
- columns = self.get_child_columns()
46
- child_id_column_name = self.child_models.get_id_column_name()
47
- child_properties = [columns[child_id_column_name].documentation()]
48
-
49
- for column_name in self.config("readable_child_columns"):
50
- child_docs = columns[column_name].documentation()
51
- if type(child_docs) != list:
52
- child_docs = [child_docs]
53
- child_properties.extend(child_docs)
54
-
55
- return AutoDocObject(
56
- self.camel_to_nice(self.child_models.model_class().__name__),
57
- child_properties,
58
- )
@@ -1,41 +0,0 @@
1
- from .column import Column
2
- from ..autodoc.schema import Integer as AutoDocInteger
3
-
4
-
5
- class Integer(Column):
6
- _auto_doc_class = AutoDocInteger
7
-
8
- def __init__(self, di):
9
- super().__init__(di)
10
-
11
- def to_backend(self, data):
12
- if self.name not in data or data[self.name] is None:
13
- return data
14
-
15
- return {
16
- **data,
17
- self.name: int(data[self.name]),
18
- }
19
-
20
- def from_backend(self, value):
21
- return int(value)
22
-
23
- def input_error_for_value(self, value, operator=None):
24
- if operator == "in":
25
- if type(value) != list:
26
- return f'{self.name} must be an integer when searching with the "IN" operator'
27
- for val in value:
28
- if type(val) != int:
29
- return f"All items in {self.name} must be integers"
30
- return ""
31
- return f"{self.name} must be an integer" if type(value) != int else ""
32
-
33
- def build_condition(self, value, operator=None, column_prefix=""):
34
- if operator == "in":
35
- return f"{column_prefix}{self.name} IN (" + ",".join([str(val) for val in value]) + ")"
36
- if not operator:
37
- operator = "="
38
- return f"{column_prefix}{self.name}{operator}{value}"
39
-
40
- def is_allowed_operator(self, operator, relationship_reference=None):
41
- return operator in ["=", "<", ">", "<=", ">=", "in"]
@@ -1,25 +0,0 @@
1
- import json
2
- from .column import Column
3
-
4
-
5
- class JSON(Column):
6
- def __init__(self, di):
7
- super().__init__(di)
8
-
9
- def from_backend(self, value):
10
- if type(value) == list or type(value) == dict:
11
- return value
12
- if not value:
13
- return None
14
- try:
15
- return json.loads(value)
16
- except json.JSONDecodeError:
17
- return None
18
-
19
- def to_backend(self, data):
20
- if self.name in data:
21
- data[self.name] = json.dumps(data[self.name]) if data[self.name] else ""
22
- return data
23
-
24
- def to_json(self, model):
25
- return {self.name: model.get(self.name, silent=True)}
@@ -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)