clear-skies 1.22.31__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of clear-skies might be problematic. Click here for more details.
- {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/METADATA +12 -14
- clear_skies-2.0.1.dist-info/RECORD +249 -0
- {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/WHEEL +1 -1
- clearskies/__init__.py +42 -25
- clearskies/action.py +7 -0
- clearskies/authentication/__init__.py +8 -41
- clearskies/authentication/authentication.py +46 -0
- clearskies/authentication/authorization.py +8 -9
- clearskies/authentication/authorization_pass_through.py +11 -9
- clearskies/authentication/jwks.py +133 -58
- clearskies/authentication/public.py +3 -38
- clearskies/authentication/secret_bearer.py +516 -54
- clearskies/autodoc/formats/oai3_json/__init__.py +1 -1
- clearskies/autodoc/formats/oai3_json/oai3_json.py +9 -7
- 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 +4 -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 +7 -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 +1100 -284
- clearskies/backends/backend.py +53 -84
- clearskies/backends/cursor_backend.py +236 -186
- clearskies/backends/memory_backend.py +519 -226
- clearskies/backends/secrets_backend.py +75 -31
- clearskies/column.py +1229 -0
- clearskies/columns/__init__.py +71 -0
- clearskies/columns/audit.py +205 -0
- clearskies/columns/belongs_to_id.py +483 -0
- clearskies/columns/belongs_to_model.py +128 -0
- clearskies/columns/belongs_to_self.py +105 -0
- clearskies/columns/boolean.py +109 -0
- clearskies/columns/category_tree.py +275 -0
- clearskies/columns/category_tree_ancestors.py +51 -0
- clearskies/columns/category_tree_children.py +127 -0
- clearskies/columns/category_tree_descendants.py +48 -0
- clearskies/columns/created.py +94 -0
- clearskies/columns/created_by_authorization_data.py +116 -0
- clearskies/columns/created_by_header.py +99 -0
- clearskies/columns/created_by_ip.py +92 -0
- clearskies/columns/created_by_routing_data.py +96 -0
- clearskies/columns/created_by_user_agent.py +92 -0
- clearskies/columns/date.py +230 -0
- clearskies/columns/datetime.py +278 -0
- clearskies/columns/email.py +76 -0
- clearskies/columns/float.py +149 -0
- clearskies/columns/has_many.py +505 -0
- clearskies/columns/has_many_self.py +56 -0
- clearskies/columns/has_one.py +14 -0
- clearskies/columns/integer.py +156 -0
- clearskies/columns/json.py +122 -0
- clearskies/columns/many_to_many_ids.py +333 -0
- clearskies/columns/many_to_many_ids_with_data.py +270 -0
- clearskies/columns/many_to_many_models.py +154 -0
- clearskies/columns/many_to_many_pivots.py +133 -0
- clearskies/columns/phone.py +158 -0
- clearskies/columns/select.py +91 -0
- clearskies/columns/string.py +98 -0
- clearskies/columns/timestamp.py +160 -0
- clearskies/columns/updated.py +110 -0
- clearskies/columns/uuid.py +86 -0
- clearskies/configs/README.md +105 -0
- clearskies/configs/__init__.py +162 -0
- clearskies/configs/actions.py +43 -0
- clearskies/configs/any.py +13 -0
- clearskies/configs/any_dict.py +22 -0
- clearskies/configs/any_dict_or_callable.py +23 -0
- clearskies/configs/authentication.py +23 -0
- clearskies/configs/authorization.py +23 -0
- clearskies/configs/boolean.py +16 -0
- clearskies/configs/boolean_or_callable.py +18 -0
- clearskies/configs/callable_config.py +18 -0
- clearskies/configs/columns.py +34 -0
- clearskies/configs/conditions.py +30 -0
- clearskies/configs/config.py +24 -0
- clearskies/configs/datetime.py +18 -0
- clearskies/configs/datetime_or_callable.py +19 -0
- clearskies/configs/endpoint.py +23 -0
- clearskies/configs/endpoint_list.py +28 -0
- clearskies/configs/float.py +16 -0
- clearskies/configs/float_or_callable.py +18 -0
- clearskies/configs/integer.py +16 -0
- clearskies/configs/integer_or_callable.py +18 -0
- clearskies/configs/joins.py +30 -0
- clearskies/configs/list_any_dict.py +30 -0
- clearskies/configs/list_any_dict_or_callable.py +31 -0
- clearskies/configs/model_class.py +35 -0
- clearskies/configs/model_column.py +65 -0
- clearskies/configs/model_columns.py +56 -0
- clearskies/configs/model_destination_name.py +25 -0
- clearskies/configs/model_to_id_column.py +43 -0
- clearskies/configs/readable_model_column.py +9 -0
- clearskies/configs/readable_model_columns.py +9 -0
- clearskies/configs/schema.py +23 -0
- clearskies/configs/searchable_model_columns.py +9 -0
- clearskies/configs/security_headers.py +39 -0
- clearskies/configs/select.py +26 -0
- clearskies/configs/select_list.py +47 -0
- clearskies/configs/string.py +29 -0
- clearskies/configs/string_dict.py +32 -0
- clearskies/configs/string_list.py +32 -0
- clearskies/configs/string_list_or_callable.py +35 -0
- clearskies/configs/string_or_callable.py +18 -0
- clearskies/configs/timedelta.py +18 -0
- clearskies/configs/timezone.py +18 -0
- clearskies/configs/url.py +23 -0
- clearskies/configs/validators.py +45 -0
- clearskies/configs/writeable_model_column.py +9 -0
- clearskies/configs/writeable_model_columns.py +9 -0
- clearskies/configurable.py +76 -0
- clearskies/contexts/__init__.py +8 -8
- clearskies/contexts/cli.py +8 -41
- clearskies/contexts/context.py +91 -56
- clearskies/contexts/wsgi.py +16 -29
- clearskies/contexts/wsgi_ref.py +53 -0
- clearskies/di/__init__.py +10 -7
- clearskies/di/additional_config.py +115 -4
- clearskies/di/additional_config_auto_import.py +12 -0
- clearskies/di/di.py +742 -121
- clearskies/di/inject/__init__.py +23 -0
- clearskies/di/inject/by_class.py +21 -0
- clearskies/di/inject/by_name.py +18 -0
- clearskies/di/inject/di.py +13 -0
- clearskies/di/inject/environment.py +14 -0
- clearskies/di/inject/input_output.py +20 -0
- clearskies/di/inject/now.py +13 -0
- clearskies/di/inject/requests.py +13 -0
- clearskies/di/inject/secrets.py +14 -0
- clearskies/di/inject/utcnow.py +13 -0
- clearskies/di/inject/uuid.py +15 -0
- clearskies/di/injectable.py +29 -0
- clearskies/di/injectable_properties.py +131 -0
- clearskies/end.py +183 -0
- clearskies/endpoint.py +1310 -0
- clearskies/endpoint_group.py +310 -0
- clearskies/endpoints/__init__.py +23 -0
- clearskies/endpoints/advanced_search.py +526 -0
- clearskies/endpoints/callable.py +388 -0
- clearskies/endpoints/create.py +202 -0
- clearskies/endpoints/delete.py +139 -0
- clearskies/endpoints/get.py +275 -0
- clearskies/endpoints/health_check.py +181 -0
- clearskies/endpoints/list.py +573 -0
- clearskies/endpoints/restful_api.py +427 -0
- clearskies/endpoints/simple_search.py +286 -0
- clearskies/endpoints/update.py +190 -0
- clearskies/environment.py +5 -3
- clearskies/exceptions/__init__.py +17 -0
- clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
- clearskies/exceptions/moved_permanently.py +3 -0
- clearskies/exceptions/moved_temporarily.py +3 -0
- clearskies/exceptions/not_found.py +2 -0
- clearskies/functional/__init__.py +2 -2
- 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 +130 -142
- clearskies/input_outputs/exceptions/__init__.py +1 -1
- clearskies/input_outputs/headers.py +45 -0
- clearskies/input_outputs/input_output.py +91 -122
- clearskies/input_outputs/programmatic.py +69 -0
- clearskies/input_outputs/wsgi.py +23 -38
- clearskies/model.py +984 -183
- clearskies/parameters_to_properties.py +31 -0
- clearskies/query/__init__.py +12 -0
- clearskies/query/condition.py +223 -0
- clearskies/query/join.py +136 -0
- clearskies/query/query.py +196 -0
- clearskies/query/sort.py +27 -0
- clearskies/schema.py +82 -0
- clearskies/secrets/__init__.py +3 -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 +88 -147
- clearskies/secrets/secrets.py +8 -8
- clearskies/security_header.py +15 -0
- clearskies/security_headers/__init__.py +8 -8
- clearskies/security_headers/cache_control.py +47 -110
- clearskies/security_headers/cors.py +40 -95
- clearskies/security_headers/csp.py +76 -151
- clearskies/security_headers/hsts.py +14 -16
- clearskies/test_base.py +8 -0
- clearskies/typing.py +11 -0
- clearskies/validator.py +37 -0
- clearskies/validators/__init__.py +33 -0
- clearskies/validators/after_column.py +62 -0
- clearskies/validators/before_column.py +13 -0
- clearskies/validators/in_the_future.py +32 -0
- clearskies/validators/in_the_future_at_least.py +11 -0
- clearskies/validators/in_the_future_at_most.py +10 -0
- clearskies/validators/in_the_past.py +32 -0
- clearskies/validators/in_the_past_at_least.py +10 -0
- clearskies/validators/in_the_past_at_most.py +10 -0
- clearskies/validators/maximum_length.py +26 -0
- clearskies/validators/maximum_value.py +29 -0
- clearskies/validators/minimum_length.py +26 -0
- clearskies/validators/minimum_value.py +29 -0
- clearskies/validators/required.py +35 -0
- clearskies/validators/timedelta.py +59 -0
- clearskies/validators/unique.py +31 -0
- clear_skies-1.22.31.dist-info/RECORD +0 -214
- 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 -12
- 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 -60
- 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 -41
- clearskies/decorators/allow_non_json_bodies.py +0 -9
- 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/handlers/__init__.py +0 -41
- clearskies/handlers/advanced_search.py +0 -271
- clearskies/handlers/base.py +0 -479
- clearskies/handlers/callable.py +0 -192
- 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 -197
- clearskies/handlers/simple_search.py +0 -136
- clearskies/handlers/update.py +0 -102
- 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/required.py +0 -23
- 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.31.dist-info → clear_skies-2.0.1.dist-info}/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/{tests/__init__.py → input_outputs/py.typed} +0 -0
- /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
|
@@ -1,265 +1,313 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
|
|
3
|
+
import clearskies.model
|
|
4
|
+
import clearskies.query
|
|
5
|
+
from clearskies.autodoc.schema import Integer as AutoDocInteger
|
|
6
|
+
from clearskies.autodoc.schema import Schema as AutoDocSchema
|
|
7
|
+
from clearskies.backends.backend import Backend
|
|
8
|
+
from clearskies.di import InjectableProperties, inject
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CursorBackend(Backend, InjectableProperties):
|
|
12
|
+
"""
|
|
13
|
+
The cursor backend connects your models to a MySQL or MariaDB database.
|
|
14
|
+
|
|
15
|
+
## Installing Dependencies
|
|
16
|
+
|
|
17
|
+
clearskies uses PyMySQL to manage the database connection and make queries. This is not installed by default,
|
|
18
|
+
but is a named extra that you can install when needed via:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install clear-skies[mysql]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Connecting to your server
|
|
25
|
+
|
|
26
|
+
By default, database credentials are expected in environment variables:
|
|
27
|
+
|
|
28
|
+
| Name | Default | Value |
|
|
29
|
+
|-------------|---------|---------------------------------------------------------------|
|
|
30
|
+
| db_host | | The hostname where the database can be found |
|
|
31
|
+
| db_username | | The username to connect as |
|
|
32
|
+
| db_password | | The password to connect with |
|
|
33
|
+
| db_database | | The name of the database to use |
|
|
34
|
+
| db_port | 3306 | The network port to connect to |
|
|
35
|
+
| db_ssl_ca | | Path to a certificate to use: enables SSL over the connection |
|
|
36
|
+
|
|
37
|
+
However, you can fully control the credential provisioning process by declaring a dependency named `connection_details` and
|
|
38
|
+
setting it to a dictionary with the above keys, minus the `db_` prefix:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
class ConnectionDetails(clearskies.di.AdditionalConfig):
|
|
42
|
+
provide_connection_details(self, secrets):
|
|
43
|
+
return {
|
|
44
|
+
"host": secrets.get("database_host"),
|
|
45
|
+
"username": secrets.get("db_username"),
|
|
46
|
+
"password": secrets.get("db_password"),
|
|
47
|
+
"database": secrets.get("db_database"),
|
|
48
|
+
"port": 3306,
|
|
49
|
+
"ssl_ca": "/path/to/ca",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
wsgi = clearskies.contexts.Wsgi(
|
|
53
|
+
some_application,
|
|
54
|
+
additional_configs=[ConnectionDetails()],
|
|
55
|
+
bindings={
|
|
56
|
+
"secrets": "" # some configuration here to point to your secret manager
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Similarly, some alternate credential provisioning schemes are built into clearskies. See the
|
|
62
|
+
clearskies.secrets.additional_configs module for those options.
|
|
63
|
+
|
|
64
|
+
## Connecting models to tables
|
|
65
|
+
|
|
66
|
+
The table name for your model comes from calling the `destination_name` class method of the model class. By
|
|
67
|
+
default, this takes the class name, converts it to snake case, and then pluralizes it. So, if you have a model
|
|
68
|
+
class named `UserPreference` then the cursor backend will look for a table called `user_preferences`. If this
|
|
69
|
+
isn't what you want, then you can simply override `destination_name` to return whatever table you want:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
class UserPreference(clearskies.Model):
|
|
73
|
+
@classmethod
|
|
74
|
+
def destination_name(cls):
|
|
75
|
+
return "some_other_table_name"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Additionally, the cursor backend accepts an argument called `table_prefix` which, if provided, will be prefixed
|
|
79
|
+
to your table name. Finally, you can declare a dependency called `global_table_prefix` which will automatically
|
|
80
|
+
be added to every table name. In the following example, the table name will be `user_configuration_preferences`
|
|
81
|
+
due to:
|
|
82
|
+
|
|
83
|
+
1. The `destination_name` method sets the table name to `preferences`
|
|
84
|
+
2. The `table_prefix` argument to the CursorBackend constructor adds a prefix of `configuration_`
|
|
85
|
+
3. The `global_table_prefix` binding sets a prefix of `user_`, wihch goes before everything else.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import clearskies
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class UserPreference(clearskies.Model):
|
|
92
|
+
id_column_name = "id"
|
|
93
|
+
backend = clearskies.backends.CursorBackend(table_prefix="configuration_")
|
|
94
|
+
id = clearskies.columns.Uuid()
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def destination_name(cls):
|
|
98
|
+
return "preferences"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
cli = clearskies.contexts.Cli(
|
|
102
|
+
clearskies.endpoints.Callable(
|
|
103
|
+
lambda user_preferences: user_preferences.create(no_data=True).id,
|
|
104
|
+
),
|
|
105
|
+
classes=[UserPreference],
|
|
106
|
+
bindings={
|
|
107
|
+
"global_table_prefix": "user_",
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
"""
|
|
5
113
|
|
|
6
|
-
|
|
7
|
-
class CursorBackend(Backend):
|
|
8
114
|
supports_n_plus_one = True
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"select_all",
|
|
20
|
-
"joins",
|
|
21
|
-
"model_columns",
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
_required_configs = [
|
|
25
|
-
"table_name",
|
|
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
|
|
115
|
+
cursor = inject.ByName("cursor")
|
|
116
|
+
global_table_prefix = inject.ByName("global_table_prefix")
|
|
117
|
+
table_escape_character = "`"
|
|
118
|
+
column_escape_character = "`"
|
|
119
|
+
table_prefix = ""
|
|
120
|
+
|
|
121
|
+
def __init__(self, table_escape_character="`", column_escape_character="`", table_prefix=""):
|
|
122
|
+
self.table_escape_character = table_escape_character
|
|
123
|
+
self.column_escape_character = column_escape_character
|
|
124
|
+
self.table_prefix = table_prefix
|
|
44
125
|
|
|
45
126
|
def _finalize_table_name(self, table_name):
|
|
46
|
-
|
|
127
|
+
table_name = f"{self.global_table_prefix}{self.table_prefix}{table_name}"
|
|
47
128
|
if "." not in table_name:
|
|
48
|
-
return f"{
|
|
49
|
-
return
|
|
129
|
+
return f"{self.table_escape_character}{table_name}{self.table_escape_character}"
|
|
130
|
+
return (
|
|
131
|
+
self.table_escape_character
|
|
132
|
+
+ f"{self.table_escape_character}.{self.table_escape_character}".join(table_name.split("."))
|
|
133
|
+
+ self.table_escape_character
|
|
134
|
+
)
|
|
50
135
|
|
|
51
|
-
def update(self, id, data, model):
|
|
136
|
+
def update(self, id: int | str, data: dict[str, Any], model: clearskies.model.Model) -> dict[str, Any]:
|
|
52
137
|
query_parts = []
|
|
53
138
|
parameters = []
|
|
54
|
-
escape = self.
|
|
139
|
+
escape = self.column_escape_character
|
|
55
140
|
for key, val in data.items():
|
|
56
141
|
query_parts.append(f"{escape}{key}{escape}=%s")
|
|
57
142
|
parameters.append(val)
|
|
58
143
|
updates = ", ".join(query_parts)
|
|
59
144
|
|
|
60
|
-
|
|
61
|
-
self.
|
|
145
|
+
# update the record
|
|
146
|
+
table_name = self._finalize_table_name(model.destination_name())
|
|
147
|
+
self.cursor.execute(
|
|
62
148
|
f"UPDATE {table_name} SET {updates} WHERE {model.id_column_name}=%s", tuple([*parameters, id])
|
|
63
149
|
)
|
|
64
150
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
|
|
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]
|
|
151
|
+
# and now query again to fetch the updated record.
|
|
152
|
+
return self.records(
|
|
153
|
+
clearskies.query.Query(
|
|
154
|
+
model.__class__, conditions=[clearskies.query.Condition(f"{model.id_column_name}={id}")]
|
|
155
|
+
)
|
|
156
|
+
)[0]
|
|
81
157
|
|
|
82
|
-
def create(self, data, model):
|
|
83
|
-
escape = self.
|
|
158
|
+
def create(self, data: dict[str, Any], model: clearskies.model.Model) -> dict[str, Any]:
|
|
159
|
+
escape = self.column_escape_character
|
|
84
160
|
columns = escape + f"{escape}, {escape}".join(data.keys()) + escape
|
|
85
161
|
placeholders = ", ".join(["%s" for i in range(len(data))])
|
|
86
162
|
|
|
87
|
-
table_name = self._finalize_table_name(model.
|
|
88
|
-
self.
|
|
163
|
+
table_name = self._finalize_table_name(model.destination_name())
|
|
164
|
+
self.cursor.execute(f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})", tuple(data.values()))
|
|
89
165
|
new_id = data.get(model.id_column_name)
|
|
90
166
|
if not new_id:
|
|
91
|
-
new_id = self.
|
|
167
|
+
new_id = self.cursor.lastrowid
|
|
92
168
|
if not new_id:
|
|
93
169
|
raise ValueError("I can't figure out what the id is for a newly created record :(")
|
|
94
170
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"
|
|
98
|
-
|
|
99
|
-
|
|
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]
|
|
171
|
+
return self.records(
|
|
172
|
+
clearskies.query.Query(
|
|
173
|
+
model.__class__, conditions=[clearskies.query.Condition(f"{model.id_column_name}={new_id}")]
|
|
174
|
+
)
|
|
175
|
+
)[0]
|
|
111
176
|
|
|
112
|
-
def delete(self, id, model):
|
|
113
|
-
table_name = self._finalize_table_name(model.
|
|
114
|
-
self.
|
|
177
|
+
def delete(self, id: int | str, model: clearskies.model.Model) -> bool:
|
|
178
|
+
table_name = self._finalize_table_name(model.destination_name())
|
|
179
|
+
self.cursor.execute(f"DELETE FROM {table_name} WHERE {model.id_column_name}=%s", (id,))
|
|
115
180
|
return True
|
|
116
181
|
|
|
117
|
-
def count(self,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
self.
|
|
121
|
-
for row in self._cursor:
|
|
182
|
+
def count(self, query: clearskies.query.Query) -> int:
|
|
183
|
+
(sql, parameters) = self.as_count_sql(query)
|
|
184
|
+
self.cursor.execute(sql, parameters)
|
|
185
|
+
for row in self.cursor:
|
|
122
186
|
return row[0] if type(row) == tuple else row["count"]
|
|
123
187
|
return 0
|
|
124
188
|
|
|
125
189
|
def records(
|
|
126
|
-
self,
|
|
127
|
-
) ->
|
|
190
|
+
self, query: clearskies.query.Query, next_page_data: dict[str, str | int] | None = None
|
|
191
|
+
) -> list[dict[str, Any]]:
|
|
128
192
|
# I was going to get fancy and have this return an iterator, but since I'm going to load up
|
|
129
193
|
# 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]
|
|
194
|
+
(sql, parameters) = self.as_sql(query)
|
|
195
|
+
self.cursor.execute(sql, parameters)
|
|
196
|
+
records = [row for row in self.cursor]
|
|
134
197
|
if type(next_page_data) == dict:
|
|
135
|
-
limit =
|
|
136
|
-
start =
|
|
198
|
+
limit = query.limit
|
|
199
|
+
start = query.pagination.get("start", 0)
|
|
137
200
|
if limit and len(records) == limit:
|
|
138
201
|
next_page_data["start"] = int(start) + int(limit)
|
|
139
202
|
return records
|
|
140
203
|
|
|
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"]
|
|
204
|
+
def as_sql(self, query: clearskies.query.Query) -> tuple[str, tuple[Any]]:
|
|
205
|
+
escape = self.column_escape_character
|
|
206
|
+
table_name = query.model_class.destination_name()
|
|
207
|
+
(wheres, parameters) = self.conditions_as_wheres_and_parameters(
|
|
208
|
+
query.conditions, query.model_class.destination_name()
|
|
156
209
|
)
|
|
157
210
|
select_parts = []
|
|
158
|
-
if
|
|
159
|
-
select_parts.append(self._finalize_table_name(
|
|
160
|
-
if
|
|
161
|
-
select_parts.extend(
|
|
211
|
+
if query.select_all:
|
|
212
|
+
select_parts.append(self._finalize_table_name(table_name) + ".*")
|
|
213
|
+
if query.selects:
|
|
214
|
+
select_parts.extend(query.selects)
|
|
162
215
|
select = ", ".join(select_parts)
|
|
163
|
-
if
|
|
164
|
-
joins = " " + " ".join([join
|
|
216
|
+
if query.joins:
|
|
217
|
+
joins = " " + " ".join([join._raw_join for join in query.joins])
|
|
165
218
|
else:
|
|
166
219
|
joins = ""
|
|
167
|
-
if
|
|
220
|
+
if query.sorts:
|
|
168
221
|
sort_parts = []
|
|
169
|
-
for sort in
|
|
170
|
-
table_name = sort.
|
|
171
|
-
column_name = sort
|
|
172
|
-
direction = sort
|
|
222
|
+
for sort in query.sorts:
|
|
223
|
+
table_name = sort.table_name
|
|
224
|
+
column_name = sort.column_name
|
|
225
|
+
direction = sort.direction
|
|
173
226
|
prefix = self._finalize_table_name(table_name) + "." if table_name else ""
|
|
174
227
|
sort_parts.append(f"{prefix}{escape}{column_name}{escape} {direction}")
|
|
175
228
|
order_by = " ORDER BY " + ", ".join(sort_parts)
|
|
176
229
|
else:
|
|
177
230
|
order_by = ""
|
|
178
|
-
group_by = self.group_by_clause(
|
|
231
|
+
group_by = self.group_by_clause(query.group_by)
|
|
179
232
|
limit = ""
|
|
180
|
-
if
|
|
233
|
+
if query.limit:
|
|
181
234
|
start = 0
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
235
|
+
limit_size = int(query.limit)
|
|
236
|
+
if "start" in query.pagination:
|
|
237
|
+
start = int(query.pagination["start"])
|
|
238
|
+
limit = f" LIMIT {start}, {limit_size}"
|
|
185
239
|
|
|
186
|
-
table_name = self._finalize_table_name(
|
|
187
|
-
return
|
|
240
|
+
table_name = self._finalize_table_name(table_name)
|
|
241
|
+
return (
|
|
188
242
|
f"SELECT {select} FROM {table_name}{joins}{wheres}{group_by}{order_by}{limit}".strip(),
|
|
189
243
|
parameters,
|
|
190
|
-
|
|
244
|
+
)
|
|
191
245
|
|
|
192
|
-
def as_count_sql(self,
|
|
193
|
-
escape = self.
|
|
246
|
+
def as_count_sql(self, query: clearskies.query.Query) -> tuple[str, tuple[Any]]:
|
|
247
|
+
escape = self.column_escape_character
|
|
194
248
|
# note that this won't work if we start including a HAVING clause
|
|
195
|
-
|
|
196
|
-
|
|
249
|
+
(wheres, parameters) = self.conditions_as_wheres_and_parameters(
|
|
250
|
+
query.conditions, query.model_class.destination_name()
|
|
197
251
|
)
|
|
198
252
|
# we also don't currently support parameters in the join clause - I'll probably need that though
|
|
199
|
-
if
|
|
253
|
+
if query.joins:
|
|
200
254
|
# We can ignore left joins because they don't change the count
|
|
201
|
-
join_sections = filter(lambda join: join
|
|
202
|
-
joins = " " + " ".join([join
|
|
255
|
+
join_sections = filter(lambda join: join.type != "LEFT", query.joins) # type: ignore
|
|
256
|
+
joins = " " + " ".join([join._raw_join for join in join_sections])
|
|
203
257
|
else:
|
|
204
258
|
joins = ""
|
|
205
|
-
table_name = self._finalize_table_name(
|
|
206
|
-
if not
|
|
207
|
-
|
|
259
|
+
table_name = self._finalize_table_name(query.model_class.destination_name())
|
|
260
|
+
if not query.group_by:
|
|
261
|
+
query_string = f"SELECT COUNT(*) AS count FROM {table_name}{joins}{wheres}"
|
|
208
262
|
else:
|
|
209
|
-
group_by = self.group_by_clause(
|
|
210
|
-
|
|
263
|
+
group_by = self.group_by_clause(query.group_by)
|
|
264
|
+
query_string = (
|
|
211
265
|
f"SELECT COUNT(*) AS count FROM (SELECT 1 FROM {table_name}{joins}{wheres}{group_by}) AS count_inner"
|
|
212
266
|
)
|
|
213
|
-
return
|
|
267
|
+
return (query_string, parameters)
|
|
214
268
|
|
|
215
|
-
def
|
|
269
|
+
def conditions_as_wheres_and_parameters(
|
|
270
|
+
self, conditions: list[clearskies.query.Condition], default_table_name: str
|
|
271
|
+
) -> tuple[str, tuple[Any]]:
|
|
216
272
|
if not conditions:
|
|
217
|
-
return
|
|
273
|
+
return ("", ()) # type: ignore
|
|
218
274
|
|
|
219
275
|
parameters = []
|
|
220
276
|
where_parts = []
|
|
221
277
|
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}"
|
|
278
|
+
parameters.extend(condition.values)
|
|
279
|
+
table = condition.table_name if condition.table_name else self._finalize_table_name(default_table_name)
|
|
280
|
+
column = condition.column_name
|
|
228
281
|
where_parts.append(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
condition
|
|
232
|
-
condition
|
|
282
|
+
condition._with_placeholders(
|
|
283
|
+
f"{table}.{column}",
|
|
284
|
+
condition.operator,
|
|
285
|
+
condition.values,
|
|
233
286
|
escape=False,
|
|
234
287
|
)
|
|
235
288
|
)
|
|
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())
|
|
289
|
+
return (" WHERE " + " AND ".join(where_parts), tuple(parameters)) # type: ignore
|
|
290
|
+
|
|
291
|
+
def group_by_clause(self, group_by: str) -> str:
|
|
292
|
+
if not group_by:
|
|
293
|
+
return ""
|
|
294
|
+
escape = self.column_escape_character
|
|
295
|
+
if "." not in group_by:
|
|
296
|
+
return f" GROUP BY {escape}{group_by}{escape}"
|
|
297
|
+
parts = group_by.split(".", 1)
|
|
298
|
+
table = parts[0]
|
|
299
|
+
column = parts[1]
|
|
300
|
+
return f" GROUP BY {escape}{table}{escape}.{escape}{column}{escape}"
|
|
301
|
+
|
|
302
|
+
def validate_pagination_data(self, data: dict[str, Any], case_mapping: Callable) -> str:
|
|
303
|
+
extra_keys = set(data.keys()) - set(self.allowed_pagination_keys())
|
|
256
304
|
if len(extra_keys):
|
|
257
305
|
key_name = case_mapping("start")
|
|
258
306
|
return "Invalid pagination key(s): '" + "','".join(extra_keys) + f"'. Only '{key_name}' is allowed"
|
|
259
|
-
if "start" not in
|
|
307
|
+
if "start" not in data:
|
|
260
308
|
key_name = case_mapping("start")
|
|
261
309
|
return f"You must specify '{key_name}' when setting pagination"
|
|
262
|
-
start =
|
|
310
|
+
start = data["start"]
|
|
263
311
|
try:
|
|
264
312
|
start = int(start)
|
|
265
313
|
except:
|
|
@@ -267,16 +315,18 @@ class CursorBackend(Backend):
|
|
|
267
315
|
return f"Invalid pagination data: '{key_name}' must be a number"
|
|
268
316
|
return ""
|
|
269
317
|
|
|
270
|
-
def allowed_pagination_keys(self) ->
|
|
318
|
+
def allowed_pagination_keys(self) -> list[str]:
|
|
271
319
|
return ["start"]
|
|
272
320
|
|
|
273
|
-
def documentation_pagination_next_page_response(self, case_mapping: Callable) ->
|
|
321
|
+
def documentation_pagination_next_page_response(self, case_mapping: Callable[[str], str]) -> list[Any]:
|
|
274
322
|
return [AutoDocInteger(case_mapping("start"), example=0)]
|
|
275
323
|
|
|
276
|
-
def documentation_pagination_next_page_example(self, case_mapping: Callable) ->
|
|
324
|
+
def documentation_pagination_next_page_example(self, case_mapping: Callable[[str], str]) -> dict[str, Any]:
|
|
277
325
|
return {case_mapping("start"): 0}
|
|
278
326
|
|
|
279
|
-
def documentation_pagination_parameters(
|
|
327
|
+
def documentation_pagination_parameters(
|
|
328
|
+
self, case_mapping: Callable[[str], str]
|
|
329
|
+
) -> list[tuple[AutoDocSchema, str]]:
|
|
280
330
|
return [
|
|
281
331
|
(
|
|
282
332
|
AutoDocInteger(case_mapping("start"), example=0),
|