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
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from clearskies import exceptions
|
|
6
|
+
from clearskies.endpoints.simple_search import SimpleSearch
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from clearskies import Model, autodoc
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AdvancedSearch(SimpleSearch):
|
|
13
|
+
"""
|
|
14
|
+
An endpoint that grants the client extensive control over searching and filtering.
|
|
15
|
+
|
|
16
|
+
Rather than accepting URL parameters (like the SimpleSearch endpoint), this endpoint accepts a JSON POST
|
|
17
|
+
body. Search conditions are specified as a list of dictionaries containing `column`, `operator`, and
|
|
18
|
+
`value`. It also accepts up to two sort directives. Of course, while this endpoint supports arbitrary
|
|
19
|
+
searching, it won't work if the backend itself doesn't support it. The following is the list of allowed
|
|
20
|
+
keys in the JSON body:
|
|
21
|
+
|
|
22
|
+
| Name | Type | Description | Example |
|
|
23
|
+
|-------|----------------------|----------------------------------------------------------------------------|---------|
|
|
24
|
+
| sort | list[dict[str, str]] | A list of sort directives containing `column` and `direction` | `{"sort": [ {"column": "age", "direction": "desc} ] }` |
|
|
25
|
+
| limit | int | The number of records to return | `{"limit": `100`}` |
|
|
26
|
+
| where | list[dict[str, Any]] | A list of conditions containing `column`, `operator`, and `value` | `{"where": [ {"column": "age", "operator": ">", "value": 10} ] }` |
|
|
27
|
+
| * | str, int | Pagination information. The key name and value type depend on the backend | `{"start": 100}` |
|
|
28
|
+
|
|
29
|
+
Here's an example making use of the AdvancedSearch endpoint:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import clearskies
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Company(clearskies.Model):
|
|
36
|
+
id_column_name = "id"
|
|
37
|
+
backend = clearskies.backends.MemoryBackend()
|
|
38
|
+
|
|
39
|
+
id = clearskies.columns.Uuid()
|
|
40
|
+
name = clearskies.columns.String()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class User(clearskies.Model):
|
|
44
|
+
id_column_name = "id"
|
|
45
|
+
backend = clearskies.backends.MemoryBackend()
|
|
46
|
+
|
|
47
|
+
id = clearskies.columns.Uuid()
|
|
48
|
+
name = clearskies.columns.String()
|
|
49
|
+
username = clearskies.columns.String()
|
|
50
|
+
age = clearskies.columns.Integer()
|
|
51
|
+
company_id = clearskies.columns.BelongsToId(Company, readable_parent_columns=["id", "name"])
|
|
52
|
+
company = clearskies.columns.BelongsToModel("company_id")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
56
|
+
clearskies.endpoints.AdvancedSearch(
|
|
57
|
+
model_class=User,
|
|
58
|
+
readable_column_names=["id", "name", "username", "age", "company"],
|
|
59
|
+
sortable_column_names=["name", "username", "age", "company.name"],
|
|
60
|
+
searchable_column_names=["id", "name", "username", "age", "company_id", "company.name"],
|
|
61
|
+
default_sort_column_name="name",
|
|
62
|
+
),
|
|
63
|
+
bindings={
|
|
64
|
+
"memory_backend_default_data": [
|
|
65
|
+
{
|
|
66
|
+
"model_class": Company,
|
|
67
|
+
"records": [
|
|
68
|
+
{"id": "5-5-5-5", "name": "Bob's Widgets"},
|
|
69
|
+
{"id": "3-3-3-3", "name": "New Venture"},
|
|
70
|
+
{"id": "7-7-7-7", "name": "Jane's Cool Stuff"},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"model_class": User,
|
|
75
|
+
"records": [
|
|
76
|
+
{
|
|
77
|
+
"id": "1-2-3-4",
|
|
78
|
+
"name": "Bob Brown",
|
|
79
|
+
"username": "bobbrown",
|
|
80
|
+
"age": 18,
|
|
81
|
+
"company_id": "5-5-5-5",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "1-2-3-5",
|
|
85
|
+
"name": "Jane Doe",
|
|
86
|
+
"username": "janedoe",
|
|
87
|
+
"age": 52,
|
|
88
|
+
"company_id": "7-7-7-7",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "1-2-3-6",
|
|
92
|
+
"name": "Greg",
|
|
93
|
+
"username": "greg",
|
|
94
|
+
"age": 37,
|
|
95
|
+
"company_id": "7-7-7-7",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": "1-2-3-7",
|
|
99
|
+
"name": "Curious George",
|
|
100
|
+
"username": "curious",
|
|
101
|
+
"age": 7,
|
|
102
|
+
"company_id": "3-3-3-3",
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
wsgi()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
If you invoke the endpoint without any additional data, it will simply list all records:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
$ curl 'http://localhost:8080/' | jq
|
|
116
|
+
{
|
|
117
|
+
"status": "success",
|
|
118
|
+
"error": "",
|
|
119
|
+
"data": [
|
|
120
|
+
{
|
|
121
|
+
"id": "1-2-3-4",
|
|
122
|
+
"name": "Bob Brown",
|
|
123
|
+
"username": "bobbrown",
|
|
124
|
+
"age": 18,
|
|
125
|
+
"company": {
|
|
126
|
+
"id": "5-5-5-5",
|
|
127
|
+
"name": "Bob's Widgets"
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"id": "1-2-3-7",
|
|
132
|
+
"name": "Curious George",
|
|
133
|
+
"username": "curious",
|
|
134
|
+
"age": 7,
|
|
135
|
+
"company": {
|
|
136
|
+
"id": "3-3-3-3",
|
|
137
|
+
"name": "New Venture"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"id": "1-2-3-6",
|
|
142
|
+
"name": "Greg",
|
|
143
|
+
"username": "greg",
|
|
144
|
+
"age": 37,
|
|
145
|
+
"company": {
|
|
146
|
+
"id": "7-7-7-7",
|
|
147
|
+
"name": "Jane's Cool Stuff"
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"id": "1-2-3-5",
|
|
152
|
+
"name": "Jane Doe",
|
|
153
|
+
"username": "janedoe",
|
|
154
|
+
"age": 52,
|
|
155
|
+
"company": {
|
|
156
|
+
"id": "7-7-7-7",
|
|
157
|
+
"name": "Jane's Cool Stuff"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
"pagination": {
|
|
162
|
+
"number_results": 4,
|
|
163
|
+
"limit": 50,
|
|
164
|
+
"next_page": {}
|
|
165
|
+
},
|
|
166
|
+
"input_errors": {}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Of course you can also sort and paginate. Keep in mind that pagination is backend-dependent:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
$ curl 'http://localhost:8080/' -d '{"sort":[ {"column": "name", "direction": "desc"} ], "limit": 2, "start": 1}' | jq
|
|
174
|
+
{
|
|
175
|
+
"status": "success",
|
|
176
|
+
"error": "",
|
|
177
|
+
"data": [
|
|
178
|
+
{
|
|
179
|
+
"id": "1-2-3-6",
|
|
180
|
+
"name": "Greg",
|
|
181
|
+
"username": "greg",
|
|
182
|
+
"age": 37,
|
|
183
|
+
"company": {
|
|
184
|
+
"id": "7-7-7-7",
|
|
185
|
+
"name": "Jane's Cool Stuff"
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"id": "1-2-3-7",
|
|
190
|
+
"name": "Curious George",
|
|
191
|
+
"username": "curious",
|
|
192
|
+
"age": 7,
|
|
193
|
+
"company": {
|
|
194
|
+
"id": "3-3-3-3",
|
|
195
|
+
"name": "New Venture"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
],
|
|
199
|
+
"pagination": {
|
|
200
|
+
"number_results": 4,
|
|
201
|
+
"limit": 2,
|
|
202
|
+
"next_page": {
|
|
203
|
+
"start": 3
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"input_errors": {}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Note that sorting on columns in related models is done via the syntax `relationship_column.column_name`. These
|
|
212
|
+
must be listed as such in the list of sortable/searchable columns, and then you use the same name to sort/search
|
|
213
|
+
by them:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
$ curl 'http://localhost:8080/' -d '{"sort":[ {"column": "company.name", "direction": "desc"}, {"column": "age", "direction": "asc"} ]}' | jq
|
|
217
|
+
{
|
|
218
|
+
"status": "success",
|
|
219
|
+
"error": "",
|
|
220
|
+
"data": [
|
|
221
|
+
{
|
|
222
|
+
"id": "1-2-3-7",
|
|
223
|
+
"name": "Curious George",
|
|
224
|
+
"username": "curious",
|
|
225
|
+
"age": 7,
|
|
226
|
+
"company": {
|
|
227
|
+
"id": "3-3-3-3",
|
|
228
|
+
"name": "New Venture"
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"id": "1-2-3-6",
|
|
233
|
+
"name": "Greg",
|
|
234
|
+
"username": "greg",
|
|
235
|
+
"age": 37,
|
|
236
|
+
"company": {
|
|
237
|
+
"id": "7-7-7-7",
|
|
238
|
+
"name": "Jane's Cool Stuff"
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"id": "1-2-3-5",
|
|
243
|
+
"name": "Jane Doe",
|
|
244
|
+
"username": "janedoe",
|
|
245
|
+
"age": 52,
|
|
246
|
+
"company": {
|
|
247
|
+
"id": "7-7-7-7",
|
|
248
|
+
"name": "Jane's Cool Stuff"
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"id": "1-2-3-4",
|
|
253
|
+
"name": "Bob Brown",
|
|
254
|
+
"username": "bobbrown",
|
|
255
|
+
"age": 18,
|
|
256
|
+
"company": {
|
|
257
|
+
"id": "5-5-5-5",
|
|
258
|
+
"name": "Bob's Widgets"
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
"pagination": {
|
|
263
|
+
"number_results": 4,
|
|
264
|
+
"limit": 50,
|
|
265
|
+
"next_page": {}
|
|
266
|
+
},
|
|
267
|
+
"input_errors": {}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
And finally searching:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
$ curl 'http://localhost:8080/' -d '{"where":[ {"column": "age", "operator": "<=", "value": 37}, {"column": "username", "operator": "in", "value": ["curious", "greg"]} ]}' | jq
|
|
276
|
+
{
|
|
277
|
+
"status": "success",
|
|
278
|
+
"error": "",
|
|
279
|
+
"data": [
|
|
280
|
+
{
|
|
281
|
+
"id": "1-2-3-7",
|
|
282
|
+
"name": "Curious George",
|
|
283
|
+
"username": "curious",
|
|
284
|
+
"age": 7,
|
|
285
|
+
"company": {
|
|
286
|
+
"id": "3-3-3-3",
|
|
287
|
+
"name": "New Venture"
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"id": "1-2-3-6",
|
|
292
|
+
"name": "Greg",
|
|
293
|
+
"username": "greg",
|
|
294
|
+
"age": 37,
|
|
295
|
+
"company": {
|
|
296
|
+
"id": "7-7-7-7",
|
|
297
|
+
"name": "Jane's Cool Stuff"
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
],
|
|
301
|
+
"pagination": {
|
|
302
|
+
"number_results": 2,
|
|
303
|
+
"limit": 50,
|
|
304
|
+
"next_page": {}
|
|
305
|
+
},
|
|
306
|
+
"input_errors": {}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
In terms of the allowed search operators, the standard list of operators is:
|
|
312
|
+
|
|
313
|
+
* `<=>`
|
|
314
|
+
* `!=`
|
|
315
|
+
* `<=`
|
|
316
|
+
* `>=`
|
|
317
|
+
* `>`
|
|
318
|
+
* `<`
|
|
319
|
+
* `=`
|
|
320
|
+
* `in`
|
|
321
|
+
* `is not null`
|
|
322
|
+
* `is null`
|
|
323
|
+
* `is not`
|
|
324
|
+
* `is`
|
|
325
|
+
* `like`
|
|
326
|
+
|
|
327
|
+
Although not all operators are supported by all columns. You can use `%` with the `LIKE` operator
|
|
328
|
+
to perform a wildcard search.
|
|
329
|
+
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def allowed_request_keys(self) -> list[str]:
|
|
334
|
+
return self.internal_request_keys
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def internal_request_keys(self) -> list[str]:
|
|
338
|
+
return ["sort", "limit", "where"]
|
|
339
|
+
|
|
340
|
+
def check_request_data(
|
|
341
|
+
self, request_data: dict[str, Any], query_parameters: dict[str, Any], pagination_data: dict[str, Any]
|
|
342
|
+
) -> None:
|
|
343
|
+
if pagination_data:
|
|
344
|
+
error = self.model.validate_pagination_data(pagination_data, self.auto_case_internal_column_name)
|
|
345
|
+
if error:
|
|
346
|
+
raise exceptions.ClientError(error)
|
|
347
|
+
if query_parameters:
|
|
348
|
+
raise exceptions.ClientError("Query parameters were found but are not supported.")
|
|
349
|
+
for key in request_data.keys():
|
|
350
|
+
if key not in self.allowed_request_keys:
|
|
351
|
+
raise exceptions.ClientError(
|
|
352
|
+
f"Invalid request parameter found in request body: '{key}'. Expected parameters: "
|
|
353
|
+
+ ", ".join([self.auto_case_internal_column_name(key) for key in self.allowed_request_keys])
|
|
354
|
+
)
|
|
355
|
+
self.validate_limit(request_data, {})
|
|
356
|
+
sort_key_name = self.auto_case_internal_column_name("sort")
|
|
357
|
+
sort = request_data.get(sort_key_name, [])
|
|
358
|
+
if not isinstance(sort, list):
|
|
359
|
+
raise exceptions.ClientError(
|
|
360
|
+
f"'{sort_key_name}' property in request body should be a list, but I found a value of type "
|
|
361
|
+
+ sort.__class__.__name
|
|
362
|
+
)
|
|
363
|
+
if sort:
|
|
364
|
+
column_key_name = self.auto_case_internal_column_name("column")
|
|
365
|
+
direction_key_name = self.auto_case_internal_column_name("direction")
|
|
366
|
+
for index, sort_entry in enumerate(sort):
|
|
367
|
+
if not isinstance(sort_entry, dict):
|
|
368
|
+
raise exceptions.ClientError(
|
|
369
|
+
f"'{sort_key_name}' should be a list of dictionaries, but entry #{index + 1} is a value of type '{sort_entry.__class__.__name}', not a dict"
|
|
370
|
+
)
|
|
371
|
+
for key_name in [column_key_name, direction_key_name]:
|
|
372
|
+
if not sort_entry.get(key_name):
|
|
373
|
+
raise exceptions.ClientError(
|
|
374
|
+
f"Each entry in the sort list should contain both '{column_key_name}' and '{direction_key_name}' but entry #{index + 1} is missing '{key_name}'"
|
|
375
|
+
)
|
|
376
|
+
if not isinstance(sort_entry[key_name], str):
|
|
377
|
+
raise exceptions.ClientError(
|
|
378
|
+
f"{key_name}' must be a string, but for entry #{index + 1} it is a value of type "
|
|
379
|
+
+ sort_entry[key_name].__class__.__name__
|
|
380
|
+
)
|
|
381
|
+
if sort_entry[direction_key_name].lower() not in ["asc", "desc"]:
|
|
382
|
+
raise exceptions.ClientError(
|
|
383
|
+
f"{direction_key_name}' must be either 'ASC' or 'DESC', but a different value was found for entry #{index + 1}"
|
|
384
|
+
)
|
|
385
|
+
if self.auto_case_column_name(sort_entry[column_key_name], False) not in self.sortable_column_names:
|
|
386
|
+
raise exceptions.ClientError(
|
|
387
|
+
f"Invalid sort column for entry #{index + 1}. Allowed values are: "
|
|
388
|
+
+ ", ".join(
|
|
389
|
+
[
|
|
390
|
+
self.auto_case_column_name(column_name, False)
|
|
391
|
+
for column_name in self.sortable_column_names
|
|
392
|
+
]
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
where_key_name = self.auto_case_internal_column_name("where")
|
|
396
|
+
where = request_data.get(where_key_name, [])
|
|
397
|
+
if not isinstance(where, list):
|
|
398
|
+
raise exceptions.ClientError(
|
|
399
|
+
f"'{where_key_name}' property in request body should be a list, but I found a value of type "
|
|
400
|
+
+ where.__class__.__name
|
|
401
|
+
)
|
|
402
|
+
if where:
|
|
403
|
+
column_key_name = self.auto_case_internal_column_name("column")
|
|
404
|
+
operator_key_name = self.auto_case_internal_column_name("operator")
|
|
405
|
+
value_key_name = self.auto_case_internal_column_name("value")
|
|
406
|
+
for index, where_entry in enumerate(where):
|
|
407
|
+
if not isinstance(where_entry, dict):
|
|
408
|
+
raise exceptions.ClientError(
|
|
409
|
+
f"'{where_key_name}' should be a list of dictionaries, but entry #{index + 1} is a value of type '{where_entry.__class__.__name}', not a dict"
|
|
410
|
+
)
|
|
411
|
+
for key_name in [column_key_name, operator_key_name, value_key_name]:
|
|
412
|
+
if key_name not in where_entry:
|
|
413
|
+
raise exceptions.ClientError(
|
|
414
|
+
f"Each entry in the where list should contain '{column_key_name}', '{operator_key_name}', and '{value_key_name}', but entry #{index + 1} is missing '{key_name}'"
|
|
415
|
+
)
|
|
416
|
+
if key_name != value_key_name and not isinstance(where_entry[key_name], str):
|
|
417
|
+
raise exceptions.ClientError(
|
|
418
|
+
f"{key_name}' must be a string, but for entry #{index + 1} it is a value of type "
|
|
419
|
+
+ sort_entry[key_name].__class__.__name__
|
|
420
|
+
)
|
|
421
|
+
if where_entry[column_key_name] not in self.searchable_column_names:
|
|
422
|
+
raise exceptions.ClientError(
|
|
423
|
+
f"Invalid where column for entry #{index + 1}. Allowed values are: "
|
|
424
|
+
+ ", ".join(
|
|
425
|
+
[
|
|
426
|
+
self.auto_case_column_name(column_name, True)
|
|
427
|
+
for column_name in self.searchable_column_names
|
|
428
|
+
]
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
[relationship_column_name, column_name] = self.unpack_column_name_with_relationship(
|
|
432
|
+
self.auto_case_column_name(where_entry[column_key_name], False),
|
|
433
|
+
)
|
|
434
|
+
operator = where_entry[operator_key_name].lower()
|
|
435
|
+
value = where_entry[value_key_name]
|
|
436
|
+
error_allowed_operators = None
|
|
437
|
+
if relationship_column_name:
|
|
438
|
+
column = self.columns[relationship_column_name]
|
|
439
|
+
if not column.is_allowed_search_operator(operator, relationship_reference=column_name):
|
|
440
|
+
error_allowed_operators = column.allowed_search_operators(
|
|
441
|
+
relationship_reference=column_name
|
|
442
|
+
)
|
|
443
|
+
else:
|
|
444
|
+
error = column.check_search_value(
|
|
445
|
+
value if operator != "in" else value[0],
|
|
446
|
+
where_entry[operator_key_name],
|
|
447
|
+
relationship_reference=column_name,
|
|
448
|
+
)
|
|
449
|
+
else:
|
|
450
|
+
column = self.columns[column_name]
|
|
451
|
+
if not column.is_allowed_search_operator(operator):
|
|
452
|
+
error_allowed_operators = column.allowed_search_operators()
|
|
453
|
+
else:
|
|
454
|
+
error = column.check_search_value(
|
|
455
|
+
value if operator != "in" else value[0], where_entry[operator_key_name]
|
|
456
|
+
)
|
|
457
|
+
if error_allowed_operators:
|
|
458
|
+
raise exceptions.ClientError(
|
|
459
|
+
f"Invalid operator for entry #{index + 1}. Allowed operators are: "
|
|
460
|
+
+ ", ".join(column.allowed_search_operators(relationship_reference=column_name))
|
|
461
|
+
)
|
|
462
|
+
if error:
|
|
463
|
+
raise exceptions.ClientError(f"Invalid search value for entry #{index + 1}: {error}")
|
|
464
|
+
|
|
465
|
+
def configure_model_from_request_data(
|
|
466
|
+
self,
|
|
467
|
+
model: Model,
|
|
468
|
+
request_data: dict[str, Any],
|
|
469
|
+
query_parameters: dict[str, Any],
|
|
470
|
+
pagination_data: dict[str, Any],
|
|
471
|
+
) -> Model:
|
|
472
|
+
if pagination_data:
|
|
473
|
+
model = model.pagination(**pagination_data)
|
|
474
|
+
sort = request_data.get(self.auto_case_internal_column_name("sort"), [])
|
|
475
|
+
if sort:
|
|
476
|
+
column_key_name = self.auto_case_internal_column_name("column")
|
|
477
|
+
direction_key_name = self.auto_case_internal_column_name("direction")
|
|
478
|
+
model = self.add_join(sort[0][column_key_name], model)
|
|
479
|
+
[primary_table_name, primary_column_name] = self.resolve_references_for_query(sort[0][column_key_name])
|
|
480
|
+
primary_direction = sort[0][direction_key_name]
|
|
481
|
+
|
|
482
|
+
if len(sort) > 1:
|
|
483
|
+
[secondary_table_name, secondary_column_name] = self.resolve_references_for_query(
|
|
484
|
+
sort[1][column_key_name]
|
|
485
|
+
)
|
|
486
|
+
secondary_direction = sort[1][direction_key_name]
|
|
487
|
+
else:
|
|
488
|
+
secondary_column_name = ""
|
|
489
|
+
secondary_direction = ""
|
|
490
|
+
secondary_table_name = ""
|
|
491
|
+
model = model.sort_by(
|
|
492
|
+
primary_column_name if primary_column_name else "",
|
|
493
|
+
primary_direction if primary_direction else "",
|
|
494
|
+
primary_table_name=primary_table_name if primary_table_name else "",
|
|
495
|
+
secondary_column_name=secondary_column_name if secondary_column_name else "",
|
|
496
|
+
secondary_direction=secondary_direction if secondary_direction else "",
|
|
497
|
+
secondary_table_name=secondary_table_name if secondary_table_name else "",
|
|
498
|
+
)
|
|
499
|
+
if request_data.get("limit"):
|
|
500
|
+
model = model.limit(request_data["limit"])
|
|
501
|
+
|
|
502
|
+
for where in request_data.get(self.auto_case_internal_column_name("where"), []):
|
|
503
|
+
raw_column_name = self.auto_case_column_name(where[self.auto_case_internal_column_name("column")], False)
|
|
504
|
+
[relationship_column_name, column_name] = self.unpack_column_name_with_relationship(raw_column_name)
|
|
505
|
+
operator = where[self.auto_case_internal_column_name("operator")].lower()
|
|
506
|
+
value = where[self.auto_case_internal_column_name("value")]
|
|
507
|
+
|
|
508
|
+
model = self.add_join(raw_column_name, model)
|
|
509
|
+
if relationship_column_name:
|
|
510
|
+
model = self.columns[relationship_column_name].add_search(
|
|
511
|
+
model, value, operator=operator, relationship_reference=column_name
|
|
512
|
+
)
|
|
513
|
+
else:
|
|
514
|
+
model = self.columns[column_name].add_search(model, value, operator=operator)
|
|
515
|
+
|
|
516
|
+
return model
|
|
517
|
+
|
|
518
|
+
def documentation_url_search_parameters(self) -> list[autodoc.request.Parameter]:
|
|
519
|
+
return []
|