edu-async-tasks 1.0.0__tar.gz
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.
- edu_async_tasks-1.0.0/PKG-INFO +38 -0
- edu_async_tasks-1.0.0/README.md +11 -0
- edu_async_tasks-1.0.0/pyproject.toml +46 -0
- edu_async_tasks-1.0.0/setup.cfg +4 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/__init__.py +35 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/config.py +57 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/core/__init__.py +1 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/core/adapters/__init__.py +0 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/core/adapters/db.py +35 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/core/apps.py +9 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/core/domain/__init__.py +7 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/core/domain/model.py +163 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/core/migrations/0001_initial.py +65 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/core/migrations/0002_task_type_and_status_data.py +115 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/core/migrations/0003_rename_async_task__profile_c97023_idx_edu_async_t_profile_70c100_idx_and_more.py +26 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/core/migrations/__init__.py +0 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/core/models.py +232 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/core/services.py +94 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/core/tasks.py +362 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/__init__.py +0 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/apps.py +9 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/async_task_statuses/__init__.py +0 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/async_task_statuses/serializers.py +14 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/async_task_statuses/urls.py +20 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/async_task_statuses/views.py +20 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/async_task_types/__init__.py +0 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/async_task_types/serializers.py +14 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/async_task_types/urls.py +20 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/async_task_types/views.py +20 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/running_tasks/__init__.py +0 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/running_tasks/filters.py +30 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/running_tasks/serializers.py +22 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/running_tasks/tests.py +0 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/running_tasks/urls.py +12 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/running_tasks/views.py +82 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/urls.py +23 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/utils/__init__.py +0 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/utils/bootstrap.py +21 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks/rest/utils/pagination.py +10 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks.egg-info/PKG-INFO +38 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks.egg-info/SOURCES.txt +61 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks.egg-info/dependency_links.txt +1 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks.egg-info/requires.txt +10 -0
- edu_async_tasks-1.0.0/src/edu_async_tasks.egg-info/top_level.txt +2 -0
- edu_async_tasks-1.0.0/src/testapp/__init__.py +0 -0
- edu_async_tasks-1.0.0/src/testapp/app/__init__.py +0 -0
- edu_async_tasks-1.0.0/src/testapp/app/apps.py +13 -0
- edu_async_tasks-1.0.0/src/testapp/app/tasks.py +50 -0
- edu_async_tasks-1.0.0/src/testapp/app/tests/__init__.py +0 -0
- edu_async_tasks-1.0.0/src/testapp/app/tests/test_config.py +72 -0
- edu_async_tasks-1.0.0/src/testapp/app/tests/test_tasks.py +44 -0
- edu_async_tasks-1.0.0/src/testapp/entrypoints/__init__.py +0 -0
- edu_async_tasks-1.0.0/src/testapp/entrypoints/asgi.py +10 -0
- edu_async_tasks-1.0.0/src/testapp/entrypoints/celery.py +16 -0
- edu_async_tasks-1.0.0/src/testapp/entrypoints/wsgi.py +10 -0
- edu_async_tasks-1.0.0/src/testapp/manage.py +24 -0
- edu_async_tasks-1.0.0/src/testapp/persons/__init__.py +0 -0
- edu_async_tasks-1.0.0/src/testapp/persons/migrations/0001_initial.py +30 -0
- edu_async_tasks-1.0.0/src/testapp/persons/migrations/0002_auto_20240927_0651.py +21 -0
- edu_async_tasks-1.0.0/src/testapp/persons/migrations/__init__.py +0 -0
- edu_async_tasks-1.0.0/src/testapp/persons/models.py +14 -0
- edu_async_tasks-1.0.0/src/testapp/settings.py +188 -0
- edu_async_tasks-1.0.0/src/testapp/urls.py +23 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: edu-async-tasks
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Пакет "Реестра "Асинхронных задач"
|
|
5
|
+
Author-email: "АО \"БАРС Груп\"" <education_dev@bars-open.ru>
|
|
6
|
+
Classifier: Intended Audience :: Developers
|
|
7
|
+
Classifier: Environment :: Web Environment
|
|
8
|
+
Classifier: Natural Language :: Russian
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Framework :: Django :: 4.2
|
|
16
|
+
Classifier: Framework :: Django :: 5.0
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: setuptools; python_version >= "3.12"
|
|
20
|
+
Requires-Dist: Django<5.1,>=4.2
|
|
21
|
+
Requires-Dist: pydantic<2
|
|
22
|
+
Requires-Dist: djangorestframework==3.14.0
|
|
23
|
+
Requires-Dist: django_filter==23.3
|
|
24
|
+
Requires-Dist: celery[redis]>=5.3.5
|
|
25
|
+
Requires-Dist: m3-django-compat==1.11.0
|
|
26
|
+
Requires-Dist: m3-db-utils>=0.3.13
|
|
27
|
+
|
|
28
|
+
# Реестр асинхронных задач
|
|
29
|
+
|
|
30
|
+
## Сборка и распространение
|
|
31
|
+
$ python -m build && \
|
|
32
|
+
twine check ./dist/* && \
|
|
33
|
+
twine upload ./dist/* --repository-url=http://... -u user.name -p userpassword
|
|
34
|
+
|
|
35
|
+
## Запуск тестов
|
|
36
|
+
$ source deactivate
|
|
37
|
+
$ pip install tox tox-docker
|
|
38
|
+
$ tox
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Реестр асинхронных задач
|
|
2
|
+
|
|
3
|
+
## Сборка и распространение
|
|
4
|
+
$ python -m build && \
|
|
5
|
+
twine check ./dist/* && \
|
|
6
|
+
twine upload ./dist/* --repository-url=http://... -u user.name -p userpassword
|
|
7
|
+
|
|
8
|
+
## Запуск тестов
|
|
9
|
+
$ source deactivate
|
|
10
|
+
$ pip install tox tox-docker
|
|
11
|
+
$ tox
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=41", "wheel", "setuptools-git-versioning<2"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools-git-versioning]
|
|
6
|
+
enabled = true
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
dynamic = ["version"]
|
|
10
|
+
|
|
11
|
+
name = "edu-async-tasks"
|
|
12
|
+
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "АО \"БАРС Груп\"", email = "education_dev@bars-open.ru"}
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
readme = "README.md"
|
|
18
|
+
|
|
19
|
+
description = "Пакет \"Реестра \"Асинхронных задач\""
|
|
20
|
+
|
|
21
|
+
classifiers=[
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Environment :: Web Environment",
|
|
24
|
+
"Natural Language :: Russian",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Development Status :: 5 - Production/Stable",
|
|
27
|
+
"Programming Language :: Python :: 3.9",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Framework :: Django :: 4.2",
|
|
32
|
+
"Framework :: Django :: 5.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
requires-python = ">=3.9"
|
|
36
|
+
|
|
37
|
+
dependencies = [
|
|
38
|
+
"setuptools; python_version >= '3.12'",
|
|
39
|
+
"Django>=4.2,<5.1",
|
|
40
|
+
"pydantic<2",
|
|
41
|
+
"djangorestframework==3.14.0",
|
|
42
|
+
"django_filter==23.3",
|
|
43
|
+
"celery[redis]>=5.3.5",
|
|
44
|
+
"m3-django-compat==1.11.0",
|
|
45
|
+
"m3-db-utils>=0.3.13"
|
|
46
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Реестр "Асинхронные задачи"."""
|
|
2
|
+
|
|
3
|
+
from typing import (
|
|
4
|
+
Union,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from .config import (
|
|
8
|
+
Config,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__config: Union[Config, None] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def set_config(config: Config) -> None:
|
|
16
|
+
assert isinstance(config, Config)
|
|
17
|
+
|
|
18
|
+
global __config
|
|
19
|
+
|
|
20
|
+
__config = config
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_config() -> Config:
|
|
24
|
+
global __config
|
|
25
|
+
|
|
26
|
+
if __config is None:
|
|
27
|
+
raise ValueError('Конфигурация не установлена')
|
|
28
|
+
|
|
29
|
+
return __config
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def reset_config() -> None:
|
|
33
|
+
global __config
|
|
34
|
+
|
|
35
|
+
__config = None
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from pydantic.class_validators import (
|
|
2
|
+
validator,
|
|
3
|
+
)
|
|
4
|
+
from pydantic.dataclasses import (
|
|
5
|
+
dataclass,
|
|
6
|
+
)
|
|
7
|
+
from pydantic.fields import (
|
|
8
|
+
Field,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Config:
|
|
14
|
+
task_start_time_format: str = Field(
|
|
15
|
+
title='Формат времени',
|
|
16
|
+
default='%H:%M %d.%m.%Y'
|
|
17
|
+
)
|
|
18
|
+
task_default_user_name: str = Field(
|
|
19
|
+
title='Имя пользователя по-умолчанию',
|
|
20
|
+
default='Система'
|
|
21
|
+
)
|
|
22
|
+
default_lock_expire: int = Field( # noqa
|
|
23
|
+
title='Задача считается уникальной в течение',
|
|
24
|
+
default=30 * 60
|
|
25
|
+
)
|
|
26
|
+
filter_days_after_start: int = Field( # noqa
|
|
27
|
+
title='Фильтр реестра - количество дней, в течение которых были запущены задачи',
|
|
28
|
+
default=7
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
@validator('task_start_time_format')
|
|
32
|
+
@classmethod
|
|
33
|
+
def validate_task_start_time_format(cls, value):
|
|
34
|
+
if not isinstance(value, str) or not value:
|
|
35
|
+
raise ValueError(f'Неверное значение поля "{cls.task_start_time_format.title}"')
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
@validator('task_default_user_name')
|
|
39
|
+
@classmethod
|
|
40
|
+
def validate_task_default_user_name(cls, value):
|
|
41
|
+
if not isinstance(value, str) or not value:
|
|
42
|
+
raise ValueError(f'Неверное значение поля "{cls.task_default_user_name.title}"')
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
@validator('default_lock_expire')
|
|
46
|
+
@classmethod
|
|
47
|
+
def validate_default_lock_expire(cls, value):
|
|
48
|
+
if not isinstance(value, int) or value <= 0:
|
|
49
|
+
raise ValueError(f'Неверное значение поля "{cls.default_lock_expire.title}"')
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
@validator('filter_days_after_start')
|
|
53
|
+
@classmethod
|
|
54
|
+
def validate_filter_days_after_start(cls, value):
|
|
55
|
+
if not isinstance(value, int) or value <= 0:
|
|
56
|
+
raise ValueError(f'Неверное значение поля "{cls.filter_days_after_start.title}"')
|
|
57
|
+
return value
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
default_app_config = __name__ + '.apps.AppConfig'
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
TYPE_CHECKING,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
from django.core.exceptions import (
|
|
6
|
+
ValidationError,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from ..models import (
|
|
10
|
+
RunningTask,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from django.db.models.query import (
|
|
16
|
+
QuerySet,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RunningTasksRepository:
|
|
21
|
+
|
|
22
|
+
def get_by_id(self, id_: str) -> RunningTask:
|
|
23
|
+
return RunningTask.objects.get(pk=id_)
|
|
24
|
+
|
|
25
|
+
def get_by_ids(self, *ids: str) -> 'QuerySet[RunningTask]':
|
|
26
|
+
"""Возвращает кверисет асинхронных задач."""
|
|
27
|
+
try:
|
|
28
|
+
result = RunningTask.objects.filter(id__in=ids)
|
|
29
|
+
except ValidationError:
|
|
30
|
+
result = RunningTask.objects.none()
|
|
31
|
+
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
running_tasks_repository = RunningTasksRepository()
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from dataclasses import (
|
|
2
|
+
dataclass,
|
|
3
|
+
)
|
|
4
|
+
from logging import (
|
|
5
|
+
getLogger,
|
|
6
|
+
)
|
|
7
|
+
from typing import (
|
|
8
|
+
Optional,
|
|
9
|
+
Union,
|
|
10
|
+
)
|
|
11
|
+
from uuid import (
|
|
12
|
+
UUID,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from celery.exceptions import (
|
|
16
|
+
Ignore,
|
|
17
|
+
)
|
|
18
|
+
from celery.result import (
|
|
19
|
+
AsyncResult,
|
|
20
|
+
)
|
|
21
|
+
from django.core.cache import (
|
|
22
|
+
cache,
|
|
23
|
+
)
|
|
24
|
+
from django.utils.functional import (
|
|
25
|
+
cached_property,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
import edu_async_tasks
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class TaskResult:
|
|
33
|
+
"""Результат асинхронной задачи."""
|
|
34
|
+
|
|
35
|
+
values: dict
|
|
36
|
+
progress: str = ''
|
|
37
|
+
error: str = ''
|
|
38
|
+
state: str = ''
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def uuid_or_none(value) -> Union[UUID, None]:
|
|
42
|
+
"""Преобразует строку в UUID или возвращает None."""
|
|
43
|
+
if value is None or isinstance(value, UUID):
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
return UUID(value)
|
|
48
|
+
|
|
49
|
+
except (AttributeError, ValueError):
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EduAsyncTasksException(Exception):
|
|
54
|
+
"""Базовое исключение."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TasksNotCancellable(EduAsyncTasksException):
|
|
58
|
+
def __init__(self, msg='Необходимо выбрать только выполняющиеся задачи', *args, **kwargs):
|
|
59
|
+
super().__init__(msg, *args, **kwargs)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TaskUniqueException(EduAsyncTasksException):
|
|
63
|
+
def __init__(self, msg='Задача в очереди или выполняется', *args, **kwargs):
|
|
64
|
+
super().__init__(msg, *args, **kwargs)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TaskLocker:
|
|
68
|
+
"""Класс, отвечающий за блокировку задач."""
|
|
69
|
+
|
|
70
|
+
# формат ключа в кэше
|
|
71
|
+
LOCK_ID_FORMAT = 'async_task:lock:{task_name}_{params}'
|
|
72
|
+
|
|
73
|
+
DEFAULT_LOCK_MSG = 'Задача уже выполняется или находится в очереди'
|
|
74
|
+
DEFAULT_LOCK_VALUE = 'value'
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
task_name: str = '',
|
|
79
|
+
params: Optional[dict] = None,
|
|
80
|
+
task_id: Optional[str] = None,
|
|
81
|
+
expire_on: Optional[int] = None,
|
|
82
|
+
):
|
|
83
|
+
self.task_name = task_name
|
|
84
|
+
self.task_id = task_id
|
|
85
|
+
self.params = params if params is not None else {}
|
|
86
|
+
self.expire_on = expire_on if expire_on is not None else edu_async_tasks.get_config().default_lock_expire
|
|
87
|
+
|
|
88
|
+
@cached_property
|
|
89
|
+
def lock_id(self) -> str:
|
|
90
|
+
"""Ключ в кэше."""
|
|
91
|
+
return self.LOCK_ID_FORMAT.format(
|
|
92
|
+
task_name=self.task_name,
|
|
93
|
+
params='&'.join(f'{k}={v}' for k, v in self.params.items())
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def acquire_lock(self) -> str:
|
|
97
|
+
"""Установка блокировки."""
|
|
98
|
+
value = self.task_id or self.DEFAULT_LOCK_VALUE
|
|
99
|
+
cache.set(self.lock_id, value, self.expire_on)
|
|
100
|
+
getLogger(__name__).debug(
|
|
101
|
+
f'Lock acquired for Task {self.task_name} ({self.params}) with value: {value}'
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return self.lock_id
|
|
105
|
+
|
|
106
|
+
def delete_lock(self) -> None:
|
|
107
|
+
"""Удаление блокировки."""
|
|
108
|
+
self.delete_lock_by_id(self.lock_id)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def delete_lock_by_id(lock_id: str) -> bool:
|
|
112
|
+
"""Удаление блокировки по ключу.
|
|
113
|
+
|
|
114
|
+
:param lock_id: ключ
|
|
115
|
+
"""
|
|
116
|
+
return cache.delete(lock_id)
|
|
117
|
+
|
|
118
|
+
def is_locked(self) -> bool:
|
|
119
|
+
"""Установлена ли блокировка."""
|
|
120
|
+
is_locked = False
|
|
121
|
+
|
|
122
|
+
value = cache.get(self.lock_id)
|
|
123
|
+
|
|
124
|
+
if value:
|
|
125
|
+
is_locked = True
|
|
126
|
+
|
|
127
|
+
# Возможна ситуация, когда задача по которой была выставлена блокировка
|
|
128
|
+
# завершила свою работу, при этом блокировка не была снята. Поэтому проверяем
|
|
129
|
+
# статус задачи по которой выставлялась блокировка:
|
|
130
|
+
if value != self.DEFAULT_LOCK_VALUE and uuid_or_none(value):
|
|
131
|
+
# значит в value должен быть task_id предыдущей задачи и по нему пытаемся определить её статус
|
|
132
|
+
async_result = AsyncResult(value)
|
|
133
|
+
if async_result and async_result.ready():
|
|
134
|
+
# задача есть, но она уже завершилась - снимаем блокировку
|
|
135
|
+
self.delete_lock()
|
|
136
|
+
is_locked = False
|
|
137
|
+
|
|
138
|
+
return is_locked
|
|
139
|
+
|
|
140
|
+
def raise_if_locked(self, message: Optional[str] = None):
|
|
141
|
+
"""Если блокировано, то вызывает исключение.
|
|
142
|
+
|
|
143
|
+
:raises: edu_async_tasks.exceptions.TaskUniqueException
|
|
144
|
+
"""
|
|
145
|
+
if self.is_locked():
|
|
146
|
+
getLogger(__name__).debug(f'Add failed. Task {self.task_name} currently locked ({self.params})')
|
|
147
|
+
|
|
148
|
+
raise TaskUniqueException(message or self.DEFAULT_LOCK_MSG)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class PeriodicTaskLocker(TaskLocker):
|
|
152
|
+
"""Класс отвечающий за блокировку задач.
|
|
153
|
+
|
|
154
|
+
Переопределён для возможности игнорирования повторного запуска для уже
|
|
155
|
+
запущенных периодических задач.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def raise_if_locked(self, message: Optional[str] = None):
|
|
159
|
+
"""Если блокировано, то вызывает исключение."""
|
|
160
|
+
if self.is_locked():
|
|
161
|
+
getLogger(__name__).debug(f'Add failed. Task {self.task_name} currently locked ({self.params})')
|
|
162
|
+
|
|
163
|
+
raise Ignore()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import django.contrib.postgres.fields.jsonb
|
|
2
|
+
import django.db.models.deletion
|
|
3
|
+
from django.db import (
|
|
4
|
+
migrations,
|
|
5
|
+
models,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
initial = True
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
('contenttypes', '0002_remove_content_type_name'),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
operations = [
|
|
18
|
+
migrations.CreateModel(
|
|
19
|
+
name='AsyncTaskStatus',
|
|
20
|
+
fields=[
|
|
21
|
+
('title', models.TextField(verbose_name='расшифровка значения')),
|
|
22
|
+
('key', models.CharField(db_index=True, max_length=512, primary_key=True, serialize=False, verbose_name='ключ')),
|
|
23
|
+
('order_number', models.PositiveIntegerField(default=100000, verbose_name='Порядковый номер')),
|
|
24
|
+
],
|
|
25
|
+
options={
|
|
26
|
+
'abstract': False,
|
|
27
|
+
},
|
|
28
|
+
),
|
|
29
|
+
migrations.CreateModel(
|
|
30
|
+
name='AsyncTaskType',
|
|
31
|
+
fields=[
|
|
32
|
+
('title', models.TextField(verbose_name='расшифровка значения')),
|
|
33
|
+
('key', models.CharField(db_index=True, max_length=512, primary_key=True, serialize=False, verbose_name='ключ')),
|
|
34
|
+
('order_number', models.PositiveIntegerField(default=100000, verbose_name='Порядковый номер')),
|
|
35
|
+
],
|
|
36
|
+
options={
|
|
37
|
+
'verbose_name': 'Тип асинхронных задач',
|
|
38
|
+
'verbose_name_plural': 'Типы асинхронных задач',
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
migrations.CreateModel(
|
|
42
|
+
name='RunningTask',
|
|
43
|
+
fields=[
|
|
44
|
+
('id', models.UUIDField(primary_key=True, serialize=False, verbose_name='ID задачи')),
|
|
45
|
+
('name', models.CharField(blank=True, max_length=512, verbose_name='Наименование задачи')),
|
|
46
|
+
('profile_id', models.PositiveIntegerField(blank=True, null=True)),
|
|
47
|
+
('description', models.CharField(blank=True, max_length=512, verbose_name='Описание задачи')),
|
|
48
|
+
('options', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='Дополнительные опции задачи')),
|
|
49
|
+
('queued_at', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Дата и время помещения в очередь')),
|
|
50
|
+
('started_at', models.DateTimeField(blank=True, null=True, verbose_name='Дата и время запуска задачи')),
|
|
51
|
+
('finished_at', models.DateTimeField(blank=True, null=True, verbose_name='Дата и время завершения задачи')),
|
|
52
|
+
('profile_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contenttypes.ContentType')),
|
|
53
|
+
('status', models.ForeignKey(default='PENDING', on_delete=django.db.models.deletion.PROTECT, to='edu_async_tasks_core.AsyncTaskStatus', verbose_name='Состояние задачи')),
|
|
54
|
+
('task_type', models.ForeignKey(default='UNKNOWN', on_delete=django.db.models.deletion.PROTECT, to='edu_async_tasks_core.AsyncTaskType', verbose_name='Тип задачи')),
|
|
55
|
+
],
|
|
56
|
+
options={
|
|
57
|
+
'verbose_name': 'Асинхронная задача',
|
|
58
|
+
'verbose_name_plural': 'Асинхронные задачи',
|
|
59
|
+
},
|
|
60
|
+
),
|
|
61
|
+
migrations.AddIndex(
|
|
62
|
+
model_name='runningtask',
|
|
63
|
+
index=models.Index(fields=['profile_type', 'profile_id'], name='async_task__profile_c97023_idx'),
|
|
64
|
+
),
|
|
65
|
+
]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from django.db import (
|
|
2
|
+
migrations,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def init_task_types(apps, schema_editor):
|
|
7
|
+
"""Заполнение первоначальными данными AsyncTaskType."""
|
|
8
|
+
AsyncTaskType = apps.get_model('edu_async_tasks_core', 'AsyncTaskType') # noqa: N806
|
|
9
|
+
|
|
10
|
+
AsyncTaskType.objects.bulk_create([
|
|
11
|
+
AsyncTaskType(
|
|
12
|
+
key='UNKNOWN',
|
|
13
|
+
title='Неизвестно',
|
|
14
|
+
),
|
|
15
|
+
AsyncTaskType(
|
|
16
|
+
key='SYSTEM',
|
|
17
|
+
title='Системная',
|
|
18
|
+
),
|
|
19
|
+
AsyncTaskType(
|
|
20
|
+
key='REPORT',
|
|
21
|
+
title='Отчет',
|
|
22
|
+
),
|
|
23
|
+
AsyncTaskType(
|
|
24
|
+
key='IMPORT',
|
|
25
|
+
title='Импорт данных',
|
|
26
|
+
),
|
|
27
|
+
AsyncTaskType(
|
|
28
|
+
key='SCHEDULE_FORMING',
|
|
29
|
+
title='Формирование расписания',
|
|
30
|
+
),
|
|
31
|
+
AsyncTaskType(
|
|
32
|
+
key='SCHEDULE_CLEANER',
|
|
33
|
+
title='Очищение расписания',
|
|
34
|
+
),
|
|
35
|
+
AsyncTaskType(
|
|
36
|
+
key='SCHEDULE_PATTERN_COPY',
|
|
37
|
+
title='Копирование шаблона расписания',
|
|
38
|
+
),
|
|
39
|
+
AsyncTaskType(
|
|
40
|
+
key='ST_PRINTING',
|
|
41
|
+
title='Печать учебных планов',
|
|
42
|
+
),
|
|
43
|
+
AsyncTaskType(
|
|
44
|
+
key='ASYNC_REQUEST',
|
|
45
|
+
title='Отправка асинхронных запросов',
|
|
46
|
+
),
|
|
47
|
+
AsyncTaskType(
|
|
48
|
+
key='MASS_EXPULSION',
|
|
49
|
+
title='Массовый выпуск',
|
|
50
|
+
),
|
|
51
|
+
AsyncTaskType(
|
|
52
|
+
key='AIO_CLIENT',
|
|
53
|
+
title='AIO клиент',
|
|
54
|
+
),
|
|
55
|
+
], ignore_conflicts=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def init_task_statuses(apps, schema_editor):
|
|
59
|
+
"""Заполнение первоначальными данными AsyncTaskStatus."""
|
|
60
|
+
AsyncTaskStatus = apps.get_model('edu_async_tasks_core', 'AsyncTaskStatus') # noqa: N806
|
|
61
|
+
|
|
62
|
+
AsyncTaskStatus.objects.bulk_create([
|
|
63
|
+
AsyncTaskStatus(
|
|
64
|
+
key='PENDING',
|
|
65
|
+
title='В ожидании',
|
|
66
|
+
),
|
|
67
|
+
AsyncTaskStatus(
|
|
68
|
+
key='RECEIVED',
|
|
69
|
+
title='В очереди',
|
|
70
|
+
),
|
|
71
|
+
AsyncTaskStatus(
|
|
72
|
+
key='STARTED',
|
|
73
|
+
title='Выполняется',
|
|
74
|
+
),
|
|
75
|
+
AsyncTaskStatus(
|
|
76
|
+
key='SUCCESS',
|
|
77
|
+
title='Успешно выполнена',
|
|
78
|
+
),
|
|
79
|
+
AsyncTaskStatus(
|
|
80
|
+
key='REVOKED',
|
|
81
|
+
title='Остановлена',
|
|
82
|
+
),
|
|
83
|
+
AsyncTaskStatus(
|
|
84
|
+
key='FAILURE',
|
|
85
|
+
title='Ошибка',
|
|
86
|
+
),
|
|
87
|
+
AsyncTaskStatus(
|
|
88
|
+
key='RETRY',
|
|
89
|
+
title='Перезапуск',
|
|
90
|
+
),
|
|
91
|
+
AsyncTaskStatus(
|
|
92
|
+
key='IGNORED',
|
|
93
|
+
title='Игнорирована',
|
|
94
|
+
),
|
|
95
|
+
AsyncTaskStatus(
|
|
96
|
+
key='REJECTED',
|
|
97
|
+
title='Отменена',
|
|
98
|
+
),
|
|
99
|
+
AsyncTaskStatus(
|
|
100
|
+
key='UNKNOWN',
|
|
101
|
+
title='Неизвестно',
|
|
102
|
+
),
|
|
103
|
+
], ignore_conflicts=True)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Migration(migrations.Migration):
|
|
107
|
+
|
|
108
|
+
dependencies = [
|
|
109
|
+
('edu_async_tasks_core', '0001_initial'),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
operations = [
|
|
113
|
+
migrations.RunPython(init_task_types, reverse_code=migrations.RunPython.noop),
|
|
114
|
+
migrations.RunPython(init_task_statuses, reverse_code=migrations.RunPython.noop),
|
|
115
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Generated by Django 5.0.9 on 2024-09-25 10:08
|
|
2
|
+
|
|
3
|
+
from django.db import (
|
|
4
|
+
migrations,
|
|
5
|
+
models,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
('edu_async_tasks_core', '0002_task_type_and_status_data'),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.RenameIndex(
|
|
17
|
+
model_name='runningtask',
|
|
18
|
+
new_name='edu_async_t_profile_70c100_idx',
|
|
19
|
+
old_name='async_task__profile_c97023_idx',
|
|
20
|
+
),
|
|
21
|
+
migrations.AlterField(
|
|
22
|
+
model_name='runningtask',
|
|
23
|
+
name='options',
|
|
24
|
+
field=models.JSONField(blank=True, null=True, verbose_name='Дополнительные опции задачи'),
|
|
25
|
+
),
|
|
26
|
+
]
|
|
File without changes
|