explicit-python-dlq 1.2.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.2.0/src/explicit_python_dlq.egg-info → explicit_python_dlq-2.0.0}/PKG-INFO +5 -5
  2. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/pyproject.toml +14 -39
  3. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/config.py +0 -6
  4. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/adapters/db.py +4 -3
  5. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/models.py +16 -6
  6. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/domain/commands.py +7 -7
  7. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/domain/factories.py +7 -13
  8. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/domain/model.py +18 -11
  9. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/domain/services.py +1 -5
  10. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/infrastructure/handlers.py +1 -4
  11. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0/src/explicit_python_dlq.egg-info}/PKG-INFO +5 -5
  12. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit_python_dlq.egg-info/requires.txt +2 -2
  13. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/MANIFEST.in +0 -0
  14. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/README.md +0 -0
  15. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/setup.cfg +0 -0
  16. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/__init__.py +0 -0
  17. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/adapters/__init__.py +0 -0
  18. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/adapters/db.py +0 -0
  19. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/__init__.py +0 -0
  20. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/README.md +0 -0
  21. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/__init__.py +0 -0
  22. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/adapters/__init__.py +0 -0
  23. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/apps.py +0 -0
  24. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/migrations/0001_initial.py +0 -0
  25. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/django/migrations/__init__.py +0 -0
  26. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/drf/README.md +0 -0
  27. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/drf/__init__.py +0 -0
  28. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/drf/serializers.py +0 -0
  29. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/contrib/drf/views.py +0 -0
  30. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/domain/__init__.py +0 -0
  31. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/infrastructure/__init__.py +0 -0
  32. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/py.typed +0 -0
  33. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/services/__init__.py +0 -0
  34. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/services/handlers.py +0 -0
  35. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit/dlq/types.py +0 -0
  36. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit_python_dlq.egg-info/SOURCES.txt +0 -0
  37. {explicit_python_dlq-1.2.0 → explicit_python_dlq-2.0.0}/src/explicit_python_dlq.egg-info/dependency_links.txt +0 -0
  38. {explicit_python_dlq-1.2.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.2.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"
@@ -27,17 +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",
34
33
  "Programming Language :: Python :: 3.13",
34
+ "Programming Language :: Python :: 3.14",
35
35
  ]
36
36
 
37
- requires-python = ">=3.9"
37
+ requires-python = ">=3.10"
38
38
 
39
39
  dependencies = [
40
- "explicit-python>=2.2.1,<3",
40
+ "explicit-python>=3,<4",
41
41
  ]
42
42
 
43
43
  [project.optional-dependencies]
@@ -45,7 +45,7 @@ dev = [
45
45
  "freezegun"
46
46
  ]
47
47
  django = [
48
- "explicit-python-django>=1.0.2,<2.0"
48
+ "explicit-python-django>=2,<3"
49
49
  ]
50
50
  rest = [
51
51
  "djangorestframework>=3.13.0",
@@ -63,19 +63,21 @@ include_trailing_comma = true
63
63
  line_length = 120
64
64
  combine_as_imports = true
65
65
  lines_after_imports = 2
66
- extra_standard_library = ["dataclasses"]
67
66
  known_project = ["explicit", "testapp"]
68
67
  sections = ["FUTURE","STDLIB","THIRDPARTY","FIRSTPARTY","PROJECT","LOCALFOLDER"]
69
68
 
70
69
 
71
70
  [tool.ruff]
72
- target-version = "py312"
71
+ target-version = "py310"
73
72
  line-length = 120
74
73
  indent-width = 4
74
+ exclude = [
75
+ "**/migrations/*.py",
76
+ ]
75
77
 
76
78
 
77
79
  [tool.ruff.lint]
78
- select = ["F", "E", "D", "W", "S", "N",]
80
+ select = ["F", "E", "D", "W", "S", "N", "UP007", "UP045"]
79
81
  ignore = [
80
82
  # Missing docstring in public module
81
83
  "D100",
@@ -86,36 +88,9 @@ ignore = [
86
88
  # use of assert detected (useless with pytest)
87
89
  "S101",
88
90
  ]
89
- exclude = [
90
- ".bzr",
91
- ".direnv",
92
- ".eggs",
93
- ".git",
94
- ".git-rewrite",
95
- ".hg",
96
- ".idea",
97
- ".ipynb_checkpoints",
98
- ".mypy_cache",
99
- ".nox",
100
- ".pants.d",
101
- ".pyenv",
102
- ".pytest_cache",
103
- ".pytype",
104
- ".ruff_cache",
105
- ".svn",
106
- ".tox",
107
- ".venv",
108
- ".vscode",
109
- "__pypackages__",
110
- "_build",
111
- "buck-out",
112
- "build",
113
- "dist",
114
- "node_modules",
115
- "site-packages",
116
- "venv",
117
- "py-libs/**",
118
- ]
91
+
92
+ [tool.ruff.lint.pydocstyle]
93
+ convention = "google"
119
94
 
120
95
  [tool.ruff.format]
121
96
  quote-style = "single"
@@ -123,8 +98,8 @@ indent-style = "space"
123
98
  docstring-code-format = true
124
99
 
125
100
  [tool.ruff.lint.per-file-ignores]
126
- "*/tests/*" = ["D"]
127
- "*/migrations/*" = ["D"]
101
+ "**/testapp/**" = ["D"]
102
+ "**/testapp/settings.py" = ["S105"]
128
103
 
129
104
  [tool.mypy]
130
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'
@@ -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)
@@ -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.2.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"
@@ -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