diff --git a/.agent/current-plan.md b/.agent/current-plan.md index 52fdc05..31f793c 100644 --- a/.agent/current-plan.md +++ b/.agent/current-plan.md @@ -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 -Create a UserId value object in the marketplace domain with an immutable UUID value. +## Changes -## Decisions Made -- **Location**: `domain/marketplace/user_id.py` -- **Class style**: `@dataclass(frozen=True)` for immutability -- **Tests**: Yes +### 1. Dependencies +- Add `fastapi` and `uvicorn[standard]` to `pyproject.toml` under `[project.dependencies]` +- Install dependencies into the virtual environment -## Value Object Structure -```python -@dataclass(frozen=True) -class UserId: - value: UUID -``` +### 2. Application Entry Point +- Create `main.py` with a FastAPI app instance +- Add a health check endpoint (`GET /`) that returns `{"status": "ok"}` -## Files Created -- `domain/marketplace/user_id.py` -- `tests/test_user_id.py` - -## Test Results -- `test_user_id_creation` - PASSED -- `test_user_id_is_immutable` - PASSED -- `test_user_id_equality` - PASSED \ No newline at end of file +## Verification +- `pytest tests/` — all tests pass +- `uvicorn main:app --reload` — server starts and health endpoint responds \ No newline at end of file diff --git a/.gitignore b/.gitignore index ac78f51..3fe6a55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -env/ +.venv/ __pycache__/ *.pyc .pytest_cache/ diff --git a/domain/framework/__init__.py b/domain/framework/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/domain/framework/entity.py b/domain/framework/entity.py new file mode 100644 index 0000000..b783d51 --- /dev/null +++ b/domain/framework/entity.py @@ -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=()) diff --git a/main.py b/main.py new file mode 100644 index 0000000..e59205b --- /dev/null +++ b/main.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def health_check(): + return {"status": "ok"} diff --git a/pyproject.toml b/pyproject.toml index 41c5525..925f0d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,10 @@ build-backend = "setuptools.backends._legacy:_Backend" name = "python-fun" version = "0.1.0" requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.30.0", +] [tool.pytest.ini_options] pythonpath = ["."] \ No newline at end of file diff --git a/tests/test_entity.py b/tests/test_entity.py new file mode 100644 index 0000000..50cb056 --- /dev/null +++ b/tests/test_entity.py @@ -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)