explicit-python-dlq 1.0.0__tar.gz → 2.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.
Files changed (38) hide show
  1. {explicit_python_dlq-1.0.0/src/explicit_python_dlq.egg-info → explicit_python_dlq-2.0.0}/PKG-INFO +43 -5
  2. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/README.md +37 -0
  3. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/pyproject.toml +15 -39
  4. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/config.py +0 -6
  5. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/README.md +4 -4
  6. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/adapters/db.py +4 -3
  7. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/models.py +16 -6
  8. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/drf/README.md +0 -1
  9. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/domain/commands.py +7 -7
  10. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/domain/factories.py +7 -13
  11. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/domain/model.py +18 -11
  12. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/domain/services.py +1 -5
  13. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/infrastructure/handlers.py +1 -4
  14. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0/src/explicit_python_dlq.egg-info}/PKG-INFO +43 -5
  15. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit_python_dlq.egg-info/requires.txt +2 -2
  16. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/MANIFEST.in +0 -0
  17. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/setup.cfg +0 -0
  18. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/__init__.py +0 -0
  19. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/adapters/__init__.py +0 -0
  20. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/adapters/db.py +0 -0
  21. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/__init__.py +0 -0
  22. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/__init__.py +0 -0
  23. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/adapters/__init__.py +0 -0
  24. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/apps.py +0 -0
  25. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/migrations/0001_initial.py +0 -0
  26. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/migrations/__init__.py +0 -0
  27. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/drf/__init__.py +0 -0
  28. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/drf/serializers.py +0 -0
  29. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/drf/views.py +0 -0
  30. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/domain/__init__.py +0 -0
  31. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/infrastructure/__init__.py +0 -0
  32. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/py.typed +0 -0
  33. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/services/__init__.py +0 -0
  34. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/services/handlers.py +0 -0
  35. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/types.py +0 -0
  36. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit_python_dlq.egg-info/SOURCES.txt +0 -0
  37. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit_python_dlq.egg-info/dependency_links.txt +0 -0
  38. {explicit_python_dlq-1.0.0 → explicit_python_dlq-2.0.0}/src/explicit_python_dlq.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: explicit-python-dlq
3
- Version: 1.0.0
3
+ Version: 2.0.0
4
4
  Summary: Набор компонентов реализующих DQL для систем на базе explicit
5
5
  Author-email: "АО \"БАРС Груп\"" <education_dev@bars-open.ru>
6
6
  Classifier: Intended Audience :: Developers
@@ -8,17 +8,18 @@ Classifier: Environment :: Web Environment
8
8
  Classifier: Natural Language :: Russian
9
9
  Classifier: Operating System :: OS Independent
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
- Classifier: Programming Language :: Python :: 3.9
12
11
  Classifier: Programming Language :: Python :: 3.10
13
12
  Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
15
- Requires-Python: >=3.9
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.10
16
17
  Description-Content-Type: text/markdown
17
- Requires-Dist: explicit-python<3,>=2.2.1
18
+ Requires-Dist: explicit-python<4,>=3
18
19
  Provides-Extra: dev
19
20
  Requires-Dist: freezegun; extra == "dev"
20
21
  Provides-Extra: django
21
- Requires-Dist: explicit-python-django<2.0,>=1.0.2; extra == "django"
22
+ Requires-Dist: explicit-python-django<3,>=2; extra == "django"
22
23
  Provides-Extra: rest
23
24
  Requires-Dist: djangorestframework>=3.13.0; extra == "rest"
24
25
  Requires-Dist: django_filter>=23.3; extra == "rest"
@@ -103,3 +104,40 @@ def bootstrap() -> None:
103
104
  В пакете реализованы готовые к использованию компоненты:
104
105
  * Реализация абстрактного [хранилища сообщений](./src/explicit/dlq/contrib/django/README.md) на базе Django ORM
105
106
  * [REST API](./src/explicit/dlq/contrib/drf/README.md) для работы с хранилищем сообщений. Предназначен для совместной работы с реализацией хранилища сообщений django ORM.
107
+
108
+ ## Запуск тестов
109
+
110
+ Тесты запускаются через `tox`. В системе должен быть установлен [uv](https://docs.astral.sh/uv/).
111
+
112
+ Для корректного запуска тестов необходимы следующие значения переменных окружения в системе. Вместо `<login>` и `<password>` необходимо подставить данные от УЗ БАРС:
113
+
114
+ ```bash
115
+ UV_INDEX="internal=http://<login>:<password>@nexus.py.bars.group/repository/pypi-all/simple"
116
+ UV_DEFAULT_INDEX="https://pypi.org/simple"
117
+ UV_INDEX_STRATEGY=unsafe-best-match
118
+ # Для docker 29+
119
+ TOX_DOCKER_GATEWAY=localhost
120
+ ```
121
+
122
+ Чтобы не менять значения системных переменных, можно создать `.env` файл c переменными окружения и запускать команды через [python-dotenv](https://github.com/theskumar/python-dotenv/).
123
+
124
+ Установка `tox` с необходимыми зависимостями:
125
+ ```bash
126
+ uv tool install tox --with tox-uv --with m3-tox-docker
127
+ ```
128
+
129
+ Или с использованием `python-dotenv`:
130
+ ```bash
131
+ dotenv run uv tool install tox --with tox-uv --with m3-tox-docker
132
+ ```
133
+
134
+ Запуск тестов:
135
+ ```bash
136
+ tox
137
+ ```
138
+
139
+ Через `python-dotenv`:
140
+ ```bash
141
+ dotenv run tox
142
+ ```
143
+
@@ -78,3 +78,40 @@ def bootstrap() -> None:
78
78
  В пакете реализованы готовые к использованию компоненты:
79
79
  * Реализация абстрактного [хранилища сообщений](./src/explicit/dlq/contrib/django/README.md) на базе Django ORM
80
80
  * [REST API](./src/explicit/dlq/contrib/drf/README.md) для работы с хранилищем сообщений. Предназначен для совместной работы с реализацией хранилища сообщений django ORM.
81
+
82
+ ## Запуск тестов
83
+
84
+ Тесты запускаются через `tox`. В системе должен быть установлен [uv](https://docs.astral.sh/uv/).
85
+
86
+ Для корректного запуска тестов необходимы следующие значения переменных окружения в системе. Вместо `<login>` и `<password>` необходимо подставить данные от УЗ БАРС:
87
+
88
+ ```bash
89
+ UV_INDEX="internal=http://<login>:<password>@nexus.py.bars.group/repository/pypi-all/simple"
90
+ UV_DEFAULT_INDEX="https://pypi.org/simple"
91
+ UV_INDEX_STRATEGY=unsafe-best-match
92
+ # Для docker 29+
93
+ TOX_DOCKER_GATEWAY=localhost
94
+ ```
95
+
96
+ Чтобы не менять значения системных переменных, можно создать `.env` файл c переменными окружения и запускать команды через [python-dotenv](https://github.com/theskumar/python-dotenv/).
97
+
98
+ Установка `tox` с необходимыми зависимостями:
99
+ ```bash
100
+ uv tool install tox --with tox-uv --with m3-tox-docker
101
+ ```
102
+
103
+ Или с использованием `python-dotenv`:
104
+ ```bash
105
+ dotenv run uv tool install tox --with tox-uv --with m3-tox-docker
106
+ ```
107
+
108
+ Запуск тестов:
109
+ ```bash
110
+ tox
111
+ ```
112
+
113
+ Через `python-dotenv`:
114
+ ```bash
115
+ dotenv run tox
116
+ ```
117
+
@@ -27,16 +27,17 @@ classifiers=[
27
27
  "Natural Language :: Russian",
28
28
  "Operating System :: OS Independent",
29
29
  "Development Status :: 5 - Production/Stable",
30
- "Programming Language :: Python :: 3.9",
31
30
  "Programming Language :: Python :: 3.10",
32
31
  "Programming Language :: Python :: 3.11",
33
32
  "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Programming Language :: Python :: 3.14",
34
35
  ]
35
36
 
36
- requires-python = ">=3.9"
37
+ requires-python = ">=3.10"
37
38
 
38
39
  dependencies = [
39
- "explicit-python>=2.2.1,<3",
40
+ "explicit-python>=3,<4",
40
41
  ]
41
42
 
42
43
  [project.optional-dependencies]
@@ -44,7 +45,7 @@ dev = [
44
45
  "freezegun"
45
46
  ]
46
47
  django = [
47
- "explicit-python-django>=1.0.2,<2.0"
48
+ "explicit-python-django>=2,<3"
48
49
  ]
49
50
  rest = [
50
51
  "djangorestframework>=3.13.0",
@@ -62,19 +63,21 @@ include_trailing_comma = true
62
63
  line_length = 120
63
64
  combine_as_imports = true
64
65
  lines_after_imports = 2
65
- extra_standard_library = ["dataclasses"]
66
66
  known_project = ["explicit", "testapp"]
67
67
  sections = ["FUTURE","STDLIB","THIRDPARTY","FIRSTPARTY","PROJECT","LOCALFOLDER"]
68
68
 
69
69
 
70
70
  [tool.ruff]
71
- target-version = "py312"
71
+ target-version = "py310"
72
72
  line-length = 120
73
73
  indent-width = 4
74
+ exclude = [
75
+ "**/migrations/*.py",
76
+ ]
74
77
 
75
78
 
76
79
  [tool.ruff.lint]
77
- select = ["F", "E", "D", "W", "S", "N",]
80
+ select = ["F", "E", "D", "W", "S", "N", "UP007", "UP045"]
78
81
  ignore = [
79
82
  # Missing docstring in public module
80
83
  "D100",
@@ -85,36 +88,9 @@ ignore = [
85
88
  # use of assert detected (useless with pytest)
86
89
  "S101",
87
90
  ]
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
- ]
91
+
92
+ [tool.ruff.lint.pydocstyle]
93
+ convention = "google"
118
94
 
119
95
  [tool.ruff.format]
120
96
  quote-style = "single"
@@ -122,8 +98,8 @@ indent-style = "space"
122
98
  docstring-code-format = true
123
99
 
124
100
  [tool.ruff.lint.per-file-ignores]
125
- "*/tests/*" = ["D"]
126
- "*/migrations/*" = ["D"]
101
+ "**/testapp/**" = ["D"]
102
+ "**/testapp/settings.py" = ["S105"]
127
103
 
128
104
  [tool.mypy]
129
105
  plugins = ["pydantic.mypy"]
@@ -7,10 +7,6 @@ from typing import (
7
7
  TYPE_CHECKING,
8
8
  )
9
9
 
10
- from pydantic import (
11
- ConfigDict,
12
- )
13
-
14
10
  from explicit.dlq.domain import (
15
11
  commands,
16
12
  )
@@ -35,8 +31,6 @@ if TYPE_CHECKING:
35
31
  class DLQConfig:
36
32
  """Конфигурация DLQ."""
37
33
 
38
- model_config = ConfigDict(arbitrary_types_allowed=True)
39
-
40
34
  bus: 'MessageBus'
41
35
  repository: 'AbstractDLQRepository'
42
36
  message_dispatcher: 'DLQMessageDispatcher'
@@ -7,13 +7,12 @@ testapp/settings.py:
7
7
  INSTALLED_APPS = [
8
8
  # другие приложения
9
9
  'explicit.dlq.contrib.django', # подключение приложения с моделью DeadLetter
10
- 'testapp.core', # настройка компонентов из explicit.dlq
10
+ 'testapp.core', # настройка компонентов из explicit.dlq
11
11
  ]
12
12
  ```
13
13
 
14
14
  testapp/dlq/apps.py:
15
15
  ```python
16
-
17
16
  from datetime import timedelta
18
17
 
19
18
  from django.apps import AppConfig as AppConfigBase
@@ -27,9 +26,10 @@ class AppConfig(AppConfigBase):
27
26
 
28
27
  def _configure_dlq(self):
29
28
  from explicit.dlq.config import DLQConfig, configure_dlq
30
-
31
- # реализация репозитория на базе Django ORM
29
+
30
+ # реализация репозитория на базе Django ORM
32
31
  from explicit.dlq.contrib.django.adapters.db import Repository
32
+
33
33
  config = DLQConfig(
34
34
  # ...
35
35
  repository=Repository(),
@@ -1,9 +1,10 @@
1
1
  """Адаптеры для работы с Dead Letter Queue (DLQ) в базе данных."""
2
2
 
3
+ from collections.abc import (
4
+ Iterator,
5
+ )
3
6
  from typing import (
4
7
  TYPE_CHECKING,
5
- Iterator,
6
- Union,
7
8
  )
8
9
 
9
10
  from django.core.exceptions import (
@@ -95,7 +96,7 @@ class Repository(AbstractDLQRepository):
95
96
  for db_instance in self._base_qs.iterator():
96
97
  yield self._to_domain(db_instance)
97
98
 
98
- def get_object_by_id(self, identifier: 'Union[UUID, str]') -> DeadLetter:
99
+ def get_object_by_id(self, identifier: 'UUID | str') -> DeadLetter:
99
100
  """Получить сообщение по идентификатору."""
100
101
  try:
101
102
  db_instance = self._base_qs.get(pk=identifier)
@@ -12,8 +12,12 @@ from django.db import (
12
12
  )
13
13
 
14
14
  from explicit.dlq.domain.model import (
15
+ TOPIC_MAX_LENGTH,
15
16
  DeadLetter as DomainDeadLetter,
16
17
  )
18
+ from explicit.domain import (
19
+ get_field_title,
20
+ )
17
21
 
18
22
 
19
23
  if DJANGO_VERSION >= (4, 0):
@@ -27,12 +31,18 @@ else:
27
31
  class DeadLetter(models.Model):
28
32
  """Недоставленное/необработанное сообщение."""
29
33
 
30
- id = models.UUIDField(primary_key=True, default=uuid4, editable=False, verbose_name=DomainDeadLetter.id.title)
31
- raw_message_value = models.BinaryField(verbose_name=DomainDeadLetter.raw_message_value.title)
32
- raw_message_key = models.BinaryField(verbose_name=DomainDeadLetter.raw_message_key.title, null=True, blank=True)
33
- topic = models.CharField(verbose_name=DomainDeadLetter.topic.title, max_length=DomainDeadLetter.topic.max_length)
34
- attempts = JSONField(verbose_name=DomainDeadLetter.attempts.title)
35
- processed_at = models.DateTimeField(verbose_name=DomainDeadLetter.processed_at.title, null=True, blank=True)
34
+ id = models.UUIDField(
35
+ primary_key=True, default=uuid4, editable=False, verbose_name=get_field_title(DomainDeadLetter, 'id')
36
+ )
37
+ raw_message_value = models.BinaryField(verbose_name=get_field_title(DomainDeadLetter, 'raw_message_value'))
38
+ raw_message_key = models.BinaryField(
39
+ verbose_name=get_field_title(DomainDeadLetter, 'raw_message_key'), null=True, blank=True
40
+ )
41
+ topic = models.CharField(verbose_name=get_field_title(DomainDeadLetter, 'topic'), max_length=TOPIC_MAX_LENGTH)
42
+ attempts = JSONField(verbose_name=get_field_title(DomainDeadLetter, 'attempts'))
43
+ processed_at = models.DateTimeField(
44
+ verbose_name=get_field_title(DomainDeadLetter, 'processed_at'), null=True, blank=True
45
+ )
36
46
 
37
47
  # служебные поля используемые для фильтрации и сортировки, чтобы не лезть в json
38
48
  _first_attempted_at = models.DateTimeField(db_index=True)
@@ -14,7 +14,6 @@ class DeadLetterQueueViewSet(BaseDeadLetterQueueViewSet):
14
14
  from testapp import core
15
15
 
16
16
  return core.bus.handle(command)
17
-
18
17
  ```
19
18
 
20
19
  testapp/rest/urls.py:
@@ -1,12 +1,13 @@
1
1
  """Команды DLQ."""
2
2
 
3
- from typing import (
4
- Optional,
5
- )
6
3
  from uuid import (
7
4
  UUID,
8
5
  )
9
6
 
7
+ from pydantic import (
8
+ ConfigDict,
9
+ )
10
+
10
11
  from explicit.messagebus.commands import (
11
12
  Command,
12
13
  )
@@ -15,14 +16,13 @@ from explicit.messagebus.commands import (
15
16
  class RegisterDeadLetter(Command):
16
17
  """Зарегистрировать сообщение, которое не удалось обработать."""
17
18
 
19
+ model_config = ConfigDict(arbitrary_types_allowed=True)
20
+
18
21
  topic: str
19
22
  raw_message_value: bytes
20
- raw_message_key: Optional[bytes] = None
23
+ raw_message_key: bytes | None = None
21
24
  exception: Exception
22
25
 
23
- class Config: # noqa: D106
24
- arbitrary_types_allowed = True
25
-
26
26
 
27
27
  class ProcessDeadLetter(Command):
28
28
  """Выполнить попытку обработки сообщения."""
@@ -5,9 +5,6 @@ from datetime import (
5
5
  datetime,
6
6
  timezone,
7
7
  )
8
- from typing import (
9
- Union,
10
- )
11
8
 
12
9
  from explicit.domain import (
13
10
  Str,
@@ -21,9 +18,6 @@ from explicit.domain.model import (
21
18
  Unset,
22
19
  unset,
23
20
  )
24
- from explicit.domain.types import (
25
- NoneStr,
26
- )
27
21
 
28
22
  from .model import (
29
23
  Attempt,
@@ -34,7 +28,7 @@ from .model import (
34
28
  class AttemptDTO(DTOBase):
35
29
  """Объект передачи данных о попытке обработки сообщения."""
36
30
 
37
- failed_at: Union[datetime, Unset] = unset
31
+ failed_at: datetime | Unset = unset
38
32
  error_message: Str = unset
39
33
  traceback: Str = unset
40
34
 
@@ -43,11 +37,11 @@ class DeadLetterDTO(DTOBase):
43
37
  """Объект передачи данных о необработанном сообщении."""
44
38
 
45
39
  id: UnsetUUID = unset
46
- raw_message_value: Union[bytes, Unset] = unset
47
- raw_message_key: NoneStr = unset
40
+ raw_message_value: bytes | Unset = unset
41
+ raw_message_key: bytes | None | Unset = unset
48
42
  topic: Str = unset
49
- attempts: Union[list[AttemptDTO], Unset] = unset
50
- processed_at: Union[datetime, None, Unset] = unset
43
+ attempts: list[AttemptDTO] | Unset = unset
44
+ processed_at: datetime | None | Unset = unset
51
45
 
52
46
 
53
47
  class Factory(AbstractDomainFactory):
@@ -55,7 +49,7 @@ class Factory(AbstractDomainFactory):
55
49
 
56
50
  def create(self, data: DeadLetterDTO) -> DeadLetter:
57
51
  """Создать DLQ из DTO."""
58
- params = data.dict()
52
+ params = data.model_dump()
59
53
 
60
54
  return DeadLetter(**params)
61
55
 
@@ -68,7 +62,7 @@ class Factory(AbstractDomainFactory):
68
62
  )
69
63
 
70
64
  def create_from_exception(
71
- self, raw_message_value: bytes, raw_message_key: Union[bytes, None], topic: str, exception: Exception
65
+ self, raw_message_value: bytes, raw_message_key: bytes | None, topic: str, exception: Exception
72
66
  ) -> DeadLetter:
73
67
  """Создать DLQ из исключения."""
74
68
  attempt = self.create_attempt_from_exception(exception)
@@ -7,13 +7,11 @@ from datetime import (
7
7
  )
8
8
  from typing import (
9
9
  TYPE_CHECKING,
10
- Optional,
11
- Union,
12
10
  )
13
11
 
14
12
  from pydantic import (
15
13
  Field,
16
- validator,
14
+ field_validator,
17
15
  )
18
16
 
19
17
  from explicit.contrib.domain.model import (
@@ -22,9 +20,16 @@ from explicit.contrib.domain.model import (
22
20
 
23
21
 
24
22
  if TYPE_CHECKING:
25
- from dataclasses import dataclass # noqa
23
+ from dataclasses import (
24
+ dataclass,
25
+ )
26
26
  else:
27
- from pydantic.dataclasses import dataclass # noqa
27
+ from pydantic.dataclasses import (
28
+ dataclass,
29
+ )
30
+
31
+
32
+ TOPIC_MAX_LENGTH = 256
28
33
 
29
34
 
30
35
  class DeadLetterNotFound(Exception): # noqa: N818
@@ -49,12 +54,14 @@ class DeadLetter:
49
54
 
50
55
  id: uuid.UUID = uuid_identifier()
51
56
  raw_message_value: bytes = Field(title='Содержимое сообщения')
52
- raw_message_key: Union[bytes, None] = Field(title='Ключ сообщения', default=None)
53
- topic: str = Field(title='Топик сообщения', max_length=256)
54
- attempts: tuple[Attempt, ...] = Field(default_factory=tuple, title='Попытки обработки сообщения')
55
- processed_at: Optional[datetime] = Field(default=None, title='Время успешной обработки сообщения')
56
-
57
- @validator('attempts', always=True)
57
+ raw_message_key: bytes | None = Field(title='Ключ сообщения', default=None)
58
+ topic: str = Field(title='Топик сообщения', max_length=TOPIC_MAX_LENGTH)
59
+ attempts: tuple[Attempt, ...] = Field(
60
+ default_factory=tuple, validate_default=True, title='Попытки обработки сообщения'
61
+ )
62
+ processed_at: datetime | None = Field(default=None, title='Время успешной обработки сообщения')
63
+
64
+ @field_validator('attempts')
58
65
  @classmethod
59
66
  def _validate_attempts(cls, value):
60
67
  if not value:
@@ -1,9 +1,5 @@
1
1
  """Сервисы предметной области для работы с DLQ."""
2
2
 
3
- from typing import (
4
- Optional,
5
- )
6
-
7
3
  from explicit.unit_of_work import (
8
4
  AbstractUnitOfWork,
9
5
  )
@@ -19,7 +15,7 @@ from .model import (
19
15
 
20
16
  def register_dead_letter(
21
17
  raw_message_value: bytes,
22
- raw_message_key: Optional[bytes],
18
+ raw_message_key: bytes | None,
23
19
  topic: str,
24
20
  exception: Exception,
25
21
  uow: 'AbstractUnitOfWork',
@@ -3,9 +3,6 @@
3
3
  from contextlib import (
4
4
  AbstractContextManager,
5
5
  )
6
- from typing import (
7
- Optional,
8
- )
9
6
 
10
7
  from explicit.dlq.domain.commands import (
11
8
  RegisterDeadLetter,
@@ -40,7 +37,7 @@ class RegisterInDLQOnFailure(AbstractContextManager):
40
37
  bus: 'MessageBus',
41
38
  topic: str,
42
39
  raw_message_value: bytes,
43
- raw_message_key: Optional[bytes] = None,
40
+ raw_message_key: bytes | None = None,
44
41
  ):
45
42
  self._bus = bus
46
43
  self._topic = topic
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: explicit-python-dlq
3
- Version: 1.0.0
3
+ Version: 2.0.0
4
4
  Summary: Набор компонентов реализующих DQL для систем на базе explicit
5
5
  Author-email: "АО \"БАРС Груп\"" <education_dev@bars-open.ru>
6
6
  Classifier: Intended Audience :: Developers
@@ -8,17 +8,18 @@ Classifier: Environment :: Web Environment
8
8
  Classifier: Natural Language :: Russian
9
9
  Classifier: Operating System :: OS Independent
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
- Classifier: Programming Language :: Python :: 3.9
12
11
  Classifier: Programming Language :: Python :: 3.10
13
12
  Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
15
- Requires-Python: >=3.9
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.10
16
17
  Description-Content-Type: text/markdown
17
- Requires-Dist: explicit-python<3,>=2.2.1
18
+ Requires-Dist: explicit-python<4,>=3
18
19
  Provides-Extra: dev
19
20
  Requires-Dist: freezegun; extra == "dev"
20
21
  Provides-Extra: django
21
- Requires-Dist: explicit-python-django<2.0,>=1.0.2; extra == "django"
22
+ Requires-Dist: explicit-python-django<3,>=2; extra == "django"
22
23
  Provides-Extra: rest
23
24
  Requires-Dist: djangorestframework>=3.13.0; extra == "rest"
24
25
  Requires-Dist: django_filter>=23.3; extra == "rest"
@@ -103,3 +104,40 @@ def bootstrap() -> None:
103
104
  В пакете реализованы готовые к использованию компоненты:
104
105
  * Реализация абстрактного [хранилища сообщений](./src/explicit/dlq/contrib/django/README.md) на базе Django ORM
105
106
  * [REST API](./src/explicit/dlq/contrib/drf/README.md) для работы с хранилищем сообщений. Предназначен для совместной работы с реализацией хранилища сообщений django ORM.
107
+
108
+ ## Запуск тестов
109
+
110
+ Тесты запускаются через `tox`. В системе должен быть установлен [uv](https://docs.astral.sh/uv/).
111
+
112
+ Для корректного запуска тестов необходимы следующие значения переменных окружения в системе. Вместо `<login>` и `<password>` необходимо подставить данные от УЗ БАРС:
113
+
114
+ ```bash
115
+ UV_INDEX="internal=http://<login>:<password>@nexus.py.bars.group/repository/pypi-all/simple"
116
+ UV_DEFAULT_INDEX="https://pypi.org/simple"
117
+ UV_INDEX_STRATEGY=unsafe-best-match
118
+ # Для docker 29+
119
+ TOX_DOCKER_GATEWAY=localhost
120
+ ```
121
+
122
+ Чтобы не менять значения системных переменных, можно создать `.env` файл c переменными окружения и запускать команды через [python-dotenv](https://github.com/theskumar/python-dotenv/).
123
+
124
+ Установка `tox` с необходимыми зависимостями:
125
+ ```bash
126
+ uv tool install tox --with tox-uv --with m3-tox-docker
127
+ ```
128
+
129
+ Или с использованием `python-dotenv`:
130
+ ```bash
131
+ dotenv run uv tool install tox --with tox-uv --with m3-tox-docker
132
+ ```
133
+
134
+ Запуск тестов:
135
+ ```bash
136
+ tox
137
+ ```
138
+
139
+ Через `python-dotenv`:
140
+ ```bash
141
+ dotenv run tox
142
+ ```
143
+
@@ -1,10 +1,10 @@
1
- explicit-python<3,>=2.2.1
1
+ explicit-python<4,>=3
2
2
 
3
3
  [dev]
4
4
  freezegun
5
5
 
6
6
  [django]
7
- explicit-python-django<2.0,>=1.0.2
7
+ explicit-python-django<3,>=2
8
8
 
9
9
  [rest]
10
10
  djangorestframework>=3.13.0