instant-python 0.20.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.
Files changed (202) hide show
  1. instant_python/__init__.py +1 -0
  2. instant_python/cli/__init__.py +0 -0
  3. instant_python/cli/cli.py +58 -0
  4. instant_python/cli/instant_python_typer.py +35 -0
  5. instant_python/config/__init__.py +0 -0
  6. instant_python/config/application/__init__.py +0 -0
  7. instant_python/config/application/config_generator.py +23 -0
  8. instant_python/config/delivery/__init__.py +0 -0
  9. instant_python/config/delivery/cli.py +19 -0
  10. instant_python/config/domain/__init__.py +0 -0
  11. instant_python/config/domain/question_wizard.py +7 -0
  12. instant_python/config/infra/__init__.py +0 -0
  13. instant_python/config/infra/question_wizard/__init__.py +0 -0
  14. instant_python/config/infra/question_wizard/questionary_console_wizard.py +25 -0
  15. instant_python/config/infra/question_wizard/step/__init__.py +0 -0
  16. instant_python/config/infra/question_wizard/step/dependencies_step.py +64 -0
  17. instant_python/config/infra/question_wizard/step/general_step.py +81 -0
  18. instant_python/config/infra/question_wizard/step/git_step.py +41 -0
  19. instant_python/config/infra/question_wizard/step/questionary.py +17 -0
  20. instant_python/config/infra/question_wizard/step/steps.py +21 -0
  21. instant_python/config/infra/question_wizard/step/template_step.py +63 -0
  22. instant_python/initialize/__init__.py +0 -0
  23. instant_python/initialize/application/__init__.py +0 -0
  24. instant_python/initialize/application/project_initializer.py +47 -0
  25. instant_python/initialize/delivery/__init__.py +0 -0
  26. instant_python/initialize/delivery/cli.py +48 -0
  27. instant_python/initialize/domain/__init__.py +0 -0
  28. instant_python/initialize/domain/env_manager.py +9 -0
  29. instant_python/initialize/domain/node.py +73 -0
  30. instant_python/initialize/domain/project_formatter.py +7 -0
  31. instant_python/initialize/domain/project_renderer.py +10 -0
  32. instant_python/initialize/domain/project_structure.py +77 -0
  33. instant_python/initialize/domain/project_writer.py +20 -0
  34. instant_python/initialize/domain/version_control_configurer.py +9 -0
  35. instant_python/initialize/infra/__init__.py +0 -0
  36. instant_python/initialize/infra/env_manager/__init__.py +0 -0
  37. instant_python/initialize/infra/env_manager/env_manager_factory.py +27 -0
  38. instant_python/initialize/infra/env_manager/pdm_env_manager.py +66 -0
  39. instant_python/initialize/infra/env_manager/system_console.py +65 -0
  40. instant_python/initialize/infra/env_manager/uv_env_manager.py +74 -0
  41. instant_python/initialize/infra/formatter/__init__.py +0 -0
  42. instant_python/initialize/infra/formatter/ruff_project_formatter.py +10 -0
  43. instant_python/initialize/infra/renderer/__init__.py +0 -0
  44. instant_python/initialize/infra/renderer/jinja_environment.py +71 -0
  45. instant_python/initialize/infra/renderer/jinja_project_renderer.py +57 -0
  46. instant_python/initialize/infra/version_control/__init__.py +0 -0
  47. instant_python/initialize/infra/version_control/git_configurer.py +29 -0
  48. instant_python/initialize/infra/writer/__init__.py +0 -0
  49. instant_python/initialize/infra/writer/file_system_project_writer.py +23 -0
  50. instant_python/shared/__init__.py +0 -0
  51. instant_python/shared/application_error.py +8 -0
  52. instant_python/shared/domain/__init__.py +0 -0
  53. instant_python/shared/domain/config_repository.py +18 -0
  54. instant_python/shared/domain/config_schema.py +113 -0
  55. instant_python/shared/domain/dependency_config.py +41 -0
  56. instant_python/shared/domain/general_config.py +71 -0
  57. instant_python/shared/domain/git_config.py +32 -0
  58. instant_python/shared/domain/template_config.py +76 -0
  59. instant_python/shared/infra/__init__.py +0 -0
  60. instant_python/shared/infra/persistence/__init__.py +0 -0
  61. instant_python/shared/infra/persistence/yaml_config_repository.py +39 -0
  62. instant_python/shared/supported_built_in_features.py +20 -0
  63. instant_python/shared/supported_licenses.py +11 -0
  64. instant_python/shared/supported_managers.py +10 -0
  65. instant_python/shared/supported_python_versions.py +12 -0
  66. instant_python/shared/supported_templates.py +12 -0
  67. instant_python/templates/boilerplate/.gitignore +164 -0
  68. instant_python/templates/boilerplate/.pre-commit-config.yml +73 -0
  69. instant_python/templates/boilerplate/.python-version +1 -0
  70. instant_python/templates/boilerplate/CITATION.cff +13 -0
  71. instant_python/templates/boilerplate/LICENSE +896 -0
  72. instant_python/templates/boilerplate/README.md +8 -0
  73. instant_python/templates/boilerplate/SECURITY.md +43 -0
  74. instant_python/templates/boilerplate/event_bus/__init__.py +0 -0
  75. instant_python/templates/boilerplate/event_bus/domain_event.py +15 -0
  76. instant_python/templates/boilerplate/event_bus/domain_event_json_deserializer.py +25 -0
  77. instant_python/templates/boilerplate/event_bus/domain_event_json_serializer.py +16 -0
  78. instant_python/templates/boilerplate/event_bus/domain_event_subscriber.py +33 -0
  79. instant_python/templates/boilerplate/event_bus/event_aggregate.py +19 -0
  80. instant_python/templates/boilerplate/event_bus/event_bus.py +9 -0
  81. instant_python/templates/boilerplate/event_bus/exchange_type.py +14 -0
  82. instant_python/templates/boilerplate/event_bus/mock_event_bus.py +16 -0
  83. instant_python/templates/boilerplate/event_bus/rabbit_mq_configurer.py +45 -0
  84. instant_python/templates/boilerplate/event_bus/rabbit_mq_connection.py +71 -0
  85. instant_python/templates/boilerplate/event_bus/rabbit_mq_consumer.py +56 -0
  86. instant_python/templates/boilerplate/event_bus/rabbit_mq_event_bus.py +26 -0
  87. instant_python/templates/boilerplate/event_bus/rabbit_mq_queue_formatter.py +21 -0
  88. instant_python/templates/boilerplate/event_bus/rabbit_mq_settings.py +8 -0
  89. instant_python/templates/boilerplate/exceptions/__init__.py +0 -0
  90. instant_python/templates/boilerplate/exceptions/base_error.py +13 -0
  91. instant_python/templates/boilerplate/exceptions/domain_error.py +6 -0
  92. instant_python/templates/boilerplate/exceptions/domain_event_type_not_found_error.py +6 -0
  93. instant_python/templates/boilerplate/exceptions/rabbit_mq_connection_not_established_error.py +7 -0
  94. instant_python/templates/boilerplate/exceptions/required_value_error.py +6 -0
  95. instant_python/templates/boilerplate/fastapi/__init__.py +0 -0
  96. instant_python/templates/boilerplate/fastapi/application.py +74 -0
  97. instant_python/templates/boilerplate/fastapi/error_handlers.py +88 -0
  98. instant_python/templates/boilerplate/fastapi/error_response.py +31 -0
  99. instant_python/templates/boilerplate/fastapi/fastapi_log_middleware.py +32 -0
  100. instant_python/templates/boilerplate/fastapi/lifespan.py +13 -0
  101. instant_python/templates/boilerplate/fastapi/success_response.py +13 -0
  102. instant_python/templates/boilerplate/github/action.yml +35 -0
  103. instant_python/templates/boilerplate/github/bug_report.yml +60 -0
  104. instant_python/templates/boilerplate/github/ci.yml +199 -0
  105. instant_python/templates/boilerplate/github/feature_request.yml +21 -0
  106. instant_python/templates/boilerplate/github/release.yml +94 -0
  107. instant_python/templates/boilerplate/logger/__init__.py +0 -0
  108. instant_python/templates/boilerplate/logger/file_logger.py +55 -0
  109. instant_python/templates/boilerplate/logger/file_rotating_handler.py +36 -0
  110. instant_python/templates/boilerplate/logger/json_formatter.py +16 -0
  111. instant_python/templates/boilerplate/mypy.ini +41 -0
  112. instant_python/templates/boilerplate/persistence/__init__.py +0 -0
  113. instant_python/templates/boilerplate/persistence/alembic_migrator.py +19 -0
  114. instant_python/templates/boilerplate/persistence/async/README.md +1 -0
  115. instant_python/templates/boilerplate/persistence/async/__init__.py +0 -0
  116. instant_python/templates/boilerplate/persistence/async/alembic.ini +124 -0
  117. instant_python/templates/boilerplate/persistence/async/async_engine_fixture.py +20 -0
  118. instant_python/templates/boilerplate/persistence/async/async_session.py +20 -0
  119. instant_python/templates/boilerplate/persistence/async/env.py +94 -0
  120. instant_python/templates/boilerplate/persistence/async/models_metadata.py +10 -0
  121. instant_python/templates/boilerplate/persistence/async/postgres_settings.py +15 -0
  122. instant_python/templates/boilerplate/persistence/async/script.py.mako +26 -0
  123. instant_python/templates/boilerplate/persistence/async/sqlalchemy_repository.py +28 -0
  124. instant_python/templates/boilerplate/persistence/base.py +4 -0
  125. instant_python/templates/boilerplate/persistence/synchronous/__init__.py +0 -0
  126. instant_python/templates/boilerplate/persistence/synchronous/session_maker.py +21 -0
  127. instant_python/templates/boilerplate/persistence/synchronous/sqlalchemy_repository.py +40 -0
  128. instant_python/templates/boilerplate/pyproject.toml +134 -0
  129. instant_python/templates/boilerplate/pytest.ini +10 -0
  130. instant_python/templates/boilerplate/scripts/add_dependency.py +45 -0
  131. instant_python/templates/boilerplate/scripts/create_aggregate.py +33 -0
  132. instant_python/templates/boilerplate/scripts/insert_template.py +90 -0
  133. instant_python/templates/boilerplate/scripts/integration.sh +39 -0
  134. instant_python/templates/boilerplate/scripts/local_setup.py +12 -0
  135. instant_python/templates/boilerplate/scripts/makefile +184 -0
  136. instant_python/templates/boilerplate/scripts/post-merge.py +40 -0
  137. instant_python/templates/boilerplate/scripts/pre-commit.py +15 -0
  138. instant_python/templates/boilerplate/scripts/pre-push.py +6 -0
  139. instant_python/templates/boilerplate/scripts/remove_dependency.py +40 -0
  140. instant_python/templates/boilerplate/scripts/unit.sh +40 -0
  141. instant_python/templates/project_structure/clean_architecture/layers/application.yml +3 -0
  142. instant_python/templates/project_structure/clean_architecture/layers/delivery.yml +8 -0
  143. instant_python/templates/project_structure/clean_architecture/layers/domain.yml +10 -0
  144. instant_python/templates/project_structure/clean_architecture/layers/infra.yml +12 -0
  145. instant_python/templates/project_structure/clean_architecture/layers/test_application.yml +3 -0
  146. instant_python/templates/project_structure/clean_architecture/layers/test_delivery.yml +3 -0
  147. instant_python/templates/project_structure/clean_architecture/layers/test_domain.yml +3 -0
  148. instant_python/templates/project_structure/clean_architecture/layers/test_infra.yml +9 -0
  149. instant_python/templates/project_structure/clean_architecture/main_structure.yml +38 -0
  150. instant_python/templates/project_structure/clean_architecture/source.yml +9 -0
  151. instant_python/templates/project_structure/clean_architecture/test.yml +9 -0
  152. instant_python/templates/project_structure/config_files/gitignore.yml +3 -0
  153. instant_python/templates/project_structure/config_files/mypy.yml +4 -0
  154. instant_python/templates/project_structure/config_files/pyproject.yml +4 -0
  155. instant_python/templates/project_structure/config_files/pytest.yml +4 -0
  156. instant_python/templates/project_structure/config_files/python_version.yml +3 -0
  157. instant_python/templates/project_structure/documentation/citation.yml +4 -0
  158. instant_python/templates/project_structure/documentation/license.yml +3 -0
  159. instant_python/templates/project_structure/documentation/readme.yml +4 -0
  160. instant_python/templates/project_structure/documentation/security.yml +4 -0
  161. instant_python/templates/project_structure/domain_driven_design/layers/bounded_context.yml +17 -0
  162. instant_python/templates/project_structure/domain_driven_design/layers/delivery.yml +8 -0
  163. instant_python/templates/project_structure/domain_driven_design/layers/shared.yml +18 -0
  164. instant_python/templates/project_structure/domain_driven_design/layers/shared_domain.yml +10 -0
  165. instant_python/templates/project_structure/domain_driven_design/layers/shared_infra.yml +12 -0
  166. instant_python/templates/project_structure/domain_driven_design/layers/test_shared.yml +8 -0
  167. instant_python/templates/project_structure/domain_driven_design/layers/test_shared_delivery.yml +3 -0
  168. instant_python/templates/project_structure/domain_driven_design/layers/test_shared_domain.yml +3 -0
  169. instant_python/templates/project_structure/domain_driven_design/layers/test_shared_infra.yml +9 -0
  170. instant_python/templates/project_structure/domain_driven_design/main_structure.yml +38 -0
  171. instant_python/templates/project_structure/domain_driven_design/source.yml +10 -0
  172. instant_python/templates/project_structure/domain_driven_design/test.yml +9 -0
  173. instant_python/templates/project_structure/errors.yml +12 -0
  174. instant_python/templates/project_structure/events/event_bus_domain.yml +48 -0
  175. instant_python/templates/project_structure/events/event_bus_infra.yml +40 -0
  176. instant_python/templates/project_structure/events/mock_event_bus.yml +4 -0
  177. instant_python/templates/project_structure/fastapi/fastapi_app.yml +32 -0
  178. instant_python/templates/project_structure/fastapi/fastapi_domain.yml +12 -0
  179. instant_python/templates/project_structure/fastapi/fastapi_infra.yml +12 -0
  180. instant_python/templates/project_structure/github/github_action.yml +24 -0
  181. instant_python/templates/project_structure/github/github_issues_template.yml +14 -0
  182. instant_python/templates/project_structure/github/makefile.yml +3 -0
  183. instant_python/templates/project_structure/github/precommit_hook.yml +4 -0
  184. instant_python/templates/project_structure/logger.yml +16 -0
  185. instant_python/templates/project_structure/macros.j2 +73 -0
  186. instant_python/templates/project_structure/persistence/alembic_migrator.yml +6 -0
  187. instant_python/templates/project_structure/persistence/async_alembic.yml +27 -0
  188. instant_python/templates/project_structure/persistence/async_engine_conftest.yml +4 -0
  189. instant_python/templates/project_structure/persistence/async_sqlalchemy.yml +14 -0
  190. instant_python/templates/project_structure/persistence/persistence.yml +8 -0
  191. instant_python/templates/project_structure/persistence/synchronous_sqlalchemy.yml +20 -0
  192. instant_python/templates/project_structure/standard_project/layers/source_features.yml +13 -0
  193. instant_python/templates/project_structure/standard_project/layers/test_event_bus.yml +6 -0
  194. instant_python/templates/project_structure/standard_project/layers/test_features.yml +6 -0
  195. instant_python/templates/project_structure/standard_project/main_structure.yml +38 -0
  196. instant_python/templates/project_structure/standard_project/source.yml +5 -0
  197. instant_python/templates/project_structure/standard_project/test.yml +5 -0
  198. instant_python-0.20.0.dist-info/METADATA +318 -0
  199. instant_python-0.20.0.dist-info/RECORD +202 -0
  200. instant_python-0.20.0.dist-info/WHEEL +4 -0
  201. instant_python-0.20.0.dist-info/entry_points.txt +2 -0
  202. instant_python-0.20.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,8 @@
1
+ # {{ general.slug }}
2
+
3
+ > [!NOTE]
4
+ > This project was generated using [Instant Python](https://github.com/dimanu-py/instant-python), a fast, easy and reliable project generator for Python projects.
5
+
6
+ ## Description
7
+
8
+ {{ general.description }}
@@ -0,0 +1,43 @@
1
+ # Security Policy
2
+
3
+ Thank you for helping keep **{{ general.slug }}** package and its users safe.
4
+ We take security issues seriously and appreciate responsible disclosures.
5
+
6
+ ## Reporting a Vulnerability
7
+
8
+ > [!NOTE]
9
+ > **Please do NOT open public issues for security reports.**
10
+ > Use one of the private channels below so we can coordinate a safe disclosure.
11
+
12
+ | Channel | How it works |
13
+ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
14
+ | **GitHub Security Advisory** | 1. Navigate to the repository's **“Security → Advisories”** tab<br>2. Click **“Report a vulnerability”** and fill in the form |
15
+
16
+ Include the following, if possible:
17
+
18
+ 1. A **concise description** of the issue and its impact.
19
+ 2. **Reproduction steps** or a proof-of-concept script.
20
+ 3. Any **mitigation** ideas you've identified.
21
+
22
+ ## Our Disclosure Process
23
+
24
+ 1. **Acknowledge** report within 24–48 hours.
25
+ 2. **Triage & validate** the issue; request additional info if needed.
26
+ 3. **Fix & prepare**: develop a patch and regression tests.
27
+ 4. **Coordinate release**:
28
+ - Agree on a disclosure date with the reporter (usually ≤ 30 days).
29
+ - Publish a CVE (if applicable) and a new PyPI release.
30
+ - Post a security advisory and update CHANGELOG.
31
+ 5. **Credit** the reporter (optional & with consent).
32
+
33
+ ## Responsible Disclosure
34
+
35
+ We kindly ask you to:
36
+
37
+ - Allow us reasonable time to remediate before any public disclosure.
38
+ - Avoid violating user privacy, destroying data, or disrupting production services while researching.
39
+ - Test only on your own instances or in minimal, isolated cases.
40
+
41
+ We are committed to keeping this project and its users safe and will strive to resolve all legitimate reports swiftly and transparently.
42
+
43
+ _Thank you for keeping the **{{ general.slug }}** package community secure!_
@@ -0,0 +1,15 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass, asdict
3
+
4
+
5
+ @dataclass(frozen=True, kw_only=True)
6
+ class DomainEvent(ABC):
7
+ id: str
8
+
9
+ @classmethod
10
+ @abstractmethod
11
+ def name(cls) -> str:
12
+ raise NotImplementedError
13
+
14
+ def to_dict(self) -> dict:
15
+ return asdict(self)
@@ -0,0 +1,25 @@
1
+ import json
2
+
3
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event" | resolve_import_path(template.name) }} import DomainEvent
4
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event_subscriber" | resolve_import_path(template.name) }} import (
5
+ DomainEventSubscriber,
6
+ )
7
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event_type_not_found_errorr" | resolve_import_path(template.name) }} import (
8
+ DomainEventTypeNotFoundError,
9
+ )
10
+
11
+
12
+ class DomainEventJsonDeserializer:
13
+ _events_mapping: dict[str, type[DomainEvent]]
14
+
15
+ def __init__(self, subscriber: DomainEventSubscriber[DomainEvent]) -> None:
16
+ self._events_mapping = {event.name(): event for event in subscriber.subscribed_to()}
17
+
18
+ def deserialize(self, body: bytes) -> DomainEvent:
19
+ content = json.loads(body)
20
+ event_class = self._events_mapping.get(content["data"]["type"])
21
+
22
+ if not event_class:
23
+ raise DomainEventTypeNotFoundError(content["data"]["type"])
24
+
25
+ return event_class(**content["data"]["attributes"])
@@ -0,0 +1,16 @@
1
+ import json
2
+
3
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event" | resolve_import_path(template.name) }} import DomainEvent
4
+
5
+
6
+ class DomainEventJsonSerializer:
7
+ @staticmethod
8
+ def serialize(event: DomainEvent) -> str:
9
+ body = {
10
+ "data": {
11
+ "id": event.id,
12
+ "type": event.name(),
13
+ "attributes": event.to_dict(),
14
+ }
15
+ }
16
+ return json.dumps(body)
@@ -0,0 +1,33 @@
1
+ {% if python_version in ["3.12", "3.13"] %}
2
+ from abc import ABC, abstractmethod
3
+
4
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event" | resolve_import_path(template.name) }} import DomainEvent
5
+
6
+
7
+ class DomainEventSubscriber[EventType: DomainEvent](ABC):
8
+ @staticmethod
9
+ @abstractmethod
10
+ def subscribed_to() -> list[type[EventType]]:
11
+ raise NotImplementedError
12
+
13
+ @abstractmethod
14
+ def on(self, event: EventType) -> None:
15
+ raise NotImplementedError
16
+ {% else %}
17
+ from abc import ABC, abstractmethod
18
+ from typing import Generic, TypeVar
19
+
20
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event" | resolve_import_path(template.name) }} import DomainEvent
21
+
22
+ EventType = TypeVar("EventType", bound=DomainEvent)
23
+
24
+ class DomainEventSubscriber(Generic[EventType], ABC):
25
+ @staticmethod
26
+ @abstractmethod
27
+ def subscribed_to() -> list[type[EventType]]:
28
+ raise NotImplementedError
29
+
30
+ @abstractmethod
31
+ def on(self, event: EventType) -> None:
32
+ raise NotImplementedError
33
+ {% endif %}
@@ -0,0 +1,19 @@
1
+ from sindripy.value_objects import Aggregate
2
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event" | resolve_import_path(template.name) }} import DomainEvent
3
+
4
+
5
+ class EventAggregate(Aggregate):
6
+ _domain_events: list[DomainEvent]
7
+
8
+ def __init__(self) -> None:
9
+ super().__init__()
10
+ self._domain_events = []
11
+
12
+ def record(self, event: DomainEvent) -> None:
13
+ self._domain_events.append(event)
14
+
15
+ def pull_domain_events(self) -> list[DomainEvent]:
16
+ recorded_domain_events = self._domain_events
17
+ self._domain_events = []
18
+
19
+ return recorded_domain_events
@@ -0,0 +1,9 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event" | resolve_import_path(template.name) }} import DomainEvent
4
+
5
+
6
+ class EventBus(ABC):
7
+ @abstractmethod
8
+ async def publish(self, events: list[DomainEvent]) -> None:
9
+ raise NotImplementedError
@@ -0,0 +1,14 @@
1
+ {% if general.python_version in ["3.13", "3.12", "3.11"] %}
2
+ from enum import StrEnum
3
+
4
+
5
+ class ExchangeType(StrEnum):
6
+ {% else %}
7
+ from enum import Enum
8
+
9
+
10
+ class ExchangeType(str, Enum):
11
+ {% endif %}
12
+ TOPIC = "topic"
13
+ DIRECT = "direct"
14
+ FANOUT = "fanout"
@@ -0,0 +1,16 @@
1
+ from unittest.mock import AsyncMock
2
+
3
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event" | resolve_import_path(template.name) }} import DomainEvent
4
+ from {{ general.source_name }}{{ "shared.domain.event.event_bus" | resolve_import_path(template.name) }} import EventBus
5
+
6
+
7
+ class MockEventBus(EventBus):
8
+ def __init__(self) -> None:
9
+ self._mock_publish = AsyncMock()
10
+
11
+ async def publish(self, events: list[DomainEvent]) -> None:
12
+ await self._mock_publish(events)
13
+
14
+ def should_have_published(self, event: DomainEvent) -> None:
15
+ self._mock_publish.assert_awaited_once_with([event])
16
+ self._mock_publish.reset_mock()
@@ -0,0 +1,45 @@
1
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event" | resolve_import_path(template.name) }} import DomainEvent
2
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event_subscriber" | resolve_import_path(template.name) }} import (
3
+ DomainEventSubscriber,
4
+ )
5
+ from {{ general.source_name }}{{ "shared.infra.event.rabbit_mq.rabbit_mq_connection" | resolve_import_path(template.name) }} import (
6
+ RabbitMqConnection,
7
+ )
8
+ from {{ general.source_name }}{{ "shared.infra.event.rabbit_mq.rabbit_mq_queue_formatter" | resolve_import_path(template.name) }} import (
9
+ RabbitMqQueueFormatter,
10
+ )
11
+
12
+
13
+ class RabbitMqConfigurer:
14
+ _queue_formatter: RabbitMqQueueFormatter
15
+ _connection: RabbitMqConnection
16
+
17
+ def __init__(self, connection: RabbitMqConnection, queue_formatter: RabbitMqQueueFormatter) -> None:
18
+ self._queue_formatter = queue_formatter
19
+ self._connection = connection
20
+
21
+ def configure(self, exchange_name: str, subscribers: list[DomainEventSubscriber[DomainEvent]]) -> None:
22
+ self._create_exchange(exchange_name)
23
+ for subscriber in subscribers:
24
+ self._create_and_bind_queue(subscriber, exchange_name)
25
+
26
+ def _create_exchange(self, exchange_name: str) -> None:
27
+ self._connection.create_exchange(name=exchange_name)
28
+
29
+ def _create_and_bind_queue(self, subscriber: DomainEventSubscriber[DomainEvent], exchange_name: str) -> None:
30
+ routing_keys = self._get_queues_routing_keys_for(subscriber)
31
+ queue_name = self._queue_formatter.format(subscriber)
32
+ self._connection.create_queue(name=queue_name)
33
+
34
+ for routing_key in routing_keys:
35
+ self._connection.bind_queue_to_exchange(
36
+ queue_name=queue_name,
37
+ exchange_name=exchange_name,
38
+ routing_key=routing_key,
39
+ )
40
+
41
+ @staticmethod
42
+ def _get_queues_routing_keys_for(
43
+ subscriber: DomainEventSubscriber[DomainEvent],
44
+ ) -> list[str]:
45
+ return [event.name() for event in subscriber.subscribed_to()]
@@ -0,0 +1,71 @@
1
+ from typing import Callable
2
+
3
+ import pika
4
+ from pika.adapters.blocking_connection import BlockingChannel
5
+
6
+ from {{ general.source_name }}{{ "shared.domain.event.exchange_type" | resolve_import_path(template.name) }} import ExchangeType
7
+ from {{ general.source_name }}{{ "shared.domain.errors.rabbit_mq_connection_not_established_error" | resolve_import_path(template.name) }} import (
8
+ RabbitMqConnectionNotEstablishedError,
9
+ )
10
+ from {{ general.source_name }}{{ "shared.infra.event.rabbit_mq.rabbit_mq_settings" | resolve_import_path(template.name) }} import (
11
+ RabbitMqSettings,
12
+ )
13
+
14
+
15
+ class RabbitMqConnection:
16
+ _channel: BlockingChannel | None
17
+ _connection: pika.BlockingConnection | None
18
+ _connection_settings: RabbitMqSettings
19
+
20
+ def __init__(self, connection_settings: RabbitMqSettings) -> None:
21
+ self._connection_settings = connection_settings
22
+ self._connection = None
23
+ self._channel = None
24
+ self.open_connection()
25
+
26
+ def open_connection(self) -> None:
27
+ credentials = pika.PlainCredentials(
28
+ username=self._connection_settings.user,
29
+ password=self._connection_settings.password,
30
+ )
31
+ self._connection = pika.BlockingConnection(
32
+ parameters=pika.ConnectionParameters(host=self._connection_settings.host, credentials=credentials)
33
+ )
34
+ self._channel = self._connection.channel()
35
+
36
+ def _ensure_channel_exists(self) -> None:
37
+ if self._channel is None:
38
+ raise RabbitMqConnectionNotEstablishedError
39
+
40
+ def create_exchange(self, name: str) -> None:
41
+ self._ensure_channel_exists()
42
+ self._channel.exchange_declare(exchange=name, exchange_type=ExchangeType.TOPIC) # type: ignore
43
+
44
+ def publish(self, content: str, exchange: str, routing_key: str) -> None:
45
+ self._ensure_channel_exists()
46
+ self._channel.basic_publish( # type: ignore
47
+ exchange=exchange,
48
+ routing_key=routing_key,
49
+ body=content,
50
+ properties=pika.BasicProperties(delivery_mode=pika.DeliveryMode.Persistent),
51
+ )
52
+
53
+ def bind_queue_to_exchange(self, queue_name: str, exchange_name: str, routing_key: str) -> None:
54
+ self._ensure_channel_exists()
55
+ self._channel.queue_bind( # type: ignore
56
+ exchange=exchange_name, queue=queue_name, routing_key=routing_key
57
+ )
58
+
59
+ def create_queue(self, name: str) -> None:
60
+ self._ensure_channel_exists()
61
+ self._channel.queue_declare(queue=name, durable=True) # type: ignore
62
+
63
+ def consume(self, queue_name: str, callback: Callable) -> None:
64
+ self._ensure_channel_exists()
65
+ self._channel.basic_consume( # type: ignore
66
+ queue=queue_name, on_message_callback=callback, auto_ack=False
67
+ )
68
+ self._channel.start_consuming() # type: ignore
69
+
70
+ def close_connection(self) -> None:
71
+ self._channel.close() # type: ignore
@@ -0,0 +1,56 @@
1
+ from pika.adapters.blocking_connection import BlockingChannel
2
+ from pika.spec import Basic, BasicProperties
3
+
4
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event" | resolve_import_path(template.name) }} import DomainEvent
5
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event_subscriber" | resolve_import_path(template.name) }} import (
6
+ DomainEventSubscriber,
7
+ )
8
+ from {{ general.source_name }}{{ "shared.infra.event.domain_event_json_deserializer" | resolve_import_path(template.name) }} import (
9
+ DomainEventJsonDeserializer,
10
+ )
11
+ from {{ general.source_name }}{{ "shared.infra.event.rabbit_mq.rabbit_mq_connection" | resolve_import_path(template.name) }} import (
12
+ RabbitMqConnection,
13
+ )
14
+ from {{ general.source_name }}{{ "shared.infra.event.rabbit_mq.rabbit_mq_queue_formatter" | resolve_import_path(template.name) }} import (
15
+ RabbitMqQueueFormatter,
16
+ )
17
+
18
+
19
+ class RabbitMqConsumer:
20
+ _queue_formatter: RabbitMqQueueFormatter
21
+ _subscriber: DomainEventSubscriber[DomainEvent]
22
+ _client: RabbitMqConnection
23
+
24
+ def __init__(
25
+ self,
26
+ client: RabbitMqConnection,
27
+ subscriber: DomainEventSubscriber[DomainEvent],
28
+ queue_formatter: RabbitMqQueueFormatter,
29
+ ) -> None:
30
+ self._queue_formatter = queue_formatter
31
+ self._subscriber = subscriber
32
+ self._client = client
33
+ self._event_deserializer = DomainEventJsonDeserializer(subscriber=subscriber)
34
+
35
+ def _on_call(
36
+ self,
37
+ channel: BlockingChannel,
38
+ method: Basic.Deliver,
39
+ properties: BasicProperties,
40
+ body: bytes,
41
+ ) -> None:
42
+ event = self._deserialize_event(body)
43
+ self._subscriber.on(event)
44
+ channel.basic_ack(delivery_tag=method.delivery_tag)
45
+
46
+ def start_consuming(self) -> None:
47
+ self._client.consume(
48
+ queue_name=self._queue_formatter.format(self._subscriber),
49
+ callback=self._on_call,
50
+ )
51
+
52
+ def stop_consuming(self) -> None:
53
+ self._client.close_connection()
54
+
55
+ def _deserialize_event(self, body: bytes) -> DomainEvent:
56
+ return self._event_deserializer.deserialize(body)
@@ -0,0 +1,26 @@
1
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event" | resolve_import_path(template.name) }} import DomainEvent
2
+ from {{ general.source_name }}{{ "shared.domain.event.event_bus" | resolve_import_path(template.name) }} import EventBus
3
+ from {{ general.source_name }}{{ "shared.infra.event.domain_event_json_serializer" | resolve_import_path(template.name) }} import (
4
+ DomainEventJsonSerializer,
5
+ )
6
+ from {{ general.source_name }}{{ "shared.infra.event.rabbit_mq.rabbit_mq_connection" | resolve_import_path(template.name) }} import (
7
+ RabbitMqConnection,
8
+ )
9
+
10
+
11
+ class RabbitMqEventBus(EventBus):
12
+ def __init__(self, client: RabbitMqConnection, exchange_name: str) -> None:
13
+ self._client = client
14
+ self._exchange_name = exchange_name
15
+ self._event_serializer = DomainEventJsonSerializer()
16
+
17
+ def publish(self, events: list[DomainEvent]) -> None:
18
+ for event in events:
19
+ self._client.publish(
20
+ content=self._serialize_event(event),
21
+ exchange=self._exchange_name,
22
+ routing_key=event.name(),
23
+ )
24
+
25
+ def _serialize_event(self, event: DomainEvent) -> str:
26
+ return self._event_serializer.serialize(event)
@@ -0,0 +1,21 @@
1
+ import re
2
+
3
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event" | resolve_import_path(template.name) }} import DomainEvent
4
+ from {{ general.source_name }}{{ "shared.domain.event.domain_event_subscriber" | resolve_import_path(template.name) }} import (
5
+ DomainEventSubscriber,
6
+ )
7
+
8
+
9
+ class RabbitMqQueueFormatter:
10
+ _bounded_context: str
11
+ CAMEL_CASE_TO_SNAKE_CASE_PATTERN = r"(?<!^)(?=[A-Z])"
12
+
13
+ def __init__(self, bounded_context: str) -> None:
14
+ self._bounded_context = bounded_context
15
+
16
+ def format(self, subscriber: DomainEventSubscriber[DomainEvent]) -> str:
17
+ unformatted_subscriber_name = subscriber.__class__.__name__
18
+ formatted_subscriber_name = re.sub(
19
+ self.CAMEL_CASE_TO_SNAKE_CASE_PATTERN, "_", unformatted_subscriber_name
20
+ ).lower()
21
+ return f"{self._bounded_context}.{formatted_subscriber_name}"
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(kw_only=True)
5
+ class RabbitMqSettings:
6
+ user: str
7
+ password: str
8
+ host: str
@@ -0,0 +1,13 @@
1
+ from abc import ABC
2
+
3
+
4
+ class BaseError(Exception, ABC):
5
+ """Base class for all controlled errors in the application."""
6
+
7
+ def __init__(self, message: str) -> None:
8
+ self._message = message
9
+ super().__init__(self._message)
10
+
11
+ @property
12
+ def message(self) -> str:
13
+ return self._message
@@ -0,0 +1,6 @@
1
+ from {{ general.source_name }}{{ "shared.domain.errors.base_error" | resolve_import_path(template.name) }} import BaseError
2
+
3
+
4
+ class DomainError(BaseError):
5
+ ...
6
+
@@ -0,0 +1,6 @@
1
+ from {{ general.source_name }}{{ "shared.domain.errors.domain_error" | resolve_import_path(template.name) }} import DomainError
2
+
3
+
4
+ class DomainEventTypeNotFoundError(DomainError):
5
+ def __init__(self, name: str) -> None:
6
+ super().__init__(message=f"Event type {name} not found among subscriber.")
@@ -0,0 +1,7 @@
1
+ from {{ general.source_name }}{{ "shared.domain.errors.domain_error" | resolve_import_path(template.name) }} import DomainError
2
+
3
+
4
+
5
+ class RabbitMqConnectionNotEstablishedError(DomainError):
6
+ def __init__(self) -> None:
7
+ super().__init__(message="RabbitMQ connection not established.")
@@ -0,0 +1,6 @@
1
+ from {{ general.source_name }}{{ "shared.domain.errors.domain_error" | resolve_import_path(template.name) }} import DomainError
2
+
3
+
4
+ class RequiredValueError(DomainError):
5
+ def __init__(self) -> None:
6
+ super().__init__(message="Value is required, can't be None")
@@ -0,0 +1,74 @@
1
+ from fastapi import FastAPI
2
+ {% if "logger" in template.built_in_features %}
3
+ from fastapi.errors import RequestValidationError
4
+ {% endif %}
5
+ {% if "value_objects" in template.built_in_features %}
6
+ from sindripy.value_objects import SindriValidationError
7
+ {% endif %}
8
+ {% if template.name == template_types.STANDARD %}
9
+ {% if "logger" in template.built_in_features %}
10
+ from {{ general.source_name }}.api.handlers.error_handlers import (
11
+ unexpected_exception_handler,
12
+ domain_error_handler,
13
+ validation_error_handler,
14
+ {% if "value_objects" in template.built_in_features %}sindri_validation_error_handler,{% endif %}
15
+ )
16
+ {% else %}
17
+ from {{ general.source_name }}.api.handlers.error_handlers import (
18
+ unexpected_exception_handler,
19
+ domain_error_handler,
20
+ {% if "value_objects" in template.built_in_features %}sindri_validation_error_handler,{% endif %}
21
+ )
22
+ {% endif %}
23
+ {% else %}
24
+ {% if "logger" in template.built_in_features %}
25
+ from {{ general.source_name }}.delivery.api.handlers.error_handlers import (
26
+ unexpected_exception_handler,
27
+ domain_error_handler,
28
+ validation_error_handler,
29
+ {% if "value_objects" in template.built_in_features %}sindri_validation_error_handler,{% endif %}
30
+ )
31
+ {% else %}
32
+ from {{ general.source_name }}.delivery.api.handlers.error_handlers import (
33
+ unexpected_exception_handler,
34
+ domain_error_handler,
35
+ {% if "value_objects" in template.built_in_features %}sindri_validation_error_handler,{% endif %}
36
+ )
37
+ {% endif %}
38
+ {% endif %}
39
+
40
+ {% if ["async_alembic"] | is_in(template.built_in_features) %}
41
+ {% if template.name == template_types.STANDARD %}
42
+ from {{ general.source_name }}.api.lifespan import lifespan
43
+ {% else %}
44
+ from {{ general.source_name }}.delivery.api.lifespan import lifespan
45
+ {% endif %}
46
+ {% endif %}
47
+ from {{ general.source_name }}{{ "shared.domain.errors.domain_error" | resolve_import_path(template.name) }} import DomainError
48
+ {% if "logger" in template.built_in_features %}
49
+ from {{ general.source_name }}{{ "shared.infra.logger.file_logger" | resolve_import_path(template.name) }} import create_file_logger
50
+ {% if template.name == template_types.STANDARD %}
51
+ from {{ general.source_name }}.api.middleare.fast_api_log_middleware import FastapiLogMiddleware
52
+ {% else %}
53
+ from {{ general.source_name }}.delivery.api.middleare.fast_api_log_middleware import FastapiLogMiddleware
54
+ {% endif %}
55
+ {% endif %}
56
+
57
+
58
+ {% if ["async_alembic"] | is_in(template.built_in_features) %}
59
+ app = FastAPI(lifespan=lifespan)
60
+ {% else %}
61
+ app = FastAPI()
62
+ {% endif %}
63
+
64
+ {% if "logger" in template.built_in_features %}
65
+ logger = create_file_logger(name="{{ general.slug }}")
66
+
67
+ app.add_middleware(FastapiLogMiddleware, logger=logger)
68
+ app.add_exception_handler(RequestValidationError, validation_error_handler)
69
+ {% endif %}
70
+ {% if "value_objects" in template.built_in_features %}
71
+ app.add_exception_handler(SindriValidationError, sindri_validation_error_handler)
72
+ {% endif %}
73
+ app.add_exception_handler(Exception, unexpected_exception_handler)
74
+ app.add_exception_handler(DomainError, domain_error_handler)
@@ -0,0 +1,88 @@
1
+ from fastapi import Request
2
+ from fastapi.responses import JSONResponse
3
+ {% if "logger" in template.built_in_features %}
4
+ from fastapi.errors import RequestValidationError
5
+ from fastapi.exception_handlers import request_validation_exception_handler
6
+ from {{ general.source_name }}{{ "shared.infra.logger.file_logger" | resolve_import_path(template.name) }} import create_file_logger
7
+ {% endif %}
8
+ from {{ general.source_name }}{{ "shared.infra.http.error_response" | resolve_import_path(template.name) }} import InternalServerError, UnprocessableEntityError
9
+ from {{ general.source_name }}{{ "shared.domain.errors.domain_error" | resolve_import_path(template.name) }} import DomainError
10
+ {% if "value_objects" in template.built_in_features %}
11
+ from sindripy.value_objects import SindriValidationError
12
+ {% endif %}
13
+
14
+ {% if "logger" in template.built_in_features %}
15
+ logger = create_file_logger(name="{{ general.slug }}")
16
+
17
+ async def unexpected_exception_handler(request: Request, exc: Exception) -> JSONResponse:
18
+ logger.error(
19
+ message=f"error - {request.url.path}",
20
+ details={
21
+ "error": {
22
+ "message": str(exc),
23
+ },
24
+ "method": request.method,
25
+ "source": request.url.path,
26
+ },
27
+ )
28
+ return InternalServerError().as_json()
29
+
30
+
31
+ async def domain_error_handler(request: Request, exc: DomainError) -> JSONResponse:
32
+ logger.error(
33
+ message=f"error - {request.url.path}",
34
+ details={
35
+ "error": {"message": exc.message},
36
+ "method": request.method,
37
+ "source": request.url.path,
38
+ },
39
+ )
40
+ return UnprocessableEntityError().as_json()
41
+
42
+
43
+ async def validation_error_handler(
44
+ request: Request,
45
+ exc: RequestValidationError,
46
+ ) -> JSONResponse:
47
+ logger.error(
48
+ message=f"error - {request.url.path}",
49
+ details={
50
+ "error": {"message": str(exc)},
51
+ "method": request.method,
52
+ "source": request.url.path,
53
+ },
54
+ )
55
+ return await request_validation_exception_handler(request, exc)
56
+
57
+ {% if "value_objects" in template.built_in_features %}
58
+ async def sindri_validation_error_handler(
59
+ request: Request,
60
+ exc: SindriValidationError,
61
+ ) -> JSONResponse:
62
+ logger.error(
63
+ message=f"error - {request.url.path}",
64
+ details={
65
+ "error": {"message": str(exc)},
66
+ "method": request.method,
67
+ "source": request.url.path,
68
+ },
69
+ )
70
+ return UnprocessableEntityError().as_json()
71
+ {% endif %}
72
+ {% else %}
73
+ async def unexpected_exception_handler(_: Request, __: Exception) -> JSONResponse:
74
+ return InternalServerError().as_json()
75
+
76
+
77
+ async def domain_error_handler(_: Request, __: DomainError) -> JSONResponse:
78
+ return UnprocessableEntityError().as_json()
79
+
80
+
81
+ {% if "value_objects" in template.built_in_features %}
82
+ async def sindri_validation_error_handler(
83
+ _: Request,
84
+ __: SindriValidationError,
85
+ ) -> JSONResponse:
86
+ return UnprocessableEntityError().as_json()
87
+ {% endif %}
88
+ {% endif %}