smartdjango 4.4.0__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.
@@ -0,0 +1,29 @@
1
+ from smartdjango.validation.validator import Validator, ValidatorErrors
2
+ from smartdjango.validation.params import Params
3
+ from smartdjango.validation.list_validator import ListValidator
4
+ from smartdjango.validation.dict_validator import DictValidator
5
+ from smartdjango.validation.key import Key
6
+
7
+ from smartdjango.analyse import AnalyseErrors
8
+ from smartdjango import analyse
9
+ from smartdjango.choice import Choice
10
+ from smartdjango.code import Code
11
+ from smartdjango.error import Error, OK
12
+ from smartdjango.middleware import APIPacker
13
+
14
+
15
+ __all__ = [
16
+ 'Validator',
17
+ 'ValidatorErrors',
18
+ 'Params',
19
+ 'ListValidator',
20
+ 'DictValidator',
21
+ 'Key',
22
+ 'AnalyseErrors',
23
+ 'analyse',
24
+ 'Choice',
25
+ 'Code',
26
+ 'Error',
27
+ 'APIPacker',
28
+ 'OK'
29
+ ]
smartdjango/analyse.py ADDED
@@ -0,0 +1,132 @@
1
+ from functools import wraps
2
+
3
+ from django.http import HttpRequest
4
+ from django.utils.translation import gettext as _
5
+ from oba import Obj
6
+
7
+ from smartdjango.code import Code
8
+ from smartdjango.error import Error
9
+ from smartdjango.utils import io, inspect
10
+ from smartdjango.validation.dict_validator import DictValidator
11
+ from smartdjango.validation.validator import Validator
12
+
13
+
14
+ class Request(HttpRequest):
15
+ json: Obj
16
+ query: Obj
17
+ argument: Obj
18
+ data: Obj
19
+
20
+
21
+ @Error.register
22
+ class AnalyseErrors:
23
+ REQUEST_NOT_FOUND = Error(_("Cannot find request"), code=Code.InternalServerError)
24
+
25
+
26
+ def get_request(*args):
27
+ for i, arg in enumerate(args):
28
+ if isinstance(arg, HttpRequest):
29
+ return arg
30
+ raise AnalyseErrors.REQUEST_NOT_FOUND
31
+
32
+
33
+ def update_to_data(req: Request, target):
34
+ data = getattr(req, 'data', {})
35
+ if isinstance(data, Obj):
36
+ data = data()
37
+ data.update(target())
38
+ req.data = Obj(data)
39
+
40
+
41
+ def analyse(*validators: Validator | str, target_getter, target_setter, restrict_keys):
42
+ def decorator(func):
43
+ @wraps(func)
44
+ def wrapper(*args, **kwargs):
45
+ req = get_request(*args)
46
+ target = target_getter(req, kwargs)
47
+
48
+ validator = DictValidator().fields(*validators)
49
+ if restrict_keys:
50
+ validator.restrict_keys()
51
+ target = validator.clean(target)
52
+ target_setter(req, Obj(target))
53
+
54
+ return func(*args, **kwargs)
55
+ return wrapper
56
+ return decorator
57
+
58
+
59
+ def json(*validators: Validator | str, restrict_keys=True):
60
+ def getter(req, kwargs):
61
+ return io.json_loads(req.body.decode())
62
+
63
+ def setter(req, target):
64
+ req.json = target
65
+ update_to_data(req, target)
66
+
67
+ return analyse(
68
+ *validators,
69
+ target_getter=getter,
70
+ target_setter=setter,
71
+ restrict_keys=restrict_keys
72
+ )
73
+
74
+
75
+ def query(*validators: Validator | str, restrict_keys=False):
76
+ def getter(req, kwargs):
77
+ return req.GET.dict()
78
+
79
+ def setter(req, target):
80
+ req.query = target
81
+ update_to_data(req, target)
82
+
83
+ return analyse(
84
+ *validators,
85
+ target_getter=getter,
86
+ target_setter=setter,
87
+ restrict_keys=restrict_keys
88
+ )
89
+
90
+
91
+ def argument(*validators: Validator | str, restrict_keys=True):
92
+ def getter(req, kwargs):
93
+ return kwargs
94
+
95
+ def setter(req, target):
96
+ req.argument = target
97
+ update_to_data(req, target)
98
+
99
+ return analyse(
100
+ *validators,
101
+ target_getter=getter,
102
+ target_setter=setter,
103
+ restrict_keys=restrict_keys
104
+ )
105
+
106
+
107
+ def request(bool_func, message=None):
108
+ def decorator(func):
109
+ @wraps(func)
110
+ def wrapper(*args, **kwargs):
111
+ req = get_request(*args)
112
+ Validator().bool(bool_func, message=message).clean(req)
113
+ return func(*args, **kwargs)
114
+
115
+ return wrapper
116
+ return decorator
117
+
118
+
119
+ def function(*validators: Validator | str, restrict_keys=True):
120
+ validator = DictValidator().fields(*validators)
121
+ if restrict_keys:
122
+ validator.restrict_keys()
123
+
124
+ def decorator(func):
125
+ @wraps(func)
126
+ def wrapper(*args, **kwargs):
127
+ arguments = inspect.get_function_arguments(func, *args, **kwargs)
128
+ arguments = validator.clean(arguments)
129
+
130
+ return func(**arguments)
131
+ return wrapper
132
+ return decorator
smartdjango/choice.py ADDED
@@ -0,0 +1,4 @@
1
+ class Choice:
2
+ @classmethod
3
+ def to_choices(cls):
4
+ return [(v, v) for k, v in cls.__dict__.items() if not k.startswith('_')]
smartdjango/code.py ADDED
@@ -0,0 +1,63 @@
1
+ class Code:
2
+ Continue = 100
3
+ SwitchingProtocols = 101
4
+ Processing = 102
5
+
6
+ OK = 200
7
+ Created = 201
8
+ Accepted = 202
9
+ NonAuthoritativeInformation = 203
10
+ NoContent = 204
11
+ ResetContent = 205
12
+ PartialContent = 206
13
+ MultiStatus = 207
14
+
15
+ MultipleChoices = 300
16
+ MovedPermanently = 301
17
+ MoveTemporarily = 302
18
+ SeeOther = 303
19
+ NotModified = 304
20
+ UseProxy = 305
21
+ SwitchProxy = 306 # 不再使用
22
+ TemporaryRedirect = 307
23
+
24
+ BadRequest = 400
25
+ Unauthorized = 401
26
+ PaymentRequired = 402
27
+ Forbidden = 403
28
+ NotFound = 404
29
+ MethodNotAllowed = 405
30
+ NotAcceptable = 406
31
+ ProxyAuthenticationRequired = 407
32
+ RequestTimeout = 408
33
+ Conflict = 409
34
+ Gone = 410
35
+ LengthRequired = 411
36
+ PreconditionFailed = 412
37
+ RequestEntityTooLarge = 413
38
+ RequestURITooLong = 414
39
+ UnsupportedMediaType = 415
40
+ RequestedRangeNotSatisfiable = 416
41
+ ExpectationFailed = 417
42
+ ImATeapot = 418
43
+ TooManyConnections = 421
44
+ UnprocessableEntity = 422
45
+ Locked = 423
46
+ FailedDependency = 424
47
+ TooEarly = 425
48
+ UpgradeRequired = 426
49
+ RetryWith = 449
50
+ UnavailableForLegalReasons = 451
51
+
52
+ InternalServerError = 500
53
+ NotImplemented = 501
54
+ BadGateway = 502
55
+ ServiceUnavailable = 503
56
+ GatewayTimeout = 504
57
+ HTTPVersionNotSupported = 505
58
+ VariantAlsoNegotiates = 506
59
+ InsufficientStorage = 507
60
+ BandwidthLimitExceeded = 509
61
+ NotExtended = 510
62
+
63
+ UnparseableResponseHeaders = 600
smartdjango/error.py ADDED
@@ -0,0 +1,103 @@
1
+ from typing import Dict, Optional, Union
2
+
3
+ from django.utils.encoding import force_str
4
+ from django.utils.functional import Promise
5
+
6
+ from smartdjango.code import Code
7
+
8
+
9
+ MessageLike = Union[str, Promise]
10
+
11
+
12
+ class Error(Exception):
13
+ __ERRORS: Dict[str, 'Error'] = dict()
14
+
15
+ """Base class for exceptions in this module."""
16
+ def __init__(
17
+ self,
18
+ message: MessageLike,
19
+ code=Code.OK,
20
+ user_message: Optional[MessageLike] = None,
21
+ identifier=None,
22
+ ):
23
+ super().__init__(message)
24
+
25
+ self.__identifier = identifier or None
26
+ self.message = message
27
+ self.code = code
28
+ self.details = []
29
+ self.user_message = user_message or message
30
+
31
+ @property
32
+ def identifier(self) -> str:
33
+ return self.__identifier
34
+
35
+ @identifier.setter
36
+ def identifier(self, value):
37
+ if self.__identifier is not None:
38
+ raise AttributeError('Identifier is already set')
39
+ if value in self.__ERRORS:
40
+ raise AttributeError(f'Conflict error identifier: {value}')
41
+ self.__identifier = value
42
+ self.__ERRORS[value] = self
43
+
44
+ def json(self):
45
+ return {
46
+ 'message': force_str(self.message),
47
+ 'code': self.code,
48
+ 'details': self.details,
49
+ 'user_message': force_str(self.user_message),
50
+ 'identifier': self.identifier,
51
+ }
52
+
53
+ def jsonl(self):
54
+ return {
55
+ 'message': force_str(self.message),
56
+ 'code': self.code,
57
+ 'identifier': self.identifier,
58
+ }
59
+
60
+ def __call__(self, details=None, user_message=None, **kwargs):
61
+ message = force_str(self.message).format(**kwargs)
62
+ if user_message is None:
63
+ user_message = force_str(self.user_message).format(**kwargs)
64
+
65
+ if details is not None and not isinstance(details, str):
66
+ details = force_str(details)
67
+ if user_message is not None and not isinstance(user_message, str):
68
+ user_message = force_str(user_message)
69
+ error = Error(message, code=self.code, user_message=user_message, identifier=self.identifier)
70
+ error.details = self.details.copy()
71
+ if details is not None:
72
+ error.details.append(details)
73
+ return error
74
+
75
+ def __eq__(self, other: 'Error'):
76
+ return self.identifier == other.identifier
77
+
78
+ @classmethod
79
+ def register(cls, class_):
80
+ class_name = class_.__name__
81
+ if not class_name.upper().endswith('ERRORS'):
82
+ raise AttributeError('Error class name should end with "Errors"')
83
+ class_name = class_name[:-6].upper()
84
+ for name in class_.__dict__: # type: str
85
+ e = getattr(class_, name)
86
+ if not isinstance(e, Error):
87
+ continue
88
+ e.identifier = f'{class_name}@{name}'
89
+
90
+ return class_
91
+
92
+ def ok(self) -> bool:
93
+ return self.identifier == 'OK'
94
+
95
+ @classmethod
96
+ def all(cls) -> Dict[str, 'Error']:
97
+ return cls.__ERRORS
98
+
99
+ def equals(self, other: 'Error'):
100
+ return self.identifier == other.identifier
101
+
102
+
103
+ OK = Error('OK', code=Code.OK, identifier='OK')
@@ -0,0 +1,40 @@
1
+ from django.http import HttpResponse
2
+
3
+ from smartdjango.error import Error, OK
4
+ from smartdjango.utils import io
5
+
6
+
7
+ class APIPacker:
8
+ def __init__(self, get_response):
9
+ self.get_response = get_response
10
+
11
+ def __call__(self, request, *args, **kwargs):
12
+ response = self.get_response(request, *args, **kwargs)
13
+ if isinstance(response, HttpResponse):
14
+ return response
15
+
16
+ return self.pack(response)
17
+
18
+ @classmethod
19
+ def process_exception(cls, _, error):
20
+ if isinstance(error, Error):
21
+ return cls.pack(error)
22
+ return None
23
+
24
+ @staticmethod
25
+ def pack(response):
26
+ if isinstance(response, Error):
27
+ body, error = None, response
28
+ else:
29
+ body, error = response, OK
30
+
31
+ response = error.json()
32
+ response['body'] = body
33
+
34
+ response = io.json_dumps(response, indent=False)
35
+ response = HttpResponse(
36
+ response,
37
+ status=error.code,
38
+ content_type="application/json; encoding=utf-8",
39
+ )
40
+ return response
@@ -0,0 +1,20 @@
1
+ from diq import Dictify
2
+ from django.db.models import *
3
+ from django.db.models import Model as BaseModel
4
+
5
+ from smartdjango.models.queryset import QuerySet
6
+ from smartdjango.models.manager import Manager
7
+
8
+
9
+ class Model(BaseModel, Dictify):
10
+ objects = Manager()
11
+ normalizers = None
12
+ validators = None
13
+ normalizer = None
14
+ validator = None
15
+ norm = None
16
+ vldt = None
17
+
18
+ class Meta:
19
+ abstract = True
20
+ default_manager_name = 'objects'
@@ -0,0 +1,12 @@
1
+ # 自定义 Manager
2
+ from django.db import models
3
+
4
+ from smartdjango.models.queryset import QuerySet
5
+
6
+
7
+ class Manager(models.Manager):
8
+ def get_queryset(self) -> QuerySet:
9
+ return QuerySet(self.model, using=self._db)
10
+
11
+ def map(self, func, *args, **kwargs):
12
+ return self.get_queryset().map(func, *args, **kwargs)
@@ -0,0 +1,6 @@
1
+ from django.db import models
2
+
3
+
4
+ class QuerySet(models.QuerySet):
5
+ def map(self, func, *args, **kwargs):
6
+ return [func(obj, *args, **kwargs) for obj in self]
@@ -0,0 +1,69 @@
1
+ from smartdjango.models import QuerySet
2
+
3
+
4
+ class Page:
5
+ OBJECTS = 'objects'
6
+ NEXT = 'next'
7
+ COUNT = 'count'
8
+
9
+ def __init__(self, queryset, count, next_value):
10
+ self.queryset: QuerySet = queryset
11
+ self.count = count
12
+ self.next_value = next_value
13
+
14
+ @classmethod
15
+ def rename(cls, objects, next_value, count):
16
+ cls.OBJECTS = objects
17
+ cls.NEXT = next_value
18
+ cls.COUNT = count
19
+
20
+ def dict(self, object_map, next_map=None):
21
+ return {
22
+ self.OBJECTS: self.queryset.map(object_map),
23
+ self.NEXT: next_map(self.next_value) if next_map else self.next_value,
24
+ self.COUNT: self.count,
25
+ }
26
+
27
+
28
+ class Paginator:
29
+ def __init__(
30
+ self,
31
+ queryset: QuerySet,
32
+ page_size: int,
33
+ ):
34
+ self.queryset = queryset
35
+ self.page_size = page_size
36
+ self.count = self.queryset.count()
37
+
38
+ self.target_field = None
39
+
40
+ def filter(self, **kwargs):
41
+ assert len(kwargs) == 1, "only one filter is allowed"
42
+ order = list(kwargs.keys())[0]
43
+ assert '__' in order, "attribute must contain '__' to specify the comparison method"
44
+ self.target_field, compare_op = order.split('__', 1)
45
+ assert compare_op in ['lt', 'gt'], "comparison operation must be 'lt' or 'gt'"
46
+
47
+ order = self.target_field
48
+ if compare_op == 'lt':
49
+ order = '-' + order
50
+
51
+ self.queryset = self.queryset.filter(**kwargs).order_by(order)
52
+ self.count = self.queryset.count()
53
+
54
+ def get_page(
55
+ self,
56
+ page: int = None,
57
+ ) -> Page:
58
+ if page is None:
59
+ assert self.target_field is not None, "filter should be firstly called when page is None"
60
+
61
+ queryset = self.queryset[:self.page_size]
62
+ next_value = self.queryset.count() > self.page_size and getattr(queryset.last(), self.target_field)
63
+
64
+ return Page(queryset, self.count, next_value)
65
+
66
+ queryset = self.queryset[self.page_size * page:self.page_size * (page + 1)]
67
+ next_value = self.queryset.count() > self.page_size * (page + 1) and page + 1
68
+
69
+ return Page(queryset, self.count, next_value)
File without changes
@@ -0,0 +1,8 @@
1
+ import inspect
2
+
3
+
4
+ def get_function_arguments(func, *args, **kwargs):
5
+ signature = inspect.signature(func)
6
+ bound = signature.bind_partial(*args, **kwargs)
7
+ bound.apply_defaults()
8
+ return dict(bound.arguments)
@@ -0,0 +1,61 @@
1
+ import json
2
+ import pickle
3
+
4
+ from django.core.serializers.json import DjangoJSONEncoder
5
+
6
+ from typing import Protocol, cast
7
+
8
+
9
+ class SupportsWriteStr(Protocol):
10
+ def write(self, __s: str) -> object:
11
+ ...
12
+
13
+
14
+ class SupportsWriteBytes(Protocol):
15
+ def write(self, __s: bytes) -> object:
16
+ ...
17
+
18
+
19
+ def json_load(filepath: str):
20
+ with open(filepath, 'r', encoding='utf-8') as f:
21
+ return json.load(f)
22
+
23
+
24
+ def json_loads(s: str):
25
+ return json.loads(s)
26
+
27
+
28
+ def json_dumps(obj, indent=2) -> str:
29
+ if indent is False:
30
+ indent = None
31
+ return json.dumps(obj, indent=indent, ensure_ascii=False, cls=DjangoJSONEncoder)
32
+
33
+
34
+ def json_save(obj, filepath: str):
35
+ with open(filepath, 'w', encoding='utf-8') as f:
36
+ json.dump(obj, cast(SupportsWriteStr, f), indent=2, ensure_ascii=False, cls=DjangoJSONEncoder)
37
+
38
+
39
+ def jsonl_load(filepath: str):
40
+ lines = file_load(filepath).split('\n') # 读取整个文件并按行分割
41
+ lines = list(filter(lambda line: line.strip(), lines)) # 去掉空行
42
+ lines = list(map(lambda line: json_loads(line), lines)) # 每行解析成 JSON 对象
43
+ return lines
44
+
45
+
46
+ def file_load(filepath: str) -> str:
47
+ with open(filepath, 'r', encoding='utf-8') as f:
48
+ return f.read()
49
+
50
+
51
+ def file_save(filepath: str, content: str, append=False):
52
+ with open(filepath, 'a+' if append else 'w', encoding='utf-8') as f:
53
+ f.write(content)
54
+
55
+
56
+ def pkl_load(filepath: str):
57
+ return pickle.load(open(filepath, "rb"))
58
+
59
+
60
+ def pkl_save(obj, filepath: str):
61
+ pickle.dump(obj, cast(SupportsWriteBytes, open(filepath, "wb")))
File without changes
@@ -0,0 +1,61 @@
1
+ from smartdjango.error import Error
2
+ from smartdjango.validation.validator import Validator, ValidatorErrors
3
+
4
+
5
+ class DictValidator(Validator):
6
+ def __init__(self, *args, **kwargs):
7
+ super().__init__(*args, **kwargs)
8
+
9
+ self.field_validators = dict()
10
+
11
+ def copy(self):
12
+ new = super().copy()
13
+ new.field_validators = self.field_validators.copy()
14
+ return new
15
+
16
+ def field(self, validator: Validator | str):
17
+ if isinstance(validator, str):
18
+ validator = Validator(validator)
19
+ if validator.key is None:
20
+ raise ValueError('Validator key is required for DictValidator field')
21
+ if validator.key in self.field_validators:
22
+ raise ValidatorErrors.EXIST_PARAM_KEY(key=validator.key)
23
+ self.field_validators[validator.key] = validator
24
+ return self
25
+
26
+ def fields(self, *validators: Validator | str):
27
+ for validator in validators:
28
+ self.field(validator)
29
+ return self
30
+
31
+ def restrict_keys(self):
32
+ def key_validator(value):
33
+ for key in value:
34
+ if key not in self.field_validators:
35
+ raise ValidatorErrors.INVALID_KEY(key=key)
36
+ self.exception(key_validator)
37
+ return self
38
+
39
+ def clean(self, value):
40
+ value = super().clean(value)
41
+ if value is None:
42
+ return None
43
+ if not isinstance(value, dict):
44
+ raise ValidatorErrors.NOT_A_DICT
45
+
46
+ new_value = dict()
47
+ for key, validator in self.field_validators.items():
48
+ try:
49
+ final_value = validator.clean(value.get(key.name, Validator.unset()))
50
+ except Error as error:
51
+ raise error
52
+ new_value[key.final_name] = final_value
53
+ return new_value
54
+
55
+ def __str__(self):
56
+ string = super().__str__()
57
+ if self.field_validators:
58
+ string += ' with fields:'
59
+ for key, validator in self.field_validators.items():
60
+ string += self.indent('\n' + str(validator))
61
+ return string
@@ -0,0 +1,23 @@
1
+ class Key:
2
+ def __init__(self, name, verbose_name=None, final_name=None):
3
+ self.name = name
4
+ self.verbose_name = verbose_name or name
5
+ self.final_name = final_name or name
6
+
7
+ def __hash__(self):
8
+ return hash(self.name)
9
+
10
+ def __eq__(self, other):
11
+ if isinstance(other, Key):
12
+ return self.name == other.name
13
+ if isinstance(other, str):
14
+ return self.name == other
15
+ return False
16
+
17
+ def copy(self):
18
+ return Key(self.name, self.verbose_name, self.final_name)
19
+
20
+ def __str__(self):
21
+ if self.verbose_name != self.name:
22
+ return f'Key({self.name}, {self.verbose_name})'
23
+ return f'Key({self.name})'
@@ -0,0 +1,52 @@
1
+ from smartdjango.validation.validator import Validator, ValidatorErrors
2
+
3
+
4
+ class ListValidator(Validator):
5
+ def __init__(self, *args, **kwargs):
6
+ super().__init__(*args, **kwargs)
7
+ self.element_validator = None
8
+
9
+ def element(self, validator: Validator):
10
+ self.element_validator = validator
11
+ return self
12
+
13
+ def elements(self, *validators: Validator):
14
+ self.element_validator = list(validators)
15
+ return self
16
+
17
+ def copy(self):
18
+ new = super().copy()
19
+ new.element_validator = self.element_validator
20
+ if isinstance(self.element_validator, (list, tuple)):
21
+ new.element_validator = new.element_validator.copy()
22
+ return new
23
+
24
+ def clean(self, value):
25
+ value = super().clean(value)
26
+ if value is None:
27
+ return None
28
+ if not isinstance(value, list):
29
+ raise ValidatorErrors.NOT_A_LIST
30
+ if self.element_validator is None:
31
+ return value
32
+
33
+ if isinstance(self.element_validator, (list, tuple)):
34
+ if len(self.element_validator) != len(value):
35
+ raise ValidatorErrors.LIST_LENGTH_MISMATCH
36
+ values = [validator.clean(v) for validator, v in zip(self.element_validator, value)]
37
+ else:
38
+ values = [self.element_validator.clean(v) for v in value]
39
+
40
+ return values
41
+
42
+ def __str__(self):
43
+ string = super().__str__()
44
+ if self.element_validator is not None:
45
+ string += ' with element validator:'
46
+
47
+ if isinstance(self.element_validator, (list, tuple)):
48
+ for validator in self.element_validator:
49
+ string += self.indent('\n' + str(validator))
50
+ else:
51
+ string += self.indent('\n' + str(self.element_validator))
52
+ return string
@@ -0,0 +1,60 @@
1
+ from typing import Type
2
+
3
+ from django.db import models
4
+
5
+ from smartdjango.validation.validator import Validator
6
+
7
+
8
+ class Params(type):
9
+ model_class: Type[models.Model]
10
+ __params = None
11
+
12
+ @staticmethod
13
+ def _handler_source(model_class, *attr_names):
14
+ sources = []
15
+ for attr_name in attr_names:
16
+ source = getattr(model_class, attr_name, None)
17
+ if source is not None:
18
+ sources.append((attr_name, source))
19
+
20
+ unique_sources = []
21
+ for attr_name, source in sources:
22
+ if all(existing is not source for _, existing in unique_sources):
23
+ unique_sources.append((attr_name, source))
24
+
25
+ if len(unique_sources) > 1:
26
+ attr_names_str = ', '.join(name for name, _ in unique_sources)
27
+ raise AttributeError(f'Conflicting params hook sources: {attr_names_str}')
28
+
29
+ if not unique_sources:
30
+ return None
31
+ return unique_sources[0][1]
32
+
33
+ @classmethod
34
+ def _field_handler(cls, model_class, field_name, *attr_names):
35
+ source = cls._handler_source(model_class, *attr_names)
36
+ if source is None:
37
+ return None
38
+ handler = getattr(source, field_name, None)
39
+ if callable(handler):
40
+ return handler
41
+ return None
42
+
43
+ def __getattr__(cls, field_name):
44
+ if cls.__params is None:
45
+ cls.__params = {}
46
+
47
+ if field_name not in cls.__params:
48
+ field = cls.model_class._meta.get_field(field_name)
49
+ validator = Validator.from_field(field, name=field.name, verbose_name=field.verbose_name)
50
+
51
+ norm = cls._field_handler(cls.model_class, field_name, 'normalizers', 'normalizer', 'norm')
52
+ if norm is not None:
53
+ validator.to(norm)
54
+
55
+ vldt = cls._field_handler(cls.model_class, field_name, 'validators', 'validator', 'vldt')
56
+ if vldt is not None:
57
+ validator.exception(vldt)
58
+
59
+ cls.__params[field_name] = validator
60
+ return cls.__params[field_name]
@@ -0,0 +1,197 @@
1
+ import datetime
2
+ from typing import Callable, Union
3
+
4
+ from django.core.validators import BaseValidator
5
+ from django.db import models
6
+ from django.utils.translation import gettext as _
7
+
8
+ from smartdjango.code import Code
9
+ from smartdjango.error import Error
10
+ from smartdjango.validation.key import Key
11
+
12
+
13
+ @Error.register
14
+ class ValidatorErrors:
15
+ NO_DEFAULT = Error(_('No default value'), code=Code.BadRequest)
16
+ NULL_NOT_ALLOW = Error(_('Null is not allowed'), code=Code.BadRequest)
17
+ NOT_VALID = Error(_('Not valid: {message}'), code=Code.BadRequest)
18
+ VALIDATOR_CRUSHED = Error(_('Validator crushed'), code=Code.InternalServerError)
19
+ PROCESSOR_CRUSHED = Error(_('Processor crushed'), code=Code.InternalServerError)
20
+ NOT_A_LIST = Error(_('Not a list'), code=Code.BadRequest)
21
+ LIST_LENGTH_MISMATCH = Error(_('List length mismatch'), code=Code.BadRequest)
22
+ NOT_A_DICT = Error(_('Not a dict'), code=Code.BadRequest)
23
+ INVALID_KEY = Error(_('{key} is an invalid key'), code=Code.BadRequest)
24
+ EXIST_PARAM_KEY = Error(_('Param key {key} already exists'), code=Code.InternalServerError)
25
+
26
+
27
+ class Validator:
28
+ class __NoDefaultValue:
29
+ ...
30
+
31
+ class __UnSetValue:
32
+ ...
33
+
34
+ @classmethod
35
+ def unset(cls):
36
+ return cls.__UnSetValue
37
+
38
+ def __init__(self, name=None, verbose_name=None, final_name=None, **kwargs):
39
+ self.allow_null = False
40
+ self.default_value = self.__NoDefaultValue
41
+ self.to_python = []
42
+ self.validators = []
43
+
44
+ self._default_as_final = False
45
+
46
+ if isinstance(name, str):
47
+ name = Key(name, verbose_name, final_name)
48
+ if name and not isinstance(name, Key):
49
+ raise TypeError('name must be a string or Key instance')
50
+ self.key = name
51
+
52
+ def _carry_key_info(self, error: Error):
53
+ if self.key is None:
54
+ return error
55
+
56
+ err = error()
57
+ err.details.append(_('Target key: {key}').format(key=str(self.key)))
58
+ return err
59
+
60
+ def rename(self, name: Union[str, Key], verbose_name=__UnSetValue, final_name=__UnSetValue):
61
+ if isinstance(name, Key):
62
+ self.key = name
63
+ return self
64
+
65
+ self.key = self.key.copy() if self.key else Key(name)
66
+
67
+ self.key.name = name
68
+ if verbose_name is not self.unset():
69
+ self.key.verbose_name = verbose_name
70
+ if final_name is not self.unset():
71
+ self.key.final_name = final_name
72
+ return self
73
+
74
+ def copy(self):
75
+ new = Validator()
76
+ new.allow_null = self.allow_null
77
+ new.default_value = self.default_value
78
+ new.to_python = self.to_python.copy()
79
+ new.validators = self.validators.copy()
80
+ new._default_as_final = self._default_as_final
81
+ new.key = self.key.copy() if self.key else None
82
+ return new
83
+
84
+ def null(self, allow_null=True):
85
+ self.allow_null = allow_null
86
+ return self
87
+
88
+ def default(self, value, as_final=False):
89
+ self.default_value = value
90
+ self._default_as_final = as_final
91
+ return self
92
+
93
+ def to(self, to_python: Callable):
94
+ self.to_python.append(to_python)
95
+ return self
96
+
97
+ def exception(self, validator: Callable, message: str = None):
98
+ def wrap(value):
99
+ try:
100
+ validator(value)
101
+ except Error as e:
102
+ raise e
103
+ except Exception as err:
104
+ raise ValidatorErrors.NOT_VALID(message=message, details=err)
105
+
106
+ self.validators.append(wrap)
107
+ return self
108
+
109
+ def bool(self, validator: Callable, message: str = None):
110
+ def wrap(value):
111
+ if not validator(value):
112
+ raise ValidatorErrors.NOT_VALID(message=message or '')
113
+
114
+ return self.exception(wrap)
115
+
116
+ def clean(self, value):
117
+ if value is self.__UnSetValue:
118
+ if self.default_value is self.__NoDefaultValue:
119
+ raise self._carry_key_info(ValidatorErrors.NO_DEFAULT)
120
+
121
+ value = self.default_value
122
+ if self._default_as_final:
123
+ return value
124
+
125
+ if value is None:
126
+ if not self.allow_null:
127
+ raise self._carry_key_info(ValidatorErrors.NULL_NOT_ALLOW)
128
+ return None
129
+
130
+ for to_python in self.to_python:
131
+ try:
132
+ value = to_python(value)
133
+ except Error as e:
134
+ raise self._carry_key_info(e)
135
+ except Exception as err:
136
+ raise self._carry_key_info(ValidatorErrors.PROCESSOR_CRUSHED(details=err))
137
+ for validator in self.validators:
138
+ try:
139
+ validator(value)
140
+ except Error as e:
141
+ raise self._carry_key_info(e)
142
+ except Exception as err:
143
+ raise self._carry_key_info(ValidatorErrors.VALIDATOR_CRUSHED(details=err))
144
+ return value
145
+
146
+ def __call__(self, value):
147
+ return self.clean(value)
148
+
149
+ @classmethod
150
+ def from_field(cls, field: models.Field, *args, **kwargs):
151
+ validator = cls(*args, **kwargs)
152
+ validator.null(field.null)
153
+ if field.default != models.fields.NOT_PROVIDED:
154
+ validator.default(field.default)
155
+ if field.choices:
156
+ validator.bool(lambda x: x in dict(field.choices), message=_('Invalid choice'))
157
+ if field.validators:
158
+ for field_validator in field.validators:
159
+ if not isinstance(field_validator, BaseValidator):
160
+ validator.exception(field_validator)
161
+ if isinstance(field, models.CharField):
162
+ validator.bool(lambda x: isinstance(x, str), message=_('Not a string'))
163
+ validator.bool(lambda x: len(x) <= field.max_length, message=_('Too long'))
164
+ if isinstance(field, models.IntegerField):
165
+ validator.bool(lambda x: isinstance(x, int), message=_('Not an integer'))
166
+ if isinstance(field, models.FloatField):
167
+ validator.bool(lambda x: isinstance(x, float), message=_('Not a float'))
168
+ if isinstance(field, models.BooleanField):
169
+ validator.bool(lambda x: isinstance(x, bool), message=_('Not a boolean'))
170
+ if isinstance(field, models.DateField):
171
+ validator.bool(lambda x: isinstance(x, datetime.date), message=_('Not a date'))
172
+ if isinstance(field, models.DateTimeField):
173
+ validator.bool(lambda x: isinstance(x, datetime.datetime), message=_('Not a datetime'))
174
+ return validator
175
+
176
+ @property
177
+ def classname(self):
178
+ return self.__class__.__name__
179
+
180
+ @staticmethod
181
+ def indent(string, indent='\t'):
182
+ lines = string.split('\n')
183
+ return '\n'.join([indent + line for line in lines])
184
+
185
+ def __str__(self):
186
+ num_validators = len(self.validators)
187
+ name_str = f'{self.key}' if self.key else ''
188
+ if num_validators == 0:
189
+ validator_str = ''
190
+ elif num_validators == 1:
191
+ validator_str = ' 1 validator'
192
+ else:
193
+ validator_str = f'{num_validators} validators'
194
+ if name_str and validator_str:
195
+ name_str += ', '
196
+
197
+ return f'{self.classname}({name_str}{validator_str})'
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: smartdjango
3
+ Version: 4.4.0
4
+ Summary: fast Django app development
5
+ Home-page: https://github.com/Jyonn/smartdjango
6
+ Author: Jyonn Liu
7
+ Author-email: i@6-79.cn
8
+ License: MIT Licence
9
+ Keywords: django
10
+ Platform: any
11
+ License-File: LICENSE
12
+ Requires-Dist: django
13
+ Requires-Dist: oba
14
+ Requires-Dist: diq
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: description
18
+ Dynamic: home-page
19
+ Dynamic: keywords
20
+ Dynamic: license
21
+ Dynamic: license-file
22
+ Dynamic: platform
23
+ Dynamic: requires-dist
24
+ Dynamic: summary
25
+
26
+ field validation detector, model advanced search, unified error class
@@ -0,0 +1,24 @@
1
+ smartdjango/__init__.py,sha256=CEly2u6Zt8wnZ9JutDklexuCcOFwbMjisEL_ej1-n3A,752
2
+ smartdjango/analyse.py,sha256=f8sKVoiPC5qJY_hS5w_gGsbCIVnOciA-ok2sBOOvpSE,3378
3
+ smartdjango/choice.py,sha256=EYuJu4NU0lmGeVNuhi1WUYgsE2FexJeM_YXV7aPy9s0,138
4
+ smartdjango/code.py,sha256=u7mFnpPJUASqIfNHevsgks3M-BOSWVdo4dYLgeHkmvI,1481
5
+ smartdjango/error.py,sha256=xvDJkq9bJFX__Daj5ACsPzm6LcUdgIu_OQnKmY-kjfU,3140
6
+ smartdjango/middleware.py,sha256=q0eirW_f-dBZ9atTF_NCG1tCKyGA5IxuYxkFZn4ISqY,1067
7
+ smartdjango/paginator.py,sha256=BhN6lwcvgduYvO7CV0zoQ7OhmjiR25bfJvzAjTKWYa4,2189
8
+ smartdjango/models/__init__.py,sha256=xKJfWojCOCw3CpbCc0EQkJ9O2gRiJtHGqML32Y0posQ,461
9
+ smartdjango/models/manager.py,sha256=CS-7nLRElxX6RVT4S4a8gv59Xd4-198cLMVQq1ANAAY,329
10
+ smartdjango/models/queryset.py,sha256=iHP67SY4C7luc-EBa3b2Dm6xSBFXhNzC3WbTmaP3dJI,166
11
+ smartdjango/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ smartdjango/utils/inspect.py,sha256=-6gTEfEWVS1TL545G6tPUTu_RD157bqTLr9PKQC3VA0,220
13
+ smartdjango/utils/io.py,sha256=bECY69QVi-9lxOl__OaFIYVCw9CYt1CxglRO1Rnx6ks,1583
14
+ smartdjango/validation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ smartdjango/validation/dict_validator.py,sha256=4ICxEuO9UwkQTQvNseW5FOb0ILIVNaikEE6gqaDOE3o,2065
16
+ smartdjango/validation/key.py,sha256=Q6bKmHgmwC7Rw9zRuv6dEp2F0GgIR1ZYRd8YfU41fMM,703
17
+ smartdjango/validation/list_validator.py,sha256=RLFiGxkNR9RK27CZlNB_YYXzy9T5QCD9Zchu7JhwcSE,1817
18
+ smartdjango/validation/params.py,sha256=m7U_YjGXyP1tubcyhT6qEkixy4FORSLMFpbctv60fRY,2042
19
+ smartdjango/validation/validator.py,sha256=ImT_Awk4PWD6FNBhbTfUnjTXvtKn01ykXwWW-rtCeQw,7137
20
+ smartdjango-4.4.0.dist-info/licenses/LICENSE,sha256=MNmQxQoNjYQdJiUYOg0kOT_lCLbdR8p53uuWSFjNNss,1065
21
+ smartdjango-4.4.0.dist-info/METADATA,sha256=ijPO4CNaDcXmxXtxumGnJh3fr0zUiQvD3gmw3LcII94,580
22
+ smartdjango-4.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
23
+ smartdjango-4.4.0.dist-info/top_level.txt,sha256=U7Ho0CB4EW-R-rDFMAC7MU9kOsoR4Babc2O434oW_UM,12
24
+ smartdjango-4.4.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 JyonnLIU
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ smartdjango