explicit-python-dlq 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.
- explicit_python_dlq-1.0.0/MANIFEST.in +18 -0
- explicit_python_dlq-1.0.0/PKG-INFO +105 -0
- explicit_python_dlq-1.0.0/README.md +80 -0
- explicit_python_dlq-1.0.0/pyproject.toml +132 -0
- explicit_python_dlq-1.0.0/setup.cfg +4 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/__init__.py +1 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/adapters/__init__.py +0 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/adapters/db.py +13 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/config.py +63 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/contrib/__init__.py +0 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/contrib/django/README.md +40 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/contrib/django/__init__.py +0 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/contrib/django/adapters/__init__.py +0 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/contrib/django/adapters/db.py +104 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/contrib/django/apps.py +12 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/contrib/django/migrations/0001_initial.py +60 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/contrib/django/migrations/__init__.py +0 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/contrib/django/models.py +45 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/contrib/drf/README.md +32 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/contrib/drf/__init__.py +0 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/contrib/drf/serializers.py +45 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/contrib/drf/views.py +72 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/domain/__init__.py +0 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/domain/commands.py +37 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/domain/factories.py +81 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/domain/model.py +71 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/domain/services.py +64 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/infrastructure/__init__.py +0 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/infrastructure/handlers.py +72 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/py.typed +0 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/services/__init__.py +0 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/services/handlers.py +95 -0
- explicit_python_dlq-1.0.0/src/explicit/dlq/types.py +15 -0
- explicit_python_dlq-1.0.0/src/explicit_python_dlq.egg-info/PKG-INFO +105 -0
- explicit_python_dlq-1.0.0/src/explicit_python_dlq.egg-info/SOURCES.txt +36 -0
- explicit_python_dlq-1.0.0/src/explicit_python_dlq.egg-info/dependency_links.txt +1 -0
- explicit_python_dlq-1.0.0/src/explicit_python_dlq.egg-info/requires.txt +11 -0
- explicit_python_dlq-1.0.0/src/explicit_python_dlq.egg-info/top_level.txt +2 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
graft src
|
|
2
|
+
|
|
3
|
+
global-exclude *.pyc
|
|
4
|
+
global-exclude __pycache__
|
|
5
|
+
|
|
6
|
+
prune src/testapp
|
|
7
|
+
|
|
8
|
+
exclude pylintrc
|
|
9
|
+
exclude mypy.ini
|
|
10
|
+
exclude db.sqlite3
|
|
11
|
+
exclude isort.cfg
|
|
12
|
+
exclude .coveragerc
|
|
13
|
+
exclude .coverage
|
|
14
|
+
|
|
15
|
+
include README.md
|
|
16
|
+
include version.conf
|
|
17
|
+
include requirements/base.txt
|
|
18
|
+
include requirements/prod.txt
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: explicit-python-dlq
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Набор компонентов реализующих DQL для систем на базе explicit
|
|
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
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: explicit-python<3,>=2.2.1
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: freezegun; extra == "dev"
|
|
20
|
+
Provides-Extra: django
|
|
21
|
+
Requires-Dist: explicit-python-django<2.0,>=1.0.2; extra == "django"
|
|
22
|
+
Provides-Extra: rest
|
|
23
|
+
Requires-Dist: djangorestframework>=3.13.0; extra == "rest"
|
|
24
|
+
Requires-Dist: django_filter>=23.3; extra == "rest"
|
|
25
|
+
|
|
26
|
+
# Набор компонентов реализующих очередь необработанных сообщений (Dead Letter Queue).
|
|
27
|
+
|
|
28
|
+
## Пример подключения
|
|
29
|
+
testapp/dlq/apps.py:
|
|
30
|
+
```python
|
|
31
|
+
from django.apps import AppConfig as AppConfigBase
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AppConfig(AppConfigBase):
|
|
35
|
+
name = __package__
|
|
36
|
+
|
|
37
|
+
def ready(self):
|
|
38
|
+
self._configure_dlq()
|
|
39
|
+
|
|
40
|
+
def _configure_dlq(self):
|
|
41
|
+
from testapp.core import bus
|
|
42
|
+
from explicit.dlq.config import DLQConfig, configure_dlq
|
|
43
|
+
# реализация репозитория специфичная для используемого слоя хранения данных
|
|
44
|
+
from testapp.dlq.adapters.db import Repository
|
|
45
|
+
|
|
46
|
+
# реализация обработчика сообщений, специфичного для приложения
|
|
47
|
+
from testapp.dlq.services.dispatch import dispatch_message
|
|
48
|
+
|
|
49
|
+
config = DLQConfig(
|
|
50
|
+
bus=bus,
|
|
51
|
+
repository=Repository(),
|
|
52
|
+
message_dispatcher=dispatch_message
|
|
53
|
+
)
|
|
54
|
+
# регистрация репозитория и регистрация обработчиков команд
|
|
55
|
+
configure_dlq(config)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
testapp/dlq/services/dispatch.py:
|
|
59
|
+
```python
|
|
60
|
+
import json
|
|
61
|
+
from explicit.contrib.messagebus.event_registry import Message
|
|
62
|
+
|
|
63
|
+
from testapp.core import bus
|
|
64
|
+
from testapp.core import event_registry
|
|
65
|
+
|
|
66
|
+
def dispatch_message(raw_message_value: bytes, topic: str):
|
|
67
|
+
"""Преобразует сообщение в событие и отправляет его в шину."""
|
|
68
|
+
message = json.loads(raw_message_value)
|
|
69
|
+
|
|
70
|
+
if event := event_registry.resolve(
|
|
71
|
+
Message(
|
|
72
|
+
topic=topic,
|
|
73
|
+
type=message.get('type'),
|
|
74
|
+
body=message
|
|
75
|
+
)
|
|
76
|
+
):
|
|
77
|
+
bus.handle(event)
|
|
78
|
+
|
|
79
|
+
````
|
|
80
|
+
|
|
81
|
+
testapp/entrypoints/kafka.py:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
def bootstrap() -> None:
|
|
85
|
+
from explicit.dlq.infrastructure.handlers import RegisterInDLQOnFailure
|
|
86
|
+
|
|
87
|
+
from testapp.core import bus
|
|
88
|
+
|
|
89
|
+
from testapp.dlq.services.dispatch import dispatch_message
|
|
90
|
+
topics = ('test.foo.topic',)
|
|
91
|
+
|
|
92
|
+
for raw_message in adapter.subscribe(*topics):
|
|
93
|
+
with RegisterInDLQOnFailure(
|
|
94
|
+
bus=bus,
|
|
95
|
+
topic=raw_message.topic(),
|
|
96
|
+
raw_message_value=raw_message.value(),
|
|
97
|
+
raw_message_key=raw_message.key
|
|
98
|
+
):
|
|
99
|
+
dispatch_message(raw_message.value(), raw_message.topic())
|
|
100
|
+
```
|
|
101
|
+
## Готовые компоненты (contrib)
|
|
102
|
+
|
|
103
|
+
В пакете реализованы готовые к использованию компоненты:
|
|
104
|
+
* Реализация абстрактного [хранилища сообщений](./src/explicit/dlq/contrib/django/README.md) на базе Django ORM
|
|
105
|
+
* [REST API](./src/explicit/dlq/contrib/drf/README.md) для работы с хранилищем сообщений. Предназначен для совместной работы с реализацией хранилища сообщений django ORM.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Набор компонентов реализующих очередь необработанных сообщений (Dead Letter Queue).
|
|
2
|
+
|
|
3
|
+
## Пример подключения
|
|
4
|
+
testapp/dlq/apps.py:
|
|
5
|
+
```python
|
|
6
|
+
from django.apps import AppConfig as AppConfigBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AppConfig(AppConfigBase):
|
|
10
|
+
name = __package__
|
|
11
|
+
|
|
12
|
+
def ready(self):
|
|
13
|
+
self._configure_dlq()
|
|
14
|
+
|
|
15
|
+
def _configure_dlq(self):
|
|
16
|
+
from testapp.core import bus
|
|
17
|
+
from explicit.dlq.config import DLQConfig, configure_dlq
|
|
18
|
+
# реализация репозитория специфичная для используемого слоя хранения данных
|
|
19
|
+
from testapp.dlq.adapters.db import Repository
|
|
20
|
+
|
|
21
|
+
# реализация обработчика сообщений, специфичного для приложения
|
|
22
|
+
from testapp.dlq.services.dispatch import dispatch_message
|
|
23
|
+
|
|
24
|
+
config = DLQConfig(
|
|
25
|
+
bus=bus,
|
|
26
|
+
repository=Repository(),
|
|
27
|
+
message_dispatcher=dispatch_message
|
|
28
|
+
)
|
|
29
|
+
# регистрация репозитория и регистрация обработчиков команд
|
|
30
|
+
configure_dlq(config)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
testapp/dlq/services/dispatch.py:
|
|
34
|
+
```python
|
|
35
|
+
import json
|
|
36
|
+
from explicit.contrib.messagebus.event_registry import Message
|
|
37
|
+
|
|
38
|
+
from testapp.core import bus
|
|
39
|
+
from testapp.core import event_registry
|
|
40
|
+
|
|
41
|
+
def dispatch_message(raw_message_value: bytes, topic: str):
|
|
42
|
+
"""Преобразует сообщение в событие и отправляет его в шину."""
|
|
43
|
+
message = json.loads(raw_message_value)
|
|
44
|
+
|
|
45
|
+
if event := event_registry.resolve(
|
|
46
|
+
Message(
|
|
47
|
+
topic=topic,
|
|
48
|
+
type=message.get('type'),
|
|
49
|
+
body=message
|
|
50
|
+
)
|
|
51
|
+
):
|
|
52
|
+
bus.handle(event)
|
|
53
|
+
|
|
54
|
+
````
|
|
55
|
+
|
|
56
|
+
testapp/entrypoints/kafka.py:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
def bootstrap() -> None:
|
|
60
|
+
from explicit.dlq.infrastructure.handlers import RegisterInDLQOnFailure
|
|
61
|
+
|
|
62
|
+
from testapp.core import bus
|
|
63
|
+
|
|
64
|
+
from testapp.dlq.services.dispatch import dispatch_message
|
|
65
|
+
topics = ('test.foo.topic',)
|
|
66
|
+
|
|
67
|
+
for raw_message in adapter.subscribe(*topics):
|
|
68
|
+
with RegisterInDLQOnFailure(
|
|
69
|
+
bus=bus,
|
|
70
|
+
topic=raw_message.topic(),
|
|
71
|
+
raw_message_value=raw_message.value(),
|
|
72
|
+
raw_message_key=raw_message.key
|
|
73
|
+
):
|
|
74
|
+
dispatch_message(raw_message.value(), raw_message.topic())
|
|
75
|
+
```
|
|
76
|
+
## Готовые компоненты (contrib)
|
|
77
|
+
|
|
78
|
+
В пакете реализованы готовые к использованию компоненты:
|
|
79
|
+
* Реализация абстрактного [хранилища сообщений](./src/explicit/dlq/contrib/django/README.md) на базе Django ORM
|
|
80
|
+
* [REST API](./src/explicit/dlq/contrib/drf/README.md) для работы с хранилищем сообщений. Предназначен для совместной работы с реализацией хранилища сообщений django ORM.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"packaging>=24,<25",
|
|
4
|
+
"wheel>=0.45,<0.46",
|
|
5
|
+
"setuptools>=77,<78",
|
|
6
|
+
"setuptools-git-versioning>=2,<3",
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
build-backend = "setuptools.build_meta"
|
|
10
|
+
|
|
11
|
+
[project]
|
|
12
|
+
dynamic = ["version"]
|
|
13
|
+
|
|
14
|
+
name = "explicit-python-dlq"
|
|
15
|
+
|
|
16
|
+
authors = [
|
|
17
|
+
{name = "АО \"БАРС Груп\"", email = "education_dev@bars-open.ru"}
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
readme = "README.md"
|
|
21
|
+
|
|
22
|
+
description = "Набор компонентов реализующих DQL для систем на базе explicit"
|
|
23
|
+
|
|
24
|
+
classifiers=[
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Environment :: Web Environment",
|
|
27
|
+
"Natural Language :: Russian",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
"Development Status :: 5 - Production/Stable",
|
|
30
|
+
"Programming Language :: Python :: 3.9",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
requires-python = ">=3.9"
|
|
37
|
+
|
|
38
|
+
dependencies = [
|
|
39
|
+
"explicit-python>=2.2.1,<3",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
dev = [
|
|
44
|
+
"freezegun"
|
|
45
|
+
]
|
|
46
|
+
django = [
|
|
47
|
+
"explicit-python-django>=1.0.2,<2.0"
|
|
48
|
+
]
|
|
49
|
+
rest = [
|
|
50
|
+
"djangorestframework>=3.13.0",
|
|
51
|
+
"django_filter>=23.3",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
[tool.setuptools-git-versioning]
|
|
56
|
+
enabled = true
|
|
57
|
+
|
|
58
|
+
[tool.isort]
|
|
59
|
+
multi_line_output=3
|
|
60
|
+
force_grid_wrap=1
|
|
61
|
+
include_trailing_comma = true
|
|
62
|
+
line_length = 120
|
|
63
|
+
combine_as_imports = true
|
|
64
|
+
lines_after_imports = 2
|
|
65
|
+
extra_standard_library = ["dataclasses"]
|
|
66
|
+
known_project = ["explicit", "testapp"]
|
|
67
|
+
sections = ["FUTURE","STDLIB","THIRDPARTY","FIRSTPARTY","PROJECT","LOCALFOLDER"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
[tool.ruff]
|
|
71
|
+
target-version = "py312"
|
|
72
|
+
line-length = 120
|
|
73
|
+
indent-width = 4
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint]
|
|
77
|
+
select = ["F", "E", "D", "W", "S", "N",]
|
|
78
|
+
ignore = [
|
|
79
|
+
# Missing docstring in public module
|
|
80
|
+
"D100",
|
|
81
|
+
# Missing docstring in public package
|
|
82
|
+
"D104",
|
|
83
|
+
# Missing docstring in __init__
|
|
84
|
+
"D107",
|
|
85
|
+
# use of assert detected (useless with pytest)
|
|
86
|
+
"S101",
|
|
87
|
+
]
|
|
88
|
+
exclude = [
|
|
89
|
+
".bzr",
|
|
90
|
+
".direnv",
|
|
91
|
+
".eggs",
|
|
92
|
+
".git",
|
|
93
|
+
".git-rewrite",
|
|
94
|
+
".hg",
|
|
95
|
+
".idea",
|
|
96
|
+
".ipynb_checkpoints",
|
|
97
|
+
".mypy_cache",
|
|
98
|
+
".nox",
|
|
99
|
+
".pants.d",
|
|
100
|
+
".pyenv",
|
|
101
|
+
".pytest_cache",
|
|
102
|
+
".pytype",
|
|
103
|
+
".ruff_cache",
|
|
104
|
+
".svn",
|
|
105
|
+
".tox",
|
|
106
|
+
".venv",
|
|
107
|
+
".vscode",
|
|
108
|
+
"__pypackages__",
|
|
109
|
+
"_build",
|
|
110
|
+
"buck-out",
|
|
111
|
+
"build",
|
|
112
|
+
"dist",
|
|
113
|
+
"node_modules",
|
|
114
|
+
"site-packages",
|
|
115
|
+
"venv",
|
|
116
|
+
"py-libs/**",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
[tool.ruff.format]
|
|
120
|
+
quote-style = "single"
|
|
121
|
+
indent-style = "space"
|
|
122
|
+
docstring-code-format = true
|
|
123
|
+
|
|
124
|
+
[tool.ruff.lint.per-file-ignores]
|
|
125
|
+
"*/tests/*" = ["D"]
|
|
126
|
+
"*/migrations/*" = ["D"]
|
|
127
|
+
|
|
128
|
+
[tool.mypy]
|
|
129
|
+
plugins = ["pydantic.mypy"]
|
|
130
|
+
no_site_packages = true
|
|
131
|
+
ignore_missing_imports = true
|
|
132
|
+
exclude = "migrations/"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Адаптеры для работы с Dead Letter Queue (DLQ) в базе данных."""
|
|
2
|
+
|
|
3
|
+
from abc import (
|
|
4
|
+
ABCMeta,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from explicit.adapters.db import (
|
|
8
|
+
AbstractRepository,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AbstractDLQRepository(AbstractRepository, metaclass=ABCMeta):
|
|
13
|
+
"""Абстрактный репозиторий для работы с DLQ."""
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Конфигурация DLQ."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import (
|
|
4
|
+
dataclass,
|
|
5
|
+
)
|
|
6
|
+
from typing import (
|
|
7
|
+
TYPE_CHECKING,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from pydantic import (
|
|
11
|
+
ConfigDict,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from explicit.dlq.domain import (
|
|
15
|
+
commands,
|
|
16
|
+
)
|
|
17
|
+
from explicit.dlq.services.handlers import (
|
|
18
|
+
DLQHandlers,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from explicit.dlq.adapters.db import (
|
|
24
|
+
AbstractDLQRepository,
|
|
25
|
+
)
|
|
26
|
+
from explicit.dlq.types import (
|
|
27
|
+
DLQMessageDispatcher,
|
|
28
|
+
)
|
|
29
|
+
from explicit.messagebus import (
|
|
30
|
+
MessageBus,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class DLQConfig:
|
|
36
|
+
"""Конфигурация DLQ."""
|
|
37
|
+
|
|
38
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
39
|
+
|
|
40
|
+
bus: 'MessageBus'
|
|
41
|
+
repository: 'AbstractDLQRepository'
|
|
42
|
+
message_dispatcher: 'DLQMessageDispatcher'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def configure_dlq(config: DLQConfig):
|
|
46
|
+
"""Настроить DLQ."""
|
|
47
|
+
config.bus.get_uow().register_repositories(('dead_letters', config.repository))
|
|
48
|
+
|
|
49
|
+
handlers = DLQHandlers(bus=config.bus, message_dispatcher=config.message_dispatcher)
|
|
50
|
+
|
|
51
|
+
config.bus.add_command_handler(
|
|
52
|
+
commands.RegisterDeadLetter,
|
|
53
|
+
handlers.register_dead_letter, # type: ignore[attr-defined,arg-type]
|
|
54
|
+
)
|
|
55
|
+
config.bus.add_command_handler(
|
|
56
|
+
commands.ProcessDeadLetter,
|
|
57
|
+
handlers.process_dead_letter, # type: ignore[attr-defined,arg-type]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
config.bus.add_command_handler(
|
|
61
|
+
commands.UpdateDeadLetterRawMessage,
|
|
62
|
+
handlers.update_raw_message_value, # type: ignore[attr-defined,arg-type]
|
|
63
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
## Набор компонентов для интеграции explicit.dlq с Django.
|
|
2
|
+
Содержит реализацию репозитория необработанных сообщений.
|
|
3
|
+
|
|
4
|
+
## Пример подключения
|
|
5
|
+
testapp/settings.py:
|
|
6
|
+
```python
|
|
7
|
+
INSTALLED_APPS = [
|
|
8
|
+
# другие приложения
|
|
9
|
+
'explicit.dlq.contrib.django', # подключение приложения с моделью DeadLetter
|
|
10
|
+
'testapp.core', # настройка компонентов из explicit.dlq
|
|
11
|
+
]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
testapp/dlq/apps.py:
|
|
15
|
+
```python
|
|
16
|
+
|
|
17
|
+
from datetime import timedelta
|
|
18
|
+
|
|
19
|
+
from django.apps import AppConfig as AppConfigBase
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AppConfig(AppConfigBase):
|
|
23
|
+
name = __package__
|
|
24
|
+
|
|
25
|
+
def ready(self):
|
|
26
|
+
self._configure_dlq()
|
|
27
|
+
|
|
28
|
+
def _configure_dlq(self):
|
|
29
|
+
from explicit.dlq.config import DLQConfig, configure_dlq
|
|
30
|
+
|
|
31
|
+
# реализация репозитория на базе Django ORM
|
|
32
|
+
from explicit.dlq.contrib.django.adapters.db import Repository
|
|
33
|
+
config = DLQConfig(
|
|
34
|
+
# ...
|
|
35
|
+
repository=Repository(),
|
|
36
|
+
# ...
|
|
37
|
+
)
|
|
38
|
+
# регистрация репозитория и регистрация обработчиков команд
|
|
39
|
+
configure_dlq(config)
|
|
40
|
+
```
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Адаптеры для работы с Dead Letter Queue (DLQ) в базе данных."""
|
|
2
|
+
|
|
3
|
+
from typing import (
|
|
4
|
+
TYPE_CHECKING,
|
|
5
|
+
Iterator,
|
|
6
|
+
Union,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from django.core.exceptions import (
|
|
10
|
+
ObjectDoesNotExist,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from explicit.dlq.adapters.db import (
|
|
14
|
+
AbstractDLQRepository,
|
|
15
|
+
)
|
|
16
|
+
from explicit.dlq.domain.model import (
|
|
17
|
+
DeadLetter,
|
|
18
|
+
DeadLetterNotFound,
|
|
19
|
+
)
|
|
20
|
+
from explicit.domain import (
|
|
21
|
+
asdict,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from uuid import (
|
|
27
|
+
UUID,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from explicit.dlq.contrib.django import (
|
|
31
|
+
models as db,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Repository(AbstractDLQRepository):
|
|
36
|
+
"""Репозиторий DeadLetterQueue."""
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def _base_qs(self):
|
|
40
|
+
# pylint: disable-next=import-outside-toplevel
|
|
41
|
+
from explicit.dlq.contrib.django.models import (
|
|
42
|
+
DeadLetter as DBDeadLetter,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return DBDeadLetter.objects.order_by('_first_attempted_at')
|
|
46
|
+
|
|
47
|
+
def _to_domain(self, dbinstance: 'db.DeadLetter') -> DeadLetter:
|
|
48
|
+
return DeadLetter(
|
|
49
|
+
id=dbinstance.id,
|
|
50
|
+
raw_message_value=bytes(dbinstance.raw_message_value),
|
|
51
|
+
raw_message_key=bytes(dbinstance.raw_message_key) if dbinstance.raw_message_key else None,
|
|
52
|
+
topic=dbinstance.topic,
|
|
53
|
+
attempts=tuple(dbinstance.attempts),
|
|
54
|
+
processed_at=dbinstance.processed_at,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _to_db(self, modelinstance: DeadLetter) -> DeadLetter:
|
|
58
|
+
# pylint: disable-next=import-outside-toplevel
|
|
59
|
+
from explicit.dlq.contrib.django.models import (
|
|
60
|
+
DeadLetter as DBDeadLetter,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
assert isinstance(modelinstance, DeadLetter)
|
|
64
|
+
db_instance, _ = DBDeadLetter.objects.update_or_create(
|
|
65
|
+
pk=modelinstance.id,
|
|
66
|
+
defaults=asdict(modelinstance, exclude={'id', 'attempts'})
|
|
67
|
+
| {
|
|
68
|
+
'attempts': [
|
|
69
|
+
asdict(attempt) | {'failed_at': attempt.failed_at.isoformat()} for attempt in modelinstance.attempts
|
|
70
|
+
],
|
|
71
|
+
'_first_attempted_at': modelinstance.attempts[0].failed_at,
|
|
72
|
+
'_attempts_count': len(modelinstance.attempts),
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
return self.get_object_by_id(db_instance.pk)
|
|
76
|
+
|
|
77
|
+
def add(self, obj: DeadLetter) -> DeadLetter:
|
|
78
|
+
"""Добавить сообщение."""
|
|
79
|
+
return self._to_db(obj)
|
|
80
|
+
|
|
81
|
+
def update(self, obj: DeadLetter) -> DeadLetter:
|
|
82
|
+
"""Обновить сообщение."""
|
|
83
|
+
assert isinstance(obj, DeadLetter)
|
|
84
|
+
|
|
85
|
+
return self._to_db(obj)
|
|
86
|
+
|
|
87
|
+
def delete(self, obj: DeadLetter) -> None:
|
|
88
|
+
"""Удалить сообщение."""
|
|
89
|
+
assert isinstance(obj, DeadLetter)
|
|
90
|
+
|
|
91
|
+
self._base_qs.filter(pk=obj.id).delete()
|
|
92
|
+
|
|
93
|
+
def get_all_objects(self) -> Iterator[DeadLetter]:
|
|
94
|
+
"""Получить все сообщения."""
|
|
95
|
+
for db_instance in self._base_qs.iterator():
|
|
96
|
+
yield self._to_domain(db_instance)
|
|
97
|
+
|
|
98
|
+
def get_object_by_id(self, identifier: 'Union[UUID, str]') -> DeadLetter:
|
|
99
|
+
"""Получить сообщение по идентификатору."""
|
|
100
|
+
try:
|
|
101
|
+
db_instance = self._base_qs.get(pk=identifier)
|
|
102
|
+
return self._to_domain(db_instance)
|
|
103
|
+
except ObjectDoesNotExist as exc:
|
|
104
|
+
raise DeadLetterNotFound from exc
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# pylint: disable=missing-module-docstring,invalid-name
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from django import (
|
|
5
|
+
VERSION as DJANGO_VERSION,
|
|
6
|
+
)
|
|
7
|
+
from django.db import (
|
|
8
|
+
migrations,
|
|
9
|
+
models,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if DJANGO_VERSION >= (4, 0):
|
|
14
|
+
from django.db.models import (
|
|
15
|
+
JSONField,
|
|
16
|
+
)
|
|
17
|
+
else:
|
|
18
|
+
from django.contrib.postgres.fields import (
|
|
19
|
+
JSONField,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Migration(migrations.Migration):
|
|
24
|
+
initial = True
|
|
25
|
+
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
operations = [
|
|
29
|
+
migrations.CreateModel(
|
|
30
|
+
name='DeadLetter',
|
|
31
|
+
fields=[
|
|
32
|
+
(
|
|
33
|
+
'id',
|
|
34
|
+
models.UUIDField(
|
|
35
|
+
default=uuid.uuid4,
|
|
36
|
+
editable=False,
|
|
37
|
+
primary_key=True,
|
|
38
|
+
serialize=False,
|
|
39
|
+
verbose_name='Идентификатор UUID',
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
('raw_message_value', models.BinaryField(verbose_name='Содержимое сообщения')),
|
|
43
|
+
('raw_message_key', models.BinaryField(verbose_name='Ключ сообщения', null=True, blank=True)),
|
|
44
|
+
('topic', models.CharField(max_length=256, verbose_name='Топик сообщения')),
|
|
45
|
+
('attempts', JSONField(verbose_name='Попытки обработки сообщения')),
|
|
46
|
+
(
|
|
47
|
+
'processed_at',
|
|
48
|
+
models.DateTimeField(blank=True, null=True, verbose_name='Время успешной обработки сообщения'),
|
|
49
|
+
),
|
|
50
|
+
('_first_attempted_at', models.DateTimeField(db_index=True)),
|
|
51
|
+
('_attempts_count', models.IntegerField(db_index=True)),
|
|
52
|
+
],
|
|
53
|
+
options={
|
|
54
|
+
'verbose_name': 'Необработанное сообщение',
|
|
55
|
+
'verbose_name_plural': 'Необработанные сообщения',
|
|
56
|
+
'indexes': [models.Index(fields=['_attempts_count', '_first_attempted_at'], name='dlq_attempts_idx')],
|
|
57
|
+
'db_table': 'dlq_dead_letter',
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
]
|
|
File without changes
|