Skip to content

Commit 17c5cf3

Browse files
authored
Merge pull request #8 from getmarkus/cm-branch-7
feat: add SQLAlchemy dependency and improve logging messages
2 parents 9528ca9 + eda9c31 commit 17c5cf3

File tree

16 files changed

+470
-85
lines changed

16 files changed

+470
-85
lines changed

.trunk/trunk.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ runtimes:
1717
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
1818
lint:
1919
disabled:
20+
- git-diff-check
2021
- bandit
2122
enabled:
2223
2324
24-
- git-diff-check
2525
2626
2727

config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import tomllib
12
from functools import lru_cache
23
from typing import List
34

4-
import tomllib
55
from pydantic import AnyHttpUrl
66
from pydantic_settings import BaseSettings, SettingsConfigDict
77

@@ -16,6 +16,7 @@ class Settings(BaseSettings):
1616
execution_mode: str = ""
1717
env_smoke_test: str = ""
1818
project_name: str = ""
19+
database_url: str = "sqlite:///./issues.db"
1920
# BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
2021
# e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \
2122
# "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'

main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ async def lifespan(app: FastAPI):
3434
global running
3535
running = True
3636
logger.info("Lifespan started")
37+
38+
# Initialize database if using SQLModel
39+
if Settings.get_settings().execution_mode == "sqlmodel":
40+
from src.resource_adapters.persistence.sqlmodel.database import init_db
41+
init_db()
42+
3743
yield
3844
running = False
3945
logger.info("Lifespan stopped")

pyproject.toml

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ authors = [{ name = "Chris Markus", email = "[email protected]" }]
66
requires-python = "~=3.12"
77
readme = "README.md"
88
dependencies = [
9-
"fastapi[standard]>=0.115.6",
10-
"httpx>=0.28.1",
11-
"loguru>=0.7.3",
12-
"pydantic>=2.10.5",
13-
"pydantic-settings>=2.7.1",
14-
"pytest>=8.3.4",
9+
"fastapi[standard]>=0.115.6",
10+
"httpx>=0.28.1",
11+
"loguru>=0.7.3",
12+
"pydantic>=2.10.5",
13+
"pydantic-settings>=2.7.1",
14+
"sqlmodel>=0.0.22",
1515
]
1616

1717
[tool.uv]
@@ -22,4 +22,9 @@ requires = ["hatchling"]
2222
build-backend = "hatchling.build"
2323

2424
[dependency-groups]
25-
dev = []
25+
dev = ["pytest>=8.3.4", "pytest-asyncio>=0.23.5", "pytest-anyio>=0.1.0"]
26+
27+
[tool.pytest.ini_options]
28+
asyncio_mode = "auto"
29+
anyio_backend = "asyncio"
30+
asyncio_default_fixture_loop_scope = "function"

src/domain/aggregate_root.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,23 @@
22
from datetime import datetime
33
from typing import Protocol
44

5-
from pydantic import BaseModel
5+
from sqlmodel import SQLModel
66

77

88
class BaseCommand(Protocol):
99
command_id: str
1010
timestamp: datetime
1111

1212
# might need specification pattern here
13-
def validate(self) -> bool:
14-
...
13+
def validate(self) -> bool: ...
1514

1615

1716
class BaseEvent(Protocol):
1817
event_id: str
1918
timestamp: datetime
2019

2120

22-
class AggregateRoot(BaseModel, abc.ABC):
21+
class AggregateRoot(SQLModel, abc.ABC):
2322
version: int = 0
2423

2524
# or handle()

src/domain/issue.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from types import MappingProxyType
55
from typing import Any, Final, Optional
66

7+
from sqlmodel import Field
8+
79
from src.domain.aggregate_root import AggregateRoot, BaseCommand, BaseEvent
810

911
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#issuesevent
@@ -15,6 +17,7 @@
1517

1618
### Enums ###
1719

20+
1821
class IssueTransitionState(Enum):
1922
COMPLETED = "COMPLETED"
2023
NOT_PLANNED = "NOT_PLANNED"
@@ -180,8 +183,8 @@ def __init__(
180183

181184

182185
### Entitites ###
183-
class Issue(AggregateRoot):
184-
issue_number: int
186+
class Issue(AggregateRoot, table=True):
187+
issue_number: int = Field(primary_key=True)
185188
issue_state: IssueState = IssueState.OPEN
186189

187190
def process(self, command: BaseCommand) -> list[BaseEvent]:
Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,66 @@
11
from typing import Annotated
2+
23
from fastapi import APIRouter, Depends
34
from loguru import logger
45

5-
from src.app.usecases.analyze_issue import AnalyzeIssue
66
from config import Settings
7+
from src.app.ports.repositories.issues import IssueRepository
8+
from src.app.repository import UnitOfWork as BaseUnitOfWork
9+
from src.app.usecases.analyze_issue import AnalyzeIssue
710
from src.domain.issue import Issue
811
from src.interface_adapters.exceptions import UnsupportedOperationException
912
from src.resource_adapters.persistence.in_memory.issues import InMemoryIssueRepository
10-
from src.resource_adapters.persistence.in_memory.unit_of_work import UnitOfWork
13+
from src.resource_adapters.persistence.in_memory.unit_of_work import (
14+
UnitOfWork as InMemoryUnitOfWork,
15+
)
16+
from src.resource_adapters.persistence.sqlmodel.issues import SQLModelIssueRepository
17+
from src.resource_adapters.persistence.sqlmodel.unit_of_work import SQLModelUnitOfWork
1118

1219
router = APIRouter()
1320

1421

1522
# https://fastapi.tiangolo.com/tutorial/dependencies/
16-
async def configure_unit_of_work() -> UnitOfWork:
17-
if Settings.get_settings().execution_mode == "in-memory":
18-
return UnitOfWork()
23+
async def configure_unit_of_work() -> BaseUnitOfWork:
24+
execution_mode = Settings.get_settings().execution_mode
25+
if execution_mode == "in-memory":
26+
return InMemoryUnitOfWork()
27+
elif execution_mode == "sqlmodel":
28+
return SQLModelUnitOfWork(database_url=Settings.get_settings().database_url)
1929
else:
2030
raise UnsupportedOperationException(
2131
message="Unsupported unit of work configuration",
22-
detail="Only in-memory unit of work is currently supported"
32+
detail=f"Execution mode '{execution_mode}' is not supported",
2333
)
2434

2535

26-
async def configure_repository() -> InMemoryIssueRepository:
27-
if Settings.get_settings().execution_mode == "in-memory":
36+
async def configure_repository() -> IssueRepository:
37+
execution_mode = Settings.get_settings().execution_mode
38+
if execution_mode == "in-memory":
2839
return InMemoryIssueRepository()
40+
elif execution_mode == "sqlmodel":
41+
# For SQLModel, the repository is managed by the UnitOfWork
42+
# This is just a placeholder that will be replaced
43+
return SQLModelIssueRepository(None) # type: ignore
2944
else:
3045
raise UnsupportedOperationException(
3146
message="Unsupported repository configuration",
32-
detail="Only in-memory repository is currently supported"
47+
detail=f"Execution mode '{execution_mode}' is not supported",
3348
)
3449

3550

3651
@router.post("/issues/{issue_number}/analyze", response_model=Issue)
3752
async def analyze_issue(
3853
issue_number: int,
39-
unit_of_work: Annotated[UnitOfWork, Depends(configure_unit_of_work)],
40-
repo: Annotated[InMemoryIssueRepository, Depends(configure_repository)],
54+
unit_of_work: Annotated[BaseUnitOfWork, Depends(configure_unit_of_work)],
55+
repo: Annotated[IssueRepository, Depends(configure_repository)],
4156
) -> Issue:
4257
logger.info(f"analyzing issue: {issue_number}")
43-
use_case = AnalyzeIssue(issue_number=issue_number, repo=repo, unit_of_work=unit_of_work)
58+
59+
# For SQLModel, we need to use the repository from the unit of work
60+
if isinstance(unit_of_work, SQLModelUnitOfWork):
61+
repo = unit_of_work.issues
62+
63+
use_case = AnalyzeIssue(
64+
issue_number=issue_number, repo=repo, unit_of_work=unit_of_work
65+
)
4466
return await use_case.analyze()

src/resource_adapters/persistence/in_memory/unit_of_work.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def rollback(self) -> None:
1616
self.committed = False
1717

1818
def __enter__(self) -> "UnitOfWork":
19-
logger.info(f"enter uow")
19+
logger.info("enter uow")
2020
return self
2121

2222
def __exit__(
@@ -26,8 +26,8 @@ def __exit__(
2626
exc_tb: TracebackType,
2727
) -> None:
2828
if exc_type:
29-
logger.info(f"exit rollback uow")
29+
logger.info("exit rollback uow")
3030
self.rollback()
3131
else:
32-
logger.info(f"exit commit uow")
32+
logger.info("exit commit uow")
3333
self.commit()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from src.resource_adapters.persistence.sqlmodel.issues import SQLModelIssueRepository
2+
from src.resource_adapters.persistence.sqlmodel.unit_of_work import SQLModelUnitOfWork
3+
4+
__all__ = ["SQLModelIssueRepository", "SQLModelUnitOfWork"]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from loguru import logger
2+
from sqlmodel import SQLModel, create_engine
3+
4+
from config import Settings
5+
from src.domain.issue import Issue # noqa: F401
6+
7+
_engine = None
8+
9+
10+
def get_engine(database_url: str | None = None):
11+
"""Get or create SQLModel engine instance."""
12+
global _engine
13+
14+
if _engine is not None:
15+
return _engine
16+
17+
if database_url is None:
18+
database_url = Settings.get_settings().database_url
19+
if not database_url:
20+
database_url = "sqlite:///./issues.db"
21+
logger.warning(f"No database URL configured, using default: {database_url}")
22+
23+
_engine = create_engine(database_url,connect_args={"check_same_thread": False}, echo=True)
24+
return _engine
25+
26+
27+
def init_db() -> None:
28+
"""Initialize the database by creating all tables."""
29+
engine = get_engine()
30+
logger.info("Creating database tables...")
31+
SQLModel.metadata.create_all(engine)
32+
logger.info("Database tables created successfully")

0 commit comments

Comments
 (0)