adjango 0.3.6__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.
- adjango/__init__.py +0 -0
- adjango/adecorators.py +170 -0
- adjango/apps.py +9 -0
- adjango/aserializers.py +109 -0
- adjango/conf.py +26 -0
- adjango/constants/__init__.py +0 -0
- adjango/constants/times.py +53 -0
- adjango/decorators.py +195 -0
- adjango/descriptors.py +17 -0
- adjango/fields.py +11 -0
- adjango/handlers.py +74 -0
- adjango/management/__init__.py +0 -0
- adjango/management/commands/__init__.py +0 -0
- adjango/management/commands/add_paths.py +188 -0
- adjango/management/commands/copy_from_root.py +120 -0
- adjango/management/commands/copy_proj.py +150 -0
- adjango/management/commands/deletemigrations.py +38 -0
- adjango/management/commands/dumpdata_to_dir.py +38 -0
- adjango/management/commands/loaddata_from_dir.py +55 -0
- adjango/management/commands/remakemigrations.py +25 -0
- adjango/managers/__init__.py +0 -0
- adjango/managers/base.py +17 -0
- adjango/managers/polymorphic.py +10 -0
- adjango/middleware.py +33 -0
- adjango/models/__init__.py +6 -0
- adjango/models/base.py +20 -0
- adjango/models/mixins.py +42 -0
- adjango/models/polymorphic.py +15 -0
- adjango/querysets/__init__.py +0 -0
- adjango/querysets/base.py +26 -0
- adjango/querysets/polymorphic.py +24 -0
- adjango/serializers.py +62 -0
- adjango/services/__init__.py +1 -0
- adjango/services/base.py +9 -0
- adjango/services/polymorphic.py +18 -0
- adjango/tasks.py +15 -0
- adjango/testing.py +69 -0
- adjango/utils/__init__.py +0 -0
- adjango/utils/base.py +212 -0
- adjango/utils/celery/__init__.py +0 -0
- adjango/utils/celery/tasker.py +116 -0
- adjango/utils/common.py +39 -0
- adjango/utils/crontab.py +65 -0
- adjango/utils/funcs.py +173 -0
- adjango/utils/mail.py +31 -0
- adjango-0.3.6.dist-info/LICENSE +21 -0
- adjango-0.3.6.dist-info/METADATA +376 -0
- adjango-0.3.6.dist-info/RECORD +50 -0
- adjango-0.3.6.dist-info/WHEEL +5 -0
- adjango-0.3.6.dist-info/top_level.txt +1 -0
adjango/__init__.py
ADDED
|
File without changes
|
adjango/adecorators.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# wwwadecorators.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from time import time
|
|
9
|
+
from typing import Callable, Any
|
|
10
|
+
|
|
11
|
+
from asgiref.sync import sync_to_async
|
|
12
|
+
from django.conf import settings
|
|
13
|
+
from django.contrib.auth import REDIRECT_FIELD_NAME
|
|
14
|
+
from django.core.handlers.asgi import ASGIRequest
|
|
15
|
+
from django.http import HttpResponseNotAllowed, HttpResponse, QueryDict, RawPostDataException
|
|
16
|
+
|
|
17
|
+
from adjango.conf import ADJANGO_CONTROLLERS_LOGGER_NAME, ADJANGO_CONTROLLERS_LOGGING
|
|
18
|
+
from adjango.utils.base import AsyncAtomicContextManager
|
|
19
|
+
from adjango.utils.common import traceback_str
|
|
20
|
+
from adjango.utils.funcs import auser_passes_test
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def aforce_data(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
24
|
+
"""
|
|
25
|
+
Асинхронный декоратор для объединения данных из POST, GET и JSON тела запроса.
|
|
26
|
+
|
|
27
|
+
:param fn: Асинхронная функция, которая будет обернута.
|
|
28
|
+
|
|
29
|
+
:return: Асинхронная функция, в которой объединены данные из разных частей запроса.
|
|
30
|
+
|
|
31
|
+
@usage:
|
|
32
|
+
@force_data
|
|
33
|
+
def my_view(request):
|
|
34
|
+
print(request.data)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@wraps(fn)
|
|
38
|
+
async def _wrapped_view(request: ASGIRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
|
39
|
+
if not hasattr(request, 'data'): request.data = {}
|
|
40
|
+
request.data.update(request.POST.dict() if isinstance(request.POST, QueryDict) else request.POST)
|
|
41
|
+
request.data.update(request.GET.dict() if isinstance(request.GET, QueryDict) else request.GET)
|
|
42
|
+
try:
|
|
43
|
+
json_data = json.loads(request.body.decode('utf-8'))
|
|
44
|
+
if isinstance(json_data, dict): request.data.update(json_data)
|
|
45
|
+
except (ValueError, TypeError, UnicodeDecodeError, RawPostDataException):
|
|
46
|
+
pass
|
|
47
|
+
return await fn(request, *args, **kwargs)
|
|
48
|
+
|
|
49
|
+
return _wrapped_view
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def aatomic(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
53
|
+
"""
|
|
54
|
+
Асинхронный декоратор, который оборачивает представление в контекст менеджера транзакций.
|
|
55
|
+
|
|
56
|
+
@usage: @aatomic
|
|
57
|
+
async def my_view(request): ...
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
@wraps(fn)
|
|
61
|
+
async def _wrapped_view(request: ASGIRequest, *args: Any, **kwargs: Any) -> Any:
|
|
62
|
+
async with AsyncAtomicContextManager():
|
|
63
|
+
return await fn(request, *args, **kwargs)
|
|
64
|
+
|
|
65
|
+
return _wrapped_view
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def acontroller(
|
|
69
|
+
name: str | None = None,
|
|
70
|
+
logger: str = None,
|
|
71
|
+
log_name: bool = None,
|
|
72
|
+
log_time: bool = False
|
|
73
|
+
) -> Callable[..., Any]:
|
|
74
|
+
"""
|
|
75
|
+
Асинхронный контроллер с логированием и обработкой исключений.
|
|
76
|
+
|
|
77
|
+
:param name: Название контроллера.
|
|
78
|
+
:param logger: Имя логгера для записи сообщений.
|
|
79
|
+
:param log_name: Логировать имя контроллера.
|
|
80
|
+
:param log_time: Логировать время выполнения контроллера.
|
|
81
|
+
|
|
82
|
+
:return: Асинхронный контроллер с логированием и обработкой исключений.
|
|
83
|
+
|
|
84
|
+
@usage:
|
|
85
|
+
@acontroller
|
|
86
|
+
async def my_view(request):
|
|
87
|
+
...
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
91
|
+
@wraps(fn)
|
|
92
|
+
async def inner(request: ASGIRequest, *args: Any, **kwargs: Any) -> Any:
|
|
93
|
+
log = logging.getLogger(logger or ADJANGO_CONTROLLERS_LOGGER_NAME)
|
|
94
|
+
fn_name = name or fn.__name__
|
|
95
|
+
start_time = None
|
|
96
|
+
if log_name or (log_name is None and ADJANGO_CONTROLLERS_LOGGING):
|
|
97
|
+
log.info(f'ACtrl: {request.method} | {fn_name}')
|
|
98
|
+
|
|
99
|
+
if log_time: start_time = time()
|
|
100
|
+
if settings.DEBUG:
|
|
101
|
+
result = await fn(request, *args, **kwargs)
|
|
102
|
+
if log_time:
|
|
103
|
+
end_time = time()
|
|
104
|
+
elapsed_time = end_time - start_time
|
|
105
|
+
log.info(f"Execution time {fn_name}: {elapsed_time:.2f} seconds")
|
|
106
|
+
return result
|
|
107
|
+
else:
|
|
108
|
+
try:
|
|
109
|
+
result = await fn(request, *args, **kwargs)
|
|
110
|
+
if log_time:
|
|
111
|
+
end_time = time()
|
|
112
|
+
elapsed_time = end_time - start_time
|
|
113
|
+
log.info(f"Execution time {fn_name}: {elapsed_time:.2f} seconds")
|
|
114
|
+
return result
|
|
115
|
+
except Exception as e:
|
|
116
|
+
log.critical(f"ERROR in {fn_name}: {traceback_str(e)}", exc_info=True)
|
|
117
|
+
|
|
118
|
+
raise e
|
|
119
|
+
|
|
120
|
+
return inner
|
|
121
|
+
|
|
122
|
+
return decorator
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def aallowed_only(allowed_methods: list[str]) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
126
|
+
"""
|
|
127
|
+
Асинхронный декоратор для ограничения методов запроса.
|
|
128
|
+
|
|
129
|
+
:param allowed_methods: Список разрешенных методов (GET, POST и т.д.).
|
|
130
|
+
|
|
131
|
+
:return: Асинхронная функция, которая ограничивает вызов view-функции в зависимости от метода запроса.
|
|
132
|
+
|
|
133
|
+
@usage:
|
|
134
|
+
@aallowed_only(['GET', 'POST'])
|
|
135
|
+
async def my_view(request):
|
|
136
|
+
...
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
140
|
+
async def wrapped_view(request: ASGIRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
|
141
|
+
if request.method in allowed_methods:
|
|
142
|
+
if asyncio.iscoroutinefunction(fn):
|
|
143
|
+
return await fn(request, *args, **kwargs)
|
|
144
|
+
else:
|
|
145
|
+
return fn(request, *args, **kwargs)
|
|
146
|
+
else:
|
|
147
|
+
return HttpResponseNotAllowed(allowed_methods)
|
|
148
|
+
|
|
149
|
+
return wrapped_view
|
|
150
|
+
|
|
151
|
+
return decorator
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def alogin_required(
|
|
155
|
+
function: Callable[..., Any] | None = None,
|
|
156
|
+
redirect_field_name: str = REDIRECT_FIELD_NAME,
|
|
157
|
+
login_url: str | None = None,
|
|
158
|
+
) -> Callable[..., Any]:
|
|
159
|
+
"""
|
|
160
|
+
Asynchronous decorator for views that checks if the user is authenticated,
|
|
161
|
+
redirecting to the login page if necessary.
|
|
162
|
+
"""
|
|
163
|
+
actual_decorator = auser_passes_test(
|
|
164
|
+
sync_to_async(lambda u: u.is_authenticated),
|
|
165
|
+
login_url=login_url,
|
|
166
|
+
redirect_field_name=redirect_field_name,
|
|
167
|
+
)
|
|
168
|
+
if function:
|
|
169
|
+
return actual_decorator(function)
|
|
170
|
+
return actual_decorator
|
adjango/apps.py
ADDED
adjango/aserializers.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from typing import TypedDict, List
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from rest_framework.serializers import ListSerializer as DRFListSerializer
|
|
5
|
+
from rest_framework.serializers import ModelSerializer as DRFModelSerializer
|
|
6
|
+
from rest_framework.serializers import Serializer as DRFSerializer
|
|
7
|
+
from rest_framework import status
|
|
8
|
+
from rest_framework.exceptions import APIException
|
|
9
|
+
from rest_framework.status import HTTP_400_BAD_REQUEST
|
|
10
|
+
except ImportError:
|
|
11
|
+
pass
|
|
12
|
+
from asgiref.sync import sync_to_async
|
|
13
|
+
from django.utils.translation import gettext_lazy as _
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FieldError(TypedDict):
|
|
17
|
+
field: str
|
|
18
|
+
message: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def serializer_errors_to_field_errors(serializer_errors) -> List[FieldError]:
|
|
22
|
+
field_errors = []
|
|
23
|
+
for field, messages in serializer_errors.items():
|
|
24
|
+
for message in messages:
|
|
25
|
+
field_errors.append(FieldError(
|
|
26
|
+
field=field,
|
|
27
|
+
message=_(message)
|
|
28
|
+
))
|
|
29
|
+
return field_errors
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DetailExceptionDict(TypedDict):
|
|
33
|
+
message: str
|
|
34
|
+
fields_errors: List[FieldError]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DetailAPIException(APIException):
|
|
38
|
+
status_code = status.HTTP_400_BAD_REQUEST
|
|
39
|
+
|
|
40
|
+
def __init__(self, detail: DetailExceptionDict, code: str = None, status_code: str = None):
|
|
41
|
+
if status_code is not None:
|
|
42
|
+
self.status_code = status_code
|
|
43
|
+
super().__init__(detail=detail, code=code or 'error')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SerializerErrors(DetailAPIException):
|
|
47
|
+
def __init__(self, serializer_errors: dict, code: str = None, status_code: str = HTTP_400_BAD_REQUEST,
|
|
48
|
+
message: str = _('Correct the mistakes.')):
|
|
49
|
+
detail = DetailExceptionDict(
|
|
50
|
+
message=message,
|
|
51
|
+
fields_errors=serializer_errors_to_field_errors(serializer_errors)
|
|
52
|
+
)
|
|
53
|
+
super().__init__(detail=detail, code=code, status_code=status_code)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AListSerializer(DRFListSerializer):
|
|
57
|
+
@property
|
|
58
|
+
async def adata(self):
|
|
59
|
+
items_data = []
|
|
60
|
+
for item in self.instance:
|
|
61
|
+
serializer = self.child.__class__(item, context=self.context)
|
|
62
|
+
data = await serializer.adata
|
|
63
|
+
items_data.append(data)
|
|
64
|
+
return items_data
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ASerializer(DRFSerializer):
|
|
68
|
+
async def asave(self, **kwargs):
|
|
69
|
+
return await sync_to_async(self.save)(**kwargs)
|
|
70
|
+
|
|
71
|
+
async def ais_valid(self, raise_exception=False, **kwargs):
|
|
72
|
+
is_valid = await sync_to_async(self.is_valid)(**kwargs)
|
|
73
|
+
if raise_exception and not is_valid:
|
|
74
|
+
raise SerializerErrors(self.errors)
|
|
75
|
+
return is_valid
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
async def adata(self): return await sync_to_async(lambda: self.data)()
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
async def avalid_data(self): return await sync_to_async(lambda: self.validated_data)()
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def many_init(cls, *args, **kwargs):
|
|
85
|
+
kwargs['child'] = cls()
|
|
86
|
+
return AListSerializer(*args, **kwargs)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AModelSerializer(DRFModelSerializer):
|
|
90
|
+
async def asave(self, **kwargs):
|
|
91
|
+
return await sync_to_async(self.save)(**kwargs)
|
|
92
|
+
|
|
93
|
+
async def ais_valid(self, raise_exception=False, **kwargs):
|
|
94
|
+
is_valid = await sync_to_async(self.is_valid)(**kwargs)
|
|
95
|
+
if raise_exception and not is_valid: raise SerializerErrors(self.errors)
|
|
96
|
+
return is_valid
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
async def adata(self):
|
|
100
|
+
return await sync_to_async(lambda: self.data)()
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
async def avalid_data(self):
|
|
104
|
+
return await sync_to_async(lambda: self.validated_data)()
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def many_init(cls, *args, **kwargs):
|
|
108
|
+
kwargs['child'] = cls()
|
|
109
|
+
return AListSerializer(*args, **kwargs)
|
adjango/conf.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# conf.py
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_setting(name, default=None, required=False):
|
|
6
|
+
value = getattr(settings, name, None)
|
|
7
|
+
if value is None:
|
|
8
|
+
if required:
|
|
9
|
+
if default is not None:
|
|
10
|
+
return default
|
|
11
|
+
raise ValueError(f'Missing required django setting: {name}')
|
|
12
|
+
return default
|
|
13
|
+
return value
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
ADJANGO_BACKENDS_APPS = get_setting('ADJANGO_BACKENDS_APPS', settings.BASE_DIR)
|
|
17
|
+
ADJANGO_FRONTEND_APPS = get_setting('ADJANGO_FRONTEND_APPS', settings.BASE_DIR)
|
|
18
|
+
ADJANGO_APPS_PREPATH = get_setting('ADJANGO_APPS_PREPATH')
|
|
19
|
+
ADJANGO_UNCAUGHT_EXCEPTION_HANDLING_FUNCTION = get_setting(
|
|
20
|
+
'ADJANGO_UNCAUGHT_EXCEPTION_HANDLING_FUNCTION',
|
|
21
|
+
)
|
|
22
|
+
ADJANGO_CONTROLLERS_LOGGER_NAME = get_setting('ADJANGO_CONTROLLERS_LOGGER_NAME', 'global')
|
|
23
|
+
ADJANGO_CONTROLLERS_LOGGING = get_setting('ADJANGO_CONTROLLERS_LOGGING',)
|
|
24
|
+
ADJANGO_EMAIL_LOGGER_NAME = get_setting('ADJANGO_EMAIL_LOGGER_NAME', 'email')
|
|
25
|
+
ADJANGO_IP_LOGGER = get_setting('ADJANGO_IP_LOGGER')
|
|
26
|
+
ADJANGO_IP_META_NAME = get_setting('ADJANGO_IP_META_NAME')
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# constants/times.py
|
|
2
|
+
MIN_1 = 1 * 60
|
|
3
|
+
MIN_5 = 5 * 60
|
|
4
|
+
MIN_10 = 10 * 60
|
|
5
|
+
MIN_15 = 15 * 60
|
|
6
|
+
MIN_20 = 20 * 60
|
|
7
|
+
MIN_25 = 25 * 60
|
|
8
|
+
MIN_30 = 30 * 60
|
|
9
|
+
MIN_40 = 40 * 60
|
|
10
|
+
MIN_50 = 50 * 60
|
|
11
|
+
HOUR_1 = 1 * 60 * 60
|
|
12
|
+
HOUR_2 = 2 * 60 * 60
|
|
13
|
+
HOUR_3 = 3 * 60 * 60
|
|
14
|
+
HOUR_4 = 4 * 60 * 60
|
|
15
|
+
HOUR_5 = 5 * 60 * 60
|
|
16
|
+
HOUR_6 = 6 * 60 * 60
|
|
17
|
+
HOUR_7 = 7 * 60 * 60
|
|
18
|
+
HOUR_8 = 8 * 60 * 60
|
|
19
|
+
HOUR_9 = 9 * 60 * 60
|
|
20
|
+
HOUR_10 = 10 * 60 * 60
|
|
21
|
+
HOUR_11 = 11 * 60 * 60
|
|
22
|
+
HOUR_12 = 12 * 60 * 60
|
|
23
|
+
HOUR_13 = 13 * 60 * 60
|
|
24
|
+
HOUR_14 = 14 * 60 * 60
|
|
25
|
+
HOUR_15 = 15 * 60 * 60
|
|
26
|
+
HOUR_16 = 16 * 60 * 60
|
|
27
|
+
HOUR_17 = 17 * 60 * 60
|
|
28
|
+
HOUR_18 = 18 * 60 * 60
|
|
29
|
+
HOUR_19 = 19 * 60 * 60
|
|
30
|
+
HOUR_20 = 20 * 60 * 60
|
|
31
|
+
HOUR_21 = 21 * 60 * 60
|
|
32
|
+
HOUR_22 = 22 * 60 * 60
|
|
33
|
+
HOUR_23 = 23 * 60 * 60
|
|
34
|
+
DAY_1 = 1 * 60 * 60 * 24
|
|
35
|
+
DAYS_2 = 2 * 60 * 60 * 24
|
|
36
|
+
DAYS_3 = 3 * 60 * 60 * 24
|
|
37
|
+
DAYS_4 = 4 * 60 * 60 * 24
|
|
38
|
+
DAYS_5 = 5 * 60 * 60 * 24
|
|
39
|
+
DAYS_6 = 6 * 60 * 60 * 24
|
|
40
|
+
DAYS_7 = 7 * 60 * 60 * 24
|
|
41
|
+
DAYS_8 = 8 * 60 * 60 * 24
|
|
42
|
+
DAYS_9 = 9 * 60 * 60 * 24
|
|
43
|
+
DAYS_10 = 10 * 60 * 60 * 24
|
|
44
|
+
DAYS_11 = 11 * 60 * 60 * 24
|
|
45
|
+
DAYS_12 = 12 * 60 * 60 * 24
|
|
46
|
+
DAYS_13 = 13 * 60 * 60 * 24
|
|
47
|
+
DAYS_14 = 14 * 60 * 60 * 24
|
|
48
|
+
DAYS_15 = 15 * 60 * 60 * 24
|
|
49
|
+
DAYS_16 = 16 * 60 * 60 * 24
|
|
50
|
+
DAYS_17 = 17 * 60 * 60 * 24
|
|
51
|
+
DAYS_18 = 18 * 60 * 60 * 24
|
|
52
|
+
DAYS_19 = 19 * 60 * 60 * 24
|
|
53
|
+
DAYS_20 = 20 * 60 * 60 * 24
|
adjango/decorators.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# decorators.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from time import time
|
|
8
|
+
from typing import Callable, Any
|
|
9
|
+
|
|
10
|
+
from django.conf import settings
|
|
11
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
12
|
+
from django.http import HttpResponseNotAllowed, HttpResponse, QueryDict, RawPostDataException
|
|
13
|
+
from django.shortcuts import redirect
|
|
14
|
+
|
|
15
|
+
from adjango.conf import ADJANGO_UNCAUGHT_EXCEPTION_HANDLING_FUNCTION, ADJANGO_CONTROLLERS_LOGGING, \
|
|
16
|
+
ADJANGO_CONTROLLERS_LOGGER_NAME
|
|
17
|
+
from adjango.utils.common import traceback_str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def admin_description(description: str):
|
|
21
|
+
def decorator(func):
|
|
22
|
+
func.short_description = description
|
|
23
|
+
return func
|
|
24
|
+
|
|
25
|
+
return decorator
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def admin_label(label: str):
|
|
29
|
+
def decorator(func):
|
|
30
|
+
func.label = label
|
|
31
|
+
return func
|
|
32
|
+
|
|
33
|
+
return decorator
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def admin_order_field(field: str):
|
|
37
|
+
def decorator(func):
|
|
38
|
+
func.admin_order_field = field
|
|
39
|
+
return func
|
|
40
|
+
|
|
41
|
+
return decorator
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def admin_allow_tags(allow: bool = True):
|
|
45
|
+
def decorator(func):
|
|
46
|
+
func.allow_tags = allow
|
|
47
|
+
return func
|
|
48
|
+
|
|
49
|
+
return decorator
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def task(logger: str = None):
|
|
53
|
+
"""
|
|
54
|
+
Декоратор для задач Celery, который логирует начало и конец выполнения задачи и её ошибки.
|
|
55
|
+
|
|
56
|
+
:param logger: Имя логгера для логирования. Если не передано, логирование не будет выполнено.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def decorator(func):
|
|
60
|
+
@wraps(func)
|
|
61
|
+
def wrapper(*args, **kwargs):
|
|
62
|
+
log = None
|
|
63
|
+
if logger:
|
|
64
|
+
log = logging.getLogger(logger)
|
|
65
|
+
log.info(f"Start executing task: {func.__name__}\n{args}\n{kwargs}")
|
|
66
|
+
try:
|
|
67
|
+
result = func(*args, **kwargs)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
log.critical(f'Error executing task: {func.__name__}')
|
|
70
|
+
log.critical(traceback_str(e))
|
|
71
|
+
raise e
|
|
72
|
+
if log: log.info(f"End executing task: {func.__name__}\n{args}\n{kwargs}")
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
return wrapper
|
|
76
|
+
|
|
77
|
+
return decorator
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def allowed_only(allowed_methods: list[str]) -> Callable[[Callable[..., HttpResponse]], Callable[..., HttpResponse]]:
|
|
81
|
+
"""
|
|
82
|
+
Декоратор для ограничения методов запроса.
|
|
83
|
+
|
|
84
|
+
:param allowed_methods: Список разрешенных методов (GET, POST и т.д.).
|
|
85
|
+
|
|
86
|
+
:return: Функция, которая ограничивает вызов view-функции в зависимости от метода запроса.
|
|
87
|
+
|
|
88
|
+
@usage:
|
|
89
|
+
@allowed_only(['GET', 'POST'])
|
|
90
|
+
def my_view(request):
|
|
91
|
+
...
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def decorator(fn: Callable[..., HttpResponse]) -> Callable[..., HttpResponse]:
|
|
95
|
+
def wrapped_view(request: WSGIRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
|
96
|
+
if request.method in allowed_methods:
|
|
97
|
+
return fn(request, *args, **kwargs)
|
|
98
|
+
else:
|
|
99
|
+
return HttpResponseNotAllowed(allowed_methods)
|
|
100
|
+
|
|
101
|
+
return wrapped_view
|
|
102
|
+
|
|
103
|
+
return decorator
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def force_data(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
107
|
+
"""
|
|
108
|
+
Декоратор для объединения данных из POST, GET и JSON тела запроса.
|
|
109
|
+
|
|
110
|
+
:param fn: Функция, которая будет обернута.
|
|
111
|
+
|
|
112
|
+
:return: Функция, в которой объединены данные из разных частей запроса.
|
|
113
|
+
|
|
114
|
+
@usage:
|
|
115
|
+
@force_data
|
|
116
|
+
def my_view(request):
|
|
117
|
+
print(request.data)
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
@wraps(fn)
|
|
121
|
+
def _wrapped_view(request: WSGIRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
|
122
|
+
if not hasattr(request, 'data'): request.data = {}
|
|
123
|
+
request.data.update(request.POST.dict() if isinstance(request.POST, QueryDict) else request.POST)
|
|
124
|
+
request.data.update(request.GET.dict() if isinstance(request.GET, QueryDict) else request.GET)
|
|
125
|
+
try:
|
|
126
|
+
json_data = json.loads(request.body.decode('utf-8'))
|
|
127
|
+
if isinstance(json_data, dict): request.data.update(json_data)
|
|
128
|
+
except (ValueError, TypeError, UnicodeDecodeError, RawPostDataException):
|
|
129
|
+
pass
|
|
130
|
+
return fn(request, *args, **kwargs)
|
|
131
|
+
|
|
132
|
+
return _wrapped_view
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def controller(
|
|
136
|
+
name: str | None = None,
|
|
137
|
+
logger: str = None,
|
|
138
|
+
log_name: bool = True,
|
|
139
|
+
log_time: bool = False,
|
|
140
|
+
auth_required: bool = False,
|
|
141
|
+
not_auth_redirect: str = settings.LOGIN_URL
|
|
142
|
+
) -> Callable[..., Any]:
|
|
143
|
+
"""
|
|
144
|
+
Синхронный контроллер с логированием, проверкой аутентификации и обработкой исключений.
|
|
145
|
+
|
|
146
|
+
:param name: Название контроллера.
|
|
147
|
+
:param logger: Имя логгера для записи сообщений.
|
|
148
|
+
:param log_name: Логировать имя контроллера.
|
|
149
|
+
:param log_time: Логировать время выполнения контроллера.
|
|
150
|
+
:param auth_required: Проверять ли аутентификацию пользователя.
|
|
151
|
+
:param not_auth_redirect: URL для редиректа, если пользователь не аутентифицирован.
|
|
152
|
+
|
|
153
|
+
:return: Синхронный контроллер с логированием и обработкой исключений.
|
|
154
|
+
|
|
155
|
+
@usage:
|
|
156
|
+
@controller
|
|
157
|
+
def my_view(request):
|
|
158
|
+
...
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
162
|
+
@wraps(fn)
|
|
163
|
+
def inner(request: WSGIRequest, *args: Any, **kwargs: Any) -> Any:
|
|
164
|
+
log = logging.getLogger(logger or ADJANGO_CONTROLLERS_LOGGER_NAME)
|
|
165
|
+
fn_name = name or fn.__name__
|
|
166
|
+
start_time = None
|
|
167
|
+
if log_name or (log_name is None and ADJANGO_CONTROLLERS_LOGGING):
|
|
168
|
+
log.info(f'Ctrl: {request.method} | {fn_name}')
|
|
169
|
+
if log_time: start_time = time()
|
|
170
|
+
if auth_required and not request.user.is_authenticated: return redirect(not_auth_redirect)
|
|
171
|
+
if settings.DEBUG:
|
|
172
|
+
result = fn(request, *args, **kwargs)
|
|
173
|
+
if log_time:
|
|
174
|
+
end_time = time()
|
|
175
|
+
elapsed_time = end_time - start_time
|
|
176
|
+
log.info(f"Execution time {fn_name}: {elapsed_time:.2f} seconds")
|
|
177
|
+
return result
|
|
178
|
+
else:
|
|
179
|
+
try:
|
|
180
|
+
result = fn(request, *args, **kwargs)
|
|
181
|
+
if log_time:
|
|
182
|
+
end_time = time()
|
|
183
|
+
elapsed_time = end_time - start_time
|
|
184
|
+
log.info(f"Execution time {fn_name}: {elapsed_time:.2f} seconds")
|
|
185
|
+
return result
|
|
186
|
+
except Exception as e:
|
|
187
|
+
log.critical(f"ERROR in {fn_name}: {traceback_str(e)}", exc_info=True)
|
|
188
|
+
if hasattr(settings, 'ADJANGO_UNCAUGHT_EXCEPTION_HANDLING_FUNCTION'):
|
|
189
|
+
handling_function = ADJANGO_UNCAUGHT_EXCEPTION_HANDLING_FUNCTION
|
|
190
|
+
if callable(handling_function): handling_function(fn_name, request, e, *args, **kwargs)
|
|
191
|
+
raise e
|
|
192
|
+
|
|
193
|
+
return inner
|
|
194
|
+
|
|
195
|
+
return decorator
|
adjango/descriptors.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# descriptors.py
|
|
2
|
+
from django.db.models.fields.related_descriptors import ManyToManyDescriptor
|
|
3
|
+
|
|
4
|
+
from adjango.managers.base import AManager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AManyToManyDescriptor(ManyToManyDescriptor):
|
|
8
|
+
@property
|
|
9
|
+
def related_manager_cls(self):
|
|
10
|
+
# Get the original related_manager_cls
|
|
11
|
+
original_manager_cls = super().related_manager_cls
|
|
12
|
+
|
|
13
|
+
# Define a new manager class that extends the original and adds the 'aall' method
|
|
14
|
+
class AManyRelatedManager(original_manager_cls, AManager):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
return AManyRelatedManager
|
adjango/fields.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# fields.py
|
|
2
|
+
from django.db.models import ManyToManyField
|
|
3
|
+
|
|
4
|
+
from adjango.descriptors import AManyToManyDescriptor
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AManyToManyField(ManyToManyField):
|
|
8
|
+
def contribute_to_class(self, cls, name, **kwargs):
|
|
9
|
+
super().contribute_to_class(cls, name, **kwargs)
|
|
10
|
+
# Replace the descriptor with our custom one
|
|
11
|
+
setattr(cls, self.name, AManyToManyDescriptor(self.remote_field, reverse=False))
|
adjango/handlers.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# handlers.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
|
|
6
|
+
from django.core.handlers.asgi import ASGIRequest
|
|
7
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
8
|
+
|
|
9
|
+
from adjango.utils.common import traceback_str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class IHandlerControllerException(ABC):
|
|
13
|
+
@staticmethod
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def handle(fn_name: str, request: WSGIRequest | ASGIRequest, e: Exception, *args, **kwargs) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Пример функции обработки исключений.
|
|
18
|
+
|
|
19
|
+
:param fn_name: Имя функции, в которой произошло исключение.
|
|
20
|
+
:param request: Объект запроса (WSGIRequest или ASGIRequest).
|
|
21
|
+
:param e: Исключение, которое нужно обработать.
|
|
22
|
+
:param args: Позиционные аргументы, переданные в функцию.
|
|
23
|
+
:param kwargs: Именованные аргументы, переданные в функцию.
|
|
24
|
+
|
|
25
|
+
:return: None
|
|
26
|
+
|
|
27
|
+
@usage:
|
|
28
|
+
_handling_function(fn_name, request, e)
|
|
29
|
+
"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HCE(IHandlerControllerException):
|
|
34
|
+
"""
|
|
35
|
+
Пример реализации обработчика исключений контроллеров.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def handle(fn_name: str, request: WSGIRequest | ASGIRequest, e: Exception, *args, **kwargs) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Пример функции обработки исключений.
|
|
42
|
+
|
|
43
|
+
:param fn_name: Имя функции, в которой произошло исключение.
|
|
44
|
+
:param request: Объект запроса (WSGIRequest или ASGIRequest).
|
|
45
|
+
:param e: Исключение, которое нужно обработать.
|
|
46
|
+
:param args: Позиционные аргументы, переданные в функцию.
|
|
47
|
+
:param kwargs: Именованные аргументы, переданные в функцию.
|
|
48
|
+
|
|
49
|
+
:return: None
|
|
50
|
+
|
|
51
|
+
@usage:
|
|
52
|
+
_handling_function(fn_name, request, e)
|
|
53
|
+
"""
|
|
54
|
+
import logging
|
|
55
|
+
from django.conf import settings
|
|
56
|
+
from adjango.tasks import send_emails_task
|
|
57
|
+
log = logging.getLogger('global')
|
|
58
|
+
error_text = (f'ERROR in {fn_name}:\n'
|
|
59
|
+
f'{traceback_str(e)}\n'
|
|
60
|
+
f'{request.POST=}\n'
|
|
61
|
+
f'{request.GET=}\n'
|
|
62
|
+
f'{request.FILES=}\n'
|
|
63
|
+
f'{request.COOKIES=}\n'
|
|
64
|
+
f'{request.user=}\n'
|
|
65
|
+
f'{args=}\n'
|
|
66
|
+
f'{kwargs=}')
|
|
67
|
+
log.error(error_text)
|
|
68
|
+
if not settings.DEBUG:
|
|
69
|
+
send_emails_task.delay(
|
|
70
|
+
subject='SERVER ERROR',
|
|
71
|
+
emails=('admin@example.com', 'admin2@example.com',),
|
|
72
|
+
template='admin/exception_report.html',
|
|
73
|
+
context={'error': error_text}
|
|
74
|
+
)
|
|
File without changes
|
|
File without changes
|