clear-skies 1.19.22__py3-none-any.whl → 2.0.23__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.
- clear_skies-2.0.23.dist-info/METADATA +76 -0
- clear_skies-2.0.23.dist-info/RECORD +265 -0
- {clear_skies-1.19.22.dist-info → clear_skies-2.0.23.dist-info}/WHEEL +1 -1
- clearskies/__init__.py +37 -21
- clearskies/action.py +7 -0
- clearskies/authentication/__init__.py +9 -38
- clearskies/authentication/authentication.py +44 -0
- clearskies/authentication/authorization.py +14 -8
- clearskies/authentication/authorization_pass_through.py +22 -0
- clearskies/authentication/jwks.py +135 -58
- clearskies/authentication/public.py +3 -26
- clearskies/authentication/secret_bearer.py +515 -44
- clearskies/autodoc/formats/oai3_json/__init__.py +2 -2
- clearskies/autodoc/formats/oai3_json/oai3_json.py +11 -9
- clearskies/autodoc/formats/oai3_json/parameter.py +6 -3
- clearskies/autodoc/formats/oai3_json/request.py +7 -5
- clearskies/autodoc/formats/oai3_json/response.py +7 -4
- clearskies/autodoc/formats/oai3_json/schema/object.py +10 -1
- clearskies/autodoc/request/__init__.py +2 -0
- clearskies/autodoc/request/header.py +4 -6
- clearskies/autodoc/request/json_body.py +4 -6
- clearskies/autodoc/request/parameter.py +8 -0
- clearskies/autodoc/request/request.py +16 -4
- clearskies/autodoc/request/url_parameter.py +4 -6
- clearskies/autodoc/request/url_path.py +4 -6
- clearskies/autodoc/schema/__init__.py +4 -2
- clearskies/autodoc/schema/array.py +5 -6
- clearskies/autodoc/schema/boolean.py +4 -10
- clearskies/autodoc/schema/date.py +0 -3
- clearskies/autodoc/schema/datetime.py +1 -4
- clearskies/autodoc/schema/double.py +0 -3
- clearskies/autodoc/schema/enum.py +4 -2
- clearskies/autodoc/schema/integer.py +4 -9
- clearskies/autodoc/schema/long.py +0 -3
- clearskies/autodoc/schema/number.py +4 -9
- clearskies/autodoc/schema/object.py +5 -7
- clearskies/autodoc/schema/password.py +0 -3
- clearskies/autodoc/schema/schema.py +11 -0
- clearskies/autodoc/schema/string.py +4 -10
- clearskies/backends/__init__.py +56 -17
- clearskies/backends/api_backend.py +1128 -166
- clearskies/backends/backend.py +54 -85
- clearskies/backends/cursor_backend.py +246 -191
- clearskies/backends/memory_backend.py +514 -208
- clearskies/backends/secrets_backend.py +68 -31
- clearskies/column.py +1221 -0
- clearskies/columns/__init__.py +71 -0
- clearskies/columns/audit.py +306 -0
- clearskies/columns/belongs_to_id.py +478 -0
- clearskies/columns/belongs_to_model.py +129 -0
- clearskies/columns/belongs_to_self.py +109 -0
- clearskies/columns/boolean.py +110 -0
- clearskies/columns/category_tree.py +273 -0
- clearskies/columns/category_tree_ancestors.py +51 -0
- clearskies/columns/category_tree_children.py +126 -0
- clearskies/columns/category_tree_descendants.py +48 -0
- clearskies/columns/created.py +92 -0
- clearskies/columns/created_by_authorization_data.py +114 -0
- clearskies/columns/created_by_header.py +103 -0
- clearskies/columns/created_by_ip.py +90 -0
- clearskies/columns/created_by_routing_data.py +102 -0
- clearskies/columns/created_by_user_agent.py +89 -0
- clearskies/columns/date.py +232 -0
- clearskies/columns/datetime.py +284 -0
- clearskies/columns/email.py +78 -0
- clearskies/columns/float.py +149 -0
- clearskies/columns/has_many.py +529 -0
- clearskies/columns/has_many_self.py +62 -0
- clearskies/columns/has_one.py +21 -0
- clearskies/columns/integer.py +158 -0
- clearskies/columns/json.py +126 -0
- clearskies/columns/many_to_many_ids.py +335 -0
- clearskies/columns/many_to_many_ids_with_data.py +274 -0
- clearskies/columns/many_to_many_models.py +156 -0
- clearskies/columns/many_to_many_pivots.py +132 -0
- clearskies/columns/phone.py +162 -0
- clearskies/columns/select.py +95 -0
- clearskies/columns/string.py +102 -0
- clearskies/columns/timestamp.py +164 -0
- clearskies/columns/updated.py +107 -0
- clearskies/columns/uuid.py +83 -0
- clearskies/configs/README.md +105 -0
- clearskies/configs/__init__.py +170 -0
- clearskies/configs/actions.py +43 -0
- clearskies/configs/any.py +15 -0
- clearskies/configs/any_dict.py +24 -0
- clearskies/configs/any_dict_or_callable.py +25 -0
- clearskies/configs/authentication.py +23 -0
- clearskies/configs/authorization.py +23 -0
- clearskies/configs/boolean.py +18 -0
- clearskies/configs/boolean_or_callable.py +20 -0
- clearskies/configs/callable_config.py +20 -0
- clearskies/configs/columns.py +34 -0
- clearskies/configs/conditions.py +30 -0
- clearskies/configs/config.py +26 -0
- clearskies/configs/datetime.py +20 -0
- clearskies/configs/datetime_or_callable.py +21 -0
- clearskies/configs/email.py +10 -0
- clearskies/configs/email_list.py +17 -0
- clearskies/configs/email_list_or_callable.py +17 -0
- clearskies/configs/email_or_email_list_or_callable.py +59 -0
- clearskies/configs/endpoint.py +23 -0
- clearskies/configs/endpoint_list.py +29 -0
- clearskies/configs/float.py +18 -0
- clearskies/configs/float_or_callable.py +20 -0
- clearskies/configs/headers.py +28 -0
- clearskies/configs/integer.py +18 -0
- clearskies/configs/integer_or_callable.py +20 -0
- clearskies/configs/joins.py +30 -0
- clearskies/configs/list_any_dict.py +32 -0
- clearskies/configs/list_any_dict_or_callable.py +33 -0
- clearskies/configs/model_class.py +35 -0
- clearskies/configs/model_column.py +67 -0
- clearskies/configs/model_columns.py +58 -0
- clearskies/configs/model_destination_name.py +26 -0
- clearskies/configs/model_to_id_column.py +45 -0
- clearskies/configs/readable_model_column.py +11 -0
- clearskies/configs/readable_model_columns.py +11 -0
- clearskies/configs/schema.py +23 -0
- clearskies/configs/searchable_model_columns.py +11 -0
- clearskies/configs/security_headers.py +39 -0
- clearskies/configs/select.py +28 -0
- clearskies/configs/select_list.py +49 -0
- clearskies/configs/string.py +31 -0
- clearskies/configs/string_dict.py +34 -0
- clearskies/configs/string_list.py +47 -0
- clearskies/configs/string_list_or_callable.py +48 -0
- clearskies/configs/string_or_callable.py +18 -0
- clearskies/configs/timedelta.py +20 -0
- clearskies/configs/timezone.py +20 -0
- clearskies/configs/url.py +25 -0
- clearskies/configs/validators.py +45 -0
- clearskies/configs/writeable_model_column.py +11 -0
- clearskies/configs/writeable_model_columns.py +11 -0
- clearskies/configurable.py +78 -0
- clearskies/contexts/__init__.py +8 -8
- clearskies/contexts/cli.py +129 -43
- clearskies/contexts/context.py +93 -56
- clearskies/contexts/wsgi.py +79 -33
- clearskies/contexts/wsgi_ref.py +87 -0
- clearskies/cursors/__init__.py +7 -0
- clearskies/cursors/cursor.py +166 -0
- clearskies/cursors/from_environment/__init__.py +5 -0
- clearskies/cursors/from_environment/mysql.py +51 -0
- clearskies/cursors/from_environment/postgresql.py +49 -0
- clearskies/cursors/from_environment/sqlite.py +35 -0
- clearskies/cursors/mysql.py +61 -0
- clearskies/cursors/postgresql.py +61 -0
- clearskies/cursors/sqlite.py +62 -0
- clearskies/decorators.py +33 -0
- clearskies/decorators.pyi +10 -0
- clearskies/di/__init__.py +11 -7
- clearskies/di/additional_config.py +117 -3
- clearskies/di/additional_config_auto_import.py +12 -0
- clearskies/di/di.py +717 -126
- clearskies/di/inject/__init__.py +23 -0
- clearskies/di/inject/akeyless_sdk.py +16 -0
- clearskies/di/inject/by_class.py +24 -0
- clearskies/di/inject/by_name.py +22 -0
- clearskies/di/inject/di.py +16 -0
- clearskies/di/inject/environment.py +15 -0
- clearskies/di/inject/input_output.py +19 -0
- clearskies/di/inject/now.py +16 -0
- clearskies/di/inject/requests.py +16 -0
- clearskies/di/inject/secrets.py +15 -0
- clearskies/di/inject/utcnow.py +16 -0
- clearskies/di/inject/uuid.py +16 -0
- clearskies/di/injectable.py +32 -0
- clearskies/di/injectable_properties.py +131 -0
- clearskies/end.py +219 -0
- clearskies/endpoint.py +1303 -0
- clearskies/endpoint_group.py +333 -0
- clearskies/endpoints/__init__.py +25 -0
- clearskies/endpoints/advanced_search.py +519 -0
- clearskies/endpoints/callable.py +382 -0
- clearskies/endpoints/create.py +201 -0
- clearskies/endpoints/delete.py +133 -0
- clearskies/endpoints/get.py +267 -0
- clearskies/endpoints/health_check.py +181 -0
- clearskies/endpoints/list.py +567 -0
- clearskies/endpoints/restful_api.py +417 -0
- clearskies/endpoints/schema.py +185 -0
- clearskies/endpoints/simple_search.py +279 -0
- clearskies/endpoints/update.py +188 -0
- clearskies/environment.py +7 -3
- clearskies/exceptions/__init__.py +19 -0
- clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
- clearskies/exceptions/missing_dependency.py +2 -0
- clearskies/exceptions/moved_permanently.py +3 -0
- clearskies/exceptions/moved_temporarily.py +3 -0
- clearskies/functional/__init__.py +2 -2
- clearskies/functional/json.py +47 -0
- clearskies/functional/routing.py +92 -0
- clearskies/functional/string.py +19 -11
- clearskies/functional/validations.py +61 -9
- clearskies/input_outputs/__init__.py +9 -7
- clearskies/input_outputs/cli.py +135 -152
- clearskies/input_outputs/exceptions/__init__.py +6 -1
- clearskies/input_outputs/headers.py +54 -0
- clearskies/input_outputs/input_output.py +77 -123
- clearskies/input_outputs/programmatic.py +62 -0
- clearskies/input_outputs/wsgi.py +36 -48
- clearskies/model.py +1894 -199
- clearskies/query/__init__.py +12 -0
- clearskies/query/condition.py +228 -0
- clearskies/query/join.py +136 -0
- clearskies/query/query.py +193 -0
- clearskies/query/sort.py +27 -0
- clearskies/schema.py +82 -0
- clearskies/secrets/__init__.py +4 -31
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
- clearskies/secrets/akeyless.py +421 -155
- clearskies/secrets/exceptions/__init__.py +7 -1
- clearskies/secrets/exceptions/not_found_error.py +2 -0
- clearskies/secrets/exceptions/permissions_error.py +2 -0
- clearskies/secrets/secrets.py +12 -11
- clearskies/security_header.py +17 -0
- clearskies/security_headers/__init__.py +8 -8
- clearskies/security_headers/cache_control.py +47 -109
- clearskies/security_headers/cors.py +38 -92
- clearskies/security_headers/csp.py +76 -150
- clearskies/security_headers/hsts.py +14 -15
- clearskies/typing.py +11 -0
- clearskies/validator.py +36 -0
- clearskies/validators/__init__.py +33 -0
- clearskies/validators/after_column.py +61 -0
- clearskies/validators/before_column.py +15 -0
- clearskies/validators/in_the_future.py +29 -0
- clearskies/validators/in_the_future_at_least.py +13 -0
- clearskies/validators/in_the_future_at_most.py +12 -0
- clearskies/validators/in_the_past.py +29 -0
- clearskies/validators/in_the_past_at_least.py +12 -0
- clearskies/validators/in_the_past_at_most.py +12 -0
- clearskies/validators/maximum_length.py +25 -0
- clearskies/validators/maximum_value.py +28 -0
- clearskies/validators/minimum_length.py +25 -0
- clearskies/validators/minimum_value.py +28 -0
- clearskies/{input_requirements → validators}/required.py +18 -9
- clearskies/validators/timedelta.py +58 -0
- clearskies/validators/unique.py +28 -0
- clear_skies-1.19.22.dist-info/METADATA +0 -46
- clear_skies-1.19.22.dist-info/RECORD +0 -206
- clearskies/application.py +0 -29
- clearskies/authentication/auth0_jwks.py +0 -118
- clearskies/authentication/auth_exception.py +0 -2
- clearskies/authentication/jwks_jwcrypto.py +0 -39
- clearskies/backends/example_backend.py +0 -43
- clearskies/backends/file_backend.py +0 -48
- clearskies/backends/json_backend.py +0 -7
- clearskies/backends/restful_api_advanced_search_backend.py +0 -138
- clearskies/binding_config.py +0 -16
- clearskies/column_types/__init__.py +0 -184
- clearskies/column_types/audit.py +0 -235
- clearskies/column_types/belongs_to.py +0 -250
- clearskies/column_types/boolean.py +0 -60
- clearskies/column_types/category_tree.py +0 -226
- clearskies/column_types/column.py +0 -373
- clearskies/column_types/created.py +0 -26
- clearskies/column_types/created_by_authorization_data.py +0 -26
- clearskies/column_types/created_by_header.py +0 -24
- clearskies/column_types/created_by_ip.py +0 -17
- clearskies/column_types/created_by_routing_data.py +0 -25
- clearskies/column_types/created_by_user_agent.py +0 -17
- clearskies/column_types/created_micro.py +0 -26
- clearskies/column_types/datetime.py +0 -108
- clearskies/column_types/datetime_micro.py +0 -12
- clearskies/column_types/email.py +0 -18
- clearskies/column_types/float.py +0 -43
- clearskies/column_types/has_many.py +0 -139
- clearskies/column_types/integer.py +0 -41
- clearskies/column_types/json.py +0 -25
- clearskies/column_types/many_to_many.py +0 -278
- clearskies/column_types/many_to_many_with_data.py +0 -162
- clearskies/column_types/select.py +0 -11
- clearskies/column_types/string.py +0 -24
- clearskies/column_types/updated.py +0 -24
- clearskies/column_types/updated_micro.py +0 -24
- clearskies/column_types/uuid.py +0 -25
- clearskies/columns.py +0 -123
- clearskies/condition_parser.py +0 -172
- clearskies/contexts/build_context.py +0 -54
- clearskies/contexts/convert_to_application.py +0 -190
- clearskies/contexts/extract_handler.py +0 -37
- clearskies/contexts/test.py +0 -94
- clearskies/decorators/__init__.py +0 -39
- clearskies/decorators/auth0_jwks.py +0 -22
- clearskies/decorators/authorization.py +0 -10
- clearskies/decorators/binding_classes.py +0 -9
- clearskies/decorators/binding_modules.py +0 -9
- clearskies/decorators/bindings.py +0 -9
- clearskies/decorators/create.py +0 -10
- clearskies/decorators/delete.py +0 -10
- clearskies/decorators/docs.py +0 -14
- clearskies/decorators/get.py +0 -10
- clearskies/decorators/jwks.py +0 -26
- clearskies/decorators/merge.py +0 -124
- clearskies/decorators/patch.py +0 -10
- clearskies/decorators/post.py +0 -10
- clearskies/decorators/public.py +0 -11
- clearskies/decorators/response_headers.py +0 -10
- clearskies/decorators/return_raw_response.py +0 -9
- clearskies/decorators/schema.py +0 -10
- clearskies/decorators/secret_bearer.py +0 -24
- clearskies/decorators/security_headers.py +0 -10
- clearskies/di/standard_dependencies.py +0 -140
- 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/handlers/__init__.py +0 -41
- clearskies/handlers/advanced_search.py +0 -271
- clearskies/handlers/base.py +0 -473
- clearskies/handlers/callable.py +0 -189
- clearskies/handlers/create.py +0 -35
- clearskies/handlers/crud_by_method.py +0 -18
- clearskies/handlers/database_connector.py +0 -32
- clearskies/handlers/delete.py +0 -61
- clearskies/handlers/exceptions/__init__.py +0 -5
- clearskies/handlers/exceptions/not_found.py +0 -3
- clearskies/handlers/get.py +0 -156
- clearskies/handlers/health_check.py +0 -59
- clearskies/handlers/input_processing.py +0 -79
- clearskies/handlers/list.py +0 -530
- clearskies/handlers/mygrations.py +0 -82
- clearskies/handlers/request_method_routing.py +0 -47
- clearskies/handlers/restful_api.py +0 -218
- clearskies/handlers/routing.py +0 -62
- clearskies/handlers/schema_helper.py +0 -128
- clearskies/handlers/simple_routing.py +0 -204
- clearskies/handlers/simple_routing_route.py +0 -192
- clearskies/handlers/simple_search.py +0 -136
- clearskies/handlers/update.py +0 -96
- clearskies/handlers/write.py +0 -193
- clearskies/input_requirements/__init__.py +0 -68
- clearskies/input_requirements/after.py +0 -36
- clearskies/input_requirements/before.py +0 -36
- clearskies/input_requirements/in_the_future_at_least.py +0 -19
- clearskies/input_requirements/in_the_future_at_most.py +0 -19
- clearskies/input_requirements/in_the_past_at_least.py +0 -19
- clearskies/input_requirements/in_the_past_at_most.py +0 -19
- clearskies/input_requirements/maximum_length.py +0 -19
- clearskies/input_requirements/minimum_length.py +0 -22
- clearskies/input_requirements/requirement.py +0 -25
- clearskies/input_requirements/time_delta.py +0 -38
- clearskies/input_requirements/unique.py +0 -18
- clearskies/mocks/__init__.py +0 -7
- clearskies/mocks/input_output.py +0 -124
- clearskies/mocks/models.py +0 -142
- clearskies/models.py +0 -345
- clearskies/security_headers/base.py +0 -12
- clearskies/tests/simple_api/models/__init__.py +0 -2
- clearskies/tests/simple_api/models/status.py +0 -23
- clearskies/tests/simple_api/models/user.py +0 -21
- clearskies/tests/simple_api/users_api.py +0 -64
- {clear_skies-1.19.22.dist-info → clear_skies-2.0.23.dist-info/licenses}/LICENSE +0 -0
- /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
- /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
- /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
- /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
- /clearskies/{secrets/exceptions → exceptions}/not_found.py +0 -0
- /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
- /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
from .column import Column
|
|
2
|
-
from ..autodoc.schema import Boolean as AutoDocBoolean
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class Boolean(Column):
|
|
6
|
-
_auto_doc_class = AutoDocBoolean
|
|
7
|
-
|
|
8
|
-
my_configs = {
|
|
9
|
-
"on_true": None,
|
|
10
|
-
"on_false": None,
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
def __init__(self, di):
|
|
14
|
-
super().__init__(di)
|
|
15
|
-
|
|
16
|
-
def _check_configuration(self, configuration):
|
|
17
|
-
"""Check the configuration and throw exceptions as needed"""
|
|
18
|
-
super()._check_configuration(configuration)
|
|
19
|
-
for trigger in ["on_true", "on_false"]:
|
|
20
|
-
if configuration.get(trigger):
|
|
21
|
-
self._check_actions(configuration[trigger], trigger)
|
|
22
|
-
|
|
23
|
-
def to_backend(self, data):
|
|
24
|
-
if self.name not in data or data[self.name] is None:
|
|
25
|
-
return data
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
**data,
|
|
29
|
-
self.name: bool(data[self.name]),
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
def from_backend(self, value):
|
|
33
|
-
return bool(value)
|
|
34
|
-
|
|
35
|
-
def input_error_for_value(self, value, operator=None):
|
|
36
|
-
return f"{self.name} must be a boolean" if type(value) != bool else ""
|
|
37
|
-
|
|
38
|
-
def build_condition(self, value, operator=None, column_prefix=""):
|
|
39
|
-
condition_value = "1" if value else "0"
|
|
40
|
-
if not operator:
|
|
41
|
-
operator = "="
|
|
42
|
-
return f"{column_prefix}{self.name}{operator}{condition_value}"
|
|
43
|
-
|
|
44
|
-
def save_finished(self, model):
|
|
45
|
-
"""
|
|
46
|
-
Make any necessary changes needed after a save has completely finished.
|
|
47
|
-
"""
|
|
48
|
-
super().save_finished(model)
|
|
49
|
-
|
|
50
|
-
on_true = self.config("on_true", silent=True)
|
|
51
|
-
on_false = self.config("on_false", silent=True)
|
|
52
|
-
if not on_true and not on_false:
|
|
53
|
-
return
|
|
54
|
-
if not model.was_changed(self.name):
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
if model.get(self.name) and on_true:
|
|
58
|
-
self.execute_actions(on_true, model)
|
|
59
|
-
if not model.get(self.name) and on_false:
|
|
60
|
-
self.execute_actions(on_false, model)
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from .belongs_to import BelongsTo
|
|
3
|
-
from ..autodoc.schema import Array as AutoDocArray
|
|
4
|
-
from ..autodoc.schema import Object as AutoDocObject
|
|
5
|
-
from ..autodoc.schema import String as AutoDocString
|
|
6
|
-
from collections import OrderedDict
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class CategoryTree(BelongsTo):
|
|
10
|
-
"""
|
|
11
|
-
Builds a tree table for quick lookups in a category heirarchy.
|
|
12
|
-
|
|
13
|
-
Imagine you have a model that represents a category heirarchy:
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
CREATE TABLE categories (
|
|
17
|
-
id varchar(255),
|
|
18
|
-
parent_id varchar(255),
|
|
19
|
-
name varchar(255)
|
|
20
|
-
)
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Where `parent_id` references a record in the same categories table - a category tree! This works
|
|
24
|
-
fine but it gets tricky when you want to answer the question "what are all the parent categories
|
|
25
|
-
of X category?" or "what are all the child categories of Y category?". This column class solves that
|
|
26
|
-
by building a tree table that caches this data as the categories are updated. That table should look
|
|
27
|
-
like this:
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
CREATE TABLE category_tree (
|
|
31
|
-
id varchar(255),
|
|
32
|
-
parent_id varchar(255),
|
|
33
|
-
child_id varchar(255),
|
|
34
|
-
is_parent tinyint(1),
|
|
35
|
-
level tinyint(1),
|
|
36
|
-
)
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
(add indexes as desired). You then you have your corresponding models:
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
import clearskies
|
|
43
|
-
|
|
44
|
-
class CategoryTree(clearskies.Model):
|
|
45
|
-
def __init__(self, cursor_backend, columns):
|
|
46
|
-
super().__init__(cursor_backend, columns)
|
|
47
|
-
|
|
48
|
-
def columns_configuration(self):
|
|
49
|
-
return OrderedDict([
|
|
50
|
-
clearskies.column_types.string('parent_id'),
|
|
51
|
-
clearskies.column_types.string('child_id'),
|
|
52
|
-
clearskies.column_types.integer('is_parent'),
|
|
53
|
-
clearskies.column_types.integer('level'),
|
|
54
|
-
])
|
|
55
|
-
|
|
56
|
-
class Category(clearskies.Model):
|
|
57
|
-
def __init__(self, cursor_backend, columns):
|
|
58
|
-
super().__init__(cursor_backend, columns)
|
|
59
|
-
|
|
60
|
-
def columns_configuration(self):
|
|
61
|
-
return OrderedDict([
|
|
62
|
-
clearskies.column_types.string('name'),
|
|
63
|
-
clearskies.column_types.category_tree('parent_id', tree_models_class=CategoryTree),
|
|
64
|
-
])
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
You would then build your cateogry tree normally:
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
# categories object comes in from dependency injection
|
|
71
|
-
root_category = categories.create({'name': 'my root category'})
|
|
72
|
-
sub_category = categories.create({'name': 'my sub category', parent_id=root_category.id})
|
|
73
|
-
alt_sub_category = categories.create({'name': 'my alternate sub category', parent_id=root_category.id})
|
|
74
|
-
sub_sub_category = categories.create({'name': 'my sub-sub category', parent_id=sub_category.id})
|
|
75
|
-
sub_sub_sub_category = categories.create({'name': 'my sub-sub-sub category', parent_id=sub_sub_category.id})
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
and your database would look like this (using auto-incrementing ids for hopeful clarity)
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
$ SELECT * FROM category_tree
|
|
82
|
-
|
|
83
|
-
parent_id | child_id | is_parent | level
|
|
84
|
-
1 | 2 | 1 | 0 # sub category
|
|
85
|
-
1 | 3 | 1 | 0 # alt sub category
|
|
86
|
-
1 | 4 | 0 | 0 # sub sub category referencing the root category
|
|
87
|
-
2 | 4 | 1 | 1 # sub sub category referencing the sub category
|
|
88
|
-
1 | 5 | 0 | 0 # sub sub sub category referencing the root category
|
|
89
|
-
2 | 5 | 0 | 1 # sub sub sub category referencing the sub category
|
|
90
|
-
4 | 5 | 1 | 2 # sub sub sub category referencing the sub sub category
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
You can then use various SQL statements to efficiently fetch various pieces of data.
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
# the category tree for a specific category
|
|
97
|
-
SELECT parent_id FROM category_tree WHERE child_id=5 ORDER BY level DESC;
|
|
98
|
-
# all the children of a parent (excluding sub-children)
|
|
99
|
-
SELECT child_id FROM category_tree WHERE parent_id=1 AND is_parent=1
|
|
100
|
-
# All the children of a parent (including sub-children)
|
|
101
|
-
SELECT child_id FROM category_tree WHERE parent_id=1;
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Of course other kinds of databases are better at this (such as graph databases), but it isn't
|
|
105
|
-
always worth managing another database unless performance is becoming a problem.
|
|
106
|
-
"""
|
|
107
|
-
|
|
108
|
-
required_configs = [
|
|
109
|
-
"tree_models_class",
|
|
110
|
-
]
|
|
111
|
-
|
|
112
|
-
my_configs = [
|
|
113
|
-
"model_column_name",
|
|
114
|
-
"readable_parent_columns",
|
|
115
|
-
"join_type",
|
|
116
|
-
"tree_parent_id_column_name",
|
|
117
|
-
"tree_child_id_column_name",
|
|
118
|
-
"tree_is_parent_column_name",
|
|
119
|
-
"tree_level_column_name",
|
|
120
|
-
"max_iterations",
|
|
121
|
-
"parent_models_class",
|
|
122
|
-
]
|
|
123
|
-
|
|
124
|
-
def __init__(self, di):
|
|
125
|
-
super().__init__(di)
|
|
126
|
-
|
|
127
|
-
def _check_configuration(self, configuration):
|
|
128
|
-
# our parent class is the BelongsTo which needs to know the parent model class.
|
|
129
|
-
# with a category tree, we _are_ our own parent model class, so no need to ask for it.
|
|
130
|
-
super()._check_configuration(
|
|
131
|
-
{
|
|
132
|
-
**configuration,
|
|
133
|
-
"parent_models_class": configuration.get("parent_models_class", self.model_class),
|
|
134
|
-
}
|
|
135
|
-
)
|
|
136
|
-
self.validate_models_class(
|
|
137
|
-
configuration["tree_models_class"],
|
|
138
|
-
config_name="tree_models_class",
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
def _finalize_configuration(self, configuration):
|
|
142
|
-
return {
|
|
143
|
-
**super()._finalize_configuration(
|
|
144
|
-
{
|
|
145
|
-
**configuration,
|
|
146
|
-
"parent_models_class": configuration.get("parent_models_class", self.model_class),
|
|
147
|
-
}
|
|
148
|
-
),
|
|
149
|
-
**{
|
|
150
|
-
"tree_parent_id_column_name": configuration.get("tree_parent_id_column_name", "parent_id"),
|
|
151
|
-
"tree_child_id_column_name": configuration.get("tree_child_id_column_name", "child_id"),
|
|
152
|
-
"tree_is_parent_column_name": configuration.get("tree_is_parent_column_name", "is_parent"),
|
|
153
|
-
"tree_level_column_name": configuration.get("tree_level_column_name", "level"),
|
|
154
|
-
"max_iterations": configuration.get("max_iterations", 100),
|
|
155
|
-
},
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
@property
|
|
159
|
-
def tree_models(self):
|
|
160
|
-
return self.di.build(self.config("tree_models_class"), cache=True)
|
|
161
|
-
|
|
162
|
-
def post_save(self, data, model, id):
|
|
163
|
-
if not model.is_changing(self.name, data):
|
|
164
|
-
return data
|
|
165
|
-
|
|
166
|
-
self.update_tree_table(model, id, model.latest(self.name, data))
|
|
167
|
-
return data
|
|
168
|
-
|
|
169
|
-
def force_tree_update(self, model):
|
|
170
|
-
self.update_tree_table(model, model.id, model.__getattr__(self.name))
|
|
171
|
-
|
|
172
|
-
def update_tree_table(self, model, child_id, direct_parent_id):
|
|
173
|
-
tree_models = self.tree_models
|
|
174
|
-
parent_models = self.parent_models
|
|
175
|
-
model_column_name = self.config("model_column_name")
|
|
176
|
-
tree_parent_id_column_name = self.config("tree_parent_id_column_name")
|
|
177
|
-
tree_child_id_column_name = self.config("tree_child_id_column_name")
|
|
178
|
-
tree_is_parent_column_name = self.config("tree_is_parent_column_name")
|
|
179
|
-
tree_level_column_name = self.config("tree_level_column_name")
|
|
180
|
-
max_iterations = self.config("max_iterations")
|
|
181
|
-
|
|
182
|
-
# we're going to be lazy and just delete the data for the current record in the tree table,
|
|
183
|
-
# and then re-insert everything (but we can skip this if creating a new record)
|
|
184
|
-
if model.exists:
|
|
185
|
-
for tree in tree_models.where(f"{tree_child_id_column_name}={child_id}"):
|
|
186
|
-
tree.delete()
|
|
187
|
-
|
|
188
|
-
# if we are a root category then we don't have a tree
|
|
189
|
-
if not direct_parent_id:
|
|
190
|
-
return
|
|
191
|
-
|
|
192
|
-
is_root = False
|
|
193
|
-
id_column_name = parent_models.id_column_name
|
|
194
|
-
next_parent = parent_models.find(f"{id_column_name}={direct_parent_id}")
|
|
195
|
-
tree = []
|
|
196
|
-
c = 0
|
|
197
|
-
while not is_root:
|
|
198
|
-
c += 1
|
|
199
|
-
if c > max_iterations:
|
|
200
|
-
self._circular(max_iterations)
|
|
201
|
-
|
|
202
|
-
tree.append(next_parent.__getattr__(next_parent.id_column_name))
|
|
203
|
-
if not next_parent.__getattr__(self.name):
|
|
204
|
-
is_root = True
|
|
205
|
-
else:
|
|
206
|
-
next_next_parent_id = next_parent.__getattr__(self.name)
|
|
207
|
-
next_parent = parent_models.find(f"{id_column_name}={next_next_parent_id}")
|
|
208
|
-
|
|
209
|
-
tree.reverse()
|
|
210
|
-
for index, parent_id in enumerate(tree):
|
|
211
|
-
tree_models.create(
|
|
212
|
-
{
|
|
213
|
-
tree_parent_id_column_name: parent_id,
|
|
214
|
-
tree_child_id_column_name: child_id,
|
|
215
|
-
tree_is_parent_column_name: 1 if parent_id == direct_parent_id else 0,
|
|
216
|
-
tree_level_column_name: index,
|
|
217
|
-
}
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
def _circular(self, max_iterations):
|
|
221
|
-
raise ValueError(
|
|
222
|
-
f"Error for column '{self.name}' for model class '{self.model_class.__name__}': "
|
|
223
|
-
+ f"I've climbed through {max_iterations} parents and haven't found the root yet."
|
|
224
|
-
+ "You may have accidentally created a circular cateogry tree. If not, and your category tree "
|
|
225
|
-
+ "really _is_ that deep, then adjust the 'max_iterations' configuration for this column accordingly. "
|
|
226
|
-
)
|
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
from abc import ABC
|
|
2
|
-
import re
|
|
3
|
-
from ..autodoc.schema import String as AutoDocString
|
|
4
|
-
from .. import input_requirements
|
|
5
|
-
from .. import binding_config
|
|
6
|
-
import inspect
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Column(ABC):
|
|
10
|
-
_auto_doc_class = AutoDocString
|
|
11
|
-
_is_unique = None
|
|
12
|
-
_is_required = None
|
|
13
|
-
configuration = None
|
|
14
|
-
common_configs = [
|
|
15
|
-
"input_requirements",
|
|
16
|
-
"class",
|
|
17
|
-
"is_writeable",
|
|
18
|
-
"is_temporary",
|
|
19
|
-
"on_change",
|
|
20
|
-
"default",
|
|
21
|
-
"setable",
|
|
22
|
-
"created_by_source_type",
|
|
23
|
-
"created_by_source_key",
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
def __init__(self, di):
|
|
27
|
-
self.di = di
|
|
28
|
-
|
|
29
|
-
my_configs = []
|
|
30
|
-
required_configs = []
|
|
31
|
-
|
|
32
|
-
@property
|
|
33
|
-
def is_writeable(self):
|
|
34
|
-
is_writeable = self.config("is_writeable", True)
|
|
35
|
-
return True if (is_writeable or is_writeable is None) else False
|
|
36
|
-
|
|
37
|
-
@property
|
|
38
|
-
def is_readable(self):
|
|
39
|
-
return True
|
|
40
|
-
|
|
41
|
-
@property
|
|
42
|
-
def is_unique(self):
|
|
43
|
-
if self._is_unique is None:
|
|
44
|
-
requirements = self.config("input_requirements")
|
|
45
|
-
self._is_unique = False
|
|
46
|
-
for requirement in requirements:
|
|
47
|
-
if isinstance(requirement, input_requirements.Unique):
|
|
48
|
-
self._is_unique = True
|
|
49
|
-
return self._is_unique
|
|
50
|
-
|
|
51
|
-
@property
|
|
52
|
-
def is_temporary(self):
|
|
53
|
-
return bool(self.config("is_temporary", silent=True))
|
|
54
|
-
|
|
55
|
-
@property
|
|
56
|
-
def is_required(self):
|
|
57
|
-
if self._is_required is None:
|
|
58
|
-
requirements = self.config("input_requirements")
|
|
59
|
-
self._is_required = False
|
|
60
|
-
for requirement in requirements:
|
|
61
|
-
if isinstance(requirement, input_requirements.Required):
|
|
62
|
-
self._is_required = True
|
|
63
|
-
return self._is_required
|
|
64
|
-
|
|
65
|
-
def model_column_configurations(self):
|
|
66
|
-
nargs = len(inspect.getfullargspec(self.model_class.__init__).args) - 1
|
|
67
|
-
fake_model = self.model_class(*([""] * nargs))
|
|
68
|
-
return fake_model.all_columns()
|
|
69
|
-
|
|
70
|
-
def configure(self, name, configuration, model_class):
|
|
71
|
-
if not name:
|
|
72
|
-
raise ValueError(f"Missing name for column in '{model_class.__name__}'")
|
|
73
|
-
self.model_class = model_class
|
|
74
|
-
self.name = name
|
|
75
|
-
self._check_configuration(configuration)
|
|
76
|
-
configuration = self._finalize_configuration(configuration)
|
|
77
|
-
self.configuration = configuration
|
|
78
|
-
|
|
79
|
-
def _check_configuration(self, configuration):
|
|
80
|
-
"""Check the configuration and throw exceptions as needed"""
|
|
81
|
-
for key in self.required_configs:
|
|
82
|
-
if key not in configuration:
|
|
83
|
-
raise KeyError(
|
|
84
|
-
f"Missing required configuration '{key}' for column '{self.name}' in '{self.model_class.__name__}'"
|
|
85
|
-
)
|
|
86
|
-
for key in configuration.keys():
|
|
87
|
-
if key not in self.common_configs and key not in self.my_configs and key not in self.required_configs:
|
|
88
|
-
raise KeyError(
|
|
89
|
-
f"Configuration '{key}' not allowed for column '{self.name}' in '{self.model_class.__name__}'"
|
|
90
|
-
)
|
|
91
|
-
if "is_writeable" in configuration and type(configuration["is_writeable"]) != bool:
|
|
92
|
-
raise ValueError("'is_writeable' must be a boolean")
|
|
93
|
-
if configuration.get("on_change"):
|
|
94
|
-
self._check_actions(configuration.get("on_change"), "on_change")
|
|
95
|
-
|
|
96
|
-
self._check_created_by_source(configuration)
|
|
97
|
-
|
|
98
|
-
def _finalize_configuration(self, configuration):
|
|
99
|
-
"""Make any changes to the configuration/fill in defaults"""
|
|
100
|
-
if not "input_requirements" in configuration:
|
|
101
|
-
configuration["input_requirements"] = []
|
|
102
|
-
return configuration
|
|
103
|
-
|
|
104
|
-
def _check_created_by_source(self, configuration):
|
|
105
|
-
source_type = configuration.get("created_by_source_type")
|
|
106
|
-
source_key = configuration.get("created_by_source_key")
|
|
107
|
-
if not source_type and not source_key:
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
error_prefix = f"Misconfiguration for column '{self.name}' in '{self.model_class.__name__}': "
|
|
111
|
-
if not source_type or not source_key:
|
|
112
|
-
raise ValueError(
|
|
113
|
-
f"{error_prefix} must provide both 'created_by_source_type' and 'created_by_source_key' but only one was provided."
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
if not isinstance(source_type, str):
|
|
117
|
-
raise ValueError(
|
|
118
|
-
f"{error_prefix} 'created_by_source_type' must be a string but is a '"
|
|
119
|
-
+ source_type.__class__.__name__
|
|
120
|
-
+ "'"
|
|
121
|
-
)
|
|
122
|
-
if not isinstance(source_key, str):
|
|
123
|
-
raise ValueError(
|
|
124
|
-
f"{error_prefix} 'created_by_source_key' must be a string but is a '"
|
|
125
|
-
+ source_key.__class__.__name__
|
|
126
|
-
+ "'"
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
allowed_types = ["authorization_data"]
|
|
130
|
-
if source_type not in allowed_types:
|
|
131
|
-
raise ValueError(
|
|
132
|
-
f"{error_prefix} 'created_by_source_type' must be one of '" + "', '".join(allowed_types) + "'"
|
|
133
|
-
)
|
|
134
|
-
if configuration.get("setable"):
|
|
135
|
-
raise ValueError(f"{error_prefix} you cannot set both 'setable' and 'created_by_source_type'")
|
|
136
|
-
|
|
137
|
-
def _check_actions(self, actions, trigger_name):
|
|
138
|
-
"""Check that the given actions are actually understandable by the system"""
|
|
139
|
-
if type(actions) != list:
|
|
140
|
-
raise ValueError(
|
|
141
|
-
"The actions provided to a trigger should be a list of callables/binding configs, but something "
|
|
142
|
-
+ f"else was provided for the '{trigger_name}' trigger in '{self.model_class.__name__}'"
|
|
143
|
-
)
|
|
144
|
-
for index, action in enumerate(actions):
|
|
145
|
-
# if it's callable we're good. This includes functions, lambdas, callable objects,
|
|
146
|
-
# and classes that will be callable when instantiated
|
|
147
|
-
if callable(action):
|
|
148
|
-
continue
|
|
149
|
-
# the above pretty much covers everything. The only thing that we support otherwise
|
|
150
|
-
# is a binding config containing a callable class.
|
|
151
|
-
if type(action) == binding_config.BindingConfig:
|
|
152
|
-
if callable(action.object_class):
|
|
153
|
-
continue
|
|
154
|
-
|
|
155
|
-
raise ValueError(
|
|
156
|
-
f"Invalid action: action #{index+1} for trigger '{trigger_name} in '{self.model_class.__name__}'"
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
def config(self, key, silent=False):
|
|
160
|
-
if not key in self.configuration:
|
|
161
|
-
if silent:
|
|
162
|
-
return None
|
|
163
|
-
raise KeyError(f"column '{self.__class__.__name__}' does not have a configuration named '{key}'")
|
|
164
|
-
|
|
165
|
-
return self.configuration[key]
|
|
166
|
-
|
|
167
|
-
def additional_write_columns(self, is_create=False):
|
|
168
|
-
additional_write_columns = {}
|
|
169
|
-
for requirement in self.config("input_requirements"):
|
|
170
|
-
additional_write_columns = {
|
|
171
|
-
**additional_write_columns,
|
|
172
|
-
**requirement.additional_write_columns(is_create=is_create),
|
|
173
|
-
}
|
|
174
|
-
return additional_write_columns
|
|
175
|
-
|
|
176
|
-
def from_backend(self, value):
|
|
177
|
-
"""
|
|
178
|
-
Takes the database representation and returns a python representation
|
|
179
|
-
|
|
180
|
-
For instance, for an SQL date field, this will return a Python DateTime object
|
|
181
|
-
"""
|
|
182
|
-
return value
|
|
183
|
-
|
|
184
|
-
def to_backend(self, data):
|
|
185
|
-
"""
|
|
186
|
-
Makes any changes needed to save the data to the backend.
|
|
187
|
-
|
|
188
|
-
This typically means formatting changes - converting DateTime objects to database
|
|
189
|
-
date strings, etc...
|
|
190
|
-
"""
|
|
191
|
-
return data
|
|
192
|
-
|
|
193
|
-
def to_json(self, model):
|
|
194
|
-
"""
|
|
195
|
-
Grabs the column out of the model and converts it into a representation that can be turned into JSON
|
|
196
|
-
"""
|
|
197
|
-
return {self.name: model.get(self.name, silent=True)}
|
|
198
|
-
|
|
199
|
-
def input_errors(self, model, data):
|
|
200
|
-
error = self.check_input(model, data)
|
|
201
|
-
if error:
|
|
202
|
-
return {self.name: error}
|
|
203
|
-
|
|
204
|
-
for requirement in self.config("input_requirements"):
|
|
205
|
-
error = requirement.check(model, data)
|
|
206
|
-
if error:
|
|
207
|
-
return {self.name: error}
|
|
208
|
-
|
|
209
|
-
return {}
|
|
210
|
-
|
|
211
|
-
def check_input(self, model, data):
|
|
212
|
-
if self.name not in data or not data[self.name]:
|
|
213
|
-
return ""
|
|
214
|
-
return self.input_error_for_value(data[self.name])
|
|
215
|
-
|
|
216
|
-
def pre_save(self, data, model):
|
|
217
|
-
"""
|
|
218
|
-
Make any changes needed to the data before starting the save process
|
|
219
|
-
|
|
220
|
-
The difference between this and transform_for_database is that transform_for_database only affects
|
|
221
|
-
the data as it is going into the database, while this affects the data that will get persisted
|
|
222
|
-
in the object as well. So for instance, for a "created" field, pre_save may fill in the current
|
|
223
|
-
date with a Python DateTime object when the record is being saved, and then transform_for_database may
|
|
224
|
-
turn that into an SQL-compatible date string.
|
|
225
|
-
|
|
226
|
-
The difference between this and post_save is that this happens before the database is updated.
|
|
227
|
-
As a result, if you need the model id to make your changes, it has to happen in post_save, not pre_save
|
|
228
|
-
"""
|
|
229
|
-
if not model.exists:
|
|
230
|
-
source_type = self.configuration.get("created_by_source_type")
|
|
231
|
-
if source_type:
|
|
232
|
-
if source_type == "authorization_data":
|
|
233
|
-
authorization_data = self.di.build("input_output", cache=True).get_authorization_data()
|
|
234
|
-
data[self.name] = authorization_data.get(self.config("created_by_source_key"), "N/A")
|
|
235
|
-
if "setable" in self.configuration:
|
|
236
|
-
setable = self.configuration["setable"]
|
|
237
|
-
if callable(setable):
|
|
238
|
-
data[self.name] = self.di.call_function(setable, data=data, model=model)
|
|
239
|
-
else:
|
|
240
|
-
data[self.name] = setable
|
|
241
|
-
if not model.exists and "default" in self.configuration and self.name not in data:
|
|
242
|
-
data[self.name] = self.configuration["default"]
|
|
243
|
-
return data
|
|
244
|
-
|
|
245
|
-
def post_save(self, data, model, id):
|
|
246
|
-
"""
|
|
247
|
-
Make any changes needed after saving to the database
|
|
248
|
-
|
|
249
|
-
data is the data being saved and id is the id of the record. Note that while the database is updated
|
|
250
|
-
before this is called, the model isn't, so there will be a difference between what is in the database
|
|
251
|
-
and what is in the object.
|
|
252
|
-
"""
|
|
253
|
-
return data
|
|
254
|
-
|
|
255
|
-
def save_finished(self, model):
|
|
256
|
-
"""
|
|
257
|
-
Make any necessary changes needed after a save has completely finished.
|
|
258
|
-
|
|
259
|
-
This is typically used for configurable triggers set by the developer. Column-specific behavior
|
|
260
|
-
that needs to always happen is placed in pre_save or post_save because those affect the save
|
|
261
|
-
process itself.
|
|
262
|
-
"""
|
|
263
|
-
on_change_actions = self.config("on_change", silent=True)
|
|
264
|
-
if on_change_actions and model.was_changed(self.name):
|
|
265
|
-
self.execute_actions(on_change_actions, model)
|
|
266
|
-
|
|
267
|
-
def values_match(self, value_1, value_2):
|
|
268
|
-
"""
|
|
269
|
-
Compares two values to see if they are the same
|
|
270
|
-
"""
|
|
271
|
-
return value_1 == value_2
|
|
272
|
-
|
|
273
|
-
def pre_delete(self, model):
|
|
274
|
-
"""
|
|
275
|
-
Make any changes needed to the data before starting the delete process
|
|
276
|
-
"""
|
|
277
|
-
pass
|
|
278
|
-
|
|
279
|
-
def post_delete(self, model):
|
|
280
|
-
"""
|
|
281
|
-
Make any changes needed to the data before finishing the delete process
|
|
282
|
-
"""
|
|
283
|
-
pass
|
|
284
|
-
|
|
285
|
-
def can_provide(self, column_name):
|
|
286
|
-
"""
|
|
287
|
-
This works together with self.provide to load ancillary data
|
|
288
|
-
|
|
289
|
-
For instance, a foreign key will have an "id" column such as `user_id` but it can also load up
|
|
290
|
-
the user model, which you expect to happen by requesting `model.user`. If a model receives a
|
|
291
|
-
request for a column name that it doesn't recognize, it will loop through all the columns and
|
|
292
|
-
call `can_provide` with the column name. We then have to return True or False to denote whether
|
|
293
|
-
or not we can provide the thing being requested. If we return True then the model will then
|
|
294
|
-
call `column.provide` with the data from the model and the requested column name
|
|
295
|
-
"""
|
|
296
|
-
return False
|
|
297
|
-
|
|
298
|
-
def provide(self, data, column_name):
|
|
299
|
-
"""
|
|
300
|
-
This is called if the column declares that it can provide something, and should return the value
|
|
301
|
-
|
|
302
|
-
See can_provide for more details on the flow here
|
|
303
|
-
"""
|
|
304
|
-
pass
|
|
305
|
-
|
|
306
|
-
def execute_actions(self, actions, model):
|
|
307
|
-
for action in actions:
|
|
308
|
-
if type(action) == binding_config.BindingConfig:
|
|
309
|
-
action = self.di.build(action)
|
|
310
|
-
elif inspect.isclass(action):
|
|
311
|
-
action = self.di.build(action)
|
|
312
|
-
if hasattr(action, "__call__"):
|
|
313
|
-
self.di.call_function(action.__call__, model=model)
|
|
314
|
-
else:
|
|
315
|
-
self.di.call_function(action.__call__, model=model)
|
|
316
|
-
|
|
317
|
-
def add_search(self, models, value, operator=None, relationship_reference=None):
|
|
318
|
-
return models.where(self.build_condition(value, operator=operator))
|
|
319
|
-
|
|
320
|
-
def build_condition(self, value, operator=None, column_prefix=""):
|
|
321
|
-
"""
|
|
322
|
-
This is called by the read (and related) handlers to turn user input into a condition.
|
|
323
|
-
|
|
324
|
-
Note that this may look like it is vulnerable to SQLi, but it isn't. These conditions aren't passed directly
|
|
325
|
-
into a query. Rather, they are parsed by the condition parser before being sent into the backend.
|
|
326
|
-
The condition parser can safely reconstruct the original pieces, and the backend can then use the data
|
|
327
|
-
safely (and remember, the backend may not be an SQL anyway)
|
|
328
|
-
|
|
329
|
-
As a result, this is perfectly safe for any user input, assuming normal system flow.
|
|
330
|
-
"""
|
|
331
|
-
return f"{column_prefix}{self.name}={value}"
|
|
332
|
-
|
|
333
|
-
def is_allowed_operator(self, operator, relationship_reference=None):
|
|
334
|
-
"""
|
|
335
|
-
This is called when processing user data to decide if the end-user is specifying an allowed operator
|
|
336
|
-
"""
|
|
337
|
-
return operator == "="
|
|
338
|
-
|
|
339
|
-
def configure_n_plus_one(self, models):
|
|
340
|
-
return models
|
|
341
|
-
|
|
342
|
-
def check_search_value(self, value, operator=None, relationship_reference=None):
|
|
343
|
-
return self.input_error_for_value(value, operator=operator)
|
|
344
|
-
|
|
345
|
-
def input_error_for_value(self, value, operator=None):
|
|
346
|
-
return ""
|
|
347
|
-
|
|
348
|
-
def where_for_request(self, models, routing_data, authorization_data, input_output):
|
|
349
|
-
"""
|
|
350
|
-
A hook to automatically apply filtering whenever the column makes an appearance in a get/update/list/search handler.
|
|
351
|
-
"""
|
|
352
|
-
return models
|
|
353
|
-
|
|
354
|
-
def validate_models_class(self, models_class, config_name="parent_models_class"):
|
|
355
|
-
if not hasattr(models_class, "model_class"):
|
|
356
|
-
if hasattr(models_class, "columns_configuration"):
|
|
357
|
-
raise ValueError(
|
|
358
|
-
f"'{config_name}' in configuration for column '{self.name}' in model class "
|
|
359
|
-
+ f"'{self.model_class.__name__}' appears to be a Model class, but it should be a Models class"
|
|
360
|
-
)
|
|
361
|
-
else:
|
|
362
|
-
raise ValueError(
|
|
363
|
-
f"'{config_name}' in configuration for column '{self.name}' should be a Models class, "
|
|
364
|
-
+ f"but it appears to be something unknown."
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
def camel_to_nice(self, string):
|
|
368
|
-
string = re.sub("(.)([A-Z][a-z]+)", r"\1 \2", string)
|
|
369
|
-
string = re.sub("([a-z0-9])([A-Z])", r"\1 \2", string).lower()
|
|
370
|
-
return string
|
|
371
|
-
|
|
372
|
-
def documentation(self, name=None, example=None, value=None):
|
|
373
|
-
return self._auto_doc_class(name if name is not None else self.name, example=example, value=value)
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from .datetime import DateTime
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Created(DateTime):
|
|
5
|
-
my_configs = [
|
|
6
|
-
"date_format",
|
|
7
|
-
"default_date",
|
|
8
|
-
"utc",
|
|
9
|
-
]
|
|
10
|
-
|
|
11
|
-
def __init__(self, di, datetime):
|
|
12
|
-
super().__init__(di)
|
|
13
|
-
self.datetime = datetime
|
|
14
|
-
|
|
15
|
-
@property
|
|
16
|
-
def is_writeable(self):
|
|
17
|
-
return False
|
|
18
|
-
|
|
19
|
-
def pre_save(self, data, model):
|
|
20
|
-
if model.exists:
|
|
21
|
-
return data
|
|
22
|
-
if self.config("utc", silent=True):
|
|
23
|
-
now = self.datetime.datetime.now(self.datetime.timezone.utc)
|
|
24
|
-
else:
|
|
25
|
-
now = self.datetime.datetime.now()
|
|
26
|
-
return {**data, self.name: now}
|