feat: add FastAPI, Entity base class, and health check endpoint
This commit is contained in:
@@ -1,27 +1,18 @@
|
|||||||
# Plan: UserId Value Object
|
# Plan: Add FastAPI to the Project
|
||||||
|
|
||||||
## Status: COMPLETED
|
## Goal
|
||||||
|
Add FastAPI as the web framework with a minimal health check endpoint.
|
||||||
|
|
||||||
## Objective
|
## Changes
|
||||||
Create a UserId value object in the marketplace domain with an immutable UUID value.
|
|
||||||
|
|
||||||
## Decisions Made
|
### 1. Dependencies
|
||||||
- **Location**: `domain/marketplace/user_id.py`
|
- Add `fastapi` and `uvicorn[standard]` to `pyproject.toml` under `[project.dependencies]`
|
||||||
- **Class style**: `@dataclass(frozen=True)` for immutability
|
- Install dependencies into the virtual environment
|
||||||
- **Tests**: Yes
|
|
||||||
|
|
||||||
## Value Object Structure
|
### 2. Application Entry Point
|
||||||
```python
|
- Create `main.py` with a FastAPI app instance
|
||||||
@dataclass(frozen=True)
|
- Add a health check endpoint (`GET /`) that returns `{"status": "ok"}`
|
||||||
class UserId:
|
|
||||||
value: UUID
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files Created
|
## Verification
|
||||||
- `domain/marketplace/user_id.py`
|
- `pytest tests/` — all tests pass
|
||||||
- `tests/test_user_id.py`
|
- `uvicorn main:app --reload` — server starts and health endpoint responds
|
||||||
|
|
||||||
## Test Results
|
|
||||||
- `test_user_id_creation` - PASSED
|
|
||||||
- `test_user_id_is_immutable` - PASSED
|
|
||||||
- `test_user_id_equality` - PASSED
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
env/
|
.venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|||||||
0
domain/framework/__init__.py
Normal file
0
domain/framework/__init__.py
Normal file
18
domain/framework/entity.py
Normal file
18
domain/framework/entity.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from abc import ABC
|
||||||
|
from dataclasses import replace
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
from domain.marketplace.domain_event import DomainEvent
|
||||||
|
|
||||||
|
|
||||||
|
class Entity(ABC):
|
||||||
|
_events: tuple[DomainEvent, ...] = ()
|
||||||
|
|
||||||
|
def raise_event(self, event: DomainEvent) -> Self:
|
||||||
|
return replace(self, _events=(*self._events, event))
|
||||||
|
|
||||||
|
def get_changes(self) -> tuple[DomainEvent, ...]:
|
||||||
|
return self._events
|
||||||
|
|
||||||
|
def clear_changes(self) -> Self:
|
||||||
|
return replace(self, _events=())
|
||||||
8
main.py
Normal file
8
main.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def health_check():
|
||||||
|
return {"status": "ok"}
|
||||||
@@ -6,6 +6,10 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
|||||||
name = "python-fun"
|
name = "python-fun"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.115.0",
|
||||||
|
"uvicorn[standard]>=0.30.0",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
pythonpath = ["."]
|
pythonpath = ["."]
|
||||||
79
tests/test_entity.py
Normal file
79
tests/test_entity.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from domain.framework.entity import Entity
|
||||||
|
from domain.marketplace.domain_event import (
|
||||||
|
ClassifiedAdCreated,
|
||||||
|
ClassifiedAdSentForReview,
|
||||||
|
DomainEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TestEntity(Entity):
|
||||||
|
__test__ = False
|
||||||
|
_events: tuple[DomainEvent, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_entity_creation():
|
||||||
|
entity = TestEntity()
|
||||||
|
assert entity.get_changes() == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_raise_event_returns_new_instance_with_event():
|
||||||
|
entity = TestEntity()
|
||||||
|
event = ClassifiedAdCreated(id=uuid4(), owner_id=uuid4())
|
||||||
|
|
||||||
|
new_entity = entity.raise_event(event)
|
||||||
|
|
||||||
|
assert new_entity.get_changes() == (event,)
|
||||||
|
|
||||||
|
|
||||||
|
def test_raise_event_does_not_mutate_original():
|
||||||
|
entity = TestEntity()
|
||||||
|
event = ClassifiedAdCreated(id=uuid4(), owner_id=uuid4())
|
||||||
|
|
||||||
|
entity.raise_event(event)
|
||||||
|
|
||||||
|
assert entity.get_changes() == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_events_accumulate():
|
||||||
|
entity = TestEntity()
|
||||||
|
event1 = ClassifiedAdCreated(id=uuid4(), owner_id=uuid4())
|
||||||
|
event2 = ClassifiedAdSentForReview(id=uuid4())
|
||||||
|
|
||||||
|
entity = entity.raise_event(event1)
|
||||||
|
entity = entity.raise_event(event2)
|
||||||
|
|
||||||
|
assert entity.get_changes() == (event1, event2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_changes_returns_instance_with_empty_events():
|
||||||
|
entity = TestEntity()
|
||||||
|
event = ClassifiedAdCreated(id=uuid4(), owner_id=uuid4())
|
||||||
|
entity = entity.raise_event(event)
|
||||||
|
|
||||||
|
cleared = entity.clear_changes()
|
||||||
|
|
||||||
|
assert cleared.get_changes() == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_changes_does_not_mutate_original():
|
||||||
|
entity = TestEntity()
|
||||||
|
event = ClassifiedAdCreated(id=uuid4(), owner_id=uuid4())
|
||||||
|
entity = entity.raise_event(event)
|
||||||
|
|
||||||
|
entity.clear_changes()
|
||||||
|
|
||||||
|
assert entity.get_changes() == (event,)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_changes_returns_tuple():
|
||||||
|
entity = TestEntity()
|
||||||
|
event = ClassifiedAdCreated(id=uuid4(), owner_id=uuid4())
|
||||||
|
entity = entity.raise_event(event)
|
||||||
|
|
||||||
|
changes = entity.get_changes()
|
||||||
|
|
||||||
|
assert isinstance(changes, tuple)
|
||||||
Reference in New Issue
Block a user