clear-skies 2.0.4__py3-none-any.whl → 2.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of clear-skies might be problematic. Click here for more details.
- clear_skies-2.0.5.dist-info/METADATA +74 -0
- clear_skies-2.0.5.dist-info/RECORD +4 -0
- {clear_skies-2.0.4.dist-info → clear_skies-2.0.5.dist-info}/WHEEL +1 -1
- clear_skies-2.0.4.dist-info/METADATA +0 -36
- clear_skies-2.0.4.dist-info/RECORD +0 -251
- clearskies/__init__.py +0 -61
- clearskies/action.py +0 -7
- clearskies/authentication/__init__.py +0 -15
- clearskies/authentication/authentication.py +0 -46
- clearskies/authentication/authorization.py +0 -16
- clearskies/authentication/authorization_pass_through.py +0 -20
- clearskies/authentication/jwks.py +0 -163
- clearskies/authentication/public.py +0 -5
- clearskies/authentication/secret_bearer.py +0 -553
- clearskies/autodoc/__init__.py +0 -8
- clearskies/autodoc/formats/__init__.py +0 -5
- clearskies/autodoc/formats/oai3_json/__init__.py +0 -7
- clearskies/autodoc/formats/oai3_json/oai3_json.py +0 -87
- clearskies/autodoc/formats/oai3_json/oai3_schema_resolver.py +0 -15
- clearskies/autodoc/formats/oai3_json/parameter.py +0 -35
- clearskies/autodoc/formats/oai3_json/request.py +0 -68
- clearskies/autodoc/formats/oai3_json/response.py +0 -28
- clearskies/autodoc/formats/oai3_json/schema/__init__.py +0 -11
- clearskies/autodoc/formats/oai3_json/schema/array.py +0 -9
- clearskies/autodoc/formats/oai3_json/schema/default.py +0 -13
- clearskies/autodoc/formats/oai3_json/schema/enum.py +0 -7
- clearskies/autodoc/formats/oai3_json/schema/object.py +0 -35
- clearskies/autodoc/formats/oai3_json/test.json +0 -1985
- clearskies/autodoc/py.typed +0 -0
- clearskies/autodoc/request/__init__.py +0 -15
- clearskies/autodoc/request/header.py +0 -6
- clearskies/autodoc/request/json_body.py +0 -6
- clearskies/autodoc/request/parameter.py +0 -8
- clearskies/autodoc/request/request.py +0 -47
- clearskies/autodoc/request/url_parameter.py +0 -6
- clearskies/autodoc/request/url_path.py +0 -6
- clearskies/autodoc/response/__init__.py +0 -5
- clearskies/autodoc/response/response.py +0 -9
- clearskies/autodoc/schema/__init__.py +0 -31
- clearskies/autodoc/schema/array.py +0 -10
- clearskies/autodoc/schema/base64.py +0 -8
- clearskies/autodoc/schema/boolean.py +0 -5
- clearskies/autodoc/schema/date.py +0 -5
- clearskies/autodoc/schema/datetime.py +0 -5
- clearskies/autodoc/schema/double.py +0 -5
- clearskies/autodoc/schema/enum.py +0 -17
- clearskies/autodoc/schema/integer.py +0 -6
- clearskies/autodoc/schema/long.py +0 -5
- clearskies/autodoc/schema/number.py +0 -6
- clearskies/autodoc/schema/object.py +0 -13
- clearskies/autodoc/schema/password.py +0 -5
- clearskies/autodoc/schema/schema.py +0 -11
- clearskies/autodoc/schema/string.py +0 -5
- clearskies/backends/__init__.py +0 -65
- clearskies/backends/api_backend.py +0 -1178
- clearskies/backends/backend.py +0 -136
- clearskies/backends/cursor_backend.py +0 -335
- clearskies/backends/memory_backend.py +0 -797
- clearskies/backends/secrets_backend.py +0 -106
- clearskies/column.py +0 -1233
- clearskies/columns/__init__.py +0 -71
- clearskies/columns/audit.py +0 -206
- clearskies/columns/belongs_to_id.py +0 -483
- clearskies/columns/belongs_to_model.py +0 -132
- clearskies/columns/belongs_to_self.py +0 -105
- clearskies/columns/boolean.py +0 -113
- clearskies/columns/category_tree.py +0 -275
- clearskies/columns/category_tree_ancestors.py +0 -51
- clearskies/columns/category_tree_children.py +0 -127
- clearskies/columns/category_tree_descendants.py +0 -48
- clearskies/columns/created.py +0 -95
- clearskies/columns/created_by_authorization_data.py +0 -116
- clearskies/columns/created_by_header.py +0 -99
- clearskies/columns/created_by_ip.py +0 -92
- clearskies/columns/created_by_routing_data.py +0 -97
- clearskies/columns/created_by_user_agent.py +0 -92
- clearskies/columns/date.py +0 -234
- clearskies/columns/datetime.py +0 -282
- clearskies/columns/email.py +0 -76
- clearskies/columns/float.py +0 -153
- clearskies/columns/has_many.py +0 -505
- clearskies/columns/has_many_self.py +0 -56
- clearskies/columns/has_one.py +0 -14
- clearskies/columns/integer.py +0 -160
- clearskies/columns/json.py +0 -126
- clearskies/columns/many_to_many_ids.py +0 -337
- clearskies/columns/many_to_many_ids_with_data.py +0 -274
- clearskies/columns/many_to_many_models.py +0 -158
- clearskies/columns/many_to_many_pivots.py +0 -134
- clearskies/columns/phone.py +0 -159
- clearskies/columns/select.py +0 -92
- clearskies/columns/string.py +0 -102
- clearskies/columns/timestamp.py +0 -164
- clearskies/columns/updated.py +0 -110
- clearskies/columns/uuid.py +0 -86
- clearskies/configs/README.md +0 -105
- clearskies/configs/__init__.py +0 -162
- clearskies/configs/actions.py +0 -43
- clearskies/configs/any.py +0 -13
- clearskies/configs/any_dict.py +0 -22
- clearskies/configs/any_dict_or_callable.py +0 -23
- clearskies/configs/authentication.py +0 -23
- clearskies/configs/authorization.py +0 -23
- clearskies/configs/boolean.py +0 -16
- clearskies/configs/boolean_or_callable.py +0 -18
- clearskies/configs/callable_config.py +0 -18
- clearskies/configs/columns.py +0 -34
- clearskies/configs/conditions.py +0 -30
- clearskies/configs/config.py +0 -24
- clearskies/configs/datetime.py +0 -18
- clearskies/configs/datetime_or_callable.py +0 -19
- clearskies/configs/endpoint.py +0 -23
- clearskies/configs/endpoint_list.py +0 -28
- clearskies/configs/float.py +0 -16
- clearskies/configs/float_or_callable.py +0 -18
- clearskies/configs/integer.py +0 -16
- clearskies/configs/integer_or_callable.py +0 -18
- clearskies/configs/joins.py +0 -30
- clearskies/configs/list_any_dict.py +0 -30
- clearskies/configs/list_any_dict_or_callable.py +0 -31
- clearskies/configs/model_class.py +0 -35
- clearskies/configs/model_column.py +0 -65
- clearskies/configs/model_columns.py +0 -56
- clearskies/configs/model_destination_name.py +0 -25
- clearskies/configs/model_to_id_column.py +0 -43
- clearskies/configs/readable_model_column.py +0 -9
- clearskies/configs/readable_model_columns.py +0 -9
- clearskies/configs/schema.py +0 -23
- clearskies/configs/searchable_model_columns.py +0 -9
- clearskies/configs/security_headers.py +0 -39
- clearskies/configs/select.py +0 -26
- clearskies/configs/select_list.py +0 -47
- clearskies/configs/string.py +0 -29
- clearskies/configs/string_dict.py +0 -32
- clearskies/configs/string_list.py +0 -32
- clearskies/configs/string_list_or_callable.py +0 -35
- clearskies/configs/string_or_callable.py +0 -18
- clearskies/configs/timedelta.py +0 -18
- clearskies/configs/timezone.py +0 -18
- clearskies/configs/url.py +0 -23
- clearskies/configs/validators.py +0 -45
- clearskies/configs/writeable_model_column.py +0 -9
- clearskies/configs/writeable_model_columns.py +0 -9
- clearskies/configurable.py +0 -76
- clearskies/contexts/__init__.py +0 -11
- clearskies/contexts/cli.py +0 -117
- clearskies/contexts/context.py +0 -98
- clearskies/contexts/wsgi.py +0 -76
- clearskies/contexts/wsgi_ref.py +0 -82
- clearskies/decorators.py +0 -33
- clearskies/di/__init__.py +0 -14
- clearskies/di/additional_config.py +0 -130
- clearskies/di/additional_config_auto_import.py +0 -17
- clearskies/di/di.py +0 -973
- clearskies/di/inject/__init__.py +0 -23
- clearskies/di/inject/by_class.py +0 -21
- clearskies/di/inject/by_name.py +0 -18
- clearskies/di/inject/di.py +0 -13
- clearskies/di/inject/environment.py +0 -14
- clearskies/di/inject/input_output.py +0 -20
- clearskies/di/inject/now.py +0 -13
- clearskies/di/inject/requests.py +0 -13
- clearskies/di/inject/secrets.py +0 -14
- clearskies/di/inject/utcnow.py +0 -13
- clearskies/di/inject/uuid.py +0 -15
- clearskies/di/injectable.py +0 -29
- clearskies/di/injectable_properties.py +0 -131
- clearskies/di/test_module/__init__.py +0 -6
- clearskies/di/test_module/another_module/__init__.py +0 -2
- clearskies/di/test_module/module_class.py +0 -5
- clearskies/end.py +0 -183
- clearskies/endpoint.py +0 -1314
- clearskies/endpoint_group.py +0 -338
- clearskies/endpoints/__init__.py +0 -25
- clearskies/endpoints/advanced_search.py +0 -526
- clearskies/endpoints/callable.py +0 -388
- clearskies/endpoints/create.py +0 -205
- clearskies/endpoints/delete.py +0 -139
- clearskies/endpoints/get.py +0 -271
- clearskies/endpoints/health_check.py +0 -183
- clearskies/endpoints/list.py +0 -574
- clearskies/endpoints/restful_api.py +0 -427
- clearskies/endpoints/schema.py +0 -189
- clearskies/endpoints/simple_search.py +0 -286
- clearskies/endpoints/update.py +0 -193
- clearskies/environment.py +0 -104
- clearskies/exceptions/__init__.py +0 -19
- clearskies/exceptions/authentication.py +0 -2
- clearskies/exceptions/authorization.py +0 -2
- clearskies/exceptions/client_error.py +0 -2
- clearskies/exceptions/input_errors.py +0 -4
- clearskies/exceptions/missing_dependency.py +0 -2
- clearskies/exceptions/moved_permanently.py +0 -3
- clearskies/exceptions/moved_temporarily.py +0 -3
- clearskies/exceptions/not_found.py +0 -2
- clearskies/functional/__init__.py +0 -7
- clearskies/functional/routing.py +0 -92
- clearskies/functional/string.py +0 -112
- clearskies/functional/validations.py +0 -76
- clearskies/input_outputs/__init__.py +0 -13
- clearskies/input_outputs/cli.py +0 -171
- clearskies/input_outputs/exceptions/__init__.py +0 -2
- clearskies/input_outputs/exceptions/cli_input_error.py +0 -2
- clearskies/input_outputs/exceptions/cli_not_found.py +0 -2
- clearskies/input_outputs/headers.py +0 -45
- clearskies/input_outputs/input_output.py +0 -138
- clearskies/input_outputs/programmatic.py +0 -69
- clearskies/input_outputs/py.typed +0 -0
- clearskies/input_outputs/wsgi.py +0 -77
- clearskies/model.py +0 -1922
- clearskies/py.typed +0 -0
- clearskies/query/__init__.py +0 -12
- clearskies/query/condition.py +0 -223
- clearskies/query/join.py +0 -136
- clearskies/query/query.py +0 -196
- clearskies/query/sort.py +0 -27
- clearskies/schema.py +0 -82
- clearskies/secrets/__init__.py +0 -6
- clearskies/secrets/additional_configs/__init__.py +0 -32
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +0 -61
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +0 -160
- clearskies/secrets/akeyless.py +0 -182
- clearskies/secrets/exceptions/__init__.py +0 -1
- clearskies/secrets/exceptions/not_found.py +0 -2
- clearskies/secrets/secrets.py +0 -38
- clearskies/security_header.py +0 -15
- clearskies/security_headers/__init__.py +0 -11
- clearskies/security_headers/cache_control.py +0 -67
- clearskies/security_headers/cors.py +0 -50
- clearskies/security_headers/csp.py +0 -94
- clearskies/security_headers/hsts.py +0 -22
- clearskies/security_headers/x_content_type_options.py +0 -0
- clearskies/security_headers/x_frame_options.py +0 -0
- clearskies/test_base.py +0 -8
- clearskies/typing.py +0 -11
- clearskies/validator.py +0 -37
- clearskies/validators/__init__.py +0 -33
- clearskies/validators/after_column.py +0 -62
- clearskies/validators/before_column.py +0 -13
- clearskies/validators/in_the_future.py +0 -32
- clearskies/validators/in_the_future_at_least.py +0 -11
- clearskies/validators/in_the_future_at_most.py +0 -10
- clearskies/validators/in_the_past.py +0 -32
- clearskies/validators/in_the_past_at_least.py +0 -10
- clearskies/validators/in_the_past_at_most.py +0 -10
- clearskies/validators/maximum_length.py +0 -26
- clearskies/validators/maximum_value.py +0 -29
- clearskies/validators/minimum_length.py +0 -26
- clearskies/validators/minimum_value.py +0 -29
- clearskies/validators/required.py +0 -34
- clearskies/validators/timedelta.py +0 -59
- clearskies/validators/unique.py +0 -30
- {clear_skies-2.0.4.dist-info → clear_skies-2.0.5.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
from typing import Callable
|
|
2
|
-
|
|
3
|
-
import clearskies.decorators
|
|
4
|
-
import clearskies.typing
|
|
5
|
-
from clearskies.columns.belongs_to_id import BelongsToId
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class BelongsToSelf(BelongsToId):
|
|
9
|
-
"""
|
|
10
|
-
This is a standard BelongsToId column except it's used in cases where the model relates to itself.
|
|
11
|
-
|
|
12
|
-
This exists because a model can't refer to itself inside it's own class definition. There are
|
|
13
|
-
workarounds, but having this class is usually quicker for the developer.
|
|
14
|
-
|
|
15
|
-
The only difference between this and BelongsToId is that you don't have to provide the parent class.
|
|
16
|
-
|
|
17
|
-
See also HasManySelf
|
|
18
|
-
|
|
19
|
-
```python
|
|
20
|
-
from typing import Any
|
|
21
|
-
|
|
22
|
-
import clearskies
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class Category(clearskies.Model):
|
|
26
|
-
id_column_name = "id"
|
|
27
|
-
backend = clearskies.backends.MemoryBackend()
|
|
28
|
-
|
|
29
|
-
id = clearskies.columns.Uuid()
|
|
30
|
-
name = clearskies.columns.String()
|
|
31
|
-
parent_id = clearskies.columns.BelongsToSelf()
|
|
32
|
-
parent = clearskies.columns.BelongsToModel("parent_id")
|
|
33
|
-
children = clearskies.columns.HasManySelf()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_self_relationship(categories: Category) -> dict[str, Any]:
|
|
37
|
-
root = categories.create({"name": "Root"})
|
|
38
|
-
sub = categories.create({"name": "Sub", "parent": root})
|
|
39
|
-
subsub_1 = categories.create({"name": "Sub Sub 1", "parent": sub})
|
|
40
|
-
subsub_2 = categories.create({"name": "Sub Sub 2", "parent_id": sub.id})
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
"root_from_child": subsub_1.parent.parent.name,
|
|
44
|
-
"subsubs_from_sub": [subsub.name for subsub in sub.children],
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
cli = clearskies.contexts.Cli(
|
|
49
|
-
clearskies.endpoints.Callable(test_self_relationship),
|
|
50
|
-
classes=[Category],
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
if __name__ == "__main__":
|
|
54
|
-
cli()
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Which when invoked returns:
|
|
58
|
-
|
|
59
|
-
```json
|
|
60
|
-
{
|
|
61
|
-
"status": "success",
|
|
62
|
-
"error": "",
|
|
63
|
-
"data": {"root_from_child": "Root", "subsubs_from_sub": ["Sub Sub 1", "Sub Sub 2"]},
|
|
64
|
-
"pagination": {},
|
|
65
|
-
"input_errors": {},
|
|
66
|
-
}
|
|
67
|
-
```
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
_descriptor_config_map = None
|
|
71
|
-
|
|
72
|
-
@clearskies.decorators.parameters_to_properties
|
|
73
|
-
def __init__(
|
|
74
|
-
self,
|
|
75
|
-
readable_parent_columns: list[str] = [],
|
|
76
|
-
join_type: str | None = None,
|
|
77
|
-
where: clearskies.typing.condition | list[clearskies.typing.condition] = [],
|
|
78
|
-
default: str | None = None,
|
|
79
|
-
setable: str | Callable | None = None,
|
|
80
|
-
is_readable: bool = True,
|
|
81
|
-
is_writeable: bool = True,
|
|
82
|
-
is_searchable: bool = True,
|
|
83
|
-
is_temporary: bool = False,
|
|
84
|
-
validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
|
|
85
|
-
on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
86
|
-
on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
87
|
-
on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
88
|
-
created_by_source_type: str = "",
|
|
89
|
-
created_by_source_key: str = "",
|
|
90
|
-
created_by_source_strict: bool = True,
|
|
91
|
-
):
|
|
92
|
-
pass
|
|
93
|
-
|
|
94
|
-
def finalize_configuration(self, model_class, name) -> None:
|
|
95
|
-
"""
|
|
96
|
-
Finalize and check the configuration.
|
|
97
|
-
|
|
98
|
-
This is an external trigger called by the model class when the model class is ready.
|
|
99
|
-
The reason it exists here instead of in the constructor is because some columns are tightly
|
|
100
|
-
connected to the model class, and can't validate configuration until they know what the model is.
|
|
101
|
-
Therefore, we need the model involved, and the only way for a property to know what class it is
|
|
102
|
-
in is if the parent class checks in (which is what happens here).
|
|
103
|
-
"""
|
|
104
|
-
self.parent_model_class = model_class
|
|
105
|
-
super().finalize_configuration(model_class, name)
|
clearskies/columns/boolean.py
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING, Callable, Self, overload
|
|
4
|
-
|
|
5
|
-
import clearskies.configs.actions
|
|
6
|
-
import clearskies.decorators
|
|
7
|
-
import clearskies.typing
|
|
8
|
-
from clearskies import configs
|
|
9
|
-
from clearskies.autodoc.schema import Boolean as AutoDocBoolean
|
|
10
|
-
from clearskies.autodoc.schema import Schema as AutoDocSchema
|
|
11
|
-
from clearskies.column import Column
|
|
12
|
-
from clearskies.query import Condition
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from clearskies import Model
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class Boolean(Column):
|
|
19
|
-
"""Represents a column with a true/false type."""
|
|
20
|
-
|
|
21
|
-
"""
|
|
22
|
-
Actions to trigger when the column changes to True
|
|
23
|
-
"""
|
|
24
|
-
on_true = clearskies.configs.actions.Actions(default=[])
|
|
25
|
-
|
|
26
|
-
"""
|
|
27
|
-
Actions to trigger when the column changes to False
|
|
28
|
-
"""
|
|
29
|
-
on_false = clearskies.configs.actions.Actions(default=[])
|
|
30
|
-
|
|
31
|
-
"""
|
|
32
|
-
The class to use when documenting this column
|
|
33
|
-
"""
|
|
34
|
-
auto_doc_class: type[AutoDocSchema] = AutoDocBoolean
|
|
35
|
-
|
|
36
|
-
_allowed_search_operators = ["="]
|
|
37
|
-
default = configs.Boolean() # type: ignore
|
|
38
|
-
setable = configs.BooleanOrCallable() # type: ignore
|
|
39
|
-
_descriptor_config_map = None
|
|
40
|
-
|
|
41
|
-
@clearskies.decorators.parameters_to_properties
|
|
42
|
-
def __init__(
|
|
43
|
-
self,
|
|
44
|
-
default: bool | None = None,
|
|
45
|
-
setable: bool | Callable[..., bool] | None = None,
|
|
46
|
-
is_readable: bool = True,
|
|
47
|
-
is_writeable: bool = True,
|
|
48
|
-
is_searchable: bool = True,
|
|
49
|
-
is_temporary: bool = False,
|
|
50
|
-
validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
|
|
51
|
-
on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
52
|
-
on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
53
|
-
on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
54
|
-
on_true: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
55
|
-
on_false: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
56
|
-
created_by_source_type: str = "",
|
|
57
|
-
created_by_source_key: str = "",
|
|
58
|
-
created_by_source_strict: bool = True,
|
|
59
|
-
):
|
|
60
|
-
pass
|
|
61
|
-
|
|
62
|
-
def from_backend(self, value) -> bool:
|
|
63
|
-
if value == "0":
|
|
64
|
-
return False
|
|
65
|
-
return bool(value)
|
|
66
|
-
|
|
67
|
-
def to_backend(self, data):
|
|
68
|
-
if self.name not in data:
|
|
69
|
-
return data
|
|
70
|
-
|
|
71
|
-
return {**data, self.name: bool(data[self.name])}
|
|
72
|
-
|
|
73
|
-
@overload
|
|
74
|
-
def __get__(self, instance: None, cls: type[Model]) -> Self:
|
|
75
|
-
pass
|
|
76
|
-
|
|
77
|
-
@overload
|
|
78
|
-
def __get__(self, instance: Model, cls: type[Model]) -> bool:
|
|
79
|
-
pass
|
|
80
|
-
|
|
81
|
-
def __get__(self, instance, cls):
|
|
82
|
-
return super().__get__(instance, cls)
|
|
83
|
-
|
|
84
|
-
def __set__(self, instance, value: bool) -> None:
|
|
85
|
-
# this makes sure we're initialized
|
|
86
|
-
if "name" not in self._config: # type: ignore
|
|
87
|
-
instance.get_columns()
|
|
88
|
-
|
|
89
|
-
instance._next_data[self.name] = value
|
|
90
|
-
|
|
91
|
-
def input_error_for_value(self, value: str, operator: str | None = None) -> str:
|
|
92
|
-
return f"{self.name} must be a boolean" if type(value) != bool else ""
|
|
93
|
-
|
|
94
|
-
def build_condition(self, value: str, operator: str | None = None, column_prefix: str = ""):
|
|
95
|
-
condition_value = "1" if value else "0"
|
|
96
|
-
if not operator:
|
|
97
|
-
operator = "="
|
|
98
|
-
return f"{column_prefix}{self.name}{operator}{condition_value}"
|
|
99
|
-
|
|
100
|
-
def save_finished(self, model: Model) -> None:
|
|
101
|
-
"""Make any necessary changes needed after a save has completely finished."""
|
|
102
|
-
super().save_finished(model)
|
|
103
|
-
|
|
104
|
-
if (not self.on_true and not self.on_false) or not model.was_changed(self.name):
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
if getattr(model, self.name) and self.on_true:
|
|
108
|
-
self.execute_actions(self.on_true, model)
|
|
109
|
-
if not getattr(model, self.name) and self.on_false:
|
|
110
|
-
self.execute_actions(self.on_false, model)
|
|
111
|
-
|
|
112
|
-
def equals(self, value: bool) -> Condition:
|
|
113
|
-
return super().equals(value)
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING, Any, Callable
|
|
4
|
-
|
|
5
|
-
import clearskies.decorators
|
|
6
|
-
import clearskies.typing
|
|
7
|
-
from clearskies import configs
|
|
8
|
-
from clearskies.columns.belongs_to_id import BelongsToId
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from clearskies import Model
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class CategoryTree(BelongsToId):
|
|
15
|
-
"""
|
|
16
|
-
The category tree helps you do quick lookups on a typical category tree.
|
|
17
|
-
|
|
18
|
-
It's a very niche tool. In general, graph databases solve this problem better, but
|
|
19
|
-
it's not always worth the effort of spinning up a new kind of database.
|
|
20
|
-
|
|
21
|
-
This column needs a special tree table where it will pre-compute and store the
|
|
22
|
-
necessary information to perform quick lookups about relationships in a cateogry
|
|
23
|
-
tree. So, imagine you have a table that represents a standard category heirarchy:
|
|
24
|
-
|
|
25
|
-
```sql
|
|
26
|
-
CREATE TABLE categories (
|
|
27
|
-
id varchar(255),
|
|
28
|
-
parent_id varchar(255),
|
|
29
|
-
name varchar(255)
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
`parent_id`, in this case, would be a reference to the `categories` table itself -
|
|
33
|
-
hence the heirarchy. This works fine as a starting point but it gets tricky when you want to answer questions like
|
|
34
|
-
"what are all the parent categories of category X?" or "what are all the child categories of category Y?".
|
|
35
|
-
This column class solves that by building a tree table that caches this data as the categories are updated.
|
|
36
|
-
That table should look like this:
|
|
37
|
-
|
|
38
|
-
```sql
|
|
39
|
-
CREATE TABLE category_tree (
|
|
40
|
-
id varchar(255),
|
|
41
|
-
parent_id varchar(255),
|
|
42
|
-
child_id varchar(255),
|
|
43
|
-
is_parent tinyint(1),
|
|
44
|
-
level tinyint(1),
|
|
45
|
-
)
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Then you would attach this column to your category model as a replacement for a typical BelongsToId relationship:
|
|
49
|
-
|
|
50
|
-
```python
|
|
51
|
-
import clearskies
|
|
52
|
-
|
|
53
|
-
class Tree(clearskies.Model):
|
|
54
|
-
id_column_name = "id"
|
|
55
|
-
backend = clearskies.backends.MemoryBackend(silent_on_missing_tables=True)
|
|
56
|
-
|
|
57
|
-
id = clearskies.columns.Uuid()
|
|
58
|
-
parent_id = clearskies.columns.String()
|
|
59
|
-
child_id = clearskies.columns.String()
|
|
60
|
-
is_parent = clearskies.columns.Boolean()
|
|
61
|
-
level = clearskies.columns.Integer()
|
|
62
|
-
|
|
63
|
-
class Category(clearskies.Model):
|
|
64
|
-
id_column_name = "id"
|
|
65
|
-
backend = clearskies.backends.MemoryBackend(silent_on_missing_tables=True)
|
|
66
|
-
|
|
67
|
-
id = clearskies.columns.Uuid()
|
|
68
|
-
name = clearskies.columns.String()
|
|
69
|
-
parent_id = clearskies.columns.CategoryTree(Tree)
|
|
70
|
-
parent = clearskies.columns.BelongsToModel("parent_id")
|
|
71
|
-
children = clearskies.columns.CategoryTreeChildren("parent_id")
|
|
72
|
-
descendants = clearskies.columns.CategoryTreeDescendants("parent_id")
|
|
73
|
-
ancestors = clearskies.columns.CategoryTreeAncestors("parent_id")
|
|
74
|
-
|
|
75
|
-
def test_category_tree(category: Category):
|
|
76
|
-
root_1 = category.create({"name": "Root 1"})
|
|
77
|
-
root_2 = category.create({"name": "Root 2"})
|
|
78
|
-
sub_1_root_1 = category.create({"name": "Sub 1 of Root 1", "parent_id": root_1.id})
|
|
79
|
-
sub_2_root_1 = category.create({"name": "Sub 2 of Root 1", "parent_id": root_1.id})
|
|
80
|
-
sub_sub = category.create({"name": "Sub Sub", "parent_id": sub_1_root_1.id})
|
|
81
|
-
sub_1_root_2 = category.create({"name": "Sub 1 of Root 2", "parent_id": root_2.id})
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
"descendants_of_root_1": [descendant.name for descendant in root_1.descendants],
|
|
85
|
-
"children_of_root_1": [child.name for child in root_1.children],
|
|
86
|
-
"descendants_of_root_2": [descendant.name for descendant in root_2.descendants],
|
|
87
|
-
"ancestors_of_sub_sub": [ancestor.name for ancestor in sub_sub.ancestors],
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
cli = clearskies.contexts.Cli(
|
|
91
|
-
clearskies.endpoints.Callable(test_category_tree),
|
|
92
|
-
classes=[Category, Tree],
|
|
93
|
-
)
|
|
94
|
-
cli()
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
And if you invoke the above you will get:
|
|
98
|
-
|
|
99
|
-
```json
|
|
100
|
-
{
|
|
101
|
-
"status": "success",
|
|
102
|
-
"error": "",
|
|
103
|
-
"data": {
|
|
104
|
-
"descendants_of_root_1": ["Sub 1 of Root 1", "Sub 2 of Root 1", "Sub Sub"],
|
|
105
|
-
"children_of_root_1": ["Sub 1 of Root 1", "Sub 2 of Root 1"],
|
|
106
|
-
"descendants_of_root_2": ["Sub 1 of Root 2"],
|
|
107
|
-
"ancestors_of_sub_sub": ["Root 1", "Sub 1 of Root 1"],
|
|
108
|
-
},
|
|
109
|
-
"pagination": {},
|
|
110
|
-
"input_errors": {},
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
In case it's not clear, the definition of these things are:
|
|
115
|
-
|
|
116
|
-
1. Descendants: All children under a given category (recursively).
|
|
117
|
-
2. Children: The direct descendants of a given category.
|
|
118
|
-
3. Ancestors: The parents of a given category, starting from the root category.
|
|
119
|
-
4. Parent: the immediate parent of the category.
|
|
120
|
-
|
|
121
|
-
"""
|
|
122
|
-
|
|
123
|
-
"""
|
|
124
|
-
The model class that will persist our tree data
|
|
125
|
-
"""
|
|
126
|
-
tree_model_class = configs.ModelClass(required=True)
|
|
127
|
-
|
|
128
|
-
"""
|
|
129
|
-
The column in the tree model that references the parent in the relationship
|
|
130
|
-
"""
|
|
131
|
-
tree_parent_id_column_name = configs.ModelColumn("tree_model_class", default="parent_id")
|
|
132
|
-
|
|
133
|
-
"""
|
|
134
|
-
The column in the tree model that references the child in the relationship
|
|
135
|
-
"""
|
|
136
|
-
tree_child_id_column_name = configs.ModelColumn("tree_model_class", default="child_id")
|
|
137
|
-
|
|
138
|
-
"""
|
|
139
|
-
The column in the tree model that denotes which node in the relationship represents the tree
|
|
140
|
-
"""
|
|
141
|
-
tree_is_parent_column_name = configs.ModelColumn("tree_model_class", default="is_parent")
|
|
142
|
-
|
|
143
|
-
"""
|
|
144
|
-
The column in the tree model that references the parent in a relationship
|
|
145
|
-
"""
|
|
146
|
-
tree_level_column_name = configs.ModelColumn("tree_model_class", default="level")
|
|
147
|
-
|
|
148
|
-
"""
|
|
149
|
-
The maximum expected depth of the tree
|
|
150
|
-
"""
|
|
151
|
-
max_iterations = configs.Integer(default=100)
|
|
152
|
-
|
|
153
|
-
"""
|
|
154
|
-
The strategy for loading relatives.
|
|
155
|
-
|
|
156
|
-
Choose whatever one actually works for your backend
|
|
157
|
-
|
|
158
|
-
* JOIN: use an actual `JOIN` (e.g. quick and efficient, but mostly only works for SQL backends).
|
|
159
|
-
* WHERE IN: Use a `WHERE IN` condition.
|
|
160
|
-
* INDIVIDUAL: Load each record separately. Works for any backend but is also the slowest.
|
|
161
|
-
"""
|
|
162
|
-
load_relatives_strategy = configs.Select(["join", "where_in", "individual"], default="join")
|
|
163
|
-
|
|
164
|
-
_descriptor_config_map = None
|
|
165
|
-
|
|
166
|
-
@clearskies.decorators.parameters_to_properties
|
|
167
|
-
def __init__(
|
|
168
|
-
self,
|
|
169
|
-
tree_model_class,
|
|
170
|
-
tree_parent_id_column_name: str = "parent_id",
|
|
171
|
-
tree_child_id_column_name: str = "child_id",
|
|
172
|
-
tree_is_parent_column_name: str = "is_parent",
|
|
173
|
-
tree_level_column_name: str = "level",
|
|
174
|
-
max_iterations: int = 100,
|
|
175
|
-
load_relatives_strategy: str = "join",
|
|
176
|
-
readable_parent_columns: list[str] = [],
|
|
177
|
-
join_type: str | None = None,
|
|
178
|
-
where: clearskies.typing.condition | list[clearskies.typing.condition] = [],
|
|
179
|
-
default: str | None = None,
|
|
180
|
-
setable: str | Callable | None = None,
|
|
181
|
-
is_readable: bool = True,
|
|
182
|
-
is_writeable: bool = True,
|
|
183
|
-
is_searchable: bool = True,
|
|
184
|
-
is_temporary: bool = False,
|
|
185
|
-
validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
|
|
186
|
-
on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
187
|
-
on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
188
|
-
on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
189
|
-
created_by_source_type: str = "",
|
|
190
|
-
created_by_source_key: str = "",
|
|
191
|
-
created_by_source_strict: bool = True,
|
|
192
|
-
):
|
|
193
|
-
pass
|
|
194
|
-
|
|
195
|
-
def finalize_configuration(self, model_class, name) -> None:
|
|
196
|
-
"""
|
|
197
|
-
Finalize and check the configuration.
|
|
198
|
-
|
|
199
|
-
This is an external trigger called by the model class when the model class is ready.
|
|
200
|
-
The reason it exists here instead of in the constructor is because some columns are tightly
|
|
201
|
-
connected to the model class, and can't validate configuration until they know what the model is.
|
|
202
|
-
Therefore, we need the model involved, and the only way for a property to know what class it is
|
|
203
|
-
in is if the parent class checks in (which is what happens here).
|
|
204
|
-
"""
|
|
205
|
-
self.parent_model_class = model_class
|
|
206
|
-
super().finalize_configuration(model_class, name)
|
|
207
|
-
|
|
208
|
-
@property
|
|
209
|
-
def tree_model(self):
|
|
210
|
-
return self.di.build(self.tree_model_class, cache=True)
|
|
211
|
-
|
|
212
|
-
def post_save(self, data: dict[str, Any], model: Model, id: int | str) -> None:
|
|
213
|
-
if not model.is_changing(self.name, data):
|
|
214
|
-
return
|
|
215
|
-
|
|
216
|
-
self.update_tree_table(model, id, model.latest(self.name, data))
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
def force_tree_update(self, model: Model):
|
|
220
|
-
self.update_tree_table(model, getattr(model, model.id_column_name), getattr(model, self.name))
|
|
221
|
-
|
|
222
|
-
def update_tree_table(self, model: Model, child_id: int | str, direct_parent_id: int | str) -> None:
|
|
223
|
-
tree_model = self.tree_model
|
|
224
|
-
parent_model = self.parent_model
|
|
225
|
-
tree_parent_id_column_name = self.tree_parent_id_column_name
|
|
226
|
-
tree_child_id_column_name = self.tree_child_id_column_name
|
|
227
|
-
tree_is_parent_column_name = self.tree_is_parent_column_name
|
|
228
|
-
tree_level_column_name = self.tree_level_column_name
|
|
229
|
-
max_iterations = self.max_iterations
|
|
230
|
-
|
|
231
|
-
# we're going to be lazy and just delete the data for the current record in the tree table,
|
|
232
|
-
# and then re-insert everything (but we can skip this if creating a new record)
|
|
233
|
-
if model:
|
|
234
|
-
for tree in tree_model.where(f"{tree_child_id_column_name}={child_id}"):
|
|
235
|
-
tree.delete()
|
|
236
|
-
|
|
237
|
-
# if we are a root category then we don't have a tree
|
|
238
|
-
if not direct_parent_id:
|
|
239
|
-
return
|
|
240
|
-
|
|
241
|
-
is_root = False
|
|
242
|
-
id_column_name = parent_model.id_column_name
|
|
243
|
-
next_parent = parent_model.find(f"{id_column_name}={direct_parent_id}")
|
|
244
|
-
tree = []
|
|
245
|
-
c = 0
|
|
246
|
-
while not is_root:
|
|
247
|
-
c += 1
|
|
248
|
-
if c > max_iterations:
|
|
249
|
-
self._circular(max_iterations)
|
|
250
|
-
|
|
251
|
-
tree.append(getattr(next_parent, next_parent.id_column_name))
|
|
252
|
-
if not getattr(next_parent, self.name):
|
|
253
|
-
is_root = True
|
|
254
|
-
else:
|
|
255
|
-
next_next_parent_id = getattr(next_parent, self.name)
|
|
256
|
-
next_parent = model.find(f"{id_column_name}={next_next_parent_id}")
|
|
257
|
-
|
|
258
|
-
tree.reverse()
|
|
259
|
-
for index, parent_id in enumerate(tree):
|
|
260
|
-
tree_model.create(
|
|
261
|
-
{
|
|
262
|
-
tree_parent_id_column_name: parent_id,
|
|
263
|
-
tree_child_id_column_name: child_id,
|
|
264
|
-
tree_is_parent_column_name: 1 if parent_id == direct_parent_id else 0,
|
|
265
|
-
tree_level_column_name: index,
|
|
266
|
-
}
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
def _circular(self, max_iterations):
|
|
270
|
-
raise ValueError(
|
|
271
|
-
f"Error for column {self.model_class.__name__}.{self.name}: "
|
|
272
|
-
+ f"I've climbed through {max_iterations} parents and haven't found the root yet."
|
|
273
|
-
+ "You may have accidentally created a circular cateogry tree. If not, and your category tree "
|
|
274
|
-
+ "really _is_ that deep, then adjust the 'max_iterations' configuration for this column accordingly. "
|
|
275
|
-
)
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING, Self, overload
|
|
4
|
-
|
|
5
|
-
from clearskies.columns.category_tree_children import CategoryTreeChildren
|
|
6
|
-
|
|
7
|
-
if TYPE_CHECKING:
|
|
8
|
-
from clearskies import Model
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class CategoryTreeAncestors(CategoryTreeChildren):
|
|
12
|
-
"""
|
|
13
|
-
A column to fetch the ancestors from a category tree column.
|
|
14
|
-
|
|
15
|
-
See the CategoryTree column for usage examples.
|
|
16
|
-
|
|
17
|
-
The ancestors are all parents of a given category, starting from the root category and working
|
|
18
|
-
down to the direct parent. So, given the following category tree:
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
Root/
|
|
22
|
-
├─ Sub/
|
|
23
|
-
│ ├─ Sub Sub/
|
|
24
|
-
│ │ ├─ Sub Sub Sub/
|
|
25
|
-
├─ Another Child/
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
The ancesotrs of `Sub Sub Sub` are `["Root", "Sub", "Sub Sub"]` while the ancestors of `Another Child`
|
|
29
|
-
are `["Root"]`
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
_descriptor_config_map = None
|
|
33
|
-
|
|
34
|
-
@overload
|
|
35
|
-
def __get__(self, instance: None, cls: type) -> Self:
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
|
-
@overload
|
|
39
|
-
def __get__(self, instance: Model, cls: type) -> Model:
|
|
40
|
-
pass
|
|
41
|
-
|
|
42
|
-
def __get__(self, model, cls):
|
|
43
|
-
if model is None:
|
|
44
|
-
self.model_class = cls
|
|
45
|
-
return self # type: ignore
|
|
46
|
-
|
|
47
|
-
# this makes sure we're initialized
|
|
48
|
-
if "name" not in self._config: # type: ignore
|
|
49
|
-
model.get_columns()
|
|
50
|
-
|
|
51
|
-
return self.relatives(model, find_parents=True, include_all=True)
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING, Self, overload
|
|
4
|
-
|
|
5
|
-
import clearskies.decorators
|
|
6
|
-
from clearskies import configs
|
|
7
|
-
from clearskies.column import Column
|
|
8
|
-
from clearskies.columns import CategoryTree
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from clearskies import Model
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class CategoryTreeChildren(Column):
|
|
15
|
-
"""
|
|
16
|
-
Return the child categories from a category tree column.
|
|
17
|
-
|
|
18
|
-
See the CategoryTree column for usage examples.
|
|
19
|
-
|
|
20
|
-
The ancestors are all direct descendants of a given category. So, given the following tree:
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
Root/
|
|
24
|
-
├─ Sub/
|
|
25
|
-
│ ├─ Sub Sub/
|
|
26
|
-
│ │ ├─ Sub Sub Sub/
|
|
27
|
-
├─ Another Child/
|
|
28
|
-
|
|
29
|
-
The children of `Root` are `["Sub", "Another Child"]`. The children of `Sub Sub` are `["Sub Sub Sub"]`.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
""" The name of the category tree column we are connected to. """
|
|
33
|
-
category_tree_column_name = configs.ModelColumn(required=True)
|
|
34
|
-
|
|
35
|
-
is_writeable = configs.Boolean(default=False)
|
|
36
|
-
is_searchable = configs.Boolean(default=False)
|
|
37
|
-
_descriptor_config_map = None
|
|
38
|
-
|
|
39
|
-
@clearskies.decorators.parameters_to_properties
|
|
40
|
-
def __init__(
|
|
41
|
-
self,
|
|
42
|
-
category_tree_column_name: str,
|
|
43
|
-
):
|
|
44
|
-
pass
|
|
45
|
-
|
|
46
|
-
def finalize_configuration(self, model_class: type, name: str) -> None:
|
|
47
|
-
"""Finalize and check the configuration."""
|
|
48
|
-
getattr(self.__class__, "category_tree_column_name").set_model_class(model_class)
|
|
49
|
-
self.model_class = model_class
|
|
50
|
-
self.name = name
|
|
51
|
-
self.finalize_and_validate_configuration()
|
|
52
|
-
|
|
53
|
-
# double check that we are pointed to a category tree column
|
|
54
|
-
category_tree_column = getattr(model_class, self.category_tree_column_name)
|
|
55
|
-
if not isinstance(category_tree_column, CategoryTree):
|
|
56
|
-
raise ValueError(
|
|
57
|
-
f"Error with configuration for {model_class.__name__}.{name}, which is a {self.__class__.__name__}. It needs to point to a category tree column, and it was told to use {model_class.__name__}.{self.category_tree_column_name}, but this is not a CategoryTree column."
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
@overload
|
|
61
|
-
def __get__(self, instance: None, cls: type[Model]) -> Self:
|
|
62
|
-
pass
|
|
63
|
-
|
|
64
|
-
@overload
|
|
65
|
-
def __get__(self, instance: Model, cls: type[Model]) -> Model:
|
|
66
|
-
pass
|
|
67
|
-
|
|
68
|
-
def __get__(self, model, cls):
|
|
69
|
-
if model is None:
|
|
70
|
-
self.model_class = cls
|
|
71
|
-
return self # type: ignore
|
|
72
|
-
|
|
73
|
-
# this makes sure we're initialized
|
|
74
|
-
if "name" not in self._config: # type: ignore
|
|
75
|
-
model.get_columns()
|
|
76
|
-
|
|
77
|
-
return self.relatives(model)
|
|
78
|
-
|
|
79
|
-
def __set__(self, model: Model, value: Model) -> None:
|
|
80
|
-
raise ValueError(
|
|
81
|
-
f"Attempt to set a value to '{model.__class__.__name__}.{self.name}, but this column is not writeable"
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
def relatives(self, model: Model, include_all: bool = False, find_parents: bool = False) -> Model | list[Model]:
|
|
85
|
-
id_column_name = model.id_column_name
|
|
86
|
-
model_id = getattr(model, id_column_name)
|
|
87
|
-
model_table_name = model.destination_name()
|
|
88
|
-
category_tree_column = getattr(self.model_class, self.category_tree_column_name)
|
|
89
|
-
tree_table_name = category_tree_column.tree_model_class.destination_name()
|
|
90
|
-
parent_id_column_name = category_tree_column.tree_parent_id_column_name
|
|
91
|
-
child_id_column_name = category_tree_column.tree_child_id_column_name
|
|
92
|
-
is_parent_column_name = category_tree_column.tree_is_parent_column_name
|
|
93
|
-
level_column_name = category_tree_column.tree_level_column_name
|
|
94
|
-
|
|
95
|
-
if find_parents:
|
|
96
|
-
join_on = parent_id_column_name
|
|
97
|
-
search_on = child_id_column_name
|
|
98
|
-
else:
|
|
99
|
-
join_on = child_id_column_name
|
|
100
|
-
search_on = parent_id_column_name
|
|
101
|
-
|
|
102
|
-
# if we can join then use a join.
|
|
103
|
-
if category_tree_column.load_relatives_strategy:
|
|
104
|
-
relatives = category_tree_column.parent_model.join(
|
|
105
|
-
f"JOIN {tree_table_name} as tree on tree.{join_on}={model_table_name}.{id_column_name}"
|
|
106
|
-
)
|
|
107
|
-
relatives = relatives.where(f"tree.{search_on}={model_id}")
|
|
108
|
-
if not include_all:
|
|
109
|
-
relatives = relatives.where(f"tree.{is_parent_column_name}=1")
|
|
110
|
-
if find_parents:
|
|
111
|
-
relatives = relatives.sort_by(level_column_name, "asc", "tree")
|
|
112
|
-
return relatives
|
|
113
|
-
|
|
114
|
-
# joins only work for SQL-like backends. Otherwise, we have to pull out our list of ids
|
|
115
|
-
branches = category_tree_column.tree_model.where(f"{search_on}={model_id}")
|
|
116
|
-
if not include_all:
|
|
117
|
-
branches = branches.where(f"{is_parent_column_name}=1")
|
|
118
|
-
if find_parents:
|
|
119
|
-
branches = branches.sort_by(level_column_name, "asc")
|
|
120
|
-
ids = [str(branch.get(join_on)) for branch in branches]
|
|
121
|
-
|
|
122
|
-
# Can we search with a WHERE IN() clause? If the backend supports it, it is probably faster
|
|
123
|
-
if category_tree_column.load_relatives_strategy == "where_in":
|
|
124
|
-
return category_tree_column.parent_model.where(f"{id_column_name} IN ('" + "','".join(ids) + "')")
|
|
125
|
-
|
|
126
|
-
# otherwise we have to load each model individually which is SLOW....
|
|
127
|
-
return [category_tree_column.parent_model.find(f"{id_column_name}={id}") for id in ids]
|