Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: Check

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
frontend-lint:
name: "Frontend: Lint"
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- run: npm run lint

frontend-format:
name: "Frontend: Format"
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- run: npm run format:check

frontend-typecheck:
name: "Frontend: Type Check"
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- run: npm run type-check

backend-lint:
name: "Backend: Lint"
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv sync --extra dev
- run: uv run ruff check .

backend-format:
name: "Backend: Format"
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv sync --extra dev
- run: uv run ruff format --check .

backend-typecheck:
name: "Backend: Type Check"
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv sync --extra dev
- run: uv run mypy .

backend-test:
name: "Backend: Test"
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv sync --extra test
- run: uv run pytest
17 changes: 10 additions & 7 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import enum
from typing import Any, Optional
from functools import lru_cache
from typing import Any

from pydantic import PostgresDsn, ValidationInfo, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict


class AppEnvironment(str, enum.Enum):
class AppEnvironment(enum.StrEnum):
DEVELOP = "development"
PRODUCTION = "production"


class Settings(BaseSettings):
# 明示的に環境変数を読み込む(Pydantic_v2以降)
model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8')
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")

ENVIRONMENT: AppEnvironment

Expand All @@ -25,7 +26,7 @@ class Settings(BaseSettings):
POSTGRES_PASSWORD: str
POSTGRES_DB: str
POSTGRES_PORT: str
SQLALCHEMY_DATABASE_URI: Optional[str] = None
SQLALCHEMY_DATABASE_URI: str | None = None

# Kubernetes設定
K8S_NAMESPACE: str = "default"
Expand All @@ -46,7 +47,7 @@ class Settings(BaseSettings):
ANTHROPIC_API_KEY: str = ""

@field_validator("SQLALCHEMY_DATABASE_URI", mode="after")
def assemble_db_connection(cls, v: Optional[str], values: ValidationInfo) -> Any:
def assemble_db_connection(cls, v: str | None, values: ValidationInfo) -> Any:
if isinstance(v, str):
return v

Expand All @@ -56,10 +57,12 @@ def assemble_db_connection(cls, v: Optional[str], values: ValidationInfo) -> Any
username=values.data.get("POSTGRES_USER"),
password=values.data.get("POSTGRES_PASSWORD"),
host=values.data.get("POSTGRES_SERVER"),
port=int(values.data.get("POSTGRES_PORT")),
port=int(values.data.get("POSTGRES_PORT", "5432")),
path=f"{values.data.get('POSTGRES_DB') or ''}",
)
)


settings = Settings()
@lru_cache
def get_settings() -> Settings:
return Settings()
8 changes: 2 additions & 6 deletions backend/app/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ class ProposalNotFoundError(Exception):
def __init__(self, job_id: str, proposal_index: int) -> None:
self.job_id = job_id
self.proposal_index = proposal_index
super().__init__(
f"Proposal not found: job={job_id}, index={proposal_index}"
)
super().__init__(f"Proposal not found: job={job_id}, index={proposal_index}")


class K8sServiceError(Exception):
Expand All @@ -36,6 +34,4 @@ def __init__(self, job_id: str, current_state: str, expected_state: str) -> None
self.job_id = job_id
self.current_state = current_state
self.expected_state = expected_state
super().__init__(
f"Job {job_id} is in state '{current_state}', expected '{expected_state}'"
)
super().__init__(f"Job {job_id} is in state '{current_state}', expected '{expected_state}'")
13 changes: 9 additions & 4 deletions backend/app/core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.responses import Response

from app.core.exceptions import (
ArtifactNotFoundError,
Expand All @@ -19,7 +20,7 @@
class ErrorHandlerMiddleware(BaseHTTPMiddleware):
"""Global error handler for uncaught exceptions."""

async def dispatch(self, request: Request, call_next):
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
try:
response = await call_next(request)
return response
Expand All @@ -38,7 +39,11 @@ async def dispatch(self, request: Request, call_next):
content={"detail": "Kubernetes service unavailable"},
)
except Exception:
logger.exception("Unhandled exception during request %s %s", request.method, request.url.path)
logger.exception(
"Unhandled exception during request %s %s",
request.method,
request.url.path,
)
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"},
Expand All @@ -48,7 +53,7 @@ async def dispatch(self, request: Request, call_next):
class RequestLoggingMiddleware(BaseHTTPMiddleware):
"""Log request method, path, and response time."""

async def dispatch(self, request: Request, call_next):
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
start = time.time()
response = await call_next(request)
duration = time.time() - start
Expand Down
4 changes: 2 additions & 2 deletions backend/app/di/container.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Generator
from collections.abc import Generator

from sqlalchemy.orm import Session

Expand All @@ -12,7 +12,7 @@ class DIContainer:
"""依存性注入コンテナ"""

@staticmethod
def get_db() -> Generator[Session, None, None]:
def get_db() -> Generator[Session]:
"""データベースセッションを取得"""
try:
db = SessionLocal()
Expand Down
9 changes: 7 additions & 2 deletions backend/app/di/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Generator
from collections.abc import Generator

from fastapi import Depends
from sqlalchemy.orm import Session
Expand All @@ -7,9 +7,10 @@
from app.repository.job_repository import JobRepository
from app.repository.proposal_repository import ProposalRepository
from app.repository.setting_repository import SettingRepository
from app.service.artifact_service import ArtifactService


def get_db() -> Generator[Session, None, None]:
def get_db() -> Generator[Session]:
"""データベースセッションを取得"""
yield from DIContainer.get_db()

Expand All @@ -24,3 +25,7 @@ def get_proposal_repository(db: Session = Depends(get_db)) -> ProposalRepository

def get_setting_repository(db: Session = Depends(get_db)) -> SettingRepository:
return DIContainer.get_setting_repository(db)


def get_artifact_service() -> ArtifactService:
return ArtifactService()
4 changes: 4 additions & 0 deletions backend/app/model/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from typing import Any

from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import as_declarative


@as_declarative()
class Base:
__allow_unmapped__ = True

id: Any
__name__: str

Expand Down
20 changes: 8 additions & 12 deletions backend/app/model/job.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import enum
import uuid
from datetime import datetime, timezone
from datetime import UTC, datetime

from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String, Text, Uuid
from sqlalchemy.orm import relationship

from app.model.base import Base


class JobStatus(str, enum.Enum):
class JobStatus(enum.StrEnum):
PENDING = "pending"
ANALYZING = "analyzing"
ANALYZED = "analyzed"
Expand All @@ -17,15 +17,15 @@ class JobStatus(str, enum.Enum):
FAILED = "failed"


class ProposalStatus(str, enum.Enum):
class ProposalStatus(enum.StrEnum):
PENDING = "pending"
IMPLEMENTING = "implementing"
COMPLETED = "completed"
FAILED = "failed"


def _utcnow() -> datetime:
return datetime.now(timezone.utc)
return datetime.now(UTC)


class Job(Base):
Expand All @@ -42,7 +42,7 @@ class Job(Base):
created_at = Column(DateTime(timezone=True), default=_utcnow)
updated_at = Column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)

proposals = relationship(
proposals: list["Proposal"] = relationship(
"Proposal", back_populates="job", cascade="all, delete-orphan"
)

Expand All @@ -51,25 +51,21 @@ class Proposal(Base):
__tablename__ = "proposals"

id = Column(Uuid, primary_key=True, default=uuid.uuid4)
job_id = Column(
Uuid, ForeignKey("jobs.id", ondelete="CASCADE"), nullable=False
)
job_id = Column(Uuid, ForeignKey("jobs.id", ondelete="CASCADE"), nullable=False)
proposal_index = Column(Integer, nullable=False)
title = Column(String(200), nullable=False)
concept = Column(Text, nullable=False)
plan = Column(Text, nullable=False) # JSON array stored as text
files = Column(Text, nullable=True) # JSON array stored as text
complexity = Column(String(20), nullable=True)
status = Column(
Enum(ProposalStatus), nullable=False, default=ProposalStatus.PENDING
)
status = Column(Enum(ProposalStatus), nullable=False, default=ProposalStatus.PENDING)
after_screenshot_path = Column(String(500), nullable=True)
diff_path = Column(Text, nullable=True)
k8s_job_name = Column(String(200), nullable=True)
error_message = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=_utcnow)

job = relationship("Job", back_populates="proposals")
job: "Job" = relationship("Job", back_populates="proposals")


class Setting(Base):
Expand Down
3 changes: 1 addition & 2 deletions backend/app/repository/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@
# - *_repository_interface.py: リポジトリインターフェース
# - *_repository_sql.py: SQL実装

from .database import SessionLocal, engine
from .database import SessionLocal
from .job_repository import JobRepository
from .proposal_repository import ProposalRepository
from .setting_repository import SettingRepository

__all__ = [
"SessionLocal",
"engine",
"JobRepository",
"ProposalRepository",
"SettingRepository",
Expand Down
Loading