Update code via agent code generation

This commit is contained in:
Automated Action 2025-06-08 21:57:05 +00:00
parent c3b0efedfe
commit 2586769e3b
24 changed files with 1166 additions and 0 deletions

84
alembic.ini Normal file
View File

@ -0,0 +1,84 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; this defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat migrations/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks=black
# black.type=console_scripts
# black.entrypoint=black
# black.options=-l 79
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

120
app/api/deps.py Normal file
View File

@ -0,0 +1,120 @@
"""API dependencies module."""
from typing import Generator
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.security import verify_password
from app.db.session import get_db
from app.models.user import User
from app.schemas.token import TokenPayload
# OAuth2 token URL
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")
def get_current_user(
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
) -> User:
"""
Get the current user from the token.
Args:
db: Database session
token: JWT token
Returns:
User: The authenticated user
Raises:
HTTPException: If the token is invalid or the user doesn't exist
"""
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
token_data = TokenPayload(**payload)
except (JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
user = db.query(User).filter(User.id == token_data.sub).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return user
def get_current_active_user(
current_user: User = Depends(get_current_user),
) -> User:
"""
Get the current active user.
Args:
current_user: The authenticated user
Returns:
User: The active authenticated user
Raises:
HTTPException: If the user is inactive
"""
if not current_user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return current_user
def get_current_active_superuser(
current_user: User = Depends(get_current_active_user),
) -> User:
"""
Get the current active superuser.
Args:
current_user: The authenticated active user
Returns:
User: The active superuser
Raises:
HTTPException: If the user is not a superuser
"""
if not current_user.is_superuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return current_user
def authenticate_user(db: Session, email: str, password: str) -> Optional[User]:
"""
Authenticate a user by email and password.
Args:
db: Database session
email: User email
password: User password
Returns:
Optional[User]: The authenticated user or None if authentication fails
"""
user = db.query(User).filter(User.email == email).first()
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user

View File

@ -0,0 +1 @@
"""API endpoints package."""

View File

@ -0,0 +1,92 @@
"""Authentication endpoints."""
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.api.deps import authenticate_user
from app.core.config import settings
from app.core.security import create_access_token, get_password_hash
from app.db.session import get_db
from app.models.user import User
from app.schemas.token import Token
from app.schemas.user import UserCreate, User as UserSchema
router = APIRouter()
@router.post("/login", response_model=Token)
def login_access_token(
db: Session = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
) -> Any:
"""
OAuth2 compatible token login, get an access token for future requests.
Args:
db: Database session
form_data: Form data with username (email) and password
Returns:
Token: Access token for future requests
"""
user = authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
elif not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
"access_token": create_access_token(
user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}
@router.post("/register", response_model=UserSchema, status_code=status.HTTP_201_CREATED)
def register_user(
*,
db: Session = Depends(get_db),
user_in: UserCreate,
) -> Any:
"""
Register a new user.
Args:
db: Database session
user_in: User data
Returns:
User: The created user
"""
# Check if a user with this email already exists
user = db.query(User).filter(User.email == user_in.email).first()
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="A user with this email already exists",
)
# Create new user
user = User(
email=user_in.email,
hashed_password=get_password_hash(user_in.password),
full_name=user_in.full_name,
is_superuser=user_in.is_superuser,
is_active=user_in.is_active,
)
db.add(user)
db.commit()
db.refresh(user)
return user

View File

@ -0,0 +1,224 @@
"""User endpoints."""
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.api.deps import (
get_current_active_superuser,
get_current_active_user,
get_db,
)
from app.core.security import get_password_hash
from app.models.user import User
from app.schemas.user import User as UserSchema
from app.schemas.user import UserCreate, UserUpdate
router = APIRouter()
@router.get("/me", response_model=UserSchema)
def read_current_user(
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get current user information.
Args:
current_user: Current active user
Returns:
User: Current user information
"""
return current_user
@router.patch("/me", response_model=UserSchema)
def update_current_user(
*,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
user_in: UserUpdate,
) -> Any:
"""
Update current user information.
Args:
db: Database session
current_user: Current active user
user_in: User update data
Returns:
User: Updated user information
"""
# Update user attributes
for field, value in user_in.model_dump(exclude_unset=True).items():
if field == "password" and value:
setattr(current_user, "hashed_password", get_password_hash(value))
elif field != "password":
setattr(current_user, field, value)
db.add(current_user)
db.commit()
db.refresh(current_user)
return current_user
@router.get("", response_model=List[UserSchema])
def read_users(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Retrieve users. Only for superusers.
Args:
db: Database session
skip: Skip first N users
limit: Limit number of users to return
current_user: Current active superuser
Returns:
List[User]: List of users
"""
users = db.query(User).offset(skip).limit(limit).all()
return users
@router.post("", response_model=UserSchema, status_code=status.HTTP_201_CREATED)
def create_user(
*,
db: Session = Depends(get_db),
user_in: UserCreate,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Create a new user. Only for superusers.
Args:
db: Database session
user_in: User creation data
current_user: Current active superuser
Returns:
User: Created user
"""
# Check if user with this email already exists
user = db.query(User).filter(User.email == user_in.email).first()
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="A user with this email already exists",
)
# Create new user
user = User(
email=user_in.email,
hashed_password=get_password_hash(user_in.password),
full_name=user_in.full_name,
is_superuser=user_in.is_superuser,
is_active=user_in.is_active,
)
db.add(user)
db.commit()
db.refresh(user)
return user
@router.get("/{user_id}", response_model=UserSchema)
def read_user(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Get a specific user by id. Only for superusers.
Args:
user_id: User ID
db: Database session
current_user: Current active superuser
Returns:
User: User with given ID
"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
return user
@router.patch("/{user_id}", response_model=UserSchema)
def update_user(
*,
db: Session = Depends(get_db),
user_id: int,
user_in: UserUpdate,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Update a user. Only for superusers.
Args:
db: Database session
user_id: User ID
user_in: User update data
current_user: Current active superuser
Returns:
User: Updated user
"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
# Update user attributes
for field, value in user_in.model_dump(exclude_unset=True).items():
if field == "password" and value:
setattr(user, "hashed_password", get_password_hash(value))
elif field != "password":
setattr(user, field, value)
db.add(user)
db.commit()
db.refresh(user)
return user
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
def delete_user(
*,
db: Session = Depends(get_db),
user_id: int,
current_user: User = Depends(get_current_active_superuser),
) -> None:
"""
Delete a user. Only for superusers.
Args:
db: Database session
user_id: User ID
current_user: Current active superuser
"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
db.delete(user)
db.commit()
return None

10
app/api/v1/router.py Normal file
View File

@ -0,0 +1,10 @@
"""API router module for v1 endpoints."""
from fastapi import APIRouter
from app.api.v1.endpoints import auth, users
api_router = APIRouter()
# Include routers from different endpoint modules
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
api_router.include_router(users.router, prefix="/users", tags=["Users"])

44
app/core/config.py Normal file
View File

@ -0,0 +1,44 @@
"""Application configuration module."""
import os
from pathlib import Path
from typing import Any, Dict, List, Optional
from pydantic import AnyHttpUrl, EmailStr, Field, validator
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""Application settings."""
# Base settings
PROJECT_NAME: str = "User Authentication Service"
PROJECT_DESCRIPTION: str = "FastAPI service for user authentication"
VERSION: str = "0.1.0"
API_V1_STR: str = "/api/v1"
DEBUG: bool = False
HOST: str = "0.0.0.0"
PORT: int = 8000
# Security settings
SECRET_KEY: str = os.getenv("SECRET_KEY", "CHANGE_THIS_SECRET_KEY_IN_PRODUCTION")
ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
ALGORITHM: str = "HS256"
# Super admin settings (for first user creation)
FIRST_SUPERUSER_EMAIL: Optional[EmailStr] = os.getenv("FIRST_SUPERUSER_EMAIL")
FIRST_SUPERUSER_PASSWORD: Optional[str] = os.getenv("FIRST_SUPERUSER_PASSWORD")
# CORS settings
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
@validator("BACKEND_CORS_ORIGINS", pre=True)
def assemble_cors_origins(cls, v: Any) -> List[str]:
"""Parse and validate CORS origins."""
if isinstance(v, str) and not v.startswith("["):
return [i.strip() for i in v.split(",")]
if isinstance(v, (list, str)):
return v
raise ValueError(v)
settings = Settings()

38
app/core/security.py Normal file
View File

@ -0,0 +1,38 @@
"""Security utilities for authentication and authorization."""
from datetime import datetime, timedelta
from typing import Any, Optional, Union
from jose import jwt
from passlib.context import CryptContext
from app.core.config import settings
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against a hash."""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""Generate a password hash."""
return pwd_context.hash(password)
def create_access_token(
subject: Union[str, Any], expires_delta: Optional[timedelta] = None
) -> str:
"""Create a JWT access token."""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(
to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM
)
return encoded_jwt

6
app/db/__init__.py Normal file
View File

@ -0,0 +1,6 @@
"""Database package."""
from app.db.base import Base
from app.db.init_db import init_db
from app.db.session import SessionLocal, engine, get_db
__all__ = ["Base", "init_db", "get_db", "engine", "SessionLocal"]

4
app/db/base.py Normal file
View File

@ -0,0 +1,4 @@
"""SQLAlchemy Base model definition."""
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

24
app/db/base_class.py Normal file
View File

@ -0,0 +1,24 @@
"""Base class for SQLAlchemy models."""
import datetime
from typing import Any
from sqlalchemy import Column, DateTime
from sqlalchemy.ext.declarative import declared_attr
class Base:
"""Base class for SQLAlchemy models."""
# Generate __tablename__ automatically
@declared_attr
def __tablename__(cls) -> str:
return cls.__name__.lower()
# Common columns for all models
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
updated_at = Column(
DateTime,
default=datetime.datetime.utcnow,
onupdate=datetime.datetime.utcnow,
nullable=False,
)

43
app/db/init_db.py Normal file
View File

@ -0,0 +1,43 @@
"""Database initialization module."""
import logging
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.security import get_password_hash
from app.models.user import User
from app.schemas.user import UserCreate
logger = logging.getLogger(__name__)
def init_db(db: Session) -> None:
"""Initialize database with initial data."""
# Check if there are any users in the database
user = db.query(User).first()
if user:
logger.info("Database already initialized, skipping")
return
# Create first superuser if environment variables are set
if settings.FIRST_SUPERUSER_EMAIL and settings.FIRST_SUPERUSER_PASSWORD:
logger.info("Creating initial superuser")
user_in = UserCreate(
email=settings.FIRST_SUPERUSER_EMAIL,
password=settings.FIRST_SUPERUSER_PASSWORD,
full_name="Initial Superuser",
is_superuser=True,
)
db_user = User(
email=user_in.email,
hashed_password=get_password_hash(user_in.password),
full_name=user_in.full_name,
is_superuser=user_in.is_superuser,
is_active=True,
)
db.add(db_user)
db.commit()
logger.info(f"Superuser {settings.FIRST_SUPERUSER_EMAIL} created")

27
app/db/session.py Normal file
View File

@ -0,0 +1,27 @@
"""Database session setup."""
from pathlib import Path
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# Create the database directory if it doesn't exist
DB_DIR = Path("/app") / "storage" / "db"
DB_DIR.mkdir(parents=True, exist_ok=True)
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/db.sqlite"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
"""Get a database session."""
db = SessionLocal()
try:
yield db
finally:
db.close()

134
app/middleware/auth.py Normal file
View File

@ -0,0 +1,134 @@
"""Authentication middleware."""
from typing import Callable, Optional
from fastapi import Depends, FastAPI, Header, HTTPException, Request, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from app.core.config import settings
from app.db.session import get_db
from app.models.user import User
from app.schemas.token import TokenPayload
async def get_token_from_header(
authorization: Optional[str] = Header(None),
) -> Optional[str]:
"""
Extract token from the Authorization header.
Args:
authorization: Authorization header
Returns:
Optional[str]: JWT token or None
"""
if not authorization:
return None
scheme, _, token = authorization.partition(" ")
if scheme.lower() != "bearer":
return None
return token
async def validate_token(
token: Optional[str],
db: Session,
) -> Optional[User]:
"""
Validate JWT token and return the user.
Args:
token: JWT token
db: Database session
Returns:
Optional[User]: User or None if token is invalid
"""
if not token:
return None
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
token_data = TokenPayload(**payload)
if token_data.sub is None:
return None
except (JWTError, ValueError):
return None
user = db.query(User).filter(User.id == token_data.sub).first()
if not user or not user.is_active:
return None
return user
class AuthMiddleware:
"""Authentication middleware for FastAPI applications."""
def __init__(self, app: FastAPI):
"""
Initialize middleware.
Args:
app: FastAPI application
"""
self.app = app
async def __call__(self, request: Request, call_next: Callable):
"""
Process request through middleware.
Args:
request: FastAPI request
call_next: Next middleware/endpoint in the chain
Returns:
Response: FastAPI response
"""
# Skip authentication for auth endpoints and docs
path = request.url.path
if (
path.startswith(f"{settings.API_V1_STR}/auth")
or path == "/"
or path == "/health"
or path.startswith("/docs")
or path.startswith("/redoc")
or path.startswith("/openapi.json")
):
return await call_next(request)
# Get token from header
token = await get_token_from_header(request.headers.get("Authorization"))
# For protected endpoints, token is required
if not token:
return HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
# Validate token
db = next(get_db())
user = await validate_token(token, db)
if not user:
return HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
# Attach user to request state
request.state.user = user
# Continue with the request
return await call_next(request)

4
app/models/__init__.py Normal file
View File

@ -0,0 +1,4 @@
"""Models package."""
from app.models.user import User
__all__ = ["User"]

18
app/models/user.py Normal file
View File

@ -0,0 +1,18 @@
"""User model module."""
from sqlalchemy import Boolean, Column, Integer, String
from sqlalchemy.orm import relationship
from app.db.base import Base
class User(Base):
"""User model representing registered users in the system."""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
full_name = Column(String, nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
is_superuser = Column(Boolean, default=False, nullable=False)

5
app/schemas/__init__.py Normal file
View File

@ -0,0 +1,5 @@
"""Schemas package."""
from app.schemas.token import Token, TokenPayload
from app.schemas.user import User, UserCreate, UserInDB, UserUpdate
__all__ = ["User", "UserCreate", "UserInDB", "UserUpdate", "Token", "TokenPayload"]

17
app/schemas/token.py Normal file
View File

@ -0,0 +1,17 @@
"""Token schema module."""
from typing import Optional
from pydantic import BaseModel
class Token(BaseModel):
"""Token schema."""
access_token: str
token_type: str
class TokenPayload(BaseModel):
"""Token payload schema."""
sub: Optional[int] = None

48
app/schemas/user.py Normal file
View File

@ -0,0 +1,48 @@
"""User schema module."""
from typing import Optional
from pydantic import BaseModel, EmailStr, Field
class UserBase(BaseModel):
"""Base user schema with common attributes."""
email: EmailStr
full_name: Optional[str] = None
is_active: bool = True
is_superuser: bool = False
class UserCreate(UserBase):
"""User creation schema."""
password: str = Field(..., min_length=8)
class UserUpdate(UserBase):
"""User update schema."""
password: Optional[str] = Field(None, min_length=8)
class UserInDBBase(UserBase):
"""Base schema for users in the database."""
id: int
class Config:
"""Pydantic configuration."""
from_attributes = True
class User(UserInDBBase):
"""User schema returned from API calls."""
pass
class UserInDB(UserInDBBase):
"""User schema with hashed password."""
hashed_password: str

57
main.py Normal file
View File

@ -0,0 +1,57 @@
"""Main application entry point."""
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.responses import JSONResponse
from app.api.v1.router import api_router
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
description=settings.PROJECT_DESCRIPTION,
version=settings.VERSION,
openapi_url="/openapi.json",
docs_url="/docs",
redoc_url="/redoc",
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API router
app.include_router(api_router, prefix="/api/v1")
@app.get("/", tags=["Root"])
async def root():
"""Root endpoint returning service information."""
return JSONResponse(
{
"name": settings.PROJECT_NAME,
"version": settings.VERSION,
"documentation": "/docs",
"health": "/health",
}
)
@app.get("/health", tags=["Health"])
async def health_check():
"""Health check endpoint."""
return JSONResponse({"status": "healthy"})
if __name__ == "__main__":
uvicorn.run(
"main:app",
host=settings.HOST,
port=settings.PORT,
reload=settings.DEBUG,
)

87
migrations/env.py Normal file
View File

@ -0,0 +1,87 @@
import sys
from logging.config import fileConfig
from pathlib import Path
from alembic import context
from sqlalchemy import engine_from_config, pool
# Add the parent directory to sys.path
sys.path.append(str(Path(__file__).parent.parent))
# Import the SQLAlchemy base from app/db/base.py
from app.db.base import Base
from app.models import User # noqa
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
is_sqlite = connection.dialect.name == 'sqlite'
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=is_sqlite, # Enable batch mode for SQLite
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,43 @@
"""create users table
Revision ID: 1b35ba5b0d34
Revises:
Create Date: 2023-09-25 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1b35ba5b0d34'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# Create users table
op.create_table(
'users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('full_name', sa.String(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False, default=True),
sa.Column('is_superuser', sa.Boolean(), nullable=False, default=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id'),
)
# Create indexes
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
def downgrade():
# Drop indexes
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')
# Drop table
op.drop_table('users')

12
requirements.txt Normal file
View File

@ -0,0 +1,12 @@
fastapi>=0.103.1,<0.104.0
uvicorn>=0.23.2,<0.24.0
sqlalchemy>=2.0.21,<2.1.0
pydantic>=2.4.2,<2.5.0
pydantic-settings>=2.0.3,<2.1.0
alembic>=1.12.0,<1.13.0
python-jose[cryptography]>=3.3.0,<3.4.0
passlib[bcrypt]>=1.7.4,<1.8.0
python-multipart>=0.0.6,<0.1.0
ruff>=0.0.290,<0.1.0
email-validator>=2.0.0,<2.1.0
pathlib>=1.0.1,<1.1.0