clear-skies 2.0.5__py3-none-any.whl → 2.0.7__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.
- {clear_skies-2.0.5.dist-info → clear_skies-2.0.7.dist-info}/METADATA +1 -1
- clear_skies-2.0.7.dist-info/RECORD +251 -0
- clearskies/__init__.py +61 -0
- clearskies/action.py +7 -0
- clearskies/authentication/__init__.py +15 -0
- clearskies/authentication/authentication.py +46 -0
- clearskies/authentication/authorization.py +16 -0
- clearskies/authentication/authorization_pass_through.py +20 -0
- clearskies/authentication/jwks.py +163 -0
- clearskies/authentication/public.py +5 -0
- clearskies/authentication/secret_bearer.py +553 -0
- clearskies/autodoc/__init__.py +8 -0
- clearskies/autodoc/formats/__init__.py +5 -0
- clearskies/autodoc/formats/oai3_json/__init__.py +7 -0
- clearskies/autodoc/formats/oai3_json/oai3_json.py +87 -0
- clearskies/autodoc/formats/oai3_json/oai3_schema_resolver.py +15 -0
- clearskies/autodoc/formats/oai3_json/parameter.py +35 -0
- clearskies/autodoc/formats/oai3_json/request.py +68 -0
- clearskies/autodoc/formats/oai3_json/response.py +28 -0
- clearskies/autodoc/formats/oai3_json/schema/__init__.py +11 -0
- clearskies/autodoc/formats/oai3_json/schema/array.py +9 -0
- clearskies/autodoc/formats/oai3_json/schema/default.py +13 -0
- clearskies/autodoc/formats/oai3_json/schema/enum.py +7 -0
- clearskies/autodoc/formats/oai3_json/schema/object.py +35 -0
- clearskies/autodoc/formats/oai3_json/test.json +1985 -0
- clearskies/autodoc/py.typed +0 -0
- clearskies/autodoc/request/__init__.py +15 -0
- clearskies/autodoc/request/header.py +6 -0
- clearskies/autodoc/request/json_body.py +6 -0
- clearskies/autodoc/request/parameter.py +8 -0
- clearskies/autodoc/request/request.py +47 -0
- clearskies/autodoc/request/url_parameter.py +6 -0
- clearskies/autodoc/request/url_path.py +6 -0
- clearskies/autodoc/response/__init__.py +5 -0
- clearskies/autodoc/response/response.py +9 -0
- clearskies/autodoc/schema/__init__.py +31 -0
- clearskies/autodoc/schema/array.py +10 -0
- clearskies/autodoc/schema/base64.py +8 -0
- clearskies/autodoc/schema/boolean.py +5 -0
- clearskies/autodoc/schema/date.py +5 -0
- clearskies/autodoc/schema/datetime.py +5 -0
- clearskies/autodoc/schema/double.py +5 -0
- clearskies/autodoc/schema/enum.py +17 -0
- clearskies/autodoc/schema/integer.py +6 -0
- clearskies/autodoc/schema/long.py +5 -0
- clearskies/autodoc/schema/number.py +6 -0
- clearskies/autodoc/schema/object.py +13 -0
- clearskies/autodoc/schema/password.py +5 -0
- clearskies/autodoc/schema/schema.py +11 -0
- clearskies/autodoc/schema/string.py +5 -0
- clearskies/backends/__init__.py +65 -0
- clearskies/backends/api_backend.py +1178 -0
- clearskies/backends/backend.py +136 -0
- clearskies/backends/cursor_backend.py +335 -0
- clearskies/backends/memory_backend.py +797 -0
- clearskies/backends/secrets_backend.py +106 -0
- clearskies/column.py +1233 -0
- clearskies/columns/__init__.py +71 -0
- clearskies/columns/audit.py +206 -0
- clearskies/columns/belongs_to_id.py +483 -0
- clearskies/columns/belongs_to_model.py +132 -0
- clearskies/columns/belongs_to_self.py +105 -0
- clearskies/columns/boolean.py +113 -0
- clearskies/columns/category_tree.py +275 -0
- clearskies/columns/category_tree_ancestors.py +51 -0
- clearskies/columns/category_tree_children.py +127 -0
- clearskies/columns/category_tree_descendants.py +48 -0
- clearskies/columns/created.py +95 -0
- clearskies/columns/created_by_authorization_data.py +116 -0
- clearskies/columns/created_by_header.py +99 -0
- clearskies/columns/created_by_ip.py +92 -0
- clearskies/columns/created_by_routing_data.py +97 -0
- clearskies/columns/created_by_user_agent.py +92 -0
- clearskies/columns/date.py +234 -0
- clearskies/columns/datetime.py +282 -0
- clearskies/columns/email.py +76 -0
- clearskies/columns/float.py +153 -0
- clearskies/columns/has_many.py +505 -0
- clearskies/columns/has_many_self.py +56 -0
- clearskies/columns/has_one.py +14 -0
- clearskies/columns/integer.py +160 -0
- clearskies/columns/json.py +128 -0
- clearskies/columns/many_to_many_ids.py +337 -0
- clearskies/columns/many_to_many_ids_with_data.py +274 -0
- clearskies/columns/many_to_many_models.py +158 -0
- clearskies/columns/many_to_many_pivots.py +134 -0
- clearskies/columns/phone.py +159 -0
- clearskies/columns/select.py +92 -0
- clearskies/columns/string.py +102 -0
- clearskies/columns/timestamp.py +164 -0
- clearskies/columns/updated.py +110 -0
- clearskies/columns/uuid.py +86 -0
- clearskies/configs/README.md +105 -0
- clearskies/configs/__init__.py +162 -0
- clearskies/configs/actions.py +43 -0
- clearskies/configs/any.py +13 -0
- clearskies/configs/any_dict.py +22 -0
- clearskies/configs/any_dict_or_callable.py +23 -0
- clearskies/configs/authentication.py +23 -0
- clearskies/configs/authorization.py +23 -0
- clearskies/configs/boolean.py +16 -0
- clearskies/configs/boolean_or_callable.py +18 -0
- clearskies/configs/callable_config.py +18 -0
- clearskies/configs/columns.py +34 -0
- clearskies/configs/conditions.py +30 -0
- clearskies/configs/config.py +24 -0
- clearskies/configs/datetime.py +18 -0
- clearskies/configs/datetime_or_callable.py +19 -0
- clearskies/configs/endpoint.py +23 -0
- clearskies/configs/endpoint_list.py +29 -0
- clearskies/configs/float.py +16 -0
- clearskies/configs/float_or_callable.py +18 -0
- clearskies/configs/integer.py +16 -0
- clearskies/configs/integer_or_callable.py +18 -0
- clearskies/configs/joins.py +30 -0
- clearskies/configs/list_any_dict.py +30 -0
- clearskies/configs/list_any_dict_or_callable.py +31 -0
- clearskies/configs/model_class.py +35 -0
- clearskies/configs/model_column.py +65 -0
- clearskies/configs/model_columns.py +56 -0
- clearskies/configs/model_destination_name.py +25 -0
- clearskies/configs/model_to_id_column.py +43 -0
- clearskies/configs/readable_model_column.py +9 -0
- clearskies/configs/readable_model_columns.py +9 -0
- clearskies/configs/schema.py +23 -0
- clearskies/configs/searchable_model_columns.py +9 -0
- clearskies/configs/security_headers.py +39 -0
- clearskies/configs/select.py +26 -0
- clearskies/configs/select_list.py +47 -0
- clearskies/configs/string.py +29 -0
- clearskies/configs/string_dict.py +32 -0
- clearskies/configs/string_list.py +32 -0
- clearskies/configs/string_list_or_callable.py +35 -0
- clearskies/configs/string_or_callable.py +18 -0
- clearskies/configs/timedelta.py +18 -0
- clearskies/configs/timezone.py +18 -0
- clearskies/configs/url.py +23 -0
- clearskies/configs/validators.py +45 -0
- clearskies/configs/writeable_model_column.py +9 -0
- clearskies/configs/writeable_model_columns.py +9 -0
- clearskies/configurable.py +76 -0
- clearskies/contexts/__init__.py +11 -0
- clearskies/contexts/cli.py +117 -0
- clearskies/contexts/context.py +98 -0
- clearskies/contexts/wsgi.py +76 -0
- clearskies/contexts/wsgi_ref.py +82 -0
- clearskies/decorators.py +33 -0
- clearskies/di/__init__.py +15 -0
- clearskies/di/additional_config.py +130 -0
- clearskies/di/additional_config_auto_import.py +17 -0
- clearskies/di/di.py +973 -0
- clearskies/di/inject/__init__.py +23 -0
- clearskies/di/inject/by_class.py +21 -0
- clearskies/di/inject/by_name.py +18 -0
- clearskies/di/inject/di.py +13 -0
- clearskies/di/inject/environment.py +14 -0
- clearskies/di/inject/input_output.py +20 -0
- clearskies/di/inject/now.py +13 -0
- clearskies/di/inject/requests.py +13 -0
- clearskies/di/inject/secrets.py +14 -0
- clearskies/di/inject/utcnow.py +13 -0
- clearskies/di/inject/uuid.py +15 -0
- clearskies/di/injectable.py +29 -0
- clearskies/di/injectable_properties.py +131 -0
- clearskies/di/test_module/__init__.py +6 -0
- clearskies/di/test_module/another_module/__init__.py +2 -0
- clearskies/di/test_module/module_class.py +5 -0
- clearskies/end.py +183 -0
- clearskies/endpoint.py +1314 -0
- clearskies/endpoint_group.py +336 -0
- clearskies/endpoints/__init__.py +25 -0
- clearskies/endpoints/advanced_search.py +526 -0
- clearskies/endpoints/callable.py +388 -0
- clearskies/endpoints/create.py +205 -0
- clearskies/endpoints/delete.py +139 -0
- clearskies/endpoints/get.py +271 -0
- clearskies/endpoints/health_check.py +183 -0
- clearskies/endpoints/list.py +574 -0
- clearskies/endpoints/restful_api.py +427 -0
- clearskies/endpoints/schema.py +189 -0
- clearskies/endpoints/simple_search.py +286 -0
- clearskies/endpoints/update.py +193 -0
- clearskies/environment.py +104 -0
- clearskies/exceptions/__init__.py +19 -0
- clearskies/exceptions/authentication.py +2 -0
- clearskies/exceptions/authorization.py +2 -0
- clearskies/exceptions/client_error.py +2 -0
- clearskies/exceptions/input_errors.py +4 -0
- clearskies/exceptions/missing_dependency.py +2 -0
- clearskies/exceptions/moved_permanently.py +3 -0
- clearskies/exceptions/moved_temporarily.py +3 -0
- clearskies/exceptions/not_found.py +2 -0
- clearskies/functional/__init__.py +7 -0
- clearskies/functional/routing.py +92 -0
- clearskies/functional/string.py +112 -0
- clearskies/functional/validations.py +76 -0
- clearskies/input_outputs/__init__.py +13 -0
- clearskies/input_outputs/cli.py +171 -0
- clearskies/input_outputs/exceptions/__init__.py +2 -0
- clearskies/input_outputs/exceptions/cli_input_error.py +2 -0
- clearskies/input_outputs/exceptions/cli_not_found.py +2 -0
- clearskies/input_outputs/headers.py +45 -0
- clearskies/input_outputs/input_output.py +138 -0
- clearskies/input_outputs/programmatic.py +69 -0
- clearskies/input_outputs/py.typed +0 -0
- clearskies/input_outputs/wsgi.py +77 -0
- clearskies/model.py +1922 -0
- clearskies/py.typed +0 -0
- clearskies/query/__init__.py +12 -0
- clearskies/query/condition.py +223 -0
- clearskies/query/join.py +136 -0
- clearskies/query/query.py +196 -0
- clearskies/query/sort.py +27 -0
- clearskies/schema.py +82 -0
- clearskies/secrets/__init__.py +6 -0
- clearskies/secrets/additional_configs/__init__.py +32 -0
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +61 -0
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +160 -0
- clearskies/secrets/akeyless.py +182 -0
- clearskies/secrets/exceptions/__init__.py +1 -0
- clearskies/secrets/exceptions/not_found.py +2 -0
- clearskies/secrets/secrets.py +38 -0
- clearskies/security_header.py +15 -0
- clearskies/security_headers/__init__.py +11 -0
- clearskies/security_headers/cache_control.py +67 -0
- clearskies/security_headers/cors.py +50 -0
- clearskies/security_headers/csp.py +94 -0
- clearskies/security_headers/hsts.py +22 -0
- clearskies/security_headers/x_content_type_options.py +0 -0
- clearskies/security_headers/x_frame_options.py +0 -0
- clearskies/test_base.py +8 -0
- clearskies/typing.py +11 -0
- clearskies/validator.py +37 -0
- clearskies/validators/__init__.py +33 -0
- clearskies/validators/after_column.py +62 -0
- clearskies/validators/before_column.py +13 -0
- clearskies/validators/in_the_future.py +32 -0
- clearskies/validators/in_the_future_at_least.py +11 -0
- clearskies/validators/in_the_future_at_most.py +10 -0
- clearskies/validators/in_the_past.py +32 -0
- clearskies/validators/in_the_past_at_least.py +10 -0
- clearskies/validators/in_the_past_at_most.py +10 -0
- clearskies/validators/maximum_length.py +26 -0
- clearskies/validators/maximum_value.py +29 -0
- clearskies/validators/minimum_length.py +26 -0
- clearskies/validators/minimum_value.py +29 -0
- clearskies/validators/required.py +34 -0
- clearskies/validators/timedelta.py +59 -0
- clearskies/validators/unique.py +30 -0
- clear_skies-2.0.5.dist-info/RECORD +0 -4
- {clear_skies-2.0.5.dist-info → clear_skies-2.0.7.dist-info}/WHEEL +0 -0
- {clear_skies-2.0.5.dist-info → clear_skies-2.0.7.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)]
|