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