...
This commit is contained in:
parent
82052ef385
commit
9c7baa3b4e
131
herodb/src/models/py/README.md
Normal file
131
herodb/src/models/py/README.md
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
# Business Models Python Port
|
||||||
|
|
||||||
|
This directory contains a Python port of the business models from the Rust codebase, using SQLModel for database integration.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This project includes:
|
||||||
|
|
||||||
|
1. Python port of Rust business models using SQLModel
|
||||||
|
2. FastAPI server with OpenAPI/Swagger documentation
|
||||||
|
3. CRUD operations for all models
|
||||||
|
4. Convenience endpoints for common operations
|
||||||
|
|
||||||
|
The models ported from Rust to Python include:
|
||||||
|
|
||||||
|
- **Currency**: Represents a monetary value with amount and currency code
|
||||||
|
- **Customer**: Represents a customer who can purchase products or services
|
||||||
|
- **Product**: Represents a product or service offered
|
||||||
|
- **ProductComponent**: Represents a component of a product
|
||||||
|
- **SaleItem**: Represents an item in a sale
|
||||||
|
- **Sale**: Represents a sale of products or services
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- `models.py`: Contains the SQLModel definitions for all business models
|
||||||
|
- `example.py`: Demonstrates how to use the models with a sample application
|
||||||
|
- `install_and_run.sh`: Bash script to install dependencies using `uv` and run the example
|
||||||
|
- `api.py`: FastAPI server providing CRUD operations for all models
|
||||||
|
- `server.sh`: Bash script to start the FastAPI server
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.7+
|
||||||
|
- [uv](https://github.com/astral-sh/uv) for dependency management
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
The project uses `uv` for dependency management. To install dependencies and run the example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./install_and_run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Server
|
||||||
|
|
||||||
|
The project includes a FastAPI server that provides CRUD operations for all models and some convenience endpoints.
|
||||||
|
|
||||||
|
### Starting the Server
|
||||||
|
|
||||||
|
To start the API server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./server.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script will:
|
||||||
|
1. Create a virtual environment if it doesn't exist
|
||||||
|
2. Install the required dependencies using `uv`
|
||||||
|
3. Start the FastAPI server with hot reloading enabled
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
|
||||||
|
Once the server is running, you can access the OpenAPI documentation at:
|
||||||
|
|
||||||
|
- Swagger UI: http://localhost:8000/docs
|
||||||
|
- ReDoc: http://localhost:8000/redoc
|
||||||
|
|
||||||
|
### Available Endpoints
|
||||||
|
|
||||||
|
The API provides the following endpoints:
|
||||||
|
|
||||||
|
#### Currencies
|
||||||
|
- `GET /currencies/`: List all currencies
|
||||||
|
- `POST /currencies/`: Create a new currency
|
||||||
|
- `GET /currencies/{currency_id}`: Get a specific currency
|
||||||
|
- `PUT /currencies/{currency_id}`: Update a currency
|
||||||
|
- `DELETE /currencies/{currency_id}`: Delete a currency
|
||||||
|
|
||||||
|
#### Customers
|
||||||
|
- `GET /customers/`: List all customers
|
||||||
|
- `POST /customers/`: Create a new customer
|
||||||
|
- `GET /customers/{customer_id}`: Get a specific customer
|
||||||
|
- `PUT /customers/{customer_id}`: Update a customer
|
||||||
|
- `DELETE /customers/{customer_id}`: Delete a customer
|
||||||
|
- `GET /customers/{customer_id}/sales/`: Get all sales for a customer
|
||||||
|
|
||||||
|
#### Products
|
||||||
|
- `GET /products/`: List all products
|
||||||
|
- `POST /products/`: Create a new product
|
||||||
|
- `GET /products/{product_id}`: Get a specific product
|
||||||
|
- `PUT /products/{product_id}`: Update a product
|
||||||
|
- `DELETE /products/{product_id}`: Delete a product
|
||||||
|
- `GET /products/available/`: Get all available products
|
||||||
|
- `POST /products/{product_id}/components/`: Add a component to a product
|
||||||
|
- `GET /products/{product_id}/components/`: Get all components for a product
|
||||||
|
|
||||||
|
#### Sales
|
||||||
|
- `GET /sales/`: List all sales
|
||||||
|
- `POST /sales/`: Create a new sale
|
||||||
|
- `GET /sales/{sale_id}`: Get a specific sale
|
||||||
|
- `PUT /sales/{sale_id}`: Update a sale
|
||||||
|
- `DELETE /sales/{sale_id}`: Delete a sale
|
||||||
|
- `PUT /sales/{sale_id}/status/{status}`: Update the status of a sale
|
||||||
|
- `POST /sales/{sale_id}/items/`: Add an item to a sale
|
||||||
|
- `GET /sales/{sale_id}/items/`: Get all items for a sale
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- SQLModel: For database models and ORM functionality
|
||||||
|
- Pydantic: For data validation (used by SQLModel)
|
||||||
|
- FastAPI: For creating the API server
|
||||||
|
- Uvicorn: ASGI server for running FastAPI applications
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
The `example.py` script demonstrates:
|
||||||
|
|
||||||
|
1. Creating an SQLite database
|
||||||
|
2. Defining and creating tables for the models
|
||||||
|
3. Creating sample data (customers, products, sales)
|
||||||
|
4. Performing operations on the data
|
||||||
|
5. Querying and displaying the data
|
||||||
|
|
||||||
|
To run the example manually (after activating the virtual environment):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the py directory
|
||||||
|
python example.py
|
||||||
|
|
||||||
|
# Or from the parent directory
|
||||||
|
cd py && python example.py
|
3
herodb/src/models/py/__init__.py
Normal file
3
herodb/src/models/py/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Python port of the business models from Rust.
|
||||||
|
"""
|
BIN
herodb/src/models/py/__pycache__/api.cpython-312.pyc
Normal file
BIN
herodb/src/models/py/__pycache__/api.cpython-312.pyc
Normal file
Binary file not shown.
BIN
herodb/src/models/py/__pycache__/models.cpython-312.pyc
Normal file
BIN
herodb/src/models/py/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
455
herodb/src/models/py/api.py
Executable file
455
herodb/src/models/py/api.py
Executable file
@ -0,0 +1,455 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
FastAPI server providing CRUD operations for business models.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from fastapi import Depends, FastAPI, HTTPException, Query
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from sqlmodel import Session, SQLModel, create_engine, select
|
||||||
|
|
||||||
|
from models import (
|
||||||
|
Currency,
|
||||||
|
Customer,
|
||||||
|
Product,
|
||||||
|
ProductComponent,
|
||||||
|
ProductStatus,
|
||||||
|
ProductType,
|
||||||
|
Sale,
|
||||||
|
SaleItem,
|
||||||
|
SaleStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create database
|
||||||
|
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///business.db")
|
||||||
|
engine = create_engine(DATABASE_URL, echo=False)
|
||||||
|
|
||||||
|
# Create tables
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
|
||||||
|
# Create FastAPI app
|
||||||
|
app = FastAPI(
|
||||||
|
title="Business API",
|
||||||
|
description="API for business models",
|
||||||
|
version="1.0.0",
|
||||||
|
docs_url="/docs",
|
||||||
|
redoc_url="/redoc",
|
||||||
|
openapi_url="/openapi.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add CORS middleware
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dependency to get database session
|
||||||
|
def get_session():
|
||||||
|
with Session(engine) as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
|
||||||
|
# Root endpoint
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {"message": "Welcome to the Business API"}
|
||||||
|
|
||||||
|
|
||||||
|
# Currency endpoints
|
||||||
|
@app.post("/currencies/", response_model=Currency, tags=["Currencies"])
|
||||||
|
def create_currency(currency: Currency, session: Session = Depends(get_session)):
|
||||||
|
"""Create a new currency"""
|
||||||
|
session.add(currency)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(currency)
|
||||||
|
return currency
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/currencies/", response_model=List[Currency], tags=["Currencies"])
|
||||||
|
def read_currencies(
|
||||||
|
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Get all currencies"""
|
||||||
|
currencies = session.exec(select(Currency).offset(skip).limit(limit)).all()
|
||||||
|
return currencies
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"])
|
||||||
|
def read_currency(currency_id: int, session: Session = Depends(get_session)):
|
||||||
|
"""Get a currency by ID"""
|
||||||
|
currency = session.get(Currency, currency_id)
|
||||||
|
if not currency:
|
||||||
|
raise HTTPException(status_code=404, detail="Currency not found")
|
||||||
|
return currency
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"])
|
||||||
|
def update_currency(
|
||||||
|
currency_id: int, currency_data: Currency, session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Update a currency"""
|
||||||
|
currency = session.get(Currency, currency_id)
|
||||||
|
if not currency:
|
||||||
|
raise HTTPException(status_code=404, detail="Currency not found")
|
||||||
|
|
||||||
|
# Update currency attributes
|
||||||
|
currency_data_dict = currency_data.dict(exclude_unset=True)
|
||||||
|
for key, value in currency_data_dict.items():
|
||||||
|
setattr(currency, key, value)
|
||||||
|
|
||||||
|
session.add(currency)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(currency)
|
||||||
|
return currency
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/currencies/{currency_id}", tags=["Currencies"])
|
||||||
|
def delete_currency(currency_id: int, session: Session = Depends(get_session)):
|
||||||
|
"""Delete a currency"""
|
||||||
|
currency = session.get(Currency, currency_id)
|
||||||
|
if not currency:
|
||||||
|
raise HTTPException(status_code=404, detail="Currency not found")
|
||||||
|
|
||||||
|
session.delete(currency)
|
||||||
|
session.commit()
|
||||||
|
return {"message": "Currency deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
# Customer endpoints
|
||||||
|
@app.post("/customers/", response_model=Customer, tags=["Customers"])
|
||||||
|
def create_customer(customer: Customer, session: Session = Depends(get_session)):
|
||||||
|
"""Create a new customer"""
|
||||||
|
session.add(customer)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(customer)
|
||||||
|
return customer
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/customers/", response_model=List[Customer], tags=["Customers"])
|
||||||
|
def read_customers(
|
||||||
|
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Get all customers"""
|
||||||
|
customers = session.exec(select(Customer).offset(skip).limit(limit)).all()
|
||||||
|
return customers
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/customers/{customer_id}", response_model=Customer, tags=["Customers"])
|
||||||
|
def read_customer(customer_id: int, session: Session = Depends(get_session)):
|
||||||
|
"""Get a customer by ID"""
|
||||||
|
customer = session.get(Customer, customer_id)
|
||||||
|
if not customer:
|
||||||
|
raise HTTPException(status_code=404, detail="Customer not found")
|
||||||
|
return customer
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/customers/{customer_id}", response_model=Customer, tags=["Customers"])
|
||||||
|
def update_customer(
|
||||||
|
customer_id: int, customer_data: Customer, session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Update a customer"""
|
||||||
|
customer = session.get(Customer, customer_id)
|
||||||
|
if not customer:
|
||||||
|
raise HTTPException(status_code=404, detail="Customer not found")
|
||||||
|
|
||||||
|
# Update customer attributes
|
||||||
|
customer_data_dict = customer_data.dict(exclude_unset=True)
|
||||||
|
for key, value in customer_data_dict.items():
|
||||||
|
setattr(customer, key, value)
|
||||||
|
|
||||||
|
session.add(customer)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(customer)
|
||||||
|
return customer
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/customers/{customer_id}", tags=["Customers"])
|
||||||
|
def delete_customer(customer_id: int, session: Session = Depends(get_session)):
|
||||||
|
"""Delete a customer"""
|
||||||
|
customer = session.get(Customer, customer_id)
|
||||||
|
if not customer:
|
||||||
|
raise HTTPException(status_code=404, detail="Customer not found")
|
||||||
|
|
||||||
|
session.delete(customer)
|
||||||
|
session.commit()
|
||||||
|
return {"message": "Customer deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
# Product endpoints
|
||||||
|
@app.post("/products/", response_model=Product, tags=["Products"])
|
||||||
|
def create_product(product: Product, session: Session = Depends(get_session)):
|
||||||
|
"""Create a new product"""
|
||||||
|
session.add(product)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(product)
|
||||||
|
return product
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/products/", response_model=List[Product], tags=["Products"])
|
||||||
|
def read_products(
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
category: Optional[str] = None,
|
||||||
|
status: Optional[ProductStatus] = None,
|
||||||
|
istemplate: Optional[bool] = None,
|
||||||
|
session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Get all products with optional filtering"""
|
||||||
|
query = select(Product)
|
||||||
|
|
||||||
|
if category:
|
||||||
|
query = query.where(Product.category == category)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
query = query.where(Product.status == status)
|
||||||
|
|
||||||
|
if istemplate is not None:
|
||||||
|
query = query.where(Product.istemplate == istemplate)
|
||||||
|
|
||||||
|
products = session.exec(query.offset(skip).limit(limit)).all()
|
||||||
|
return products
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/products/{product_id}", response_model=Product, tags=["Products"])
|
||||||
|
def read_product(product_id: int, session: Session = Depends(get_session)):
|
||||||
|
"""Get a product by ID"""
|
||||||
|
product = session.get(Product, product_id)
|
||||||
|
if not product:
|
||||||
|
raise HTTPException(status_code=404, detail="Product not found")
|
||||||
|
return product
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/products/{product_id}", response_model=Product, tags=["Products"])
|
||||||
|
def update_product(
|
||||||
|
product_id: int, product_data: Product, session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Update a product"""
|
||||||
|
product = session.get(Product, product_id)
|
||||||
|
if not product:
|
||||||
|
raise HTTPException(status_code=404, detail="Product not found")
|
||||||
|
|
||||||
|
# Update product attributes
|
||||||
|
product_data_dict = product_data.dict(exclude_unset=True)
|
||||||
|
for key, value in product_data_dict.items():
|
||||||
|
setattr(product, key, value)
|
||||||
|
|
||||||
|
session.add(product)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(product)
|
||||||
|
return product
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/products/{product_id}", tags=["Products"])
|
||||||
|
def delete_product(product_id: int, session: Session = Depends(get_session)):
|
||||||
|
"""Delete a product"""
|
||||||
|
product = session.get(Product, product_id)
|
||||||
|
if not product:
|
||||||
|
raise HTTPException(status_code=404, detail="Product not found")
|
||||||
|
|
||||||
|
session.delete(product)
|
||||||
|
session.commit()
|
||||||
|
return {"message": "Product deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
# Product Component endpoints
|
||||||
|
@app.post("/products/{product_id}/components/", response_model=ProductComponent, tags=["Product Components"])
|
||||||
|
def create_product_component(
|
||||||
|
product_id: int, component: ProductComponent, session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Add a component to a product"""
|
||||||
|
product = session.get(Product, product_id)
|
||||||
|
if not product:
|
||||||
|
raise HTTPException(status_code=404, detail="Product not found")
|
||||||
|
|
||||||
|
component.product_id = product_id
|
||||||
|
session.add(component)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(component)
|
||||||
|
return component
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/products/{product_id}/components/", response_model=List[ProductComponent], tags=["Product Components"])
|
||||||
|
def read_product_components(
|
||||||
|
product_id: int, session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Get all components for a product"""
|
||||||
|
product = session.get(Product, product_id)
|
||||||
|
if not product:
|
||||||
|
raise HTTPException(status_code=404, detail="Product not found")
|
||||||
|
|
||||||
|
return product.components
|
||||||
|
|
||||||
|
|
||||||
|
# Sale endpoints
|
||||||
|
@app.post("/sales/", response_model=Sale, tags=["Sales"])
|
||||||
|
def create_sale(sale: Sale, session: Session = Depends(get_session)):
|
||||||
|
"""Create a new sale"""
|
||||||
|
# Ensure customer exists if customer_id is provided
|
||||||
|
if sale.customer_id:
|
||||||
|
customer = session.get(Customer, sale.customer_id)
|
||||||
|
if not customer:
|
||||||
|
raise HTTPException(status_code=404, detail="Customer not found")
|
||||||
|
|
||||||
|
session.add(sale)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(sale)
|
||||||
|
return sale
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/sales/", response_model=List[Sale], tags=["Sales"])
|
||||||
|
def read_sales(
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
status: Optional[SaleStatus] = None,
|
||||||
|
customer_id: Optional[int] = None,
|
||||||
|
session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Get all sales with optional filtering"""
|
||||||
|
query = select(Sale)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
query = query.where(Sale.status == status)
|
||||||
|
|
||||||
|
if customer_id:
|
||||||
|
query = query.where(Sale.customer_id == customer_id)
|
||||||
|
|
||||||
|
sales = session.exec(query.offset(skip).limit(limit)).all()
|
||||||
|
return sales
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/sales/{sale_id}", response_model=Sale, tags=["Sales"])
|
||||||
|
def read_sale(sale_id: int, session: Session = Depends(get_session)):
|
||||||
|
"""Get a sale by ID"""
|
||||||
|
sale = session.get(Sale, sale_id)
|
||||||
|
if not sale:
|
||||||
|
raise HTTPException(status_code=404, detail="Sale not found")
|
||||||
|
return sale
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/sales/{sale_id}", response_model=Sale, tags=["Sales"])
|
||||||
|
def update_sale(
|
||||||
|
sale_id: int, sale_data: Sale, session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Update a sale"""
|
||||||
|
sale = session.get(Sale, sale_id)
|
||||||
|
if not sale:
|
||||||
|
raise HTTPException(status_code=404, detail="Sale not found")
|
||||||
|
|
||||||
|
# Update sale attributes
|
||||||
|
sale_data_dict = sale_data.dict(exclude_unset=True)
|
||||||
|
for key, value in sale_data_dict.items():
|
||||||
|
setattr(sale, key, value)
|
||||||
|
|
||||||
|
session.add(sale)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(sale)
|
||||||
|
return sale
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/sales/{sale_id}", tags=["Sales"])
|
||||||
|
def delete_sale(sale_id: int, session: Session = Depends(get_session)):
|
||||||
|
"""Delete a sale"""
|
||||||
|
sale = session.get(Sale, sale_id)
|
||||||
|
if not sale:
|
||||||
|
raise HTTPException(status_code=404, detail="Sale not found")
|
||||||
|
|
||||||
|
session.delete(sale)
|
||||||
|
session.commit()
|
||||||
|
return {"message": "Sale deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
# Sale Item endpoints
|
||||||
|
@app.post("/sales/{sale_id}/items/", response_model=SaleItem, tags=["Sale Items"])
|
||||||
|
def create_sale_item(
|
||||||
|
sale_id: int, item: SaleItem, session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Add an item to a sale"""
|
||||||
|
sale = session.get(Sale, sale_id)
|
||||||
|
if not sale:
|
||||||
|
raise HTTPException(status_code=404, detail="Sale not found")
|
||||||
|
|
||||||
|
item.sale_id = sale_id
|
||||||
|
session.add(item)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(item)
|
||||||
|
|
||||||
|
# Update the sale's total amount
|
||||||
|
sale.add_item(item)
|
||||||
|
session.add(sale)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/sales/{sale_id}/items/", response_model=List[SaleItem], tags=["Sale Items"])
|
||||||
|
def read_sale_items(
|
||||||
|
sale_id: int, session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Get all items for a sale"""
|
||||||
|
sale = session.get(Sale, sale_id)
|
||||||
|
if not sale:
|
||||||
|
raise HTTPException(status_code=404, detail="Sale not found")
|
||||||
|
|
||||||
|
return sale.items
|
||||||
|
|
||||||
|
|
||||||
|
# Convenience endpoints
|
||||||
|
@app.put("/sales/{sale_id}/status/{status}", response_model=Sale, tags=["Convenience"])
|
||||||
|
def update_sale_status(
|
||||||
|
sale_id: int, status: SaleStatus, session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Update the status of a sale"""
|
||||||
|
sale = session.get(Sale, sale_id)
|
||||||
|
if not sale:
|
||||||
|
raise HTTPException(status_code=404, detail="Sale not found")
|
||||||
|
|
||||||
|
sale.update_status(status)
|
||||||
|
session.add(sale)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(sale)
|
||||||
|
return sale
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/products/available/", response_model=List[Product], tags=["Convenience"])
|
||||||
|
def get_available_products(
|
||||||
|
istemplate: Optional[bool] = False,
|
||||||
|
session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Get all available products"""
|
||||||
|
query = select(Product).where(
|
||||||
|
Product.status == ProductStatus.AVAILABLE,
|
||||||
|
Product.purchase_till > datetime.utcnow(),
|
||||||
|
Product.istemplate == istemplate
|
||||||
|
)
|
||||||
|
products = session.exec(query).all()
|
||||||
|
return products
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/customers/{customer_id}/sales/", response_model=List[Sale], tags=["Convenience"])
|
||||||
|
def get_customer_sales(
|
||||||
|
customer_id: int,
|
||||||
|
status: Optional[SaleStatus] = None,
|
||||||
|
session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Get all sales for a customer"""
|
||||||
|
customer = session.get(Customer, customer_id)
|
||||||
|
if not customer:
|
||||||
|
raise HTTPException(status_code=404, detail="Customer not found")
|
||||||
|
|
||||||
|
query = select(Sale).where(Sale.customer_id == customer_id)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
query = query.where(Sale.status == status)
|
||||||
|
|
||||||
|
sales = session.exec(query).all()
|
||||||
|
return sales
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
BIN
herodb/src/models/py/business.db
Normal file
BIN
herodb/src/models/py/business.db
Normal file
Binary file not shown.
190
herodb/src/models/py/example.py
Executable file
190
herodb/src/models/py/example.py
Executable file
@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Example script demonstrating the use of the business models.
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from sqlmodel import Session, SQLModel, create_engine, select
|
||||||
|
|
||||||
|
from models import (
|
||||||
|
Currency,
|
||||||
|
Customer,
|
||||||
|
Product,
|
||||||
|
ProductComponent,
|
||||||
|
ProductStatus,
|
||||||
|
ProductType,
|
||||||
|
Sale,
|
||||||
|
SaleItem,
|
||||||
|
SaleStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_tables(engine):
|
||||||
|
"""Create all tables in the database"""
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
|
||||||
|
|
||||||
|
def create_sample_data(session: Session) -> None:
|
||||||
|
"""Create sample data for demonstration"""
|
||||||
|
# Create currencies
|
||||||
|
usd = Currency(currency_code="USD", amount=0.0)
|
||||||
|
eur = Currency(currency_code="EUR", amount=0.0)
|
||||||
|
session.add(usd)
|
||||||
|
session.add(eur)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Create a customer
|
||||||
|
customer = Customer.new(
|
||||||
|
name="Acme Corporation",
|
||||||
|
description="A fictional company",
|
||||||
|
pubkey="acme123456",
|
||||||
|
contact_sids=["circle1_contact123", "circle2_contact456"]
|
||||||
|
)
|
||||||
|
session.add(customer)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Create product components
|
||||||
|
cpu_component = ProductComponent.new(
|
||||||
|
name="CPU",
|
||||||
|
description="Central Processing Unit",
|
||||||
|
quantity=1,
|
||||||
|
)
|
||||||
|
ram_component = ProductComponent.new(
|
||||||
|
name="RAM",
|
||||||
|
description="Random Access Memory",
|
||||||
|
quantity=2,
|
||||||
|
)
|
||||||
|
session.add(cpu_component)
|
||||||
|
session.add(ram_component)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Create products
|
||||||
|
laptop_price = Currency(currency_code="USD", amount=1200.0)
|
||||||
|
session.add(laptop_price)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
laptop = Product.new(
|
||||||
|
name="Laptop",
|
||||||
|
description="High-performance laptop",
|
||||||
|
price=laptop_price,
|
||||||
|
type_=ProductType.PRODUCT,
|
||||||
|
category="Electronics",
|
||||||
|
status=ProductStatus.AVAILABLE,
|
||||||
|
max_amount=100,
|
||||||
|
validity_days=365,
|
||||||
|
istemplate=False,
|
||||||
|
)
|
||||||
|
laptop.add_component(cpu_component)
|
||||||
|
laptop.add_component(ram_component)
|
||||||
|
session.add(laptop)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
support_price = Currency(currency_code="USD", amount=50.0)
|
||||||
|
session.add(support_price)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
support = Product.new(
|
||||||
|
name="Technical Support",
|
||||||
|
description="24/7 technical support",
|
||||||
|
price=support_price,
|
||||||
|
type_=ProductType.SERVICE,
|
||||||
|
category="Support",
|
||||||
|
status=ProductStatus.AVAILABLE,
|
||||||
|
max_amount=1000,
|
||||||
|
validity_days=30,
|
||||||
|
istemplate=True, # This is a template product
|
||||||
|
)
|
||||||
|
session.add(support)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Create a sale
|
||||||
|
sale = Sale.new(
|
||||||
|
customer=customer,
|
||||||
|
currency_code="USD",
|
||||||
|
)
|
||||||
|
session.add(sale)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Create sale items
|
||||||
|
laptop_unit_price = Currency(currency_code="USD", amount=1200.0)
|
||||||
|
session.add(laptop_unit_price)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
laptop_item = SaleItem.new(
|
||||||
|
product=laptop,
|
||||||
|
quantity=1,
|
||||||
|
unit_price=laptop_unit_price,
|
||||||
|
active_till=datetime.datetime.utcnow() + datetime.timedelta(days=365),
|
||||||
|
)
|
||||||
|
sale.add_item(laptop_item)
|
||||||
|
|
||||||
|
support_unit_price = Currency(currency_code="USD", amount=50.0)
|
||||||
|
session.add(support_unit_price)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
support_item = SaleItem.new(
|
||||||
|
product=support,
|
||||||
|
quantity=2,
|
||||||
|
unit_price=support_unit_price,
|
||||||
|
active_till=datetime.datetime.utcnow() + datetime.timedelta(days=30),
|
||||||
|
)
|
||||||
|
sale.add_item(support_item)
|
||||||
|
|
||||||
|
# Complete the sale
|
||||||
|
sale.update_status(SaleStatus.COMPLETED)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def query_data(session: Session) -> None:
|
||||||
|
"""Query and display data from the database"""
|
||||||
|
print("\n=== Customers ===")
|
||||||
|
customers = session.exec(select(Customer)).all()
|
||||||
|
for customer in customers:
|
||||||
|
print(f"Customer: {customer.name} ({customer.pubkey})")
|
||||||
|
print(f" Description: {customer.description}")
|
||||||
|
print(f" Contact SIDs: {', '.join(customer.contact_sids)}")
|
||||||
|
print(f" Created at: {customer.created_at}")
|
||||||
|
|
||||||
|
print("\n=== Products ===")
|
||||||
|
products = session.exec(select(Product)).all()
|
||||||
|
for product in products:
|
||||||
|
print(f"Product: {product.name} ({product.type_.value})")
|
||||||
|
print(f" Description: {product.description}")
|
||||||
|
print(f" Price: {product.price.amount} {product.price.currency_code}")
|
||||||
|
print(f" Status: {product.status.value}")
|
||||||
|
print(f" Is Template: {product.istemplate}")
|
||||||
|
print(f" Components:")
|
||||||
|
for component in product.components:
|
||||||
|
print(f" - {component.name}: {component.quantity}")
|
||||||
|
|
||||||
|
print("\n=== Sales ===")
|
||||||
|
sales = session.exec(select(Sale)).all()
|
||||||
|
for sale in sales:
|
||||||
|
print(f"Sale to: {sale.customer.name}")
|
||||||
|
print(f" Status: {sale.status.value}")
|
||||||
|
print(f" Total: {sale.total_amount.amount} {sale.total_amount.currency_code}")
|
||||||
|
print(f" Items:")
|
||||||
|
for item in sale.items:
|
||||||
|
print(f" - {item.name}: {item.quantity} x {item.unit_price.amount} = {item.subtotal.amount} {item.subtotal.currency_code}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function"""
|
||||||
|
print("Creating in-memory SQLite database...")
|
||||||
|
engine = create_engine("sqlite:///business.db", echo=False)
|
||||||
|
|
||||||
|
print("Creating tables...")
|
||||||
|
create_tables(engine)
|
||||||
|
|
||||||
|
print("Creating sample data...")
|
||||||
|
with Session(engine) as session:
|
||||||
|
create_sample_data(session)
|
||||||
|
|
||||||
|
print("Querying data...")
|
||||||
|
with Session(engine) as session:
|
||||||
|
query_data(session)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
49
herodb/src/models/py/install_and_run.sh
Executable file
49
herodb/src/models/py/install_and_run.sh
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script to install dependencies using uv and run the example script
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Change to the directory where this script is located
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
echo "Changed to directory: $SCRIPT_DIR"
|
||||||
|
|
||||||
|
# Define variables
|
||||||
|
VENV_DIR=".venv"
|
||||||
|
REQUIREMENTS="sqlmodel pydantic"
|
||||||
|
|
||||||
|
# Check if uv is installed
|
||||||
|
if ! command -v uv &> /dev/null; then
|
||||||
|
echo "Error: uv is not installed."
|
||||||
|
echo "Please install it using: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create virtual environment if it doesn't exist
|
||||||
|
if [ ! -d "$VENV_DIR" ]; then
|
||||||
|
echo "Creating virtual environment..."
|
||||||
|
uv venv "$VENV_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activate virtual environment
|
||||||
|
echo "Activating virtual environment..."
|
||||||
|
source "$VENV_DIR/bin/activate"
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
echo "Installing dependencies using uv..."
|
||||||
|
uv pip install $REQUIREMENTS
|
||||||
|
|
||||||
|
# Make example.py executable
|
||||||
|
chmod +x example.py
|
||||||
|
|
||||||
|
# Remove existing database file if it exists
|
||||||
|
if [ -f "business.db" ]; then
|
||||||
|
echo "Removing existing database file..."
|
||||||
|
rm business.db
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the example script
|
||||||
|
echo "Running example script..."
|
||||||
|
python example.py
|
||||||
|
|
||||||
|
echo "Done!"
|
297
herodb/src/models/py/models.py
Normal file
297
herodb/src/models/py/models.py
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
"""
|
||||||
|
Python port of the business models from Rust using SQLModel.
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from enum import Enum
|
||||||
|
import json
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from sqlmodel import Field, Relationship, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class SaleStatus(str, Enum):
|
||||||
|
"""SaleStatus represents the status of a sale"""
|
||||||
|
PENDING = "pending"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
CANCELLED = "cancelled"
|
||||||
|
|
||||||
|
|
||||||
|
class ProductType(str, Enum):
|
||||||
|
"""ProductType represents the type of a product"""
|
||||||
|
PRODUCT = "product"
|
||||||
|
SERVICE = "service"
|
||||||
|
|
||||||
|
|
||||||
|
class ProductStatus(str, Enum):
|
||||||
|
"""ProductStatus represents the status of a product"""
|
||||||
|
AVAILABLE = "available"
|
||||||
|
UNAVAILABLE = "unavailable"
|
||||||
|
|
||||||
|
|
||||||
|
class Currency(SQLModel, table=True):
|
||||||
|
"""Currency represents a monetary value with amount and currency code"""
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
amount: float
|
||||||
|
currency_code: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new(cls, amount: float, currency_code: str) -> "Currency":
|
||||||
|
"""Create a new currency with amount and code"""
|
||||||
|
return cls(amount=amount, currency_code=currency_code)
|
||||||
|
|
||||||
|
|
||||||
|
class Customer(SQLModel, table=True):
|
||||||
|
"""Customer represents a customer who can purchase products or services"""
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
pubkey: str
|
||||||
|
contact_sids_json: str = Field(default="[]")
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
sales: List["Sale"] = Relationship(back_populates="customer")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def contact_sids(self) -> List[str]:
|
||||||
|
"""Get the contact SIDs as a list"""
|
||||||
|
return json.loads(self.contact_sids_json)
|
||||||
|
|
||||||
|
@contact_sids.setter
|
||||||
|
def contact_sids(self, value: List[str]) -> None:
|
||||||
|
"""Set the contact SIDs from a list"""
|
||||||
|
self.contact_sids_json = json.dumps(value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new(cls, name: str, description: str, pubkey: str, contact_sids: List[str] = None) -> "Customer":
|
||||||
|
"""Create a new customer with default timestamps"""
|
||||||
|
customer = cls(
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
pubkey=pubkey,
|
||||||
|
)
|
||||||
|
if contact_sids:
|
||||||
|
customer.contact_sids = contact_sids
|
||||||
|
return customer
|
||||||
|
|
||||||
|
def add_contact(self, contact_id: int) -> None:
|
||||||
|
"""Add a contact ID to the customer"""
|
||||||
|
# In a real implementation, this would add a relationship to a Contact model
|
||||||
|
# For simplicity, we're not implementing the Contact model in this example
|
||||||
|
self.updated_at = datetime.utcnow()
|
||||||
|
|
||||||
|
def add_contact_sid(self, circle_id: str, object_id: str) -> None:
|
||||||
|
"""Add a smart ID (sid) to the customer's contact_sids list"""
|
||||||
|
sid = f"{circle_id}_{object_id}"
|
||||||
|
sids = self.contact_sids
|
||||||
|
if sid not in sids:
|
||||||
|
sids.append(sid)
|
||||||
|
self.contact_sids = sids
|
||||||
|
self.updated_at = datetime.utcnow()
|
||||||
|
|
||||||
|
|
||||||
|
class ProductComponent(SQLModel, table=True):
|
||||||
|
"""ProductComponent represents a component of a product"""
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
quantity: int
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
product_id: Optional[int] = Field(default=None, foreign_key="product.id")
|
||||||
|
product: Optional["Product"] = Relationship(back_populates="components")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new(cls, name: str, description: str, quantity: int) -> "ProductComponent":
|
||||||
|
"""Create a new product component with default timestamps"""
|
||||||
|
return cls(
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
quantity=quantity,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Product(SQLModel, table=True):
|
||||||
|
"""Product represents a product or service offered"""
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
type_: ProductType = Field(sa_column_kwargs={"name": "type"})
|
||||||
|
category: str
|
||||||
|
status: ProductStatus
|
||||||
|
max_amount: int
|
||||||
|
purchase_till: datetime
|
||||||
|
active_till: datetime
|
||||||
|
istemplate: bool = Field(default=False)
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
|
||||||
|
# Price relationship
|
||||||
|
price_id: Optional[int] = Field(default=None, foreign_key="currency.id")
|
||||||
|
price: Optional[Currency] = Relationship()
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
components: List[ProductComponent] = Relationship(back_populates="product")
|
||||||
|
sale_items: List["SaleItem"] = Relationship(back_populates="product")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new(
|
||||||
|
cls,
|
||||||
|
name: str,
|
||||||
|
description: str,
|
||||||
|
price: Currency,
|
||||||
|
type_: ProductType,
|
||||||
|
category: str,
|
||||||
|
status: ProductStatus,
|
||||||
|
max_amount: int,
|
||||||
|
validity_days: int,
|
||||||
|
istemplate: bool = False,
|
||||||
|
) -> "Product":
|
||||||
|
"""Create a new product with default timestamps"""
|
||||||
|
now = datetime.utcnow()
|
||||||
|
return cls(
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
price=price,
|
||||||
|
type_=type_,
|
||||||
|
category=category,
|
||||||
|
status=status,
|
||||||
|
max_amount=max_amount,
|
||||||
|
purchase_till=now + timedelta(days=365),
|
||||||
|
active_till=now + timedelta(days=validity_days),
|
||||||
|
istemplate=istemplate,
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_component(self, component: ProductComponent) -> None:
|
||||||
|
"""Add a component to this product"""
|
||||||
|
component.product = self
|
||||||
|
self.components.append(component)
|
||||||
|
self.updated_at = datetime.utcnow()
|
||||||
|
|
||||||
|
def set_purchase_period(self, purchase_till: datetime) -> None:
|
||||||
|
"""Update the purchase availability timeframe"""
|
||||||
|
self.purchase_till = purchase_till
|
||||||
|
self.updated_at = datetime.utcnow()
|
||||||
|
|
||||||
|
def set_active_period(self, active_till: datetime) -> None:
|
||||||
|
"""Update the active timeframe"""
|
||||||
|
self.active_till = active_till
|
||||||
|
self.updated_at = datetime.utcnow()
|
||||||
|
|
||||||
|
def is_purchasable(self) -> bool:
|
||||||
|
"""Check if the product is available for purchase"""
|
||||||
|
return self.status == ProductStatus.AVAILABLE and datetime.utcnow() <= self.purchase_till
|
||||||
|
|
||||||
|
def is_active(self) -> bool:
|
||||||
|
"""Check if the product is still active (for services)"""
|
||||||
|
return datetime.utcnow() <= self.active_till
|
||||||
|
|
||||||
|
|
||||||
|
class SaleItem(SQLModel, table=True):
|
||||||
|
"""SaleItem represents an item in a sale"""
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
name: str
|
||||||
|
quantity: int
|
||||||
|
active_till: datetime
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
sale_id: Optional[int] = Field(default=None, foreign_key="sale.id")
|
||||||
|
sale: Optional["Sale"] = Relationship(back_populates="items")
|
||||||
|
|
||||||
|
product_id: Optional[int] = Field(default=None, foreign_key="product.id")
|
||||||
|
product: Optional[Product] = Relationship(back_populates="sale_items")
|
||||||
|
|
||||||
|
unit_price_id: Optional[int] = Field(default=None, foreign_key="currency.id")
|
||||||
|
unit_price: Optional[Currency] = Relationship(sa_relationship_kwargs={"foreign_keys": "[SaleItem.unit_price_id]"})
|
||||||
|
|
||||||
|
subtotal_id: Optional[int] = Field(default=None, foreign_key="currency.id")
|
||||||
|
subtotal: Optional[Currency] = Relationship(sa_relationship_kwargs={"foreign_keys": "[SaleItem.subtotal_id]"})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new(
|
||||||
|
cls,
|
||||||
|
product: Product,
|
||||||
|
quantity: int,
|
||||||
|
unit_price: Currency,
|
||||||
|
active_till: datetime,
|
||||||
|
) -> "SaleItem":
|
||||||
|
"""Create a new sale item"""
|
||||||
|
# Calculate subtotal
|
||||||
|
amount = unit_price.amount * quantity
|
||||||
|
subtotal = Currency(
|
||||||
|
amount=amount,
|
||||||
|
currency_code=unit_price.currency_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
name=product.name,
|
||||||
|
product=product,
|
||||||
|
quantity=quantity,
|
||||||
|
unit_price=unit_price,
|
||||||
|
subtotal=subtotal,
|
||||||
|
active_till=active_till,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Sale(SQLModel, table=True):
|
||||||
|
"""Sale represents a sale of products or services"""
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
status: SaleStatus
|
||||||
|
sale_date: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
customer_id: Optional[int] = Field(default=None, foreign_key="customer.id")
|
||||||
|
customer: Optional[Customer] = Relationship(back_populates="sales")
|
||||||
|
|
||||||
|
total_amount_id: Optional[int] = Field(default=None, foreign_key="currency.id")
|
||||||
|
total_amount: Optional[Currency] = Relationship()
|
||||||
|
|
||||||
|
items: List[SaleItem] = Relationship(back_populates="sale")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new(
|
||||||
|
cls,
|
||||||
|
customer: Customer,
|
||||||
|
currency_code: str,
|
||||||
|
status: SaleStatus = SaleStatus.PENDING,
|
||||||
|
) -> "Sale":
|
||||||
|
"""Create a new sale with default timestamps"""
|
||||||
|
total_amount = Currency(amount=0.0, currency_code=currency_code)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
customer=customer,
|
||||||
|
total_amount=total_amount,
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_item(self, item: SaleItem) -> None:
|
||||||
|
"""Add an item to the sale and update the total amount"""
|
||||||
|
item.sale = self
|
||||||
|
|
||||||
|
# Update the total amount
|
||||||
|
if not self.items:
|
||||||
|
# First item, initialize the total amount with the same currency
|
||||||
|
self.total_amount = Currency(
|
||||||
|
amount=item.subtotal.amount,
|
||||||
|
currency_code=item.subtotal.currency_code,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Add to the existing total
|
||||||
|
# (Assumes all items have the same currency)
|
||||||
|
self.total_amount.amount += item.subtotal.amount
|
||||||
|
|
||||||
|
# Add the item to the list
|
||||||
|
self.items.append(item)
|
||||||
|
|
||||||
|
# Update the sale timestamp
|
||||||
|
self.updated_at = datetime.utcnow()
|
||||||
|
|
||||||
|
def update_status(self, status: SaleStatus) -> None:
|
||||||
|
"""Update the status of the sale"""
|
||||||
|
self.status = status
|
||||||
|
self.updated_at = datetime.utcnow()
|
42
herodb/src/models/py/server.sh
Executable file
42
herodb/src/models/py/server.sh
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script to start the FastAPI server
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Change to the directory where this script is located
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
echo "Changed to directory: $SCRIPT_DIR"
|
||||||
|
|
||||||
|
# Define variables
|
||||||
|
VENV_DIR=".venv"
|
||||||
|
REQUIREMENTS="sqlmodel pydantic fastapi uvicorn"
|
||||||
|
|
||||||
|
# Check if uv is installed
|
||||||
|
if ! command -v uv &> /dev/null; then
|
||||||
|
echo "Error: uv is not installed."
|
||||||
|
echo "Please install it using: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create virtual environment if it doesn't exist
|
||||||
|
if [ ! -d "$VENV_DIR" ]; then
|
||||||
|
echo "Creating virtual environment..."
|
||||||
|
uv venv "$VENV_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activate virtual environment
|
||||||
|
echo "Activating virtual environment..."
|
||||||
|
source "$VENV_DIR/bin/activate"
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
echo "Installing dependencies using uv..."
|
||||||
|
uv pip install $REQUIREMENTS
|
||||||
|
|
||||||
|
# Make api.py executable
|
||||||
|
chmod +x api.py
|
||||||
|
|
||||||
|
# Start the FastAPI server
|
||||||
|
echo "Starting FastAPI server..."
|
||||||
|
echo "API documentation available at: http://localhost:8000/docs"
|
||||||
|
uvicorn api:app --host 0.0.0.0 --port 8000 --reload
|
Loading…
Reference in New Issue
Block a user