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.
- instant_python/__init__.py +1 -0
- instant_python/cli/__init__.py +0 -0
- instant_python/cli/cli.py +58 -0
- instant_python/cli/instant_python_typer.py +35 -0
- instant_python/config/__init__.py +0 -0
- instant_python/config/application/__init__.py +0 -0
- instant_python/config/application/config_generator.py +23 -0
- instant_python/config/delivery/__init__.py +0 -0
- instant_python/config/delivery/cli.py +19 -0
- instant_python/config/domain/__init__.py +0 -0
- instant_python/config/domain/question_wizard.py +7 -0
- instant_python/config/infra/__init__.py +0 -0
- instant_python/config/infra/question_wizard/__init__.py +0 -0
- instant_python/config/infra/question_wizard/questionary_console_wizard.py +25 -0
- instant_python/config/infra/question_wizard/step/__init__.py +0 -0
- instant_python/config/infra/question_wizard/step/dependencies_step.py +64 -0
- instant_python/config/infra/question_wizard/step/general_step.py +81 -0
- instant_python/config/infra/question_wizard/step/git_step.py +41 -0
- instant_python/config/infra/question_wizard/step/questionary.py +17 -0
- instant_python/config/infra/question_wizard/step/steps.py +21 -0
- instant_python/config/infra/question_wizard/step/template_step.py +63 -0
- instant_python/initialize/__init__.py +0 -0
- instant_python/initialize/application/__init__.py +0 -0
- instant_python/initialize/application/project_initializer.py +47 -0
- instant_python/initialize/delivery/__init__.py +0 -0
- instant_python/initialize/delivery/cli.py +48 -0
- instant_python/initialize/domain/__init__.py +0 -0
- instant_python/initialize/domain/env_manager.py +9 -0
- instant_python/initialize/domain/node.py +73 -0
- instant_python/initialize/domain/project_formatter.py +7 -0
- instant_python/initialize/domain/project_renderer.py +10 -0
- instant_python/initialize/domain/project_structure.py +77 -0
- instant_python/initialize/domain/project_writer.py +20 -0
- instant_python/initialize/domain/version_control_configurer.py +9 -0
- instant_python/initialize/infra/__init__.py +0 -0
- instant_python/initialize/infra/env_manager/__init__.py +0 -0
- instant_python/initialize/infra/env_manager/env_manager_factory.py +27 -0
- instant_python/initialize/infra/env_manager/pdm_env_manager.py +66 -0
- instant_python/initialize/infra/env_manager/system_console.py +65 -0
- instant_python/initialize/infra/env_manager/uv_env_manager.py +74 -0
- instant_python/initialize/infra/formatter/__init__.py +0 -0
- instant_python/initialize/infra/formatter/ruff_project_formatter.py +10 -0
- instant_python/initialize/infra/renderer/__init__.py +0 -0
- instant_python/initialize/infra/renderer/jinja_environment.py +71 -0
- instant_python/initialize/infra/renderer/jinja_project_renderer.py +57 -0
- instant_python/initialize/infra/version_control/__init__.py +0 -0
- instant_python/initialize/infra/version_control/git_configurer.py +29 -0
- instant_python/initialize/infra/writer/__init__.py +0 -0
- instant_python/initialize/infra/writer/file_system_project_writer.py +23 -0
- instant_python/shared/__init__.py +0 -0
- instant_python/shared/application_error.py +8 -0
- instant_python/shared/domain/__init__.py +0 -0
- instant_python/shared/domain/config_repository.py +18 -0
- instant_python/shared/domain/config_schema.py +113 -0
- instant_python/shared/domain/dependency_config.py +41 -0
- instant_python/shared/domain/general_config.py +71 -0
- instant_python/shared/domain/git_config.py +32 -0
- instant_python/shared/domain/template_config.py +76 -0
- instant_python/shared/infra/__init__.py +0 -0
- instant_python/shared/infra/persistence/__init__.py +0 -0
- instant_python/shared/infra/persistence/yaml_config_repository.py +39 -0
- instant_python/shared/supported_built_in_features.py +20 -0
- instant_python/shared/supported_licenses.py +11 -0
- instant_python/shared/supported_managers.py +10 -0
- instant_python/shared/supported_python_versions.py +12 -0
- instant_python/shared/supported_templates.py +12 -0
- instant_python/templates/boilerplate/.gitignore +164 -0
- instant_python/templates/boilerplate/.pre-commit-config.yml +73 -0
- instant_python/templates/boilerplate/.python-version +1 -0
- instant_python/templates/boilerplate/CITATION.cff +13 -0
- instant_python/templates/boilerplate/LICENSE +896 -0
- instant_python/templates/boilerplate/README.md +8 -0
- instant_python/templates/boilerplate/SECURITY.md +43 -0
- instant_python/templates/boilerplate/event_bus/__init__.py +0 -0
- instant_python/templates/boilerplate/event_bus/domain_event.py +15 -0
- instant_python/templates/boilerplate/event_bus/domain_event_json_deserializer.py +25 -0
- instant_python/templates/boilerplate/event_bus/domain_event_json_serializer.py +16 -0
- instant_python/templates/boilerplate/event_bus/domain_event_subscriber.py +33 -0
- instant_python/templates/boilerplate/event_bus/event_aggregate.py +19 -0
- instant_python/templates/boilerplate/event_bus/event_bus.py +9 -0
- instant_python/templates/boilerplate/event_bus/exchange_type.py +14 -0
- instant_python/templates/boilerplate/event_bus/mock_event_bus.py +16 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_configurer.py +45 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_connection.py +71 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_consumer.py +56 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_event_bus.py +26 -0
- instant_python/templates/boilerplate/event_bus/rabbit_mq_queue_formatter.py +21 -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/base_error.py +13 -0
- instant_python/templates/boilerplate/exceptions/domain_error.py +6 -0
- instant_python/templates/boilerplate/exceptions/domain_event_type_not_found_error.py +6 -0
- instant_python/templates/boilerplate/exceptions/rabbit_mq_connection_not_established_error.py +7 -0
- instant_python/templates/boilerplate/exceptions/required_value_error.py +6 -0
- instant_python/templates/boilerplate/fastapi/__init__.py +0 -0
- instant_python/templates/boilerplate/fastapi/application.py +74 -0
- instant_python/templates/boilerplate/fastapi/error_handlers.py +88 -0
- instant_python/templates/boilerplate/fastapi/error_response.py +31 -0
- instant_python/templates/boilerplate/fastapi/fastapi_log_middleware.py +32 -0
- instant_python/templates/boilerplate/fastapi/lifespan.py +13 -0
- instant_python/templates/boilerplate/fastapi/success_response.py +13 -0
- instant_python/templates/boilerplate/github/action.yml +35 -0
- instant_python/templates/boilerplate/github/bug_report.yml +60 -0
- instant_python/templates/boilerplate/github/ci.yml +199 -0
- instant_python/templates/boilerplate/github/feature_request.yml +21 -0
- instant_python/templates/boilerplate/github/release.yml +94 -0
- instant_python/templates/boilerplate/logger/__init__.py +0 -0
- instant_python/templates/boilerplate/logger/file_logger.py +55 -0
- instant_python/templates/boilerplate/logger/file_rotating_handler.py +36 -0
- instant_python/templates/boilerplate/logger/json_formatter.py +16 -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 +19 -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 +20 -0
- instant_python/templates/boilerplate/persistence/async/async_session.py +20 -0
- instant_python/templates/boilerplate/persistence/async/env.py +94 -0
- instant_python/templates/boilerplate/persistence/async/models_metadata.py +10 -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 +28 -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 +21 -0
- instant_python/templates/boilerplate/persistence/synchronous/sqlalchemy_repository.py +40 -0
- instant_python/templates/boilerplate/pyproject.toml +134 -0
- instant_python/templates/boilerplate/pytest.ini +10 -0
- instant_python/templates/boilerplate/scripts/add_dependency.py +45 -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.py +12 -0
- instant_python/templates/boilerplate/scripts/makefile +184 -0
- instant_python/templates/boilerplate/scripts/post-merge.py +40 -0
- instant_python/templates/boilerplate/scripts/pre-commit.py +15 -0
- instant_python/templates/boilerplate/scripts/pre-push.py +6 -0
- instant_python/templates/boilerplate/scripts/remove_dependency.py +40 -0
- instant_python/templates/boilerplate/scripts/unit.sh +40 -0
- instant_python/templates/project_structure/clean_architecture/layers/application.yml +3 -0
- instant_python/templates/project_structure/clean_architecture/layers/delivery.yml +8 -0
- instant_python/templates/project_structure/clean_architecture/layers/domain.yml +10 -0
- instant_python/templates/project_structure/clean_architecture/layers/infra.yml +12 -0
- instant_python/templates/project_structure/clean_architecture/layers/test_application.yml +3 -0
- instant_python/templates/project_structure/clean_architecture/layers/test_delivery.yml +3 -0
- instant_python/templates/project_structure/clean_architecture/layers/test_domain.yml +3 -0
- instant_python/templates/project_structure/clean_architecture/layers/test_infra.yml +9 -0
- instant_python/templates/project_structure/clean_architecture/main_structure.yml +38 -0
- instant_python/templates/project_structure/clean_architecture/source.yml +9 -0
- instant_python/templates/project_structure/clean_architecture/test.yml +9 -0
- instant_python/templates/project_structure/config_files/gitignore.yml +3 -0
- instant_python/templates/project_structure/config_files/mypy.yml +4 -0
- instant_python/templates/project_structure/config_files/pyproject.yml +4 -0
- instant_python/templates/project_structure/config_files/pytest.yml +4 -0
- instant_python/templates/project_structure/config_files/python_version.yml +3 -0
- instant_python/templates/project_structure/documentation/citation.yml +4 -0
- instant_python/templates/project_structure/documentation/license.yml +3 -0
- instant_python/templates/project_structure/documentation/readme.yml +4 -0
- instant_python/templates/project_structure/documentation/security.yml +4 -0
- instant_python/templates/project_structure/domain_driven_design/layers/bounded_context.yml +17 -0
- instant_python/templates/project_structure/domain_driven_design/layers/delivery.yml +8 -0
- instant_python/templates/project_structure/domain_driven_design/layers/shared.yml +18 -0
- instant_python/templates/project_structure/domain_driven_design/layers/shared_domain.yml +10 -0
- instant_python/templates/project_structure/domain_driven_design/layers/shared_infra.yml +12 -0
- instant_python/templates/project_structure/domain_driven_design/layers/test_shared.yml +8 -0
- instant_python/templates/project_structure/domain_driven_design/layers/test_shared_delivery.yml +3 -0
- instant_python/templates/project_structure/domain_driven_design/layers/test_shared_domain.yml +3 -0
- instant_python/templates/project_structure/domain_driven_design/layers/test_shared_infra.yml +9 -0
- instant_python/templates/project_structure/domain_driven_design/main_structure.yml +38 -0
- instant_python/templates/project_structure/domain_driven_design/source.yml +10 -0
- instant_python/templates/project_structure/domain_driven_design/test.yml +9 -0
- instant_python/templates/project_structure/errors.yml +12 -0
- instant_python/templates/project_structure/events/event_bus_domain.yml +48 -0
- instant_python/templates/project_structure/events/event_bus_infra.yml +40 -0
- instant_python/templates/project_structure/events/mock_event_bus.yml +4 -0
- instant_python/templates/project_structure/fastapi/fastapi_app.yml +32 -0
- instant_python/templates/project_structure/fastapi/fastapi_domain.yml +12 -0
- instant_python/templates/project_structure/fastapi/fastapi_infra.yml +12 -0
- instant_python/templates/project_structure/github/github_action.yml +24 -0
- instant_python/templates/project_structure/github/github_issues_template.yml +14 -0
- instant_python/templates/project_structure/github/makefile.yml +3 -0
- instant_python/templates/project_structure/github/precommit_hook.yml +4 -0
- instant_python/templates/project_structure/logger.yml +16 -0
- instant_python/templates/project_structure/macros.j2 +73 -0
- instant_python/templates/project_structure/persistence/alembic_migrator.yml +6 -0
- instant_python/templates/project_structure/persistence/async_alembic.yml +27 -0
- instant_python/templates/project_structure/persistence/async_engine_conftest.yml +4 -0
- instant_python/templates/project_structure/persistence/async_sqlalchemy.yml +14 -0
- instant_python/templates/project_structure/persistence/persistence.yml +8 -0
- instant_python/templates/project_structure/persistence/synchronous_sqlalchemy.yml +20 -0
- instant_python/templates/project_structure/standard_project/layers/source_features.yml +13 -0
- instant_python/templates/project_structure/standard_project/layers/test_event_bus.yml +6 -0
- instant_python/templates/project_structure/standard_project/layers/test_features.yml +6 -0
- instant_python/templates/project_structure/standard_project/main_structure.yml +38 -0
- instant_python/templates/project_structure/standard_project/source.yml +5 -0
- instant_python/templates/project_structure/standard_project/test.yml +5 -0
- instant_python-0.20.0.dist-info/METADATA +318 -0
- instant_python-0.20.0.dist-info/RECORD +202 -0
- instant_python-0.20.0.dist-info/WHEEL +4 -0
- instant_python-0.20.0.dist-info/entry_points.txt +2 -0
- instant_python-0.20.0.dist-info/licenses/LICENSE +201 -0
|
@@ -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!_
|
|
File without changes
|
|
@@ -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}"
|
|
File without changes
|
|
@@ -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.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.")
|
|
File without changes
|
|
@@ -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 %}
|