instant-python 0.0.1__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 (149) hide show
  1. instant_python/__init__.py +0 -0
  2. instant_python/cli.py +11 -0
  3. instant_python/folder_cli.py +50 -0
  4. instant_python/installer/__init__.py +0 -0
  5. instant_python/installer/dependency_manager.py +15 -0
  6. instant_python/installer/dependency_manager_factory.py +14 -0
  7. instant_python/installer/git_configurer.py +47 -0
  8. instant_python/installer/installer.py +18 -0
  9. instant_python/installer/managers.py +7 -0
  10. instant_python/installer/operating_systems.py +7 -0
  11. instant_python/installer/pdm_manager.py +72 -0
  12. instant_python/installer/uv_manager.py +73 -0
  13. instant_python/project_cli.py +100 -0
  14. instant_python/project_generator/__init__.py +0 -0
  15. instant_python/project_generator/custom_template_manager.py +20 -0
  16. instant_python/project_generator/default_template_manager.py +45 -0
  17. instant_python/project_generator/directory.py +28 -0
  18. instant_python/project_generator/file.py +20 -0
  19. instant_python/project_generator/folder_tree.py +40 -0
  20. instant_python/project_generator/jinja_custom_filters.py +18 -0
  21. instant_python/project_generator/node.py +14 -0
  22. instant_python/project_generator/project_generator.py +31 -0
  23. instant_python/project_generator/template_manager.py +7 -0
  24. instant_python/question_prompter/__init__.py +0 -0
  25. instant_python/question_prompter/question/__init__.py +0 -0
  26. instant_python/question_prompter/question/boolean_question.py +13 -0
  27. instant_python/question_prompter/question/choice_question.py +18 -0
  28. instant_python/question_prompter/question/conditional_question.py +25 -0
  29. instant_python/question_prompter/question/dependencies_question.py +43 -0
  30. instant_python/question_prompter/question/free_text_question.py +13 -0
  31. instant_python/question_prompter/question/multiple_choice_question.py +13 -0
  32. instant_python/question_prompter/question/question.py +15 -0
  33. instant_python/question_prompter/question_wizard.py +15 -0
  34. instant_python/question_prompter/step/__init__.py +0 -0
  35. instant_python/question_prompter/step/dependencies_step.py +20 -0
  36. instant_python/question_prompter/step/general_custom_template_project_step.py +45 -0
  37. instant_python/question_prompter/step/general_project_step.py +50 -0
  38. instant_python/question_prompter/step/git_step.py +23 -0
  39. instant_python/question_prompter/step/steps.py +16 -0
  40. instant_python/question_prompter/step/template_step.py +63 -0
  41. instant_python/question_prompter/template_types.py +7 -0
  42. instant_python/question_prompter/user_requirements.py +39 -0
  43. instant_python/templates/__init__.py +0 -0
  44. instant_python/templates/boilerplate/.gitignore +164 -0
  45. instant_python/templates/boilerplate/.pre-commit-config.yml +33 -0
  46. instant_python/templates/boilerplate/.python-version +1 -0
  47. instant_python/templates/boilerplate/LICENSE +896 -0
  48. instant_python/templates/boilerplate/event_bus/__init__.py +0 -0
  49. instant_python/templates/boilerplate/event_bus/aggregate_root.py +19 -0
  50. instant_python/templates/boilerplate/event_bus/domain_event.py +15 -0
  51. instant_python/templates/boilerplate/event_bus/domain_event_json_deserializer.py +28 -0
  52. instant_python/templates/boilerplate/event_bus/domain_event_json_serializer.py +17 -0
  53. instant_python/templates/boilerplate/event_bus/domain_event_subscriber.py +15 -0
  54. instant_python/templates/boilerplate/event_bus/event_bus.py +10 -0
  55. instant_python/templates/boilerplate/event_bus/exchange_type.py +7 -0
  56. instant_python/templates/boilerplate/event_bus/mock_event_bus.py +18 -0
  57. instant_python/templates/boilerplate/event_bus/rabbit_mq_configurer.py +54 -0
  58. instant_python/templates/boilerplate/event_bus/rabbit_mq_connection.py +77 -0
  59. instant_python/templates/boilerplate/event_bus/rabbit_mq_consumer.py +58 -0
  60. instant_python/templates/boilerplate/event_bus/rabbit_mq_event_bus.py +28 -0
  61. instant_python/templates/boilerplate/event_bus/rabbit_mq_queue_formatter.py +22 -0
  62. instant_python/templates/boilerplate/event_bus/rabbit_mq_settings.py +8 -0
  63. instant_python/templates/boilerplate/exceptions/__init__.py +0 -0
  64. instant_python/templates/boilerplate/exceptions/domain_error.py +17 -0
  65. instant_python/templates/boilerplate/exceptions/domain_event_type_not_found_error.py +17 -0
  66. instant_python/templates/boilerplate/exceptions/incorrect_value_type_error.py +21 -0
  67. instant_python/templates/boilerplate/exceptions/invalid_id_format_error.py +17 -0
  68. instant_python/templates/boilerplate/exceptions/invalid_negative_value_error.py +17 -0
  69. instant_python/templates/boilerplate/exceptions/required_value_error.py +17 -0
  70. instant_python/templates/boilerplate/fastapi/__init__.py +0 -0
  71. instant_python/templates/boilerplate/fastapi/application.py +25 -0
  72. instant_python/templates/boilerplate/fastapi/http_response.py +45 -0
  73. instant_python/templates/boilerplate/fastapi/lifespan.py +14 -0
  74. instant_python/templates/boilerplate/fastapi/status_code.py +9 -0
  75. instant_python/templates/boilerplate/github/action.yml +22 -0
  76. instant_python/templates/boilerplate/github/test_lint.yml +36 -0
  77. instant_python/templates/boilerplate/logger/__init__.py +0 -0
  78. instant_python/templates/boilerplate/logger/json_formatter.py +16 -0
  79. instant_python/templates/boilerplate/logger/logger.py +39 -0
  80. instant_python/templates/boilerplate/mypy.ini +41 -0
  81. instant_python/templates/boilerplate/persistence/__init__.py +0 -0
  82. instant_python/templates/boilerplate/persistence/alembic_migrator.py +20 -0
  83. instant_python/templates/boilerplate/persistence/async/README.md +1 -0
  84. instant_python/templates/boilerplate/persistence/async/__init__.py +0 -0
  85. instant_python/templates/boilerplate/persistence/async/alembic.ini +124 -0
  86. instant_python/templates/boilerplate/persistence/async/async_engine_fixture.py +21 -0
  87. instant_python/templates/boilerplate/persistence/async/env.py +95 -0
  88. instant_python/templates/boilerplate/persistence/async/models_metadata.py +11 -0
  89. instant_python/templates/boilerplate/persistence/async/postgres_settings.py +15 -0
  90. instant_python/templates/boilerplate/persistence/async/script.py.mako +26 -0
  91. instant_python/templates/boilerplate/persistence/async/sqlalchemy_repository.py +30 -0
  92. instant_python/templates/boilerplate/persistence/base.py +4 -0
  93. instant_python/templates/boilerplate/persistence/synchronous/__init__.py +0 -0
  94. instant_python/templates/boilerplate/persistence/synchronous/session_maker.py +22 -0
  95. instant_python/templates/boilerplate/persistence/synchronous/sqlalchemy_repository.py +35 -0
  96. instant_python/templates/boilerplate/pyproject.toml +29 -0
  97. instant_python/templates/boilerplate/pytest.ini +10 -0
  98. instant_python/templates/boilerplate/random_generator.py +9 -0
  99. instant_python/templates/boilerplate/scripts/add_dependency.sh +37 -0
  100. instant_python/templates/boilerplate/scripts/create_aggregate.py +33 -0
  101. instant_python/templates/boilerplate/scripts/insert_template.py +90 -0
  102. instant_python/templates/boilerplate/scripts/integration.sh +39 -0
  103. instant_python/templates/boilerplate/scripts/local_setup.sh +15 -0
  104. instant_python/templates/boilerplate/scripts/makefile +137 -0
  105. instant_python/templates/boilerplate/scripts/post-merge +11 -0
  106. instant_python/templates/boilerplate/scripts/pre-commit +4 -0
  107. instant_python/templates/boilerplate/scripts/pre-push +6 -0
  108. instant_python/templates/boilerplate/scripts/remove_dependency.sh +36 -0
  109. instant_python/templates/boilerplate/scripts/unit.sh +40 -0
  110. instant_python/templates/boilerplate/value_object/__init__.py +0 -0
  111. instant_python/templates/boilerplate/value_object/int_value_object.py +11 -0
  112. instant_python/templates/boilerplate/value_object/string_value_object.py +19 -0
  113. instant_python/templates/boilerplate/value_object/uuid.py +17 -0
  114. instant_python/templates/boilerplate/value_object/value_object.py +21 -0
  115. instant_python/templates/project_structure/alembic_migrator.yml.j2 +3 -0
  116. instant_python/templates/project_structure/async_alembic.yml.j2 +20 -0
  117. instant_python/templates/project_structure/async_sqlalchemy.yml.j2 +17 -0
  118. instant_python/templates/project_structure/clean_architecture/main_structure.yml.j2 +25 -0
  119. instant_python/templates/project_structure/clean_architecture/source.yml.j2 +51 -0
  120. instant_python/templates/project_structure/clean_architecture/test.yml.j2 +23 -0
  121. instant_python/templates/project_structure/domain_driven_design/bounded_context.yml.j2 +20 -0
  122. instant_python/templates/project_structure/domain_driven_design/main_structure.yml.j2 +25 -0
  123. instant_python/templates/project_structure/domain_driven_design/source.yml.j2 +55 -0
  124. instant_python/templates/project_structure/domain_driven_design/test.yml.j2 +26 -0
  125. instant_python/templates/project_structure/event_bus_domain.yml.j2 +26 -0
  126. instant_python/templates/project_structure/event_bus_infra.yml.j2 +32 -0
  127. instant_python/templates/project_structure/fastapi_app.yml.j2 +10 -0
  128. instant_python/templates/project_structure/fastapi_infra.yml.j2 +10 -0
  129. instant_python/templates/project_structure/github_action.yml.j2 +18 -0
  130. instant_python/templates/project_structure/gitignore.yml.j2 +2 -0
  131. instant_python/templates/project_structure/license.yml.j2 +2 -0
  132. instant_python/templates/project_structure/logger.yml.j2 +10 -0
  133. instant_python/templates/project_structure/macros.j2 +6 -0
  134. instant_python/templates/project_structure/makefile.yml.j2 +38 -0
  135. instant_python/templates/project_structure/mypy.yml.j2 +3 -0
  136. instant_python/templates/project_structure/pre_commit.yml.j2 +3 -0
  137. instant_python/templates/project_structure/pyproject.yml.j2 +3 -0
  138. instant_python/templates/project_structure/pytest.yml.j2 +3 -0
  139. instant_python/templates/project_structure/python_version.yml.j2 +2 -0
  140. instant_python/templates/project_structure/standard_project/main_structure.yml.j2 +25 -0
  141. instant_python/templates/project_structure/standard_project/source.yml.j2 +30 -0
  142. instant_python/templates/project_structure/standard_project/test.yml.j2 +16 -0
  143. instant_python/templates/project_structure/synchronous_sqlalchemy.yml.j2 +17 -0
  144. instant_python/templates/project_structure/value_objects.yml.j2 +35 -0
  145. instant_python-0.0.1.dist-info/METADATA +276 -0
  146. instant_python-0.0.1.dist-info/RECORD +149 -0
  147. instant_python-0.0.1.dist-info/WHEEL +4 -0
  148. instant_python-0.0.1.dist-info/entry_points.txt +2 -0
  149. instant_python-0.0.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,19 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+
3
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event import DomainEvent
4
+
5
+
6
+ class AggregateRoot:
7
+ _domain_events: list[DomainEvent]
8
+
9
+ def __init__(self) -> None:
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,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,28 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ import json
3
+
4
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event import DomainEvent
5
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event_subscriber import (
6
+ DomainEventSubscriber,
7
+ )
8
+ from {{ source_name }}.{{ template_domain_import }}.exceptions.domain_event_type_not_found import (
9
+ DomainEventTypeNotFound,
10
+ )
11
+
12
+
13
+ class DomainEventJsonDeserializer:
14
+ _events_mapping: dict[str, type[DomainEvent]]
15
+
16
+ def __init__(self, subscriber: DomainEventSubscriber[DomainEvent]) -> None:
17
+ self._events_mapping = {
18
+ event.name(): event for event in subscriber.subscribed_to()
19
+ }
20
+
21
+ def deserialize(self, body: bytes) -> DomainEvent:
22
+ content = json.loads(body)
23
+ event_class = self._events_mapping.get(content["data"]["type"])
24
+
25
+ if not event_class:
26
+ raise DomainEventTypeNotFound(content["data"]["type"])
27
+
28
+ return event_class(**content["data"]["attributes"])
@@ -0,0 +1,17 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ import json
3
+
4
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event import DomainEvent
5
+
6
+
7
+ class DomainEventJsonSerializer:
8
+ @staticmethod
9
+ def serialize(event: DomainEvent) -> str:
10
+ body = {
11
+ "data": {
12
+ "id": event.id,
13
+ "type": event.name(),
14
+ "attributes": event.to_dict(),
15
+ }
16
+ }
17
+ return json.dumps(body)
@@ -0,0 +1,15 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ from abc import ABC, abstractmethod
3
+
4
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event 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
@@ -0,0 +1,10 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ from abc import ABC, abstractmethod
3
+
4
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event import DomainEvent
5
+
6
+
7
+ class EventBus(ABC):
8
+ @abstractmethod
9
+ def publish(self, events: list[DomainEvent]) -> None:
10
+ raise NotImplementedError
@@ -0,0 +1,7 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class ExchangeType(StrEnum):
5
+ TOPIC = "topic"
6
+ DIRECT = "direct"
7
+ FANOUT = "fanout"
@@ -0,0 +1,18 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ from unittest.mock import AsyncMock
3
+
4
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event import DomainEvent
5
+ from {{ source_name }}.{{ template_domain_import }}.event.event_bus import EventBus
6
+
7
+
8
+ class MockEventBus(EventBus):
9
+
10
+ def __init__(self) -> None:
11
+ self._mock_publish = AsyncMock()
12
+
13
+ async def publish(self, events: list[DomainEvent]) -> None:
14
+ await self._mock_publish(events)
15
+
16
+ def should_have_published(self, event: DomainEvent) -> None:
17
+ self._mock_publish.assert_awaited_once_with([event])
18
+ self._mock_publish.reset_mock()
@@ -0,0 +1,54 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ {% set template_infra_import = "shared.infra"|compute_base_path(template) %}
3
+
4
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event import DomainEvent
5
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event_subscriber import (
6
+ DomainEventSubscriber,
7
+ )
8
+ from {{ source_name }}.{{ template_infra_import }}.event.rabbit_mq.rabbit_mq_connection import (
9
+ RabbitMqConnection,
10
+ )
11
+ from {{ source_name }}.{{ template_infra_import }}.event.rabbit_mq.rabbit_mq_queue_formatter import (
12
+ RabbitMqQueueFormatter,
13
+ )
14
+
15
+
16
+ class RabbitMqConfigurer:
17
+ _queue_formatter: RabbitMqQueueFormatter
18
+ _connection: RabbitMqConnection
19
+
20
+ def __init__(
21
+ self, connection: RabbitMqConnection, queue_formatter: RabbitMqQueueFormatter
22
+ ) -> None:
23
+ self._queue_formatter = queue_formatter
24
+ self._connection = connection
25
+
26
+ def configure(
27
+ self, exchange_name: str, subscribers: list[DomainEventSubscriber[DomainEvent]]
28
+ ) -> None:
29
+ self._create_exchange(exchange_name)
30
+ for subscriber in subscribers:
31
+ self._create_and_bind_queue(subscriber, exchange_name)
32
+
33
+ def _create_exchange(self, exchange_name: str) -> None:
34
+ self._connection.create_exchange(name=exchange_name)
35
+
36
+ def _create_and_bind_queue(
37
+ self, subscriber: DomainEventSubscriber[DomainEvent], exchange_name: str
38
+ ) -> None:
39
+ routing_keys = self._get_queues_routing_keys_for(subscriber)
40
+ queue_name = self._queue_formatter.format(subscriber)
41
+ self._connection.create_queue(name=queue_name)
42
+
43
+ for routing_key in routing_keys:
44
+ self._connection.bind_queue_to_exchange(
45
+ queue_name=queue_name,
46
+ exchange_name=exchange_name,
47
+ routing_key=routing_key,
48
+ )
49
+
50
+ @staticmethod
51
+ def _get_queues_routing_keys_for(
52
+ subscriber: DomainEventSubscriber[DomainEvent],
53
+ ) -> list[str]:
54
+ return [event.name() for event in subscriber.subscribed_to()]
@@ -0,0 +1,77 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ {% set template_infra_import = "shared.infra"|compute_base_path(template) %}
3
+ from typing import Callable
4
+
5
+ import pika
6
+ from pika.adapters.blocking_connection import BlockingChannel
7
+
8
+ from {{ source_name }}.{{ template_domain_import }}.event.exchange_type import ExchangeType
9
+ from {{ source_name }}.{{ template_domain_import }}.exceptions.rabbit_mq_connection_not_established_error import (
10
+ RabbitMqConnectionNotEstablishedError,
11
+ )
12
+ from {{ source_name }}.{{ template_infra_import }}.event.rabbit_mq.rabbit_mq_settings import (
13
+ RabbitMqSettings,
14
+ )
15
+
16
+
17
+ class RabbitMqConnection:
18
+ _channel: BlockingChannel | None
19
+ _connection: pika.BlockingConnection | None
20
+ _connection_settings: RabbitMqSettings
21
+
22
+ def __init__(self, connection_settings: RabbitMqSettings) -> None:
23
+ self._connection_settings = connection_settings
24
+ self._connection = None
25
+ self._channel = None
26
+ self.open_connection()
27
+
28
+ def open_connection(self) -> None:
29
+ credentials = pika.PlainCredentials(
30
+ username=self._connection_settings.user,
31
+ password=self._connection_settings.password,
32
+ )
33
+ self._connection = pika.BlockingConnection(
34
+ parameters=pika.ConnectionParameters(
35
+ host=self._connection_settings.host, credentials=credentials
36
+ )
37
+ )
38
+ self._channel = self._connection.channel()
39
+
40
+ def _ensure_channel_exists(self) -> None:
41
+ if self._channel is None:
42
+ raise RabbitMqConnectionNotEstablishedError
43
+
44
+ def create_exchange(self, name: str) -> None:
45
+ self._ensure_channel_exists()
46
+ self._channel.exchange_declare(exchange=name, exchange_type=ExchangeType.TOPIC) # type: ignore
47
+
48
+ def publish(self, content: str, exchange: str, routing_key: str) -> None:
49
+ self._ensure_channel_exists()
50
+ self._channel.basic_publish( # type: ignore
51
+ exchange=exchange,
52
+ routing_key=routing_key,
53
+ body=content,
54
+ properties=pika.BasicProperties(delivery_mode=pika.DeliveryMode.Persistent),
55
+ )
56
+
57
+ def bind_queue_to_exchange(
58
+ self, queue_name: str, exchange_name: str, routing_key: str
59
+ ) -> None:
60
+ self._ensure_channel_exists()
61
+ self._channel.queue_bind( # type: ignore
62
+ exchange=exchange_name, queue=queue_name, routing_key=routing_key
63
+ )
64
+
65
+ def create_queue(self, name: str) -> None:
66
+ self._ensure_channel_exists()
67
+ self._channel.queue_declare(queue=name, durable=True) # type: ignore
68
+
69
+ def consume(self, queue_name: str, callback: Callable) -> None:
70
+ self._ensure_channel_exists()
71
+ self._channel.basic_consume( # type: ignore
72
+ queue=queue_name, on_message_callback=callback, auto_ack=False
73
+ )
74
+ self._channel.start_consuming() # type: ignore
75
+
76
+ def close_connection(self) -> None:
77
+ self._channel.close() # type: ignore
@@ -0,0 +1,58 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ {% set template_infra_import = "shared.infra"|compute_base_path(template) %}
3
+ from pika.adapters.blocking_connection import BlockingChannel
4
+ from pika.spec import Basic, BasicProperties
5
+
6
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event import DomainEvent
7
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event_subscriber import (
8
+ DomainEventSubscriber,
9
+ )
10
+ from {{ source_name }}.{{ template_infra_import }}.event.domain_event_json_deserializer import (
11
+ DomainEventJsonDeserializer,
12
+ )
13
+ from {{ source_name }}.{{ template_infra_import }}.event.rabbit_mq.rabbit_mq_connection import (
14
+ RabbitMqConnection,
15
+ )
16
+ from {{ source_name }}.{{ template_infra_import }}.event.rabbit_mq.rabbit_mq_queue_formatter import (
17
+ RabbitMqQueueFormatter,
18
+ )
19
+
20
+
21
+ class RabbitMqConsumer:
22
+ _queue_formatter: RabbitMqQueueFormatter
23
+ _subscriber: DomainEventSubscriber[DomainEvent]
24
+ _client: RabbitMqConnection
25
+
26
+ def __init__(
27
+ self,
28
+ client: RabbitMqConnection,
29
+ subscriber: DomainEventSubscriber[DomainEvent],
30
+ queue_formatter: RabbitMqQueueFormatter,
31
+ ) -> None:
32
+ self._queue_formatter = queue_formatter
33
+ self._subscriber = subscriber
34
+ self._client = client
35
+ self._event_deserializer = DomainEventJsonDeserializer(subscriber=subscriber)
36
+
37
+ def _on_call(
38
+ self,
39
+ channel: BlockingChannel,
40
+ method: Basic.Deliver,
41
+ properties: BasicProperties,
42
+ body: bytes,
43
+ ) -> None:
44
+ event = self._deserialize_event(body)
45
+ self._subscriber.on(event)
46
+ channel.basic_ack(delivery_tag=method.delivery_tag)
47
+
48
+ def start_consuming(self) -> None:
49
+ self._client.consume(
50
+ queue_name=self._queue_formatter.format(self._subscriber),
51
+ callback=self._on_call,
52
+ )
53
+
54
+ def stop_consuming(self) -> None:
55
+ self._client.close_connection()
56
+
57
+ def _deserialize_event(self, body: bytes) -> DomainEvent:
58
+ return self._event_deserializer.deserialize(body)
@@ -0,0 +1,28 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ {% set template_infra_import = "shared.infra"|compute_base_path(template) %}
3
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event import DomainEvent
4
+ from {{ source_name }}.{{ template_domain_import }}.event.event_bus import EventBus
5
+ from {{ source_name }}.{{ template_infra_import }}.event.domain_event_json_serializer import (
6
+ DomainEventJsonSerializer,
7
+ )
8
+ from {{ source_name }}.{{ template_infra_import }}.event.rabbit_mq.rabbit_mq_connection import (
9
+ RabbitMqConnection,
10
+ )
11
+
12
+
13
+ class RabbitMqEventBus(EventBus):
14
+ def __init__(self, client: RabbitMqConnection, exchange_name: str) -> None:
15
+ self._client = client
16
+ self._exchange_name = exchange_name
17
+ self._event_serializer = DomainEventJsonSerializer()
18
+
19
+ def publish(self, events: list[DomainEvent]) -> None:
20
+ for event in events:
21
+ self._client.publish(
22
+ content=self._serialize_event(event),
23
+ exchange=self._exchange_name,
24
+ routing_key=event.name(),
25
+ )
26
+
27
+ def _serialize_event(self, event: DomainEvent) -> str:
28
+ return self._event_serializer.serialize(event)
@@ -0,0 +1,22 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ import re
3
+
4
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event import DomainEvent
5
+ from {{ source_name }}.{{ template_domain_import }}.event.domain_event_subscriber import (
6
+ DomainEventSubscriber,
7
+ )
8
+
9
+
10
+ class RabbitMqQueueFormatter:
11
+ _bounded_context: str
12
+ CAMEL_CASE_TO_SNAKE_CASE_PATTERN = r"(?<!^)(?=[A-Z])"
13
+
14
+ def __init__(self, bounded_context: str) -> None:
15
+ self._bounded_context = bounded_context
16
+
17
+ def format(self, subscriber: DomainEventSubscriber[DomainEvent]) -> str:
18
+ unformatted_subscriber_name = subscriber.__class__.__name__
19
+ formatted_subscriber_name = re.sub(
20
+ self.CAMEL_CASE_TO_SNAKE_CASE_PATTERN, "_", unformatted_subscriber_name
21
+ ).lower()
22
+ 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,17 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class DomainError(Exception, ABC):
5
+ @property
6
+ @abstractmethod
7
+ def type(self) -> str: ...
8
+
9
+ @property
10
+ @abstractmethod
11
+ def message(self) -> str: ...
12
+
13
+ def to_dict(self) -> dict:
14
+ return {
15
+ "type": self.type,
16
+ "message": self.message,
17
+ }
@@ -0,0 +1,17 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ from {{ source_name }}.{{ template_domain_import }}.exceptions.domain_error import DomainError
3
+
4
+
5
+ class DomainEventTypeNotFound(DomainError):
6
+ def __init__(self, name: str) -> None:
7
+ self._message = f"Event type {name} not found among subscriber."
8
+ self._type = "domain_event_type_not_found"
9
+ super().__init__(self._message)
10
+
11
+ @property
12
+ def type(self) -> str:
13
+ return self._type
14
+
15
+ @property
16
+ def message(self) -> str:
17
+ return self._message
@@ -0,0 +1,21 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ from typing import TypeVar
3
+
4
+ from {{ source_name }}.{{ template_domain_import }}.exceptions.domain_error import DomainError
5
+
6
+ T = TypeVar("T")
7
+
8
+
9
+ class IncorrectValueTypeError(DomainError):
10
+ def __init__(self, value: T) -> None:
11
+ self._message = f"Value '{value}' is not of type {type(value).__name__}"
12
+ self._type = "incorrect_value_type"
13
+ super().__init__(self._message)
14
+
15
+ @property
16
+ def type(self) -> str:
17
+ return self._type
18
+
19
+ @property
20
+ def message(self) -> str:
21
+ return self._message
@@ -0,0 +1,17 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ from {{ source_name }}.{{ template_domain_import }}.exceptions.domain_error import DomainError
3
+
4
+
5
+ class InvalidIdFormatError(DomainError):
6
+ def __init__(self) -> None:
7
+ self._message = "User id must be a valid UUID"
8
+ self._type = "invalid_id_format"
9
+ super().__init__(self._message)
10
+
11
+ @property
12
+ def type(self) -> str:
13
+ return self._type
14
+
15
+ @property
16
+ def message(self) -> str:
17
+ return self._message
@@ -0,0 +1,17 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ from {{ source_name }}.{{ template_domain_import }}.exceptions.domain_error import DomainError
3
+
4
+
5
+ class InvalidNegativeValueError(DomainError):
6
+ def __init__(self, value: int) -> None:
7
+ self._message = f"Invalid negative value: {value}"
8
+ self._type = "invalid_negative_value"
9
+ super().__init__(self._message)
10
+
11
+ @property
12
+ def type(self) -> str:
13
+ return self._type
14
+
15
+ @property
16
+ def message(self) -> str:
17
+ return self._message
@@ -0,0 +1,17 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ from {{ source_name }}.{{ template_domain_import }}.exceptions.domain_error import DomainError
3
+
4
+
5
+ class RequiredValueError(DomainError):
6
+ def __init__(self) -> None:
7
+ self._message = "Value is required, can't be None"
8
+ self._type = "required_value"
9
+ super().__init__(self._message)
10
+
11
+ @property
12
+ def type(self) -> str:
13
+ return self._type
14
+
15
+ @property
16
+ def message(self) -> str:
17
+ return self._message
@@ -0,0 +1,25 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ {% set template_infra_import = "shared.infra"|compute_base_path(template) %}
3
+ from fastapi import FastAPI, Request
4
+ from fastapi.responses import JSONResponse
5
+
6
+ {% if template == template_types.STANDARD %}
7
+ from {{ source_name }}.api.lifespan import lifespan
8
+ {% else %}
9
+ from {{ source_name }}.delivery.api.lifespan import lifespan
10
+ {% endif %}
11
+ from {{ source_name }}.{{ template_infra_import }}.http.http_response import HttpResponse
12
+ from {{ source_name }}.{{ template_infra_import }}.http.status_code import StatusCode
13
+ from {{ source_name }}.{{ template_domain_import }}.exceptions.domain_error import DomainError
14
+
15
+ app = FastAPI(lifespan=lifespan)
16
+
17
+
18
+ @app.exception_handler(Exception)
19
+ async def unexpected_exception_handler(_: Request, exc: Exception) -> JSONResponse:
20
+ return HttpResponse.internal_error(exc)
21
+
22
+
23
+ @app.exception_handler(DomainError)
24
+ async def domain_error_handler(_: Request, exc: DomainError) -> JSONResponse:
25
+ return HttpResponse.domain_error(exc, status_code=StatusCode.BAD_REQUEST)
@@ -0,0 +1,45 @@
1
+ {% set template_domain_import = "shared.domain"|compute_base_path(template) %}
2
+ {% set template_infra_import = "shared.infra"|compute_base_path(template) %}
3
+ from fastapi.responses import JSONResponse
4
+
5
+ from {{ source_name }}.{{ template_domain_import }}.exceptions.domain_error import DomainError
6
+ from {{ source_name }}.{{ template_infra_import }}.http.status_code import StatusCode
7
+ from {{ source_name }}.{{ template_infra_import }}.log.logger import create_logger
8
+
9
+ logger = create_logger("logger")
10
+
11
+
12
+ class HttpResponse:
13
+ @staticmethod
14
+ def domain_error(error: DomainError, status_code: StatusCode) -> JSONResponse:
15
+ logger.error(
16
+ "error - domain error",
17
+ extra={"extra": {"error": error.to_dict(), "status_code": status_code}},
18
+ )
19
+ return JSONResponse(content={"error": error.to_dict()}, status_code=status_code)
20
+
21
+ @staticmethod
22
+ def internal_error(error: Exception) -> JSONResponse:
23
+ logger.error(
24
+ "error - internal server error",
25
+ extra={
26
+ "extra": {"error": str(error)},
27
+ "status_code": StatusCode.INTERNAL_SERVER_ERROR,
28
+ },
29
+ )
30
+ return JSONResponse(
31
+ content={"error": "Internal server error"},
32
+ status_code=StatusCode.INTERNAL_SERVER_ERROR,
33
+ )
34
+
35
+ @staticmethod
36
+ def created(resource: str) -> JSONResponse:
37
+ logger.info(
38
+ f"resource - {resource}",
39
+ extra={"extra": {"status_code": StatusCode.CREATED}},
40
+ )
41
+ return JSONResponse(content={}, status_code=StatusCode.CREATED)
42
+
43
+ @staticmethod
44
+ def ok(content: dict) -> JSONResponse:
45
+ return JSONResponse(content=content, status_code=StatusCode.OK)
@@ -0,0 +1,14 @@
1
+ {% set template_infra_import = "shared.infra"|compute_base_path(template) %}
2
+ from collections.abc import AsyncGenerator
3
+ from contextlib import asynccontextmanager
4
+
5
+ from fastapi import FastAPI
6
+
7
+ from {{ source_name }}.{{ template_infra_import }}.alembic_migrator import AlembicMigrator
8
+
9
+
10
+ @asynccontextmanager
11
+ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
12
+ migrator = AlembicMigrator()
13
+ await migrator.migrate()
14
+ yield
@@ -0,0 +1,9 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ class StatusCode(IntEnum):
5
+ OK = 200
6
+ CREATED = 201
7
+ BAD_REQUEST = 400
8
+ NOT_FOUND = 404
9
+ INTERNAL_SERVER_ERROR = 500
@@ -0,0 +1,22 @@
1
+ name: Install Python and setup environment
2
+
3
+ inputs:
4
+ python-version:
5
+ description: 'The version of Python to use'
6
+ required: false
7
+ default: {{ python_version }}
8
+ outputs: {}
9
+ runs:
10
+ using: composite
11
+ steps:
12
+ - uses: actions/setup-python@v5
13
+ with:
14
+ python-version: {% raw %}${{ inputs.python-version }}{% endraw %}
15
+
16
+ - name: Install dependency manager
17
+ run: python -m pip install {{ dependency_manager }}
18
+ shell: bash
19
+ - name: Install dependencies
20
+ run: {% if dependency_manager == "uv" %}uv sync --all-groups{% elif dependency_manager == "pdm" %}pdm install {% endif %}
21
+
22
+ shell: bash