Create RESTful API service with FastAPI and SQLite

- Set up project structure with FastAPI and dependency files
- Configure SQLAlchemy with SQLite database
- Implement user authentication using JWT tokens
- Create comprehensive API routes for user management
- Add health check endpoint for application monitoring
- Set up Alembic for database migrations
- Add detailed documentation in README.md
This commit is contained in:
Automated Action 2025-06-16 14:56:03 +00:00
parent 52b91e98b9
commit f207cc3c64
31 changed files with 1121 additions and 2 deletions

131
README.md
View File

@ -1,3 +1,130 @@
# FastAPI Application
# RESTAPIService
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A RESTful API service built with FastAPI and SQLite.
## Features
- FastAPI framework for high-performance API development
- SQLite database with SQLAlchemy ORM
- Alembic for database migrations
- JWT authentication with role-based access control
- Comprehensive user management API
- Health check endpoint
- Documentation with OpenAPI (Swagger UI and ReDoc)
## Requirements
- Python 3.8+
- Virtual environment (recommended)
## Environment Variables
The application uses the following environment variables:
| Variable | Description | Default Value |
|-----------------------------|---------------------------------------|--------------------|
| SECRET_KEY | Secret key for JWT token generation | supersecretkey |
| ACCESS_TOKEN_EXPIRE_MINUTES | Token expiration time in minutes | 30 |
## Installation
1. Clone the repository:
```bash
git clone https://github.com/yourusername/restapiservice.git
cd restapiservice
```
2. Create and activate a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
3. Install dependencies:
```bash
pip install -r requirements.txt
```
4. Run database migrations:
```bash
alembic upgrade head
```
5. Initialize the database with initial data:
```bash
python -m app.initial_data
```
## Running the Application
Start the application with uvicorn:
```bash
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
```
The API will be available at http://localhost:8000
## API Documentation
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- OpenAPI JSON: http://localhost:8000/openapi.json
## API Endpoints
### Root Endpoint
- `GET /`: Returns basic information about the API
### Health Check
- `GET /health`: Returns the health status of the application
### Authentication
- `POST /api/v1/auth/login`: Authenticate and get access token
- `POST /api/v1/auth/test-token`: Test if a token is valid
### Users
- `GET /api/v1/users/`: List all users (admin only)
- `POST /api/v1/users/`: Create a new user (admin only)
- `GET /api/v1/users/me`: Get current user information
- `PUT /api/v1/users/me`: Update current user information
- `GET /api/v1/users/{user_id}`: Get user by ID (admin only)
- `PUT /api/v1/users/{user_id}`: Update user by ID (admin only)
## Development
### Database Migrations
To create a new migration after modifying models:
```bash
alembic revision --autogenerate -m "description"
```
To apply migrations:
```bash
alembic upgrade head
```
### Code Style
This project uses Ruff for linting and formatting:
```bash
ruff check .
ruff format .
```
## License
This project is licensed under the MIT License - see the LICENSE file for details.

108
alembic.ini Normal file
View File

@ -0,0 +1,108 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = alembic
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# 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 alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# SQLite URL with absolute path
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 REVISION_SCRIPT_FILENAME
# 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

1
alembic/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration with SQLite.

83
alembic/env.py Normal file
View File

@ -0,0 +1,83 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# 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.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from app.db.base_models import Base # noqa
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() -> None:
"""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() -> None:
"""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, # Key configuration for SQLite
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
alembic/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() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,41 @@
"""Initial migration
Revision ID: 01_initial_migration
Revises:
Create Date: 2023-05-10 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "01_initial_migration"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create users table
op.create_table(
"user",
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=True),
sa.Column("is_superuser", sa.Boolean(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True)
op.create_index(op.f("ix_user_id"), "user", ["id"], unique=False)
def downgrade() -> None:
op.drop_index(op.f("ix_user_id"), table_name="user")
op.drop_index(op.f("ix_user_email"), table_name="user")
op.drop_table("user")

0
app/__init__.py Normal file
View File

0
app/api/__init__.py Normal file
View File

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

@ -0,0 +1,87 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
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
# OAuth2 scheme for token authentication
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_PREFIX}/auth/login")
def get_current_user(
db: Session = Depends(get_db),
token: str = Depends(oauth2_scheme),
) -> User:
"""
Get the current user based on the provided JWT token.
Args:
db: Database session.
token: JWT token from the Authorization header.
Returns:
The current authenticated user.
Raises:
HTTPException: If the token is invalid or the user is not found.
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
# Decode JWT token
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
token_data = TokenPayload(**payload)
if token_data.sub is None:
raise credentials_exception
except JWTError:
raise credentials_exception
# Get the user from the database
user = db.query(User).filter(User.id == token_data.sub).first()
if user is None:
raise credentials_exception
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user",
)
return user
def get_current_active_superuser(
current_user: User = Depends(get_current_user),
) -> User:
"""
Get the current user and verify they are a superuser.
Args:
current_user: Current authenticated user.
Returns:
The current authenticated 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

View File

61
app/api/endpoints/auth.py Normal file
View File

@ -0,0 +1,61 @@
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.core.config import settings
from app.core.security import create_access_token, verify_password
from app.db.session import get_db
from app.models.user import User
from app.schemas.token import Token
from app.api.deps import get_current_user
router = APIRouter()
@router.post("/login", response_model=Token)
async 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.
"""
# Try to authenticate the user
user = db.query(User).filter(User.email == form_data.username).first()
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
if not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
if 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("/test-token", response_model=dict)
async def test_token(current_user: User = Depends(get_current_user)) -> Any:
"""
Test access token endpoint.
"""
return {"msg": "Token is valid", "user_id": current_user.id}

View File

@ -0,0 +1,25 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.db.session import get_db
router = APIRouter()
@router.get("/health", tags=["Health"])
async def health_check(db: Session = Depends(get_db)):
"""
Health check endpoint that reports on the application's health.
Returns OK if the application is running and can connect to the database.
"""
try:
# Try to execute a simple query to check database connectivity
db.execute("SELECT 1")
db_status = "OK"
except Exception as e:
db_status = f"Error: {str(e)}"
return {
"status": "OK",
"database": db_status,
}

165
app/api/endpoints/users.py Normal file
View File

@ -0,0 +1,165 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session
from app.api.deps import get_current_active_superuser, get_current_user
from app.core.security import get_password_hash
from app.db.session import get_db
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("/", 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 superusers can access this endpoint.
"""
users = db.query(User).offset(skip).limit(limit).all()
return users
@router.post("/", response_model=UserSchema)
def create_user(
*,
db: Session = Depends(get_db),
user_in: UserCreate,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Create new user.
Only superusers can create new users.
"""
# 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="The user with this email already exists in the system",
)
# Create new user
user = User(
email=user_in.email,
hashed_password=get_password_hash(user_in.password),
full_name=user_in.full_name,
is_active=user_in.is_active,
is_superuser=False,
)
db.add(user)
db.commit()
db.refresh(user)
return user
@router.get("/me", response_model=UserSchema)
def read_user_me(
current_user: User = Depends(get_current_user),
) -> Any:
"""
Get current user.
"""
return current_user
@router.put("/me", response_model=UserSchema)
def update_user_me(
*,
db: Session = Depends(get_db),
user_in: UserUpdate,
current_user: User = Depends(get_current_user),
) -> Any:
"""
Update own user.
"""
user_data = jsonable_encoder(current_user)
# Update user data
update_data = user_in.model_dump(exclude_unset=True)
# Hash password if it's provided
if "password" in update_data and update_data["password"]:
update_data["hashed_password"] = get_password_hash(update_data.pop("password"))
# Update user fields
for field in user_data:
if field in update_data:
setattr(current_user, field, update_data[field])
db.add(current_user)
db.commit()
db.refresh(current_user)
return current_user
@router.get("/{user_id}", response_model=UserSchema)
def read_user_by_id(
user_id: int,
current_user: User = Depends(get_current_active_superuser),
db: Session = Depends(get_db),
) -> Any:
"""
Get a specific user by id.
Only superusers can access other users.
"""
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.put("/{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 superusers can update users.
"""
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 data
update_data = user_in.model_dump(exclude_unset=True)
# Hash password if it's provided
if "password" in update_data and update_data["password"]:
update_data["hashed_password"] = get_password_hash(update_data.pop("password"))
# Update user fields
for field in update_data:
if hasattr(user, field):
setattr(user, field, update_data[field])
db.add(user)
db.commit()
db.refresh(user)
return user

16
app/api/routes.py Normal file
View File

@ -0,0 +1,16 @@
from fastapi import APIRouter
from app.api.endpoints import auth, health, users
from app.core.config import settings
# Create the main API router
api_router = APIRouter()
# Include all endpoint routers
api_router.include_router(health.router, tags=["Health"])
api_router.include_router(
auth.router, prefix=f"{settings.API_V1_PREFIX}/auth", tags=["Authentication"]
)
api_router.include_router(
users.router, prefix=f"{settings.API_V1_PREFIX}/users", tags=["Users"]
)

0
app/core/__init__.py Normal file
View File

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

@ -0,0 +1,43 @@
from typing import List, Optional
from pydantic_settings import BaseSettings
from pydantic import validator
import os
class Settings(BaseSettings):
# API information
PROJECT_NAME: str = "RESTAPIService"
DESCRIPTION: str = "A RESTful API service built with FastAPI and SQLite"
VERSION: str = "0.1.0"
API_V1_PREFIX: str = "/api/v1"
# Security
SECRET_KEY: str = os.environ.get("SECRET_KEY", "supersecretkey")
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# CORS
ALLOWED_HOSTS: List[str] = ["*"]
# Database
SQLALCHEMY_DATABASE_URI: Optional[str] = None
@validator("SQLALCHEMY_DATABASE_URI", pre=True)
def assemble_db_uri(cls, v, values):
if v:
return v
from pathlib import Path
# Set up database directory
db_dir = Path("/app") / "storage" / "db"
db_dir.mkdir(parents=True, exist_ok=True)
return f"sqlite:////{db_dir}/db.sqlite"
class Config:
env_file = ".env"
case_sensitive = True
settings = Settings()

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

@ -0,0 +1,62 @@
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 context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def create_access_token(
subject: Union[str, Any], expires_delta: Optional[timedelta] = None
) -> str:
"""
Create a JWT access token.
Args:
subject: The subject of the token, typically the user ID.
expires_delta: Optional timedelta for token expiration.
Returns:
Encoded JWT token as a string.
"""
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)}
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verify if a plain password matches a hashed password.
Args:
plain_password: The plain text password.
hashed_password: The hashed password from the database.
Returns:
True if passwords match, False otherwise.
"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""
Hash a password using the configured password hashing algorithm.
Args:
password: The plain text password to hash.
Returns:
A hashed password string.
"""
return pwd_context.hash(password)

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

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

@ -0,0 +1,4 @@
from sqlalchemy.ext.declarative import declarative_base
# Create a base class for SQLAlchemy models
Base = declarative_base()

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

@ -0,0 +1,25 @@
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy import Column, Integer, DateTime
from datetime import datetime
from app.db.base import Base
class BaseModel(Base):
"""
Base model class that includes common fields and functionality
for all models.
"""
__abstract__ = True
id = Column(Integer, primary_key=True, index=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@declared_attr
def __tablename__(cls) -> str:
"""
Generate table name automatically from the class name.
"""
return cls.__name__.lower()

5
app/db/base_models.py Normal file
View File

@ -0,0 +1,5 @@
# Import all the models, so that Base has them before being
# imported by Alembic
from app.db.base import Base # noqa
from app.models.user import User # noqa

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

@ -0,0 +1,26 @@
from sqlalchemy.orm import Session
from app.core.security import get_password_hash
from app.models.user import User
def init_db(db: Session) -> None:
"""
Initialize the database with a superuser if one doesn't exist.
"""
# Check if there are any users in the database
user = db.query(User).first()
if not user:
# Create a superuser
superuser = User(
email="admin@example.com",
hashed_password=get_password_hash("adminpassword"),
full_name="Admin User",
is_active=True,
is_superuser=True,
)
db.add(superuser)
db.commit()
db.refresh(superuser)

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

@ -0,0 +1,29 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from pathlib import Path
from app.core.config import settings
# 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 = settings.SQLALCHEMY_DATABASE_URI
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}, # Needed for SQLite
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
"""
Dependency function that yields db sessions
"""
db = SessionLocal()
try:
yield db
finally:
db.close()

32
app/initial_data.py Normal file
View File

@ -0,0 +1,32 @@
import logging
from app.db.init_db import init_db
from app.db.session import SessionLocal
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def init() -> None:
"""
Initialize the database with initial data.
"""
db = SessionLocal()
try:
init_db(db)
logger.info("Initial data created")
finally:
db.close()
def main() -> None:
"""
Main function to initialize the database.
"""
logger.info("Creating initial data")
init()
logger.info("Initial data creation completed")
if __name__ == "__main__":
main()

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

@ -0,0 +1 @@
from app.models.user import User # noqa

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

@ -0,0 +1,15 @@
from sqlalchemy import Column, String, Boolean
from app.db.base_class import BaseModel
class User(BaseModel):
"""
User model for authentication and authorization.
"""
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)
is_superuser = Column(Boolean, default=False)

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

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

@ -0,0 +1,19 @@
from pydantic import BaseModel
from typing import Optional
class Token(BaseModel):
"""
Schema for the token response.
"""
access_token: str
token_type: str
class TokenPayload(BaseModel):
"""
Schema for the token payload.
"""
sub: Optional[int] = None

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

@ -0,0 +1,61 @@
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime
class UserBase(BaseModel):
"""
Base schema with common user attributes.
"""
email: EmailStr
full_name: Optional[str] = None
is_active: Optional[bool] = True
class UserCreate(UserBase):
"""
Schema for creating a new user, includes password.
"""
password: str = Field(..., min_length=8)
class UserUpdate(BaseModel):
"""
Schema for updating an existing user.
"""
email: Optional[EmailStr] = None
full_name: Optional[str] = None
password: Optional[str] = Field(None, min_length=8)
is_active: Optional[bool] = None
class UserInDBBase(UserBase):
"""
Base schema for user data from the database.
"""
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class User(UserInDBBase):
"""
Schema for user data to return to client.
"""
pass
class UserInDB(UserInDBBase):
"""
Schema for user data with hashed_password field.
"""
hashed_password: str

46
main.py Normal file
View File

@ -0,0 +1,46 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.routes import api_router
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
version=settings.VERSION,
description=settings.DESCRIPTION,
openapi_url="/openapi.json",
docs_url="/docs",
redoc_url="/redoc",
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allow all origins
allow_credentials=True,
allow_methods=["*"], # Allow all methods
allow_headers=["*"], # Allow all headers
)
# Include API router
app.include_router(api_router)
@app.get("/", tags=["Root"])
async def root():
"""
Root endpoint that returns basic information about the API.
"""
return {
"title": settings.PROJECT_NAME,
"description": settings.DESCRIPTION,
"version": settings.VERSION,
"docs": "/docs",
"health": "/health",
}
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

13
requirements.txt Normal file
View File

@ -0,0 +1,13 @@
fastapi>=0.104.0
uvicorn>=0.23.2
sqlalchemy>=2.0.22
alembic>=1.12.0
pydantic>=2.4.2
pydantic-settings>=2.0.3
python-jose>=3.3.0
passlib>=1.7.4
python-multipart>=0.0.6
bcrypt>=4.0.1
ruff>=0.1.3
httpx>=0.24.1
python-dotenv>=1.0.0