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,232 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable, Self, overload
|
|
5
|
+
|
|
6
|
+
import dateparser
|
|
7
|
+
|
|
8
|
+
from clearskies import configs, decorators
|
|
9
|
+
from clearskies.autodoc.schema import Datetime as AutoDocDatetime
|
|
10
|
+
from clearskies.columns.datetime import Datetime
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from clearskies import Model, typing
|
|
14
|
+
from clearskies.autodoc.schema import Schema as AutoDocSchema
|
|
15
|
+
from clearskies.query import Condition
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Date(Datetime):
|
|
19
|
+
"""
|
|
20
|
+
Stores date data in a column.
|
|
21
|
+
|
|
22
|
+
This is specifically for a column that only stores date information - not time information. When processing user input,
|
|
23
|
+
this value is passed through `dateparser.parse()` to decide if it is a proper date string. This makes for relatively
|
|
24
|
+
flexible input validation. Example:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import clearskies
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MyModel(clearskies.Model):
|
|
31
|
+
backend = clearskies.backends.MemoryBackend()
|
|
32
|
+
id_column_name = "id"
|
|
33
|
+
|
|
34
|
+
id = clearskies.columns.Uuid()
|
|
35
|
+
name = clearskies.columns.String()
|
|
36
|
+
my_date = clearskies.columns.Date()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
40
|
+
clearskies.endpoints.Create(
|
|
41
|
+
MyModel,
|
|
42
|
+
writeable_column_names=["name", "my_date"],
|
|
43
|
+
readable_column_names=["id", "name", "my_date"],
|
|
44
|
+
),
|
|
45
|
+
classes=[MyModel],
|
|
46
|
+
)
|
|
47
|
+
wsgi()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
And when invoked:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
$ curl 'http://localhost:8080' -d '{"name":"Bob", "my_date":"May 5th 2025"}' | jq
|
|
54
|
+
{
|
|
55
|
+
"status": "success",
|
|
56
|
+
"error": "",
|
|
57
|
+
"data": {
|
|
58
|
+
"id": "a8c8ac79-bc28-4b24-9728-e85f13fc4104",
|
|
59
|
+
"name": "Bob",
|
|
60
|
+
"my_date": "2025-05-05"
|
|
61
|
+
},
|
|
62
|
+
"pagination": {},
|
|
63
|
+
"input_errors": {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
$ curl 'http://localhost:8080' -d '{"name":"Bob", "my_date":"2025-05-03"}' | jq
|
|
67
|
+
{
|
|
68
|
+
"status": "success",
|
|
69
|
+
"error": "",
|
|
70
|
+
"data": {
|
|
71
|
+
"id": "21376ae7-4090-4c2b-a50b-8d932ad5dac1",
|
|
72
|
+
"name": "Bob",
|
|
73
|
+
"my_date": "2025-05-03"
|
|
74
|
+
},
|
|
75
|
+
"pagination": {},
|
|
76
|
+
"input_errors": {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
$ curl 'http://localhost:8080' -d '{"name":"Bob", "my_date":"not a date"}' | jq
|
|
80
|
+
{
|
|
81
|
+
"status": "input_errors",
|
|
82
|
+
"error": "",
|
|
83
|
+
"data": [],
|
|
84
|
+
"pagination": {},
|
|
85
|
+
"input_errors": {
|
|
86
|
+
"my_date": "given value did not appear to be a valid date"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
date_format = configs.String(default="%Y-%m-%d")
|
|
93
|
+
backend_default = configs.String(default="0000-00-00")
|
|
94
|
+
|
|
95
|
+
default = configs.Datetime() # type: ignore
|
|
96
|
+
setable = configs.DatetimeOrCallable(default=None) # type: ignore
|
|
97
|
+
|
|
98
|
+
_allowed_search_operators = ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null"]
|
|
99
|
+
|
|
100
|
+
auto_doc_class: type[AutoDocSchema] = AutoDocDatetime
|
|
101
|
+
_descriptor_config_map = None
|
|
102
|
+
|
|
103
|
+
@decorators.parameters_to_properties
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
date_format: str = "%Y-%m-%d",
|
|
107
|
+
backend_default: str = "0000-00-00",
|
|
108
|
+
default: datetime.datetime | None = None,
|
|
109
|
+
setable: datetime.datetime | Callable[..., datetime.datetime] | None = None,
|
|
110
|
+
is_readable: bool = True,
|
|
111
|
+
is_writeable: bool = True,
|
|
112
|
+
is_searchable: bool = True,
|
|
113
|
+
is_temporary: bool = False,
|
|
114
|
+
validators: typing.validator | list[typing.validator] = [],
|
|
115
|
+
on_change_pre_save: typing.action | list[typing.action] = [],
|
|
116
|
+
on_change_post_save: typing.action | list[typing.action] = [],
|
|
117
|
+
on_change_save_finished: typing.action | list[typing.action] = [],
|
|
118
|
+
created_by_source_type: str = "",
|
|
119
|
+
created_by_source_key: str = "",
|
|
120
|
+
created_by_source_strict: bool = True,
|
|
121
|
+
):
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
def from_backend(self, value) -> datetime.date | None: # type: ignore
|
|
125
|
+
if not value or value == self.backend_default:
|
|
126
|
+
return None
|
|
127
|
+
if isinstance(value, str):
|
|
128
|
+
value = dateparser.parse(value)
|
|
129
|
+
if not isinstance(value, datetime.datetime):
|
|
130
|
+
raise TypeError(
|
|
131
|
+
f"I was expecting to get a datetime from the backend but I didn't get anything recognizable. I have a value of type '{value.__class__.__name__}'. I need either a datetime object or a datetime serialized as a string."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return datetime.date(value.year, value.month, value.day)
|
|
135
|
+
|
|
136
|
+
def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
137
|
+
if self.name not in data or isinstance(data[self.name], str) or data[self.name] is None:
|
|
138
|
+
return data
|
|
139
|
+
|
|
140
|
+
value = data[self.name]
|
|
141
|
+
if not isinstance(data[self.name], datetime.datetime) and not isinstance(data[self.name], datetime.date):
|
|
142
|
+
raise TypeError(
|
|
143
|
+
f"I was expecting a stringified-date or a datetime object to send to the backend, but instead I found a value of {value.__class__.__name__}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
**data,
|
|
148
|
+
self.name: value.strftime(self.date_format),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@overload # type: ignore
|
|
152
|
+
def __get__(self, instance: None, cls: type[Model]) -> Self:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
@overload
|
|
156
|
+
def __get__(self, instance: Model, cls: type[Model]) -> datetime.date:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
def __get__(self, instance, cls):
|
|
160
|
+
return super().__get__(instance, cls)
|
|
161
|
+
|
|
162
|
+
def __set__(self, instance, value: datetime.datetime | datetime.date) -> None:
|
|
163
|
+
# this makes sure we're initialized
|
|
164
|
+
if "name" not in self._config: # type: ignore
|
|
165
|
+
instance.get_columns()
|
|
166
|
+
|
|
167
|
+
instance._next_data[self.name] = value
|
|
168
|
+
|
|
169
|
+
def equals(self, value: str | datetime.datetime | datetime.date) -> Condition:
|
|
170
|
+
return super().equals(value) # type: ignore
|
|
171
|
+
|
|
172
|
+
def spaceship(self, value: str | datetime.datetime | datetime.date) -> Condition:
|
|
173
|
+
return super().spaceship(value) # type: ignore
|
|
174
|
+
|
|
175
|
+
def not_equals(self, value: str | datetime.datetime | datetime.date) -> Condition:
|
|
176
|
+
return super().not_equals(value) # type: ignore
|
|
177
|
+
|
|
178
|
+
def less_than_equals(self, value: str | datetime.datetime | datetime.date) -> Condition:
|
|
179
|
+
return super().less_than_equals(value) # type: ignore
|
|
180
|
+
|
|
181
|
+
def greater_than_equals(self, value: str | datetime.datetime | datetime.date) -> Condition:
|
|
182
|
+
return super().greater_than_equals(value) # type: ignore
|
|
183
|
+
|
|
184
|
+
def less_than(self, value: str | datetime.datetime | datetime.date) -> Condition:
|
|
185
|
+
return super().less_than(value) # type: ignore
|
|
186
|
+
|
|
187
|
+
def greater_than(self, value: str | datetime.datetime | datetime.date) -> Condition:
|
|
188
|
+
return super().greater_than(value) # type: ignore
|
|
189
|
+
|
|
190
|
+
def is_in(self, values: list[str | datetime.datetime | datetime.date]) -> Condition: # type: ignore
|
|
191
|
+
return super().is_in(values) # type: ignore
|
|
192
|
+
|
|
193
|
+
def input_error_for_value(self, value, operator=None):
|
|
194
|
+
value = dateparser.parse(value)
|
|
195
|
+
if not value:
|
|
196
|
+
return "given value did not appear to be a valid date"
|
|
197
|
+
return ""
|
|
198
|
+
|
|
199
|
+
def values_match(self, value_1, value_2):
|
|
200
|
+
"""Compare two values to see if they are the same."""
|
|
201
|
+
# in this function we deal with data directly out of the backend, so our date is likely
|
|
202
|
+
# to be string-ified and we want to look for default (e.g. null) values in string form.
|
|
203
|
+
if type(value_1) == str and ("0000-00-00" in value_1 or value_1 == self.backend_default):
|
|
204
|
+
value_1 = None
|
|
205
|
+
if type(value_2) == str and ("0000-00-00" in value_2 or value_2 == self.backend_default):
|
|
206
|
+
value_2 = None
|
|
207
|
+
number_values = 0
|
|
208
|
+
if value_1:
|
|
209
|
+
number_values += 1
|
|
210
|
+
if value_2:
|
|
211
|
+
number_values += 1
|
|
212
|
+
if number_values == 0:
|
|
213
|
+
return True
|
|
214
|
+
if number_values == 1:
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
if type(value_1) == str:
|
|
218
|
+
value_1 = dateparser.parse(value_1)
|
|
219
|
+
value_1 = datetime.date(value_1.year, value_1.month, value_1.day) if value_1 else None
|
|
220
|
+
if type(value_2) == str:
|
|
221
|
+
value_2 = dateparser.parse(value_2)
|
|
222
|
+
value_2 = datetime.date(value_2.year, value_2.month, value_2.day) if value_2 else None
|
|
223
|
+
|
|
224
|
+
# two times can be the same but if one is datetime-aware and one is not, python will treat them as not equal.
|
|
225
|
+
# we want to treat such times as being the same. Therefore, check for equality but ignore the timezone.
|
|
226
|
+
for to_check in ["year", "month", "day"]:
|
|
227
|
+
if getattr(value_1, to_check) != getattr(value_2, to_check):
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
# and since we already converted the timezones to match (or one has a timezone and one doesn't), we're good to go.
|
|
231
|
+
# if we passed the above loop then the times are the same.
|
|
232
|
+
return True
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable, Self, overload
|
|
5
|
+
|
|
6
|
+
import dateparser # type: ignore
|
|
7
|
+
|
|
8
|
+
from clearskies import configs, decorators
|
|
9
|
+
from clearskies.autodoc.schema import Datetime as AutoDocDatetime
|
|
10
|
+
from clearskies.column import Column
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from clearskies import Model, typing
|
|
14
|
+
from clearskies.autodoc.schema import Schema as AutoDocSchema
|
|
15
|
+
from clearskies.query import Condition
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Datetime(Column):
|
|
19
|
+
"""
|
|
20
|
+
Stores date+time data in a column.
|
|
21
|
+
|
|
22
|
+
When processing user input, this value is passed through `dateparser.parse()` to decide if it is a proper date string.
|
|
23
|
+
This makes for relatively flexible input validation. Example:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import clearskies
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MyModel(clearskies.Model):
|
|
30
|
+
backend = clearskies.backends.MemoryBackend()
|
|
31
|
+
id_column_name = "id"
|
|
32
|
+
|
|
33
|
+
id = clearskies.columns.Uuid()
|
|
34
|
+
name = clearskies.columns.String()
|
|
35
|
+
my_datetime = clearskies.columns.Datetime()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
39
|
+
clearskies.endpoints.Create(
|
|
40
|
+
MyModel,
|
|
41
|
+
writeable_column_names=["name", "my_datetime"],
|
|
42
|
+
readable_column_names=["id", "name", "my_datetime"],
|
|
43
|
+
),
|
|
44
|
+
classes=[MyModel],
|
|
45
|
+
)
|
|
46
|
+
wsgi()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
And when invoked:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
$ curl 'http://localhost:8080' -d '{"name":"Bob", "my_datetime":"2025-05-13 12:35:45+00:00"}' | jq
|
|
53
|
+
{
|
|
54
|
+
"status": "success",
|
|
55
|
+
"error": "",
|
|
56
|
+
"data": {
|
|
57
|
+
"id": "68095d0d-c909-4ab3-8c15-bd2667b7b074",
|
|
58
|
+
"name": "Bob",
|
|
59
|
+
"my_datetime": "2025-05-13T12:35:45+00:00"
|
|
60
|
+
},
|
|
61
|
+
"pagination": {},
|
|
62
|
+
"input_errors": {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
$ curl 'http://localhost:8080' -d '{"name":"Bob", "my_datetime":"May 13th 2025 2:35:45UTC"}' | jq
|
|
66
|
+
{
|
|
67
|
+
"status": "success",
|
|
68
|
+
"error": "",
|
|
69
|
+
"data": {
|
|
70
|
+
"id": "9fea6933-86ac-4dd1-b9e0-a9fa50608410",
|
|
71
|
+
"name": "Bob",
|
|
72
|
+
"my_datetime": "2025-05-13T12:35:45+00:00"
|
|
73
|
+
},
|
|
74
|
+
"pagination": {},
|
|
75
|
+
"input_errors": {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
$ curl 'http://localhost:8080' -d '{"name":"Bob", "my_datetime":"not a date"}' | jq
|
|
79
|
+
{
|
|
80
|
+
"status": "input_errors",
|
|
81
|
+
"error": "",
|
|
82
|
+
"data": [],
|
|
83
|
+
"pagination": {},
|
|
84
|
+
"input_errors": {
|
|
85
|
+
"my_datetime": "given value did not appear to be a valid date"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
Whether or not to make datetime objects timezone-aware
|
|
93
|
+
"""
|
|
94
|
+
timezone_aware = configs.Boolean(default=True)
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
The timezone to use for the datetime object (if it is timezone aware)
|
|
98
|
+
"""
|
|
99
|
+
timezone = configs.Timezone(default=datetime.timezone.utc)
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
The format string to use when sending to the backend (default: %Y-%m-%d %H:%M:%S)
|
|
103
|
+
"""
|
|
104
|
+
date_format = configs.String(default="%Y-%m-%d %H:%M:%S")
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
A default value to set for this column.
|
|
108
|
+
|
|
109
|
+
The default is only used when creating a record for the first time, and only if
|
|
110
|
+
a value for this column has not been set.
|
|
111
|
+
"""
|
|
112
|
+
default = configs.Datetime() # type: ignore
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
Sets a default date that the backend is going to provide.
|
|
116
|
+
|
|
117
|
+
Some backends, depending on configuration, may provide a default value for the column
|
|
118
|
+
instead of null. By setting this equal to that default value, clearskies can detect
|
|
119
|
+
when a given value is actually a non-value.
|
|
120
|
+
"""
|
|
121
|
+
backend_default = configs.String(default="0000-00-00 00:00:00")
|
|
122
|
+
|
|
123
|
+
setable = configs.DatetimeOrCallable(default=None) # type: ignore
|
|
124
|
+
_allowed_search_operators = ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null"]
|
|
125
|
+
auto_doc_class: type[AutoDocSchema] = AutoDocDatetime
|
|
126
|
+
_descriptor_config_map = None
|
|
127
|
+
|
|
128
|
+
@decorators.parameters_to_properties
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
date_format: str = "%Y-%m-%d %H:%M:%S",
|
|
132
|
+
backend_default: str = "0000-00-00 00:00:00",
|
|
133
|
+
timezone_aware: bool = True,
|
|
134
|
+
timezone: datetime.timezone = datetime.timezone.utc,
|
|
135
|
+
default: datetime.datetime | None = None,
|
|
136
|
+
setable: datetime.datetime | Callable[..., datetime.datetime] | None = None,
|
|
137
|
+
is_readable: bool = True,
|
|
138
|
+
is_writeable: bool = True,
|
|
139
|
+
is_searchable: bool = True,
|
|
140
|
+
is_temporary: bool = False,
|
|
141
|
+
validators: typing.validator | list[typing.validator] = [],
|
|
142
|
+
on_change_pre_save: typing.action | list[typing.action] = [],
|
|
143
|
+
on_change_post_save: typing.action | list[typing.action] = [],
|
|
144
|
+
on_change_save_finished: typing.action | list[typing.action] = [],
|
|
145
|
+
created_by_source_type: str = "",
|
|
146
|
+
created_by_source_key: str = "",
|
|
147
|
+
created_by_source_strict: bool = True,
|
|
148
|
+
):
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
def from_backend(self, value) -> datetime.datetime | None:
|
|
152
|
+
if not value or value == self.backend_default:
|
|
153
|
+
return None
|
|
154
|
+
if isinstance(value, str):
|
|
155
|
+
value = dateparser.parse(value)
|
|
156
|
+
if not isinstance(value, datetime.datetime):
|
|
157
|
+
raise TypeError(
|
|
158
|
+
f"I was expecting to get a datetime from the backend but I didn't get anything recognizable. I have a value of type '{value.__class__.__name__}'. I need either a datetime object or a datetime serialized as a string."
|
|
159
|
+
)
|
|
160
|
+
if self.timezone_aware:
|
|
161
|
+
if not value.tzinfo:
|
|
162
|
+
value = value.replace(tzinfo=self.timezone)
|
|
163
|
+
elif value.tzinfo != self.timezone:
|
|
164
|
+
value = value.astimezone(self.timezone)
|
|
165
|
+
else:
|
|
166
|
+
value = value.replace(tzinfo=None)
|
|
167
|
+
|
|
168
|
+
return value
|
|
169
|
+
|
|
170
|
+
def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
171
|
+
if self.name not in data or isinstance(data[self.name], str) or data[self.name] is None:
|
|
172
|
+
return data
|
|
173
|
+
|
|
174
|
+
value = data[self.name]
|
|
175
|
+
if not isinstance(data[self.name], datetime.datetime):
|
|
176
|
+
raise TypeError(
|
|
177
|
+
f"I was expecting a stringified-date or a datetime object to send to the backend, but instead I found a value of {value.__class__.__name__}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
**data,
|
|
182
|
+
self.name: value.strftime(self.date_format),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
def to_json(self, model: Model) -> dict[str, Any]:
|
|
186
|
+
"""Grabs the column out of the model and converts it into a representation that can be turned into JSON."""
|
|
187
|
+
value = self.__get__(model, model.__class__)
|
|
188
|
+
if value and (isinstance(value, datetime.datetime) or isinstance(value, datetime.date)):
|
|
189
|
+
value = value.isoformat() # type: ignore
|
|
190
|
+
|
|
191
|
+
return {self.name: value}
|
|
192
|
+
|
|
193
|
+
@overload
|
|
194
|
+
def __get__(self, instance: None, cls: type[Model]) -> Self:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
@overload
|
|
198
|
+
def __get__(self, instance: Model, cls: type[Model]) -> datetime.datetime:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
def __get__(self, instance, cls):
|
|
202
|
+
return super().__get__(instance, cls)
|
|
203
|
+
|
|
204
|
+
def __set__(self, instance, value: datetime.datetime) -> None:
|
|
205
|
+
# this makes sure we're initialized
|
|
206
|
+
if "name" not in self._config: # type: ignore
|
|
207
|
+
instance.get_columns()
|
|
208
|
+
|
|
209
|
+
instance._next_data[self.name] = value
|
|
210
|
+
|
|
211
|
+
def equals(self, value: str | datetime.datetime) -> Condition:
|
|
212
|
+
return super().equals(value)
|
|
213
|
+
|
|
214
|
+
def spaceship(self, value: str | datetime.datetime) -> Condition:
|
|
215
|
+
return super().spaceship(value)
|
|
216
|
+
|
|
217
|
+
def not_equals(self, value: str | datetime.datetime) -> Condition:
|
|
218
|
+
return super().not_equals(value)
|
|
219
|
+
|
|
220
|
+
def less_than_equals(self, value: str | datetime.datetime) -> Condition:
|
|
221
|
+
return super().less_than_equals(value)
|
|
222
|
+
|
|
223
|
+
def greater_than_equals(self, value: str | datetime.datetime) -> Condition:
|
|
224
|
+
return super().greater_than_equals(value)
|
|
225
|
+
|
|
226
|
+
def less_than(self, value: str | datetime.datetime) -> Condition:
|
|
227
|
+
return super().less_than(value)
|
|
228
|
+
|
|
229
|
+
def greater_than(self, value: str | datetime.datetime) -> Condition:
|
|
230
|
+
return super().greater_than(value)
|
|
231
|
+
|
|
232
|
+
def is_in(self, values: list[str | datetime.datetime]) -> Condition:
|
|
233
|
+
return super().is_in(values)
|
|
234
|
+
|
|
235
|
+
def input_error_for_value(self, value, operator=None):
|
|
236
|
+
value = dateparser.parse(value)
|
|
237
|
+
if not value:
|
|
238
|
+
return "given value did not appear to be a valid date"
|
|
239
|
+
if not value.tzinfo and self.timezone_aware:
|
|
240
|
+
return "date is missing timezone information"
|
|
241
|
+
return ""
|
|
242
|
+
|
|
243
|
+
def values_match(self, value_1: None | str | datetime.datetime, value_2: None | str | datetime.datetime) -> bool:
|
|
244
|
+
"""Compare two values to see if they are the same."""
|
|
245
|
+
# in this function we deal with data directly out of the backend, so our date is likely
|
|
246
|
+
# to be string-ified and we want to look for default (e.g. null) values in string form.
|
|
247
|
+
if isinstance(value_1, str) and ("0000-00-00" in value_1 or value_1 == self.backend_default):
|
|
248
|
+
value_1 = None
|
|
249
|
+
if isinstance(value_2, str) and ("0000-00-00" in value_2 or value_2 == self.backend_default):
|
|
250
|
+
value_2 = None
|
|
251
|
+
number_values = 0
|
|
252
|
+
if value_1:
|
|
253
|
+
number_values += 1
|
|
254
|
+
if value_2:
|
|
255
|
+
number_values += 1
|
|
256
|
+
if number_values == 0:
|
|
257
|
+
return True
|
|
258
|
+
if number_values == 1:
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
if isinstance(value_1, str):
|
|
262
|
+
value_1 = dateparser.parse(value_1)
|
|
263
|
+
if isinstance(value_2, str):
|
|
264
|
+
value_2 = dateparser.parse(value_2)
|
|
265
|
+
|
|
266
|
+
# If neither value is a datetime, we can't compare them so they don't match.
|
|
267
|
+
if not isinstance(value_1, datetime.datetime) or not isinstance(value_2, datetime.datetime):
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
# we need to make sure we're comparing in the same timezones. For our purposes, a difference in timezone
|
|
271
|
+
# is fine as long as they represent the same time (e.g. 16:00EST == 20:00UTC). For python, same time in different
|
|
272
|
+
# timezones is treated as different datetime objects.
|
|
273
|
+
if value_1.tzinfo is not None and value_2.tzinfo is not None:
|
|
274
|
+
value_1 = value_1.astimezone(value_2.tzinfo)
|
|
275
|
+
|
|
276
|
+
# two times can be the same but if one is datetime-aware and one is not, python will treat them as not equal.
|
|
277
|
+
# we want to treat such times as being the same. Therefore, check for equality but ignore the timezone.
|
|
278
|
+
for to_check in ["year", "month", "day", "hour", "minute", "second", "microsecond"]:
|
|
279
|
+
if getattr(value_1, to_check) != getattr(value_2, to_check):
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
# and since we already converted the timezones to match (or one has a timezone and one doesn't), we're good to go.
|
|
283
|
+
# if we passed the above loop then the times are the same.
|
|
284
|
+
return True
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from clearskies.columns.string import String
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Email(String):
|
|
9
|
+
"""
|
|
10
|
+
A string column that specifically expects an email.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import clearskies
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MyModel(clearskies.Model):
|
|
17
|
+
backend = clearskies.backends.MemoryBackend()
|
|
18
|
+
id_column_name = "id"
|
|
19
|
+
|
|
20
|
+
id = clearskies.columns.Uuid()
|
|
21
|
+
email = clearskies.columns.Email()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
25
|
+
clearskies.endpoints.Create(
|
|
26
|
+
MyModel,
|
|
27
|
+
writeable_column_names=["email"],
|
|
28
|
+
readable_column_names=["id", "email"],
|
|
29
|
+
),
|
|
30
|
+
classes=[MyModel],
|
|
31
|
+
)
|
|
32
|
+
wsgi()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
And when invoked:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
$ curl 'http://localhost:8080' -d '{"email":"test@example.com"}' | jq
|
|
39
|
+
{
|
|
40
|
+
"status": "success",
|
|
41
|
+
"error": "",
|
|
42
|
+
"data": {
|
|
43
|
+
"id": "2a72a895-c469-45b0-b5cd-5a3cbb3a6e99",
|
|
44
|
+
"email": "test@example.com"
|
|
45
|
+
},
|
|
46
|
+
"pagination": {},
|
|
47
|
+
"input_errors": {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
$ curl 'http://localhost:8080' -d '{"email":"asdf"}' | jq
|
|
51
|
+
{
|
|
52
|
+
"status": "input_errors",
|
|
53
|
+
"error": "",
|
|
54
|
+
"data": [],
|
|
55
|
+
"pagination": {},
|
|
56
|
+
"input_errors": {
|
|
57
|
+
"email": "Invalid email address"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
_descriptor_config_map = None
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
A column that always requires an email address.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def input_error_for_value(self, value: str, operator: str | None = None) -> str:
|
|
70
|
+
if not isinstance(value, str):
|
|
71
|
+
return f"Value must be a string for {self.name}"
|
|
72
|
+
if operator and operator.lower() == "like":
|
|
73
|
+
# don't check for an email if doing a fuzzy search, since we may be searching
|
|
74
|
+
# for a partial email
|
|
75
|
+
return ""
|
|
76
|
+
if re.search(r"^[^@\s]+@[^@]+\.[^@]+$", value):
|
|
77
|
+
return ""
|
|
78
|
+
return "Invalid email address"
|