Implement Small Business Inventory Management System

- Created project structure with FastAPI framework
- Set up SQLite database with SQLAlchemy ORM
- Implemented models: User, Item, Supplier, Transaction
- Created CRUD operations for all models
- Added API endpoints for all resources
- Implemented JWT authentication and authorization
- Added Alembic migration scripts
- Created health endpoint
- Updated documentation

generated with BackendIM... (backend.im)
This commit is contained in:
Automated Action 2025-05-13 09:48:37 +00:00
parent 7b50fb73bf
commit 4dc182713c
35 changed files with 1753 additions and 2 deletions

View File

@ -1,3 +1,98 @@
# FastAPI Application
# Small Business Inventory Management System
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A FastAPI application for managing inventory for small businesses.
## Features
- **User Authentication**: Secure user authentication with JWT tokens
- **Inventory Management**: Track inventory items with details like SKU, quantity, price, and location
- **Supplier Management**: Manage supplier information and relationships with inventory items
- **Transaction Tracking**: Record and monitor inventory transactions (purchases, sales, adjustments, returns)
- **API Documentation**: Interactive API documentation using FastAPI's built-in Swagger UI
## Tech Stack
- **Backend**: Python 3.7+ with FastAPI
- **Database**: SQLite with SQLAlchemy ORM
- **Authentication**: JWT token-based authentication
- **Migration**: Alembic for database migrations
## Project Structure
```
.
├── alembic/ # Database migration scripts
├── app/ # Main application code
│ ├── api/ # API endpoints
│ ├── core/ # Core functionality (config, security)
│ ├── crud/ # CRUD operations
│ ├── db/ # Database session and connection
│ ├── models/ # SQLAlchemy models
│ └── schemas/ # Pydantic schemas/models
├── storage/ # Data storage
│ └── db/ # SQLite database files
├── main.py # Application entry point
├── alembic.ini # Alembic configuration
└── requirements.txt # Project dependencies
```
## Installation and Setup
1. Clone the repository
2. Install dependencies:
```
pip install -r requirements.txt
```
3. Apply migrations:
```
alembic upgrade head
```
4. Run the application:
```
uvicorn main:app --reload
```
## API Documentation
Once the application is running, you can access:
- Swagger UI documentation: http://localhost:8000/docs
- ReDoc documentation: http://localhost:8000/redoc
## API Endpoints
### Authentication
- `POST /api/v1/login/access-token` - Get JWT access token
- `POST /api/v1/login/test-token` - Test token validity
### Users
- `GET /api/v1/users/` - List users (admin only)
- `POST /api/v1/users/` - Create new user (admin only)
- `GET /api/v1/users/me` - Get current user info
- `PUT /api/v1/users/me` - Update current user info
- `GET /api/v1/users/{user_id}` - Get user by ID
- `PUT /api/v1/users/{user_id}` - Update user (admin only)
### Items
- `GET /api/v1/items/` - List inventory items (with optional filtering)
- `POST /api/v1/items/` - Add new inventory item
- `GET /api/v1/items/{id}` - Get item by ID
- `PUT /api/v1/items/{id}` - Update item
- `DELETE /api/v1/items/{id}` - Delete item (admin only)
### Suppliers
- `GET /api/v1/suppliers/` - List suppliers
- `POST /api/v1/suppliers/` - Add new supplier
- `GET /api/v1/suppliers/{id}` - Get supplier by ID
- `PUT /api/v1/suppliers/{id}` - Update supplier
- `DELETE /api/v1/suppliers/{id}` - Delete supplier (admin only)
### Transactions
- `GET /api/v1/transactions/` - List transactions (with optional filtering)
- `POST /api/v1/transactions/` - Record new transaction
- `GET /api/v1/transactions/{id}` - Get transaction by ID
- `PUT /api/v1/transactions/{id}` - Update transaction (admin only)
- `DELETE /api/v1/transactions/{id}` - Delete transaction (admin only)
## Health Check
- `GET /health` - Application health check endpoint

85
alembic.ini Normal file
View File

@ -0,0 +1,85 @@
# 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
# 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 alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat alembic/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

84
alembic/env.py Normal file
View File

@ -0,0 +1,84 @@
import os
import sys
from logging.config import fileConfig
from pathlib import Path
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.
fileConfig(config.config_file_name)
# Add the parent directory to sys.path
sys.path.insert(0, str(Path(__file__).parent.parent))
# Import the SQLAlchemy models
from app.db.base import Base
# Target 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:
context.configure(
connection=connection, target_metadata=target_metadata
)
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():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,108 @@
"""Initial migration
Revision ID: 001
Revises:
Create Date: 2025-05-13
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import sqlite
# revision identifiers, used by Alembic.
revision = '001'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# Create user table
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('full_name', sa.String(), nullable=True),
sa.Column('email', sa.String(), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('is_superuser', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
op.create_index(op.f('ix_user_full_name'), 'user', ['full_name'], unique=False)
op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
# Create supplier table
op.create_table('supplier',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('contact_name', sa.String(), nullable=True),
sa.Column('email', sa.String(), nullable=True),
sa.Column('phone', sa.String(), nullable=True),
sa.Column('address', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_supplier_id'), 'supplier', ['id'], unique=False)
op.create_index(op.f('ix_supplier_name'), 'supplier', ['name'], unique=False)
# Create item table
op.create_table('item',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('description', sa.String(), nullable=True),
sa.Column('sku', sa.String(), nullable=True),
sa.Column('quantity', sa.Integer(), nullable=True),
sa.Column('unit_price', sa.Float(), nullable=False),
sa.Column('category', sa.String(), nullable=True),
sa.Column('location', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('supplier_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['supplier_id'], ['supplier.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_item_category'), 'item', ['category'], unique=False)
op.create_index(op.f('ix_item_id'), 'item', ['id'], unique=False)
op.create_index(op.f('ix_item_name'), 'item', ['name'], unique=False)
op.create_index(op.f('ix_item_sku'), 'item', ['sku'], unique=True)
# Create transaction table
op.create_table('transaction',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('transaction_type', sa.Enum('PURCHASE', 'SALE', 'ADJUSTMENT', 'RETURN', name='transactiontype'), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.Column('price_per_unit', sa.Float(), nullable=False),
sa.Column('reference_number', sa.String(), nullable=True),
sa.Column('notes', sa.String(), nullable=True),
sa.Column('transaction_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('item_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['item_id'], ['item.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_transaction_id'), 'transaction', ['id'], unique=False)
def downgrade():
# Drop tables in reverse order
op.drop_index(op.f('ix_transaction_id'), table_name='transaction')
op.drop_table('transaction')
op.drop_index(op.f('ix_item_sku'), table_name='item')
op.drop_index(op.f('ix_item_name'), table_name='item')
op.drop_index(op.f('ix_item_id'), table_name='item')
op.drop_index(op.f('ix_item_category'), table_name='item')
op.drop_table('item')
op.drop_index(op.f('ix_supplier_name'), table_name='supplier')
op.drop_index(op.f('ix_supplier_id'), table_name='supplier')
op.drop_table('supplier')
op.drop_index(op.f('ix_user_id'), table_name='user')
op.drop_index(op.f('ix_user_full_name'), table_name='user')
op.drop_index(op.f('ix_user_email'), table_name='user')
op.drop_table('user')

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

@ -0,0 +1,10 @@
from fastapi import APIRouter
from app.api.endpoints import items, login, suppliers, transactions, users
api_router = APIRouter()
api_router.include_router(login.router, tags=["login"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(items.router, prefix="/items", tags=["items"])
api_router.include_router(suppliers.router, prefix="/suppliers", tags=["suppliers"])
api_router.include_router(transactions.router, prefix="/transactions", tags=["transactions"])

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

@ -0,0 +1,63 @@
from typing import Generator, Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app.core import security
from app.core.config import settings
from app.db.session import SessionLocal
from app.models.user import User
from app.schemas.token import TokenPayload
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/login/access-token"
)
def get_db() -> Generator:
try:
db = SessionLocal()
yield db
finally:
db.close()
def get_current_user(
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
) -> User:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
token_data = TokenPayload(**payload)
except (jwt.JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
user = db.query(User).filter(User.id == token_data.sub).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
def get_current_active_user(
current_user: User = Depends(get_current_user),
) -> User:
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
def get_current_active_superuser(
current_user: User = Depends(get_current_user),
) -> User:
if not current_user.is_superuser:
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
return current_user

129
app/api/endpoints/items.py Normal file
View File

@ -0,0 +1,129 @@
from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app import crud
from app.api import deps
from app.schemas.item import Item, ItemCreate, ItemUpdate
router = APIRouter()
@router.get("/", response_model=List[Item])
def read_items(
db: Session = Depends(deps.get_db),
skip: int = 0,
limit: int = 100,
category: Optional[str] = None,
supplier_id: Optional[int] = None,
current_user: Any = Depends(deps.get_current_active_user),
) -> Any:
"""
Retrieve items.
"""
if supplier_id:
items = crud.item.get_by_supplier(db, supplier_id=supplier_id, skip=skip, limit=limit)
elif category:
items = crud.item.get_by_category(db, category=category, skip=skip, limit=limit)
else:
items = crud.item.get_multi(db, skip=skip, limit=limit)
return items
@router.post("/", response_model=Item)
def create_item(
*,
db: Session = Depends(deps.get_db),
item_in: ItemCreate,
current_user: Any = Depends(deps.get_current_active_user),
) -> Any:
"""
Create new item.
"""
# Check if the item with this SKU already exists
item = crud.item.get_by_sku(db, sku=item_in.sku)
if item:
raise HTTPException(
status_code=400,
detail="An item with this SKU already exists in the system.",
)
# Check if supplier exists if supplier_id is provided
if item_in.supplier_id and not crud.supplier.get(db, id=item_in.supplier_id):
raise HTTPException(
status_code=400,
detail=f"Supplier with ID {item_in.supplier_id} does not exist.",
)
item = crud.item.create(db, obj_in=item_in)
return item
@router.put("/{id}", response_model=Item)
def update_item(
*,
db: Session = Depends(deps.get_db),
id: int,
item_in: ItemUpdate,
current_user: Any = Depends(deps.get_current_active_user),
) -> Any:
"""
Update an item.
"""
item = crud.item.get(db, id=id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
# Check if the new SKU already exists and belongs to a different item
if item_in.sku and item_in.sku != item.sku:
existing_item = crud.item.get_by_sku(db, sku=item_in.sku)
if existing_item and existing_item.id != id:
raise HTTPException(
status_code=400,
detail="An item with this SKU already exists in the system.",
)
# Check if supplier exists if supplier_id is provided
if item_in.supplier_id and item_in.supplier_id != item.supplier_id:
if not crud.supplier.get(db, id=item_in.supplier_id):
raise HTTPException(
status_code=400,
detail=f"Supplier with ID {item_in.supplier_id} does not exist.",
)
item = crud.item.update(db, db_obj=item, obj_in=item_in)
return item
@router.get("/{id}", response_model=Item)
def read_item(
*,
db: Session = Depends(deps.get_db),
id: int,
current_user: Any = Depends(deps.get_current_active_user),
) -> Any:
"""
Get item by ID.
"""
item = crud.item.get(db, id=id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
return item
@router.delete("/{id}", response_model=Item)
def delete_item(
*,
db: Session = Depends(deps.get_db),
id: int,
current_user: Any = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Delete an item.
"""
item = crud.item.get(db, id=id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
item = crud.item.remove(db, id=id)
return item

View File

@ -0,0 +1,46 @@
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app import crud
from app.api import deps
from app.core import security
from app.core.config import settings
from app.schemas.token import Token
from app.schemas.user import User
router = APIRouter()
@router.post("/login/access-token", response_model=Token)
def login_access_token(
db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends()
) -> Any:
"""
OAuth2 compatible token login, get an access token for future requests
"""
user = crud.user.authenticate(
db, email=form_data.username, password=form_data.password
)
if not user:
raise HTTPException(status_code=400, detail="Incorrect email or password")
elif not crud.user.is_active(user):
raise HTTPException(status_code=400, detail="Inactive user")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
"access_token": security.create_access_token(
user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}
@router.post("/login/test-token", response_model=User)
def test_token(current_user: User = Depends(deps.get_current_user)) -> Any:
"""
Test access token
"""
return current_user

View File

@ -0,0 +1,98 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app import crud
from app.api import deps
from app.schemas.supplier import Supplier, SupplierCreate, SupplierUpdate
router = APIRouter()
@router.get("/", response_model=List[Supplier])
def read_suppliers(
db: Session = Depends(deps.get_db),
skip: int = 0,
limit: int = 100,
current_user: Any = Depends(deps.get_current_active_user),
) -> Any:
"""
Retrieve suppliers.
"""
suppliers = crud.supplier.get_multi(db, skip=skip, limit=limit)
return suppliers
@router.post("/", response_model=Supplier)
def create_supplier(
*,
db: Session = Depends(deps.get_db),
supplier_in: SupplierCreate,
current_user: Any = Depends(deps.get_current_active_user),
) -> Any:
"""
Create new supplier.
"""
supplier = crud.supplier.create(db, obj_in=supplier_in)
return supplier
@router.put("/{id}", response_model=Supplier)
def update_supplier(
*,
db: Session = Depends(deps.get_db),
id: int,
supplier_in: SupplierUpdate,
current_user: Any = Depends(deps.get_current_active_user),
) -> Any:
"""
Update a supplier.
"""
supplier = crud.supplier.get(db, id=id)
if not supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
supplier = crud.supplier.update(db, db_obj=supplier, obj_in=supplier_in)
return supplier
@router.get("/{id}", response_model=Supplier)
def read_supplier(
*,
db: Session = Depends(deps.get_db),
id: int,
current_user: Any = Depends(deps.get_current_active_user),
) -> Any:
"""
Get supplier by ID.
"""
supplier = crud.supplier.get(db, id=id)
if not supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
return supplier
@router.delete("/{id}", response_model=Supplier)
def delete_supplier(
*,
db: Session = Depends(deps.get_db),
id: int,
current_user: Any = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Delete a supplier.
"""
supplier = crud.supplier.get(db, id=id)
if not supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
# Check if supplier has any items
items = crud.item.get_by_supplier(db, supplier_id=id)
if items:
raise HTTPException(
status_code=400,
detail="Cannot delete supplier with associated items. Remove the items first.",
)
supplier = crud.supplier.remove(db, id=id)
return supplier

View File

@ -0,0 +1,144 @@
from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app import crud
from app.api import deps
from app.models.transaction import TransactionType
from app.schemas.transaction import Transaction, TransactionCreate, TransactionUpdate
router = APIRouter()
@router.get("/", response_model=List[Transaction])
def read_transactions(
db: Session = Depends(deps.get_db),
skip: int = 0,
limit: int = 100,
item_id: Optional[int] = None,
transaction_type: Optional[TransactionType] = None,
current_user: Any = Depends(deps.get_current_active_user),
) -> Any:
"""
Retrieve transactions.
"""
if item_id:
transactions = crud.transaction.get_by_item(db, item_id=item_id, skip=skip, limit=limit)
elif transaction_type:
transactions = crud.transaction.get_by_transaction_type(
db, transaction_type=transaction_type, skip=skip, limit=limit
)
else:
transactions = crud.transaction.get_multi(db, skip=skip, limit=limit)
return transactions
@router.post("/", response_model=Transaction)
def create_transaction(
*,
db: Session = Depends(deps.get_db),
transaction_in: TransactionCreate,
current_user: Any = Depends(deps.get_current_active_user),
) -> Any:
"""
Create new transaction.
"""
# Check if item exists
item = crud.item.get(db, id=transaction_in.item_id)
if not item:
raise HTTPException(
status_code=400,
detail=f"Item with ID {transaction_in.item_id} does not exist.",
)
# Check if there's enough inventory for sales or returns
if transaction_in.transaction_type == TransactionType.SALE and item.quantity < transaction_in.quantity:
raise HTTPException(
status_code=400,
detail=f"Not enough inventory for item ID {transaction_in.item_id}. Available: {item.quantity}, Requested: {transaction_in.quantity}",
)
# Set the user ID if not provided
if not transaction_in.user_id:
transaction_in_dict = transaction_in.dict()
transaction_in_dict["user_id"] = current_user.id
transaction_in = TransactionCreate(**transaction_in_dict)
transaction = crud.transaction.create(db, obj_in=transaction_in)
return transaction
@router.get("/{id}", response_model=Transaction)
def read_transaction(
*,
db: Session = Depends(deps.get_db),
id: int,
current_user: Any = Depends(deps.get_current_active_user),
) -> Any:
"""
Get transaction by ID.
"""
transaction = crud.transaction.get(db, id=id)
if not transaction:
raise HTTPException(status_code=404, detail="Transaction not found")
return transaction
@router.put("/{id}", response_model=Transaction)
def update_transaction(
*,
db: Session = Depends(deps.get_db),
id: int,
transaction_in: TransactionUpdate,
current_user: Any = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Update a transaction. Only superusers can update transactions.
"""
transaction = crud.transaction.get(db, id=id)
if not transaction:
raise HTTPException(status_code=404, detail="Transaction not found")
# Check if item exists if item_id is provided
if transaction_in.item_id and transaction_in.item_id != transaction.item_id:
if not crud.item.get(db, id=transaction_in.item_id):
raise HTTPException(
status_code=400,
detail=f"Item with ID {transaction_in.item_id} does not exist.",
)
# TODO: Handle inventory adjustments when updating transactions
# This is complex and might require reversing the old transaction and applying the new one
transaction = crud.transaction.update(db, db_obj=transaction, obj_in=transaction_in)
return transaction
@router.delete("/{id}", response_model=Transaction)
def delete_transaction(
*,
db: Session = Depends(deps.get_db),
id: int,
current_user: Any = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Delete a transaction. Only superusers can delete transactions.
"""
transaction = crud.transaction.get(db, id=id)
if not transaction:
raise HTTPException(status_code=404, detail="Transaction not found")
# Reverse the inventory impact of this transaction
item = crud.item.get(db, id=transaction.item_id)
quantity_change = transaction.quantity
if transaction.transaction_type in [TransactionType.PURCHASE, TransactionType.ADJUSTMENT]:
quantity_change = -quantity_change
elif transaction.transaction_type in [TransactionType.SALE, TransactionType.RETURN]:
quantity_change = quantity_change
# Update item quantity
crud.item.update_quantity(db, item_id=transaction.item_id, quantity_change=quantity_change)
transaction = crud.transaction.remove(db, id=id)
return transaction

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

@ -0,0 +1,122 @@
from typing import Any, List
from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi.encoders import jsonable_encoder
from pydantic import EmailStr
from sqlalchemy.orm import Session
from app import crud
from app.api import deps
from app.core.config import settings
from app.schemas.user import User, UserCreate, UserUpdate
router = APIRouter()
@router.get("/", response_model=List[User])
def read_users(
db: Session = Depends(deps.get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Retrieve users.
"""
users = crud.user.get_multi(db, skip=skip, limit=limit)
return users
@router.post("/", response_model=User)
def create_user(
*,
db: Session = Depends(deps.get_db),
user_in: UserCreate,
current_user: User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Create new user.
"""
user = crud.user.get_by_email(db, email=user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="The user with this email already exists in the system.",
)
user = crud.user.create(db, obj_in=user_in)
return user
@router.put("/me", response_model=User)
def update_user_me(
*,
db: Session = Depends(deps.get_db),
password: str = Body(None),
full_name: str = Body(None),
email: EmailStr = Body(None),
current_user: User = Depends(deps.get_current_active_user),
) -> Any:
"""
Update own user.
"""
current_user_data = jsonable_encoder(current_user)
user_in = UserUpdate(**current_user_data)
if password is not None:
user_in.password = password
if full_name is not None:
user_in.full_name = full_name
if email is not None:
user_in.email = email
user = crud.user.update(db, db_obj=current_user, obj_in=user_in)
return user
@router.get("/me", response_model=User)
def read_user_me(
db: Session = Depends(deps.get_db),
current_user: User = Depends(deps.get_current_active_user),
) -> Any:
"""
Get current user.
"""
return current_user
@router.get("/{user_id}", response_model=User)
def read_user_by_id(
user_id: int,
current_user: User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Get a specific user by id.
"""
user = crud.user.get(db, id=user_id)
if user == current_user:
return user
if not crud.user.is_superuser(current_user):
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
return user
@router.put("/{user_id}", response_model=User)
def update_user(
*,
db: Session = Depends(deps.get_db),
user_id: int,
user_in: UserUpdate,
current_user: User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Update a user.
"""
user = crud.user.get(db, id=user_id)
if not user:
raise HTTPException(
status_code=404,
detail="The user with this id does not exist in the system",
)
user = crud.user.update(db, db_obj=user, obj_in=user_in)
return user

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

@ -0,0 +1,31 @@
import secrets
from pathlib import Path
from typing import Optional, Dict, Any, List
from pydantic import BaseSettings, validator
class Settings(BaseSettings):
API_V1_STR: str = "/api/v1"
SECRET_KEY: str = secrets.token_urlsafe(32)
# 60 minutes * 24 hours * 8 days = 8 days
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
PROJECT_NAME: str = "Small Business Inventory Management System"
# CORS allowed origins
BACKEND_CORS_ORIGINS: List[str] = ["*"]
@validator("BACKEND_CORS_ORIGINS", pre=True)
def assemble_cors_origins(cls, v: str | List[str]) -> List[str]:
if isinstance(v, str) and not v.startswith("["):
return [i.strip() for i in v.split(",")]
elif isinstance(v, (list, str)):
return v
raise ValueError(v)
class Config:
case_sensitive = True
settings = Settings()

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

@ -0,0 +1,33 @@
from datetime import datetime, timedelta
from typing import Any, Union
from jose import jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
ALGORITHM = "HS256"
def create_access_token(
subject: Union[str, Any], expires_delta: timedelta = None
) -> str:
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=ALGORITHM)
return encoded_jwt
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)

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

@ -0,0 +1,4 @@
from app.crud.crud_item import item
from app.crud.crud_supplier import supplier
from app.crud.crud_transaction import transaction
from app.crud.crud_user import user

66
app/crud/base.py Normal file
View File

@ -0,0 +1,66 @@
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.db.base_class import Base
ModelType = TypeVar("ModelType", bound=Base)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, model: Type[ModelType]):
"""
CRUD object with default methods to Create, Read, Update, Delete (CRUD).
**Parameters**
* `model`: A SQLAlchemy model class
* `schema`: A Pydantic model (schema) class
"""
self.model = model
def get(self, db: Session, id: Any) -> Optional[ModelType]:
return db.query(self.model).filter(self.model.id == id).first()
def get_multi(
self, db: Session, *, skip: int = 0, limit: int = 100
) -> List[ModelType]:
return db.query(self.model).offset(skip).limit(limit).all()
def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:
obj_in_data = jsonable_encoder(obj_in)
db_obj = self.model(**obj_in_data)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
self,
db: Session,
*,
db_obj: ModelType,
obj_in: Union[UpdateSchemaType, Dict[str, Any]]
) -> ModelType:
obj_data = jsonable_encoder(db_obj)
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.dict(exclude_unset=True)
for field in obj_data:
if field in update_data:
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def remove(self, db: Session, *, id: int) -> ModelType:
obj = db.query(self.model).get(id)
db.delete(obj)
db.commit()
return obj

49
app/crud/crud_item.py Normal file
View File

@ -0,0 +1,49 @@
from typing import List, Optional
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session
from app.crud.base import CRUDBase
from app.models.item import Item
from app.schemas.item import ItemCreate, ItemUpdate
class CRUDItem(CRUDBase[Item, ItemCreate, ItemUpdate]):
def get_by_sku(self, db: Session, *, sku: str) -> Optional[Item]:
return db.query(Item).filter(Item.sku == sku).first()
def get_by_supplier(
self, db: Session, *, supplier_id: int, skip: int = 0, limit: int = 100
) -> List[Item]:
return (
db.query(Item)
.filter(Item.supplier_id == supplier_id)
.offset(skip)
.limit(limit)
.all()
)
def get_by_category(
self, db: Session, *, category: str, skip: int = 0, limit: int = 100
) -> List[Item]:
return (
db.query(Item)
.filter(Item.category == category)
.offset(skip)
.limit(limit)
.all()
)
def update_quantity(
self, db: Session, *, item_id: int, quantity_change: int
) -> Item:
item = self.get(db, id=item_id)
if item:
item.quantity += quantity_change
db.add(item)
db.commit()
db.refresh(item)
return item
item = CRUDItem(Item)

15
app/crud/crud_supplier.py Normal file
View File

@ -0,0 +1,15 @@
from typing import List, Optional
from sqlalchemy.orm import Session
from app.crud.base import CRUDBase
from app.models.supplier import Supplier
from app.schemas.supplier import SupplierCreate, SupplierUpdate
class CRUDSupplier(CRUDBase[Supplier, SupplierCreate, SupplierUpdate]):
def get_by_name(self, db: Session, *, name: str) -> Optional[Supplier]:
return db.query(Supplier).filter(Supplier.name == name).first()
supplier = CRUDSupplier(Supplier)

View File

@ -0,0 +1,60 @@
from typing import List, Optional
from sqlalchemy.orm import Session
from app.crud.base import CRUDBase
from app.crud.crud_item import item as crud_item
from app.models.transaction import Transaction, TransactionType
from app.schemas.transaction import TransactionCreate, TransactionUpdate
class CRUDTransaction(CRUDBase[Transaction, TransactionCreate, TransactionUpdate]):
def create(self, db: Session, *, obj_in: TransactionCreate) -> Transaction:
# Create transaction
db_obj = Transaction(
transaction_type=obj_in.transaction_type,
quantity=obj_in.quantity,
price_per_unit=obj_in.price_per_unit,
reference_number=obj_in.reference_number,
notes=obj_in.notes,
item_id=obj_in.item_id,
user_id=obj_in.user_id,
transaction_date=obj_in.transaction_date
)
db.add(db_obj)
# Update item quantity based on transaction type
quantity_change = obj_in.quantity
if obj_in.transaction_type in [TransactionType.SALE, TransactionType.RETURN]:
quantity_change = -quantity_change
crud_item.update_quantity(db, item_id=obj_in.item_id, quantity_change=quantity_change)
db.commit()
db.refresh(db_obj)
return db_obj
def get_by_item(
self, db: Session, *, item_id: int, skip: int = 0, limit: int = 100
) -> List[Transaction]:
return (
db.query(Transaction)
.filter(Transaction.item_id == item_id)
.offset(skip)
.limit(limit)
.all()
)
def get_by_transaction_type(
self, db: Session, *, transaction_type: TransactionType, skip: int = 0, limit: int = 100
) -> List[Transaction]:
return (
db.query(Transaction)
.filter(Transaction.transaction_type == transaction_type)
.offset(skip)
.limit(limit)
.all()
)
transaction = CRUDTransaction(Transaction)

56
app/crud/crud_user.py Normal file
View File

@ -0,0 +1,56 @@
from typing import Any, Dict, Optional, Union
from sqlalchemy.orm import Session
from app.core.security import get_password_hash, verify_password
from app.crud.base import CRUDBase
from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
return db.query(User).filter(User.email == email).first()
def create(self, db: Session, *, obj_in: UserCreate) -> User:
db_obj = User(
email=obj_in.email,
hashed_password=get_password_hash(obj_in.password),
full_name=obj_in.full_name,
is_superuser=obj_in.is_superuser,
is_active=obj_in.is_active
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]
) -> User:
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.dict(exclude_unset=True)
if update_data.get("password"):
hashed_password = get_password_hash(update_data["password"])
del update_data["password"]
update_data["hashed_password"] = hashed_password
return super().update(db, db_obj=db_obj, obj_in=update_data)
def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]:
user = self.get_by_email(db, email=email)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user
def is_active(self, user: User) -> bool:
return user.is_active
def is_superuser(self, user: User) -> bool:
return user.is_superuser
user = CRUDUser(User)

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

@ -0,0 +1,7 @@
# Import all the models, so that Base has them before being
# imported by Alembic
from app.db.base_class import Base
from app.models.item import Item
from app.models.supplier import Supplier
from app.models.transaction import Transaction
from app.models.user import User

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

@ -0,0 +1,14 @@
from typing import Any
from sqlalchemy.ext.declarative import as_declarative, declared_attr
@as_declarative()
class Base:
id: Any
__name__: str
# Generate __tablename__ automatically based on class name
@declared_attr
def __tablename__(cls) -> str:
return cls.__name__.lower()

60
app/db/deps.py Normal file
View File

@ -0,0 +1,60 @@
from typing import Generator, Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app.core import security
from app.core.config import settings
from app.db.session import SessionLocal
from app.models.user import User
from app.schemas.token import TokenPayload
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/login/access-token")
def get_db() -> Generator:
try:
db = SessionLocal()
yield db
finally:
db.close()
def get_current_user(
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
) -> User:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
token_data = TokenPayload(**payload)
except (jwt.JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
user = db.query(User).filter(User.id == token_data.sub).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
def get_current_active_user(
current_user: User = Depends(get_current_user),
) -> User:
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
def get_current_active_superuser(
current_user: User = Depends(get_current_user),
) -> User:
if not current_user.is_superuser:
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
return current_user

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

@ -0,0 +1,15 @@
from pathlib import Path
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# Create DB directory
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)

27
app/models/item.py Normal file
View File

@ -0,0 +1,27 @@
from sqlalchemy import Column, Integer, String, Float, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base_class import Base
class Item(Base):
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
description = Column(String, nullable=True)
sku = Column(String, index=True, unique=True)
quantity = Column(Integer, default=0)
unit_price = Column(Float, nullable=False)
category = Column(String, index=True)
location = Column(String, nullable=True)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Foreign Keys
supplier_id = Column(Integer, ForeignKey("supplier.id"), nullable=True)
# Relationships
supplier = relationship("Supplier", back_populates="items")
transactions = relationship("Transaction", back_populates="item")

21
app/models/supplier.py Normal file
View File

@ -0,0 +1,21 @@
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base_class import Base
class Supplier(Base):
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
contact_name = Column(String, nullable=True)
email = Column(String, nullable=True)
phone = Column(String, nullable=True)
address = Column(String, nullable=True)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
items = relationship("Item", back_populates="supplier")

37
app/models/transaction.py Normal file
View File

@ -0,0 +1,37 @@
from sqlalchemy import Column, Integer, String, Float, ForeignKey, DateTime, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
from app.db.base_class import Base
class TransactionType(str, enum.Enum):
PURCHASE = "purchase"
SALE = "sale"
ADJUSTMENT = "adjustment"
RETURN = "return"
class Transaction(Base):
id = Column(Integer, primary_key=True, index=True)
transaction_type = Column(Enum(TransactionType), nullable=False)
quantity = Column(Integer, nullable=False)
price_per_unit = Column(Float, nullable=False)
# Reference information
reference_number = Column(String, nullable=True)
notes = Column(String, nullable=True)
# Timestamps
transaction_date = Column(DateTime(timezone=True), default=func.now())
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Foreign Keys
item_id = Column(Integer, ForeignKey("item.id"), nullable=False)
user_id = Column(Integer, ForeignKey("user.id"), nullable=True)
# Relationships
item = relationship("Item", back_populates="transactions")
user = relationship("User")

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

@ -0,0 +1,13 @@
from sqlalchemy import Boolean, Column, Integer, String
from sqlalchemy.orm import relationship
from app.db.base_class import Base
class User(Base):
id = Column(Integer, primary_key=True, index=True)
full_name = Column(String, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
is_active = Column(Boolean(), default=True)
is_superuser = Column(Boolean(), default=False)

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

@ -0,0 +1,48 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel
# Shared properties
class ItemBase(BaseModel):
name: str
description: Optional[str] = None
sku: str
quantity: int = 0
unit_price: float
category: str
location: Optional[str] = None
supplier_id: Optional[int] = None
# Properties to receive on item creation
class ItemCreate(ItemBase):
pass
# Properties to receive on item update
class ItemUpdate(ItemBase):
name: Optional[str] = None
sku: Optional[str] = None
unit_price: Optional[float] = None
category: Optional[str] = None
# Properties shared by models stored in DB
class ItemInDBBase(ItemBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True
# Properties to return to client
class Item(ItemInDBBase):
pass
# Properties properties stored in DB
class ItemInDB(ItemInDBBase):
pass

42
app/schemas/supplier.py Normal file
View File

@ -0,0 +1,42 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, EmailStr
# Shared properties
class SupplierBase(BaseModel):
name: str
contact_name: Optional[str] = None
email: Optional[EmailStr] = None
phone: Optional[str] = None
address: Optional[str] = None
# Properties to receive on supplier creation
class SupplierCreate(SupplierBase):
pass
# Properties to receive on supplier update
class SupplierUpdate(SupplierBase):
name: Optional[str] = None
# Properties shared by models stored in DB
class SupplierInDBBase(SupplierBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True
# Properties to return to client
class Supplier(SupplierInDBBase):
pass
# Properties properties stored in DB
class SupplierInDB(SupplierInDBBase):
pass

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

@ -0,0 +1,12 @@
from typing import Optional
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str
class TokenPayload(BaseModel):
sub: Optional[int] = None

View File

@ -0,0 +1,50 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel
from app.models.transaction import TransactionType
# Shared properties
class TransactionBase(BaseModel):
transaction_type: TransactionType
quantity: int
price_per_unit: float
reference_number: Optional[str] = None
notes: Optional[str] = None
item_id: int
user_id: Optional[int] = None
transaction_date: Optional[datetime] = None
# Properties to receive on transaction creation
class TransactionCreate(TransactionBase):
pass
# Properties to receive on transaction update
class TransactionUpdate(TransactionBase):
transaction_type: Optional[TransactionType] = None
quantity: Optional[int] = None
price_per_unit: Optional[float] = None
item_id: Optional[int] = None
# Properties shared by models stored in DB
class TransactionInDBBase(TransactionBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True
# Properties to return to client
class Transaction(TransactionInDBBase):
pass
# Properties properties stored in DB
class TransactionInDB(TransactionInDBBase):
pass

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

@ -0,0 +1,39 @@
from typing import Optional
from pydantic import BaseModel, EmailStr
# Shared properties
class UserBase(BaseModel):
email: Optional[EmailStr] = None
is_active: Optional[bool] = True
is_superuser: bool = False
full_name: Optional[str] = None
# Properties to receive via API on creation
class UserCreate(UserBase):
email: EmailStr
password: str
# Properties to receive via API on update
class UserUpdate(UserBase):
password: Optional[str] = None
class UserInDBBase(UserBase):
id: Optional[int] = None
class Config:
orm_mode = True
# Additional properties to return via API
class User(UserInDBBase):
pass
# Additional properties stored in DB
class UserInDB(UserInDBBase):
hashed_password: str

33
main.py Normal file
View File

@ -0,0 +1,33 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.api import api_router
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
openapi_url=f"{settings.API_V1_STR}/openapi.json",
version="0.1.0",
)
# Set all CORS enabled origins
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(api_router, prefix=settings.API_V1_STR)
@app.get("/health", tags=["Health"])
def health_check():
"""
Health check endpoint
"""
return {"status": "ok"}
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

11
requirements.txt Normal file
View File

@ -0,0 +1,11 @@
fastapi>=0.95.0
uvicorn>=0.21.1
sqlalchemy>=2.0.7
pydantic>=1.10.7
alembic>=1.10.2
python-jose>=3.3.0
passlib>=1.7.4
python-multipart>=0.0.6
email-validator>=2.0.0
python-dateutil>=2.8.2
bcrypt>=4.0.1