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
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import OrderedDict
4
+ from typing import TYPE_CHECKING, Any, Self, overload
5
+
6
+ from clearskies import configs, decorators
7
+ from clearskies.autodoc.schema import Array as AutoDocArray
8
+ from clearskies.autodoc.schema import Object as AutoDocObject
9
+ from clearskies.column import Column
10
+ from clearskies.columns.many_to_many_ids import ManyToManyIds
11
+ from clearskies.functional import string
12
+
13
+ if TYPE_CHECKING:
14
+ from clearskies import Model
15
+
16
+
17
+ class ManyToManyModels(Column):
18
+ """
19
+ A companion for the ManyToManyIds column that returns the matching models instead of the ids.
20
+
21
+ See the example in the ManyToManyIds column to understand how to use it.
22
+ """
23
+
24
+ """ The name of the many-to-many column we are attached to. """
25
+ many_to_many_column_name = configs.ModelColumn(required=True)
26
+
27
+ is_writeable = configs.Boolean(default=False)
28
+ is_searchable = configs.Boolean(default=False)
29
+ _descriptor_config_map = None
30
+
31
+ @decorators.parameters_to_properties
32
+ def __init__(
33
+ self,
34
+ many_to_many_column_name,
35
+ ):
36
+ pass
37
+
38
+ def finalize_configuration(self, model_class: type, name: str) -> None:
39
+ """Finalize and check the configuration."""
40
+ getattr(self.__class__, "many_to_many_column_name").set_model_class(model_class)
41
+ self.model_class = model_class
42
+ self.name = name
43
+ self.finalize_and_validate_configuration()
44
+
45
+ # finally, make sure we're really pointed at a many-to-many column
46
+ many_to_many_column = getattr(model_class, self.many_to_many_column_name)
47
+ if not isinstance(many_to_many_column, ManyToManyIds):
48
+ raise ValueError(
49
+ f"Error with configuration for {model_class.__name__}.{name}, which is a ManyToManyModels column. It needs to point to a ManyToManyIds column, and it was told to use {model_class.__name__}.{self.many_to_many_column_name}, but this is not a ManyToManyIds column."
50
+ )
51
+
52
+ @property
53
+ def pivot_model(self):
54
+ return self.di.build(self.many_to_many_column.pivot_model_class, cache=True) # type: ignore
55
+
56
+ @property
57
+ def related_models(self):
58
+ return self.di.build(self.many_to_many_column.related_model_class, cache=True) # type: ignore
59
+
60
+ @property
61
+ def related_columns(self):
62
+ return self.related_models.get_columns()
63
+
64
+ @property
65
+ def many_to_many_column(self) -> ManyToManyIds:
66
+ return getattr(self.model_class, self.many_to_many_column_name)
67
+
68
+ @overload
69
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
70
+ pass
71
+
72
+ @overload
73
+ def __get__(self, instance: Model, cls: type[Model]) -> Model:
74
+ pass
75
+
76
+ def __get__(self, instance, cls):
77
+ if instance is None:
78
+ self.model_class = cls
79
+ return self
80
+
81
+ # this makes sure we're initialized
82
+ if "name" not in self._config: # type: ignore
83
+ instance.get_columns()
84
+
85
+ return self.many_to_many_column.get_related_models(instance) # type: ignore
86
+
87
+ def __set__(self, instance, value: Model | list[Model] | list[dict[str, Any]]) -> None:
88
+ # this makes sure we're initialized
89
+ if "name" not in self._config: # type: ignore
90
+ instance.get_columns()
91
+
92
+ # we allow a list of models or a model, but if it's a model it may represent a single record or a query.
93
+ # if it's a single record then we want to wrap it in a list so we can iterate over it.
94
+ if hasattr(value, "_data") and isinstance(value, Model) and value._data:
95
+ value = []
96
+ many_to_many_column: ManyToManyIds = self.many_to_many_column # type: ignore
97
+ related_model_class = many_to_many_column.related_model_class
98
+ related_id_column_name = related_model_class.id_column_name
99
+ record_ids = []
100
+ for index, record in enumerate(value):
101
+ if isinstance(record, dict):
102
+ if not record.get(related_id_column_name):
103
+ raise KeyError(
104
+ f"A list of dictionaries was set to '{self.model_class.__name__}.{self.name}', in which case each dictionary should contain the key '{related_id_column_name}', which should be the id of an entry for the '{related_model_class.__name__}' model. However, no such key was found for entry #{index + 1}"
105
+ )
106
+ record_ids.append(record[related_id_column_name])
107
+ continue
108
+
109
+ # if we get here then the entry should be a model for our related model class
110
+ if not isinstance(record, related_model_class):
111
+ raise TypeError(
112
+ f"Models were sent to '{self.model_class.__name__}.{self.name}', in which case it should be a list of models of type {related_model_class.__name__}. However, an object of type '{record.__class__.__name__}' was found for entry #{index + 1}"
113
+ )
114
+ record_ids.append(getattr(record, related_id_column_name))
115
+ setattr(instance, self.many_to_many_column_name, record_ids)
116
+
117
+ def add_search(self, model: Model, value: str, operator: str = "", relationship_reference: str = "") -> Model:
118
+ return self.many_to_many_column.add_search( # type: ignore
119
+ model, value, operator, relationship_reference=relationship_reference
120
+ ) # type: ignore
121
+
122
+ def to_json(self, model: Model) -> dict[str, Any]:
123
+ records = []
124
+ many_to_many_column: ManyToManyIds = self.many_to_many_column # type: ignore
125
+ columns = many_to_many_column.related_columns
126
+ related_id_column_name = many_to_many_column.related_model_class.id_column_name
127
+ for related in many_to_many_column.get_related_models(model):
128
+ json = OrderedDict()
129
+ if related_id_column_name not in many_to_many_column.readable_related_column_names:
130
+ json[related_id_column_name] = columns[related_id_column_name].to_json(related)
131
+ for column_name in many_to_many_column.readable_related_column_names:
132
+ column_data = columns[column_name].to_json(related)
133
+ if type(column_data) == dict:
134
+ json = {**json, **column_data} # type: ignore
135
+ else:
136
+ json[column_name] = column_data
137
+ records.append(json)
138
+ return {self.name: records}
139
+
140
+ def documentation(self, name: str | None = None, example: str | None = None, value: str | None = None):
141
+ many_to_many_column = self.many_to_many_column # type: ignore
142
+ columns = many_to_many_column.related_columns
143
+ related_id_column_name = many_to_many_column.related_model_class.id_column_name
144
+ related_properties = [columns[related_id_column_name].documentation()]
145
+
146
+ for column_name in many_to_many_column.readable_related_column_names:
147
+ related_docs = columns[column_name].documentation()
148
+ if not isinstance(related_docs, list):
149
+ related_docs = [related_docs]
150
+ related_properties.extend(related_docs)
151
+
152
+ related_object = AutoDocObject(
153
+ string.title_case_to_nice(many_to_many_column.related_model_class.__name__),
154
+ related_properties,
155
+ )
156
+ return AutoDocArray(name if name is not None else self.name, related_object, value=value)
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import OrderedDict
4
+ from typing import TYPE_CHECKING, Any, Self, overload
5
+
6
+ from clearskies import configs, decorators
7
+ from clearskies.autodoc.schema import Array as AutoDocArray
8
+ from clearskies.autodoc.schema import Object as AutoDocObject
9
+ from clearskies.column import Column
10
+ from clearskies.columns.many_to_many_ids import ManyToManyIds
11
+ from clearskies.functional import string
12
+
13
+ if TYPE_CHECKING:
14
+ from clearskies import Model
15
+
16
+
17
+ class ManyToManyPivots(Column):
18
+ """
19
+ A companion for the ManyToManyIds column that returns the matching pivot models instead of the ids.
20
+
21
+ See ManyToManyIdsWithData for an example of how to use it (but note that it works just the same for the
22
+ ManyToManyIds column).
23
+ """
24
+
25
+ """ The name of the many-to-many column we are attached to. """
26
+ many_to_many_column_name = configs.ModelColumn(required=True)
27
+
28
+ is_writeable = configs.Boolean(default=False)
29
+ is_searchable = configs.Boolean(default=False)
30
+ _descriptor_config_map = None
31
+
32
+ @decorators.parameters_to_properties
33
+ def __init__(
34
+ self,
35
+ many_to_many_column_name,
36
+ ):
37
+ pass
38
+
39
+ def finalize_configuration(self, model_class: type, name: str) -> None:
40
+ """Finalize and check the configuration."""
41
+ getattr(self.__class__, "many_to_many_column_name").set_model_class(model_class)
42
+ self.model_class = model_class
43
+ self.name = name
44
+ self.finalize_and_validate_configuration()
45
+
46
+ # finally, make sure we're really pointed at a many-to-many column
47
+ many_to_many_column = getattr(model_class, self.many_to_many_column_name)
48
+ if not isinstance(many_to_many_column, ManyToManyIds):
49
+ raise ValueError(
50
+ f"Error with configuration for {model_class.__name__}.{name}, which is a ManyToManyModels column. It needs to point to a ManyToManyIds column, and it was told to use {model_class.__name__}.{self.many_to_many_column_name}, but this is not a ManyToManyIds column."
51
+ )
52
+
53
+ @property
54
+ def pivot_model(self) -> Model:
55
+ return self.di.build(self.many_to_many_column.pivot_model_class, cache=True) # type: ignore
56
+
57
+ @property
58
+ def related_models(self) -> Model:
59
+ return self.di.build(self.many_to_many_column.related_model_class, cache=True) # type: ignore
60
+
61
+ @property
62
+ def related_columns(self):
63
+ return self.related_models.get_columns()
64
+
65
+ @property
66
+ def many_to_many_column(self) -> ManyToManyIds:
67
+ return getattr(self.model_class, self.many_to_many_column_name)
68
+
69
+ @overload
70
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
71
+ pass
72
+
73
+ @overload
74
+ def __get__(self, instance: Model, cls: type[Model]) -> Model:
75
+ pass
76
+
77
+ def __get__(self, instance, cls):
78
+ if instance is None:
79
+ self.model_class = cls
80
+ return self
81
+
82
+ # this makes sure we're initialized
83
+ if "name" not in self._config: # type: ignore
84
+ instance.get_columns()
85
+
86
+ many_to_many_column = self.many_to_many_column # type: ignore
87
+ own_column_name_in_pivot = many_to_many_column._config("own_column_name_in_pivot")
88
+ my_id = getattr(instance, instance.id_column_name)
89
+ return [model for model in self.pivot_model.where(f"{own_column_name_in_pivot}={my_id}")]
90
+
91
+ def __set__(self, instance, value: Model | list[Model] | list[dict[str, Any]]) -> None:
92
+ raise NotImplementedError("Saving not supported for ManyToManyPivots")
93
+
94
+ def add_search(self, model: Model, value: str, operator: str = "", relationship_reference: str = "") -> Model:
95
+ raise NotImplementedError("Searching not supported for ManyToManyPivots")
96
+
97
+ def to_json(self, model: Model) -> dict[str, Any]:
98
+ records = []
99
+ many_to_many_column = self.many_to_many_column # type: ignore
100
+ columns = many_to_many_column.pivot_columns
101
+ readable_column_names = many_to_many_column.readable_pivot_column_names
102
+ pivot_id_column_name = many_to_many_column.pivot_model_class.id_column_name
103
+ for pivot in many_to_many_column.get_pivot_models(model):
104
+ json = OrderedDict()
105
+ if pivot_id_column_name not in readable_column_names:
106
+ json[pivot_id_column_name] = columns[pivot_id_column_name].to_json(pivot)
107
+ for column_name in readable_column_names:
108
+ column_data = columns[column_name].to_json(pivot)
109
+ if type(column_data) == dict:
110
+ json = {**json, **column_data} # type: ignore
111
+ else:
112
+ json[column_name] = column_data
113
+ records.append(json)
114
+ return {self.name: records}
115
+
116
+ def documentation(self, name: str | None = None, example: str | None = None, value: str | None = None):
117
+ many_to_many_column = self.many_to_many_column # type: ignore
118
+ columns = many_to_many_column.pivot_columns
119
+ pivot_id_column_name = many_to_many_column.pivot_model_class.id_column_name
120
+ pivot_properties = [columns[pivot_id_column_name].documentation()]
121
+
122
+ for column_name in many_to_many_column.readable_pivot_column_names:
123
+ pivot_docs = columns[column_name].documentation()
124
+ if type(pivot_docs) != list:
125
+ pivot_docs = [pivot_docs]
126
+ pivot_properties.extend(pivot_docs)
127
+
128
+ pivot_object = AutoDocObject(
129
+ string.title_case_to_nice(many_to_many_column.pivot_model_class.__name__),
130
+ pivot_properties,
131
+ )
132
+ return AutoDocArray(name if name is not None else self.name, pivot_object, value=value)
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING, Any, Callable
5
+
6
+ from clearskies import configs, decorators
7
+ from clearskies.columns.string import String
8
+
9
+ if TYPE_CHECKING:
10
+ from clearskies import typing
11
+
12
+
13
+ class Phone(String):
14
+ """
15
+ A string column that stores a phone number.
16
+
17
+ The main difference between this and a plain string column is that this will validate that the string contains
18
+ a phone number (containing only digits, dashes, spaces, plus sign, and parenthesis) of the appropriate length.
19
+ When persisting the value to the backend, this column removes all non-digit characters.
20
+
21
+ If you also set the usa_only flag to true then it will also validate that it is a valid US number containing
22
+ 9 digits and, optionally, a leading `1`. Example:
23
+
24
+ ```python
25
+ import clearskies
26
+
27
+
28
+ class User(clearskies.Model):
29
+ id_column_name = "id"
30
+ backend = clearskies.backends.MemoryBackend()
31
+
32
+ id = clearskies.columns.Uuid()
33
+ name = clearskies.columns.String()
34
+ phone = clearskies.columns.Phone(usa_only=True)
35
+
36
+
37
+ wsgi = clearskies.contexts.WsgiRef(
38
+ clearskies.endpoints.Create(
39
+ User,
40
+ writeable_column_names=["name", "phone"],
41
+ readable_column_names=["id", "name", "phone"],
42
+ ),
43
+ )
44
+ wsgi()
45
+ ```
46
+
47
+ Which you can invoke:
48
+
49
+ ```bash
50
+ $ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "+1 (555) 451-1234"}' | jq
51
+ {
52
+ "status": "success",
53
+ "error": "",
54
+ "data": {
55
+ "id": "e2b4bdad-b70f-4d44-a94c-0e265868b4d2",
56
+ "name": "John Doe",
57
+ "phone": "15554511234"
58
+ },
59
+ "pagination": {},
60
+ "input_errors": {}
61
+ }
62
+
63
+ $ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "555 451-1234"}' | jq
64
+ {
65
+ "status": "success",
66
+ "error": "",
67
+ "data": {
68
+ "id": "aea34022-4b75-4eed-ac92-65fa4f4511ae",
69
+ "name": "John Doe",
70
+ "phone": "5554511234"
71
+ },
72
+ "pagination": {},
73
+ "input_errors": {}
74
+ }
75
+
76
+
77
+ $ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "555 451-12341"}' | jq
78
+ {
79
+ "status": "input_errors",
80
+ "error": "",
81
+ "data": [],
82
+ "pagination": {},
83
+ "input_errors": {
84
+ "phone": "Invalid phone number"
85
+ }
86
+ }
87
+
88
+ $ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "1-2-3-4 asdf"}' | jq
89
+ {
90
+ "status": "input_errors",
91
+ "error": "",
92
+ "data": [],
93
+ "pagination": {},
94
+ "input_errors": {
95
+ "phone": "Invalid phone number"
96
+ }
97
+ }
98
+ ```
99
+ """
100
+
101
+ """ Whether or not to allow non-USA numbers. """
102
+ usa_only = configs.Boolean(default=True)
103
+ _descriptor_config_map = None
104
+
105
+ @decorators.parameters_to_properties
106
+ def __init__(
107
+ self,
108
+ usa_only: bool = True,
109
+ default: str | None = None,
110
+ setable: str | Callable[..., str] | None = None,
111
+ is_readable: bool = True,
112
+ is_writeable: bool = True,
113
+ is_searchable: bool = True,
114
+ is_temporary: bool = False,
115
+ validators: typing.validator | list[typing.validator] = [],
116
+ on_change_pre_save: typing.action | list[typing.action] = [],
117
+ on_change_post_save: typing.action | list[typing.action] = [],
118
+ on_change_save_finished: typing.action | list[typing.action] = [],
119
+ created_by_source_type: str = "",
120
+ created_by_source_key: str = "",
121
+ created_by_source_strict: bool = True,
122
+ ):
123
+ pass
124
+
125
+ def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
126
+ if not data.get(self.name):
127
+ return data
128
+
129
+ # phone numbers are stored as only digits.
130
+ return {**data, **{self.name: re.sub(r"\D", "", data[self.name])}}
131
+
132
+ def input_error_for_value(self, value: str, operator: str | None = None) -> str:
133
+ if type(value) != str:
134
+ return f"Value must be a string for {self.name}"
135
+
136
+ # we'll allow spaces, dashes, parenthesis, dashes, and plus signs.
137
+ # if there is anything else then it's not a valid phone number.
138
+ # However, we don't do more detailed validation, because I'm too lazy to
139
+ # figure out what is and is not a valid phone number, especially when
140
+ # you get to the world of international numbers.
141
+ if re.search(r"[^\d \-()+]", value):
142
+ return "Invalid phone number"
143
+
144
+ # for some final validation (especially US numbers) work only with the digits.
145
+ value = re.sub(r"\D", "", value)
146
+
147
+ if len(value) > 15:
148
+ return "Invalid phone number"
149
+
150
+ # we can't be too short unless we're doing a fuzzy search
151
+ if len(value) < 10 and operator and operator.lower() != "like":
152
+ return "Invalid phone number"
153
+
154
+ if self.usa_only:
155
+ if len(value) > 11:
156
+ return "Invalid phone number"
157
+ if value[0] == "1" and len(value) != 11:
158
+ return "Invalid phone number"
159
+ if value[0] != "1" and len(value) != 10:
160
+ return "Invalid phone number"
161
+
162
+ return ""
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Callable
4
+
5
+ from clearskies import configs, decorators
6
+ from clearskies.columns.string import String
7
+
8
+ if TYPE_CHECKING:
9
+ from clearskies import typing
10
+
11
+
12
+ class Select(String):
13
+ """
14
+ A string column but, when writeable via an endpoint, only specific values are allowed.
15
+
16
+ Note: the allowed values are case sensitive.
17
+
18
+ ```python
19
+ import clearskies
20
+
21
+
22
+ class Order(clearskies.Model):
23
+ id_column_name = "id"
24
+ backend = clearskies.backends.MemoryBackend()
25
+
26
+ id = clearskies.columns.Uuid()
27
+ total = clearskies.columns.Float()
28
+ status = clearskies.columns.Select(["Open", "Processing", "Shipped", "Complete"])
29
+
30
+
31
+ wsgi = clearskies.contexts.WsgiRef(
32
+ clearskies.endpoints.Create(
33
+ Order,
34
+ writeable_column_names=["total", "status"],
35
+ readable_column_names=["id", "total", "status"],
36
+ ),
37
+ )
38
+ wsgi()
39
+ ```
40
+
41
+ And when invoked:
42
+
43
+ ```bash
44
+ $ curl http://localhost:8080 -d '{"total": 125, "status": "Open"}' | jq
45
+ {
46
+ "status": "success",
47
+ "error": "",
48
+ "data": {
49
+ "id": "22f2c950-6519-4d8e-9084-013455449b07",
50
+ "total": 125.0,
51
+ "status": "Open"
52
+ },
53
+ "pagination": {},
54
+ "input_errors": {}
55
+ }
56
+
57
+ $ curl http://localhost:8080 -d '{"total": 125, "status": "huh"}' | jq
58
+ {
59
+ "status": "input_errors",
60
+ "error": "",
61
+ "data": [],
62
+ "pagination": {},
63
+ "input_errors": {
64
+ "status": "Invalid value for status"
65
+ }
66
+ }
67
+ ```
68
+ """
69
+
70
+ """ The allowed values. """
71
+ allowed_values = configs.StringList(required=True)
72
+ _descriptor_config_map = None
73
+
74
+ @decorators.parameters_to_properties
75
+ def __init__(
76
+ self,
77
+ allowed_values: list[str],
78
+ default: str | None = None,
79
+ setable: str | Callable[..., str] | None = None,
80
+ is_readable: bool = True,
81
+ is_writeable: bool = True,
82
+ is_searchable: bool = True,
83
+ is_temporary: bool = False,
84
+ validators: typing.validator | list[typing.validator] = [],
85
+ on_change_pre_save: typing.action | list[typing.action] = [],
86
+ on_change_post_save: typing.action | list[typing.action] = [],
87
+ on_change_save_finished: typing.action | list[typing.action] = [],
88
+ created_by_source_type: str = "",
89
+ created_by_source_key: str = "",
90
+ created_by_source_strict: bool = True,
91
+ ):
92
+ pass
93
+
94
+ def input_error_for_value(self, value: str, operator: str | None = None) -> str:
95
+ return f"Invalid value for {self.name}" if value not in self.allowed_values else ""
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Self, overload
4
+
5
+ from clearskies.column import Column
6
+
7
+ if TYPE_CHECKING:
8
+ from clearskies import Model
9
+
10
+
11
+ class String(Column):
12
+ """
13
+ A simple string column.
14
+
15
+ ```python
16
+ import clearskies
17
+
18
+
19
+ class Pet(clearskies.Model):
20
+ id_column_name = "id"
21
+ backend = clearskies.backends.MemoryBackend()
22
+
23
+ id = clearskies.columns.Uuid()
24
+ name = clearskies.columns.String()
25
+
26
+
27
+ wsgi = clearskies.contexts.WsgiRef(
28
+ clearskies.endpoints.Create(
29
+ Pet,
30
+ writeable_column_names=["name"],
31
+ readable_column_names=["id", "name"],
32
+ ),
33
+ )
34
+ wsgi()
35
+ ```
36
+
37
+ And when invoked:
38
+
39
+ ```bash
40
+ $ curl http://localhost:8080 -d '{"name": "Spot"}' | jq
41
+ {
42
+ "status": "success",
43
+ "error": "",
44
+ "data": {
45
+ "id": "e5b8417f-91bc-4fe5-9b64-04f571a7b10a",
46
+ "name": "Spot"
47
+ },
48
+ "pagination": {},
49
+ "input_errors": {}
50
+ }
51
+
52
+ $ curl http://localhost:8080 -d '{"name": 10}' | jq
53
+ {
54
+ "status": "input_errors",
55
+ "error": "",
56
+ "data": [],
57
+ "pagination": {},
58
+ "input_errors": {
59
+ "name": "value should be a string"
60
+ }
61
+ }
62
+
63
+ ```
64
+ """
65
+
66
+ _allowed_search_operators = ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null", "like"]
67
+ _descriptor_config_map = None
68
+
69
+ @overload
70
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
71
+ pass
72
+
73
+ @overload
74
+ def __get__(self, instance: Model, cls: type[Model]) -> str:
75
+ pass
76
+
77
+ def __get__(self, instance, cls):
78
+ if instance is None:
79
+ self.model_class = cls
80
+ return self
81
+
82
+ # this makes sure we're initialized
83
+ if "name" not in self._config: # type: ignore
84
+ instance.get_columns()
85
+
86
+ if self.name not in instance._data:
87
+ return None # type: ignore
88
+
89
+ if self.name not in instance._transformed_data:
90
+ instance._transformed_data[self.name] = self.from_backend(instance._data[self.name])
91
+
92
+ return instance._transformed_data[self.name]
93
+
94
+ def __set__(self, instance: Model, value: str) -> None:
95
+ # this makes sure we're initialized
96
+ if "name" not in self._config: # type: ignore
97
+ instance.get_columns()
98
+
99
+ instance._next_data[self.name] = value
100
+
101
+ def input_error_for_value(self, value: str, operator: str | None = None) -> str:
102
+ return "value should be a string" if not isinstance(value, str) else ""