feat: scaffold FastAPI app, config, and SQLite models
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
populate_by_name=True,
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
data_dir: Path = Field(default=Path("/data"), alias="DATA_DIR")
|
||||||
|
admin_password: str = Field(default="changeme", alias="ADMIN_PASSWORD")
|
||||||
|
secret_key: str = Field(default="dev-secret-change-me", alias="SECRET_KEY")
|
||||||
|
max_upload_mb: int = Field(default=200, alias="MAX_UPLOAD_MB")
|
||||||
|
rate_limit_per_second: int = Field(default=2, alias="RATE_LIMIT_PER_SECOND")
|
||||||
|
jwt_expire_hours: int = Field(default=12, alias="JWT_EXPIRE_HOURS")
|
||||||
|
host: str = Field(default="0.0.0.0", alias="HOST")
|
||||||
|
port: int = Field(default=8080, alias="PORT")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_url(self) -> str:
|
||||||
|
return f"sqlite:///{self.data_dir / 'mincedpad.db'}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uploads_dir(self) -> Path:
|
||||||
|
return self.data_dir / "uploads"
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
engine = create_engine(
|
||||||
|
settings.db_url,
|
||||||
|
connect_args={"check_same_thread": False},
|
||||||
|
)
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_db() -> Generator[Session, None, None]:
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
from app.db import Base, engine
|
||||||
|
from app import models # noqa: F401 — register models with Base.metadata
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(_app: FastAPI):
|
||||||
|
settings.data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
settings.uploads_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(title="MincedPad", lifespan=lifespan)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/health")
|
||||||
|
def health():
|
||||||
|
return {"ok": True, "name": "MincedPad"}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, DateTime, Integer, String, Text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Item(Base):
|
||||||
|
__tablename__ = "items"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
slug: Mapped[str] = mapped_column(String(32), unique=True, index=True)
|
||||||
|
kind: Mapped[str] = mapped_column(String(16)) # text|file
|
||||||
|
title: Mapped[str] = mapped_column(String(255), default="")
|
||||||
|
body: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
file_name: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||||
|
file_path: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||||
|
mime: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||||
|
size_bytes: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
burn_after_read: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||||
|
created_ip: Mapped[str] = mapped_column(String(64), default="")
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
view_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
burned: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class BannedIP(Base):
|
||||||
|
__tablename__ = "banned_ips"
|
||||||
|
|
||||||
|
ip: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||||
|
reason: Mapped[str] = mapped_column(String(255), default="")
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fastapi==0.115.6
|
||||||
|
uvicorn[standard]==0.34.0
|
||||||
|
sqlalchemy==2.0.36
|
||||||
|
python-multipart==0.0.20
|
||||||
|
pyjwt==2.10.1
|
||||||
|
httpx==0.28.1
|
||||||
|
pytest==8.3.4
|
||||||
|
pydantic-settings==2.7.0
|
||||||
Reference in New Issue
Block a user