clear-skies 2.0.5__py3-none-any.whl → 2.0.6__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.6.dist-info}/METADATA +1 -1
- clear_skies-2.0.6.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 +14 -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.6.dist-info}/WHEEL +0 -0
- {clear_skies-2.0.5.dist-info → clear_skies-2.0.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
5
|
+
|
|
6
|
+
import clearskies.decorators
|
|
7
|
+
import clearskies.typing
|
|
8
|
+
from clearskies import configs
|
|
9
|
+
from clearskies.autodoc.schema import Object as AutoDocObject
|
|
10
|
+
from clearskies.autodoc.schema import Schema as AutoDocSchema
|
|
11
|
+
from clearskies.autodoc.schema import String as AutoDocString
|
|
12
|
+
from clearskies.columns.string import String
|
|
13
|
+
from clearskies.di.inject import InputOutput
|
|
14
|
+
from clearskies.functional import validations
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from clearskies import Column, Model
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BelongsToId(String):
|
|
21
|
+
"""
|
|
22
|
+
Declares that this model belongs to another - that it has a parent.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
The way that a belongs to relationship works is that the child model (e.g. the one with
|
|
27
|
+
the BelongsToId column) needs to have a column that stores the id of the parent it is related
|
|
28
|
+
to. Then you can attach the BelongsToModel class and point it to the column containing the
|
|
29
|
+
id. If you allow the end-user to set the parent id in a save action, the belongs to column
|
|
30
|
+
will automatically verify that the given id corresponds to an actual record. Here's a simple
|
|
31
|
+
usage example:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import clearskies
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Category(clearskies.Model):
|
|
38
|
+
id_column_name = "id"
|
|
39
|
+
backend = clearskies.backends.MemoryBackend()
|
|
40
|
+
|
|
41
|
+
id = clearskies.columns.Uuid()
|
|
42
|
+
name = clearskies.columns.String()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Product(clearskies.Model):
|
|
46
|
+
id_column_name = "id"
|
|
47
|
+
backend = clearskies.backends.MemoryBackend()
|
|
48
|
+
|
|
49
|
+
id = clearskies.columns.Uuid()
|
|
50
|
+
name = clearskies.columns.String()
|
|
51
|
+
category_id = clearskies.columns.BelongsToId(Category)
|
|
52
|
+
category = clearskies.columns.BelongsToModel("category_id")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_belongs_to(products: Product, categories: Category):
|
|
56
|
+
toys = categories.create({"name": "Toys"})
|
|
57
|
+
auto = categories.create({"name": "Auto"})
|
|
58
|
+
|
|
59
|
+
# Note: we set the cateogry by setting "category_id"
|
|
60
|
+
ball = products.create({"name": "ball", "category_id": toys.id})
|
|
61
|
+
|
|
62
|
+
# note: we set the category by saving a category model to "category"
|
|
63
|
+
fidget_spinner = products.create({"name": "Fidget Spinner", "category": toys})
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"ball_category": ball.category.name,
|
|
67
|
+
"fidget_spinner_category": fidget_spinner.category.name,
|
|
68
|
+
"ball_id_check": ball.category_id == ball.category.id,
|
|
69
|
+
"ball_fidget_id_check": fidget_spinner.category_id == ball.category.id,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
cli = clearskies.contexts.Cli(
|
|
74
|
+
clearskies.endpoints.Callable(test_belongs_to),
|
|
75
|
+
classes=[Category, Product],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
cli()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Circular Dependency Trees
|
|
83
|
+
|
|
84
|
+
The opposite of a BelongsToId relationship is a HasMany relationship. It's common
|
|
85
|
+
for the child model to contain a BelonsToId column to point to the parent, and then
|
|
86
|
+
have the parent contain a HasMany column to point to the child. This creates circular
|
|
87
|
+
depenency errors in python. To work around this, clearskies requires the addition of
|
|
88
|
+
a "model reference" class that looks like this:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import some_model
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SomeModelReference:
|
|
95
|
+
def get_model_class(self):
|
|
96
|
+
return some_model.SomeModel
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
These have to live in their own file, should use relative imports to import the file containing
|
|
100
|
+
the model, and should not be imported into the module they live in. So, sticking with the example
|
|
101
|
+
of categories and products, you would have the following directory structure:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
├── models
|
|
105
|
+
│ ├── category.py
|
|
106
|
+
│ ├── category_reference.py
|
|
107
|
+
│ ├── product.py
|
|
108
|
+
│ └── product_reference.py
|
|
109
|
+
│
|
|
110
|
+
└── app.py
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The files would then contain:
|
|
114
|
+
|
|
115
|
+
category.py
|
|
116
|
+
```python
|
|
117
|
+
import clearskies
|
|
118
|
+
import models.product_reference
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Category(clearskies.Model):
|
|
122
|
+
id_column_name = "id"
|
|
123
|
+
backend = clearskies.backends.MemoryBackend()
|
|
124
|
+
|
|
125
|
+
id = clearskies.columns.Uuid()
|
|
126
|
+
name = clearskies.columns.String()
|
|
127
|
+
products = clearskies.columns.HasMany(product_reference.ProductReference)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
category_reference.py
|
|
131
|
+
```python
|
|
132
|
+
from clearskies.model import ModelClassReference
|
|
133
|
+
from . import cateogry
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class CategoryReference(ModelClassReference):
|
|
137
|
+
def get_model_class(self):
|
|
138
|
+
return category.Category
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
product.py
|
|
142
|
+
```python
|
|
143
|
+
import clearskies
|
|
144
|
+
import models.category_reference
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class Product(clearskies.model.Model):
|
|
148
|
+
id_column_name = "id"
|
|
149
|
+
backend = clearskies.backends.MemoryBackend()
|
|
150
|
+
|
|
151
|
+
id = clearskies.columns.Uuid()
|
|
152
|
+
name = clearskies.columns.String()
|
|
153
|
+
category_id = clearskies.columns.BelongsToId(CategoryReference)
|
|
154
|
+
category = clearskies.columns.BelongsToModel("category_id")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
product_reference.py
|
|
158
|
+
```python
|
|
159
|
+
from clearskies.model import ModelClassReference
|
|
160
|
+
from . import product
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ProductReference(ModelClassReference):
|
|
164
|
+
def get_model_class(self):
|
|
165
|
+
return product.Product
|
|
166
|
+
```
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
""" The model class we belong to. """
|
|
170
|
+
parent_model_class = configs.ModelClass(required=True)
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
The name of the property used to fetch the parent model itself.
|
|
174
|
+
|
|
175
|
+
Note that this isn't set explicitly, but by adding a BelongsToModel column to the model.
|
|
176
|
+
"""
|
|
177
|
+
model_column_name = configs.String()
|
|
178
|
+
|
|
179
|
+
"""
|
|
180
|
+
The list of columns from the parent that should be included when converting this column to JSON.
|
|
181
|
+
|
|
182
|
+
When configuring readable columns for an endpoint, you can specify the BelongsToModel column.
|
|
183
|
+
If you do this, you must set readable_parent_columns on the BelongsToId column to specify which
|
|
184
|
+
columns from the parent model should be returned in the response. See this example:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
import clearskies
|
|
188
|
+
|
|
189
|
+
class Owner(clearskies.Model):
|
|
190
|
+
id_column_name = "id"
|
|
191
|
+
backend = clearskies.backends.MemoryBackend()
|
|
192
|
+
|
|
193
|
+
id = clearskies.columns.Uuid()
|
|
194
|
+
name = clearskies.columns.String()
|
|
195
|
+
|
|
196
|
+
class Pet(clearskies.Model):
|
|
197
|
+
id_column_name = "id"
|
|
198
|
+
backend = clearskies.backends.MemoryBackend()
|
|
199
|
+
|
|
200
|
+
id = clearskies.columns.Uuid()
|
|
201
|
+
name = clearskies.columns.String()
|
|
202
|
+
owner_id = clearskies.columns.BelongsToId(
|
|
203
|
+
Owner,
|
|
204
|
+
readable_parent_columns=["id", "name"],
|
|
205
|
+
)
|
|
206
|
+
owner = clearskies.columns.BelongsToModel("owner_id")
|
|
207
|
+
|
|
208
|
+
cli = clearskies.contexts.Cli(
|
|
209
|
+
clearskies.endpoints.List(
|
|
210
|
+
Pet,
|
|
211
|
+
sortable_column_names=["id", "name"],
|
|
212
|
+
readable_column_names=["id", "name", "owner"],
|
|
213
|
+
default_sort_column_name="name",
|
|
214
|
+
),
|
|
215
|
+
classes=[Owner, Pet],
|
|
216
|
+
bindings={
|
|
217
|
+
"memory_backend_default_data": [
|
|
218
|
+
{
|
|
219
|
+
"model_class": Owner,
|
|
220
|
+
"records": [
|
|
221
|
+
{"id": "1-2-3-4", "name": "John Doe"},
|
|
222
|
+
{"id": "5-6-7-8", "name": "Jane Doe"},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"model_class": Pet,
|
|
227
|
+
"records": [
|
|
228
|
+
{"id": "a-b-c-d", "name": "Fido", "owner_id": "1-2-3-4"},
|
|
229
|
+
{"id": "e-f-g-h", "name": "Spot", "owner_id": "1-2-3-4"},
|
|
230
|
+
{"id": "i-j-k-l", "name": "Puss in Boots", "owner_id": "5-6-7-8"},
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
cli()
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
With readable_parent_columns set in the Pet.owner_id column, and owner set in the list configuration,
|
|
242
|
+
The owner id and name are included in the `owner` key of the returned Pet dictionary:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
$ ./test.py | jq
|
|
246
|
+
{
|
|
247
|
+
"status": "success",
|
|
248
|
+
"error": "",
|
|
249
|
+
"data": [
|
|
250
|
+
{
|
|
251
|
+
"id": "a-b-c-d",
|
|
252
|
+
"name": "Fido",
|
|
253
|
+
"owner": {
|
|
254
|
+
"id": "1-2-3-4",
|
|
255
|
+
"name": "John Doe"
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"id": "i-j-k-l",
|
|
260
|
+
"name": "Puss in Boots",
|
|
261
|
+
"owner": {
|
|
262
|
+
"id": "5-6-7-8",
|
|
263
|
+
"name": "Jane Doe"
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"id": "e-f-g-h",
|
|
268
|
+
"name": "Spot",
|
|
269
|
+
"owner": {
|
|
270
|
+
"id": "1-2-3-4",
|
|
271
|
+
"name": "John Doe"
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
],
|
|
275
|
+
"pagination": {},
|
|
276
|
+
"input_errors": {}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
"""
|
|
281
|
+
readable_parent_columns = configs.ReadableModelColumns("parent_model_class")
|
|
282
|
+
|
|
283
|
+
"""
|
|
284
|
+
The type of join to use when searching on the parent.
|
|
285
|
+
"""
|
|
286
|
+
join_type = configs.Select(["LEFT", "INNER", "RIGHT"], default="LEFT")
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
Any additional conditions to place on the parent table when finding related records.
|
|
290
|
+
|
|
291
|
+
where should be a list containing a combination of conditions-as-strings, queries built from the columns
|
|
292
|
+
themselves, or callable functions which accept the model and apply filters. This is primarily used in
|
|
293
|
+
input validation to exclude values as allowed parents.
|
|
294
|
+
"""
|
|
295
|
+
where = configs.Conditions()
|
|
296
|
+
|
|
297
|
+
input_output = InputOutput()
|
|
298
|
+
wants_n_plus_one = True
|
|
299
|
+
_allowed_search_operators = ["="]
|
|
300
|
+
_descriptor_config_map = None
|
|
301
|
+
|
|
302
|
+
@clearskies.decorators.parameters_to_properties
|
|
303
|
+
def __init__(
|
|
304
|
+
self,
|
|
305
|
+
parent_model_class,
|
|
306
|
+
readable_parent_columns: list[str] = [],
|
|
307
|
+
join_type: str | None = None,
|
|
308
|
+
where: clearskies.typing.condition | list[clearskies.typing.condition] = [],
|
|
309
|
+
default: str | None = None,
|
|
310
|
+
setable: str | Callable | None = None,
|
|
311
|
+
is_readable: bool = True,
|
|
312
|
+
is_writeable: bool = True,
|
|
313
|
+
is_searchable: bool = True,
|
|
314
|
+
is_temporary: bool = False,
|
|
315
|
+
validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
|
|
316
|
+
on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
317
|
+
on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
318
|
+
on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
319
|
+
created_by_source_type: str = "",
|
|
320
|
+
created_by_source_key: str = "",
|
|
321
|
+
created_by_source_strict: bool = True,
|
|
322
|
+
):
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def parent_model(self) -> Model:
|
|
327
|
+
parents = self.di.build(self.parent_model_class, cache=True)
|
|
328
|
+
if not self.where:
|
|
329
|
+
return parents
|
|
330
|
+
|
|
331
|
+
return self.apply_wheres(parents)
|
|
332
|
+
|
|
333
|
+
def apply_wheres(self, parents: Model) -> Model:
|
|
334
|
+
if not self.where:
|
|
335
|
+
return parents
|
|
336
|
+
|
|
337
|
+
for index, where in enumerate(self.where):
|
|
338
|
+
if callable(where):
|
|
339
|
+
parents = self.di.call_function(where, model=parents, **self.input_output.get_context_for_callables())
|
|
340
|
+
if not validations.is_model(parents):
|
|
341
|
+
raise ValueError(
|
|
342
|
+
f"Configuration error for {self.model_class.__name__}.{self.name}: when 'where' is a callable, it must return a model class, but when the callable in where entry #{index + 1} was called, it returned something else."
|
|
343
|
+
)
|
|
344
|
+
else:
|
|
345
|
+
parents = parents.where(where)
|
|
346
|
+
return parents
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def parent_columns(self) -> dict[str, Any]:
|
|
350
|
+
return self.parent_model_class.get_columns()
|
|
351
|
+
|
|
352
|
+
def input_error_for_value(self, value: str, operator: str | None = None) -> str:
|
|
353
|
+
parent_check = super().input_error_for_value(value)
|
|
354
|
+
if parent_check:
|
|
355
|
+
return parent_check
|
|
356
|
+
parent_model = self.parent_model
|
|
357
|
+
matching_parents = parent_model.where(f"{parent_model.id_column_name}={value}")
|
|
358
|
+
matching_parents = self.apply_wheres(matching_parents)
|
|
359
|
+
matching_parents = matching_parents.where_for_request_all(
|
|
360
|
+
matching_parents,
|
|
361
|
+
self.input_output,
|
|
362
|
+
routing_data=self.input_output.routing_data,
|
|
363
|
+
authorization_data=self.input_output.authorization_data,
|
|
364
|
+
)
|
|
365
|
+
if not len(matching_parents):
|
|
366
|
+
return f"Invalid selection for {self.name}: record does not exist"
|
|
367
|
+
return ""
|
|
368
|
+
|
|
369
|
+
def n_plus_one_add_joins(self, model: Model, column_names: list[str] = []) -> Model:
|
|
370
|
+
"""Add any additional joins to solve the N+1 problem."""
|
|
371
|
+
if not column_names:
|
|
372
|
+
column_names = self.readable_parent_columns
|
|
373
|
+
if not column_names:
|
|
374
|
+
return model
|
|
375
|
+
|
|
376
|
+
model = self.add_join(model)
|
|
377
|
+
alias = self.join_table_alias()
|
|
378
|
+
parent_id_column_name = self.parent_model.id_column_name
|
|
379
|
+
select_parts = [f"{alias}.{column_name} AS {alias}_{column_name}" for column_name in column_names]
|
|
380
|
+
if parent_id_column_name not in column_names:
|
|
381
|
+
select_parts.append(f"{alias}.{parent_id_column_name} AS {alias}_{parent_id_column_name}")
|
|
382
|
+
return model.select(", ".join(select_parts))
|
|
383
|
+
|
|
384
|
+
def add_join(self, model: Model) -> Model:
|
|
385
|
+
parent_table = self.parent_model.destination_name()
|
|
386
|
+
alias = self.join_table_alias()
|
|
387
|
+
|
|
388
|
+
if model.is_joined(parent_table, alias=alias):
|
|
389
|
+
return model
|
|
390
|
+
|
|
391
|
+
join_type = "LEFT " if self.join_type == "LEFT" else ""
|
|
392
|
+
own_table_name = model.destination_name()
|
|
393
|
+
parent_id_column_name = self.parent_model.id_column_name
|
|
394
|
+
return model.join(
|
|
395
|
+
f"{join_type}JOIN {parent_table} as {alias} on {alias}.{parent_id_column_name}={own_table_name}.{self.name}"
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def join_table_alias(self) -> str:
|
|
399
|
+
return self.parent_model.destination_name() + "_" + self.name
|
|
400
|
+
|
|
401
|
+
def is_allowed_operator(self, operator, relationship_reference=None):
|
|
402
|
+
"""Proces user data to decide if the end-user is specifying an allowed operator."""
|
|
403
|
+
if not relationship_reference:
|
|
404
|
+
return "="
|
|
405
|
+
parent_columns = self.parent_columns
|
|
406
|
+
if relationship_reference not in self.parent_columns:
|
|
407
|
+
raise ValueError(
|
|
408
|
+
"I was asked to search on a related column that doens't exist. This shouldn't have happened :("
|
|
409
|
+
)
|
|
410
|
+
return self.parent_columns[relationship_reference].is_allowed_operator(operator)
|
|
411
|
+
|
|
412
|
+
def check_search_value(self, value, operator=None, relationship_reference=None):
|
|
413
|
+
if not relationship_reference:
|
|
414
|
+
return self.input_error_for_value(value, operator=operator)
|
|
415
|
+
parent_columns = self.parent_columns
|
|
416
|
+
if relationship_reference not in self.parent_columns:
|
|
417
|
+
raise ValueError(
|
|
418
|
+
"I was asked to search on a related column that doens't exist. This shouldn't have happened :("
|
|
419
|
+
)
|
|
420
|
+
return self.parent_columns[relationship_reference].check_search_value(value, operator=operator)
|
|
421
|
+
|
|
422
|
+
def is_allowed_search_operator(self, operator: str, relationship_reference: str = "") -> bool:
|
|
423
|
+
if not relationship_reference:
|
|
424
|
+
return operator in self._allowed_search_operators
|
|
425
|
+
parent_columns = self.parent_columns
|
|
426
|
+
if relationship_reference not in self.parent_columns:
|
|
427
|
+
raise ValueError(
|
|
428
|
+
"I was asked to check search operators on a related column that doens't exist. This shouldn't have happened :("
|
|
429
|
+
)
|
|
430
|
+
return self.parent_columns[relationship_reference].is_allowed_search_operator(
|
|
431
|
+
operator, relationship_reference=relationship_reference
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
def allowed_search_operators(self, relationship_reference: str = ""):
|
|
435
|
+
if not relationship_reference:
|
|
436
|
+
return self._allowed_search_operators
|
|
437
|
+
parent_columns = self.parent_columns
|
|
438
|
+
if relationship_reference not in self.parent_columns:
|
|
439
|
+
raise ValueError(
|
|
440
|
+
"I was asked for allowed search operators on a related column that doens't exist. This shouldn't have happened :("
|
|
441
|
+
)
|
|
442
|
+
return self.parent_columns[relationship_reference].allowed_search_operators()
|
|
443
|
+
|
|
444
|
+
def add_search(
|
|
445
|
+
self, model: clearskies.model.Model, value: str, operator: str = "", relationship_reference: str = ""
|
|
446
|
+
) -> clearskies.model.Model:
|
|
447
|
+
if not relationship_reference:
|
|
448
|
+
return super().add_search(model, value, operator=operator)
|
|
449
|
+
|
|
450
|
+
if relationship_reference not in self.parent_columns:
|
|
451
|
+
raise ValueError(
|
|
452
|
+
"I was asked to search on a related column that doens't exist. This shouldn't have happened :("
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
model = self.add_join(model)
|
|
456
|
+
related_column = self.parent_columns[relationship_reference]
|
|
457
|
+
alias = self.join_table_alias()
|
|
458
|
+
return model.where(related_column.build_condition(value, operator=operator, column_prefix=f"{alias}."))
|
|
459
|
+
|
|
460
|
+
def documentation(
|
|
461
|
+
self, name: str | None = None, example: str | None = None, value: str | None = None
|
|
462
|
+
) -> list[AutoDocSchema]:
|
|
463
|
+
columns = self.parent_columns
|
|
464
|
+
parent_id_column_name = self.parent_model.id_column_name
|
|
465
|
+
parent_properties = [columns[parent_id_column_name].documentation()]
|
|
466
|
+
parent_id_doc = AutoDocString(name if name is not None else self.name)
|
|
467
|
+
|
|
468
|
+
readable_parent_columns = self.readable_parent_columns
|
|
469
|
+
if not readable_parent_columns:
|
|
470
|
+
return [parent_id_doc]
|
|
471
|
+
|
|
472
|
+
for column_name in readable_parent_columns:
|
|
473
|
+
if column_name == parent_id_column_name:
|
|
474
|
+
continue
|
|
475
|
+
parent_properties.append(columns[column_name].documentation())
|
|
476
|
+
|
|
477
|
+
return [
|
|
478
|
+
parent_id_doc,
|
|
479
|
+
AutoDocObject(
|
|
480
|
+
self.model_column_name,
|
|
481
|
+
parent_properties,
|
|
482
|
+
),
|
|
483
|
+
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Self, overload
|
|
5
|
+
|
|
6
|
+
import clearskies.decorators
|
|
7
|
+
from clearskies import configs
|
|
8
|
+
from clearskies.column import Column
|
|
9
|
+
from clearskies.columns.belongs_to_id import BelongsToId
|
|
10
|
+
from clearskies.functional import validations
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from clearskies import Model
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BelongsToModel(Column):
|
|
17
|
+
"""Return the model object for a belongs to relationship."""
|
|
18
|
+
|
|
19
|
+
""" The name of the belongs to column we are connected to. """
|
|
20
|
+
belongs_to_column_name = configs.ModelColumn(required=True)
|
|
21
|
+
|
|
22
|
+
is_temporary = clearskies.configs.boolean.Boolean(default=True)
|
|
23
|
+
_descriptor_config_map = None
|
|
24
|
+
|
|
25
|
+
@clearskies.decorators.parameters_to_properties
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
belongs_to_column_name: str,
|
|
29
|
+
):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def finalize_configuration(self, model_class: type, name: str) -> None:
|
|
33
|
+
"""Finalize and check the configuration."""
|
|
34
|
+
getattr(self.__class__, "belongs_to_column_name").set_model_class(model_class)
|
|
35
|
+
self.model_class = model_class
|
|
36
|
+
self.name = name
|
|
37
|
+
self.finalize_and_validate_configuration()
|
|
38
|
+
|
|
39
|
+
# finally, let the belongs to column know about us and make sure it's the right thing.
|
|
40
|
+
belongs_to_column = getattr(model_class, self.belongs_to_column_name)
|
|
41
|
+
if not isinstance(belongs_to_column, BelongsToId):
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Error with configuration for {model_class.__name__}.{name}, which is a BelongsToModel. It needs to point to a belongs to column, and it was told to use {model_class.__name__}.{self.belongs_to_column_name}, but this is not a BelongsToId column."
|
|
44
|
+
)
|
|
45
|
+
belongs_to_column.model_column_name = name
|
|
46
|
+
|
|
47
|
+
@overload
|
|
48
|
+
def __get__(self, instance: None, cls: type[Model]) -> Self:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@overload
|
|
52
|
+
def __get__(self, instance: Model, cls: type[Model]) -> Model:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
def __get__(self, model, cls):
|
|
56
|
+
if model is None:
|
|
57
|
+
self.model_class = cls
|
|
58
|
+
return self # type: ignore
|
|
59
|
+
|
|
60
|
+
# this makes sure we're initialized
|
|
61
|
+
if "name" not in self._config: # type: ignore
|
|
62
|
+
model.get_columns()
|
|
63
|
+
|
|
64
|
+
belongs_to_column = getattr(model.__class__, self.belongs_to_column_name)
|
|
65
|
+
parent_id = getattr(model, self.belongs_to_column_name)
|
|
66
|
+
parent_class = belongs_to_column.parent_model_class
|
|
67
|
+
parent_model = self.di.build(parent_class, cache=False)
|
|
68
|
+
if not parent_id:
|
|
69
|
+
return parent_model.empty_model()
|
|
70
|
+
|
|
71
|
+
parent_id_column_name = parent_model.id_column_name
|
|
72
|
+
join_alias = belongs_to_column.join_table_alias()
|
|
73
|
+
raw_data = model.get_raw_data()
|
|
74
|
+
|
|
75
|
+
# sometimes the model is loaded via the N+1 functionality, in which case the data will already exist
|
|
76
|
+
# in model.data but hiding under a different name.
|
|
77
|
+
if raw_data.get(f"{join_alias}.{parent_id_column_name}"):
|
|
78
|
+
parent_data = {parent_id_column_name: raw_data[f"{join_alias}_{parent_id_column_name}"]}
|
|
79
|
+
for column_name in belongs_to_column.readable_parent_columns:
|
|
80
|
+
select_alias = f"{join_alias}_{column_name}"
|
|
81
|
+
parent_data[column_name] = raw_data[select_alias] if select_alias in raw_data else None
|
|
82
|
+
return parent_model.model(parent_data)
|
|
83
|
+
|
|
84
|
+
return parent_model.find(f"{parent_id_column_name}={parent_id}")
|
|
85
|
+
|
|
86
|
+
def __set__(self, model: Model, value: Model) -> None:
|
|
87
|
+
# this makes sure we're initialized
|
|
88
|
+
if "name" not in self._config: # type: ignore
|
|
89
|
+
model.get_columns()
|
|
90
|
+
|
|
91
|
+
setattr(model, self.belongs_to_column_name, getattr(value, value.id_column_name))
|
|
92
|
+
|
|
93
|
+
def pre_save(self, data: dict[str, Any], model: Model) -> dict[str, Any]:
|
|
94
|
+
# if we have a model coming in then we want to extract the id. Either way, the model id needs to go to the
|
|
95
|
+
# belongs_to_id column, which is the only one that is actually saved.
|
|
96
|
+
if self.name in data:
|
|
97
|
+
value = data[self.name]
|
|
98
|
+
data[self.belongs_to_column_name] = (
|
|
99
|
+
getattr(value, value.id_column_name) if validations.is_model(value) else value
|
|
100
|
+
)
|
|
101
|
+
return super().pre_save(data, model)
|
|
102
|
+
|
|
103
|
+
def add_join(self, model: Model) -> Model:
|
|
104
|
+
return getattr(model.__class__, self.belongs_to_column_name).add_join(model)
|
|
105
|
+
|
|
106
|
+
def join_table_alias(self) -> str:
|
|
107
|
+
return getattr(self.model_class, self.belongs_to_column_name).join_table_alias()
|
|
108
|
+
|
|
109
|
+
def add_search(
|
|
110
|
+
self, model: clearskies.model.Model, value: str, operator: str = "", relationship_reference: str = ""
|
|
111
|
+
) -> clearskies.model.Model:
|
|
112
|
+
return getattr(self.model_class, self.belongs_to_column_name).add_search(
|
|
113
|
+
model, value, operator, relationship_reference=relationship_reference
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def to_json(self, model: Model) -> dict[str, Any]:
|
|
117
|
+
"""Convert the column into a json-friendly representation."""
|
|
118
|
+
belongs_to_column = getattr(model.__class__, self.belongs_to_column_name)
|
|
119
|
+
if not belongs_to_column.readable_parent_columns:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"Configuration error for {model.__class__.__name__}: I can't convert to JSON unless you set readable_parent_columns on my parent attribute, {model.__class__.__name__}.{self.belongs_to_column_name}."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# otherwise return an object with the readable parent columns
|
|
125
|
+
columns = belongs_to_column.parent_columns
|
|
126
|
+
parent = getattr(model, self.name)
|
|
127
|
+
json: dict[str, Any] = OrderedDict()
|
|
128
|
+
for column_name in belongs_to_column.readable_parent_columns:
|
|
129
|
+
json = {**json, **columns[column_name].to_json(parent)} # type: ignore
|
|
130
|
+
return {
|
|
131
|
+
self.name: json,
|
|
132
|
+
}
|