explicit-python-dlq 1.1.0__py3-none-any.whl → 2.0.0__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.
explicit/dlq/config.py CHANGED
@@ -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.1.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,18 +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
14
  Classifier: Programming Language :: Python :: 3.13
16
- Requires-Python: >=3.9
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.10
17
17
  Description-Content-Type: text/markdown
18
- Requires-Dist: explicit-python<3,>=2.2.1
18
+ Requires-Dist: explicit-python<4,>=3
19
19
  Provides-Extra: dev
20
20
  Requires-Dist: freezegun; extra == "dev"
21
21
  Provides-Extra: django
22
- Requires-Dist: explicit-python-django<2.0,>=1.0.2; extra == "django"
22
+ Requires-Dist: explicit-python-django<3,>=2; extra == "django"
23
23
  Provides-Extra: rest
24
24
  Requires-Dist: djangorestframework>=3.13.0; extra == "rest"
25
25
  Requires-Dist: django_filter>=23.3; extra == "rest"
@@ -104,3 +104,40 @@ def bootstrap() -> None:
104
104
  В пакете реализованы готовые к использованию компоненты:
105
105
  * Реализация абстрактного [хранилища сообщений](./src/explicit/dlq/contrib/django/README.md) на базе Django ORM
106
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,32 +1,32 @@
1
1
  explicit/dlq/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
- explicit/dlq/config.py,sha256=zpZrXqpIWPNzr6TBqvZ5Gs2tDwRVh0U43E1nIRGCm-E,1488
2
+ explicit/dlq/config.py,sha256=GcB6tJop2CYC49IlgEPBmE7iISLaNpuhJ5Qmk9oV7hY,1385
3
3
  explicit/dlq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  explicit/dlq/types.py,sha256=aVqdsdKZ-U_5TD3dpLtzksyL3jVedIhJnu7IihW3vh4,544
5
5
  explicit/dlq/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  explicit/dlq/adapters/db.py,sha256=c0j_dU3IRjm-7rx3S7nqE61vKjGsH8TYXiChbUQJoIU,346
7
7
  explicit/dlq/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- explicit/dlq/contrib/django/README.md,sha256=kpTW0GzYP8QV5ld7UI0ly-TWNbm3pbV7HWbdTpOGYAs,1290
8
+ explicit/dlq/contrib/django/README.md,sha256=TE9tt1M1qDQIgMegeQgesVeyLC1ajBbVjINGYDxpJcI,1266
9
9
  explicit/dlq/contrib/django/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  explicit/dlq/contrib/django/apps.py,sha256=WDloauYls-krHbJOp_brpS8WifMyK8O59eUTSQDZROU,263
11
- explicit/dlq/contrib/django/models.py,sha256=Zh9MTkkXaL1Pt5kwh9IFYnE29HXrd6QJxYWClxtyz6o,1772
11
+ explicit/dlq/contrib/django/models.py,sha256=eu_sobtXL1Q1xrJ9St7HiT-N6_JGgd0aEwMBRGXg_vE,1956
12
12
  explicit/dlq/contrib/django/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- explicit/dlq/contrib/django/adapters/db.py,sha256=6ta3IRLknO_J5vwvF1mJWVZqCziqmy7cfbhiiM8cjDk,3309
13
+ explicit/dlq/contrib/django/adapters/db.py,sha256=IdJGnbtzRNyTcClLR5Gf3S5-HkCFRDJs9SPddh06iHs,3324
14
14
  explicit/dlq/contrib/django/migrations/0001_initial.py,sha256=sDjCDZl1RvR_oOhBvftbW3LvRf6FbuLmRF_eQ-dauhM,2133
15
15
  explicit/dlq/contrib/django/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- explicit/dlq/contrib/drf/README.md,sha256=9eQlgFEWIwrFhrWvLVL8_Br_-3wU2kAoLxHoBUY5PS8,748
16
+ explicit/dlq/contrib/drf/README.md,sha256=0VlBusFcswT8i9FnCV_llkhDJlFCHQ7ih0qFb_hc7FU,747
17
17
  explicit/dlq/contrib/drf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  explicit/dlq/contrib/drf/serializers.py,sha256=zNASKGtXbw5unWqMJgDaqITwtVAsW54liJsXoK4FRBU,1774
19
19
  explicit/dlq/contrib/drf/views.py,sha256=xBRUSzBbZ10PL8bHktZLosR-Tw-Q9jgN1n76oQmqLa4,2099
20
20
  explicit/dlq/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- explicit/dlq/domain/commands.py,sha256=v94uHQvGd-rX08_Paq9FEC-RSmsBaZMS9qxu6dHfzTo,803
22
- explicit/dlq/domain/factories.py,sha256=5tNLTTggBA4JGm66wtiSjGrUGvvdh9ZZB6DXp5ax0pY,2270
23
- explicit/dlq/domain/model.py,sha256=GBiDRGk4wpDZQqawZwNEtpHwS25YL_idvvkFNpajFvA,2291
24
- explicit/dlq/domain/services.py,sha256=2HUZb3aL3ZAH01_lpvRcGLffVjdmiB0VV-ZxX5m8x60,2051
21
+ explicit/dlq/domain/commands.py,sha256=X94H6DrUtaArdGArUR-VxQQNpfHvg9u5hRnwgMboG58,794
22
+ explicit/dlq/domain/factories.py,sha256=7p3niFzPeghhAY6Vk1F6DlUahGk66o-PNVJzg4sfKw0,2175
23
+ explicit/dlq/domain/model.py,sha256=YUtLtLvtsXR_mxxopVv4XtEOO7n_EQwMZ6AxxwMj2l0,2349
24
+ explicit/dlq/domain/services.py,sha256=DCuTZXPGQLBhWQMYRyO_RiUlYLd71QCVRtOeCDUET5A,2010
25
25
  explicit/dlq/infrastructure/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- explicit/dlq/infrastructure/handlers.py,sha256=_1ZP8XtxkKgtDvLFZHVePHMkwtz1vhzNQJv2611pgfQ,2324
26
+ explicit/dlq/infrastructure/handlers.py,sha256=uQ05w0_9RyOfh8HQB4GtNcgENfWXSa-3h4IsLa6BwzE,2284
27
27
  explicit/dlq/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  explicit/dlq/services/handlers.py,sha256=U6y4ElqXi1VocS3SE3SvwK69MMApAXILJes-xDbk_Ps,3232
29
- explicit_python_dlq-1.1.0.dist-info/METADATA,sha256=O4nHL7VB1rdONqp1cjWZVgwxGNYDoRfplzD4rm3RsjI,4120
30
- explicit_python_dlq-1.1.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
31
- explicit_python_dlq-1.1.0.dist-info/top_level.txt,sha256=tt6T8l4Yji4ww87qZQcD4CbcwTIHy7NAPmU7QAfMcpY,9
32
- explicit_python_dlq-1.1.0.dist-info/RECORD,,
29
+ explicit_python_dlq-2.0.0.dist-info/METADATA,sha256=Z9SM9ki1Yg5c_J4oNi3L7gFx6_NIEQlCdXb7FSuaTVo,5482
30
+ explicit_python_dlq-2.0.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
31
+ explicit_python_dlq-2.0.0.dist-info/top_level.txt,sha256=tt6T8l4Yji4ww87qZQcD4CbcwTIHy7NAPmU7QAfMcpY,9
32
+ explicit_python_dlq-2.0.0.dist-info/RECORD,,