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,56 @@
|
|
|
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)
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
|
@@ -0,0 +1,160 @@
|
|
|
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)
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
setable = clearskies.configs.Any(default=None) # type: ignore
|
|
73
|
+
default = clearskies.configs.Any(default=None) # type: ignore
|
|
74
|
+
is_searchable = configs.Boolean(default=False)
|
|
75
|
+
_descriptor_config_map = None
|
|
76
|
+
|
|
77
|
+
@clearskies.decorators.parameters_to_properties
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
default: dict[str, Any] | list[Any] | None = None,
|
|
81
|
+
setable: dict[str, Any] | list[Any] | Callable[..., dict[str, Any]] | None = None,
|
|
82
|
+
is_readable: bool = True,
|
|
83
|
+
is_writeable: bool = True,
|
|
84
|
+
is_temporary: bool = False,
|
|
85
|
+
validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
|
|
86
|
+
on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
87
|
+
on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
88
|
+
on_change_save_finished: clearskies.typing.action | list[clearskies.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]) -> dict[str, Any]:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
def __get__(self, instance, cls):
|
|
104
|
+
return super().__get__(instance, cls)
|
|
105
|
+
|
|
106
|
+
def __set__(self, instance, value: dict[str, Any]) -> 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] = value
|
|
112
|
+
|
|
113
|
+
def from_backend(self, value) -> dict[str, Any] | list[Any] | None:
|
|
114
|
+
if type(value) == list or type(value) == dict:
|
|
115
|
+
return value
|
|
116
|
+
if not value:
|
|
117
|
+
return None
|
|
118
|
+
try:
|
|
119
|
+
return json.loads(value)
|
|
120
|
+
except json.JSONDecodeError:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
def to_backend(self, data):
|
|
124
|
+
if self.name not in data or data[self.name] is None:
|
|
125
|
+
return data
|
|
126
|
+
|
|
127
|
+
value = data[self.name]
|
|
128
|
+
return {**data, self.name: value if isinstance(value, str) else json.dumps(value)}
|
|
@@ -0,0 +1,337 @@
|
|
|
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))
|