Task 001: dockerized monorepo foundation
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY apps/backend/requirements.txt ./requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY apps/backend/src ./src
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "src.presentation.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -0,0 +1,5 @@
|
||||
fastapi==0.115.6
|
||||
uvicorn[standard]==0.34.0
|
||||
psycopg[binary]==3.2.3
|
||||
redis==5.2.1
|
||||
boto3==1.35.90
|
||||
@@ -0,0 +1 @@
|
||||
"""Backend service package."""
|
||||
@@ -0,0 +1,3 @@
|
||||
# Application
|
||||
|
||||
Application use cases and workflow orchestration belong in this layer.
|
||||
@@ -0,0 +1 @@
|
||||
"""Application layer for use cases and orchestration."""
|
||||
@@ -0,0 +1,3 @@
|
||||
# Domain
|
||||
|
||||
Business entities, value objects, and domain rules belong in this layer.
|
||||
@@ -0,0 +1 @@
|
||||
"""Domain layer for business concepts and invariants."""
|
||||
@@ -0,0 +1,4 @@
|
||||
# Infrastructure
|
||||
|
||||
Database, cache, object storage, queue, and other external adapters belong in
|
||||
this layer.
|
||||
@@ -0,0 +1 @@
|
||||
"""Infrastructure adapters for external services."""
|
||||
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
|
||||
import boto3
|
||||
import psycopg
|
||||
from botocore.config import Config
|
||||
from redis import Redis
|
||||
|
||||
|
||||
DependencyStatus = dict[str, str]
|
||||
|
||||
|
||||
def check_dependencies() -> DependencyStatus:
|
||||
checks: dict[str, Callable[[], None]] = {
|
||||
"postgres": _check_postgres,
|
||||
"redis": _check_redis,
|
||||
"object_storage": _check_object_storage,
|
||||
}
|
||||
|
||||
results: DependencyStatus = {}
|
||||
for name, check in checks.items():
|
||||
try:
|
||||
check()
|
||||
except Exception:
|
||||
results[name] = "error"
|
||||
else:
|
||||
results[name] = "ok"
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _check_postgres() -> None:
|
||||
dsn = _required_env("POSTGRES_DSN")
|
||||
with psycopg.connect(dsn, connect_timeout=3) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT 1")
|
||||
cursor.fetchone()
|
||||
|
||||
|
||||
def _check_redis() -> None:
|
||||
redis_url = _required_env("REDIS_URL")
|
||||
client = Redis.from_url(redis_url, socket_connect_timeout=3, socket_timeout=3)
|
||||
try:
|
||||
if client.ping() is not True:
|
||||
raise RuntimeError("Redis ping failed")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
def _check_object_storage() -> None:
|
||||
endpoint = _required_env("OBJECT_STORAGE_ENDPOINT")
|
||||
access_key_id = _required_env("OBJECT_STORAGE_ACCESS_KEY_ID")
|
||||
secret_access_key = _required_env("OBJECT_STORAGE_SECRET_ACCESS_KEY")
|
||||
bucket = _required_env("OBJECT_STORAGE_BUCKET")
|
||||
region = os.environ.get("OBJECT_STORAGE_REGION", "us-east-1")
|
||||
|
||||
client = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=endpoint,
|
||||
aws_access_key_id=access_key_id,
|
||||
aws_secret_access_key=secret_access_key,
|
||||
region_name=region,
|
||||
config=Config(signature_version="s3v4", connect_timeout=3, read_timeout=3),
|
||||
)
|
||||
client.head_bucket(Bucket=bucket)
|
||||
|
||||
|
||||
def _required_env(name: str) -> str:
|
||||
value = os.environ.get(name)
|
||||
if not value:
|
||||
raise RuntimeError(f"Missing required environment variable: {name}")
|
||||
return value
|
||||
@@ -0,0 +1,3 @@
|
||||
# Presentation
|
||||
|
||||
HTTP routes, request/response DTOs, and API wiring belong in this layer.
|
||||
@@ -0,0 +1 @@
|
||||
"""Presentation layer for HTTP APIs."""
|
||||
@@ -0,0 +1,27 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from src.infrastructure.dependencies import check_dependencies
|
||||
|
||||
|
||||
app = FastAPI(title="AI Content Pipeline Backend")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> dict[str, str]:
|
||||
return {"service": "backend", "status": "ok"}
|
||||
|
||||
|
||||
@app.get("/health/dependencies")
|
||||
def dependency_health() -> JSONResponse:
|
||||
dependencies = check_dependencies()
|
||||
is_ok = all(status == "ok" for status in dependencies.values())
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200 if is_ok else 503,
|
||||
content={
|
||||
"service": "backend",
|
||||
"status": "ok" if is_ok else "error",
|
||||
"dependencies": dependencies,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
# Shared
|
||||
|
||||
Cross-layer backend primitives belong here. Keep this layer small and avoid
|
||||
placing domain rules in shared code.
|
||||
@@ -0,0 +1 @@
|
||||
"""Shared backend primitives."""
|
||||
Reference in New Issue
Block a user