Skip to content
Open
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
9 changes: 9 additions & 0 deletions .env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# ローカルモード設定
STORAGE_MODE=local
LOCAL_STORAGE_PATH=/data/storage

# ダミーS3設定(ローカルモードでは使用されない)
AWS_ACCESS_KEY_ID=dummy
AWS_SECRET_ACCESS_KEY=dummy
AWS_DEFAULT_REGION=ap-northeast-2
S3_BUCKET_NAME=labcode-dev-artifacts
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
.venv
__pycache__
*.db
*.db
.env
*.db*

# Backup files (not to be tracked)
app/scripts_backup_local/
app/tests_backup_local/
data_backup_local/
24 changes: 23 additions & 1 deletion app/api/response_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class RunResponse(BaseModel):
finished_at: Optional[datetime]
status: str
storage_address: str
storage_mode: Optional[str] = None # ★追加: 's3' または 'local'
deleted_at: datetime | None
display_visible: bool
# project: Optional[ProjectResponse] # リレーション
Expand All @@ -55,6 +56,7 @@ class RunResponseWithProjectName(BaseModel):
finished_at: Optional[datetime]
status: str
storage_address: str
storage_mode: Optional[str] = None # ★追加: 's3' または 'local'
deleted_at: datetime | None
display_visible: bool
# project: Optional[ProjectResponse] # リレーション
Expand Down Expand Up @@ -232,4 +234,24 @@ class ProcessListResponse(BaseModel):
- items: プロセスリスト(ProcessResponseEnhanced)
"""
total: int
items: List[ProcessResponseEnhanced]
items: List[ProcessResponseEnhanced]


# ============================================================
# Admin API用の新規レスポンスモデル
# ============================================================

class ProjectResponseWithOwner(BaseModel):
"""プロジェクト情報(オーナー情報含む)のレスポンスモデル

管理画面のプロジェクト一覧で使用。
オーナーのメールアドレスを含む。
"""
model_config = ConfigDict(from_attributes=True)

id: int
name: str
user_id: int
owner_email: Optional[str] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
163 changes: 163 additions & 0 deletions app/api/route/process_operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""ProcessOperation中間テーブルのAPI

ProcessとOperationのN:M関係を管理するAPIエンドポイント。
"""

from fastapi import APIRouter, HTTPException, Query, Body
from typing import Optional, List
from pydantic import BaseModel
from define_db.models import ProcessOperation, Process, Operation
from define_db.database import SessionLocal

router = APIRouter()


class ProcessOperationCreate(BaseModel):
"""ProcessOperation作成リクエスト"""
process_id: int
operation_id: int


class ProcessOperationResponse(BaseModel):
"""ProcessOperationレスポンス"""
id: int
process_id: int
operation_id: int
created_at: Optional[str] = None

class Config:
from_attributes = True


@router.get("/process-operations", tags=["process-operations"])
def get_process_operations(
process_id: Optional[int] = Query(None, description="Filter by process_id"),
operation_id: Optional[int] = Query(None, description="Filter by operation_id"),
limit: int = Query(1000, description="Limit number of results", ge=1, le=10000),
offset: int = Query(0, description="Offset for pagination", ge=0)
) -> List[ProcessOperationResponse]:
"""
ProcessOperation一覧を取得する。

Parameters:
- process_id: プロセスIDでフィルタリング(オプション)
- operation_id: オペレーションIDでフィルタリング(オプション)
- limit: 取得件数制限(デフォルト: 1000)
- offset: オフセット(デフォルト: 0)
"""
with SessionLocal() as session:
query = session.query(ProcessOperation)

if process_id is not None:
query = query.filter(ProcessOperation.process_id == process_id)
if operation_id is not None:
query = query.filter(ProcessOperation.operation_id == operation_id)

query = query.limit(limit).offset(offset)
results = query.all()

return [
ProcessOperationResponse(
id=po.id,
process_id=po.process_id,
operation_id=po.operation_id,
created_at=po.created_at.isoformat() if po.created_at else None
)
for po in results
]


@router.post("/process-operations", tags=["process-operations"])
def create_process_operation(
data: ProcessOperationCreate = Body(...)
) -> ProcessOperationResponse:
"""
ProcessOperationを作成する。

Parameters:
- process_id: プロセスID
- operation_id: オペレーションID
"""
with SessionLocal() as session:
# プロセス存在確認
process = session.query(Process).filter(Process.id == data.process_id).first()
if not process:
raise HTTPException(
status_code=404,
detail=f"Process with id {data.process_id} not found"
)

# オペレーション存在確認
operation = session.query(Operation).filter(Operation.id == data.operation_id).first()
if not operation:
raise HTTPException(
status_code=404,
detail=f"Operation with id {data.operation_id} not found"
)

# 重複チェック
existing = session.query(ProcessOperation).filter(
ProcessOperation.process_id == data.process_id,
ProcessOperation.operation_id == data.operation_id
).first()
if existing:
raise HTTPException(
status_code=409,
detail="ProcessOperation already exists"
)

# 作成
po = ProcessOperation(
process_id=data.process_id,
operation_id=data.operation_id
)
session.add(po)
session.commit()
session.refresh(po)

return ProcessOperationResponse(
id=po.id,
process_id=po.process_id,
operation_id=po.operation_id,
created_at=po.created_at.isoformat() if po.created_at else None
)


@router.get("/process-operations/{id}", tags=["process-operations"])
def get_process_operation(id: int) -> ProcessOperationResponse:
"""
ProcessOperationを取得する。

Parameters:
- id: ProcessOperation ID
"""
with SessionLocal() as session:
po = session.query(ProcessOperation).filter(ProcessOperation.id == id).first()
if not po:
raise HTTPException(status_code=404, detail="ProcessOperation not found")

return ProcessOperationResponse(
id=po.id,
process_id=po.process_id,
operation_id=po.operation_id,
created_at=po.created_at.isoformat() if po.created_at else None
)


@router.delete("/process-operations/{id}", tags=["process-operations"])
def delete_process_operation(id: int):
"""
ProcessOperationを削除する。

Parameters:
- id: ProcessOperation ID
"""
with SessionLocal() as session:
po = session.query(ProcessOperation).filter(ProcessOperation.id == id).first()
if not po:
raise HTTPException(status_code=404, detail="ProcessOperation not found")

session.delete(po)
session.commit()

return {"message": f"ProcessOperation {id} deleted successfully"}
41 changes: 39 additions & 2 deletions app/api/route/projects.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,51 @@
from define_db.models import Project, User
from define_db.database import SessionLocal
from api.response_model import ProjectResponse
from fastapi import APIRouter
from api.response_model import ProjectResponse, ProjectResponseWithOwner
from fastapi import APIRouter, Query
from fastapi import Form
from fastapi import HTTPException
from sqlalchemy.orm import joinedload
from typing import List
import datetime as dt

router = APIRouter()


# ============================================================
# Admin API: プロジェクト一覧取得(オーナー情報含む)
# ============================================================

@router.get("/projects/list", tags=["projects"], response_model=List[ProjectResponseWithOwner])
def list_all(
limit: int = Query(default=100, ge=1, le=1000, description="Maximum number of projects to return"),
offset: int = Query(default=0, ge=0, description="Number of projects to skip")
):
"""
全プロジェクト一覧を取得(オーナー情報含む)

管理画面のプロジェクト一覧表示で使用。
オーナーのメールアドレスを含む。
ページネーション対応。
"""
with SessionLocal() as session:
projects = session.query(Project).options(
joinedload(Project.user)
).offset(offset).limit(limit).all()

result = []
for p in projects:
resp = ProjectResponseWithOwner(
id=p.id,
name=p.name,
user_id=p.user_id,
owner_email=p.user.email if p.user else None,
created_at=p.created_at,
updated_at=p.updated_at
)
result.append(resp)
return result


@router.post("/projects/", tags=["projects"], response_model=ProjectResponse)
def create(name: str = Form(), user_id: int = Form()):
with SessionLocal() as session:
Expand Down
12 changes: 12 additions & 0 deletions app/api/route/runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from api.response_model import RunResponse, OperationResponseWithProcessStorageAddress, ProcessResponseEnhanced, ProcessDetailResponse
from api.route.processes import load_port_info_from_db
from services.port_auto_generator import auto_generate_ports_for_run
from services.hal import infer_storage_mode_for_run
from fastapi import APIRouter
from fastapi import Form
from fastapi import HTTPException
Expand Down Expand Up @@ -53,6 +54,10 @@ def read(id: int):
run = session.query(Run).filter(Run.id == id, Run.deleted_at.is_(None)).first()
if not run:
raise HTTPException(status_code=404, detail="Run not found")
# storage_mode=nullの場合は推論して値を設定(DBに永続化)
# 2回目以降はキャッシュヒットでS3/DBアクセスなし
if run.storage_mode is None:
run.storage_mode = infer_storage_mode_for_run(session, run)
return RunResponse.model_validate(run)


Expand Down Expand Up @@ -204,6 +209,13 @@ def patch(id: int, attribute: str = Form(), new_value: str = Form()):
detail="display_visible must be 'true' or 'false'"
)
run.display_visible = (new_value.lower() == "true")
case "storage_mode":
if new_value not in ("s3", "local"):
raise HTTPException(
status_code=400,
detail="storage_mode must be 's3' or 'local'"
)
run.storage_mode = new_value
case _:
raise HTTPException(status_code=400, detail="Invalid attribute")
session.commit()
Expand Down
Loading