Implement complete FastAPI inventory management system

- Set up project structure with FastAPI and SQLite
- Created database models for users, categories, suppliers, items, and stock transactions
- Implemented Alembic for database migrations with proper absolute paths
- Built comprehensive CRUD operations for all entities
- Added JWT-based authentication and authorization system
- Created RESTful API endpoints for all inventory operations
- Implemented search, filtering, and low stock alerts
- Added health check endpoint and base URL response
- Configured CORS for all origins
- Set up Ruff for code linting and formatting
- Updated README with comprehensive documentation and usage examples

The system provides complete inventory management functionality for small businesses
including product tracking, supplier management, stock transactions, and reporting.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Automated Action 2025-06-18 10:55:22 +00:00
parent f03b0c72a0
commit e8172f2bc2
37 changed files with 1727 additions and 2 deletions

195
README.md
View File

@ -1,3 +1,194 @@
# FastAPI Application # Small Business Inventory Management System
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform. A comprehensive FastAPI-based inventory management system designed for small businesses to track products, manage suppliers, monitor stock levels, and handle inventory transactions.
## Features
- **Product Management**: Add, update, and track inventory items with detailed information
- **Category Management**: Organize products into categories for better organization
- **Supplier Management**: Maintain supplier information and relationships
- **Stock Tracking**: Real-time stock level monitoring with low stock alerts
- **Transaction History**: Complete audit trail of all stock movements
- **Authentication**: Secure JWT-based authentication system
- **Search & Filtering**: Powerful search capabilities across inventory items
- **RESTful API**: Clean, well-documented API endpoints
## Technology Stack
- **Framework**: FastAPI
- **Database**: SQLite with SQLAlchemy ORM
- **Authentication**: JWT tokens with PassLib for password hashing
- **Migration**: Alembic for database migrations
- **Validation**: Pydantic for data validation
- **Documentation**: Auto-generated OpenAPI/Swagger documentation
## Installation
1. Install dependencies:
```bash
pip install -r requirements.txt
```
2. Run database migrations:
```bash
alembic upgrade head
```
3. Start the application:
```bash
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
```
## Environment Variables
The following environment variables can be set to configure the application:
- `SECRET_KEY`: JWT secret key (default: "your-secret-key-change-this-in-production")
- `FIRST_SUPERUSER_EMAIL`: Admin user email (default: "admin@inventory.com")
- `FIRST_SUPERUSER_PASSWORD`: Admin user password (default: "admin123")
**Important**: Change the default SECRET_KEY and admin credentials in production!
## API Documentation
Once the application is running, you can access:
- **Interactive API Documentation**: http://localhost:8000/docs
- **Alternative Documentation**: http://localhost:8000/redoc
- **OpenAPI Schema**: http://localhost:8000/openapi.json
## API Endpoints
### Authentication
- `POST /auth/login` - User login
### Categories
- `GET /categories/` - List all categories
- `POST /categories/` - Create new category
- `GET /categories/{id}` - Get category by ID
- `PUT /categories/{id}` - Update category
- `DELETE /categories/{id}` - Delete category
### Suppliers
- `GET /suppliers/` - List all suppliers
- `POST /suppliers/` - Create new supplier
- `GET /suppliers/{id}` - Get supplier by ID
- `PUT /suppliers/{id}` - Update supplier
- `DELETE /suppliers/{id}` - Delete supplier
### Items
- `GET /items/` - List all items
- `POST /items/` - Create new item
- `GET /items/{id}` - Get item by ID
- `PUT /items/{id}` - Update item
- `DELETE /items/{id}` - Delete item
- `GET /items/search?query={query}` - Search items
- `GET /items/low-stock` - Get items with low stock
- `GET /items/active` - Get active items only
### Stock Transactions
- `GET /stock-transactions/` - List all transactions
- `POST /stock-transactions/` - Create new transaction (updates stock automatically)
- `GET /stock-transactions/{id}` - Get transaction by ID
- `GET /stock-transactions/item/{item_id}` - Get transactions for specific item
- `GET /stock-transactions/type/{type}` - Get transactions by type (in/out/adjustment)
### System
- `GET /` - API information and links
- `GET /health` - Health check endpoint
## Database Schema
The system uses the following main entities:
- **Users**: System users with authentication
- **Categories**: Product categories for organization
- **Suppliers**: Supplier information and contacts
- **Items**: Inventory items with pricing and stock information
- **StockTransactions**: Record of all stock movements
## Usage Examples
### 1. Login
```bash
curl -X POST "http://localhost:8000/auth/login" \
-H "Content-Type: application/json" \
-d '{"email": "admin@inventory.com", "password": "admin123"}'
```
### 2. Create a Category
```bash
curl -X POST "http://localhost:8000/categories/" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Electronics", "description": "Electronic devices and components"}'
```
### 3. Add an Item
```bash
curl -X POST "http://localhost:8000/items/" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Laptop Computer",
"description": "High-performance laptop",
"sku": "LAP001",
"unit_price": 999.99,
"cost_price": 750.00,
"quantity_in_stock": 10,
"minimum_stock_level": 2,
"reorder_point": 5,
"category_id": 1
}'
```
### 4. Record Stock Transaction
```bash
curl -X POST "http://localhost:8000/stock-transactions/" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"item_id": 1,
"transaction_type": "out",
"quantity": 2,
"reference_number": "SALE001",
"notes": "Sale to customer"
}'
```
## Development
### Running with Auto-reload
```bash
uvicorn main:app --reload
```
### Code Formatting
The project uses Ruff for linting and formatting:
```bash
ruff check .
ruff format .
```
### Database Migrations
Create a new migration:
```bash
alembic revision --autogenerate -m "Description of changes"
```
Apply migrations:
```bash
alembic upgrade head
```
## Production Deployment
1. Set secure environment variables
2. Use a production WSGI server like Gunicorn
3. Configure proper SSL/TLS certificates
4. Set up database backups
5. Configure logging and monitoring
## License
This project is open source and available under the MIT License.

41
alembic.ini Normal file
View File

@ -0,0 +1,41 @@
[alembic]
script_location = alembic
prepend_sys_path = .
version_path_separator = os
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
[post_write_hooks]
[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

58
alembic/env.py Normal file
View File

@ -0,0 +1,58 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
import os
import sys
# Add the project root to the Python path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.db.base import Base
from app.models import User, Category, Supplier, Item, StockTransaction
# 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)
target_metadata = Base.metadata
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode."""
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."""
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() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,142 @@
"""Initial migration
Revision ID: 001
Revises:
Create Date: 2024-01-01 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "001"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# 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=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_users_email"), "users", ["email"], unique=True)
op.create_index(op.f("ix_users_id"), "users", ["id"], unique=False)
# Create categories table
op.create_table(
"categories",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("description", sa.Text(), 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_categories_id"), "categories", ["id"], unique=False)
op.create_index(op.f("ix_categories_name"), "categories", ["name"], unique=True)
# Create suppliers table
op.create_table(
"suppliers",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("contact_person", sa.String(), nullable=True),
sa.Column("email", sa.String(), nullable=True),
sa.Column("phone", sa.String(), nullable=True),
sa.Column("address", sa.Text(), 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_suppliers_id"), "suppliers", ["id"], unique=False)
op.create_index(op.f("ix_suppliers_name"), "suppliers", ["name"], unique=True)
# Create items table
op.create_table(
"items",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("sku", sa.String(), nullable=False),
sa.Column("barcode", sa.String(), nullable=True),
sa.Column("unit_price", sa.Float(), nullable=False),
sa.Column("cost_price", sa.Float(), nullable=True),
sa.Column("quantity_in_stock", sa.Integer(), nullable=True),
sa.Column("minimum_stock_level", sa.Integer(), nullable=True),
sa.Column("maximum_stock_level", sa.Integer(), nullable=True),
sa.Column("reorder_point", sa.Integer(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=True),
sa.Column("category_id", sa.Integer(), nullable=True),
sa.Column("supplier_id", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(
["category_id"],
["categories.id"],
),
sa.ForeignKeyConstraint(
["supplier_id"],
["suppliers.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_items_barcode"), "items", ["barcode"], unique=True)
op.create_index(op.f("ix_items_id"), "items", ["id"], unique=False)
op.create_index(op.f("ix_items_name"), "items", ["name"], unique=False)
op.create_index(op.f("ix_items_sku"), "items", ["sku"], unique=True)
# Create stock_transactions table
op.create_table(
"stock_transactions",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("item_id", sa.Integer(), nullable=False),
sa.Column(
"transaction_type",
sa.Enum("IN", "OUT", "ADJUSTMENT", name="transactiontype"),
nullable=False,
),
sa.Column("quantity", sa.Integer(), nullable=False),
sa.Column("unit_cost", sa.Float(), nullable=True),
sa.Column("reference_number", sa.String(), nullable=True),
sa.Column("notes", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(
["item_id"],
["items.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_stock_transactions_id"), "stock_transactions", ["id"], unique=False
)
def downgrade() -> None:
op.drop_index(op.f("ix_stock_transactions_id"), table_name="stock_transactions")
op.drop_table("stock_transactions")
op.drop_index(op.f("ix_items_sku"), table_name="items")
op.drop_index(op.f("ix_items_name"), table_name="items")
op.drop_index(op.f("ix_items_id"), table_name="items")
op.drop_index(op.f("ix_items_barcode"), table_name="items")
op.drop_table("items")
op.drop_index(op.f("ix_suppliers_name"), table_name="suppliers")
op.drop_index(op.f("ix_suppliers_id"), table_name="suppliers")
op.drop_table("suppliers")
op.drop_index(op.f("ix_categories_name"), table_name="categories")
op.drop_index(op.f("ix_categories_id"), table_name="categories")
op.drop_table("categories")
op.drop_index(op.f("ix_users_id"), table_name="users")
op.drop_index(op.f("ix_users_email"), table_name="users")
op.drop_table("users")

36
app/api/auth.py Normal file
View File

@ -0,0 +1,36 @@
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.core import security
from app.core.config import settings
from app.crud import user as user_crud
from app.db.session import get_db
from app.schemas.user import UserLogin
router = APIRouter()
@router.post("/login")
def login_access_token(
db: Session = Depends(get_db), form_data: UserLogin = None
) -> Any:
user = user_crud.authenticate(
db, email=form_data.email, password=form_data.password
)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
elif not user_crud.is_active(user):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive user"
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
"access_token": security.create_access_token(
user.email, expires_delta=access_token_expires
),
"token_type": "bearer",
}

80
app/api/categories.py Normal file
View File

@ -0,0 +1,80 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.auth import get_current_active_user
from app.crud import category as category_crud
from app.db.session import get_db
from app.models import User
from app.schemas.category import Category, CategoryCreate, CategoryUpdate
router = APIRouter()
@router.get("/", response_model=List[Category])
def read_categories(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
categories = category_crud.get_multi(db, skip=skip, limit=limit)
return categories
@router.post("/", response_model=Category)
def create_category(
*,
db: Session = Depends(get_db),
category_in: CategoryCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
category = category_crud.get_by_name(db, name=category_in.name)
if category:
raise HTTPException(
status_code=400,
detail="Category with this name already exists.",
)
category = category_crud.create(db, obj_in=category_in)
return category
@router.put("/{id}", response_model=Category)
def update_category(
*,
db: Session = Depends(get_db),
id: int,
category_in: CategoryUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
category = category_crud.get(db, id=id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
category = category_crud.update(db, db_obj=category, obj_in=category_in)
return category
@router.get("/{id}", response_model=Category)
def read_category(
*,
db: Session = Depends(get_db),
id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
category = category_crud.get(db, id=id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
return category
@router.delete("/{id}", response_model=Category)
def delete_category(
*,
db: Session = Depends(get_db),
id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
category = category_crud.get(db, id=id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
category = category_crud.remove(db, id=id)
return category

121
app/api/items.py Normal file
View File

@ -0,0 +1,121 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.core.auth import get_current_active_user
from app.crud import item as item_crud
from app.db.session import get_db
from app.models import User
from app.schemas.item import Item, ItemCreate, ItemUpdate, ItemWithLowStock
router = APIRouter()
@router.get("/", response_model=List[Item])
def read_items(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
items = item_crud.get_multi(db, skip=skip, limit=limit)
return items
@router.get("/search", response_model=List[Item])
def search_items(
query: str = Query(..., description="Search query"),
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
items = item_crud.search_items(db, query=query, skip=skip, limit=limit)
return items
@router.get("/low-stock", response_model=List[ItemWithLowStock])
def read_low_stock_items(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
items = item_crud.get_low_stock_items(db, skip=skip, limit=limit)
return items
@router.get("/active", response_model=List[Item])
def read_active_items(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
items = item_crud.get_active_items(db, skip=skip, limit=limit)
return items
@router.post("/", response_model=Item)
def create_item(
*,
db: Session = Depends(get_db),
item_in: ItemCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
item = item_crud.get_by_sku(db, sku=item_in.sku)
if item:
raise HTTPException(
status_code=400,
detail="Item with this SKU already exists.",
)
if item_in.barcode:
item = item_crud.get_by_barcode(db, barcode=item_in.barcode)
if item:
raise HTTPException(
status_code=400,
detail="Item with this barcode already exists.",
)
item = item_crud.create(db, obj_in=item_in)
return item
@router.put("/{id}", response_model=Item)
def update_item(
*,
db: Session = Depends(get_db),
id: int,
item_in: ItemUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
item = item_crud.get(db, id=id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
item = item_crud.update(db, db_obj=item, obj_in=item_in)
return item
@router.get("/{id}", response_model=Item)
def read_item(
*,
db: Session = Depends(get_db),
id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
item = item_crud.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(get_db),
id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
item = item_crud.get(db, id=id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
item = item_crud.remove(db, id=id)
return item

View File

@ -0,0 +1,78 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.auth import get_current_active_user
from app.crud import stock_transaction as stock_transaction_crud
from app.db.session import get_db
from app.models import User
from app.models.stock_transaction import TransactionType
from app.schemas.stock_transaction import StockTransaction, StockTransactionCreate
router = APIRouter()
@router.get("/", response_model=List[StockTransaction])
def read_stock_transactions(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
transactions = stock_transaction_crud.get_multi(db, skip=skip, limit=limit)
return transactions
@router.get("/item/{item_id}", response_model=List[StockTransaction])
def read_item_transactions(
*,
db: Session = Depends(get_db),
item_id: int,
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
transactions = stock_transaction_crud.get_by_item(
db, item_id=item_id, skip=skip, limit=limit
)
return transactions
@router.get("/type/{transaction_type}", response_model=List[StockTransaction])
def read_transactions_by_type(
*,
db: Session = Depends(get_db),
transaction_type: TransactionType,
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
transactions = stock_transaction_crud.get_by_type(
db, transaction_type=transaction_type, skip=skip, limit=limit
)
return transactions
@router.post("/", response_model=StockTransaction)
def create_stock_transaction(
*,
db: Session = Depends(get_db),
transaction_in: StockTransactionCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
transaction = stock_transaction_crud.create_with_stock_update(
db, obj_in=transaction_in
)
return transaction
@router.get("/{id}", response_model=StockTransaction)
def read_stock_transaction(
*,
db: Session = Depends(get_db),
id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
transaction = stock_transaction_crud.get(db, id=id)
if not transaction:
raise HTTPException(status_code=404, detail="Stock transaction not found")
return transaction

80
app/api/suppliers.py Normal file
View File

@ -0,0 +1,80 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.auth import get_current_active_user
from app.crud import supplier as supplier_crud
from app.db.session import get_db
from app.models import User
from app.schemas.supplier import Supplier, SupplierCreate, SupplierUpdate
router = APIRouter()
@router.get("/", response_model=List[Supplier])
def read_suppliers(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
suppliers = supplier_crud.get_multi(db, skip=skip, limit=limit)
return suppliers
@router.post("/", response_model=Supplier)
def create_supplier(
*,
db: Session = Depends(get_db),
supplier_in: SupplierCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
supplier = supplier_crud.get_by_name(db, name=supplier_in.name)
if supplier:
raise HTTPException(
status_code=400,
detail="Supplier with this name already exists.",
)
supplier = supplier_crud.create(db, obj_in=supplier_in)
return supplier
@router.put("/{id}", response_model=Supplier)
def update_supplier(
*,
db: Session = Depends(get_db),
id: int,
supplier_in: SupplierUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
supplier = supplier_crud.get(db, id=id)
if not supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
supplier = supplier_crud.update(db, db_obj=supplier, obj_in=supplier_in)
return supplier
@router.get("/{id}", response_model=Supplier)
def read_supplier(
*,
db: Session = Depends(get_db),
id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
supplier = supplier_crud.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(get_db),
id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
supplier = supplier_crud.get(db, id=id)
if not supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
supplier = supplier_crud.remove(db, id=id)
return supplier

53
app/core/auth.py Normal file
View File

@ -0,0 +1,53 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from app.core.config import settings
from app.crud import user as user_crud
from app.db.session import get_db
from app.models.user import User
security = HTTPBearer()
def get_current_user(
db: Session = Depends(get_db),
credentials: HTTPAuthorizationCredentials = Depends(security),
) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(
credentials.credentials,
settings.SECRET_KEY,
algorithms=[settings.ALGORITHM],
)
email: str = payload.get("sub")
if email is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = user_crud.get_by_email(db, email=email)
if user is None:
raise credentials_exception
return user
def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
if not user_crud.is_active(current_user):
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 user_crud.is_superuser(current_user):
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
return current_user

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

@ -0,0 +1,26 @@
from decouple import config
class Settings:
PROJECT_NAME: str = "Small Business Inventory Management System"
PROJECT_VERSION: str = "1.0.0"
SECRET_KEY: str = config(
"SECRET_KEY", default="your-secret-key-change-this-in-production"
)
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# Admin user
FIRST_SUPERUSER_EMAIL: str = config(
"FIRST_SUPERUSER_EMAIL", default="admin@inventory.com"
)
FIRST_SUPERUSER_PASSWORD: str = config(
"FIRST_SUPERUSER_PASSWORD", default="admin123"
)
# CORS
BACKEND_CORS_ORIGINS: list = ["*"]
settings = Settings()

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

@ -0,0 +1,31 @@
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")
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=settings.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)

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

@ -0,0 +1,7 @@
from .user import user
from .category import category
from .supplier import supplier
from .item import item
from .stock_transaction import stock_transaction
__all__ = ["user", "category", "supplier", "item", "stock_transaction"]

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

@ -0,0 +1,56 @@
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 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]):
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.model_dump(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

13
app/crud/category.py Normal file
View File

@ -0,0 +1,13 @@
from typing import Optional
from sqlalchemy.orm import Session
from app.crud.base import CRUDBase
from app.models.category import Category
from app.schemas.category import CategoryCreate, CategoryUpdate
class CRUDCategory(CRUDBase[Category, CategoryCreate, CategoryUpdate]):
def get_by_name(self, db: Session, *, name: str) -> Optional[Category]:
return db.query(Category).filter(Category.name == name).first()
category = CRUDCategory(Category)

69
app/crud/item.py Normal file
View File

@ -0,0 +1,69 @@
from typing import List, Optional
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_barcode(self, db: Session, *, barcode: str) -> Optional[Item]:
return db.query(Item).filter(Item.barcode == barcode).first()
def get_by_category(
self, db: Session, *, category_id: int, skip: int = 0, limit: int = 100
) -> List[Item]:
return (
db.query(Item)
.filter(Item.category_id == category_id)
.offset(skip)
.limit(limit)
.all()
)
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_low_stock_items(
self, db: Session, skip: int = 0, limit: int = 100
) -> List[Item]:
return (
db.query(Item)
.filter(Item.quantity_in_stock <= Item.reorder_point)
.offset(skip)
.limit(limit)
.all()
)
def get_active_items(
self, db: Session, skip: int = 0, limit: int = 100
) -> List[Item]:
return db.query(Item).filter(Item.is_active).offset(skip).limit(limit).all()
def search_items(
self, db: Session, *, query: str, skip: int = 0, limit: int = 100
) -> List[Item]:
return (
db.query(Item)
.filter(
Item.name.contains(query)
| Item.description.contains(query)
| Item.sku.contains(query)
)
.offset(skip)
.limit(limit)
.all()
)
item = CRUDItem(Item)

View File

@ -0,0 +1,61 @@
from typing import List
from sqlalchemy.orm import Session
from app.crud.base import CRUDBase
from app.models.stock_transaction import StockTransaction, TransactionType
from app.schemas.stock_transaction import StockTransactionCreate
from app.crud.item import item as item_crud
class CRUDStockTransaction(CRUDBase[StockTransaction, StockTransactionCreate, None]):
def create_with_stock_update(
self, db: Session, *, obj_in: StockTransactionCreate
) -> StockTransaction:
# Create the transaction
transaction = self.create(db, obj_in=obj_in)
# Update item stock
db_item = item_crud.get(db, id=obj_in.item_id)
if db_item:
if obj_in.transaction_type == TransactionType.IN:
db_item.quantity_in_stock += obj_in.quantity
elif obj_in.transaction_type == TransactionType.OUT:
db_item.quantity_in_stock -= obj_in.quantity
elif obj_in.transaction_type == TransactionType.ADJUSTMENT:
db_item.quantity_in_stock = obj_in.quantity
db.add(db_item)
db.commit()
return transaction
def get_by_item(
self, db: Session, *, item_id: int, skip: int = 0, limit: int = 100
) -> List[StockTransaction]:
return (
db.query(StockTransaction)
.filter(StockTransaction.item_id == item_id)
.order_by(StockTransaction.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
def get_by_type(
self,
db: Session,
*,
transaction_type: TransactionType,
skip: int = 0,
limit: int = 100,
) -> List[StockTransaction]:
return (
db.query(StockTransaction)
.filter(StockTransaction.transaction_type == transaction_type)
.order_by(StockTransaction.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
stock_transaction = CRUDStockTransaction(StockTransaction)

13
app/crud/supplier.py Normal file
View File

@ -0,0 +1,13 @@
from typing import 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)

53
app/crud/user.py Normal file
View File

@ -0,0 +1,53 @@
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_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.model_dump(exclude_unset=True)
if "password" in update_data:
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)

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

@ -0,0 +1,3 @@
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

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

@ -0,0 +1,22 @@
from pathlib import Path
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
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():
db = SessionLocal()
try:
yield db
finally:
db.close()

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

@ -0,0 +1,14 @@
from .user import User
from .category import Category
from .supplier import Supplier
from .item import Item
from .stock_transaction import StockTransaction, TransactionType
__all__ = [
"User",
"Category",
"Supplier",
"Item",
"StockTransaction",
"TransactionType",
]

16
app/models/category.py Normal file
View File

@ -0,0 +1,16 @@
from sqlalchemy import Column, Integer, String, DateTime, Text
from sqlalchemy.orm import relationship
from datetime import datetime
from app.db.base import Base
class Category(Base):
__tablename__ = "categories"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True, nullable=False)
description = Column(Text)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
items = relationship("Item", back_populates="category")

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

@ -0,0 +1,40 @@
from sqlalchemy import (
Column,
Integer,
String,
DateTime,
Text,
Float,
ForeignKey,
Boolean,
)
from sqlalchemy.orm import relationship
from datetime import datetime
from app.db.base import Base
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True, nullable=False)
description = Column(Text)
sku = Column(String, unique=True, index=True, nullable=False)
barcode = Column(String, unique=True, index=True)
unit_price = Column(Float, nullable=False)
cost_price = Column(Float)
quantity_in_stock = Column(Integer, default=0)
minimum_stock_level = Column(Integer, default=0)
maximum_stock_level = Column(Integer)
reorder_point = Column(Integer, default=0)
is_active = Column(Boolean, default=True)
category_id = Column(Integer, ForeignKey("categories.id"))
supplier_id = Column(Integer, ForeignKey("suppliers.id"))
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
category = relationship("Category", back_populates="items")
supplier = relationship("Supplier", back_populates="items")
transactions = relationship("StockTransaction", back_populates="item")

View File

@ -0,0 +1,26 @@
from sqlalchemy import Column, Integer, String, DateTime, Text, Float, ForeignKey, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
from app.db.base import Base
import enum
class TransactionType(enum.Enum):
IN = "in"
OUT = "out"
ADJUSTMENT = "adjustment"
class StockTransaction(Base):
__tablename__ = "stock_transactions"
id = Column(Integer, primary_key=True, index=True)
item_id = Column(Integer, ForeignKey("items.id"), nullable=False)
transaction_type = Column(Enum(TransactionType), nullable=False)
quantity = Column(Integer, nullable=False)
unit_cost = Column(Float)
reference_number = Column(String)
notes = Column(Text)
created_at = Column(DateTime, default=datetime.utcnow)
item = relationship("Item", back_populates="transactions")

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

@ -0,0 +1,19 @@
from sqlalchemy import Column, Integer, String, DateTime, Text
from sqlalchemy.orm import relationship
from datetime import datetime
from app.db.base import Base
class Supplier(Base):
__tablename__ = "suppliers"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True, nullable=False)
contact_person = Column(String)
email = Column(String)
phone = Column(String)
address = Column(Text)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
items = relationship("Item", back_populates="supplier")

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

@ -0,0 +1,16 @@
from sqlalchemy import Column, Integer, String, DateTime, Boolean
from datetime import datetime
from app.db.base import Base
class User(Base):
__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)
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

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

@ -0,0 +1,24 @@
from .user import User, UserCreate, UserUpdate, UserLogin
from .category import Category, CategoryCreate, CategoryUpdate
from .supplier import Supplier, SupplierCreate, SupplierUpdate
from .item import Item, ItemCreate, ItemUpdate, ItemWithLowStock
from .stock_transaction import StockTransaction, StockTransactionCreate
__all__ = [
"User",
"UserCreate",
"UserUpdate",
"UserLogin",
"Category",
"CategoryCreate",
"CategoryUpdate",
"Supplier",
"SupplierCreate",
"SupplierUpdate",
"Item",
"ItemCreate",
"ItemUpdate",
"ItemWithLowStock",
"StockTransaction",
"StockTransactionCreate",
]

26
app/schemas/category.py Normal file
View File

@ -0,0 +1,26 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
class CategoryBase(BaseModel):
name: str
description: Optional[str] = None
class CategoryCreate(CategoryBase):
pass
class CategoryUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
class Category(CategoryBase):
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True

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

@ -0,0 +1,64 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
from app.schemas.category import Category
from app.schemas.supplier import Supplier
class ItemBase(BaseModel):
name: str
description: Optional[str] = None
sku: str
barcode: Optional[str] = None
unit_price: float
cost_price: Optional[float] = None
quantity_in_stock: int = 0
minimum_stock_level: int = 0
maximum_stock_level: Optional[int] = None
reorder_point: int = 0
is_active: bool = True
category_id: Optional[int] = None
supplier_id: Optional[int] = None
class ItemCreate(ItemBase):
pass
class ItemUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
sku: Optional[str] = None
barcode: Optional[str] = None
unit_price: Optional[float] = None
cost_price: Optional[float] = None
quantity_in_stock: Optional[int] = None
minimum_stock_level: Optional[int] = None
maximum_stock_level: Optional[int] = None
reorder_point: Optional[int] = None
is_active: Optional[bool] = None
category_id: Optional[int] = None
supplier_id: Optional[int] = None
class Item(ItemBase):
id: int
created_at: datetime
updated_at: datetime
category: Optional[Category] = None
supplier: Optional[Supplier] = None
class Config:
from_attributes = True
class ItemWithLowStock(BaseModel):
id: int
name: str
sku: str
quantity_in_stock: int
minimum_stock_level: int
reorder_point: int
class Config:
from_attributes = True

View File

@ -0,0 +1,27 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
from app.models.stock_transaction import TransactionType
from app.schemas.item import Item
class StockTransactionBase(BaseModel):
item_id: int
transaction_type: TransactionType
quantity: int
unit_cost: Optional[float] = None
reference_number: Optional[str] = None
notes: Optional[str] = None
class StockTransactionCreate(StockTransactionBase):
pass
class StockTransaction(StockTransactionBase):
id: int
created_at: datetime
item: Optional[Item] = None
class Config:
from_attributes = True

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

@ -0,0 +1,32 @@
from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional
class SupplierBase(BaseModel):
name: str
contact_person: Optional[str] = None
email: Optional[EmailStr] = None
phone: Optional[str] = None
address: Optional[str] = None
class SupplierCreate(SupplierBase):
pass
class SupplierUpdate(BaseModel):
name: Optional[str] = None
contact_person: Optional[str] = None
email: Optional[EmailStr] = None
phone: Optional[str] = None
address: Optional[str] = None
class Supplier(SupplierBase):
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True

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

@ -0,0 +1,35 @@
from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional
class UserBase(BaseModel):
email: EmailStr
full_name: Optional[str] = None
is_active: bool = True
class UserCreate(UserBase):
password: str
class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
full_name: Optional[str] = None
password: Optional[str] = None
is_active: Optional[bool] = None
class User(UserBase):
id: int
is_superuser: bool
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class UserLogin(BaseModel):
email: EmailStr
password: str

78
main.py Normal file
View File

@ -0,0 +1,78 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
from app.db.session import get_db, engine
from app.db.base import Base
from app.api import auth, categories, suppliers, items, stock_transactions
from app.crud import user as user_crud
from app.schemas.user import UserCreate
# Create database tables
Base.metadata.create_all(bind=engine)
app = FastAPI(
title=settings.PROJECT_NAME,
version=settings.PROJECT_VERSION,
openapi_url="/openapi.json",
)
# Set up CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.BACKEND_CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routers
app.include_router(auth.router, prefix="/auth", tags=["authentication"])
app.include_router(categories.router, prefix="/categories", tags=["categories"])
app.include_router(suppliers.router, prefix="/suppliers", tags=["suppliers"])
app.include_router(items.router, prefix="/items", tags=["items"])
app.include_router(
stock_transactions.router, prefix="/stock-transactions", tags=["stock-transactions"]
)
@app.get("/")
async def root():
return {
"title": settings.PROJECT_NAME,
"version": settings.PROJECT_VERSION,
"description": "API for managing inventory in small businesses",
"documentation": "/docs",
"health_check": "/health",
}
@app.get("/health")
async def health_check():
return {
"status": "healthy",
"service": settings.PROJECT_NAME,
"version": settings.PROJECT_VERSION,
}
@app.on_event("startup")
async def startup_event():
# Create first superuser if it doesn't exist
db = next(get_db())
user = user_crud.get_by_email(db, email=settings.FIRST_SUPERUSER_EMAIL)
if not user:
user_in = UserCreate(
email=settings.FIRST_SUPERUSER_EMAIL,
password=settings.FIRST_SUPERUSER_PASSWORD,
full_name="System Administrator",
)
user = user_crud.create(db, obj_in=user_in)
user.is_superuser = True
db.add(user)
db.commit()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

41
openapi.json Normal file
View File

@ -0,0 +1,41 @@
{
"openapi": "3.0.2",
"info": {
"title": "Small Business Inventory Management System",
"version": "1.0.0"
},
"paths": {
"/": {
"get": {
"summary": "Root",
"operationId": "root__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/health": {
"get": {
"summary": "Health Check",
"operationId": "health_check_health_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
}
}
}

9
requirements.txt Normal file
View File

@ -0,0 +1,9 @@
fastapi==0.104.1
uvicorn==0.24.0
sqlalchemy==2.0.23
alembic==1.13.0
python-multipart==0.0.6
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-decouple==3.8
ruff==0.1.7