clear-skies 2.0.5__py3-none-any.whl → 2.0.6__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.

Potentially problematic release.


This version of clear-skies might be problematic. Click here for more details.

Files changed (252) hide show
  1. {clear_skies-2.0.5.dist-info → clear_skies-2.0.6.dist-info}/METADATA +1 -1
  2. clear_skies-2.0.6.dist-info/RECORD +251 -0
  3. clearskies/__init__.py +61 -0
  4. clearskies/action.py +7 -0
  5. clearskies/authentication/__init__.py +15 -0
  6. clearskies/authentication/authentication.py +46 -0
  7. clearskies/authentication/authorization.py +16 -0
  8. clearskies/authentication/authorization_pass_through.py +20 -0
  9. clearskies/authentication/jwks.py +163 -0
  10. clearskies/authentication/public.py +5 -0
  11. clearskies/authentication/secret_bearer.py +553 -0
  12. clearskies/autodoc/__init__.py +8 -0
  13. clearskies/autodoc/formats/__init__.py +5 -0
  14. clearskies/autodoc/formats/oai3_json/__init__.py +7 -0
  15. clearskies/autodoc/formats/oai3_json/oai3_json.py +87 -0
  16. clearskies/autodoc/formats/oai3_json/oai3_schema_resolver.py +15 -0
  17. clearskies/autodoc/formats/oai3_json/parameter.py +35 -0
  18. clearskies/autodoc/formats/oai3_json/request.py +68 -0
  19. clearskies/autodoc/formats/oai3_json/response.py +28 -0
  20. clearskies/autodoc/formats/oai3_json/schema/__init__.py +11 -0
  21. clearskies/autodoc/formats/oai3_json/schema/array.py +9 -0
  22. clearskies/autodoc/formats/oai3_json/schema/default.py +13 -0
  23. clearskies/autodoc/formats/oai3_json/schema/enum.py +7 -0
  24. clearskies/autodoc/formats/oai3_json/schema/object.py +35 -0
  25. clearskies/autodoc/formats/oai3_json/test.json +1985 -0
  26. clearskies/autodoc/py.typed +0 -0
  27. clearskies/autodoc/request/__init__.py +15 -0
  28. clearskies/autodoc/request/header.py +6 -0
  29. clearskies/autodoc/request/json_body.py +6 -0
  30. clearskies/autodoc/request/parameter.py +8 -0
  31. clearskies/autodoc/request/request.py +47 -0
  32. clearskies/autodoc/request/url_parameter.py +6 -0
  33. clearskies/autodoc/request/url_path.py +6 -0
  34. clearskies/autodoc/response/__init__.py +5 -0
  35. clearskies/autodoc/response/response.py +9 -0
  36. clearskies/autodoc/schema/__init__.py +31 -0
  37. clearskies/autodoc/schema/array.py +10 -0
  38. clearskies/autodoc/schema/base64.py +8 -0
  39. clearskies/autodoc/schema/boolean.py +5 -0
  40. clearskies/autodoc/schema/date.py +5 -0
  41. clearskies/autodoc/schema/datetime.py +5 -0
  42. clearskies/autodoc/schema/double.py +5 -0
  43. clearskies/autodoc/schema/enum.py +17 -0
  44. clearskies/autodoc/schema/integer.py +6 -0
  45. clearskies/autodoc/schema/long.py +5 -0
  46. clearskies/autodoc/schema/number.py +6 -0
  47. clearskies/autodoc/schema/object.py +13 -0
  48. clearskies/autodoc/schema/password.py +5 -0
  49. clearskies/autodoc/schema/schema.py +11 -0
  50. clearskies/autodoc/schema/string.py +5 -0
  51. clearskies/backends/__init__.py +65 -0
  52. clearskies/backends/api_backend.py +1178 -0
  53. clearskies/backends/backend.py +136 -0
  54. clearskies/backends/cursor_backend.py +335 -0
  55. clearskies/backends/memory_backend.py +797 -0
  56. clearskies/backends/secrets_backend.py +106 -0
  57. clearskies/column.py +1233 -0
  58. clearskies/columns/__init__.py +71 -0
  59. clearskies/columns/audit.py +206 -0
  60. clearskies/columns/belongs_to_id.py +483 -0
  61. clearskies/columns/belongs_to_model.py +132 -0
  62. clearskies/columns/belongs_to_self.py +105 -0
  63. clearskies/columns/boolean.py +113 -0
  64. clearskies/columns/category_tree.py +275 -0
  65. clearskies/columns/category_tree_ancestors.py +51 -0
  66. clearskies/columns/category_tree_children.py +127 -0
  67. clearskies/columns/category_tree_descendants.py +48 -0
  68. clearskies/columns/created.py +95 -0
  69. clearskies/columns/created_by_authorization_data.py +116 -0
  70. clearskies/columns/created_by_header.py +99 -0
  71. clearskies/columns/created_by_ip.py +92 -0
  72. clearskies/columns/created_by_routing_data.py +97 -0
  73. clearskies/columns/created_by_user_agent.py +92 -0
  74. clearskies/columns/date.py +234 -0
  75. clearskies/columns/datetime.py +282 -0
  76. clearskies/columns/email.py +76 -0
  77. clearskies/columns/float.py +153 -0
  78. clearskies/columns/has_many.py +505 -0
  79. clearskies/columns/has_many_self.py +56 -0
  80. clearskies/columns/has_one.py +14 -0
  81. clearskies/columns/integer.py +160 -0
  82. clearskies/columns/json.py +128 -0
  83. clearskies/columns/many_to_many_ids.py +337 -0
  84. clearskies/columns/many_to_many_ids_with_data.py +274 -0
  85. clearskies/columns/many_to_many_models.py +158 -0
  86. clearskies/columns/many_to_many_pivots.py +134 -0
  87. clearskies/columns/phone.py +159 -0
  88. clearskies/columns/select.py +92 -0
  89. clearskies/columns/string.py +102 -0
  90. clearskies/columns/timestamp.py +164 -0
  91. clearskies/columns/updated.py +110 -0
  92. clearskies/columns/uuid.py +86 -0
  93. clearskies/configs/README.md +105 -0
  94. clearskies/configs/__init__.py +162 -0
  95. clearskies/configs/actions.py +43 -0
  96. clearskies/configs/any.py +13 -0
  97. clearskies/configs/any_dict.py +22 -0
  98. clearskies/configs/any_dict_or_callable.py +23 -0
  99. clearskies/configs/authentication.py +23 -0
  100. clearskies/configs/authorization.py +23 -0
  101. clearskies/configs/boolean.py +16 -0
  102. clearskies/configs/boolean_or_callable.py +18 -0
  103. clearskies/configs/callable_config.py +18 -0
  104. clearskies/configs/columns.py +34 -0
  105. clearskies/configs/conditions.py +30 -0
  106. clearskies/configs/config.py +24 -0
  107. clearskies/configs/datetime.py +18 -0
  108. clearskies/configs/datetime_or_callable.py +19 -0
  109. clearskies/configs/endpoint.py +23 -0
  110. clearskies/configs/endpoint_list.py +29 -0
  111. clearskies/configs/float.py +16 -0
  112. clearskies/configs/float_or_callable.py +18 -0
  113. clearskies/configs/integer.py +16 -0
  114. clearskies/configs/integer_or_callable.py +18 -0
  115. clearskies/configs/joins.py +30 -0
  116. clearskies/configs/list_any_dict.py +30 -0
  117. clearskies/configs/list_any_dict_or_callable.py +31 -0
  118. clearskies/configs/model_class.py +35 -0
  119. clearskies/configs/model_column.py +65 -0
  120. clearskies/configs/model_columns.py +56 -0
  121. clearskies/configs/model_destination_name.py +25 -0
  122. clearskies/configs/model_to_id_column.py +43 -0
  123. clearskies/configs/readable_model_column.py +9 -0
  124. clearskies/configs/readable_model_columns.py +9 -0
  125. clearskies/configs/schema.py +23 -0
  126. clearskies/configs/searchable_model_columns.py +9 -0
  127. clearskies/configs/security_headers.py +39 -0
  128. clearskies/configs/select.py +26 -0
  129. clearskies/configs/select_list.py +47 -0
  130. clearskies/configs/string.py +29 -0
  131. clearskies/configs/string_dict.py +32 -0
  132. clearskies/configs/string_list.py +32 -0
  133. clearskies/configs/string_list_or_callable.py +35 -0
  134. clearskies/configs/string_or_callable.py +18 -0
  135. clearskies/configs/timedelta.py +18 -0
  136. clearskies/configs/timezone.py +18 -0
  137. clearskies/configs/url.py +23 -0
  138. clearskies/configs/validators.py +45 -0
  139. clearskies/configs/writeable_model_column.py +9 -0
  140. clearskies/configs/writeable_model_columns.py +9 -0
  141. clearskies/configurable.py +76 -0
  142. clearskies/contexts/__init__.py +11 -0
  143. clearskies/contexts/cli.py +117 -0
  144. clearskies/contexts/context.py +98 -0
  145. clearskies/contexts/wsgi.py +76 -0
  146. clearskies/contexts/wsgi_ref.py +82 -0
  147. clearskies/decorators.py +33 -0
  148. clearskies/di/__init__.py +14 -0
  149. clearskies/di/additional_config.py +130 -0
  150. clearskies/di/additional_config_auto_import.py +17 -0
  151. clearskies/di/di.py +973 -0
  152. clearskies/di/inject/__init__.py +23 -0
  153. clearskies/di/inject/by_class.py +21 -0
  154. clearskies/di/inject/by_name.py +18 -0
  155. clearskies/di/inject/di.py +13 -0
  156. clearskies/di/inject/environment.py +14 -0
  157. clearskies/di/inject/input_output.py +20 -0
  158. clearskies/di/inject/now.py +13 -0
  159. clearskies/di/inject/requests.py +13 -0
  160. clearskies/di/inject/secrets.py +14 -0
  161. clearskies/di/inject/utcnow.py +13 -0
  162. clearskies/di/inject/uuid.py +15 -0
  163. clearskies/di/injectable.py +29 -0
  164. clearskies/di/injectable_properties.py +131 -0
  165. clearskies/di/test_module/__init__.py +6 -0
  166. clearskies/di/test_module/another_module/__init__.py +2 -0
  167. clearskies/di/test_module/module_class.py +5 -0
  168. clearskies/end.py +183 -0
  169. clearskies/endpoint.py +1314 -0
  170. clearskies/endpoint_group.py +336 -0
  171. clearskies/endpoints/__init__.py +25 -0
  172. clearskies/endpoints/advanced_search.py +526 -0
  173. clearskies/endpoints/callable.py +388 -0
  174. clearskies/endpoints/create.py +205 -0
  175. clearskies/endpoints/delete.py +139 -0
  176. clearskies/endpoints/get.py +271 -0
  177. clearskies/endpoints/health_check.py +183 -0
  178. clearskies/endpoints/list.py +574 -0
  179. clearskies/endpoints/restful_api.py +427 -0
  180. clearskies/endpoints/schema.py +189 -0
  181. clearskies/endpoints/simple_search.py +286 -0
  182. clearskies/endpoints/update.py +193 -0
  183. clearskies/environment.py +104 -0
  184. clearskies/exceptions/__init__.py +19 -0
  185. clearskies/exceptions/authentication.py +2 -0
  186. clearskies/exceptions/authorization.py +2 -0
  187. clearskies/exceptions/client_error.py +2 -0
  188. clearskies/exceptions/input_errors.py +4 -0
  189. clearskies/exceptions/missing_dependency.py +2 -0
  190. clearskies/exceptions/moved_permanently.py +3 -0
  191. clearskies/exceptions/moved_temporarily.py +3 -0
  192. clearskies/exceptions/not_found.py +2 -0
  193. clearskies/functional/__init__.py +7 -0
  194. clearskies/functional/routing.py +92 -0
  195. clearskies/functional/string.py +112 -0
  196. clearskies/functional/validations.py +76 -0
  197. clearskies/input_outputs/__init__.py +13 -0
  198. clearskies/input_outputs/cli.py +171 -0
  199. clearskies/input_outputs/exceptions/__init__.py +2 -0
  200. clearskies/input_outputs/exceptions/cli_input_error.py +2 -0
  201. clearskies/input_outputs/exceptions/cli_not_found.py +2 -0
  202. clearskies/input_outputs/headers.py +45 -0
  203. clearskies/input_outputs/input_output.py +138 -0
  204. clearskies/input_outputs/programmatic.py +69 -0
  205. clearskies/input_outputs/py.typed +0 -0
  206. clearskies/input_outputs/wsgi.py +77 -0
  207. clearskies/model.py +1922 -0
  208. clearskies/py.typed +0 -0
  209. clearskies/query/__init__.py +12 -0
  210. clearskies/query/condition.py +223 -0
  211. clearskies/query/join.py +136 -0
  212. clearskies/query/query.py +196 -0
  213. clearskies/query/sort.py +27 -0
  214. clearskies/schema.py +82 -0
  215. clearskies/secrets/__init__.py +6 -0
  216. clearskies/secrets/additional_configs/__init__.py +32 -0
  217. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +61 -0
  218. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +160 -0
  219. clearskies/secrets/akeyless.py +182 -0
  220. clearskies/secrets/exceptions/__init__.py +1 -0
  221. clearskies/secrets/exceptions/not_found.py +2 -0
  222. clearskies/secrets/secrets.py +38 -0
  223. clearskies/security_header.py +15 -0
  224. clearskies/security_headers/__init__.py +11 -0
  225. clearskies/security_headers/cache_control.py +67 -0
  226. clearskies/security_headers/cors.py +50 -0
  227. clearskies/security_headers/csp.py +94 -0
  228. clearskies/security_headers/hsts.py +22 -0
  229. clearskies/security_headers/x_content_type_options.py +0 -0
  230. clearskies/security_headers/x_frame_options.py +0 -0
  231. clearskies/test_base.py +8 -0
  232. clearskies/typing.py +11 -0
  233. clearskies/validator.py +37 -0
  234. clearskies/validators/__init__.py +33 -0
  235. clearskies/validators/after_column.py +62 -0
  236. clearskies/validators/before_column.py +13 -0
  237. clearskies/validators/in_the_future.py +32 -0
  238. clearskies/validators/in_the_future_at_least.py +11 -0
  239. clearskies/validators/in_the_future_at_most.py +10 -0
  240. clearskies/validators/in_the_past.py +32 -0
  241. clearskies/validators/in_the_past_at_least.py +10 -0
  242. clearskies/validators/in_the_past_at_most.py +10 -0
  243. clearskies/validators/maximum_length.py +26 -0
  244. clearskies/validators/maximum_value.py +29 -0
  245. clearskies/validators/minimum_length.py +26 -0
  246. clearskies/validators/minimum_value.py +29 -0
  247. clearskies/validators/required.py +34 -0
  248. clearskies/validators/timedelta.py +59 -0
  249. clearskies/validators/unique.py +30 -0
  250. clear_skies-2.0.5.dist-info/RECORD +0 -4
  251. {clear_skies-2.0.5.dist-info → clear_skies-2.0.6.dist-info}/WHEEL +0 -0
  252. {clear_skies-2.0.5.dist-info → clear_skies-2.0.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Callable, Self, overload
4
+
5
+ import clearskies.decorators
6
+ import clearskies.typing
7
+ from clearskies import configs
8
+ from clearskies.autodoc.schema import Number as AutoDocNumber
9
+ from clearskies.autodoc.schema import Schema as AutoDocSchema
10
+ from clearskies.column import Column
11
+ from clearskies.query import Condition
12
+
13
+ if TYPE_CHECKING:
14
+ from clearskies import Model
15
+
16
+
17
+ class Float(Column):
18
+ """
19
+ A column that stores a float.
20
+
21
+ ```python
22
+ import clearskies
23
+
24
+
25
+ class MyModel(clearskies.Model):
26
+ backend = clearskies.backends.MemoryBackend()
27
+ id_column_name = "id"
28
+
29
+ id = clearskies.columns.Uuid()
30
+ score = clearskies.columns.Float()
31
+
32
+
33
+ wsgi = clearskies.contexts.WsgiRef(
34
+ clearskies.endpoints.Create(
35
+ MyModel,
36
+ writeable_column_names=["score"],
37
+ readable_column_names=["id", "score"],
38
+ ),
39
+ classes=[MyModel],
40
+ )
41
+ wsgi()
42
+ ```
43
+
44
+ and when invoked:
45
+
46
+ ```bash
47
+ $ curl 'http://localhost:8080' -d '{"score":15.2}' | jq
48
+ {
49
+ "status": "success",
50
+ "error": "",
51
+ "data": {
52
+ "id": "7b5658a9-7573-4676-bf18-64ddc90ad87d",
53
+ "score": 15.2
54
+ },
55
+ "pagination": {},
56
+ "input_errors": {}
57
+ }
58
+
59
+ $ curl 'http://localhost:8080' -d '{"score":"15.2"}' | jq
60
+ {
61
+ "status": "input_errors",
62
+ "error": "",
63
+ "data": [],
64
+ "pagination": {},
65
+ "input_errors": {
66
+ "score": "value should be an integer or float"
67
+ }
68
+ }
69
+ ```
70
+ """
71
+
72
+ default = configs.Float() # type: ignore
73
+ setable = configs.FloatOrCallable(default=None) # type: ignore
74
+ _allowed_search_operators = ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null"]
75
+ auto_doc_class: type[AutoDocSchema] = AutoDocNumber
76
+ _descriptor_config_map = None
77
+
78
+ @clearskies.decorators.parameters_to_properties
79
+ def __init__(
80
+ self,
81
+ default: float | None = None,
82
+ setable: float | Callable[..., float] | None = None,
83
+ is_readable: bool = True,
84
+ is_writeable: bool = True,
85
+ is_searchable: bool = True,
86
+ is_temporary: bool = False,
87
+ validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
88
+ on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
89
+ on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
90
+ on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
91
+ created_by_source_type: str = "",
92
+ created_by_source_key: str = "",
93
+ created_by_source_strict: bool = True,
94
+ ):
95
+ pass
96
+
97
+ @overload
98
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
99
+ pass
100
+
101
+ @overload
102
+ def __get__(self, instance: Model, cls: type[Model]) -> float:
103
+ pass
104
+
105
+ def __get__(self, instance, cls):
106
+ return super().__get__(instance, cls)
107
+
108
+ def __set__(self, instance, value: float) -> None:
109
+ # this makes sure we're initialized
110
+ if "name" not in self._config: # type: ignore
111
+ instance.get_columns()
112
+
113
+ instance._next_data[self.name] = float(value)
114
+
115
+ def from_backend(self, value) -> float:
116
+ return float(value)
117
+
118
+ def to_backend(self, data):
119
+ if self.name not in data or data[self.name] is None:
120
+ return data
121
+
122
+ return {**data, self.name: float(data[self.name])}
123
+
124
+ def equals(self, value: float) -> Condition:
125
+ return super().equals(value)
126
+
127
+ def spaceship(self, value: float) -> Condition:
128
+ return super().spaceship(value)
129
+
130
+ def not_equals(self, value: float) -> Condition:
131
+ return super().not_equals(value)
132
+
133
+ def less_than_equals(self, value: float) -> Condition:
134
+ return super().less_than_equals(value)
135
+
136
+ def greater_than_equals(self, value: float) -> Condition:
137
+ return super().greater_than_equals(value)
138
+
139
+ def less_than(self, value: float) -> Condition:
140
+ return super().less_than(value)
141
+
142
+ def greater_than(self, value: float) -> Condition:
143
+ return super().greater_than(value)
144
+
145
+ def is_in(self, values: list[float]) -> Condition:
146
+ return super().is_in(values)
147
+
148
+ def input_error_for_value(self, value, operator=None):
149
+ return (
150
+ "value should be an integer or float"
151
+ if (type(value) != int and type(value) != float and value is not None)
152
+ else ""
153
+ )
@@ -0,0 +1,505 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Self, overload
4
+
5
+ import clearskies.decorators
6
+ import clearskies.typing
7
+ from clearskies import configs
8
+ from clearskies.autodoc.schema import Array as AutoDocArray
9
+ from clearskies.autodoc.schema import Object as AutoDocObject
10
+ from clearskies.autodoc.schema import Schema as AutoDocSchema
11
+ from clearskies.column import Column
12
+ from clearskies.di.inject import InputOutput
13
+ from clearskies.functional import string, validations
14
+
15
+ if TYPE_CHECKING:
16
+ from clearskies import Column, Model
17
+
18
+
19
+ class HasMany(Column):
20
+ """
21
+ A column to manage a "has many" relationship.
22
+
23
+ In order to manage a has-many relationship, the child model needs a column that stores the
24
+ id of the parent record it belongs to. Also remember that the reverse of a has-many relationship
25
+ is a belongs-to relationship: the parent has many children, the child belongs to a parent.
26
+
27
+ There's an automatic standard where the name of the column in thie child table that stores the
28
+ parent id is made by converting the parent model class name into snake case and then appending
29
+ `_id`. For instance, if the parent model is called the `DooHicky` class, the child model is
30
+ expected to have a column named `doo_hicky_id`. If you use a different column name for the
31
+ id in your child model, then just update the `foreign_column_name` property on the `HasMany`
32
+ column accordingly.
33
+
34
+ See the BelongsToId class for additional background and directions on avoiding circular dependency trees.
35
+
36
+ ```python
37
+ import clearskies
38
+
39
+
40
+ class Product(clearskies.Model):
41
+ id_column_name = "id"
42
+ backend = clearskies.backends.MemoryBackend()
43
+
44
+ id = clearskies.columns.Uuid()
45
+ name = clearskies.columns.String()
46
+ category_id = clearskies.columns.String()
47
+
48
+
49
+ class Category(clearskies.Model):
50
+ id_column_name = "id"
51
+ backend = clearskies.backends.MemoryBackend()
52
+
53
+ id = clearskies.columns.Uuid()
54
+ name = clearskies.columns.String()
55
+ products = clearskies.columns.HasMany(Product)
56
+
57
+
58
+ def test_has_many(products: Product, categories: Category):
59
+ toys = categories.create({"name": "Toys"})
60
+ auto = categories.create({"name": "Auto"})
61
+
62
+ # create some toys
63
+ ball = products.create({"name": "Ball", "category_id": toys.id})
64
+ fidget_spinner = products.create({"name": "Fidget Spinner", "category_id": toys.id})
65
+ crayon = products.create({"name": "Crayon", "category_id": toys.id})
66
+
67
+ # the HasMany column is an interable of matching records
68
+ toy_names = [product.name for product in toys.products]
69
+
70
+ # it specifically returns a models object so you can do more filtering/transformations
71
+ return toys.products.sort_by("name", "asc")
72
+
73
+
74
+ cli = clearskies.contexts.Cli(
75
+ clearskies.endpoints.Callable(
76
+ test_has_many,
77
+ model_class=Product,
78
+ readable_column_names=["id", "name"],
79
+ ),
80
+ classes=[Category, Product],
81
+ )
82
+
83
+ if __name__ == "__main__":
84
+ cli()
85
+ ```
86
+
87
+ And if you execute this it will return:
88
+
89
+ ```json
90
+ {
91
+ "status": "success",
92
+ "error": "",
93
+ "data": [
94
+ {"id": "edc68e8d-7fc8-45ce-98f0-9c6f883e4e7f", "name": "Ball"},
95
+ {"id": "b51a0de5-c784-4e0c-880c-56e5bf731dfd", "name": "Crayon"},
96
+ {"id": "06cec3af-d042-4d6b-a99c-b4a0072f188d", "name": "Fidget Spinner"},
97
+ ],
98
+ "pagination": {},
99
+ "input_errors": {},
100
+ }
101
+ ```
102
+ """
103
+
104
+ """
105
+ HasMany columns are not currently writeable.
106
+ """
107
+ is_writeable = configs.Boolean(default=False)
108
+ is_searchable = configs.Boolean(default=False)
109
+ _descriptor_config_map = None
110
+
111
+ """ The model class for the child table we keep our "many" records in. """
112
+ child_model_class = configs.ModelClass(required=True)
113
+
114
+ """
115
+ The name of the column in the child table that connects it back to the parent.
116
+
117
+ By default this is populated by converting the model class name from TitleCase to snake_case and appending _id.
118
+ So, if the model class is called `ProductCategory`, this becomes `product_category_id`. This MUST correspond to
119
+ the actual name of a column in the child table. This is used so that the parent can find its child records.
120
+
121
+ Example:
122
+
123
+ ```python
124
+ import clearskies
125
+
126
+ class Product(clearskies.Model):
127
+ id_column_name = "id"
128
+ backend = clearskies.backends.MemoryBackend()
129
+
130
+ id = clearskies.columns.Uuid()
131
+ name = clearskies.columns.String()
132
+ my_parent_category_id = clearskies.columns.String()
133
+
134
+ class Category(clearskies.Model):
135
+ id_column_name = "id"
136
+ backend = clearskies.backends.MemoryBackend()
137
+
138
+ id = clearskies.columns.Uuid()
139
+ name = clearskies.columns.String()
140
+ products = clearskies.columns.HasMany(Product, foreign_column_name="my_parent_category_id")
141
+
142
+ def test_has_many(products: Product, categories: Category):
143
+ toys = categories.create({"name": "Toys"})
144
+
145
+ fidget_spinner = products.create({"name": "Fidget Spinner", "my_parent_category_id": toys.id})
146
+ crayon = products.create({"name": "Crayon", "my_parent_category_id": toys.id})
147
+ ball = products.create({"name": "Ball", "my_parent_category_id": toys.id})
148
+
149
+ return toys.products.sort_by("name", "asc")
150
+
151
+ cli = clearskies.contexts.Cli(
152
+ clearskies.endpoints.Callable(
153
+ test_has_many,
154
+ model_class=Product,
155
+ readable_column_names=["id", "name"],
156
+ ),
157
+ classes=[Category, Product],
158
+ )
159
+
160
+ if __name__ == "__main__":
161
+ cli()
162
+ ```
163
+
164
+ Compare to the first example for the HasMany class. In that case, the column in the product model which
165
+ contained the category id was `category_id`, and the `products` column didn't have to specify the
166
+ `foreign_column_name` (since the column name followed the naming rule). As a result, `category.products`
167
+ was able to find all children of a given category. In this example, the name of the column in the product
168
+ model that contains the category id was changed to `my_parent_category_id`. Since this no longer matches
169
+ the naming convention, we had to specify `foreign_column_name="my_parent_category_id"` in `Category.products`,
170
+ in order for the `HasMany` column to find the children. Therefore, when invoked it returns the same thing:
171
+
172
+ ```json
173
+ {
174
+ "status": "success",
175
+ "error": "",
176
+ "data": [
177
+ {
178
+ "id": "3cdd06e0-b226-4a4a-962d-e8c5acc759ac",
179
+ "name": "Ball"
180
+ },
181
+ {
182
+ "id": "debc7968-976a-49cd-902c-d359a8abd032",
183
+ "name": "Crayon"
184
+ },
185
+ {
186
+ "id": "0afcd314-cdfc-4a27-ac6e-061b74ee5bf9",
187
+ "name": "Fidget Spinner"
188
+ }
189
+ ],
190
+ "pagination": {},
191
+ "input_errors": {}
192
+ }
193
+ ```
194
+ """
195
+ foreign_column_name = configs.ModelToIdColumn()
196
+
197
+ """
198
+ Columns from the child table that should be included when converting this column to JSON.
199
+
200
+ You can tell an endpoint to include a `HasMany` column in the response. If you do this, the columns
201
+ from the child class that are included in the JSON response are determined by `readable_child_column_names`.
202
+ Example:
203
+
204
+ ```python
205
+ import clearskies
206
+
207
+ class Product(clearskies.Model):
208
+ id_column_name = "id"
209
+ backend = clearskies.backends.MemoryBackend()
210
+
211
+ id = clearskies.columns.Uuid()
212
+ name = clearskies.columns.String()
213
+ category_id = clearskies.columns.String()
214
+
215
+ class Category(clearskies.Model):
216
+ id_column_name = "id"
217
+ backend = clearskies.backends.MemoryBackend()
218
+
219
+ id = clearskies.columns.Uuid()
220
+ name = clearskies.columns.String()
221
+ products = clearskies.columns.HasMany(Product, readable_child_column_names=["id", "name"])
222
+
223
+ def test_has_many(products: Product, categories: Category):
224
+ toys = categories.create({"name": "Toys"})
225
+
226
+ fidget_spinner = products.create({"name": "Fidget Spinner", "category_id": toys.id})
227
+ ball = products.create({"name": "Ball", "category_id": toys.id})
228
+ crayon = products.create({"name": "Crayon", "category_id": toys.id})
229
+
230
+ return toys
231
+
232
+ cli = clearskies.contexts.Cli(
233
+ clearskies.endpoints.Callable(
234
+ test_has_many,
235
+ model_class=Category,
236
+ readable_column_names=["id", "name", "products"],
237
+ ),
238
+ classes=[Category, Product],
239
+ )
240
+
241
+ if __name__ == "__main__":
242
+ cli()
243
+ ```
244
+
245
+ In this example we're no longer returning a list of products directly. Instead, we're returning a query
246
+ on the categories nodel and asking the endpoint to also unpack their products. We set `readable_child_column_names`
247
+ to `["id", "name"]` for `Category.products`, so when the endpoint unpacks the products, it includes those columns:
248
+
249
+ ```json
250
+ {
251
+ "status": "success",
252
+ "error": "",
253
+ "data": [
254
+ {
255
+ "id": "c8a71c81-fa0e-427d-a166-159f3c9de72b",
256
+ "name": "Office Supplies",
257
+ "products": [
258
+ {
259
+ "id": "6d24ffa2-6e1b-4ce9-87ff-daf2ba237c92",
260
+ "name": "Stapler"
261
+ },
262
+ {
263
+ "id": "3a42cd7d-6804-465e-9fb1-055fafa7fc62",
264
+ "name": "Chair"
265
+ }
266
+ ]
267
+ },
268
+ {
269
+ "id": "5a790950-858b-411a-bf5c-1338a28e73d0",
270
+ "name": "Toys",
271
+ "products": [
272
+ {
273
+ "id": "d4022224-cc22-49c2-8da9-7a8f9fa7e976",
274
+ "name": "Fidget Spinner"
275
+ },
276
+ {
277
+ "id": "415fa48e-984a-4703-b6e6-f88f741403c8",
278
+ "name": "Ball"
279
+ },
280
+ {
281
+ "id": "58328363-5180-441c-b1a8-1b92e12a8f08",
282
+ "name": "Crayon"
283
+ }
284
+ ]
285
+ }
286
+ ],
287
+ "pagination": {},
288
+ "input_errors": {}
289
+ }
290
+
291
+ ```
292
+
293
+ """
294
+ readable_child_column_names = configs.ReadableModelColumns("child_model_class")
295
+
296
+ """
297
+ Additional conditions to add to searches on the child table.
298
+
299
+ ```python
300
+ import clearskies
301
+
302
+ class Order(clearskies.Model):
303
+ id_column_name = "id"
304
+ backend = clearskies.backends.MemoryBackend()
305
+
306
+ id = clearskies.columns.Uuid()
307
+ total = clearskies.columns.Float()
308
+ status = clearskies.columns.Select(["Open", "In Progress", "Closed"])
309
+ user_id = clearskies.columns.String()
310
+
311
+ class User(clearskies.Model):
312
+ id_column_name = "id"
313
+ backend = clearskies.backends.MemoryBackend()
314
+
315
+ id = clearskies.columns.Uuid()
316
+ name = clearskies.columns.String()
317
+ orders = clearskies.columns.HasMany(Order, readable_child_column_names=["id", "status"])
318
+ large_open_orders = clearskies.columns.HasMany(
319
+ Order,
320
+ readable_child_column_names=["id", "status"],
321
+ where=[Order.status.equals("Open"), "total>100"],
322
+ )
323
+
324
+ def test_has_many(users: User, orders: Order):
325
+ user = users.create({"name": "Bob"})
326
+
327
+ order_1 = orders.create({"status": "Open", "total": 25.50, "user_id": user.id})
328
+ order_2 = orders.create({"status": "Closed", "total": 35.50, "user_id": user.id})
329
+ order_3 = orders.create({"status": "Open", "total": 125, "user_id": user.id})
330
+ order_4 = orders.create({"status": "In Progress", "total": 25.50, "user_id": user.id})
331
+
332
+ return user.large_open_orders
333
+
334
+ cli = clearskies.contexts.Cli(
335
+ clearskies.endpoints.Callable(
336
+ test_has_many,
337
+ model_class=Order,
338
+ readable_column_names=["id", "total", "status"],
339
+ return_records=True,
340
+ ),
341
+ classes=[Order, User],
342
+ )
343
+
344
+ if __name__ == "__main__":
345
+ cli()
346
+ ```
347
+
348
+ The above example shows two different ways of adding conditions. Note that `where` can be either a list or a single
349
+ condition. If you invoked this you would get:
350
+
351
+ ```json
352
+ {
353
+ "status": "success",
354
+ "error": "",
355
+ "data": [
356
+ {
357
+ "id": "6ad99935-ac9a-40ef-a1b2-f34538cc6529",
358
+ "total": 125.0,
359
+ "status": "Open"
360
+ }
361
+ ],
362
+ "pagination": {},
363
+ "input_errors": {}
364
+ }
365
+ ```
366
+
367
+ Finally, an individual condition can also be a callable that accepts the child model class, adds any desired conditions,
368
+ and then returns the modified model class. Like usual, this callable can request any defined depenency. So, for
369
+ instance, the following column definition is equivalent to the example above:
370
+
371
+ ```python
372
+ class User(clearskies.Model):
373
+ # removing unchanged part for brevity
374
+ large_open_orders = clearskies.columns.HasMany(
375
+ Order,
376
+ readable_child_column_names=["id", "status"],
377
+ where=lambda model: model.where("status=Open").where("total>100"),
378
+ )
379
+ ```
380
+ """
381
+ where = configs.Conditions()
382
+
383
+ input_output = InputOutput()
384
+
385
+ @clearskies.decorators.parameters_to_properties
386
+ def __init__(
387
+ self,
388
+ child_model_class,
389
+ foreign_column_name: str | None = None,
390
+ readable_child_column_names: list[str] = [],
391
+ where: clearskies.typing.condition | list[clearskies.typing.condition] = [],
392
+ is_readable: bool = True,
393
+ on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
394
+ on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
395
+ on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
396
+ ):
397
+ pass
398
+
399
+ def finalize_configuration(self, model_class, name) -> None:
400
+ """
401
+ Finalize and check the configuration.
402
+
403
+ This is an external trigger called by the model class when the model class is ready.
404
+ The reason it exists here instead of in the constructor is because some columns are tightly
405
+ connected to the model class, and can't validate configuration until they know what the model is.
406
+ Therefore, we need the model involved, and the only way for a property to know what class it is
407
+ in is if the parent class checks in (which is what happens here).
408
+ """
409
+ # this is where we auto-calculate the expected name of our id column in the child model.
410
+ # we can't do it until now because it comes from the model class we are connected to, and
411
+ # we only just get it.
412
+ foreign_column_name_config = self._get_config_object("foreign_column_name")
413
+ foreign_column_name_config.set_model_class(self.child_model_class)
414
+ has_value = False
415
+ try:
416
+ has_value = bool(self.foreign_column_name)
417
+ except KeyError:
418
+ pass
419
+
420
+ if not has_value:
421
+ self.foreign_column_name = string.camel_case_to_snake_case(model_class.__name__) + "_id"
422
+
423
+ super().finalize_configuration(model_class, name)
424
+
425
+ @property
426
+ def child_columns(self) -> dict[str, Column]:
427
+ return self.child_model_class.get_columns()
428
+
429
+ @property
430
+ def child_model(self) -> Model:
431
+ return self.di.build(self.child_model_class, cache=True)
432
+
433
+ @overload
434
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
435
+ pass
436
+
437
+ @overload
438
+ def __get__(self, instance: Model, cls: type[Model]) -> Model:
439
+ pass
440
+
441
+ def __get__(self, model, cls):
442
+ if model is None:
443
+ self.model_class = cls
444
+ return self # type: ignore
445
+
446
+ # this makes sure we're initialized
447
+ if "name" not in self._config: # type: ignore
448
+ model.get_columns()
449
+
450
+ foreign_column_name = self.foreign_column_name
451
+ model_id = getattr(model, model.id_column_name)
452
+ children = self.child_model.where(f"{foreign_column_name}={model_id}")
453
+
454
+ if not self.where:
455
+ return children
456
+
457
+ for index, where in enumerate(self.where):
458
+ if callable(where):
459
+ children = self.di.call_function(where, model=children, **self.input_output.get_context_for_callables())
460
+ if not validations.is_model(children):
461
+ raise ValueError(
462
+ 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"
463
+ )
464
+ else:
465
+ children = children.where(where)
466
+ return children
467
+
468
+ def __set__(self, model: Model, value: Model) -> None:
469
+ raise ValueError(
470
+ 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."
471
+ )
472
+
473
+ def to_json(self, model: Model) -> dict[str, Any]:
474
+ children = []
475
+ columns = self.child_columns
476
+ child_id_column_name = self.child_model_class.id_column_name
477
+ json: dict[str, Any] = {}
478
+ for child in getattr(model, self.name):
479
+ json = {
480
+ **json,
481
+ **columns[child_id_column_name].to_json(child),
482
+ }
483
+ for column_name in self.readable_child_column_names:
484
+ json = {
485
+ **json,
486
+ **columns[column_name].to_json(child),
487
+ }
488
+ children.append(json)
489
+ return {self.name: children}
490
+
491
+ def documentation(
492
+ self, name: str | None = None, example: str | None = None, value: str | None = None
493
+ ) -> list[AutoDocSchema]:
494
+ columns = self.child_columns
495
+ child_id_column_name = self.child_model.id_column_name
496
+ child_properties = [columns[child_id_column_name].documentation()]
497
+
498
+ for column_name in self.readable_child_column_names:
499
+ child_properties.extend(columns[column_name].documentation()) # type: ignore
500
+
501
+ child_object = AutoDocObject(
502
+ string.title_case_to_nice(self.child_model_class.__name__),
503
+ child_properties,
504
+ )
505
+ return [AutoDocArray(name if name is not None else self.name, child_object, value=value)]