clear-skies 2.0.4__py3-none-any.whl → 2.0.5__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 (253) hide show
  1. clear_skies-2.0.5.dist-info/METADATA +74 -0
  2. clear_skies-2.0.5.dist-info/RECORD +4 -0
  3. {clear_skies-2.0.4.dist-info → clear_skies-2.0.5.dist-info}/WHEEL +1 -1
  4. clear_skies-2.0.4.dist-info/METADATA +0 -36
  5. clear_skies-2.0.4.dist-info/RECORD +0 -251
  6. clearskies/__init__.py +0 -61
  7. clearskies/action.py +0 -7
  8. clearskies/authentication/__init__.py +0 -15
  9. clearskies/authentication/authentication.py +0 -46
  10. clearskies/authentication/authorization.py +0 -16
  11. clearskies/authentication/authorization_pass_through.py +0 -20
  12. clearskies/authentication/jwks.py +0 -163
  13. clearskies/authentication/public.py +0 -5
  14. clearskies/authentication/secret_bearer.py +0 -553
  15. clearskies/autodoc/__init__.py +0 -8
  16. clearskies/autodoc/formats/__init__.py +0 -5
  17. clearskies/autodoc/formats/oai3_json/__init__.py +0 -7
  18. clearskies/autodoc/formats/oai3_json/oai3_json.py +0 -87
  19. clearskies/autodoc/formats/oai3_json/oai3_schema_resolver.py +0 -15
  20. clearskies/autodoc/formats/oai3_json/parameter.py +0 -35
  21. clearskies/autodoc/formats/oai3_json/request.py +0 -68
  22. clearskies/autodoc/formats/oai3_json/response.py +0 -28
  23. clearskies/autodoc/formats/oai3_json/schema/__init__.py +0 -11
  24. clearskies/autodoc/formats/oai3_json/schema/array.py +0 -9
  25. clearskies/autodoc/formats/oai3_json/schema/default.py +0 -13
  26. clearskies/autodoc/formats/oai3_json/schema/enum.py +0 -7
  27. clearskies/autodoc/formats/oai3_json/schema/object.py +0 -35
  28. clearskies/autodoc/formats/oai3_json/test.json +0 -1985
  29. clearskies/autodoc/py.typed +0 -0
  30. clearskies/autodoc/request/__init__.py +0 -15
  31. clearskies/autodoc/request/header.py +0 -6
  32. clearskies/autodoc/request/json_body.py +0 -6
  33. clearskies/autodoc/request/parameter.py +0 -8
  34. clearskies/autodoc/request/request.py +0 -47
  35. clearskies/autodoc/request/url_parameter.py +0 -6
  36. clearskies/autodoc/request/url_path.py +0 -6
  37. clearskies/autodoc/response/__init__.py +0 -5
  38. clearskies/autodoc/response/response.py +0 -9
  39. clearskies/autodoc/schema/__init__.py +0 -31
  40. clearskies/autodoc/schema/array.py +0 -10
  41. clearskies/autodoc/schema/base64.py +0 -8
  42. clearskies/autodoc/schema/boolean.py +0 -5
  43. clearskies/autodoc/schema/date.py +0 -5
  44. clearskies/autodoc/schema/datetime.py +0 -5
  45. clearskies/autodoc/schema/double.py +0 -5
  46. clearskies/autodoc/schema/enum.py +0 -17
  47. clearskies/autodoc/schema/integer.py +0 -6
  48. clearskies/autodoc/schema/long.py +0 -5
  49. clearskies/autodoc/schema/number.py +0 -6
  50. clearskies/autodoc/schema/object.py +0 -13
  51. clearskies/autodoc/schema/password.py +0 -5
  52. clearskies/autodoc/schema/schema.py +0 -11
  53. clearskies/autodoc/schema/string.py +0 -5
  54. clearskies/backends/__init__.py +0 -65
  55. clearskies/backends/api_backend.py +0 -1178
  56. clearskies/backends/backend.py +0 -136
  57. clearskies/backends/cursor_backend.py +0 -335
  58. clearskies/backends/memory_backend.py +0 -797
  59. clearskies/backends/secrets_backend.py +0 -106
  60. clearskies/column.py +0 -1233
  61. clearskies/columns/__init__.py +0 -71
  62. clearskies/columns/audit.py +0 -206
  63. clearskies/columns/belongs_to_id.py +0 -483
  64. clearskies/columns/belongs_to_model.py +0 -132
  65. clearskies/columns/belongs_to_self.py +0 -105
  66. clearskies/columns/boolean.py +0 -113
  67. clearskies/columns/category_tree.py +0 -275
  68. clearskies/columns/category_tree_ancestors.py +0 -51
  69. clearskies/columns/category_tree_children.py +0 -127
  70. clearskies/columns/category_tree_descendants.py +0 -48
  71. clearskies/columns/created.py +0 -95
  72. clearskies/columns/created_by_authorization_data.py +0 -116
  73. clearskies/columns/created_by_header.py +0 -99
  74. clearskies/columns/created_by_ip.py +0 -92
  75. clearskies/columns/created_by_routing_data.py +0 -97
  76. clearskies/columns/created_by_user_agent.py +0 -92
  77. clearskies/columns/date.py +0 -234
  78. clearskies/columns/datetime.py +0 -282
  79. clearskies/columns/email.py +0 -76
  80. clearskies/columns/float.py +0 -153
  81. clearskies/columns/has_many.py +0 -505
  82. clearskies/columns/has_many_self.py +0 -56
  83. clearskies/columns/has_one.py +0 -14
  84. clearskies/columns/integer.py +0 -160
  85. clearskies/columns/json.py +0 -126
  86. clearskies/columns/many_to_many_ids.py +0 -337
  87. clearskies/columns/many_to_many_ids_with_data.py +0 -274
  88. clearskies/columns/many_to_many_models.py +0 -158
  89. clearskies/columns/many_to_many_pivots.py +0 -134
  90. clearskies/columns/phone.py +0 -159
  91. clearskies/columns/select.py +0 -92
  92. clearskies/columns/string.py +0 -102
  93. clearskies/columns/timestamp.py +0 -164
  94. clearskies/columns/updated.py +0 -110
  95. clearskies/columns/uuid.py +0 -86
  96. clearskies/configs/README.md +0 -105
  97. clearskies/configs/__init__.py +0 -162
  98. clearskies/configs/actions.py +0 -43
  99. clearskies/configs/any.py +0 -13
  100. clearskies/configs/any_dict.py +0 -22
  101. clearskies/configs/any_dict_or_callable.py +0 -23
  102. clearskies/configs/authentication.py +0 -23
  103. clearskies/configs/authorization.py +0 -23
  104. clearskies/configs/boolean.py +0 -16
  105. clearskies/configs/boolean_or_callable.py +0 -18
  106. clearskies/configs/callable_config.py +0 -18
  107. clearskies/configs/columns.py +0 -34
  108. clearskies/configs/conditions.py +0 -30
  109. clearskies/configs/config.py +0 -24
  110. clearskies/configs/datetime.py +0 -18
  111. clearskies/configs/datetime_or_callable.py +0 -19
  112. clearskies/configs/endpoint.py +0 -23
  113. clearskies/configs/endpoint_list.py +0 -28
  114. clearskies/configs/float.py +0 -16
  115. clearskies/configs/float_or_callable.py +0 -18
  116. clearskies/configs/integer.py +0 -16
  117. clearskies/configs/integer_or_callable.py +0 -18
  118. clearskies/configs/joins.py +0 -30
  119. clearskies/configs/list_any_dict.py +0 -30
  120. clearskies/configs/list_any_dict_or_callable.py +0 -31
  121. clearskies/configs/model_class.py +0 -35
  122. clearskies/configs/model_column.py +0 -65
  123. clearskies/configs/model_columns.py +0 -56
  124. clearskies/configs/model_destination_name.py +0 -25
  125. clearskies/configs/model_to_id_column.py +0 -43
  126. clearskies/configs/readable_model_column.py +0 -9
  127. clearskies/configs/readable_model_columns.py +0 -9
  128. clearskies/configs/schema.py +0 -23
  129. clearskies/configs/searchable_model_columns.py +0 -9
  130. clearskies/configs/security_headers.py +0 -39
  131. clearskies/configs/select.py +0 -26
  132. clearskies/configs/select_list.py +0 -47
  133. clearskies/configs/string.py +0 -29
  134. clearskies/configs/string_dict.py +0 -32
  135. clearskies/configs/string_list.py +0 -32
  136. clearskies/configs/string_list_or_callable.py +0 -35
  137. clearskies/configs/string_or_callable.py +0 -18
  138. clearskies/configs/timedelta.py +0 -18
  139. clearskies/configs/timezone.py +0 -18
  140. clearskies/configs/url.py +0 -23
  141. clearskies/configs/validators.py +0 -45
  142. clearskies/configs/writeable_model_column.py +0 -9
  143. clearskies/configs/writeable_model_columns.py +0 -9
  144. clearskies/configurable.py +0 -76
  145. clearskies/contexts/__init__.py +0 -11
  146. clearskies/contexts/cli.py +0 -117
  147. clearskies/contexts/context.py +0 -98
  148. clearskies/contexts/wsgi.py +0 -76
  149. clearskies/contexts/wsgi_ref.py +0 -82
  150. clearskies/decorators.py +0 -33
  151. clearskies/di/__init__.py +0 -14
  152. clearskies/di/additional_config.py +0 -130
  153. clearskies/di/additional_config_auto_import.py +0 -17
  154. clearskies/di/di.py +0 -973
  155. clearskies/di/inject/__init__.py +0 -23
  156. clearskies/di/inject/by_class.py +0 -21
  157. clearskies/di/inject/by_name.py +0 -18
  158. clearskies/di/inject/di.py +0 -13
  159. clearskies/di/inject/environment.py +0 -14
  160. clearskies/di/inject/input_output.py +0 -20
  161. clearskies/di/inject/now.py +0 -13
  162. clearskies/di/inject/requests.py +0 -13
  163. clearskies/di/inject/secrets.py +0 -14
  164. clearskies/di/inject/utcnow.py +0 -13
  165. clearskies/di/inject/uuid.py +0 -15
  166. clearskies/di/injectable.py +0 -29
  167. clearskies/di/injectable_properties.py +0 -131
  168. clearskies/di/test_module/__init__.py +0 -6
  169. clearskies/di/test_module/another_module/__init__.py +0 -2
  170. clearskies/di/test_module/module_class.py +0 -5
  171. clearskies/end.py +0 -183
  172. clearskies/endpoint.py +0 -1314
  173. clearskies/endpoint_group.py +0 -338
  174. clearskies/endpoints/__init__.py +0 -25
  175. clearskies/endpoints/advanced_search.py +0 -526
  176. clearskies/endpoints/callable.py +0 -388
  177. clearskies/endpoints/create.py +0 -205
  178. clearskies/endpoints/delete.py +0 -139
  179. clearskies/endpoints/get.py +0 -271
  180. clearskies/endpoints/health_check.py +0 -183
  181. clearskies/endpoints/list.py +0 -574
  182. clearskies/endpoints/restful_api.py +0 -427
  183. clearskies/endpoints/schema.py +0 -189
  184. clearskies/endpoints/simple_search.py +0 -286
  185. clearskies/endpoints/update.py +0 -193
  186. clearskies/environment.py +0 -104
  187. clearskies/exceptions/__init__.py +0 -19
  188. clearskies/exceptions/authentication.py +0 -2
  189. clearskies/exceptions/authorization.py +0 -2
  190. clearskies/exceptions/client_error.py +0 -2
  191. clearskies/exceptions/input_errors.py +0 -4
  192. clearskies/exceptions/missing_dependency.py +0 -2
  193. clearskies/exceptions/moved_permanently.py +0 -3
  194. clearskies/exceptions/moved_temporarily.py +0 -3
  195. clearskies/exceptions/not_found.py +0 -2
  196. clearskies/functional/__init__.py +0 -7
  197. clearskies/functional/routing.py +0 -92
  198. clearskies/functional/string.py +0 -112
  199. clearskies/functional/validations.py +0 -76
  200. clearskies/input_outputs/__init__.py +0 -13
  201. clearskies/input_outputs/cli.py +0 -171
  202. clearskies/input_outputs/exceptions/__init__.py +0 -2
  203. clearskies/input_outputs/exceptions/cli_input_error.py +0 -2
  204. clearskies/input_outputs/exceptions/cli_not_found.py +0 -2
  205. clearskies/input_outputs/headers.py +0 -45
  206. clearskies/input_outputs/input_output.py +0 -138
  207. clearskies/input_outputs/programmatic.py +0 -69
  208. clearskies/input_outputs/py.typed +0 -0
  209. clearskies/input_outputs/wsgi.py +0 -77
  210. clearskies/model.py +0 -1922
  211. clearskies/py.typed +0 -0
  212. clearskies/query/__init__.py +0 -12
  213. clearskies/query/condition.py +0 -223
  214. clearskies/query/join.py +0 -136
  215. clearskies/query/query.py +0 -196
  216. clearskies/query/sort.py +0 -27
  217. clearskies/schema.py +0 -82
  218. clearskies/secrets/__init__.py +0 -6
  219. clearskies/secrets/additional_configs/__init__.py +0 -32
  220. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +0 -61
  221. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +0 -160
  222. clearskies/secrets/akeyless.py +0 -182
  223. clearskies/secrets/exceptions/__init__.py +0 -1
  224. clearskies/secrets/exceptions/not_found.py +0 -2
  225. clearskies/secrets/secrets.py +0 -38
  226. clearskies/security_header.py +0 -15
  227. clearskies/security_headers/__init__.py +0 -11
  228. clearskies/security_headers/cache_control.py +0 -67
  229. clearskies/security_headers/cors.py +0 -50
  230. clearskies/security_headers/csp.py +0 -94
  231. clearskies/security_headers/hsts.py +0 -22
  232. clearskies/security_headers/x_content_type_options.py +0 -0
  233. clearskies/security_headers/x_frame_options.py +0 -0
  234. clearskies/test_base.py +0 -8
  235. clearskies/typing.py +0 -11
  236. clearskies/validator.py +0 -37
  237. clearskies/validators/__init__.py +0 -33
  238. clearskies/validators/after_column.py +0 -62
  239. clearskies/validators/before_column.py +0 -13
  240. clearskies/validators/in_the_future.py +0 -32
  241. clearskies/validators/in_the_future_at_least.py +0 -11
  242. clearskies/validators/in_the_future_at_most.py +0 -10
  243. clearskies/validators/in_the_past.py +0 -32
  244. clearskies/validators/in_the_past_at_least.py +0 -10
  245. clearskies/validators/in_the_past_at_most.py +0 -10
  246. clearskies/validators/maximum_length.py +0 -26
  247. clearskies/validators/maximum_value.py +0 -29
  248. clearskies/validators/minimum_length.py +0 -26
  249. clearskies/validators/minimum_value.py +0 -29
  250. clearskies/validators/required.py +0 -34
  251. clearskies/validators/timedelta.py +0 -59
  252. clearskies/validators/unique.py +0 -30
  253. {clear_skies-2.0.4.dist-info → clear_skies-2.0.5.dist-info/licenses}/LICENSE +0 -0
@@ -1,56 +0,0 @@
1
- import clearskies.decorators
2
- import clearskies.typing
3
- from clearskies.columns.has_many import HasMany
4
-
5
-
6
- class HasManySelf(HasMany):
7
- """
8
- This is just like the HasMany column, but is used when the model references itself.
9
-
10
- This exists because a model can't refer to itself inside it's own class definition. There are
11
- workarounds, but having this class is usually quicker for the developer.
12
-
13
- The main difference between this and HasMany is that you don't have to provide the child class.
14
- Also, the name of the column that contains the id of the parent becomes `parent_id` by default,
15
- rather than basing it on the name of the model. This is done because, since the model is also
16
- the child, using the name of the model in the name of the column id is often ambiguous.
17
-
18
- See also BelongsToSelf.
19
- """
20
-
21
- _descriptor_config_map = None
22
-
23
- @clearskies.decorators.parameters_to_properties
24
- def __init__(
25
- self,
26
- foreign_column_name: str | None = None,
27
- readable_child_columns: list[str] = [],
28
- where: clearskies.typing.condition | list[clearskies.typing.condition] = [],
29
- is_readable: bool = True,
30
- on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
31
- on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
32
- on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
33
- ):
34
- pass
35
-
36
- def finalize_configuration(self, model_class, name) -> None:
37
- """
38
- Finalize and check the configuration.
39
-
40
- This is an external trigger called by the model class when the model class is ready.
41
- The reason it exists here instead of in the constructor is because some columns are tightly
42
- connected to the model class, and can't validate configuration until they know what the model is.
43
- Therefore, we need the model involved, and the only way for a property to know what class it is
44
- in is if the parent class checks in (which is what happens here).
45
- """
46
- self.child_model_class = model_class
47
- has_value = False
48
- try:
49
- has_value = bool(self.foreign_column_name)
50
- except KeyError:
51
- pass
52
-
53
- if not has_value:
54
- self.foreign_column_name = "parent_id"
55
-
56
- super().finalize_configuration(model_class, name)
@@ -1,14 +0,0 @@
1
- from clearskies.columns.has_many import HasMany
2
-
3
-
4
- class HasOne(HasMany):
5
- """
6
- This operates exactly like the HasMany relationship, except it assumes there is only ever one child.
7
-
8
- The only real difference between this and HasMany is that the HasMany column type will return a list
9
- of models, while this returns the first model.
10
- """
11
-
12
- _descriptor_config_map = None
13
-
14
- pass
@@ -1,160 +0,0 @@
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 Integer as AutoDocInteger
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 Integer(Column):
18
- """
19
- A column that stores integer data.
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
- age = clearskies.columns.Integer()
31
-
32
-
33
- wsgi = clearskies.contexts.WsgiRef(
34
- clearskies.endpoints.Create(
35
- MyModel,
36
- writeable_column_names=["age"],
37
- readable_column_names=["id", "age"],
38
- ),
39
- classes=[MyModel],
40
- )
41
- wsgi()
42
- ```
43
-
44
- And when invoked:
45
-
46
- ```bash
47
- $ curl 'http://localhost:8080' -d '{"age":20}' | jq
48
- {
49
- "status": "success",
50
- "error": "",
51
- "data": {
52
- "id": "6ea74719-a65f-45ae-b6a3-641ce682ed25",
53
- "age": 20
54
- },
55
- "pagination": {},
56
- "input_errors": {}
57
- }
58
-
59
- $ curl 'http://localhost:8080' -d '{"age":"asdf"}' | jq
60
- {
61
- "status": "input_errors",
62
- "error": "",
63
- "data": [],
64
- "pagination": {},
65
- "input_errors": {
66
- "age": "value should be an integer"
67
- }
68
- }
69
- ```
70
- """
71
-
72
- default = configs.Integer(default=None) # type: ignore
73
- setable = configs.IntegerOrCallable(default=None) # type: ignore
74
- _allowed_search_operators = ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null"]
75
-
76
- auto_doc_class: type[AutoDocSchema] = AutoDocInteger
77
-
78
- _descriptor_config_map = None
79
-
80
- @clearskies.decorators.parameters_to_properties
81
- def __init__(
82
- self,
83
- default: int | None = None,
84
- setable: int | Callable[..., int] | None = None,
85
- is_readable: bool = True,
86
- is_writeable: bool = True,
87
- is_searchable: bool = True,
88
- is_temporary: bool = False,
89
- validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
90
- on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
91
- on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
92
- on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
93
- created_by_source_type: str = "",
94
- created_by_source_key: str = "",
95
- created_by_source_strict: bool = True,
96
- ):
97
- pass
98
-
99
- @overload
100
- def __get__(self, instance: None, cls: type[Model]) -> Self:
101
- pass
102
-
103
- @overload
104
- def __get__(self, instance: Model, cls: type[Model]) -> int:
105
- pass
106
-
107
- def __get__(self, instance, cls):
108
- if instance is None:
109
- self.model_class = cls
110
- return self
111
-
112
- value = super().__get__(instance, cls)
113
- return None if value is None else int(value)
114
-
115
- def __set__(self, instance, value: int) -> None:
116
- # this makes sure we're initialized
117
- if "name" not in self._config: # type: ignore
118
- instance.get_columns()
119
-
120
- instance._next_data[self.name] = value
121
-
122
- def from_backend(self, value) -> int | None:
123
- return None if value is None else int(value)
124
-
125
- def to_backend(self, data):
126
- if self.name not in data or data[self.name] is None:
127
- return data
128
-
129
- return {**data, self.name: int(data[self.name])}
130
-
131
- def input_error_for_value(self, value, operator=None):
132
- try:
133
- int(value)
134
- except ValueError:
135
- return "value should be an integer"
136
- return ""
137
-
138
- def equals(self, value: int) -> Condition:
139
- return super().equals(value)
140
-
141
- def spaceship(self, value: int) -> Condition:
142
- return super().spaceship(value)
143
-
144
- def not_equals(self, value: int) -> Condition:
145
- return super().not_equals(value)
146
-
147
- def less_than_equals(self, value: int) -> Condition:
148
- return super().less_than_equals(value)
149
-
150
- def greater_than_equals(self, value: int) -> Condition:
151
- return super().greater_than_equals(value)
152
-
153
- def less_than(self, value: int) -> Condition:
154
- return super().less_than(value)
155
-
156
- def greater_than(self, value: int) -> Condition:
157
- return super().greater_than(value)
158
-
159
- def is_in(self, values: list[int]) -> Condition:
160
- return super().is_in(values)
@@ -1,126 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- from typing import TYPE_CHECKING, Any, Callable, Self, overload
5
-
6
- import clearskies.decorators
7
- import clearskies.typing
8
- from clearskies import configs
9
- from clearskies.column import Column
10
-
11
- if TYPE_CHECKING:
12
- from clearskies import Model
13
-
14
-
15
- class Json(Column):
16
- """
17
- A column to store generic data.
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
- my_data = clearskies.columns.Json()
29
-
30
-
31
- wsgi = clearskies.contexts.WsgiRef(
32
- clearskies.endpoints.Create(
33
- MyModel,
34
- writeable_column_names=["my_data"],
35
- readable_column_names=["id", "my_data"],
36
- ),
37
- classes=[MyModel],
38
- )
39
- wsgi()
40
- ```
41
-
42
- And when invoked:
43
-
44
- ```bash
45
- $ curl 'http://localhost:8080' -d '{"my_data":{"count":[1,2,3,4,{"thing":true}]}}' | jq
46
- {
47
- "status": "success",
48
- "error": "",
49
- "data": {
50
- "id": "63cbd5e7-a198-4424-bd35-3890075a2a5e",
51
- "my_data": {
52
- "count": [
53
- 1,
54
- 2,
55
- 3,
56
- 4,
57
- {
58
- "thing": true
59
- }
60
- ]
61
- }
62
- },
63
- "pagination": {},
64
- "input_errors": {}
65
- }
66
- ```
67
-
68
- Note that there is no attempt to check the shape of the input passed into a JSON column.
69
-
70
- """
71
-
72
- is_searchable = configs.Boolean(default=False)
73
- _descriptor_config_map = None
74
-
75
- @clearskies.decorators.parameters_to_properties
76
- def __init__(
77
- self,
78
- default: dict[str, Any] | None = None,
79
- setable: dict[str, Any] | Callable[..., dict[str, Any]] | None = None,
80
- is_readable: bool = True,
81
- is_writeable: bool = True,
82
- is_temporary: bool = False,
83
- validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
84
- on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
85
- on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
86
- on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
87
- created_by_source_type: str = "",
88
- created_by_source_key: str = "",
89
- created_by_source_strict: bool = True,
90
- ):
91
- pass
92
-
93
- @overload
94
- def __get__(self, instance: None, cls: type[Model]) -> Self:
95
- pass
96
-
97
- @overload
98
- def __get__(self, instance: Model, cls: type[Model]) -> dict[str, Any]:
99
- pass
100
-
101
- def __get__(self, instance, cls):
102
- return super().__get__(instance, cls)
103
-
104
- def __set__(self, instance, value: dict[str, Any]) -> None:
105
- # this makes sure we're initialized
106
- if "name" not in self._config: # type: ignore
107
- instance.get_columns()
108
-
109
- instance._next_data[self.name] = value
110
-
111
- def from_backend(self, value) -> dict[str, Any] | list[Any] | None:
112
- if type(value) == list or type(value) == dict:
113
- return value
114
- if not value:
115
- return None
116
- try:
117
- return json.loads(value)
118
- except json.JSONDecodeError:
119
- return None
120
-
121
- def to_backend(self, data):
122
- if self.name not in data or data[self.name] is None:
123
- return data
124
-
125
- value = data[self.name]
126
- return {**data, self.name: value if isinstance(value, str) else json.dumps(value)}
@@ -1,337 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections import OrderedDict
4
- from typing import TYPE_CHECKING, Any, Callable, Self, overload
5
-
6
- import clearskies.decorators
7
- import clearskies.typing
8
- from clearskies import configs
9
- from clearskies.autodoc.schema import Array as AutoDocArray
10
- from clearskies.autodoc.schema import String as AutoDocString
11
- from clearskies.column import Column
12
- from clearskies.functional import string
13
-
14
- if TYPE_CHECKING:
15
- from clearskies import Column, Model
16
-
17
-
18
- class ManyToManyIds(Column):
19
- """
20
- A column that represents a many-to-many relationship.
21
-
22
- This is different from belongs to/has many because with those, every child has only one parent. With a many-to-many
23
- relationship, both models can have multiple relatives from the other model class. In order to support this, it's necessary
24
- to have a third model (the pivot model) that records the relationships. In general this table just needs three
25
- columns: it's own id, and then one column for each other model to store the id of the related records.
26
- You can specify the names of these columns but it also follows the standard naming convention by default:
27
- take the class name, convert it to snake case, and append `_id`.
28
-
29
- Note, there is a variation on this (`ManyToManyIdsWithData`) where additional data is stored in the pivot table
30
- to record information about the relationship.
31
-
32
- This column is writeable. You would set it to a list of ids from the related model that denotes which
33
- records it is related to.
34
-
35
- The following example shows usage. Normally the many-to-many column exists for both related models, but in this
36
- specific example it only exists for one of the models. This is done so that the example can fit in a single file
37
- and therefore be easy to demonstrate. In order to have both models reference eachother, you have to use model
38
- references to avoid circular imports. There are examples of doing this in the `BelongsTo` column class.
39
-
40
- ```python
41
- import clearskies
42
-
43
-
44
- class ThingyToWidget(clearskies.Model):
45
- id_column_name = "id"
46
- backend = clearskies.backends.MemoryBackend()
47
-
48
- id = clearskies.columns.Uuid()
49
- # these could also be belongs to relationships, but the pivot model
50
- # is rarely used directly, so I'm being lazy to avoid having to use
51
- # model references.
52
- thingy_id = clearskies.columns.String()
53
- widget_id = clearskies.columns.String()
54
-
55
-
56
- class Thingy(clearskies.Model):
57
- id_column_name = "id"
58
- backend = clearskies.backends.MemoryBackend()
59
-
60
- id = clearskies.columns.Uuid()
61
- name = clearskies.columns.String()
62
-
63
-
64
- class Widget(clearskies.Model):
65
- id_column_name = "id"
66
- backend = clearskies.backends.MemoryBackend()
67
-
68
- id = clearskies.columns.Uuid()
69
- name = clearskies.columns.String()
70
- thingy_ids = clearskies.columns.ManyToManyIds(
71
- related_model_class=Thingy,
72
- pivot_model_class=ThingyToWidget,
73
- )
74
- thingies = clearskies.columns.ManyToManyModels("thingy_ids")
75
-
76
-
77
- def my_application(widgets: Widget, thingies: Thingy):
78
- thing_1 = thingies.create({"name": "Thing 1"})
79
- thing_2 = thingies.create({"name": "Thing 2"})
80
- thing_3 = thingies.create({"name": "Thing 3"})
81
- widget = widgets.create({
82
- "name": "Widget 1",
83
- "thingy_ids": [thing_1.id, thing_2.id],
84
- })
85
-
86
- # remove an item by saving without it's id in place
87
- widget.save({"thingy_ids": [thing.id for thing in widget.thingies if thing.id != thing_1.id]})
88
-
89
- # add an item by saving and adding the new id
90
- widget.save({"thingy_ids": [*widget.thingy_ids, thing_3.id]})
91
-
92
- return widget.thingies
93
-
94
-
95
- cli = clearskies.contexts.Cli(
96
- clearskies.endpoints.Callable(
97
- my_application,
98
- model_class=Thingy,
99
- return_records=True,
100
- readable_column_names=["id", "name"],
101
- ),
102
- classes=[Widget, Thingy, ThingyToWidget],
103
- )
104
-
105
- if __name__ == "__main__":
106
- cli()
107
- ```
108
-
109
- And when executed:
110
-
111
- ```json
112
- {
113
- "status": "success",
114
- "error": "",
115
- "data": [
116
- {"id": "741bc838-c694-4624-9fc2-e9032f6cb962", "name": "Thing 2"},
117
- {"id": "1808a8ef-e288-44e6-9fed-46e3b0df057f", "name": "Thing 3"},
118
- ],
119
- "pagination": {},
120
- "input_errors": {},
121
- }
122
- ```
123
-
124
- Of course, you can also create or remove individual relationships by using the pivot model directly,
125
- as shown in these partial code snippets:
126
-
127
- ```python
128
- def add_items(thingy_to_widgets):
129
- thingy_to_widgets.create({
130
- "thingy_id": "some_id",
131
- "widget_id": "other_id",
132
- })
133
-
134
-
135
- def remove_item(thingy_to_widgets):
136
- thingy_to_widgets.where("thingy_id=some_id").where("widget_id=other_id").first().delete()
137
- ```
138
- """
139
-
140
- """ The model class for the model that we are related to. """
141
- related_model_class = configs.ModelClass(required=True)
142
-
143
- """ The model class for the pivot table - the table used to record connections between ourselves and our related table. """
144
- pivot_model_class = configs.ModelClass(required=True)
145
-
146
- """
147
- The name of the column in the pivot table that contains the id of records from the model with this column.
148
-
149
- A default name is created by taking the model class name, converting it to snake case, and then appending `_id`.
150
- If you name your columns according to this standard then you don't have to specify this column name.
151
- """
152
- own_column_name_in_pivot = configs.ModelToIdColumn(model_column_config_name="pivot_model_class")
153
-
154
- """
155
- The name of the column in the pivot table that contains the id of records from the related table.
156
-
157
- A default name is created by taking the name of the related model class, converting it to snake case, and then
158
- appending `_id`. If you name your columns according to this standard then you don't have to specify this column
159
- name.
160
- """
161
- related_column_name_in_pivot = configs.ModelToIdColumn(
162
- model_column_config_name="pivot_model_class", source_model_class_config_name="related_model_class"
163
- )
164
-
165
- """ The name of the pivot table."""
166
- pivot_table_name = configs.ModelDestinationName("pivot_model_class")
167
-
168
- """ The list of columns to be loaded from the related models when we are converted to JSON. """
169
- readable_related_column_names = configs.ReadableModelColumns("related_model_class")
170
-
171
- default = configs.StringList(default=None) # type: ignore
172
- setable = configs.StringListOrCallable(default=None) # type: ignore
173
- is_searchable = configs.Boolean(default=False)
174
- _descriptor_config_map = None
175
-
176
- @clearskies.decorators.parameters_to_properties
177
- def __init__(
178
- self,
179
- related_model_class,
180
- pivot_model_class,
181
- own_column_name_in_pivot: str = "",
182
- related_column_name_in_pivot: str = "",
183
- readable_related_column_names: list[str] = [],
184
- default: list[str] = [],
185
- setable: list[str] | Callable[..., list[str]] = [],
186
- is_readable: bool = True,
187
- is_writeable: bool = True,
188
- is_temporary: bool = False,
189
- validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
190
- on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
191
- on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
192
- on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
193
- created_by_source_type: str = "",
194
- created_by_source_key: str = "",
195
- created_by_source_strict: bool = True,
196
- ):
197
- pass
198
-
199
- def finalize_configuration(self, model_class: type, name: str) -> None:
200
- """
201
- Finalize and check the configuration.
202
-
203
- This is an external trigger called by the model class when the model class is ready.
204
- The reason it exists here instead of in the constructor is because some columns are tightly
205
- connected to the model class, and can't validate configuration until they know what the model is.
206
- Therefore, we need the model involved, and the only way for a property to know what class it is
207
- in is if the parent class checks in (which is what happens here).
208
- """
209
- self.model_class = model_class
210
- self.name = name
211
- getattr(self.__class__, "pivot_table_name").finalize_and_validate_configuration(self)
212
- own_column_name_in_pivot_config = getattr(self.__class__, "own_column_name_in_pivot")
213
- own_column_name_in_pivot_config.source_model_class = model_class
214
- own_column_name_in_pivot_config.finalize_and_validate_configuration(self)
215
- self.finalize_and_validate_configuration()
216
-
217
- def to_backend(self, data):
218
- # we can't persist our mapping data to the database directly, so remove anything here
219
- # and take care of things in post_save
220
- if self.name in data:
221
- del data[self.name]
222
- return data
223
-
224
- @property
225
- def pivot_model(self) -> Model:
226
- return self.di.build(self.pivot_model_class, cache=True)
227
-
228
- @property
229
- def related_model(self) -> Model:
230
- return self.di.build(self.related_model_class, cache=True)
231
-
232
- @property
233
- def related_columns(self) -> dict[str, Column]:
234
- return self.related_model.get_columns()
235
-
236
- @property
237
- def pivot_columns(self) -> dict[str, Column]:
238
- return self.pivot_model.get_columns()
239
-
240
- @overload
241
- def __get__(self, instance: None, cls: type[Model]) -> Self:
242
- pass
243
-
244
- @overload
245
- def __get__(self, instance: Model, cls: type[Model]) -> list[str | int]:
246
- pass
247
-
248
- def __get__(self, instance, cls):
249
- if instance is None:
250
- self.model_class = cls
251
- return self
252
-
253
- # this makes sure we're initialized
254
- if "name" not in self._config: # type: ignore
255
- instance.get_columns()
256
-
257
- related_id_column_name = self.related_model_class.id_column_name
258
- return [getattr(model, related_id_column_name) for model in self.get_related_models(instance)]
259
-
260
- def __set__(self, instance, value: list[str | int]) -> None:
261
- # this makes sure we're initialized
262
- if "name" not in self._config: # type: ignore
263
- instance.get_columns()
264
-
265
- instance._next_data[self.name] = value
266
-
267
- def get_related_models(self, model: Model) -> Model:
268
- related_column_name_in_pivot = self.related_column_name_in_pivot
269
- own_column_name_in_pivot = self.own_column_name_in_pivot
270
- pivot_table_name = self.pivot_table_name
271
- related_id_column_name = self.related_model_class.id_column_name
272
- model_id = getattr(model, self.model_class.id_column_name)
273
- model = self.related_model
274
- join = f"JOIN {pivot_table_name} ON {pivot_table_name}.{related_column_name_in_pivot}={model.destination_name()}.{related_id_column_name}"
275
- related_models = model.join(join).where(f"{pivot_table_name}.{own_column_name_in_pivot}={model_id}")
276
- return related_models
277
-
278
- def get_pivot_models(self, model: Model) -> Model:
279
- return self.pivot_model.where(
280
- f"{self.own_column_name_in_pivot}=" + getattr(model, self.model_class.id_column_name)
281
- )
282
-
283
- def post_save(self, data: dict[str, Any], model: clearskies.model.Model, id: int | str) -> None:
284
- # if our incoming data is not in the data array or is None, then nothing has been set and we do not want
285
- # to make any changes
286
- if self.name not in data or data[self.name] is None:
287
- return
288
-
289
- # figure out what ids need to be created or deleted from the pivot table.
290
- if not model:
291
- old_ids = set()
292
- else:
293
- old_ids = set(self.__get__(model, model.__class__))
294
-
295
- new_ids = set(data[self.name])
296
- to_delete = old_ids - new_ids
297
- to_create = new_ids - old_ids
298
- pivot_model = self.pivot_model
299
- related_column_name_in_pivot = self.related_column_name_in_pivot
300
- if to_delete:
301
- for model_to_delete in pivot_model.where(
302
- f"{related_column_name_in_pivot} IN ({','.join(map(str, to_delete))})"
303
- ):
304
- model_to_delete.delete()
305
- if to_create:
306
- own_column_name_in_pivot = self.own_column_name_in_pivot
307
- for id_to_create in to_create:
308
- pivot_model.create(
309
- {
310
- related_column_name_in_pivot: id_to_create,
311
- own_column_name_in_pivot: id,
312
- }
313
- )
314
-
315
- super().post_save(data, model, id)
316
-
317
- def add_search(self, model: Model, value: str, operator: str = "", relationship_reference: str = "") -> Model:
318
- related_column_name_in_pivot = self.related_column_name_in_pivot
319
- own_column_name_in_pivot = self.own_column_name_in_pivot
320
- own_id_column_name = self.model_class.id_column_name
321
- pivot_table_name = self.pivot_table_name
322
- my_table_name = self.model_class.destination_name()
323
- related_table_name = self.related_model.destination_name()
324
- join_pivot = f"JOIN {pivot_table_name} ON {pivot_table_name}.{own_column_name_in_pivot}={my_table_name}.{own_id_column_name}"
325
- # no reason we can't support searching by both an id or a list of ids
326
- values = value if type(value) == list else [value]
327
- search = " IN (" + ", ".join([str(val) for val in value]) + ")"
328
- return model.join(join_pivot).where(f"{pivot_table_name}.{related_column_name_in_pivot}{search}")
329
-
330
- def to_json(self, model: Model) -> dict[str, Any]:
331
- related_id_column_name = self.related_model_class.id_column_name
332
- records = [getattr(related, related_id_column_name) for related in self.get_related_models(model)]
333
- return {self.name: records}
334
-
335
- def documentation(self, name: str | None = None, example: str | None = None, value: str | None = None):
336
- related_id_column_name = self.related_model_class.id_column_name
337
- return AutoDocArray(name if name is not None else self.name, AutoDocString(related_id_column_name))