clear-skies 1.19.22__py3-none-any.whl → 2.0.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clear_skies-2.0.23.dist-info/METADATA +76 -0
- clear_skies-2.0.23.dist-info/RECORD +265 -0
- {clear_skies-1.19.22.dist-info → clear_skies-2.0.23.dist-info}/WHEEL +1 -1
- clearskies/__init__.py +37 -21
- clearskies/action.py +7 -0
- clearskies/authentication/__init__.py +9 -38
- clearskies/authentication/authentication.py +44 -0
- clearskies/authentication/authorization.py +14 -8
- clearskies/authentication/authorization_pass_through.py +22 -0
- clearskies/authentication/jwks.py +135 -58
- clearskies/authentication/public.py +3 -26
- clearskies/authentication/secret_bearer.py +515 -44
- clearskies/autodoc/formats/oai3_json/__init__.py +2 -2
- clearskies/autodoc/formats/oai3_json/oai3_json.py +11 -9
- clearskies/autodoc/formats/oai3_json/parameter.py +6 -3
- clearskies/autodoc/formats/oai3_json/request.py +7 -5
- clearskies/autodoc/formats/oai3_json/response.py +7 -4
- clearskies/autodoc/formats/oai3_json/schema/object.py +10 -1
- clearskies/autodoc/request/__init__.py +2 -0
- clearskies/autodoc/request/header.py +4 -6
- clearskies/autodoc/request/json_body.py +4 -6
- clearskies/autodoc/request/parameter.py +8 -0
- clearskies/autodoc/request/request.py +16 -4
- clearskies/autodoc/request/url_parameter.py +4 -6
- clearskies/autodoc/request/url_path.py +4 -6
- clearskies/autodoc/schema/__init__.py +4 -2
- clearskies/autodoc/schema/array.py +5 -6
- clearskies/autodoc/schema/boolean.py +4 -10
- clearskies/autodoc/schema/date.py +0 -3
- clearskies/autodoc/schema/datetime.py +1 -4
- clearskies/autodoc/schema/double.py +0 -3
- clearskies/autodoc/schema/enum.py +4 -2
- clearskies/autodoc/schema/integer.py +4 -9
- clearskies/autodoc/schema/long.py +0 -3
- clearskies/autodoc/schema/number.py +4 -9
- clearskies/autodoc/schema/object.py +5 -7
- clearskies/autodoc/schema/password.py +0 -3
- clearskies/autodoc/schema/schema.py +11 -0
- clearskies/autodoc/schema/string.py +4 -10
- clearskies/backends/__init__.py +56 -17
- clearskies/backends/api_backend.py +1128 -166
- clearskies/backends/backend.py +54 -85
- clearskies/backends/cursor_backend.py +246 -191
- clearskies/backends/memory_backend.py +514 -208
- clearskies/backends/secrets_backend.py +68 -31
- clearskies/column.py +1221 -0
- clearskies/columns/__init__.py +71 -0
- clearskies/columns/audit.py +306 -0
- clearskies/columns/belongs_to_id.py +478 -0
- clearskies/columns/belongs_to_model.py +129 -0
- clearskies/columns/belongs_to_self.py +109 -0
- clearskies/columns/boolean.py +110 -0
- clearskies/columns/category_tree.py +273 -0
- clearskies/columns/category_tree_ancestors.py +51 -0
- clearskies/columns/category_tree_children.py +126 -0
- clearskies/columns/category_tree_descendants.py +48 -0
- clearskies/columns/created.py +92 -0
- clearskies/columns/created_by_authorization_data.py +114 -0
- clearskies/columns/created_by_header.py +103 -0
- clearskies/columns/created_by_ip.py +90 -0
- clearskies/columns/created_by_routing_data.py +102 -0
- clearskies/columns/created_by_user_agent.py +89 -0
- clearskies/columns/date.py +232 -0
- clearskies/columns/datetime.py +284 -0
- clearskies/columns/email.py +78 -0
- clearskies/columns/float.py +149 -0
- clearskies/columns/has_many.py +529 -0
- clearskies/columns/has_many_self.py +62 -0
- clearskies/columns/has_one.py +21 -0
- clearskies/columns/integer.py +158 -0
- clearskies/columns/json.py +126 -0
- clearskies/columns/many_to_many_ids.py +335 -0
- clearskies/columns/many_to_many_ids_with_data.py +274 -0
- clearskies/columns/many_to_many_models.py +156 -0
- clearskies/columns/many_to_many_pivots.py +132 -0
- clearskies/columns/phone.py +162 -0
- clearskies/columns/select.py +95 -0
- clearskies/columns/string.py +102 -0
- clearskies/columns/timestamp.py +164 -0
- clearskies/columns/updated.py +107 -0
- clearskies/columns/uuid.py +83 -0
- clearskies/configs/README.md +105 -0
- clearskies/configs/__init__.py +170 -0
- clearskies/configs/actions.py +43 -0
- clearskies/configs/any.py +15 -0
- clearskies/configs/any_dict.py +24 -0
- clearskies/configs/any_dict_or_callable.py +25 -0
- clearskies/configs/authentication.py +23 -0
- clearskies/configs/authorization.py +23 -0
- clearskies/configs/boolean.py +18 -0
- clearskies/configs/boolean_or_callable.py +20 -0
- clearskies/configs/callable_config.py +20 -0
- clearskies/configs/columns.py +34 -0
- clearskies/configs/conditions.py +30 -0
- clearskies/configs/config.py +26 -0
- clearskies/configs/datetime.py +20 -0
- clearskies/configs/datetime_or_callable.py +21 -0
- clearskies/configs/email.py +10 -0
- clearskies/configs/email_list.py +17 -0
- clearskies/configs/email_list_or_callable.py +17 -0
- clearskies/configs/email_or_email_list_or_callable.py +59 -0
- clearskies/configs/endpoint.py +23 -0
- clearskies/configs/endpoint_list.py +29 -0
- clearskies/configs/float.py +18 -0
- clearskies/configs/float_or_callable.py +20 -0
- clearskies/configs/headers.py +28 -0
- clearskies/configs/integer.py +18 -0
- clearskies/configs/integer_or_callable.py +20 -0
- clearskies/configs/joins.py +30 -0
- clearskies/configs/list_any_dict.py +32 -0
- clearskies/configs/list_any_dict_or_callable.py +33 -0
- clearskies/configs/model_class.py +35 -0
- clearskies/configs/model_column.py +67 -0
- clearskies/configs/model_columns.py +58 -0
- clearskies/configs/model_destination_name.py +26 -0
- clearskies/configs/model_to_id_column.py +45 -0
- clearskies/configs/readable_model_column.py +11 -0
- clearskies/configs/readable_model_columns.py +11 -0
- clearskies/configs/schema.py +23 -0
- clearskies/configs/searchable_model_columns.py +11 -0
- clearskies/configs/security_headers.py +39 -0
- clearskies/configs/select.py +28 -0
- clearskies/configs/select_list.py +49 -0
- clearskies/configs/string.py +31 -0
- clearskies/configs/string_dict.py +34 -0
- clearskies/configs/string_list.py +47 -0
- clearskies/configs/string_list_or_callable.py +48 -0
- clearskies/configs/string_or_callable.py +18 -0
- clearskies/configs/timedelta.py +20 -0
- clearskies/configs/timezone.py +20 -0
- clearskies/configs/url.py +25 -0
- clearskies/configs/validators.py +45 -0
- clearskies/configs/writeable_model_column.py +11 -0
- clearskies/configs/writeable_model_columns.py +11 -0
- clearskies/configurable.py +78 -0
- clearskies/contexts/__init__.py +8 -8
- clearskies/contexts/cli.py +129 -43
- clearskies/contexts/context.py +93 -56
- clearskies/contexts/wsgi.py +79 -33
- clearskies/contexts/wsgi_ref.py +87 -0
- clearskies/cursors/__init__.py +7 -0
- clearskies/cursors/cursor.py +166 -0
- clearskies/cursors/from_environment/__init__.py +5 -0
- clearskies/cursors/from_environment/mysql.py +51 -0
- clearskies/cursors/from_environment/postgresql.py +49 -0
- clearskies/cursors/from_environment/sqlite.py +35 -0
- clearskies/cursors/mysql.py +61 -0
- clearskies/cursors/postgresql.py +61 -0
- clearskies/cursors/sqlite.py +62 -0
- clearskies/decorators.py +33 -0
- clearskies/decorators.pyi +10 -0
- clearskies/di/__init__.py +11 -7
- clearskies/di/additional_config.py +117 -3
- clearskies/di/additional_config_auto_import.py +12 -0
- clearskies/di/di.py +717 -126
- clearskies/di/inject/__init__.py +23 -0
- clearskies/di/inject/akeyless_sdk.py +16 -0
- clearskies/di/inject/by_class.py +24 -0
- clearskies/di/inject/by_name.py +22 -0
- clearskies/di/inject/di.py +16 -0
- clearskies/di/inject/environment.py +15 -0
- clearskies/di/inject/input_output.py +19 -0
- clearskies/di/inject/now.py +16 -0
- clearskies/di/inject/requests.py +16 -0
- clearskies/di/inject/secrets.py +15 -0
- clearskies/di/inject/utcnow.py +16 -0
- clearskies/di/inject/uuid.py +16 -0
- clearskies/di/injectable.py +32 -0
- clearskies/di/injectable_properties.py +131 -0
- clearskies/end.py +219 -0
- clearskies/endpoint.py +1303 -0
- clearskies/endpoint_group.py +333 -0
- clearskies/endpoints/__init__.py +25 -0
- clearskies/endpoints/advanced_search.py +519 -0
- clearskies/endpoints/callable.py +382 -0
- clearskies/endpoints/create.py +201 -0
- clearskies/endpoints/delete.py +133 -0
- clearskies/endpoints/get.py +267 -0
- clearskies/endpoints/health_check.py +181 -0
- clearskies/endpoints/list.py +567 -0
- clearskies/endpoints/restful_api.py +417 -0
- clearskies/endpoints/schema.py +185 -0
- clearskies/endpoints/simple_search.py +279 -0
- clearskies/endpoints/update.py +188 -0
- clearskies/environment.py +7 -3
- clearskies/exceptions/__init__.py +19 -0
- clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
- clearskies/exceptions/missing_dependency.py +2 -0
- clearskies/exceptions/moved_permanently.py +3 -0
- clearskies/exceptions/moved_temporarily.py +3 -0
- clearskies/functional/__init__.py +2 -2
- clearskies/functional/json.py +47 -0
- clearskies/functional/routing.py +92 -0
- clearskies/functional/string.py +19 -11
- clearskies/functional/validations.py +61 -9
- clearskies/input_outputs/__init__.py +9 -7
- clearskies/input_outputs/cli.py +135 -152
- clearskies/input_outputs/exceptions/__init__.py +6 -1
- clearskies/input_outputs/headers.py +54 -0
- clearskies/input_outputs/input_output.py +77 -123
- clearskies/input_outputs/programmatic.py +62 -0
- clearskies/input_outputs/wsgi.py +36 -48
- clearskies/model.py +1894 -199
- clearskies/query/__init__.py +12 -0
- clearskies/query/condition.py +228 -0
- clearskies/query/join.py +136 -0
- clearskies/query/query.py +193 -0
- clearskies/query/sort.py +27 -0
- clearskies/schema.py +82 -0
- clearskies/secrets/__init__.py +4 -31
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
- clearskies/secrets/akeyless.py +421 -155
- clearskies/secrets/exceptions/__init__.py +7 -1
- clearskies/secrets/exceptions/not_found_error.py +2 -0
- clearskies/secrets/exceptions/permissions_error.py +2 -0
- clearskies/secrets/secrets.py +12 -11
- clearskies/security_header.py +17 -0
- clearskies/security_headers/__init__.py +8 -8
- clearskies/security_headers/cache_control.py +47 -109
- clearskies/security_headers/cors.py +38 -92
- clearskies/security_headers/csp.py +76 -150
- clearskies/security_headers/hsts.py +14 -15
- clearskies/typing.py +11 -0
- clearskies/validator.py +36 -0
- clearskies/validators/__init__.py +33 -0
- clearskies/validators/after_column.py +61 -0
- clearskies/validators/before_column.py +15 -0
- clearskies/validators/in_the_future.py +29 -0
- clearskies/validators/in_the_future_at_least.py +13 -0
- clearskies/validators/in_the_future_at_most.py +12 -0
- clearskies/validators/in_the_past.py +29 -0
- clearskies/validators/in_the_past_at_least.py +12 -0
- clearskies/validators/in_the_past_at_most.py +12 -0
- clearskies/validators/maximum_length.py +25 -0
- clearskies/validators/maximum_value.py +28 -0
- clearskies/validators/minimum_length.py +25 -0
- clearskies/validators/minimum_value.py +28 -0
- clearskies/{input_requirements → validators}/required.py +18 -9
- clearskies/validators/timedelta.py +58 -0
- clearskies/validators/unique.py +28 -0
- clear_skies-1.19.22.dist-info/METADATA +0 -46
- clear_skies-1.19.22.dist-info/RECORD +0 -206
- clearskies/application.py +0 -29
- clearskies/authentication/auth0_jwks.py +0 -118
- clearskies/authentication/auth_exception.py +0 -2
- clearskies/authentication/jwks_jwcrypto.py +0 -39
- clearskies/backends/example_backend.py +0 -43
- clearskies/backends/file_backend.py +0 -48
- clearskies/backends/json_backend.py +0 -7
- clearskies/backends/restful_api_advanced_search_backend.py +0 -138
- clearskies/binding_config.py +0 -16
- clearskies/column_types/__init__.py +0 -184
- clearskies/column_types/audit.py +0 -235
- clearskies/column_types/belongs_to.py +0 -250
- clearskies/column_types/boolean.py +0 -60
- clearskies/column_types/category_tree.py +0 -226
- clearskies/column_types/column.py +0 -373
- clearskies/column_types/created.py +0 -26
- clearskies/column_types/created_by_authorization_data.py +0 -26
- clearskies/column_types/created_by_header.py +0 -24
- clearskies/column_types/created_by_ip.py +0 -17
- clearskies/column_types/created_by_routing_data.py +0 -25
- clearskies/column_types/created_by_user_agent.py +0 -17
- clearskies/column_types/created_micro.py +0 -26
- clearskies/column_types/datetime.py +0 -108
- clearskies/column_types/datetime_micro.py +0 -12
- clearskies/column_types/email.py +0 -18
- clearskies/column_types/float.py +0 -43
- clearskies/column_types/has_many.py +0 -139
- clearskies/column_types/integer.py +0 -41
- clearskies/column_types/json.py +0 -25
- clearskies/column_types/many_to_many.py +0 -278
- clearskies/column_types/many_to_many_with_data.py +0 -162
- clearskies/column_types/select.py +0 -11
- clearskies/column_types/string.py +0 -24
- clearskies/column_types/updated.py +0 -24
- clearskies/column_types/updated_micro.py +0 -24
- clearskies/column_types/uuid.py +0 -25
- clearskies/columns.py +0 -123
- clearskies/condition_parser.py +0 -172
- clearskies/contexts/build_context.py +0 -54
- clearskies/contexts/convert_to_application.py +0 -190
- clearskies/contexts/extract_handler.py +0 -37
- clearskies/contexts/test.py +0 -94
- clearskies/decorators/__init__.py +0 -39
- clearskies/decorators/auth0_jwks.py +0 -22
- clearskies/decorators/authorization.py +0 -10
- clearskies/decorators/binding_classes.py +0 -9
- clearskies/decorators/binding_modules.py +0 -9
- clearskies/decorators/bindings.py +0 -9
- clearskies/decorators/create.py +0 -10
- clearskies/decorators/delete.py +0 -10
- clearskies/decorators/docs.py +0 -14
- clearskies/decorators/get.py +0 -10
- clearskies/decorators/jwks.py +0 -26
- clearskies/decorators/merge.py +0 -124
- clearskies/decorators/patch.py +0 -10
- clearskies/decorators/post.py +0 -10
- clearskies/decorators/public.py +0 -11
- clearskies/decorators/response_headers.py +0 -10
- clearskies/decorators/return_raw_response.py +0 -9
- clearskies/decorators/schema.py +0 -10
- clearskies/decorators/secret_bearer.py +0 -24
- clearskies/decorators/security_headers.py +0 -10
- clearskies/di/standard_dependencies.py +0 -140
- clearskies/di/test_module/__init__.py +0 -6
- clearskies/di/test_module/another_module/__init__.py +0 -2
- clearskies/di/test_module/module_class.py +0 -5
- clearskies/handlers/__init__.py +0 -41
- clearskies/handlers/advanced_search.py +0 -271
- clearskies/handlers/base.py +0 -473
- clearskies/handlers/callable.py +0 -189
- clearskies/handlers/create.py +0 -35
- clearskies/handlers/crud_by_method.py +0 -18
- clearskies/handlers/database_connector.py +0 -32
- clearskies/handlers/delete.py +0 -61
- clearskies/handlers/exceptions/__init__.py +0 -5
- clearskies/handlers/exceptions/not_found.py +0 -3
- clearskies/handlers/get.py +0 -156
- clearskies/handlers/health_check.py +0 -59
- clearskies/handlers/input_processing.py +0 -79
- clearskies/handlers/list.py +0 -530
- clearskies/handlers/mygrations.py +0 -82
- clearskies/handlers/request_method_routing.py +0 -47
- clearskies/handlers/restful_api.py +0 -218
- clearskies/handlers/routing.py +0 -62
- clearskies/handlers/schema_helper.py +0 -128
- clearskies/handlers/simple_routing.py +0 -204
- clearskies/handlers/simple_routing_route.py +0 -192
- clearskies/handlers/simple_search.py +0 -136
- clearskies/handlers/update.py +0 -96
- clearskies/handlers/write.py +0 -193
- clearskies/input_requirements/__init__.py +0 -68
- clearskies/input_requirements/after.py +0 -36
- clearskies/input_requirements/before.py +0 -36
- clearskies/input_requirements/in_the_future_at_least.py +0 -19
- clearskies/input_requirements/in_the_future_at_most.py +0 -19
- clearskies/input_requirements/in_the_past_at_least.py +0 -19
- clearskies/input_requirements/in_the_past_at_most.py +0 -19
- clearskies/input_requirements/maximum_length.py +0 -19
- clearskies/input_requirements/minimum_length.py +0 -22
- clearskies/input_requirements/requirement.py +0 -25
- clearskies/input_requirements/time_delta.py +0 -38
- clearskies/input_requirements/unique.py +0 -18
- clearskies/mocks/__init__.py +0 -7
- clearskies/mocks/input_output.py +0 -124
- clearskies/mocks/models.py +0 -142
- clearskies/models.py +0 -345
- clearskies/security_headers/base.py +0 -12
- clearskies/tests/simple_api/models/__init__.py +0 -2
- clearskies/tests/simple_api/models/status.py +0 -23
- clearskies/tests/simple_api/models/user.py +0 -21
- clearskies/tests/simple_api/users_api.py +0 -64
- {clear_skies-1.19.22.dist-info → clear_skies-2.0.23.dist-info/licenses}/LICENSE +0 -0
- /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
- /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
- /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
- /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
- /clearskies/{secrets/exceptions → exceptions}/not_found.py +0 -0
- /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
- /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from clearskies.query.condition import Condition, ParsedCondition
|
|
2
|
+
from clearskies.query.join import Join
|
|
3
|
+
from clearskies.query.query import Query
|
|
4
|
+
from clearskies.query.sort import Sort
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Condition",
|
|
8
|
+
"Join",
|
|
9
|
+
"ParsedCondition",
|
|
10
|
+
"Sort",
|
|
11
|
+
"Query",
|
|
12
|
+
]
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
class Condition:
|
|
2
|
+
"""
|
|
3
|
+
Parses a condition string, e.g. "column=value" or "table.column<=other_value".
|
|
4
|
+
|
|
5
|
+
Allowed operators: ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null", "is not", "is", "like"]
|
|
6
|
+
|
|
7
|
+
NOTE: Not all backends support all operators, so make sure the condition you are building works for your backend
|
|
8
|
+
|
|
9
|
+
This is safe to use with untrusted input because it expects a stringent and easy-to-verify format. The incoming
|
|
10
|
+
string must be one of these patterns:
|
|
11
|
+
|
|
12
|
+
1. [column_name][operator][value]
|
|
13
|
+
2. [table_name].[column_name][operator][value]
|
|
14
|
+
|
|
15
|
+
SQL-like syntax is allowed, so:
|
|
16
|
+
|
|
17
|
+
1. Spaces are optionally allowed around the operator.
|
|
18
|
+
2. Backticks are optionally allowd around the table/column name.
|
|
19
|
+
3. Single quotes are optionally allowed around the values.
|
|
20
|
+
4. operators are case-insensitive.
|
|
21
|
+
|
|
22
|
+
In the case of an IN operator, the parser expects a series of comma separated values enclosed in parenthesis,
|
|
23
|
+
with each value optionally enclosed in single quotes. This parsing is very simple and there is not currently a way
|
|
24
|
+
to escape commas or single quotes.
|
|
25
|
+
|
|
26
|
+
NOTE: operators (when they are english words, of course) are always output in all upper-case.
|
|
27
|
+
|
|
28
|
+
Some examples:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
condition = Condition("id=asdf-qwerty") # note: same results for: Condition("id = asdf-qwerty")
|
|
32
|
+
print(condition.table_name) # prints ''
|
|
33
|
+
print(condition.column_name) # prints 'id'
|
|
34
|
+
print(condition.operator) # prints '='
|
|
35
|
+
print(condition.values) # prints ['asdf-qwerty']
|
|
36
|
+
print(condition.parsed) # prints 'id=%s'
|
|
37
|
+
|
|
38
|
+
condition = Condition("orders.status_id in ('ACTIVE', 'PENDING')")
|
|
39
|
+
print(condition.table_name) # prints 'orders'
|
|
40
|
+
print(condition.column_name) # prints 'status_id'
|
|
41
|
+
print(condition.operator) # prints 'IN'
|
|
42
|
+
print(condition.values) # prints ['ACTIVE', 'PENDING']
|
|
43
|
+
print(condition.parsed) # prints 'status_id IN (%s, %s)'
|
|
44
|
+
```
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
The name of the table this condition is searching on (if there is one).
|
|
49
|
+
"""
|
|
50
|
+
table_name: str = ""
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
The name of the column the condition is searching.
|
|
54
|
+
"""
|
|
55
|
+
column_name: str = ""
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
The operator we are searching with (e.g. '=', '<=', etc...)
|
|
59
|
+
"""
|
|
60
|
+
operator: str = ""
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
The values the condition is searching for.
|
|
64
|
+
|
|
65
|
+
Note this is always a list, although most of the time there is only one value in the list. Multiple values
|
|
66
|
+
are only present when searching with the IN operator.
|
|
67
|
+
"""
|
|
68
|
+
values: list[str] = []
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
An SQL-ready string
|
|
72
|
+
"""
|
|
73
|
+
parsed: str = ""
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
The original condition string
|
|
77
|
+
"""
|
|
78
|
+
_raw_condition: str = ""
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
The list of operators we can match
|
|
82
|
+
|
|
83
|
+
Note: the order is very important because this list is used to find the operator in the condition string.
|
|
84
|
+
As a result, the order of the operators in this list is important. The condition matching algorithm used
|
|
85
|
+
below will select whichever operator matches earlier in the string, but there are some operators that
|
|
86
|
+
start with the same characters: '<=>' and '<=', as well as 'is', 'is null', 'is not', etc... This leaves
|
|
87
|
+
room for ambiguity since all of these operators will match at the same location. In the event of a "tie" the
|
|
88
|
+
algorithm gives preference to the first matching operator. Therefore, for ambiguous operators, we put the
|
|
89
|
+
longer one first, which means it matches first, and so a condition with a '<=>' operator won't accidentally
|
|
90
|
+
match to the '<=' operator.
|
|
91
|
+
"""
|
|
92
|
+
operators: list[str] = [
|
|
93
|
+
"<=>",
|
|
94
|
+
"!=",
|
|
95
|
+
"<=",
|
|
96
|
+
">=",
|
|
97
|
+
">",
|
|
98
|
+
"<",
|
|
99
|
+
"=",
|
|
100
|
+
"in",
|
|
101
|
+
"is not null",
|
|
102
|
+
"is null",
|
|
103
|
+
"is not",
|
|
104
|
+
"is",
|
|
105
|
+
"like",
|
|
106
|
+
"not in",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
operator_lengths: dict[str, int] = {
|
|
110
|
+
"<=>": 3,
|
|
111
|
+
"<=": 2,
|
|
112
|
+
">=": 2,
|
|
113
|
+
"!=": 2,
|
|
114
|
+
">": 1,
|
|
115
|
+
"<": 1,
|
|
116
|
+
"=": 1,
|
|
117
|
+
"in": 4,
|
|
118
|
+
"is not null": 12,
|
|
119
|
+
"is null": 8,
|
|
120
|
+
"is not": 8,
|
|
121
|
+
"is": 4,
|
|
122
|
+
"like": 6,
|
|
123
|
+
"not in": 8,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# some operators require spaces around them
|
|
127
|
+
operators_for_matching: dict[str, str] = {
|
|
128
|
+
"like": " like ",
|
|
129
|
+
"in": " in ",
|
|
130
|
+
"not in": " not in ",
|
|
131
|
+
"is not null": " is not null",
|
|
132
|
+
"is null": " is null",
|
|
133
|
+
"is": " is ",
|
|
134
|
+
"is not": " is not ",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
operators_with_simple_placeholders: dict[str, bool] = {
|
|
138
|
+
"<=>": True,
|
|
139
|
+
"<=": True,
|
|
140
|
+
">=": True,
|
|
141
|
+
"!=": True,
|
|
142
|
+
"=": True,
|
|
143
|
+
"<": True,
|
|
144
|
+
">": True,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
operators_without_placeholders: dict[str, bool] = {
|
|
148
|
+
"is not null": True,
|
|
149
|
+
"is null": True,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
def __init__(self, condition: str):
|
|
153
|
+
self._raw_condition = condition
|
|
154
|
+
lowercase_condition = condition.lower()
|
|
155
|
+
self.operator = ""
|
|
156
|
+
matching_index = len(condition)
|
|
157
|
+
# figure out which operator comes earliest in the string: make sure and check all so we match the
|
|
158
|
+
# earliest operator so we don't get unpredictable results for things like 'age=name<=5'. We want
|
|
159
|
+
# our operator to **ALWAYS** match whatever comes first in the condition string.
|
|
160
|
+
for operator in self.operators:
|
|
161
|
+
try:
|
|
162
|
+
operator_for_match = self.operators_for_matching.get(operator, operator)
|
|
163
|
+
index = lowercase_condition.index(operator_for_match)
|
|
164
|
+
except ValueError:
|
|
165
|
+
continue
|
|
166
|
+
if index < matching_index:
|
|
167
|
+
matching_index = index
|
|
168
|
+
self.operator = operator
|
|
169
|
+
|
|
170
|
+
if not self.operator:
|
|
171
|
+
raise ValueError(f"No supported operators found in condition {condition}")
|
|
172
|
+
|
|
173
|
+
self.column_name = condition[:matching_index].strip().replace("`", "")
|
|
174
|
+
value = condition[matching_index + self.operator_lengths[self.operator] :].strip()
|
|
175
|
+
if value and (value[0] == "'" and value[-1] == "'"):
|
|
176
|
+
value = value.strip("'")
|
|
177
|
+
self.values = self._parse_condition_list(value) if self.operator == "in" else [value]
|
|
178
|
+
self.table_name = ""
|
|
179
|
+
if "." in self.column_name:
|
|
180
|
+
[self.table_name, self.column_name] = self.column_name.split(".")
|
|
181
|
+
column_for_parsed = f"{self.table_name}.{self.column_name}" if self.table_name else self.column_name
|
|
182
|
+
|
|
183
|
+
if self.operator in self.operators_without_placeholders:
|
|
184
|
+
self.values = []
|
|
185
|
+
|
|
186
|
+
self.operator = self.operator.upper()
|
|
187
|
+
self.parsed = self._with_placeholders(
|
|
188
|
+
column_for_parsed, self.operator, self.values, escape=False if self.table_name else True
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def _parse_condition_list(self, value):
|
|
192
|
+
if value[0] != "(" and value[-1] != ")":
|
|
193
|
+
raise ValueError(f"Invalid search value {value} for condition. For IN operator use `IN (value1,value2)`")
|
|
194
|
+
|
|
195
|
+
# note: this is not very smart and will mess things up if there are single quotes/commas in the data
|
|
196
|
+
return list(map(lambda value: value.strip().strip("'"), value[1:-1].split(",")))
|
|
197
|
+
|
|
198
|
+
def _with_placeholders(self, column, operator, values, escape=True, escape_character="`", placeholder="%s"):
|
|
199
|
+
quote = escape_character if escape else ""
|
|
200
|
+
column = column.replace("`", "")
|
|
201
|
+
upper_case_operator = operator.upper()
|
|
202
|
+
lower_case_operator = operator.lower()
|
|
203
|
+
if lower_case_operator in self.operators_with_simple_placeholders:
|
|
204
|
+
return f"{quote}{column}{quote}{upper_case_operator}{placeholder}"
|
|
205
|
+
if lower_case_operator in self.operators_without_placeholders:
|
|
206
|
+
return f"{quote}{column}{quote} {upper_case_operator}"
|
|
207
|
+
if lower_case_operator == "is" or lower_case_operator == "is not" or lower_case_operator == "like":
|
|
208
|
+
return f"{quote}{column}{quote} {upper_case_operator} {placeholder}"
|
|
209
|
+
if lower_case_operator == "not in":
|
|
210
|
+
return f"{quote}{column}{quote} NOT IN ({', '.join([placeholder for i in range(len(values))])})"
|
|
211
|
+
|
|
212
|
+
# the only thing left is "in" which has a variable number of placeholders
|
|
213
|
+
return f"{quote}{column}{quote} IN ({', '.join([placeholder for i in range(len(values))])})"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class ParsedCondition(Condition):
|
|
217
|
+
def __init__(self, column_name: str, operator: str, values: list[str], table_name: str = ""):
|
|
218
|
+
self.column_name = column_name
|
|
219
|
+
if operator not in self.operators:
|
|
220
|
+
raise ValueError(f"Unknown operator '{operator}'")
|
|
221
|
+
self.operator = operator
|
|
222
|
+
self.values = values
|
|
223
|
+
self.table_name = table_name
|
|
224
|
+
column_for_parsed = f"{self.table_name}.{self.column_name}" if self.table_name else self.column_name
|
|
225
|
+
self.parsed = self._with_placeholders(
|
|
226
|
+
column_for_parsed, self.operator, self.values, escape=False if self.table_name else True
|
|
227
|
+
)
|
|
228
|
+
self._raw_condition = self.parsed
|
clearskies/query/join.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Join:
|
|
5
|
+
"""
|
|
6
|
+
Parses a join clause.
|
|
7
|
+
|
|
8
|
+
Note that this expects a few very specific pattern:
|
|
9
|
+
|
|
10
|
+
1. [TYPE] JOIN [right_table_name] ON [left_table_name].[left_column_name]=[right_table_name].[right_column_name]
|
|
11
|
+
2. [TYPE] JOIN [right_table_name] AS [alias] ON [alias].[left_column_name]=[right_table_name].[right_column_name]
|
|
12
|
+
3. [TYPE] JOIN [right_table_name] [alias] ON [alias].[left_column_name]=[right_table_name].[right_column_name]
|
|
13
|
+
|
|
14
|
+
NOTE: The allowed join types are ["INNER", "OUTER", "LEFT", "RIGHT"]
|
|
15
|
+
|
|
16
|
+
NOTE: backticks are optionally allowed around column and table names.
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
```python
|
|
20
|
+
join = Join("INNER JOIN orders ON users.id=orders.user_id")
|
|
21
|
+
print(f"{join.left_table_name}.{join.left_column_name}") # prints 'users.id'
|
|
22
|
+
print(f"{join.right_table_name}.{join.right_column_name}") # prints 'orders.user_id'
|
|
23
|
+
print(join.type) # prints 'INNER'
|
|
24
|
+
print(join.alias) # prints ''
|
|
25
|
+
print(join.unaliased_table_name) # prints 'orders'
|
|
26
|
+
|
|
27
|
+
join = Join("JOIN some_long_table_name AS new_table ON old_table.id=new_table.old_id")
|
|
28
|
+
print(f"{join.left_table_name}.{join.left_column_name}") # prints 'old_table.id'
|
|
29
|
+
print(f"{join.right_table_name}.{join.right_column_name}") # prints 'new_table.old_id'
|
|
30
|
+
print(join.type) # prints 'LEFT'
|
|
31
|
+
print(join.alias) # prints 'new_table'
|
|
32
|
+
print(join.unaliased_table_name) # prints 'some_long_table_name'
|
|
33
|
+
```
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
The name of the table on the left side of the join
|
|
38
|
+
"""
|
|
39
|
+
left_table_name: str = ""
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
The name of the column on the left side of the join
|
|
43
|
+
"""
|
|
44
|
+
left_column_name: str = ""
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
The name of the table on the right side of the join
|
|
48
|
+
"""
|
|
49
|
+
right_table_name: str = ""
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
The name of the column on the right side of the join
|
|
53
|
+
"""
|
|
54
|
+
right_column_name: str = ""
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
The type of join (LEFT, RIGHT, INNER, OUTER)
|
|
58
|
+
"""
|
|
59
|
+
join_type: str = ""
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
An alias for the joined table, if needed
|
|
63
|
+
"""
|
|
64
|
+
alias: str = ""
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
The actual name of the right table, regardless of alias
|
|
68
|
+
"""
|
|
69
|
+
unaliased_table_name: str = ""
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
The original join string
|
|
73
|
+
"""
|
|
74
|
+
_raw_join: str = ""
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
The allowed join types
|
|
78
|
+
"""
|
|
79
|
+
_allowed_types = ["INNER", "OUTER", "LEFT", "RIGHT"]
|
|
80
|
+
|
|
81
|
+
def __init__(self, join: str):
|
|
82
|
+
self._raw_join = join
|
|
83
|
+
# doing this the simple and stupid way, until that doesn't work. Yes, it is ugly.
|
|
84
|
+
# Splitting this into two regexps for simplicity: this one does not check for an alias
|
|
85
|
+
matches = re.match(
|
|
86
|
+
"(\\w+\\s+)?join\\s+`?([^\\s`]+)`?\\s+on\\s+`?([^`]+)`?\\.`?([^`]+)`?\\s*=\\s*`?([^`]+)`?\\.`?([^`]+)`?",
|
|
87
|
+
join,
|
|
88
|
+
re.IGNORECASE,
|
|
89
|
+
)
|
|
90
|
+
if matches:
|
|
91
|
+
groups = matches.groups()
|
|
92
|
+
alias = ""
|
|
93
|
+
join_type = groups[0]
|
|
94
|
+
right_table = groups[1]
|
|
95
|
+
first_table = groups[2]
|
|
96
|
+
first_column = groups[3]
|
|
97
|
+
second_table = groups[4]
|
|
98
|
+
second_column = groups[5]
|
|
99
|
+
else:
|
|
100
|
+
matches = re.match(
|
|
101
|
+
"(\\w+\\s+)?join\\s+`?([^\\s`]+)`?\\s+(as\\s+)?(\\S+)\\s+on\\s+`?([^`]+)`?\\.`?([^`]+)`?\\s*=\\s*`?([^`]+)`?\\.`?([^`]+)`?",
|
|
102
|
+
join,
|
|
103
|
+
re.IGNORECASE,
|
|
104
|
+
)
|
|
105
|
+
if not matches:
|
|
106
|
+
raise ValueError(f"Specified join condition, '{join}' does not appear to be a valid join statement")
|
|
107
|
+
groups = matches.groups()
|
|
108
|
+
join_type = groups[0]
|
|
109
|
+
right_table = groups[1]
|
|
110
|
+
alias = groups[3]
|
|
111
|
+
first_table = groups[4]
|
|
112
|
+
first_column = groups[5]
|
|
113
|
+
second_table = groups[6]
|
|
114
|
+
second_column = groups[7]
|
|
115
|
+
|
|
116
|
+
# which is the left table and which is the right table?
|
|
117
|
+
match_by = alias if alias else right_table
|
|
118
|
+
if first_table == match_by:
|
|
119
|
+
self.left_table_name = second_table
|
|
120
|
+
self.left_column_name = second_column
|
|
121
|
+
self.right_table_name = first_table
|
|
122
|
+
self.right_column_name = first_column
|
|
123
|
+
elif second_table == match_by:
|
|
124
|
+
self.left_table_name = first_table
|
|
125
|
+
self.left_column_name = first_column
|
|
126
|
+
self.right_table_name = second_table
|
|
127
|
+
self.right_column_name = second_column
|
|
128
|
+
else:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"Specified join condition, '{join}' was not understandable because the joined table "
|
|
131
|
+
+ "is not referenced in the 'on' clause"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self.join_type = groups[0].strip().upper() if groups[0] else "INNER"
|
|
135
|
+
self.alias = alias
|
|
136
|
+
self.unaliased_table_name = right_table
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from clearskies import Model
|
|
7
|
+
from clearskies.query import Condition, Join, Sort
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Query:
|
|
11
|
+
"""
|
|
12
|
+
Track the various aspects of a query.
|
|
13
|
+
|
|
14
|
+
This is mostly just used by the Model class to keep track of a list request.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
The model class
|
|
19
|
+
"""
|
|
20
|
+
model_class: type[Model] = None # type: ignore
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
The list of where conditions for the query.
|
|
24
|
+
"""
|
|
25
|
+
conditions: list[Condition] = []
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
The conditions, but organized by column.
|
|
29
|
+
"""
|
|
30
|
+
conditions_by_column: dict[str, list[Condition]] = {}
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
Joins for the query.
|
|
34
|
+
"""
|
|
35
|
+
joins: list[Join] = []
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
The sort directives for the query
|
|
39
|
+
"""
|
|
40
|
+
sorts: list[Sort] = []
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
The maximum number of records to return.
|
|
44
|
+
"""
|
|
45
|
+
limit: int = 0
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
Pagination information (e.g. start/next_token/etc... the details depend on the backend.
|
|
49
|
+
"""
|
|
50
|
+
pagination: dict[str, Any] = {}
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
A list of select statements.
|
|
54
|
+
"""
|
|
55
|
+
selects: list[str] = []
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
Whether or not to just select all columns.
|
|
59
|
+
"""
|
|
60
|
+
select_all: bool = True
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
The name of the column to group by.
|
|
64
|
+
"""
|
|
65
|
+
group_by = ""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
model_class: type[Model],
|
|
70
|
+
conditions: list[Condition] = [],
|
|
71
|
+
joins: list[Join] = [],
|
|
72
|
+
sorts: list[Sort] = [],
|
|
73
|
+
limit: int = 0,
|
|
74
|
+
group_by: str = "",
|
|
75
|
+
pagination: dict[str, Any] = {},
|
|
76
|
+
selects: list[str] = [],
|
|
77
|
+
select_all: bool = True,
|
|
78
|
+
):
|
|
79
|
+
self.model_class = model_class
|
|
80
|
+
self.conditions = [*conditions]
|
|
81
|
+
self.joins = [*joins]
|
|
82
|
+
self.sorts = [*sorts]
|
|
83
|
+
self.limit = limit
|
|
84
|
+
self.group_by = group_by
|
|
85
|
+
self.pagination = {**pagination}
|
|
86
|
+
self.selects = [*selects]
|
|
87
|
+
self.select_all = select_all
|
|
88
|
+
self.conditions_by_column = {}
|
|
89
|
+
if conditions:
|
|
90
|
+
for condition in conditions:
|
|
91
|
+
if condition.column_name not in self.conditions_by_column:
|
|
92
|
+
self.conditions_by_column[condition.column_name] = []
|
|
93
|
+
self.conditions_by_column[condition.column_name].append(condition)
|
|
94
|
+
|
|
95
|
+
def as_kwargs(self):
|
|
96
|
+
"""Return the properties of this query as a dictionary so it can be used as kwargs when creating another one."""
|
|
97
|
+
return {
|
|
98
|
+
"model_class": self.model_class,
|
|
99
|
+
"conditions": [*self.conditions],
|
|
100
|
+
"joins": [*self.joins],
|
|
101
|
+
"sorts": [*self.sorts],
|
|
102
|
+
"limit": self.limit,
|
|
103
|
+
"group_by": self.group_by,
|
|
104
|
+
"pagination": self.pagination,
|
|
105
|
+
"selects": [*self.selects],
|
|
106
|
+
"select_all": self.select_all,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def add_where(self, condition: Condition) -> Self:
|
|
110
|
+
self.validate_column(condition.column_name, "filter", table=condition.table_name)
|
|
111
|
+
new_kwargs = self.as_kwargs()
|
|
112
|
+
new_kwargs["conditions"].append(condition)
|
|
113
|
+
return self.__class__(**new_kwargs)
|
|
114
|
+
|
|
115
|
+
def add_join(self, join: Join) -> Self:
|
|
116
|
+
new_kwargs = self.as_kwargs()
|
|
117
|
+
new_kwargs["joins"].append(join)
|
|
118
|
+
return self.__class__(**new_kwargs)
|
|
119
|
+
|
|
120
|
+
def set_sort(self, sort: Sort, secondary_sort: Sort | None = None) -> Self:
|
|
121
|
+
self.validate_column(sort.column_name, "sort", table=sort.table_name)
|
|
122
|
+
new_kwargs = self.as_kwargs()
|
|
123
|
+
new_kwargs["sorts"] = [sort]
|
|
124
|
+
if secondary_sort:
|
|
125
|
+
new_kwargs["sorts"].append(secondary_sort)
|
|
126
|
+
|
|
127
|
+
return self.__class__(**new_kwargs)
|
|
128
|
+
|
|
129
|
+
def set_limit(self, limit: int) -> Self:
|
|
130
|
+
if not isinstance(limit, int):
|
|
131
|
+
raise TypeError(
|
|
132
|
+
f"The limit in a query must be of type int but I received a value of type '{limit.__class__.__name__}'"
|
|
133
|
+
)
|
|
134
|
+
return self.__class__(
|
|
135
|
+
**{
|
|
136
|
+
**self.as_kwargs(),
|
|
137
|
+
"limit": limit,
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def set_group_by(self, column_name) -> Self:
|
|
142
|
+
self.validate_column(column_name, "group")
|
|
143
|
+
return self.__class__(
|
|
144
|
+
**{
|
|
145
|
+
**self.as_kwargs(),
|
|
146
|
+
"group_by": column_name,
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def set_pagination(self, pagination: dict[str, Any]) -> Self:
|
|
151
|
+
return self.__class__(
|
|
152
|
+
**{
|
|
153
|
+
**self.as_kwargs(),
|
|
154
|
+
"pagination": pagination,
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def add_select(self, select: str) -> Self:
|
|
159
|
+
new_kwargs = self.as_kwargs()
|
|
160
|
+
new_kwargs["selects"].append(select)
|
|
161
|
+
return self.__class__(**new_kwargs)
|
|
162
|
+
|
|
163
|
+
def set_select_all(self, select_all: bool) -> Self:
|
|
164
|
+
return self.__class__(
|
|
165
|
+
**{
|
|
166
|
+
**self.as_kwargs(),
|
|
167
|
+
"select_all": select_all,
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def validate_column(self: Self, column_name: str, action: str, table: str | None = None) -> None:
|
|
172
|
+
# for now, only validate columns that belong to *our* table.
|
|
173
|
+
# in some cases we are explicitly told the column name
|
|
174
|
+
if table is not None:
|
|
175
|
+
# note that table may be '', in which case it is implicitly "our" table
|
|
176
|
+
if table != "" and table != self.model_class.destination_name():
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# but in some cases we should check and see if it is included with the column name
|
|
180
|
+
column_name = column_name.replace("`", "")
|
|
181
|
+
if "." in column_name:
|
|
182
|
+
parts = column_name.split(".")
|
|
183
|
+
if parts[0] != self.model_class.destination_name():
|
|
184
|
+
return
|
|
185
|
+
column_name = column_name.split(".")[1]
|
|
186
|
+
|
|
187
|
+
model_columns = self.model_class.get_columns()
|
|
188
|
+
if column_name not in model_columns:
|
|
189
|
+
raise KeyError(
|
|
190
|
+
f"Cannot {action} by column '{column_name}' for model class {self.model_class.__name__} because this "
|
|
191
|
+
+ "column does not exist for the model. You can suppress this error by adding a matching column "
|
|
192
|
+
+ "to your model definition"
|
|
193
|
+
)
|
clearskies/query/sort.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class Sort:
|
|
2
|
+
"""Stores a sort directive."""
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The name of the table to sort on.
|
|
6
|
+
"""
|
|
7
|
+
table_name: str = ""
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
The name of the column to sort on.
|
|
11
|
+
"""
|
|
12
|
+
column_name: str = ""
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
The direction to sort.
|
|
16
|
+
"""
|
|
17
|
+
direction: str = ""
|
|
18
|
+
|
|
19
|
+
def __init__(self, table_name: str, column_name: str, direction: str):
|
|
20
|
+
if not column_name:
|
|
21
|
+
raise ValueError("Missing 'column_name' for sort")
|
|
22
|
+
direction = direction.upper().strip()
|
|
23
|
+
if direction != "ASC" and direction != "DESC":
|
|
24
|
+
raise ValueError(f"Invalid sort direction: should be ASC or DESC, not '{direction}'")
|
|
25
|
+
self.table_name = table_name
|
|
26
|
+
self.column_name = column_name
|
|
27
|
+
self.direction = direction
|
clearskies/schema.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from typing import TYPE_CHECKING, Self
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from clearskies import Column
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Schema:
|
|
11
|
+
"""
|
|
12
|
+
Define a schema by extending and declaring columns.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from clearskies.schema import Schema
|
|
16
|
+
from clearskies.validators import Required, Unique
|
|
17
|
+
|
|
18
|
+
import clearskies.columns
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Person(Schema):
|
|
22
|
+
id = clearskies.columns.Uuid()
|
|
23
|
+
name = clearskies.columns.String(validators=[Required()])
|
|
24
|
+
date_of_birth = clearskies.columns.Datetime(validators=[Required(), InThePast()])
|
|
25
|
+
email = clearskies.columns.Email()
|
|
26
|
+
```
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
id_column_name: str = ""
|
|
30
|
+
_columns: dict[str, Column] = {}
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def destination_name(cls: type[Self]) -> str:
|
|
34
|
+
raise NotImplementedError()
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self._data = {}
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def get_columns(cls: type[Self], overrides={}) -> dict[str, Column]:
|
|
41
|
+
"""
|
|
42
|
+
Return an ordered dictionary with the configuration for the columns.
|
|
43
|
+
|
|
44
|
+
Generally, this method is meant for internal use. It just pulls the column configuration
|
|
45
|
+
information out of class attributes. It doesn't return the fully prepared columns,
|
|
46
|
+
so you probably can't use the return value of this function. For that, see
|
|
47
|
+
`model.columns()`.
|
|
48
|
+
"""
|
|
49
|
+
# no caching if we have overrides
|
|
50
|
+
if cls._columns and not overrides:
|
|
51
|
+
return cls._columns
|
|
52
|
+
|
|
53
|
+
overrides = {**overrides}
|
|
54
|
+
columns: dict[str, Column] = OrderedDict()
|
|
55
|
+
for attribute_name in dir(cls):
|
|
56
|
+
attribute = getattr(cls, attribute_name)
|
|
57
|
+
# use duck typing instead of isinstance to decide which attribute is a column.
|
|
58
|
+
# We have to do this to avoid circular imports.
|
|
59
|
+
if not hasattr(attribute, "from_backend") and not hasattr(attribute, "to_backend"):
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if attribute_name in overrides:
|
|
63
|
+
columns[attribute_name] = overrides[attribute_name]
|
|
64
|
+
del overrides[attribute_name]
|
|
65
|
+
columns[attribute_name] = attribute
|
|
66
|
+
|
|
67
|
+
for attribute_name, column in overrides.items():
|
|
68
|
+
columns[attribute_name] = column # type: ignore
|
|
69
|
+
|
|
70
|
+
if not overrides:
|
|
71
|
+
cls._columns = columns
|
|
72
|
+
|
|
73
|
+
# now go through and finalize everything. We have to do this after setting cls._columns, because finalization
|
|
74
|
+
# sometimes depends on fetching the list of columns, so if we do it before caching the answer, we may end up
|
|
75
|
+
# creating circular loops. I don't *think* this will cause painful side-effects, but we'll find out!
|
|
76
|
+
for column_name, column in cls._columns.items():
|
|
77
|
+
column.finalize_configuration(cls, column_name)
|
|
78
|
+
|
|
79
|
+
return columns
|
|
80
|
+
|
|
81
|
+
def __bool__(self):
|
|
82
|
+
return False
|