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,149 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Callable, Self, overload
4
+
5
+ from clearskies import configs, decorators
6
+ from clearskies.autodoc.schema import Number as AutoDocNumber
7
+ from clearskies.column import Column
8
+
9
+ if TYPE_CHECKING:
10
+ from clearskies import Model, typing
11
+ from clearskies.autodoc.schema import Schema as AutoDocSchema
12
+ from clearskies.query import Condition
13
+
14
+
15
+ class Float(Column):
16
+ """
17
+ A column that stores a float.
18
+
19
+ ```python
20
+ import clearskies
21
+
22
+
23
+ class MyModel(clearskies.Model):
24
+ backend = clearskies.backends.MemoryBackend()
25
+ id_column_name = "id"
26
+
27
+ id = clearskies.columns.Uuid()
28
+ score = clearskies.columns.Float()
29
+
30
+
31
+ wsgi = clearskies.contexts.WsgiRef(
32
+ clearskies.endpoints.Create(
33
+ MyModel,
34
+ writeable_column_names=["score"],
35
+ readable_column_names=["id", "score"],
36
+ ),
37
+ classes=[MyModel],
38
+ )
39
+ wsgi()
40
+ ```
41
+
42
+ and when invoked:
43
+
44
+ ```bash
45
+ $ curl 'http://localhost:8080' -d '{"score":15.2}' | jq
46
+ {
47
+ "status": "success",
48
+ "error": "",
49
+ "data": {
50
+ "id": "7b5658a9-7573-4676-bf18-64ddc90ad87d",
51
+ "score": 15.2
52
+ },
53
+ "pagination": {},
54
+ "input_errors": {}
55
+ }
56
+
57
+ $ curl 'http://localhost:8080' -d '{"score":"15.2"}' | jq
58
+ {
59
+ "status": "input_errors",
60
+ "error": "",
61
+ "data": [],
62
+ "pagination": {},
63
+ "input_errors": {
64
+ "score": "value should be an integer or float"
65
+ }
66
+ }
67
+ ```
68
+ """
69
+
70
+ default = configs.Float() # type: ignore
71
+ setable = configs.FloatOrCallable(default=None) # type: ignore
72
+ _allowed_search_operators = ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null"]
73
+ auto_doc_class: type[AutoDocSchema] = AutoDocNumber
74
+ _descriptor_config_map = None
75
+
76
+ @decorators.parameters_to_properties
77
+ def __init__(
78
+ self,
79
+ default: float | None = None,
80
+ setable: float | Callable[..., float] | None = None,
81
+ is_readable: bool = True,
82
+ is_writeable: bool = True,
83
+ is_searchable: bool = True,
84
+ is_temporary: bool = False,
85
+ validators: typing.validator | list[typing.validator] = [],
86
+ on_change_pre_save: typing.action | list[typing.action] = [],
87
+ on_change_post_save: typing.action | list[typing.action] = [],
88
+ on_change_save_finished: typing.action | list[typing.action] = [],
89
+ created_by_source_type: str = "",
90
+ created_by_source_key: str = "",
91
+ created_by_source_strict: bool = True,
92
+ ):
93
+ pass
94
+
95
+ @overload
96
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
97
+ pass
98
+
99
+ @overload
100
+ def __get__(self, instance: Model, cls: type[Model]) -> float:
101
+ pass
102
+
103
+ def __get__(self, instance, cls):
104
+ return super().__get__(instance, cls)
105
+
106
+ def __set__(self, instance, value: float) -> None:
107
+ # this makes sure we're initialized
108
+ if "name" not in self._config: # type: ignore
109
+ instance.get_columns()
110
+
111
+ instance._next_data[self.name] = float(value)
112
+
113
+ def from_backend(self, value) -> float:
114
+ return float(value)
115
+
116
+ def to_backend(self, data):
117
+ if self.name not in data or data[self.name] is None:
118
+ return data
119
+
120
+ return {**data, self.name: float(data[self.name])}
121
+
122
+ def equals(self, value: float) -> Condition:
123
+ return super().equals(value)
124
+
125
+ def spaceship(self, value: float) -> Condition:
126
+ return super().spaceship(value)
127
+
128
+ def not_equals(self, value: float) -> Condition:
129
+ return super().not_equals(value)
130
+
131
+ def less_than_equals(self, value: float) -> Condition:
132
+ return super().less_than_equals(value)
133
+
134
+ def greater_than_equals(self, value: float) -> Condition:
135
+ return super().greater_than_equals(value)
136
+
137
+ def less_than(self, value: float) -> Condition:
138
+ return super().less_than(value)
139
+
140
+ def greater_than(self, value: float) -> Condition:
141
+ return super().greater_than(value)
142
+
143
+ def is_in(self, values: list[float]) -> Condition:
144
+ return super().is_in(values)
145
+
146
+ def input_error_for_value(self, value, operator=None):
147
+ return (
148
+ "value should be an integer or float" if not isinstance(value, (int, float)) and value is not None else ""
149
+ )
@@ -0,0 +1,529 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Self, overload
4
+
5
+ from clearskies import configs, decorators, typing
6
+ from clearskies.autodoc.schema import Array as AutoDocArray
7
+ from clearskies.autodoc.schema import Object as AutoDocObject
8
+ from clearskies.column import Column
9
+ from clearskies.di.inject import InputOutput
10
+ from clearskies.functional import string, validations
11
+
12
+ if TYPE_CHECKING:
13
+ from clearskies import Column, Model, typing
14
+ from clearskies.autodoc.schema import Schema as AutoDocSchema
15
+
16
+
17
+ class HasMany(Column):
18
+ """
19
+ A column to manage a "has many" relationship.
20
+
21
+ In order to manage a has-many relationship, the child model needs a column that stores the
22
+ id of the parent record it belongs to. Also remember that the reverse of a has-many relationship
23
+ is a belongs-to relationship: the parent has many children, the child belongs to a parent.
24
+
25
+ There's an automatic standard where the name of the column in thie child table that stores the
26
+ parent id is made by converting the parent model class name into snake case and then appending
27
+ `_id`. For instance, if the parent model is called the `DooHicky` class, the child model is
28
+ expected to have a column named `doo_hicky_id`. If you use a different column name for the
29
+ id in your child model, then just update the `foreign_column_name` property on the `HasMany`
30
+ column accordingly.
31
+
32
+ See the BelongsToId class for additional background and directions on avoiding circular dependency trees.
33
+
34
+ ```python
35
+ import clearskies
36
+
37
+
38
+ class Product(clearskies.Model):
39
+ id_column_name = "id"
40
+ backend = clearskies.backends.MemoryBackend()
41
+
42
+ id = clearskies.columns.Uuid()
43
+ name = clearskies.columns.String()
44
+ category_id = clearskies.columns.String()
45
+
46
+
47
+ class Category(clearskies.Model):
48
+ id_column_name = "id"
49
+ backend = clearskies.backends.MemoryBackend()
50
+
51
+ id = clearskies.columns.Uuid()
52
+ name = clearskies.columns.String()
53
+ products = clearskies.columns.HasMany(Product)
54
+
55
+
56
+ def test_has_many(products: Product, categories: Category):
57
+ toys = categories.create({"name": "Toys"})
58
+ auto = categories.create({"name": "Auto"})
59
+
60
+ # create some toys
61
+ ball = products.create({"name": "Ball", "category_id": toys.id})
62
+ fidget_spinner = products.create({"name": "Fidget Spinner", "category_id": toys.id})
63
+ crayon = products.create({"name": "Crayon", "category_id": toys.id})
64
+
65
+ # the HasMany column is an interable of matching records
66
+ toy_names = [product.name for product in toys.products]
67
+
68
+ # it specifically returns a models object so you can do more filtering/transformations
69
+ return toys.products.sort_by("name", "asc")
70
+
71
+
72
+ cli = clearskies.contexts.Cli(
73
+ clearskies.endpoints.Callable(
74
+ test_has_many,
75
+ model_class=Product,
76
+ readable_column_names=["id", "name"],
77
+ ),
78
+ classes=[Category, Product],
79
+ )
80
+
81
+ if __name__ == "__main__":
82
+ cli()
83
+ ```
84
+
85
+ And if you execute this it will return:
86
+
87
+ ```json
88
+ {
89
+ "status": "success",
90
+ "error": "",
91
+ "data": [
92
+ {"id": "edc68e8d-7fc8-45ce-98f0-9c6f883e4e7f", "name": "Ball"},
93
+ {"id": "b51a0de5-c784-4e0c-880c-56e5bf731dfd", "name": "Crayon"},
94
+ {"id": "06cec3af-d042-4d6b-a99c-b4a0072f188d", "name": "Fidget Spinner"},
95
+ ],
96
+ "pagination": {},
97
+ "input_errors": {},
98
+ }
99
+ ```
100
+ """
101
+
102
+ """
103
+ HasMany columns are not currently writeable.
104
+ """
105
+ is_writeable = configs.Boolean(default=False)
106
+ is_searchable = configs.Boolean(default=False)
107
+ _descriptor_config_map = None
108
+
109
+ """ The model class for the child table we keep our "many" records in. """
110
+ child_model_class = configs.ModelClass(required=True)
111
+
112
+ """
113
+ The name of the column in the child table that connects it back to the parent.
114
+
115
+ By default this is populated by converting the model class name from TitleCase to snake_case and appending _id.
116
+ So, if the model class is called `ProductCategory`, this becomes `product_category_id`. This MUST correspond to
117
+ the actual name of a column in the child table. This is used so that the parent can find its child records.
118
+
119
+ Example:
120
+
121
+ ```python
122
+ import clearskies
123
+
124
+ class Product(clearskies.Model):
125
+ id_column_name = "id"
126
+ backend = clearskies.backends.MemoryBackend()
127
+
128
+ id = clearskies.columns.Uuid()
129
+ name = clearskies.columns.String()
130
+ my_parent_category_id = clearskies.columns.String()
131
+
132
+ class Category(clearskies.Model):
133
+ id_column_name = "id"
134
+ backend = clearskies.backends.MemoryBackend()
135
+
136
+ id = clearskies.columns.Uuid()
137
+ name = clearskies.columns.String()
138
+ products = clearskies.columns.HasMany(Product, foreign_column_name="my_parent_category_id")
139
+
140
+ def test_has_many(products: Product, categories: Category):
141
+ toys = categories.create({"name": "Toys"})
142
+
143
+ fidget_spinner = products.create({"name": "Fidget Spinner", "my_parent_category_id": toys.id})
144
+ crayon = products.create({"name": "Crayon", "my_parent_category_id": toys.id})
145
+ ball = products.create({"name": "Ball", "my_parent_category_id": toys.id})
146
+
147
+ return toys.products.sort_by("name", "asc")
148
+
149
+ cli = clearskies.contexts.Cli(
150
+ clearskies.endpoints.Callable(
151
+ test_has_many,
152
+ model_class=Product,
153
+ readable_column_names=["id", "name"],
154
+ ),
155
+ classes=[Category, Product],
156
+ )
157
+
158
+ if __name__ == "__main__":
159
+ cli()
160
+ ```
161
+
162
+ Compare to the first example for the HasMany class. In that case, the column in the product model which
163
+ contained the category id was `category_id`, and the `products` column didn't have to specify the
164
+ `foreign_column_name` (since the column name followed the naming rule). As a result, `category.products`
165
+ was able to find all children of a given category. In this example, the name of the column in the product
166
+ model that contains the category id was changed to `my_parent_category_id`. Since this no longer matches
167
+ the naming convention, we had to specify `foreign_column_name="my_parent_category_id"` in `Category.products`,
168
+ in order for the `HasMany` column to find the children. Therefore, when invoked it returns the same thing:
169
+
170
+ ```json
171
+ {
172
+ "status": "success",
173
+ "error": "",
174
+ "data": [
175
+ {
176
+ "id": "3cdd06e0-b226-4a4a-962d-e8c5acc759ac",
177
+ "name": "Ball"
178
+ },
179
+ {
180
+ "id": "debc7968-976a-49cd-902c-d359a8abd032",
181
+ "name": "Crayon"
182
+ },
183
+ {
184
+ "id": "0afcd314-cdfc-4a27-ac6e-061b74ee5bf9",
185
+ "name": "Fidget Spinner"
186
+ }
187
+ ],
188
+ "pagination": {},
189
+ "input_errors": {}
190
+ }
191
+ ```
192
+ """
193
+ foreign_column_name = configs.ModelToIdColumn()
194
+
195
+ """
196
+ Columns from the child table that should be included when converting this column to JSON.
197
+
198
+ You can tell an endpoint to include a `HasMany` column in the response. If you do this, the columns
199
+ from the child class that are included in the JSON response are determined by `readable_child_column_names`.
200
+ Example:
201
+
202
+ ```python
203
+ import clearskies
204
+
205
+ class Product(clearskies.Model):
206
+ id_column_name = "id"
207
+ backend = clearskies.backends.MemoryBackend()
208
+
209
+ id = clearskies.columns.Uuid()
210
+ name = clearskies.columns.String()
211
+ category_id = clearskies.columns.String()
212
+
213
+ class Category(clearskies.Model):
214
+ id_column_name = "id"
215
+ backend = clearskies.backends.MemoryBackend()
216
+
217
+ id = clearskies.columns.Uuid()
218
+ name = clearskies.columns.String()
219
+ products = clearskies.columns.HasMany(Product, readable_child_column_names=["id", "name"])
220
+
221
+ def test_has_many(products: Product, categories: Category):
222
+ toys = categories.create({"name": "Toys"})
223
+
224
+ fidget_spinner = products.create({"name": "Fidget Spinner", "category_id": toys.id})
225
+ ball = products.create({"name": "Ball", "category_id": toys.id})
226
+ crayon = products.create({"name": "Crayon", "category_id": toys.id})
227
+
228
+ return toys
229
+
230
+ cli = clearskies.contexts.Cli(
231
+ clearskies.endpoints.Callable(
232
+ test_has_many,
233
+ model_class=Category,
234
+ readable_column_names=["id", "name", "products"],
235
+ ),
236
+ classes=[Category, Product],
237
+ )
238
+
239
+ if __name__ == "__main__":
240
+ cli()
241
+ ```
242
+
243
+ In this example we're no longer returning a list of products directly. Instead, we're returning a query
244
+ on the categories nodel and asking the endpoint to also unpack their products. We set `readable_child_column_names`
245
+ to `["id", "name"]` for `Category.products`, so when the endpoint unpacks the products, it includes those columns:
246
+
247
+ ```json
248
+ {
249
+ "status": "success",
250
+ "error": "",
251
+ "data": [
252
+ {
253
+ "id": "c8a71c81-fa0e-427d-a166-159f3c9de72b",
254
+ "name": "Office Supplies",
255
+ "products": [
256
+ {
257
+ "id": "6d24ffa2-6e1b-4ce9-87ff-daf2ba237c92",
258
+ "name": "Stapler"
259
+ },
260
+ {
261
+ "id": "3a42cd7d-6804-465e-9fb1-055fafa7fc62",
262
+ "name": "Chair"
263
+ }
264
+ ]
265
+ },
266
+ {
267
+ "id": "5a790950-858b-411a-bf5c-1338a28e73d0",
268
+ "name": "Toys",
269
+ "products": [
270
+ {
271
+ "id": "d4022224-cc22-49c2-8da9-7a8f9fa7e976",
272
+ "name": "Fidget Spinner"
273
+ },
274
+ {
275
+ "id": "415fa48e-984a-4703-b6e6-f88f741403c8",
276
+ "name": "Ball"
277
+ },
278
+ {
279
+ "id": "58328363-5180-441c-b1a8-1b92e12a8f08",
280
+ "name": "Crayon"
281
+ }
282
+ ]
283
+ }
284
+ ],
285
+ "pagination": {},
286
+ "input_errors": {}
287
+ }
288
+
289
+ ```
290
+
291
+ """
292
+ readable_child_column_names = configs.ReadableModelColumns("child_model_class")
293
+
294
+ """
295
+ Additional conditions to add to searches on the child table.
296
+
297
+ There are two ways to specify conditions for `where`. You can provide a static search condition
298
+ which can come in the form of a string with an sql-like where clause (e.g. `age>5`) or a
299
+ `clearskies.query.Condition` object, which is typically built using the appropriate method on the
300
+ model columns (e.g. `User.age.greater_than(5)`. Finally, you can provide a callable which will
301
+ be invoked when the query on the child model is being built. Your callable will be passed the
302
+ child model, as well as the parent model, and should then add additional conditions as needed
303
+ and return the modified qurey on the child model.
304
+
305
+ ### Example: Providing Conditions
306
+
307
+ The below example shows adding conditions with both an sql-like string and a condition object.
308
+ Note that `where` can be either a list or a single condition.
309
+
310
+ ```python
311
+ import clearskies
312
+
313
+ class Order(clearskies.Model):
314
+ id_column_name = "id"
315
+ backend = clearskies.backends.MemoryBackend()
316
+
317
+ id = clearskies.columns.Uuid()
318
+ total = clearskies.columns.Float()
319
+ status = clearskies.columns.Select(["Open", "In Progress", "Closed"])
320
+ user_id = clearskies.columns.String()
321
+
322
+ class User(clearskies.Model):
323
+ id_column_name = "id"
324
+ backend = clearskies.backends.MemoryBackend()
325
+
326
+ id = clearskies.columns.Uuid()
327
+ name = clearskies.columns.String()
328
+ orders = clearskies.columns.HasMany(Order, readable_child_column_names=["id", "status"])
329
+ large_open_orders = clearskies.columns.HasMany(
330
+ Order,
331
+ readable_child_column_names=["id", "status"],
332
+ where=[Order.status.equals("Open"), "total>100"],
333
+ )
334
+
335
+ def test_has_many(users: User, orders: Order):
336
+ user = users.create({"name": "Bob"})
337
+
338
+ order_1 = orders.create({"status": "Open", "total": 25.50, "user_id": user.id})
339
+ order_2 = orders.create({"status": "Closed", "total": 35.50, "user_id": user.id})
340
+ order_3 = orders.create({"status": "Open", "total": 125, "user_id": user.id})
341
+ order_4 = orders.create({"status": "In Progress", "total": 25.50, "user_id": user.id})
342
+
343
+ return user.large_open_orders
344
+
345
+ cli = clearskies.contexts.Cli(
346
+ clearskies.endpoints.Callable(
347
+ test_has_many,
348
+ model_class=Order,
349
+ readable_column_names=["id", "total", "status"],
350
+ return_records=True,
351
+ ),
352
+ classes=[Order, User],
353
+ )
354
+
355
+ if __name__ == "__main__":
356
+ cli()
357
+ ```
358
+
359
+ If you invoked this you would get:
360
+
361
+ ```json
362
+ {
363
+ "status": "success",
364
+ "error": "",
365
+ "data": [
366
+ {
367
+ "id": "6ad99935-ac9a-40ef-a1b2-f34538cc6529",
368
+ "total": 125.0,
369
+ "status": "Open"
370
+ }
371
+ ],
372
+ "pagination": {},
373
+ "input_errors": {}
374
+ }
375
+ ```
376
+
377
+ ### Example: Providing Callables
378
+
379
+ The below example shows how to provide a callable to the where. In this example we show the use
380
+ of a lambda function, but of course it could be a more standard function or any other callable.
381
+ The final conditions are identitical to the example above, so the end result is the same.
382
+
383
+ ```python
384
+ class User(clearskies.Model):
385
+ # removing unchanged part for brevity
386
+ large_open_orders = clearskies.columns.HasMany(
387
+ Order,
388
+ readable_child_column_names=["id", "status"],
389
+ where=lambda model: model.where("status=Open").where("total>100"),
390
+ )
391
+ ```
392
+
393
+ Note that your callable should always accept an argument named `model`. This will be an instance
394
+ of your child model class and holds the query being built to find child models. Before your callable
395
+ is invoked, the HasMany column will already have added the necessary condition to filter child records
396
+ down to the ones related to the parent in question. Therefore, your callable just needs to add
397
+ in any extra conditions you might want. You can also accept an argument named `parent` which will
398
+ be populated by the model instance for the specific parent that clearskies is working with. This
399
+ can be helpful if you need to filter on more than one column from the parent model. Finally, you
400
+ can request any additional args or kwargs defined in the dependency injection system, including
401
+ any named values provided by the context.
402
+ """
403
+ where = configs.Conditions()
404
+
405
+ input_output = InputOutput()
406
+
407
+ @decorators.parameters_to_properties
408
+ def __init__(
409
+ self,
410
+ child_model_class,
411
+ foreign_column_name: str | None = None,
412
+ readable_child_column_names: list[str] = [],
413
+ where: typing.condition | list[typing.condition] = [],
414
+ is_readable: bool = True,
415
+ on_change_pre_save: typing.action | list[typing.action] = [],
416
+ on_change_post_save: typing.action | list[typing.action] = [],
417
+ on_change_save_finished: typing.action | list[typing.action] = [],
418
+ ):
419
+ pass
420
+
421
+ def finalize_configuration(self, model_class, name) -> None:
422
+ """
423
+ Finalize and check the configuration.
424
+
425
+ This is an external trigger called by the model class when the model class is ready.
426
+ The reason it exists here instead of in the constructor is because some columns are tightly
427
+ connected to the model class, and can't validate configuration until they know what the model is.
428
+ Therefore, we need the model involved, and the only way for a property to know what class it is
429
+ in is if the parent class checks in (which is what happens here).
430
+ """
431
+ # this is where we auto-calculate the expected name of our id column in the child model.
432
+ # we can't do it until now because it comes from the model class we are connected to, and
433
+ # we only just get it.
434
+ foreign_column_name_config = self._get_config_object("foreign_column_name")
435
+ foreign_column_name_config.set_model_class(self.child_model_class)
436
+ has_value = False
437
+ try:
438
+ has_value = bool(self.foreign_column_name)
439
+ except KeyError:
440
+ pass
441
+
442
+ if not has_value:
443
+ self.foreign_column_name = string.camel_case_to_snake_case(model_class.__name__) + "_id"
444
+
445
+ super().finalize_configuration(model_class, name)
446
+
447
+ @property
448
+ def child_columns(self) -> dict[str, Column]:
449
+ return self.child_model_class.get_columns()
450
+
451
+ @property
452
+ def child_model(self) -> Model:
453
+ return self.di.build(self.child_model_class, cache=True)
454
+
455
+ @overload
456
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
457
+ pass
458
+
459
+ @overload
460
+ def __get__(self, instance: Model, cls: type[Model]) -> Model:
461
+ pass
462
+
463
+ def __get__(self, model, cls):
464
+ if model is None:
465
+ self.model_class = cls
466
+ return self # type: ignore
467
+
468
+ # this makes sure we're initialized
469
+ if "name" not in self._config: # type: ignore
470
+ model.get_columns()
471
+
472
+ foreign_column_name = self.foreign_column_name
473
+ model_id = getattr(model, model.id_column_name)
474
+ children = self.child_model.where(f"{foreign_column_name}={model_id}")
475
+
476
+ if not self.where:
477
+ return children
478
+
479
+ for index, where in enumerate(self.where):
480
+ if callable(where):
481
+ children = self.di.call_function(
482
+ where, model=children, parent=model, **self.input_output.get_context_for_callables()
483
+ )
484
+ if not validations.is_model(children):
485
+ raise ValueError(
486
+ 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"
487
+ )
488
+ else:
489
+ children = children.where(where)
490
+ return children
491
+
492
+ def __set__(self, model: Model, value: Model) -> None:
493
+ raise ValueError(
494
+ f"Attempt to set a value to {model.__class__.__name__}.{self.name}: this is not allowed because it is a HasMany column, which is not writeable."
495
+ )
496
+
497
+ def to_json(self, model: Model) -> dict[str, Any]:
498
+ children = []
499
+ columns = self.child_columns
500
+ child_id_column_name = self.child_model_class.id_column_name
501
+ json: dict[str, Any] = {}
502
+ for child in getattr(model, self.name):
503
+ json = {
504
+ **json,
505
+ **columns[child_id_column_name].to_json(child),
506
+ }
507
+ for column_name in self.readable_child_column_names:
508
+ json = {
509
+ **json,
510
+ **columns[column_name].to_json(child),
511
+ }
512
+ children.append(json)
513
+ return {self.name: children}
514
+
515
+ def documentation(
516
+ self, name: str | None = None, example: str | None = None, value: str | None = None
517
+ ) -> list[AutoDocSchema]:
518
+ columns = self.child_columns
519
+ child_id_column_name = self.child_model.id_column_name
520
+ child_properties = [columns[child_id_column_name].documentation()]
521
+
522
+ for column_name in self.readable_child_column_names:
523
+ child_properties.extend(columns[column_name].documentation()) # type: ignore
524
+
525
+ child_object = AutoDocObject(
526
+ string.title_case_to_nice(self.child_model_class.__name__),
527
+ child_properties,
528
+ )
529
+ return [AutoDocArray(name if name is not None else self.name, child_object, value=value)]