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,265 +1,318 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
from
|
|
4
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from clearskies.autodoc.schema import Integer as AutoDocInteger
|
|
7
|
+
from clearskies.autodoc.schema import Schema as AutoDocSchema
|
|
8
|
+
from clearskies.backends.backend import Backend
|
|
9
|
+
from clearskies.di import InjectableProperties, inject
|
|
10
|
+
from clearskies.query import Condition, Query
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from clearskies import Model
|
|
14
|
+
from clearskies.di import Di
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CursorBackend(Backend, InjectableProperties):
|
|
18
|
+
"""
|
|
19
|
+
The cursor backend connects your models to a MySQL or MariaDB database.
|
|
20
|
+
|
|
21
|
+
## Installing Dependencies
|
|
22
|
+
|
|
23
|
+
clearskies uses PyMySQL to manage the database connection and make queries. This is not installed by default,
|
|
24
|
+
but is a named extra that you can install when needed via:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install clear-skies[mysql]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Connecting to your server
|
|
31
|
+
|
|
32
|
+
By default, database credentials are expected in environment variables:
|
|
33
|
+
|
|
34
|
+
| Name | Default | Value |
|
|
35
|
+
|-------------|---------|---------------------------------------------------------------|
|
|
36
|
+
| db_host | | The hostname where the database can be found |
|
|
37
|
+
| db_username | | The username to connect as |
|
|
38
|
+
| db_password | | The password to connect with |
|
|
39
|
+
| db_database | | The name of the database to use |
|
|
40
|
+
| db_port | 3306 | The network port to connect to |
|
|
41
|
+
| db_ssl_ca | | Path to a certificate to use: enables SSL over the connection |
|
|
42
|
+
|
|
43
|
+
However, you can fully control the credential provisioning process by declaring a dependency named `connection_details` and
|
|
44
|
+
setting it to a dictionary with the above keys, minus the `db_` prefix:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
class ConnectionDetails(clearskies.di.AdditionalConfig):
|
|
48
|
+
provide_connection_details(self, secrets):
|
|
49
|
+
return {
|
|
50
|
+
"host": secrets.get("database_host"),
|
|
51
|
+
"username": secrets.get("db_username"),
|
|
52
|
+
"password": secrets.get("db_password"),
|
|
53
|
+
"database": secrets.get("db_database"),
|
|
54
|
+
"port": 3306,
|
|
55
|
+
"ssl_ca": "/path/to/ca",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
wsgi = clearskies.contexts.Wsgi(
|
|
59
|
+
some_application,
|
|
60
|
+
additional_configs=[ConnectionDetails()],
|
|
61
|
+
bindings={
|
|
62
|
+
"secrets": "" # some configuration here to point to your secret manager
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Similarly, some alternate credential provisioning schemes are built into clearskies. See the
|
|
68
|
+
clearskies.secrets.additional_configs module for those options.
|
|
69
|
+
|
|
70
|
+
## Connecting models to tables
|
|
71
|
+
|
|
72
|
+
The table name for your model comes from calling the `destination_name` class method of the model class. By
|
|
73
|
+
default, this takes the class name, converts it to snake case, and then pluralizes it. So, if you have a model
|
|
74
|
+
class named `UserPreference` then the cursor backend will look for a table called `user_preferences`. If this
|
|
75
|
+
isn't what you want, then you can simply override `destination_name` to return whatever table you want:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
class UserPreference(clearskies.Model):
|
|
79
|
+
@classmethod
|
|
80
|
+
def destination_name(cls):
|
|
81
|
+
return "some_other_table_name"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Additionally, the cursor backend accepts an argument called `table_prefix` which, if provided, will be prefixed
|
|
85
|
+
to your table name. Finally, you can declare a dependency called `global_table_prefix` which will automatically
|
|
86
|
+
be added to every table name. In the following example, the table name will be `user_configuration_preferences`
|
|
87
|
+
due to:
|
|
88
|
+
|
|
89
|
+
1. The `destination_name` method sets the table name to `preferences`
|
|
90
|
+
2. The `table_prefix` argument to the CursorBackend constructor adds a prefix of `configuration_`
|
|
91
|
+
3. The `global_table_prefix` binding sets a prefix of `user_`, wihch goes before everything else.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import clearskies
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class UserPreference(clearskies.Model):
|
|
98
|
+
id_column_name = "id"
|
|
99
|
+
backend = clearskies.backends.CursorBackend(table_prefix="configuration_")
|
|
100
|
+
id = clearskies.columns.Uuid()
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def destination_name(cls):
|
|
104
|
+
return "preferences"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
cli = clearskies.contexts.Cli(
|
|
108
|
+
clearskies.endpoints.Callable(
|
|
109
|
+
lambda user_preferences: user_preferences.create(no_data=True).id,
|
|
110
|
+
),
|
|
111
|
+
classes=[UserPreference],
|
|
112
|
+
bindings={
|
|
113
|
+
"global_table_prefix": "user_",
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
"""
|
|
5
119
|
|
|
6
|
-
|
|
7
|
-
class CursorBackend(Backend):
|
|
8
120
|
supports_n_plus_one = True
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def __init__(self, cursor):
|
|
29
|
-
self._cursor = cursor
|
|
30
|
-
from .. import ConditionParser
|
|
31
|
-
|
|
32
|
-
self.condition_parser = ConditionParser()
|
|
33
|
-
|
|
34
|
-
def _table_escape_character(self) -> str:
|
|
35
|
-
"""Return the character to use to escape table names in queries."""
|
|
36
|
-
return "`"
|
|
37
|
-
|
|
38
|
-
def _column_escape_character(self) -> str:
|
|
39
|
-
"""Return the character to use to escape column names in queries."""
|
|
40
|
-
return "`"
|
|
41
|
-
|
|
42
|
-
def configure(self):
|
|
43
|
-
pass
|
|
121
|
+
global_table_prefix = inject.ByName("global_table_prefix")
|
|
122
|
+
di: Di = inject.Di() # type: ignore[assignment]
|
|
123
|
+
|
|
124
|
+
def __init__(self, cursor_dependency_name="cursor", table_prefix=""):
|
|
125
|
+
self.cursor_dependency_name = cursor_dependency_name
|
|
126
|
+
self.table_prefix = table_prefix
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def cursor(self):
|
|
130
|
+
"""
|
|
131
|
+
Lazily inject and return the database cursor instance.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
The cursor object used for executing database queries.
|
|
136
|
+
"""
|
|
137
|
+
if not hasattr(self, "_cursor"):
|
|
138
|
+
self._cursor = self.di.build(self.cursor_dependency_name)
|
|
139
|
+
return self._cursor
|
|
44
140
|
|
|
45
141
|
def _finalize_table_name(self, table_name):
|
|
46
|
-
|
|
142
|
+
table_name = f"{self.global_table_prefix}{self.table_prefix}{table_name}"
|
|
47
143
|
if "." not in table_name:
|
|
48
|
-
return f"{
|
|
49
|
-
return
|
|
144
|
+
return f"{self.cursor.table_escape_character}{table_name}{self.cursor.table_escape_character}"
|
|
145
|
+
return (
|
|
146
|
+
self.cursor.table_escape_character
|
|
147
|
+
+ f"{self.cursor.table_escape_character}.{self.cursor.table_escape_character}".join(table_name.split("."))
|
|
148
|
+
+ self.cursor.table_escape_character
|
|
149
|
+
)
|
|
50
150
|
|
|
51
|
-
def update(self, id, data, model):
|
|
151
|
+
def update(self, id: int | str, data: dict[str, Any], model: Model) -> dict[str, Any]:
|
|
52
152
|
query_parts = []
|
|
53
153
|
parameters = []
|
|
54
|
-
escape = self._column_escape_character()
|
|
55
154
|
for key, val in data.items():
|
|
56
|
-
query_parts.append(
|
|
155
|
+
query_parts.append(self.cursor.column_equals_with_placeholder(key))
|
|
57
156
|
parameters.append(val)
|
|
58
157
|
updates = ", ".join(query_parts)
|
|
59
158
|
|
|
60
|
-
|
|
61
|
-
self.
|
|
62
|
-
|
|
63
|
-
)
|
|
159
|
+
# update the record
|
|
160
|
+
table_name = self._finalize_table_name(model.destination_name())
|
|
161
|
+
id_equals = self.cursor.column_equals_with_placeholder(model.id_column_name)
|
|
162
|
+
self.cursor.execute(f"UPDATE {table_name} SET {updates} WHERE {id_equals}", tuple([*parameters, id]))
|
|
64
163
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"table_name": model.table_name(),
|
|
68
|
-
"select_all": True,
|
|
69
|
-
"wheres": [
|
|
70
|
-
{
|
|
71
|
-
"column": model.id_column_name,
|
|
72
|
-
"operator": "=",
|
|
73
|
-
"parsed": f"{model.id_column_name}=%s",
|
|
74
|
-
"values": [id],
|
|
75
|
-
}
|
|
76
|
-
],
|
|
77
|
-
},
|
|
78
|
-
model,
|
|
79
|
-
)
|
|
80
|
-
return results[0]
|
|
164
|
+
# and now query again to fetch the updated record.
|
|
165
|
+
return self.records(Query(model.__class__, conditions=[Condition(f"{model.id_column_name}={id}")]))[0]
|
|
81
166
|
|
|
82
|
-
def create(self, data, model):
|
|
83
|
-
escape = self.
|
|
167
|
+
def create(self, data: dict[str, Any], model: Model) -> dict[str, Any]:
|
|
168
|
+
escape = self.cursor.column_escape_character
|
|
84
169
|
columns = escape + f"{escape}, {escape}".join(data.keys()) + escape
|
|
85
|
-
placeholders = ", ".join([
|
|
170
|
+
placeholders = ", ".join([self.cursor.value_placeholder for i in range(len(data))])
|
|
86
171
|
|
|
87
|
-
table_name = self._finalize_table_name(model.
|
|
88
|
-
self.
|
|
172
|
+
table_name = self._finalize_table_name(model.destination_name())
|
|
173
|
+
self.cursor.execute(f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})", tuple(data.values()))
|
|
89
174
|
new_id = data.get(model.id_column_name)
|
|
90
175
|
if not new_id:
|
|
91
|
-
new_id = self.
|
|
176
|
+
new_id = self.cursor.lastrowid
|
|
92
177
|
if not new_id:
|
|
93
178
|
raise ValueError("I can't figure out what the id is for a newly created record :(")
|
|
94
179
|
|
|
95
|
-
|
|
96
|
-
{
|
|
97
|
-
"table_name": model.table_name(),
|
|
98
|
-
"select_all": True,
|
|
99
|
-
"wheres": [
|
|
100
|
-
{
|
|
101
|
-
"column": model.id_column_name,
|
|
102
|
-
"operator": "=",
|
|
103
|
-
"parsed": f"{model.id_column_name}=%s",
|
|
104
|
-
"values": [new_id],
|
|
105
|
-
}
|
|
106
|
-
],
|
|
107
|
-
},
|
|
108
|
-
model,
|
|
109
|
-
)
|
|
110
|
-
return results[0]
|
|
180
|
+
return self.records(Query(model.__class__, conditions=[Condition(f"{model.id_column_name}={new_id}")]))[0]
|
|
111
181
|
|
|
112
|
-
def delete(self, id, model):
|
|
113
|
-
table_name = self._finalize_table_name(model.
|
|
114
|
-
self.
|
|
182
|
+
def delete(self, id: int | str, model: Model) -> bool:
|
|
183
|
+
table_name = self._finalize_table_name(model.destination_name())
|
|
184
|
+
id_equals = self.cursor.column_equals_with_placeholder(model.id_column_name)
|
|
185
|
+
self.cursor.execute(f"DELETE FROM {table_name} WHERE {id_equals}", (id,))
|
|
115
186
|
return True
|
|
116
187
|
|
|
117
|
-
def count(self,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
self.
|
|
121
|
-
for row in self._cursor:
|
|
188
|
+
def count(self, query: Query) -> int:
|
|
189
|
+
(sql, parameters) = self.as_count_sql(query)
|
|
190
|
+
self.cursor.execute(sql, parameters)
|
|
191
|
+
for row in self.cursor:
|
|
122
192
|
return row[0] if type(row) == tuple else row["count"]
|
|
123
193
|
return 0
|
|
124
194
|
|
|
125
|
-
def records(
|
|
126
|
-
self, configuration: Dict[str, Any], model: model.Model, next_page_data: Dict[str, str] = None
|
|
127
|
-
) -> List[Dict[str, Any]]:
|
|
195
|
+
def records(self, query: Query, next_page_data: dict[str, str | int] | None = None) -> list[dict[str, Any]]:
|
|
128
196
|
# I was going to get fancy and have this return an iterator, but since I'm going to load up
|
|
129
197
|
# everything into a list anyway, I may as well just return the list, right?
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
self.
|
|
133
|
-
records = [row for row in self._cursor]
|
|
198
|
+
(sql, parameters) = self.as_sql(query)
|
|
199
|
+
self.cursor.execute(sql, parameters)
|
|
200
|
+
records = [row for row in self.cursor]
|
|
134
201
|
if type(next_page_data) == dict:
|
|
135
|
-
limit =
|
|
136
|
-
start =
|
|
202
|
+
limit = query.limit
|
|
203
|
+
start = query.pagination.get("start", 0)
|
|
137
204
|
if limit and len(records) == limit:
|
|
138
205
|
next_page_data["start"] = int(start) + int(limit)
|
|
139
206
|
return records
|
|
140
207
|
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return f" GROUP BY {escape}{group_by}{escape}"
|
|
147
|
-
parts = group_by.split(".", 1)
|
|
148
|
-
table = parts[0]
|
|
149
|
-
column = parts[1]
|
|
150
|
-
return f" GROUP BY {escape}{table}{escape}.{escape}{column}{escape}"
|
|
151
|
-
|
|
152
|
-
def as_sql(self, configuration):
|
|
153
|
-
escape = self._column_escape_character()
|
|
154
|
-
[wheres, parameters] = self._conditions_as_wheres_and_parameters(
|
|
155
|
-
configuration["wheres"], configuration["table_name"]
|
|
208
|
+
def as_sql(self, query: Query) -> tuple[str, tuple[Any]]:
|
|
209
|
+
escape = self.cursor.column_escape_character
|
|
210
|
+
table_name = query.model_class.destination_name()
|
|
211
|
+
(wheres, parameters) = self.conditions_as_wheres_and_parameters(
|
|
212
|
+
query.conditions, query.model_class.destination_name()
|
|
156
213
|
)
|
|
157
214
|
select_parts = []
|
|
158
|
-
if
|
|
159
|
-
select_parts.append(self._finalize_table_name(
|
|
160
|
-
if
|
|
161
|
-
select_parts.extend(
|
|
215
|
+
if query.select_all:
|
|
216
|
+
select_parts.append(self._finalize_table_name(table_name) + ".*")
|
|
217
|
+
if query.selects:
|
|
218
|
+
select_parts.extend(query.selects)
|
|
162
219
|
select = ", ".join(select_parts)
|
|
163
|
-
if
|
|
164
|
-
joins = " " + " ".join([join
|
|
220
|
+
if query.joins:
|
|
221
|
+
joins = " " + " ".join([join._raw_join for join in query.joins])
|
|
165
222
|
else:
|
|
166
223
|
joins = ""
|
|
167
|
-
if
|
|
224
|
+
if query.sorts:
|
|
168
225
|
sort_parts = []
|
|
169
|
-
for sort in
|
|
170
|
-
table_name = sort.
|
|
171
|
-
column_name = sort
|
|
172
|
-
direction = sort
|
|
226
|
+
for sort in query.sorts:
|
|
227
|
+
table_name = sort.table_name
|
|
228
|
+
column_name = sort.column_name
|
|
229
|
+
direction = sort.direction
|
|
173
230
|
prefix = self._finalize_table_name(table_name) + "." if table_name else ""
|
|
174
231
|
sort_parts.append(f"{prefix}{escape}{column_name}{escape} {direction}")
|
|
175
232
|
order_by = " ORDER BY " + ", ".join(sort_parts)
|
|
176
233
|
else:
|
|
177
234
|
order_by = ""
|
|
178
|
-
group_by = self.group_by_clause(
|
|
235
|
+
group_by = self.group_by_clause(query.group_by)
|
|
179
236
|
limit = ""
|
|
180
|
-
if
|
|
237
|
+
if query.limit:
|
|
181
238
|
start = 0
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
239
|
+
limit_size = int(query.limit)
|
|
240
|
+
if "start" in query.pagination:
|
|
241
|
+
start = int(query.pagination["start"])
|
|
242
|
+
limit = f" LIMIT {start}, {limit_size}"
|
|
185
243
|
|
|
186
|
-
table_name = self._finalize_table_name(
|
|
187
|
-
return
|
|
244
|
+
table_name = self._finalize_table_name(table_name)
|
|
245
|
+
return (
|
|
188
246
|
f"SELECT {select} FROM {table_name}{joins}{wheres}{group_by}{order_by}{limit}".strip(),
|
|
189
247
|
parameters,
|
|
190
|
-
|
|
248
|
+
)
|
|
191
249
|
|
|
192
|
-
def as_count_sql(self,
|
|
193
|
-
escape = self.
|
|
250
|
+
def as_count_sql(self, query: Query) -> tuple[str, tuple[Any]]:
|
|
251
|
+
escape = self.cursor.column_escape_character
|
|
194
252
|
# note that this won't work if we start including a HAVING clause
|
|
195
|
-
|
|
196
|
-
|
|
253
|
+
(wheres, parameters) = self.conditions_as_wheres_and_parameters(
|
|
254
|
+
query.conditions, query.model_class.destination_name()
|
|
197
255
|
)
|
|
198
256
|
# we also don't currently support parameters in the join clause - I'll probably need that though
|
|
199
|
-
if
|
|
257
|
+
if query.joins:
|
|
200
258
|
# We can ignore left joins because they don't change the count
|
|
201
|
-
join_sections = filter(lambda join: join
|
|
202
|
-
joins = " " + " ".join([join
|
|
259
|
+
join_sections = filter(lambda join: join.type != "LEFT", query.joins) # type: ignore
|
|
260
|
+
joins = " " + " ".join([join._raw_join for join in join_sections])
|
|
203
261
|
else:
|
|
204
262
|
joins = ""
|
|
205
|
-
table_name = self._finalize_table_name(
|
|
206
|
-
if not
|
|
207
|
-
|
|
263
|
+
table_name = self._finalize_table_name(query.model_class.destination_name())
|
|
264
|
+
if not query.group_by:
|
|
265
|
+
query_string = f"SELECT COUNT(*) AS count FROM {table_name}{joins}{wheres}"
|
|
208
266
|
else:
|
|
209
|
-
group_by = self.group_by_clause(
|
|
210
|
-
|
|
267
|
+
group_by = self.group_by_clause(query.group_by)
|
|
268
|
+
query_string = (
|
|
211
269
|
f"SELECT COUNT(*) AS count FROM (SELECT 1 FROM {table_name}{joins}{wheres}{group_by}) AS count_inner"
|
|
212
270
|
)
|
|
213
|
-
return
|
|
271
|
+
return (query_string, parameters)
|
|
214
272
|
|
|
215
|
-
def
|
|
273
|
+
def conditions_as_wheres_and_parameters(
|
|
274
|
+
self, conditions: list[Condition], default_table_name: str
|
|
275
|
+
) -> tuple[str, tuple[Any]]:
|
|
216
276
|
if not conditions:
|
|
217
|
-
return
|
|
277
|
+
return ("", ()) # type: ignore
|
|
218
278
|
|
|
219
279
|
parameters = []
|
|
220
280
|
where_parts = []
|
|
221
281
|
for condition in conditions:
|
|
222
|
-
parameters.extend(condition
|
|
223
|
-
table = condition.
|
|
224
|
-
|
|
225
|
-
table = default_table_name
|
|
226
|
-
column = condition["column"]
|
|
227
|
-
column_with_table = f"{table}.{column}"
|
|
282
|
+
parameters.extend(condition.values)
|
|
283
|
+
table = condition.table_name if condition.table_name else self._finalize_table_name(default_table_name)
|
|
284
|
+
column = condition.column_name
|
|
228
285
|
where_parts.append(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
condition
|
|
232
|
-
condition
|
|
286
|
+
condition._with_placeholders(
|
|
287
|
+
f"{table}.{column}",
|
|
288
|
+
condition.operator,
|
|
289
|
+
condition.values,
|
|
233
290
|
escape=False,
|
|
291
|
+
placeholder=self.cursor.value_placeholder,
|
|
234
292
|
)
|
|
235
293
|
)
|
|
236
|
-
return
|
|
237
|
-
|
|
238
|
-
def
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
configuration[key] = [] if key[-1] == "s" else ""
|
|
252
|
-
return configuration
|
|
253
|
-
|
|
254
|
-
def validate_pagination_kwargs(self, kwargs: Dict[str, Any], case_mapping: Callable) -> str:
|
|
255
|
-
extra_keys = set(kwargs.keys()) - set(self.allowed_pagination_keys())
|
|
294
|
+
return (" WHERE " + " AND ".join(where_parts), tuple(parameters)) # type: ignore
|
|
295
|
+
|
|
296
|
+
def group_by_clause(self, group_by: str) -> str:
|
|
297
|
+
if not group_by:
|
|
298
|
+
return ""
|
|
299
|
+
escape = self.cursor.column_escape_character
|
|
300
|
+
if "." not in group_by:
|
|
301
|
+
return f" GROUP BY {escape}{group_by}{escape}"
|
|
302
|
+
parts = group_by.split(".", 1)
|
|
303
|
+
table = parts[0]
|
|
304
|
+
column = parts[1]
|
|
305
|
+
return f" GROUP BY {escape}{table}{escape}.{escape}{column}{escape}"
|
|
306
|
+
|
|
307
|
+
def validate_pagination_data(self, data: dict[str, Any], case_mapping: Callable) -> str:
|
|
308
|
+
extra_keys = set(data.keys()) - set(self.allowed_pagination_keys())
|
|
256
309
|
if len(extra_keys):
|
|
257
310
|
key_name = case_mapping("start")
|
|
258
311
|
return "Invalid pagination key(s): '" + "','".join(extra_keys) + f"'. Only '{key_name}' is allowed"
|
|
259
|
-
if "start" not in
|
|
312
|
+
if "start" not in data:
|
|
260
313
|
key_name = case_mapping("start")
|
|
261
314
|
return f"You must specify '{key_name}' when setting pagination"
|
|
262
|
-
start =
|
|
315
|
+
start = data["start"]
|
|
263
316
|
try:
|
|
264
317
|
start = int(start)
|
|
265
318
|
except:
|
|
@@ -267,16 +320,18 @@ class CursorBackend(Backend):
|
|
|
267
320
|
return f"Invalid pagination data: '{key_name}' must be a number"
|
|
268
321
|
return ""
|
|
269
322
|
|
|
270
|
-
def allowed_pagination_keys(self) ->
|
|
323
|
+
def allowed_pagination_keys(self) -> list[str]:
|
|
271
324
|
return ["start"]
|
|
272
325
|
|
|
273
|
-
def documentation_pagination_next_page_response(self, case_mapping: Callable) ->
|
|
326
|
+
def documentation_pagination_next_page_response(self, case_mapping: Callable[[str], str]) -> list[Any]:
|
|
274
327
|
return [AutoDocInteger(case_mapping("start"), example=0)]
|
|
275
328
|
|
|
276
|
-
def documentation_pagination_next_page_example(self, case_mapping: Callable) ->
|
|
329
|
+
def documentation_pagination_next_page_example(self, case_mapping: Callable[[str], str]) -> dict[str, Any]:
|
|
277
330
|
return {case_mapping("start"): 0}
|
|
278
331
|
|
|
279
|
-
def documentation_pagination_parameters(
|
|
332
|
+
def documentation_pagination_parameters(
|
|
333
|
+
self, case_mapping: Callable[[str], str]
|
|
334
|
+
) -> list[tuple[AutoDocSchema, str]]:
|
|
280
335
|
return [
|
|
281
336
|
(
|
|
282
337
|
AutoDocInteger(case_mapping("start"), example=0),
|