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.
- instant_python/__init__.py +0 -0
- instant_python/cli.py +11 -0
- instant_python/folder_cli.py +50 -0
- instant_python/installer/__init__.py +0 -0
- instant_python/installer/dependency_manager.py +15 -0
- instant_python/installer/dependency_manager_factory.py +14 -0
- instant_python/installer/git_configurer.py +47 -0
- instant_python/installer/installer.py +18 -0
- instant_python/installer/managers.py +7 -0
- instant_python/installer/operating_systems.py +7 -0
- instant_python/installer/pdm_manager.py +72 -0
- instant_python/installer/uv_manager.py +73 -0
- instant_python/project_cli.py +100 -0
- instant_python/project_generator/__init__.py +0 -0
- instant_python/project_generator/custom_template_manager.py +20 -0
- instant_python/project_generator/default_template_manager.py +45 -0
- instant_python/project_generator/directory.py +28 -0
- instant_python/project_generator/file.py +20 -0
- instant_python/project_generator/folder_tree.py +40 -0
- instant_python/project_generator/jinja_custom_filters.py +18 -0
- instant_python/project_generator/node.py +14 -0
- instant_python/project_generator/project_generator.py +31 -0
- instant_python/project_generator/template_manager.py +7 -0
- instant_python/question_prompter/__init__.py +0 -0
- instant_python/question_prompter/question/__init__.py +0 -0
- instant_python/question_prompter/question/boolean_question.py +13 -0
- instant_python/question_prompter/question/choice_question.py +18 -0
- instant_python/question_prompter/question/conditional_question.py +25 -0
- instant_python/question_prompter/question/dependencies_question.py +43 -0
- instant_python/question_prompter/question/free_text_question.py +13 -0
- instant_python/question_prompter/question/multiple_choice_question.py +13 -0
- instant_python/question_prompter/question/question.py +15 -0
- instant_python/question_prompter/question_wizard.py +15 -0
- instant_python/question_prompter/step/__init__.py +0 -0
- instant_python/question_prompter/step/dependencies_step.py +20 -0
- instant_python/question_prompter/step/general_custom_template_project_step.py +45 -0
- instant_python/question_prompter/step/general_project_step.py +50 -0
- instant_python/question_prompter/step/git_step.py +23 -0
- instant_python/question_prompter/step/steps.py +16 -0
- instant_python/question_prompter/step/template_step.py +63 -0
- instant_python/question_prompter/template_types.py +7 -0
- instant_python/question_prompter/user_requirements.py +39 -0
- instant_python/templates/__init__.py +0 -0
- instant_python/templates/boilerplate/.gitignore +164 -0
- instant_python/templates/boilerplate/.pre-commit-config.yml +33 -0
- instant_python/templates/boilerplate/.python-version +1 -0
- instant_python/templates/boilerplate/LICENSE +896 -0
- instant_python/templates/boilerplate/event_bus/__init__.py +0 -0
- instant_python/templates/boilerplate/event_bus/aggregate_root.py +19 -0
- instant_python/templates/boilerplate/event_bus/domain_event.py +15 -0
- instant_python/templates/boilerplate/event_bus/domain_event_json_deserializer.py +28 -0
- instant_python/templates/boilerplate/event_bus/domain_event_json_serializer.py +17 -0
- instant_python/templates/boilerplate/event_bus/domain_event_subscriber.py +15 -0
- instant_python/templates/boilerplate/event_bus/event_bus.py +10 -0
- instant_python/templates/boilerplate/event_bus/exchange_type.py +7 -0
- instant_python/templates/boilerplate/event_bus/mock_event_bus.py +18 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_configurer.py +54 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_connection.py +77 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_consumer.py +58 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_event_bus.py +28 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_queue_formatter.py +22 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_settings.py +8 -0
- instant_python/templates/boilerplate/exceptions/__init__.py +0 -0
- instant_python/templates/boilerplate/exceptions/domain_error.py +17 -0
- instant_python/templates/boilerplate/exceptions/domain_event_type_not_found_error.py +17 -0
- instant_python/templates/boilerplate/exceptions/incorrect_value_type_error.py +21 -0
- instant_python/templates/boilerplate/exceptions/invalid_id_format_error.py +17 -0
- instant_python/templates/boilerplate/exceptions/invalid_negative_value_error.py +17 -0
- instant_python/templates/boilerplate/exceptions/required_value_error.py +17 -0
- instant_python/templates/boilerplate/fastapi/__init__.py +0 -0
- instant_python/templates/boilerplate/fastapi/application.py +25 -0
- instant_python/templates/boilerplate/fastapi/http_response.py +45 -0
- instant_python/templates/boilerplate/fastapi/lifespan.py +14 -0
- instant_python/templates/boilerplate/fastapi/status_code.py +9 -0
- instant_python/templates/boilerplate/github/action.yml +22 -0
- instant_python/templates/boilerplate/github/test_lint.yml +36 -0
- instant_python/templates/boilerplate/logger/__init__.py +0 -0
- instant_python/templates/boilerplate/logger/json_formatter.py +16 -0
- instant_python/templates/boilerplate/logger/logger.py +39 -0
- instant_python/templates/boilerplate/mypy.ini +41 -0
- instant_python/templates/boilerplate/persistence/__init__.py +0 -0
- instant_python/templates/boilerplate/persistence/alembic_migrator.py +20 -0
- instant_python/templates/boilerplate/persistence/async/README.md +1 -0
- instant_python/templates/boilerplate/persistence/async/__init__.py +0 -0
- instant_python/templates/boilerplate/persistence/async/alembic.ini +124 -0
- instant_python/templates/boilerplate/persistence/async/async_engine_fixture.py +21 -0
- instant_python/templates/boilerplate/persistence/async/env.py +95 -0
- instant_python/templates/boilerplate/persistence/async/models_metadata.py +11 -0
- instant_python/templates/boilerplate/persistence/async/postgres_settings.py +15 -0
- instant_python/templates/boilerplate/persistence/async/script.py.mako +26 -0
- instant_python/templates/boilerplate/persistence/async/sqlalchemy_repository.py +30 -0
- instant_python/templates/boilerplate/persistence/base.py +4 -0
- instant_python/templates/boilerplate/persistence/synchronous/__init__.py +0 -0
- instant_python/templates/boilerplate/persistence/synchronous/session_maker.py +22 -0
- instant_python/templates/boilerplate/persistence/synchronous/sqlalchemy_repository.py +35 -0
- instant_python/templates/boilerplate/pyproject.toml +29 -0
- instant_python/templates/boilerplate/pytest.ini +10 -0
- instant_python/templates/boilerplate/random_generator.py +9 -0
- instant_python/templates/boilerplate/scripts/add_dependency.sh +37 -0
- instant_python/templates/boilerplate/scripts/create_aggregate.py +33 -0
- instant_python/templates/boilerplate/scripts/insert_template.py +90 -0
- instant_python/templates/boilerplate/scripts/integration.sh +39 -0
- instant_python/templates/boilerplate/scripts/local_setup.sh +15 -0
- instant_python/templates/boilerplate/scripts/makefile +137 -0
- instant_python/templates/boilerplate/scripts/post-merge +11 -0
- instant_python/templates/boilerplate/scripts/pre-commit +4 -0
- instant_python/templates/boilerplate/scripts/pre-push +6 -0
- instant_python/templates/boilerplate/scripts/remove_dependency.sh +36 -0
- instant_python/templates/boilerplate/scripts/unit.sh +40 -0
- instant_python/templates/boilerplate/value_object/__init__.py +0 -0
- instant_python/templates/boilerplate/value_object/int_value_object.py +11 -0
- instant_python/templates/boilerplate/value_object/string_value_object.py +19 -0
- instant_python/templates/boilerplate/value_object/uuid.py +17 -0
- instant_python/templates/boilerplate/value_object/value_object.py +21 -0
- instant_python/templates/project_structure/alembic_migrator.yml.j2 +3 -0
- instant_python/templates/project_structure/async_alembic.yml.j2 +20 -0
- instant_python/templates/project_structure/async_sqlalchemy.yml.j2 +17 -0
- instant_python/templates/project_structure/clean_architecture/main_structure.yml.j2 +25 -0
- instant_python/templates/project_structure/clean_architecture/source.yml.j2 +51 -0
- instant_python/templates/project_structure/clean_architecture/test.yml.j2 +23 -0
- instant_python/templates/project_structure/domain_driven_design/bounded_context.yml.j2 +20 -0
- instant_python/templates/project_structure/domain_driven_design/main_structure.yml.j2 +25 -0
- instant_python/templates/project_structure/domain_driven_design/source.yml.j2 +55 -0
- instant_python/templates/project_structure/domain_driven_design/test.yml.j2 +26 -0
- instant_python/templates/project_structure/event_bus_domain.yml.j2 +26 -0
- instant_python/templates/project_structure/event_bus_infra.yml.j2 +32 -0
- instant_python/templates/project_structure/fastapi_app.yml.j2 +10 -0
- instant_python/templates/project_structure/fastapi_infra.yml.j2 +10 -0
- instant_python/templates/project_structure/github_action.yml.j2 +18 -0
- instant_python/templates/project_structure/gitignore.yml.j2 +2 -0
- instant_python/templates/project_structure/license.yml.j2 +2 -0
- instant_python/templates/project_structure/logger.yml.j2 +10 -0
- instant_python/templates/project_structure/macros.j2 +6 -0
- instant_python/templates/project_structure/makefile.yml.j2 +38 -0
- instant_python/templates/project_structure/mypy.yml.j2 +3 -0
- instant_python/templates/project_structure/pre_commit.yml.j2 +3 -0
- instant_python/templates/project_structure/pyproject.yml.j2 +3 -0
- instant_python/templates/project_structure/pytest.yml.j2 +3 -0
- instant_python/templates/project_structure/python_version.yml.j2 +2 -0
- instant_python/templates/project_structure/standard_project/main_structure.yml.j2 +25 -0
- instant_python/templates/project_structure/standard_project/source.yml.j2 +30 -0
- instant_python/templates/project_structure/standard_project/test.yml.j2 +16 -0
- instant_python/templates/project_structure/synchronous_sqlalchemy.yml.j2 +17 -0
- instant_python/templates/project_structure/value_objects.yml.j2 +35 -0
- instant_python-0.0.1.dist-info/METADATA +276 -0
- instant_python-0.0.1.dist-info/RECORD +149 -0
- instant_python-0.0.1.dist-info/WHEEL +4 -0
- instant_python-0.0.1.dist-info/entry_points.txt +2 -0
- instant_python-0.0.1.dist-info/licenses/LICENSE +201 -0
|
File without changes
|
|
@@ -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,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}"
|
|
File without changes
|
|
@@ -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
|
|
File without changes
|
|
@@ -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,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
|