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.
Files changed (50) hide show
  1. adjango/__init__.py +0 -0
  2. adjango/adecorators.py +170 -0
  3. adjango/apps.py +9 -0
  4. adjango/aserializers.py +109 -0
  5. adjango/conf.py +26 -0
  6. adjango/constants/__init__.py +0 -0
  7. adjango/constants/times.py +53 -0
  8. adjango/decorators.py +195 -0
  9. adjango/descriptors.py +17 -0
  10. adjango/fields.py +11 -0
  11. adjango/handlers.py +74 -0
  12. adjango/management/__init__.py +0 -0
  13. adjango/management/commands/__init__.py +0 -0
  14. adjango/management/commands/add_paths.py +188 -0
  15. adjango/management/commands/copy_from_root.py +120 -0
  16. adjango/management/commands/copy_proj.py +150 -0
  17. adjango/management/commands/deletemigrations.py +38 -0
  18. adjango/management/commands/dumpdata_to_dir.py +38 -0
  19. adjango/management/commands/loaddata_from_dir.py +55 -0
  20. adjango/management/commands/remakemigrations.py +25 -0
  21. adjango/managers/__init__.py +0 -0
  22. adjango/managers/base.py +17 -0
  23. adjango/managers/polymorphic.py +10 -0
  24. adjango/middleware.py +33 -0
  25. adjango/models/__init__.py +6 -0
  26. adjango/models/base.py +20 -0
  27. adjango/models/mixins.py +42 -0
  28. adjango/models/polymorphic.py +15 -0
  29. adjango/querysets/__init__.py +0 -0
  30. adjango/querysets/base.py +26 -0
  31. adjango/querysets/polymorphic.py +24 -0
  32. adjango/serializers.py +62 -0
  33. adjango/services/__init__.py +1 -0
  34. adjango/services/base.py +9 -0
  35. adjango/services/polymorphic.py +18 -0
  36. adjango/tasks.py +15 -0
  37. adjango/testing.py +69 -0
  38. adjango/utils/__init__.py +0 -0
  39. adjango/utils/base.py +212 -0
  40. adjango/utils/celery/__init__.py +0 -0
  41. adjango/utils/celery/tasker.py +116 -0
  42. adjango/utils/common.py +39 -0
  43. adjango/utils/crontab.py +65 -0
  44. adjango/utils/funcs.py +173 -0
  45. adjango/utils/mail.py +31 -0
  46. adjango-0.3.6.dist-info/LICENSE +21 -0
  47. adjango-0.3.6.dist-info/METADATA +376 -0
  48. adjango-0.3.6.dist-info/RECORD +50 -0
  49. adjango-0.3.6.dist-info/WHEEL +5 -0
  50. 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
@@ -0,0 +1,9 @@
1
+ # apps.py
2
+ from django.apps import AppConfig
3
+ from django.utils.translation import gettext_lazy as _
4
+
5
+
6
+ class ADjangoConfig(AppConfig):
7
+ default_auto_field = 'django.db.models.BigAutoField'
8
+ name = 'adjango'
9
+ verbose_name = _('ADjango')
@@ -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