clear-skies 1.22.10__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.22.10.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 +8 -39
- clearskies/authentication/authentication.py +44 -0
- clearskies/authentication/authorization.py +14 -8
- clearskies/authentication/authorization_pass_through.py +14 -10
- 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 +55 -20
- clearskies/backends/api_backend.py +1118 -280
- 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 +115 -4
- clearskies/di/additional_config_auto_import.py +12 -0
- clearskies/di/di.py +714 -125
- 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 -160
- 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 +1874 -193
- 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.22.10.dist-info/METADATA +0 -47
- clear_skies-1.22.10.dist-info/RECORD +0 -213
- 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 -51
- clearskies/backends/api_get_only_backend.py +0 -48
- 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 -103
- clearskies/binding_config.py +0 -16
- clearskies/column_types/__init__.py +0 -203
- clearskies/column_types/audit.py +0 -249
- clearskies/column_types/belongs_to.py +0 -271
- clearskies/column_types/boolean.py +0 -60
- clearskies/column_types/category_tree.py +0 -304
- 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 -109
- clearskies/column_types/datetime_micro.py +0 -13
- clearskies/column_types/email.py +0 -18
- clearskies/column_types/float.py +0 -43
- clearskies/column_types/has_many.py +0 -179
- clearskies/column_types/has_one.py +0 -58
- 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/phone.py +0 -48
- clearskies/column_types/select.py +0 -11
- clearskies/column_types/string.py +0 -24
- clearskies/column_types/timestamp.py +0 -73
- clearskies/column_types/updated.py +0 -26
- clearskies/column_types/updated_micro.py +0 -26
- 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 -151
- 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 -479
- clearskies/handlers/callable.py +0 -191
- 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 -206
- 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 -78
- 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/maximum_value.py +0 -19
- clearskies/input_requirements/minimum_length.py +0 -22
- clearskies/input_requirements/minimum_value.py +0 -19
- 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 -350
- 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.22.10.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,162 +0,0 @@
|
|
|
1
|
-
from .many_to_many import ManyToMany
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class ManyToManyWithData(ManyToMany):
|
|
5
|
-
"""
|
|
6
|
-
Controls a many-to-many relationship where additional data is stored in the pivot table.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
required_configs = [
|
|
10
|
-
"pivot_models_class",
|
|
11
|
-
"related_models_class",
|
|
12
|
-
]
|
|
13
|
-
|
|
14
|
-
my_configs = [
|
|
15
|
-
"foreign_column_name_in_pivot",
|
|
16
|
-
"own_column_name_in_pivot",
|
|
17
|
-
"pivot_table",
|
|
18
|
-
"readable_related_columns",
|
|
19
|
-
"is_readable",
|
|
20
|
-
"setable_columns",
|
|
21
|
-
"persist_unique_lookup_column_to_pivot_table",
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
def __init__(self, di):
|
|
25
|
-
super().__init__(di)
|
|
26
|
-
|
|
27
|
-
@property
|
|
28
|
-
def is_readable(self):
|
|
29
|
-
is_readable = self.config("is_readable", True)
|
|
30
|
-
# default is_readable to False
|
|
31
|
-
return True if (is_readable and is_readable is not None) else False
|
|
32
|
-
|
|
33
|
-
def _check_configuration(self, configuration):
|
|
34
|
-
super()._check_configuration(configuration)
|
|
35
|
-
setable_columns = configuration.get("setable_columns")
|
|
36
|
-
if setable_columns is None:
|
|
37
|
-
return
|
|
38
|
-
pivot_columns = self.di.build(configuration["pivot_models_class"], cache=True).raw_columns_configuration()
|
|
39
|
-
if not hasattr(setable_columns, "__iter__"):
|
|
40
|
-
raise ValueError(
|
|
41
|
-
f"{error_prefix} 'setable_columns' should be None or an iterable "
|
|
42
|
-
+ "with the list of pivot columns that can be set."
|
|
43
|
-
)
|
|
44
|
-
if isinstance(setable_columns, str):
|
|
45
|
-
raise ValueError(
|
|
46
|
-
f"{error_prefix} 'setable_columns' should be None or an iterable "
|
|
47
|
-
+ "with the list of pivot columns that can be set."
|
|
48
|
-
)
|
|
49
|
-
for column_name in setable_columns:
|
|
50
|
-
if column_name not in pivot_columns:
|
|
51
|
-
raise ValueError(
|
|
52
|
-
f"{error_prefix} 'setable_columns' references column named '{column_name}' but this"
|
|
53
|
-
+ "column does not exist in the pivot model class."
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
def _finalize_configuration(self, configuration):
|
|
57
|
-
configuration = super()._finalize_configuration(configuration)
|
|
58
|
-
if "persist_unique_lookup_column_to_pivot_table" not in configuration:
|
|
59
|
-
configuration["persist_unique_lookup_column_to_pivot_table"] = False
|
|
60
|
-
return configuration
|
|
61
|
-
|
|
62
|
-
def post_save(self, data, model, id):
|
|
63
|
-
# if our incoming data is not in the data array or is None, then nothing has been set and we do not want
|
|
64
|
-
# to make any changes
|
|
65
|
-
if self.name not in data or data[self.name] is None:
|
|
66
|
-
return data
|
|
67
|
-
|
|
68
|
-
# figure out what ids need to be created or deleted from the pivot table.
|
|
69
|
-
if not model.exists:
|
|
70
|
-
old_ids = set()
|
|
71
|
-
else:
|
|
72
|
-
old_ids = set(getattr(model, f"{self.name}_ids"))
|
|
73
|
-
|
|
74
|
-
# this is trickier for many-to-many-with-data compared to many-to-many. We're generally
|
|
75
|
-
# expecting data[self.name] to be a list of dictionaries. For each entry, we need to find
|
|
76
|
-
# the corresponding entry in the pivot table to decide if we need to delete, create, or update.
|
|
77
|
-
# However, since we have a dictionary there are a variety of ways that we can connect to
|
|
78
|
-
# an entry in the related table - either related id or any unique column from the related
|
|
79
|
-
# table. Technically we might also specify a pivot id, but we're generally trying to be
|
|
80
|
-
# transparent to those, so let's ignore that one.
|
|
81
|
-
|
|
82
|
-
# unfortunately I'm using related_models and foreign_models interchangeably - this is likely
|
|
83
|
-
# an accident due to the slow inheritence from he belongs to class, to the many to many class,
|
|
84
|
-
# and now this. Keep in mind that "foreign" and "related" refer to the same thing
|
|
85
|
-
foreign_column_name_in_pivot = self.config("foreign_column_name_in_pivot")
|
|
86
|
-
own_column_name_in_pivot = self.config("own_column_name_in_pivot")
|
|
87
|
-
unique_foreign_columns = {
|
|
88
|
-
column.name: column.name for column in self.related_columns.values() if column.is_unique
|
|
89
|
-
}
|
|
90
|
-
related_models = self.related_models
|
|
91
|
-
pivot_models = self.pivot_models
|
|
92
|
-
new_ids = set()
|
|
93
|
-
for pivot_record in data[self.name]:
|
|
94
|
-
# first we need to identify which foreign column this belongs to.
|
|
95
|
-
foreign_column_id = None
|
|
96
|
-
# if they provide the foreign column id in the pivot data then we're good
|
|
97
|
-
if foreign_column_name_in_pivot in pivot_record:
|
|
98
|
-
foreign_column_id = pivot_record[foreign_column_name_in_pivot]
|
|
99
|
-
elif len(unique_foreign_columns):
|
|
100
|
-
for pivot_column, pivot_value in pivot_record.items():
|
|
101
|
-
if pivot_column not in unique_foreign_columns:
|
|
102
|
-
continue
|
|
103
|
-
foreign_model = related_models.find(f"{pivot_column}={pivot_value}")
|
|
104
|
-
foreign_column_id = foreign_model.id
|
|
105
|
-
if foreign_column_id:
|
|
106
|
-
# remove this column from the data - it was used to lookup the right
|
|
107
|
-
# record, but mostly won't exist in the model, unless we've been instructed
|
|
108
|
-
# to keep it
|
|
109
|
-
if not self.config("persist_unique_lookup_column_to_pivot_table"):
|
|
110
|
-
del pivot_record[pivot_column]
|
|
111
|
-
break
|
|
112
|
-
if not foreign_column_id:
|
|
113
|
-
column_list = "'" + "', '".join([column for column in unique_foreign_columns.key()]) + "'"
|
|
114
|
-
raise ValueError(
|
|
115
|
-
f"Missing data for {self.name}: Unable to match foreign record for a record in the many-to-many relationship: you must provide either '{foreign_column_name_in_pivot}' with the id column for the foreign table, or a value from one of the unique columns: {column_list}"
|
|
116
|
-
)
|
|
117
|
-
pivot_model = (
|
|
118
|
-
pivot_models.where(f"{foreign_column_name_in_pivot}={foreign_column_id}")
|
|
119
|
-
.where(f"{own_column_name_in_pivot}={id}")
|
|
120
|
-
.first()
|
|
121
|
-
)
|
|
122
|
-
new_ids.add(foreign_column_id)
|
|
123
|
-
# this will either update or create accordingly
|
|
124
|
-
pivot_model.save(
|
|
125
|
-
{
|
|
126
|
-
**pivot_record,
|
|
127
|
-
foreign_column_name_in_pivot: foreign_column_id,
|
|
128
|
-
own_column_name_in_pivot: id,
|
|
129
|
-
}
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
# the above took care of isnerting and updating active records. Now we need to delete
|
|
133
|
-
# records that are no longer needed.
|
|
134
|
-
to_delete = old_ids - new_ids
|
|
135
|
-
if to_delete:
|
|
136
|
-
pivot_models = self.pivot_models
|
|
137
|
-
foreign_column_name = self.config("foreign_column_name_in_pivot")
|
|
138
|
-
for model_to_delete in pivot_models.where(
|
|
139
|
-
f"{foreign_column_name} IN (" + ",".join(map(str, to_delete)) + ")"
|
|
140
|
-
):
|
|
141
|
-
model_to_delete.delete()
|
|
142
|
-
|
|
143
|
-
return data
|
|
144
|
-
|
|
145
|
-
def can_provide(self, column_name):
|
|
146
|
-
if column_name == self.name:
|
|
147
|
-
return True
|
|
148
|
-
if column_name == f"{self.name}_ids":
|
|
149
|
-
return True
|
|
150
|
-
if column_name == f"{self.name}_pivots":
|
|
151
|
-
return True
|
|
152
|
-
|
|
153
|
-
def provide(self, data, column_name):
|
|
154
|
-
# the base class handles most of this: returning the list of matching
|
|
155
|
-
# ids or returning the list of related models
|
|
156
|
-
if column_name == self.name or column_name == f"{self.name}_ids":
|
|
157
|
-
return super().provide(data, column_name)
|
|
158
|
-
|
|
159
|
-
# so if we get here then we need to provide the pivot models for this record
|
|
160
|
-
own_column_name_in_pivot = self.config("own_column_name_in_pivot")
|
|
161
|
-
my_id = data[self.config("own_id_column_name")]
|
|
162
|
-
return [model for model in self.pivot_models.where(f"{own_column_name_in_pivot}={my_id}")]
|
clearskies/column_types/phone.py
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
from .string import String
|
|
2
|
-
import re
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class Phone(String):
|
|
6
|
-
my_configs = [
|
|
7
|
-
"usa_only",
|
|
8
|
-
]
|
|
9
|
-
|
|
10
|
-
def __init__(self, di):
|
|
11
|
-
super().__init__(di)
|
|
12
|
-
|
|
13
|
-
def to_backend(self, data):
|
|
14
|
-
if self.name not in data:
|
|
15
|
-
return data
|
|
16
|
-
|
|
17
|
-
# phone numbers are stored as only digits.
|
|
18
|
-
return {**data, **{self.name: re.sub(r"\D", "", data[self.name])}}
|
|
19
|
-
|
|
20
|
-
def input_error_for_value(self, value, operator=None):
|
|
21
|
-
if type(value) != str:
|
|
22
|
-
return f"Value must be a string for {self.name}"
|
|
23
|
-
|
|
24
|
-
# we'll allow spaces, dashes, parenthesis, dashes, and plus signs.
|
|
25
|
-
# if there is anything else then it's not a valid phone number.
|
|
26
|
-
# However, we don't do more detailed validation, because I'm too lazy to
|
|
27
|
-
# figure out what is and is not a valid phone number, especially when
|
|
28
|
-
# you get to the world of international numbers.
|
|
29
|
-
if re.search(r"[^\d \-()+]", value):
|
|
30
|
-
return "Invalid phone number"
|
|
31
|
-
|
|
32
|
-
# for some final validation (especially US numbers) work only with the digits.
|
|
33
|
-
value = re.sub(r"\D", "", value)
|
|
34
|
-
|
|
35
|
-
if len(value) > 15:
|
|
36
|
-
return "Invalid phone number"
|
|
37
|
-
|
|
38
|
-
# we can't be too short unless we're doing a fuzzy search
|
|
39
|
-
if len(value) < 10 and operator and operator.lower() != "like":
|
|
40
|
-
return "Invalid phone number"
|
|
41
|
-
|
|
42
|
-
if self.config("usa_only", silent=True):
|
|
43
|
-
if len(value) > 11:
|
|
44
|
-
return "Invalid phone number"
|
|
45
|
-
if value[0] == "1" and len(value) != 11:
|
|
46
|
-
return "Invalid phone number"
|
|
47
|
-
|
|
48
|
-
return ""
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
from .string import String
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Select(String):
|
|
5
|
-
required_configs = ["values"]
|
|
6
|
-
|
|
7
|
-
def __init__(self, di):
|
|
8
|
-
super().__init__(di)
|
|
9
|
-
|
|
10
|
-
def input_error_for_value(self, value, operator=None):
|
|
11
|
-
return f"Invalid value for {self.name}" if value not in self.config("values") else ""
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
from .column import Column
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class String(Column):
|
|
5
|
-
def __init__(self, di):
|
|
6
|
-
super().__init__(di)
|
|
7
|
-
|
|
8
|
-
def build_condition(self, value, operator=None, column_prefix=""):
|
|
9
|
-
if not operator:
|
|
10
|
-
operator = "="
|
|
11
|
-
if operator.lower() == "like":
|
|
12
|
-
return f"{column_prefix}{self.name} LIKE '%{value}%'"
|
|
13
|
-
return f"{column_prefix}{self.name}{operator}{value}"
|
|
14
|
-
|
|
15
|
-
def is_allowed_operator(self, operator, relationship_reference=None):
|
|
16
|
-
"""
|
|
17
|
-
This is called when processing user data to decide if the end-user is specifying an allowed operator
|
|
18
|
-
"""
|
|
19
|
-
if operator in ["=", "<", ">", "<=", ">=", "in"]:
|
|
20
|
-
return True
|
|
21
|
-
return operator.lower() == "like"
|
|
22
|
-
|
|
23
|
-
def input_error_for_value(self, value, operator=None):
|
|
24
|
-
return "value should be a string" if type(value) != str else ""
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
from .datetime import DateTime
|
|
3
|
-
from datetime import datetime, timezone
|
|
4
|
-
import dateparser
|
|
5
|
-
from ..autodoc.schema import DateTime as AutoDocDateTime
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class Timestamp(DateTime):
|
|
9
|
-
my_configs = [
|
|
10
|
-
"date_format",
|
|
11
|
-
"milliseconds",
|
|
12
|
-
]
|
|
13
|
-
|
|
14
|
-
def _finalize_configuration(self, configuration):
|
|
15
|
-
return {
|
|
16
|
-
**{
|
|
17
|
-
"date_format": self._date_format,
|
|
18
|
-
"milliseconds": False,
|
|
19
|
-
},
|
|
20
|
-
**super()._finalize_configuration(configuration),
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
def from_backend(self, value):
|
|
24
|
-
mult = 1000 if self.config("milliseconds") else 1
|
|
25
|
-
if not value:
|
|
26
|
-
date = None
|
|
27
|
-
elif isinstance(value, str):
|
|
28
|
-
if not value.isdigit():
|
|
29
|
-
raise ValueError(
|
|
30
|
-
f"Invalid data was found in the backend for model {self.model_class.__name__} and column {self.name}: a string value was found that is not a timestamp. It was '{value}'"
|
|
31
|
-
)
|
|
32
|
-
date = datetime.fromtimestamp(int(value) / mult, self._timezone)
|
|
33
|
-
elif isinstance(value, int):
|
|
34
|
-
date = datetime.fromtimestamp(value / mult, self._timezone)
|
|
35
|
-
else:
|
|
36
|
-
if not isinstance(value, datetime):
|
|
37
|
-
raise ValueError(
|
|
38
|
-
f"Invalid data was found in the backend for model {self.model_class.__name__} and column {self.name}: the value was neither an integer, a string, nor a datetime object"
|
|
39
|
-
)
|
|
40
|
-
date = value
|
|
41
|
-
return date.replace(tzinfo=self._timezone) if date else None
|
|
42
|
-
|
|
43
|
-
def to_backend(self, data):
|
|
44
|
-
if not self.name in data or isinstance(data[self.name], int) or data[self.name] == None:
|
|
45
|
-
return data
|
|
46
|
-
|
|
47
|
-
value = data[self.name]
|
|
48
|
-
if isinstance(value, str):
|
|
49
|
-
if not value.isdigit():
|
|
50
|
-
raise ValueError(
|
|
51
|
-
f"Invalid data was sent to the backend for model {self.model_class.__name__} and column {self.name}: a string value was found that is not a timestamp. It was '{value}'"
|
|
52
|
-
)
|
|
53
|
-
value = int(value)
|
|
54
|
-
elif isinstance(value, datetime):
|
|
55
|
-
value = value.timestamp()
|
|
56
|
-
else:
|
|
57
|
-
raise ValueError(
|
|
58
|
-
f"Invalid data was sent to the backend for model {self.model_class.__name__} and column {self.name}: the value was neither an integer, a string, nor a datetime object"
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
# hopefully this is a Python datetime object in UTC timezone...
|
|
62
|
-
return {**data, **{self.name: value}}
|
|
63
|
-
|
|
64
|
-
def input_error_for_value(self, value, operator=None):
|
|
65
|
-
if not isinstance(value, int):
|
|
66
|
-
return f"'{self.name}' must be an integer"
|
|
67
|
-
return ""
|
|
68
|
-
|
|
69
|
-
def values_match(self, value_1, value_2):
|
|
70
|
-
"""
|
|
71
|
-
Compares two values to see if they are the same
|
|
72
|
-
"""
|
|
73
|
-
return value_1 == value_2
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
|
|
3
|
-
from .datetime import DateTime
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class Updated(DateTime):
|
|
7
|
-
my_configs = [
|
|
8
|
-
"date_format",
|
|
9
|
-
"default_date",
|
|
10
|
-
"utc",
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
def __init__(self, di, datetime, timezone: datetime.tzinfo):
|
|
14
|
-
super().__init__(di, timezone)
|
|
15
|
-
self.datetime = datetime
|
|
16
|
-
|
|
17
|
-
@property
|
|
18
|
-
def is_writeable(self):
|
|
19
|
-
return False
|
|
20
|
-
|
|
21
|
-
def pre_save(self, data, model):
|
|
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(self._timezone)
|
|
26
|
-
return {**data, self.name: now}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
|
|
3
|
-
from .datetime_micro import DateTimeMicro
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class UpdatedMicro(DateTimeMicro):
|
|
7
|
-
my_configs = [
|
|
8
|
-
"date_format",
|
|
9
|
-
"default_date",
|
|
10
|
-
"utc",
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
def __init__(self, di, datetime, timezone: datetime.tzinfo):
|
|
14
|
-
super().__init__(di, timezone)
|
|
15
|
-
self.datetime = datetime
|
|
16
|
-
|
|
17
|
-
@property
|
|
18
|
-
def is_writeable(self):
|
|
19
|
-
return False
|
|
20
|
-
|
|
21
|
-
def pre_save(self, data, model):
|
|
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(self._timezone)
|
|
26
|
-
return {**data, self.name: now}
|
clearskies/column_types/uuid.py
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
from .string import String
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class UUID(String):
|
|
5
|
-
def __init__(self, di, uuid):
|
|
6
|
-
super().__init__(di)
|
|
7
|
-
self.uuid = uuid
|
|
8
|
-
|
|
9
|
-
@property
|
|
10
|
-
def is_writeable(self):
|
|
11
|
-
return False
|
|
12
|
-
|
|
13
|
-
def build_condition(self, value, operator=None, column_prefix=""):
|
|
14
|
-
return f"{column_prefix}{self.name}={value}"
|
|
15
|
-
|
|
16
|
-
def is_allowed_operator(self, operator, relationship_reference=None):
|
|
17
|
-
"""
|
|
18
|
-
This is called when processing user data to decide if the end-user is specifying an allowed operator
|
|
19
|
-
"""
|
|
20
|
-
return operator == "="
|
|
21
|
-
|
|
22
|
-
def pre_save(self, data, model):
|
|
23
|
-
if model.exists:
|
|
24
|
-
return data
|
|
25
|
-
return {**data, self.name: str(self.uuid.uuid4())}
|
clearskies/columns.py
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
from collections import OrderedDict
|
|
2
|
-
from collections.abc import Sequence
|
|
3
|
-
import inspect
|
|
4
|
-
from .binding_config import BindingConfig
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Columns:
|
|
8
|
-
def __init__(self, di):
|
|
9
|
-
self.di = di
|
|
10
|
-
|
|
11
|
-
def configure(self, definitions, model_class, overrides=None):
|
|
12
|
-
columns = OrderedDict()
|
|
13
|
-
for name, configuration in definitions.items():
|
|
14
|
-
name = name.strip()
|
|
15
|
-
if not name:
|
|
16
|
-
raise ValueError(f"Missing name for column in '{model_class.__name__}'")
|
|
17
|
-
if name in columns:
|
|
18
|
-
raise ValueError(f"Duplicate column '{name}' found for model '{model_class.__name__}'")
|
|
19
|
-
column_overrides = overrides[name] if (overrides is not None and name in overrides) else {}
|
|
20
|
-
# if the overrides changes the class then we need to completely replace the column definition
|
|
21
|
-
# with what is in the overrides.
|
|
22
|
-
if "class" in column_overrides and id(column_overrides["class"]) != id(configuration["class"]):
|
|
23
|
-
configuration = column_overrides
|
|
24
|
-
else:
|
|
25
|
-
configuration = {
|
|
26
|
-
**configuration,
|
|
27
|
-
**column_overrides,
|
|
28
|
-
"input_requirements": self._resolve_input_requirements(
|
|
29
|
-
self._merge_input_requirements(
|
|
30
|
-
configuration.get("input_requirements"),
|
|
31
|
-
column_overrides.get("input_requirements"),
|
|
32
|
-
),
|
|
33
|
-
name,
|
|
34
|
-
model_class.__name__,
|
|
35
|
-
),
|
|
36
|
-
}
|
|
37
|
-
columns[name] = self.build_column(name, configuration, model_class)
|
|
38
|
-
|
|
39
|
-
# overrides can add columns too - need to handle those separately
|
|
40
|
-
if overrides is not None:
|
|
41
|
-
for name, configuration in overrides.items():
|
|
42
|
-
if name in columns:
|
|
43
|
-
continue
|
|
44
|
-
configuration["input_requirements"] = (
|
|
45
|
-
self._resolve_input_requirements(configuration["input_requirements"], name, model_class.__name__)
|
|
46
|
-
if "input_requirements" in configuration
|
|
47
|
-
else []
|
|
48
|
-
)
|
|
49
|
-
columns[name] = self.build_column(name, configuration, model_class)
|
|
50
|
-
|
|
51
|
-
return columns
|
|
52
|
-
|
|
53
|
-
def build_column(self, name, configuration, model_class):
|
|
54
|
-
if not "class" in configuration:
|
|
55
|
-
raise ValueError(f"Missing column class for column {name} in {model_class.__name__}")
|
|
56
|
-
column = self.di.build(configuration["class"], cache=False)
|
|
57
|
-
column.configure(name, configuration, model_class)
|
|
58
|
-
return column
|
|
59
|
-
|
|
60
|
-
def _merge_input_requirements(self, config_requirements, override_requirements):
|
|
61
|
-
if config_requirements is None and override_requirements is None:
|
|
62
|
-
return []
|
|
63
|
-
if config_requirements is None:
|
|
64
|
-
return override_requirements
|
|
65
|
-
if override_requirements is None:
|
|
66
|
-
return config_requirements
|
|
67
|
-
|
|
68
|
-
# if we have more than one of the same class then use the one from the overrides
|
|
69
|
-
requirements = []
|
|
70
|
-
used_classes = {}
|
|
71
|
-
for requirement in override_requirements:
|
|
72
|
-
requirements.append(requirement)
|
|
73
|
-
[requirement_class, args, kwargs] = self._input_requirement_args_and_class(requirement)
|
|
74
|
-
used_classes[requirement_class.__name__] = True
|
|
75
|
-
for requirement in config_requirements:
|
|
76
|
-
[requirement_class, args, kwargs] = self._input_requirement_args_and_class(requirement)
|
|
77
|
-
if requirement_class.__name__ in used_classes:
|
|
78
|
-
continue
|
|
79
|
-
requirements.append(requirement)
|
|
80
|
-
used_classes[requirement_class.__name__] = True
|
|
81
|
-
|
|
82
|
-
return requirements
|
|
83
|
-
|
|
84
|
-
def _input_requirement_args_and_class(self, requirement):
|
|
85
|
-
"""
|
|
86
|
-
This takes the input requirement data provided by the developer and returns the things we need to build it.
|
|
87
|
-
|
|
88
|
-
An input requirement can be:
|
|
89
|
-
|
|
90
|
-
1. An InputRequirement class (aka `Required`)
|
|
91
|
-
2. A tuple with the class and then configuration parameters for the class (aka `(MaxLength, 255)`)
|
|
92
|
-
3. A BindingConfig
|
|
93
|
-
|
|
94
|
-
This normalizes these three options and returns a list with `[Class, [args], {kwargs}]` for building
|
|
95
|
-
"""
|
|
96
|
-
if inspect.isclass(requirement):
|
|
97
|
-
return [requirement, [], {}]
|
|
98
|
-
elif isinstance(requirement, BindingConfig):
|
|
99
|
-
return [requirement.object_class, requirement.args, requirement.kwargs]
|
|
100
|
-
elif isinstance(requirement, Sequence) and type(requirement) != str:
|
|
101
|
-
if not inspect.isclass(requirement[0]):
|
|
102
|
-
raise ValueError(
|
|
103
|
-
f"{error_prefix} incorrect value for input_requirement. First element should "
|
|
104
|
-
+ f"be the Requirement class, but instead {type(requirement[0])} was found"
|
|
105
|
-
)
|
|
106
|
-
return [requirement[0], requirement[1:], {}]
|
|
107
|
-
else:
|
|
108
|
-
raise ValueError("Unrecognized value for input_requirement")
|
|
109
|
-
|
|
110
|
-
def _resolve_input_requirements(self, input_requirements, column_name, model_class_name):
|
|
111
|
-
error_prefix = f"Configuration error for column '{column_name}' in model '{model_class_name}':"
|
|
112
|
-
if not hasattr(input_requirements, "__iter__"):
|
|
113
|
-
raise ValueError(
|
|
114
|
-
f"{error_prefix} 'input_requirements' should be an iterable but is {type(input_requirements)}"
|
|
115
|
-
)
|
|
116
|
-
resolved_requirements = []
|
|
117
|
-
for requirement in input_requirements:
|
|
118
|
-
[requirement_class, args, kwargs] = self._input_requirement_args_and_class(requirement)
|
|
119
|
-
requirement_instance = self.di.build(requirement_class, cache=False)
|
|
120
|
-
requirement_instance.column_name = column_name
|
|
121
|
-
requirement_instance.configure(*args, **kwargs)
|
|
122
|
-
resolved_requirements.append(requirement_instance)
|
|
123
|
-
return resolved_requirements
|