From 9c7baa3b4e3a3382f196ce5bf4aa8569b467f689 Mon Sep 17 00:00:00 2001 From: despiegk Date: Sat, 19 Apr 2025 08:39:40 +0200 Subject: [PATCH] ... --- herodb/src/models/py/README.md | 131 +++++ herodb/src/models/py/__init__.py | 3 + .../models/py/__pycache__/api.cpython-312.pyc | Bin 0 -> 19051 bytes .../py/__pycache__/models.cpython-312.pyc | Bin 0 -> 13464 bytes herodb/src/models/py/api.py | 455 ++++++++++++++++++ herodb/src/models/py/business.db | Bin 0 -> 28672 bytes herodb/src/models/py/example.py | 190 ++++++++ herodb/src/models/py/install_and_run.sh | 49 ++ herodb/src/models/py/models.py | 297 ++++++++++++ herodb/src/models/py/server.sh | 42 ++ 10 files changed, 1167 insertions(+) create mode 100644 herodb/src/models/py/README.md create mode 100644 herodb/src/models/py/__init__.py create mode 100644 herodb/src/models/py/__pycache__/api.cpython-312.pyc create mode 100644 herodb/src/models/py/__pycache__/models.cpython-312.pyc create mode 100755 herodb/src/models/py/api.py create mode 100644 herodb/src/models/py/business.db create mode 100755 herodb/src/models/py/example.py create mode 100755 herodb/src/models/py/install_and_run.sh create mode 100644 herodb/src/models/py/models.py create mode 100755 herodb/src/models/py/server.sh diff --git a/herodb/src/models/py/README.md b/herodb/src/models/py/README.md new file mode 100644 index 0000000..07f9a9c --- /dev/null +++ b/herodb/src/models/py/README.md @@ -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 \ No newline at end of file diff --git a/herodb/src/models/py/__init__.py b/herodb/src/models/py/__init__.py new file mode 100644 index 0000000..b4c11ea --- /dev/null +++ b/herodb/src/models/py/__init__.py @@ -0,0 +1,3 @@ +""" +Python port of the business models from Rust. +""" \ No newline at end of file diff --git a/herodb/src/models/py/__pycache__/api.cpython-312.pyc b/herodb/src/models/py/__pycache__/api.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4dc565371c93f9b620d7f448380d2a6126f3ed73 GIT binary patch literal 19051 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 o#fKXy#&K)n=f0 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 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) \ No newline at end of file diff --git a/herodb/src/models/py/business.db b/herodb/src/models/py/business.db new file mode 100644 index 0000000000000000000000000000000000000000..a304f0921b93d772386f06909b78473012c09e00 GIT binary patch literal 28672 zcmeI4&u`mg7{~22ZQ=-Orcza9h58zmZG}) zms-cwdp)v6>uc4wrO;{pnmeae`ZZa$+4WR6BJ~X2?O3+1MvFV%BiD45UZni4T;CSu z2jpYv0a@3(8~&N^G-auNcgL5lk1F0EvUEq1rJb6zH^2~E__Rr(@*{DqN~padE!DGa z%}@_x)h4cHx>9E2cz-^%vbo7V?F_6tYKUs~Ez{5pH+sAfvUW80-&nn?IjXH&u5KEy z?-i=8DX!M-(ElD|ox-!aGxJ9m@R}As)e4avi3ir8V69|oWqFzXrZ(<2zNgHG0Q!;{ z&>UBBJtx4jE^M6jc>nt1ul=HIRL^nEzGg?@47-TZH$5ZTv(D=&6K4#83*@nW9H}p& zIMD$z8z>7H?eDG+fHR`ec@~YD-F)#zg}7s>GnDR0@_wBz{Td#L z`(nM}FNmA_lSaSgvvIK?6op}2T#7`@r#aDBS`f;9G#wLV3qmEAJj$|*%<8SR53aDH zn5HdnD3)tl_w*+Ro0et|7TFuBMtTGBuH4*e*IH6fQ(fDn7wC-J+>LK?xmvBv%!z4O zS;$QZ|0~%-b#D0n{{_Rpcyk6uO&|aQAOHd&00JNY0w4eaAOHd&00NT)KBbceuFx3+ z99wH{?>3~Cv=#gKp^zmnRdk;ds7OsrWDVV_xBx zD%I>heG;&Ai_V7NYP;PKVgFP5->Q@r1PLPwGz7E>4*MP^uJ*!}$L>!~e=Z zf5i(#F%SR&5C8!X009sH0T2KI5C8!X0D=FCKsCWJH(2`SpFO+&Ve|wcvTmVJ&gH09 d=0lcaRymQQs>AgM^tr&<^$&%7wwPZ?{{uE{a@GI< literal 0 HcmV?d00001 diff --git a/herodb/src/models/py/example.py b/herodb/src/models/py/example.py new file mode 100755 index 0000000..37d3f56 --- /dev/null +++ b/herodb/src/models/py/example.py @@ -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() \ No newline at end of file diff --git a/herodb/src/models/py/install_and_run.sh b/herodb/src/models/py/install_and_run.sh new file mode 100755 index 0000000..103c3e8 --- /dev/null +++ b/herodb/src/models/py/install_and_run.sh @@ -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!" \ No newline at end of file diff --git a/herodb/src/models/py/models.py b/herodb/src/models/py/models.py new file mode 100644 index 0000000..d396a2f --- /dev/null +++ b/herodb/src/models/py/models.py @@ -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() \ No newline at end of file diff --git a/herodb/src/models/py/server.sh b/herodb/src/models/py/server.sh new file mode 100755 index 0000000..d291078 --- /dev/null +++ b/herodb/src/models/py/server.sh @@ -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 \ No newline at end of file