clear-skies 2.0.27__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of clear-skies might be problematic. Click here for more details.
- clear_skies-2.0.27.dist-info/METADATA +78 -0
- clear_skies-2.0.27.dist-info/RECORD +270 -0
- clear_skies-2.0.27.dist-info/WHEEL +4 -0
- clear_skies-2.0.27.dist-info/licenses/LICENSE +7 -0
- clearskies/__init__.py +69 -0
- clearskies/action.py +7 -0
- clearskies/authentication/__init__.py +15 -0
- clearskies/authentication/authentication.py +44 -0
- clearskies/authentication/authorization.py +23 -0
- clearskies/authentication/authorization_pass_through.py +22 -0
- clearskies/authentication/jwks.py +165 -0
- clearskies/authentication/public.py +5 -0
- clearskies/authentication/secret_bearer.py +551 -0
- clearskies/autodoc/__init__.py +8 -0
- clearskies/autodoc/formats/__init__.py +5 -0
- clearskies/autodoc/formats/oai3_json/__init__.py +7 -0
- clearskies/autodoc/formats/oai3_json/oai3_json.py +87 -0
- clearskies/autodoc/formats/oai3_json/oai3_schema_resolver.py +15 -0
- clearskies/autodoc/formats/oai3_json/parameter.py +35 -0
- clearskies/autodoc/formats/oai3_json/request.py +68 -0
- clearskies/autodoc/formats/oai3_json/response.py +28 -0
- clearskies/autodoc/formats/oai3_json/schema/__init__.py +11 -0
- clearskies/autodoc/formats/oai3_json/schema/array.py +9 -0
- clearskies/autodoc/formats/oai3_json/schema/default.py +13 -0
- clearskies/autodoc/formats/oai3_json/schema/enum.py +7 -0
- clearskies/autodoc/formats/oai3_json/schema/object.py +35 -0
- clearskies/autodoc/formats/oai3_json/test.json +1985 -0
- clearskies/autodoc/py.typed +0 -0
- clearskies/autodoc/request/__init__.py +15 -0
- clearskies/autodoc/request/header.py +6 -0
- clearskies/autodoc/request/json_body.py +6 -0
- clearskies/autodoc/request/parameter.py +8 -0
- clearskies/autodoc/request/request.py +47 -0
- clearskies/autodoc/request/url_parameter.py +6 -0
- clearskies/autodoc/request/url_path.py +6 -0
- clearskies/autodoc/response/__init__.py +5 -0
- clearskies/autodoc/response/response.py +9 -0
- clearskies/autodoc/schema/__init__.py +31 -0
- clearskies/autodoc/schema/array.py +10 -0
- clearskies/autodoc/schema/base64.py +8 -0
- clearskies/autodoc/schema/boolean.py +5 -0
- clearskies/autodoc/schema/date.py +5 -0
- clearskies/autodoc/schema/datetime.py +5 -0
- clearskies/autodoc/schema/double.py +5 -0
- clearskies/autodoc/schema/enum.py +17 -0
- clearskies/autodoc/schema/integer.py +6 -0
- clearskies/autodoc/schema/long.py +5 -0
- clearskies/autodoc/schema/number.py +6 -0
- clearskies/autodoc/schema/object.py +13 -0
- clearskies/autodoc/schema/password.py +5 -0
- clearskies/autodoc/schema/schema.py +11 -0
- clearskies/autodoc/schema/string.py +5 -0
- clearskies/backends/__init__.py +67 -0
- clearskies/backends/api_backend.py +1194 -0
- clearskies/backends/backend.py +137 -0
- clearskies/backends/cursor_backend.py +339 -0
- clearskies/backends/graphql_backend.py +977 -0
- clearskies/backends/memory_backend.py +794 -0
- clearskies/backends/secrets_backend.py +100 -0
- clearskies/clients/__init__.py +5 -0
- clearskies/clients/graphql_client.py +182 -0
- 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 +145 -0
- clearskies/columns/belongs_to_self.py +109 -0
- clearskies/columns/boolean.py +110 -0
- clearskies/columns/category_tree.py +274 -0
- clearskies/columns/category_tree_ancestors.py +51 -0
- clearskies/columns/category_tree_children.py +125 -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 +552 -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 +281 -0
- clearskies/columns/many_to_many_models.py +163 -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 +11 -0
- clearskies/contexts/cli.py +130 -0
- clearskies/contexts/context.py +99 -0
- clearskies/contexts/wsgi.py +79 -0
- clearskies/contexts/wsgi_ref.py +87 -0
- clearskies/cursors/__init__.py +10 -0
- clearskies/cursors/cursor.py +161 -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 +15 -0
- clearskies/di/additional_config.py +130 -0
- clearskies/di/additional_config_auto_import.py +17 -0
- clearskies/di/di.py +948 -0
- clearskies/di/inject/__init__.py +25 -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/logger.py +16 -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 +106 -0
- clearskies/exceptions/__init__.py +19 -0
- clearskies/exceptions/authentication.py +2 -0
- clearskies/exceptions/authorization.py +2 -0
- clearskies/exceptions/client_error.py +2 -0
- clearskies/exceptions/input_errors.py +4 -0
- clearskies/exceptions/missing_dependency.py +2 -0
- clearskies/exceptions/moved_permanently.py +3 -0
- clearskies/exceptions/moved_temporarily.py +3 -0
- clearskies/exceptions/not_found.py +2 -0
- clearskies/functional/__init__.py +7 -0
- clearskies/functional/json.py +47 -0
- clearskies/functional/routing.py +92 -0
- clearskies/functional/string.py +112 -0
- clearskies/functional/validations.py +76 -0
- clearskies/input_outputs/__init__.py +13 -0
- clearskies/input_outputs/cli.py +157 -0
- clearskies/input_outputs/exceptions/__init__.py +7 -0
- clearskies/input_outputs/exceptions/cli_input_error.py +2 -0
- clearskies/input_outputs/exceptions/cli_not_found.py +2 -0
- clearskies/input_outputs/headers.py +54 -0
- clearskies/input_outputs/input_output.py +116 -0
- clearskies/input_outputs/programmatic.py +62 -0
- clearskies/input_outputs/py.typed +0 -0
- clearskies/input_outputs/wsgi.py +80 -0
- clearskies/loggable.py +19 -0
- clearskies/model.py +2039 -0
- clearskies/py.typed +0 -0
- clearskies/query/__init__.py +12 -0
- clearskies/query/condition.py +228 -0
- clearskies/query/join.py +136 -0
- clearskies/query/query.py +195 -0
- clearskies/query/sort.py +27 -0
- clearskies/schema.py +82 -0
- clearskies/secrets/__init__.py +7 -0
- clearskies/secrets/additional_configs/__init__.py +32 -0
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +61 -0
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +160 -0
- clearskies/secrets/akeyless.py +507 -0
- clearskies/secrets/exceptions/__init__.py +7 -0
- clearskies/secrets/exceptions/not_found_error.py +2 -0
- clearskies/secrets/exceptions/permissions_error.py +2 -0
- clearskies/secrets/secrets.py +39 -0
- clearskies/security_header.py +17 -0
- clearskies/security_headers/__init__.py +11 -0
- clearskies/security_headers/cache_control.py +68 -0
- clearskies/security_headers/cors.py +51 -0
- clearskies/security_headers/csp.py +95 -0
- clearskies/security_headers/hsts.py +23 -0
- clearskies/security_headers/x_content_type_options.py +0 -0
- clearskies/security_headers/x_frame_options.py +0 -0
- 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/validators/required.py +32 -0
- clearskies/validators/timedelta.py +58 -0
- clearskies/validators/unique.py +28 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Self, overload
|
|
5
|
+
|
|
6
|
+
from clearskies import configs, decorators
|
|
7
|
+
from clearskies.autodoc.schema import Array as AutoDocArray
|
|
8
|
+
from clearskies.autodoc.schema import Object as AutoDocObject
|
|
9
|
+
from clearskies.column import Column
|
|
10
|
+
from clearskies.columns.many_to_many_ids import ManyToManyIds
|
|
11
|
+
from clearskies.columns.many_to_many_ids_with_data import ManyToManyIdsWithData
|
|
12
|
+
from clearskies.functional import string
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from clearskies import Model
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ManyToManyModels(Column):
|
|
19
|
+
"""
|
|
20
|
+
A companion for the ManyToManyIds column that returns the matching models instead of the ids.
|
|
21
|
+
|
|
22
|
+
See the example in the ManyToManyIds column to understand how to use it.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
""" The name of the many-to-many column we are attached to. """
|
|
26
|
+
many_to_many_column_name = configs.ModelColumn(required=True)
|
|
27
|
+
|
|
28
|
+
is_writeable = configs.Boolean(default=False)
|
|
29
|
+
is_searchable = configs.Boolean(default=False)
|
|
30
|
+
_descriptor_config_map = None
|
|
31
|
+
|
|
32
|
+
@decorators.parameters_to_properties
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
many_to_many_column_name,
|
|
36
|
+
):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def finalize_configuration(self, model_class: type, name: str) -> None:
|
|
40
|
+
"""Finalize and check the configuration."""
|
|
41
|
+
getattr(self.__class__, "many_to_many_column_name").set_model_class(model_class)
|
|
42
|
+
self.model_class = model_class
|
|
43
|
+
self.name = name
|
|
44
|
+
self.finalize_and_validate_configuration()
|
|
45
|
+
|
|
46
|
+
# finally, make sure we're really pointed at a many-to-many column
|
|
47
|
+
many_to_many_column = getattr(model_class, self.many_to_many_column_name)
|
|
48
|
+
if not isinstance(many_to_many_column, (ManyToManyIds, ManyToManyIdsWithData)):
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Error with configuration for {model_class.__name__}.{name}, which is a {many_to_many_column.__class__.__name__} column. It needs to point to a ManyToManyIds column, and it was told to use {model_class.__name__}.{self.many_to_many_column_name}, but this is not a ManyToManyIds column."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def pivot_model(self):
|
|
55
|
+
return self.di.build(self.many_to_many_column.pivot_model_class, cache=True) # type: ignore
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def related_models(self):
|
|
59
|
+
return self.di.build(self.many_to_many_column.related_model_class, cache=True) # type: ignore
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def related_columns(self):
|
|
63
|
+
return self.related_models.get_columns()
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def many_to_many_column(self) -> ManyToManyIds:
|
|
67
|
+
return getattr(self.model_class, self.many_to_many_column_name)
|
|
68
|
+
|
|
69
|
+
@overload
|
|
70
|
+
def __get__(self, instance: None, cls: type[Model]) -> Self:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
@overload
|
|
74
|
+
def __get__(self, instance: Model, cls: type[Model]) -> Model:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def __get__(self, instance, cls):
|
|
78
|
+
if instance is None:
|
|
79
|
+
self.model_class = cls
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
# this makes sure we're initialized
|
|
83
|
+
if "name" not in self._config: # type: ignore
|
|
84
|
+
instance.get_columns()
|
|
85
|
+
|
|
86
|
+
if self.name in instance._transformed_data:
|
|
87
|
+
return instance._transformed_data[self.name]
|
|
88
|
+
|
|
89
|
+
children = self.many_to_many_column.get_related_models(instance)
|
|
90
|
+
|
|
91
|
+
instance._transformed_data[self.name] = children
|
|
92
|
+
return children
|
|
93
|
+
|
|
94
|
+
def __set__(self, instance, value: Model | list[Model] | list[dict[str, Any]]) -> None:
|
|
95
|
+
# this makes sure we're initialized
|
|
96
|
+
if "name" not in self._config: # type: ignore
|
|
97
|
+
instance.get_columns()
|
|
98
|
+
|
|
99
|
+
# we allow a list of models or a model, but if it's a model it may represent a single record or a query.
|
|
100
|
+
# if it's a single record then we want to wrap it in a list so we can iterate over it.
|
|
101
|
+
if hasattr(value, "_data") and isinstance(value, Model) and value._data:
|
|
102
|
+
value = []
|
|
103
|
+
many_to_many_column: ManyToManyIds = self.many_to_many_column # type: ignore
|
|
104
|
+
related_model_class = many_to_many_column.related_model_class
|
|
105
|
+
related_id_column_name = related_model_class.id_column_name
|
|
106
|
+
record_ids = []
|
|
107
|
+
for index, record in enumerate(value):
|
|
108
|
+
if isinstance(record, dict):
|
|
109
|
+
if not record.get(related_id_column_name):
|
|
110
|
+
raise KeyError(
|
|
111
|
+
f"A list of dictionaries was set to '{self.model_class.__name__}.{self.name}', in which case each dictionary should contain the key '{related_id_column_name}', which should be the id of an entry for the '{related_model_class.__name__}' model. However, no such key was found for entry #{index + 1}"
|
|
112
|
+
)
|
|
113
|
+
record_ids.append(record[related_id_column_name])
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
# if we get here then the entry should be a model for our related model class
|
|
117
|
+
if not isinstance(record, related_model_class):
|
|
118
|
+
raise TypeError(
|
|
119
|
+
f"Models were sent to '{self.model_class.__name__}.{self.name}', in which case it should be a list of models of type {related_model_class.__name__}. However, an object of type '{record.__class__.__name__}' was found for entry #{index + 1}"
|
|
120
|
+
)
|
|
121
|
+
record_ids.append(getattr(record, related_id_column_name))
|
|
122
|
+
setattr(instance, self.many_to_many_column_name, record_ids)
|
|
123
|
+
|
|
124
|
+
def add_search(self, model: Model, value: str, operator: str = "", relationship_reference: str = "") -> Model:
|
|
125
|
+
return self.many_to_many_column.add_search( # type: ignore
|
|
126
|
+
model, value, operator, relationship_reference=relationship_reference
|
|
127
|
+
) # type: ignore
|
|
128
|
+
|
|
129
|
+
def to_json(self, model: Model) -> dict[str, Any]:
|
|
130
|
+
records = []
|
|
131
|
+
many_to_many_column: ManyToManyIds = self.many_to_many_column # type: ignore
|
|
132
|
+
columns = many_to_many_column.related_columns
|
|
133
|
+
related_id_column_name = many_to_many_column.related_model_class.id_column_name
|
|
134
|
+
for related in many_to_many_column.get_related_models(model):
|
|
135
|
+
json = OrderedDict()
|
|
136
|
+
if related_id_column_name not in many_to_many_column.readable_related_column_names:
|
|
137
|
+
json[related_id_column_name] = columns[related_id_column_name].to_json(related)
|
|
138
|
+
for column_name in many_to_many_column.readable_related_column_names:
|
|
139
|
+
column_data = columns[column_name].to_json(related)
|
|
140
|
+
if type(column_data) == dict:
|
|
141
|
+
json = {**json, **column_data} # type: ignore
|
|
142
|
+
else:
|
|
143
|
+
json[column_name] = column_data
|
|
144
|
+
records.append(json)
|
|
145
|
+
return {self.name: records}
|
|
146
|
+
|
|
147
|
+
def documentation(self, name: str | None = None, example: str | None = None, value: str | None = None):
|
|
148
|
+
many_to_many_column = self.many_to_many_column # type: ignore
|
|
149
|
+
columns = many_to_many_column.related_columns
|
|
150
|
+
related_id_column_name = many_to_many_column.related_model_class.id_column_name
|
|
151
|
+
related_properties = [columns[related_id_column_name].documentation()]
|
|
152
|
+
|
|
153
|
+
for column_name in many_to_many_column.readable_related_column_names:
|
|
154
|
+
related_docs = columns[column_name].documentation()
|
|
155
|
+
if not isinstance(related_docs, list):
|
|
156
|
+
related_docs = [related_docs]
|
|
157
|
+
related_properties.extend(related_docs)
|
|
158
|
+
|
|
159
|
+
related_object = AutoDocObject(
|
|
160
|
+
string.title_case_to_nice(many_to_many_column.related_model_class.__name__),
|
|
161
|
+
related_properties,
|
|
162
|
+
)
|
|
163
|
+
return AutoDocArray(name if name is not None else self.name, related_object, value=value)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Self, overload
|
|
5
|
+
|
|
6
|
+
from clearskies import configs, decorators
|
|
7
|
+
from clearskies.autodoc.schema import Array as AutoDocArray
|
|
8
|
+
from clearskies.autodoc.schema import Object as AutoDocObject
|
|
9
|
+
from clearskies.column import Column
|
|
10
|
+
from clearskies.columns.many_to_many_ids import ManyToManyIds
|
|
11
|
+
from clearskies.functional import string
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from clearskies import Model
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ManyToManyPivots(Column):
|
|
18
|
+
"""
|
|
19
|
+
A companion for the ManyToManyIds column that returns the matching pivot models instead of the ids.
|
|
20
|
+
|
|
21
|
+
See ManyToManyIdsWithData for an example of how to use it (but note that it works just the same for the
|
|
22
|
+
ManyToManyIds column).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
""" The name of the many-to-many column we are attached to. """
|
|
26
|
+
many_to_many_column_name = configs.ModelColumn(required=True)
|
|
27
|
+
|
|
28
|
+
is_writeable = configs.Boolean(default=False)
|
|
29
|
+
is_searchable = configs.Boolean(default=False)
|
|
30
|
+
_descriptor_config_map = None
|
|
31
|
+
|
|
32
|
+
@decorators.parameters_to_properties
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
many_to_many_column_name,
|
|
36
|
+
):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def finalize_configuration(self, model_class: type, name: str) -> None:
|
|
40
|
+
"""Finalize and check the configuration."""
|
|
41
|
+
getattr(self.__class__, "many_to_many_column_name").set_model_class(model_class)
|
|
42
|
+
self.model_class = model_class
|
|
43
|
+
self.name = name
|
|
44
|
+
self.finalize_and_validate_configuration()
|
|
45
|
+
|
|
46
|
+
# finally, make sure we're really pointed at a many-to-many column
|
|
47
|
+
many_to_many_column = getattr(model_class, self.many_to_many_column_name)
|
|
48
|
+
if not isinstance(many_to_many_column, ManyToManyIds):
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Error with configuration for {model_class.__name__}.{name}, which is a ManyToManyModels column. It needs to point to a ManyToManyIds column, and it was told to use {model_class.__name__}.{self.many_to_many_column_name}, but this is not a ManyToManyIds column."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def pivot_model(self) -> Model:
|
|
55
|
+
return self.di.build(self.many_to_many_column.pivot_model_class, cache=True) # type: ignore
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def related_models(self) -> Model:
|
|
59
|
+
return self.di.build(self.many_to_many_column.related_model_class, cache=True) # type: ignore
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def related_columns(self):
|
|
63
|
+
return self.related_models.get_columns()
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def many_to_many_column(self) -> ManyToManyIds:
|
|
67
|
+
return getattr(self.model_class, self.many_to_many_column_name)
|
|
68
|
+
|
|
69
|
+
@overload
|
|
70
|
+
def __get__(self, instance: None, cls: type[Model]) -> Self:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
@overload
|
|
74
|
+
def __get__(self, instance: Model, cls: type[Model]) -> Model:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def __get__(self, instance, cls):
|
|
78
|
+
if instance is None:
|
|
79
|
+
self.model_class = cls
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
# this makes sure we're initialized
|
|
83
|
+
if "name" not in self._config: # type: ignore
|
|
84
|
+
instance.get_columns()
|
|
85
|
+
|
|
86
|
+
many_to_many_column: Column = self.many_to_many_column
|
|
87
|
+
own_column_name_in_pivot = many_to_many_column._config.get("own_column_name_in_pivot")
|
|
88
|
+
my_id = getattr(instance, instance.id_column_name)
|
|
89
|
+
return [model for model in self.pivot_model.where(f"{own_column_name_in_pivot}={my_id}")]
|
|
90
|
+
|
|
91
|
+
def __set__(self, instance, value: Model | list[Model] | list[dict[str, Any]]) -> None:
|
|
92
|
+
raise NotImplementedError("Saving not supported for ManyToManyPivots")
|
|
93
|
+
|
|
94
|
+
def add_search(self, model: Model, value: str, operator: str = "", relationship_reference: str = "") -> Model:
|
|
95
|
+
raise NotImplementedError("Searching not supported for ManyToManyPivots")
|
|
96
|
+
|
|
97
|
+
def to_json(self, model: Model) -> dict[str, Any]:
|
|
98
|
+
records = []
|
|
99
|
+
many_to_many_column = self.many_to_many_column # type: ignore
|
|
100
|
+
columns = many_to_many_column.pivot_columns
|
|
101
|
+
readable_column_names = many_to_many_column.readable_pivot_column_names
|
|
102
|
+
pivot_id_column_name = many_to_many_column.pivot_model_class.id_column_name
|
|
103
|
+
for pivot in many_to_many_column.get_pivot_models(model):
|
|
104
|
+
json = OrderedDict()
|
|
105
|
+
if pivot_id_column_name not in readable_column_names:
|
|
106
|
+
json[pivot_id_column_name] = columns[pivot_id_column_name].to_json(pivot)
|
|
107
|
+
for column_name in readable_column_names:
|
|
108
|
+
column_data = columns[column_name].to_json(pivot)
|
|
109
|
+
if type(column_data) == dict:
|
|
110
|
+
json = {**json, **column_data} # type: ignore
|
|
111
|
+
else:
|
|
112
|
+
json[column_name] = column_data
|
|
113
|
+
records.append(json)
|
|
114
|
+
return {self.name: records}
|
|
115
|
+
|
|
116
|
+
def documentation(self, name: str | None = None, example: str | None = None, value: str | None = None):
|
|
117
|
+
many_to_many_column = self.many_to_many_column # type: ignore
|
|
118
|
+
columns = many_to_many_column.pivot_columns
|
|
119
|
+
pivot_id_column_name = many_to_many_column.pivot_model_class.id_column_name
|
|
120
|
+
pivot_properties = [columns[pivot_id_column_name].documentation()]
|
|
121
|
+
|
|
122
|
+
for column_name in many_to_many_column.readable_pivot_column_names:
|
|
123
|
+
pivot_docs = columns[column_name].documentation()
|
|
124
|
+
if type(pivot_docs) != list:
|
|
125
|
+
pivot_docs = [pivot_docs]
|
|
126
|
+
pivot_properties.extend(pivot_docs)
|
|
127
|
+
|
|
128
|
+
pivot_object = AutoDocObject(
|
|
129
|
+
string.title_case_to_nice(many_to_many_column.pivot_model_class.__name__),
|
|
130
|
+
pivot_properties,
|
|
131
|
+
)
|
|
132
|
+
return AutoDocArray(name if name is not None else self.name, pivot_object, value=value)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
5
|
+
|
|
6
|
+
from clearskies import configs, decorators
|
|
7
|
+
from clearskies.columns.string import String
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from clearskies import typing
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Phone(String):
|
|
14
|
+
"""
|
|
15
|
+
A string column that stores a phone number.
|
|
16
|
+
|
|
17
|
+
The main difference between this and a plain string column is that this will validate that the string contains
|
|
18
|
+
a phone number (containing only digits, dashes, spaces, plus sign, and parenthesis) of the appropriate length.
|
|
19
|
+
When persisting the value to the backend, this column removes all non-digit characters.
|
|
20
|
+
|
|
21
|
+
If you also set the usa_only flag to true then it will also validate that it is a valid US number containing
|
|
22
|
+
9 digits and, optionally, a leading `1`. Example:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import clearskies
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class User(clearskies.Model):
|
|
29
|
+
id_column_name = "id"
|
|
30
|
+
backend = clearskies.backends.MemoryBackend()
|
|
31
|
+
|
|
32
|
+
id = clearskies.columns.Uuid()
|
|
33
|
+
name = clearskies.columns.String()
|
|
34
|
+
phone = clearskies.columns.Phone(usa_only=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
38
|
+
clearskies.endpoints.Create(
|
|
39
|
+
User,
|
|
40
|
+
writeable_column_names=["name", "phone"],
|
|
41
|
+
readable_column_names=["id", "name", "phone"],
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
wsgi()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Which you can invoke:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
$ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "+1 (555) 451-1234"}' | jq
|
|
51
|
+
{
|
|
52
|
+
"status": "success",
|
|
53
|
+
"error": "",
|
|
54
|
+
"data": {
|
|
55
|
+
"id": "e2b4bdad-b70f-4d44-a94c-0e265868b4d2",
|
|
56
|
+
"name": "John Doe",
|
|
57
|
+
"phone": "15554511234"
|
|
58
|
+
},
|
|
59
|
+
"pagination": {},
|
|
60
|
+
"input_errors": {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
$ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "555 451-1234"}' | jq
|
|
64
|
+
{
|
|
65
|
+
"status": "success",
|
|
66
|
+
"error": "",
|
|
67
|
+
"data": {
|
|
68
|
+
"id": "aea34022-4b75-4eed-ac92-65fa4f4511ae",
|
|
69
|
+
"name": "John Doe",
|
|
70
|
+
"phone": "5554511234"
|
|
71
|
+
},
|
|
72
|
+
"pagination": {},
|
|
73
|
+
"input_errors": {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
$ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "555 451-12341"}' | jq
|
|
78
|
+
{
|
|
79
|
+
"status": "input_errors",
|
|
80
|
+
"error": "",
|
|
81
|
+
"data": [],
|
|
82
|
+
"pagination": {},
|
|
83
|
+
"input_errors": {
|
|
84
|
+
"phone": "Invalid phone number"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
$ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "1-2-3-4 asdf"}' | jq
|
|
89
|
+
{
|
|
90
|
+
"status": "input_errors",
|
|
91
|
+
"error": "",
|
|
92
|
+
"data": [],
|
|
93
|
+
"pagination": {},
|
|
94
|
+
"input_errors": {
|
|
95
|
+
"phone": "Invalid phone number"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
""" Whether or not to allow non-USA numbers. """
|
|
102
|
+
usa_only = configs.Boolean(default=True)
|
|
103
|
+
_descriptor_config_map = None
|
|
104
|
+
|
|
105
|
+
@decorators.parameters_to_properties
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
usa_only: bool = True,
|
|
109
|
+
default: str | None = None,
|
|
110
|
+
setable: str | Callable[..., str] | None = None,
|
|
111
|
+
is_readable: bool = True,
|
|
112
|
+
is_writeable: bool = True,
|
|
113
|
+
is_searchable: bool = True,
|
|
114
|
+
is_temporary: bool = False,
|
|
115
|
+
validators: typing.validator | list[typing.validator] = [],
|
|
116
|
+
on_change_pre_save: typing.action | list[typing.action] = [],
|
|
117
|
+
on_change_post_save: typing.action | list[typing.action] = [],
|
|
118
|
+
on_change_save_finished: typing.action | list[typing.action] = [],
|
|
119
|
+
created_by_source_type: str = "",
|
|
120
|
+
created_by_source_key: str = "",
|
|
121
|
+
created_by_source_strict: bool = True,
|
|
122
|
+
):
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
126
|
+
if not data.get(self.name):
|
|
127
|
+
return data
|
|
128
|
+
|
|
129
|
+
# phone numbers are stored as only digits.
|
|
130
|
+
return {**data, **{self.name: re.sub(r"\D", "", data[self.name])}}
|
|
131
|
+
|
|
132
|
+
def input_error_for_value(self, value: str, operator: str | None = None) -> str:
|
|
133
|
+
if type(value) != str:
|
|
134
|
+
return f"Value must be a string for {self.name}"
|
|
135
|
+
|
|
136
|
+
# we'll allow spaces, dashes, parenthesis, dashes, and plus signs.
|
|
137
|
+
# if there is anything else then it's not a valid phone number.
|
|
138
|
+
# However, we don't do more detailed validation, because I'm too lazy to
|
|
139
|
+
# figure out what is and is not a valid phone number, especially when
|
|
140
|
+
# you get to the world of international numbers.
|
|
141
|
+
if re.search(r"[^\d \-()+]", value):
|
|
142
|
+
return "Invalid phone number"
|
|
143
|
+
|
|
144
|
+
# for some final validation (especially US numbers) work only with the digits.
|
|
145
|
+
value = re.sub(r"\D", "", value)
|
|
146
|
+
|
|
147
|
+
if len(value) > 15:
|
|
148
|
+
return "Invalid phone number"
|
|
149
|
+
|
|
150
|
+
# we can't be too short unless we're doing a fuzzy search
|
|
151
|
+
if len(value) < 10 and operator and operator.lower() != "like":
|
|
152
|
+
return "Invalid phone number"
|
|
153
|
+
|
|
154
|
+
if self.usa_only:
|
|
155
|
+
if len(value) > 11:
|
|
156
|
+
return "Invalid phone number"
|
|
157
|
+
if value[0] == "1" and len(value) != 11:
|
|
158
|
+
return "Invalid phone number"
|
|
159
|
+
if value[0] != "1" and len(value) != 10:
|
|
160
|
+
return "Invalid phone number"
|
|
161
|
+
|
|
162
|
+
return ""
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Callable
|
|
4
|
+
|
|
5
|
+
from clearskies import configs, decorators
|
|
6
|
+
from clearskies.columns.string import String
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from clearskies import typing
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Select(String):
|
|
13
|
+
"""
|
|
14
|
+
A string column but, when writeable via an endpoint, only specific values are allowed.
|
|
15
|
+
|
|
16
|
+
Note: the allowed values are case sensitive.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import clearskies
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Order(clearskies.Model):
|
|
23
|
+
id_column_name = "id"
|
|
24
|
+
backend = clearskies.backends.MemoryBackend()
|
|
25
|
+
|
|
26
|
+
id = clearskies.columns.Uuid()
|
|
27
|
+
total = clearskies.columns.Float()
|
|
28
|
+
status = clearskies.columns.Select(["Open", "Processing", "Shipped", "Complete"])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
32
|
+
clearskies.endpoints.Create(
|
|
33
|
+
Order,
|
|
34
|
+
writeable_column_names=["total", "status"],
|
|
35
|
+
readable_column_names=["id", "total", "status"],
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
wsgi()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
And when invoked:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
$ curl http://localhost:8080 -d '{"total": 125, "status": "Open"}' | jq
|
|
45
|
+
{
|
|
46
|
+
"status": "success",
|
|
47
|
+
"error": "",
|
|
48
|
+
"data": {
|
|
49
|
+
"id": "22f2c950-6519-4d8e-9084-013455449b07",
|
|
50
|
+
"total": 125.0,
|
|
51
|
+
"status": "Open"
|
|
52
|
+
},
|
|
53
|
+
"pagination": {},
|
|
54
|
+
"input_errors": {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
$ curl http://localhost:8080 -d '{"total": 125, "status": "huh"}' | jq
|
|
58
|
+
{
|
|
59
|
+
"status": "input_errors",
|
|
60
|
+
"error": "",
|
|
61
|
+
"data": [],
|
|
62
|
+
"pagination": {},
|
|
63
|
+
"input_errors": {
|
|
64
|
+
"status": "Invalid value for status"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
""" The allowed values. """
|
|
71
|
+
allowed_values = configs.StringList(required=True)
|
|
72
|
+
_descriptor_config_map = None
|
|
73
|
+
|
|
74
|
+
@decorators.parameters_to_properties
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
allowed_values: list[str],
|
|
78
|
+
default: str | None = None,
|
|
79
|
+
setable: str | Callable[..., str] | None = None,
|
|
80
|
+
is_readable: bool = True,
|
|
81
|
+
is_writeable: bool = True,
|
|
82
|
+
is_searchable: bool = True,
|
|
83
|
+
is_temporary: bool = False,
|
|
84
|
+
validators: typing.validator | list[typing.validator] = [],
|
|
85
|
+
on_change_pre_save: typing.action | list[typing.action] = [],
|
|
86
|
+
on_change_post_save: typing.action | list[typing.action] = [],
|
|
87
|
+
on_change_save_finished: typing.action | list[typing.action] = [],
|
|
88
|
+
created_by_source_type: str = "",
|
|
89
|
+
created_by_source_key: str = "",
|
|
90
|
+
created_by_source_strict: bool = True,
|
|
91
|
+
):
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
def input_error_for_value(self, value: str, operator: str | None = None) -> str:
|
|
95
|
+
return f"Invalid value for {self.name}" if value not in self.allowed_values else ""
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Self, overload
|
|
4
|
+
|
|
5
|
+
from clearskies.column import Column
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from clearskies import Model
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class String(Column):
|
|
12
|
+
"""
|
|
13
|
+
A simple string column.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import clearskies
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Pet(clearskies.Model):
|
|
20
|
+
id_column_name = "id"
|
|
21
|
+
backend = clearskies.backends.MemoryBackend()
|
|
22
|
+
|
|
23
|
+
id = clearskies.columns.Uuid()
|
|
24
|
+
name = clearskies.columns.String()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
28
|
+
clearskies.endpoints.Create(
|
|
29
|
+
Pet,
|
|
30
|
+
writeable_column_names=["name"],
|
|
31
|
+
readable_column_names=["id", "name"],
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
wsgi()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
And when invoked:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
$ curl http://localhost:8080 -d '{"name": "Spot"}' | jq
|
|
41
|
+
{
|
|
42
|
+
"status": "success",
|
|
43
|
+
"error": "",
|
|
44
|
+
"data": {
|
|
45
|
+
"id": "e5b8417f-91bc-4fe5-9b64-04f571a7b10a",
|
|
46
|
+
"name": "Spot"
|
|
47
|
+
},
|
|
48
|
+
"pagination": {},
|
|
49
|
+
"input_errors": {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
$ curl http://localhost:8080 -d '{"name": 10}' | jq
|
|
53
|
+
{
|
|
54
|
+
"status": "input_errors",
|
|
55
|
+
"error": "",
|
|
56
|
+
"data": [],
|
|
57
|
+
"pagination": {},
|
|
58
|
+
"input_errors": {
|
|
59
|
+
"name": "value should be a string"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
_allowed_search_operators = ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null", "like"]
|
|
67
|
+
_descriptor_config_map = None
|
|
68
|
+
|
|
69
|
+
@overload
|
|
70
|
+
def __get__(self, instance: None, cls: type[Model]) -> Self:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
@overload
|
|
74
|
+
def __get__(self, instance: Model, cls: type[Model]) -> str:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def __get__(self, instance, cls):
|
|
78
|
+
if instance is None:
|
|
79
|
+
self.model_class = cls
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
# this makes sure we're initialized
|
|
83
|
+
if "name" not in self._config: # type: ignore
|
|
84
|
+
instance.get_columns()
|
|
85
|
+
|
|
86
|
+
if self.name not in instance._data:
|
|
87
|
+
return None # type: ignore
|
|
88
|
+
|
|
89
|
+
if self.name not in instance._transformed_data:
|
|
90
|
+
instance._transformed_data[self.name] = self.from_backend(instance._data[self.name])
|
|
91
|
+
|
|
92
|
+
return instance._transformed_data[self.name]
|
|
93
|
+
|
|
94
|
+
def __set__(self, instance: Model, value: str) -> None:
|
|
95
|
+
# this makes sure we're initialized
|
|
96
|
+
if "name" not in self._config: # type: ignore
|
|
97
|
+
instance.get_columns()
|
|
98
|
+
|
|
99
|
+
instance._next_data[self.name] = value
|
|
100
|
+
|
|
101
|
+
def input_error_for_value(self, value: str, operator: str | None = None) -> str:
|
|
102
|
+
return "value should be a string" if not isinstance(value, str) else ""
|