From cb90be1c1f40116bd80d4d843f31ceeee82feb13 Mon Sep 17 00:00:00 2001 From: Automated Action Date: Mon, 9 Jun 2025 12:29:47 +0000 Subject: [PATCH] Update code via agent code generation --- app/__init__.py | 1 + app/core/__init__.py | 1 + app/core/config.py | 39 +++++++++++++++++++++++ app/core/security.py | 33 +++++++++++++++++++ app/db/__init__.py | 1 + app/db/base.py | 5 +++ app/db/base_class.py | 13 ++++++++ app/db/session.py | 22 +++++++++++++ app/models/__init__.py | 1 + app/models/order.py | 40 +++++++++++++++++++++++ app/models/product.py | 16 ++++++++++ app/models/user.py | 15 +++++++++ app/schemas/__init__.py | 1 + app/schemas/order.py | 70 +++++++++++++++++++++++++++++++++++++++++ app/schemas/product.py | 47 +++++++++++++++++++++++++++ app/schemas/token.py | 11 +++++++ app/schemas/user.py | 38 ++++++++++++++++++++++ main.py | 49 +++++++++++++++++++++++++++++ requirements.txt | 12 +++++++ 19 files changed, 415 insertions(+) create mode 100644 app/__init__.py create mode 100644 app/core/__init__.py create mode 100644 app/core/config.py create mode 100644 app/core/security.py create mode 100644 app/db/__init__.py create mode 100644 app/db/base.py create mode 100644 app/db/base_class.py create mode 100644 app/db/session.py create mode 100644 app/models/__init__.py create mode 100644 app/models/order.py create mode 100644 app/models/product.py create mode 100644 app/models/user.py create mode 100644 app/schemas/__init__.py create mode 100644 app/schemas/order.py create mode 100644 app/schemas/product.py create mode 100644 app/schemas/token.py create mode 100644 app/schemas/user.py create mode 100644 main.py create mode 100644 requirements.txt diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..31eaabb --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +# E-commerce API application package \ No newline at end of file diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..81ca28a --- /dev/null +++ b/app/core/__init__.py @@ -0,0 +1 @@ +# Core module for the application \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..5be44a3 --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,39 @@ +from typing import List, Union, Dict, Any, Optional +from pydantic import AnyHttpUrl, validator +from pydantic_settings import BaseSettings +import secrets +from pathlib import Path + + +class Settings(BaseSettings): + API_V1_STR: str = "/api/v1" + SECRET_KEY: str = secrets.token_urlsafe(32) + # 60 minutes * 24 hours * 8 days = 8 days + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 + PROJECT_NAME: str = "E-Commerce API" + + # CORS + BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] + + # Database + DB_DIR: Path = Path("/app") / "storage" / "db" + DB_PATH: Path = DB_DIR / "db.sqlite" + SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_PATH}" + + @validator("BACKEND_CORS_ORIGINS", pre=True) + def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: + if isinstance(v, str) and not v.startswith("["): + return [i.strip() for i in v.split(",")] + elif isinstance(v, (list, str)): + return v + raise ValueError(v) + + class Config: + case_sensitive = True + env_file = ".env" + + +settings = Settings() + +# Create the DB directory if it doesn't exist +settings.DB_DIR.mkdir(parents=True, exist_ok=True) \ No newline at end of file diff --git a/app/core/security.py b/app/core/security.py new file mode 100644 index 0000000..53ed719 --- /dev/null +++ b/app/core/security.py @@ -0,0 +1,33 @@ +from datetime import datetime, timedelta +from typing import Any, Union, Optional + +from jose import jwt +from passlib.context import CryptContext + +from app.core.config import settings + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +ALGORITHM = "HS256" + + +def create_access_token( + subject: Union[str, Any], expires_delta: Optional[timedelta] = None +) -> str: + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta( + minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES + ) + to_encode = {"exp": expire, "sub": str(subject)} + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) \ No newline at end of file diff --git a/app/db/__init__.py b/app/db/__init__.py new file mode 100644 index 0000000..2f0b25d --- /dev/null +++ b/app/db/__init__.py @@ -0,0 +1 @@ +# Database module \ No newline at end of file diff --git a/app/db/base.py b/app/db/base.py new file mode 100644 index 0000000..475a0ba --- /dev/null +++ b/app/db/base.py @@ -0,0 +1,5 @@ +# Import all the models here, so that Alembic can detect them +from app.db.base_class import Base +from app.models.user import User +from app.models.product import Product +from app.models.order import Order, OrderItem \ No newline at end of file diff --git a/app/db/base_class.py b/app/db/base_class.py new file mode 100644 index 0000000..6a89118 --- /dev/null +++ b/app/db/base_class.py @@ -0,0 +1,13 @@ +from typing import Any +from sqlalchemy.ext.declarative import as_declarative, declared_attr + + +@as_declarative() +class Base: + id: Any + __name__: str + + # Generate tablename automatically + @declared_attr + def __tablename__(cls) -> str: + return cls.__name__.lower() \ No newline at end of file diff --git a/app/db/session.py b/app/db/session.py new file mode 100644 index 0000000..c8b199c --- /dev/null +++ b/app/db/session.py @@ -0,0 +1,22 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from app.core.config import settings + +# Create the SQLAlchemy engine +engine = create_engine( + settings.SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False} # Only needed for SQLite +) + +# Create a SessionLocal class +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +# Dependency to get DB session +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..d8cfe8a --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1 @@ +# Models package \ No newline at end of file diff --git a/app/models/order.py b/app/models/order.py new file mode 100644 index 0000000..9ed4e69 --- /dev/null +++ b/app/models/order.py @@ -0,0 +1,40 @@ +from sqlalchemy import Column, String, Integer, Float, ForeignKey, DateTime, Enum +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +import enum + +from app.db.base_class import Base + + +class OrderStatus(str, enum.Enum): + PENDING = "pending" + PAID = "paid" + SHIPPED = "shipped" + DELIVERED = "delivered" + CANCELLED = "cancelled" + + +class Order(Base): + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("user.id"), nullable=False) + status = Column(Enum(OrderStatus), default=OrderStatus.PENDING, nullable=False) + total_amount = Column(Float, nullable=False) + shipping_address = Column(String, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + # Relationships + user = relationship("User", backref="orders") + items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan") + + +class OrderItem(Base): + id = Column(Integer, primary_key=True, index=True) + order_id = Column(Integer, ForeignKey("order.id"), nullable=False) + product_id = Column(Integer, ForeignKey("product.id"), nullable=False) + quantity = Column(Integer, nullable=False) + unit_price = Column(Float, nullable=False) + + # Relationships + order = relationship("Order", back_populates="items") + product = relationship("Product") \ No newline at end of file diff --git a/app/models/product.py b/app/models/product.py new file mode 100644 index 0000000..1b774fc --- /dev/null +++ b/app/models/product.py @@ -0,0 +1,16 @@ +from sqlalchemy import Column, String, Integer, Float, Text, Boolean, DateTime +from sqlalchemy.sql import func + +from app.db.base_class import Base + + +class Product(Base): + id = Column(Integer, primary_key=True, index=True) + name = Column(String, index=True, nullable=False) + description = Column(Text, nullable=True) + price = Column(Float, nullable=False) + stock = Column(Integer, nullable=False, default=0) + image_url = Column(String, nullable=True) + is_active = Column(Boolean, default=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..70a1217 --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,15 @@ +from sqlalchemy import Boolean, Column, String, Integer, DateTime +from sqlalchemy.sql import func + +from app.db.base_class import Base + + +class User(Base): + 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, index=True) + is_active = Column(Boolean, default=True) + is_superuser = Column(Boolean, default=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) \ No newline at end of file diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..40587b8 --- /dev/null +++ b/app/schemas/__init__.py @@ -0,0 +1 @@ +# Schemas package \ No newline at end of file diff --git a/app/schemas/order.py b/app/schemas/order.py new file mode 100644 index 0000000..ffcef43 --- /dev/null +++ b/app/schemas/order.py @@ -0,0 +1,70 @@ +from typing import List, Optional +from pydantic import BaseModel, Field +from datetime import datetime + +from app.models.order import OrderStatus + + +# OrderItem schemas +class OrderItemBase(BaseModel): + product_id: int + quantity: int = Field(..., gt=0) + + +class OrderItemCreate(OrderItemBase): + pass + + +class OrderItemUpdate(OrderItemBase): + pass + + +class OrderItemInDBBase(OrderItemBase): + id: int + order_id: int + unit_price: float + + class Config: + from_attributes = True + + +class OrderItem(OrderItemInDBBase): + pass + + +# Order schemas +class OrderBase(BaseModel): + user_id: Optional[int] = None + shipping_address: Optional[str] = None + status: Optional[OrderStatus] = None + + +class OrderCreate(OrderBase): + user_id: int + shipping_address: str + items: List[OrderItemCreate] + + +class OrderUpdate(OrderBase): + status: Optional[OrderStatus] = None + + +class OrderInDBBase(OrderBase): + id: int + user_id: int + total_amount: float + status: OrderStatus + shipping_address: str + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class Order(OrderInDBBase): + items: List[OrderItem] + + +class OrderInDB(OrderInDBBase): + pass \ No newline at end of file diff --git a/app/schemas/product.py b/app/schemas/product.py new file mode 100644 index 0000000..911c80c --- /dev/null +++ b/app/schemas/product.py @@ -0,0 +1,47 @@ +from typing import Optional +from pydantic import BaseModel, Field +from datetime import datetime + + +# Shared properties +class ProductBase(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + price: Optional[float] = None + stock: Optional[int] = None + image_url: Optional[str] = None + is_active: Optional[bool] = True + + +# Properties to receive on product creation +class ProductCreate(ProductBase): + name: str + price: float = Field(..., gt=0) + stock: int = Field(..., ge=0) + + +# Properties to receive on product update +class ProductUpdate(ProductBase): + pass + + +class ProductInDBBase(ProductBase): + id: int + name: str + price: float + stock: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +# Additional properties to return via API +class Product(ProductInDBBase): + pass + + +# Additional properties stored in DB +class ProductInDB(ProductInDBBase): + pass \ No newline at end of file diff --git a/app/schemas/token.py b/app/schemas/token.py new file mode 100644 index 0000000..713384e --- /dev/null +++ b/app/schemas/token.py @@ -0,0 +1,11 @@ +from typing import Optional +from pydantic import BaseModel + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenPayload(BaseModel): + sub: Optional[int] = None \ No newline at end of file diff --git a/app/schemas/user.py b/app/schemas/user.py new file mode 100644 index 0000000..d3593dd --- /dev/null +++ b/app/schemas/user.py @@ -0,0 +1,38 @@ +from typing import Optional +from pydantic import BaseModel, EmailStr, Field + + +# Shared properties +class UserBase(BaseModel): + email: Optional[EmailStr] = None + is_active: Optional[bool] = True + is_superuser: bool = False + full_name: Optional[str] = None + + +# Properties to receive via API on creation +class UserCreate(UserBase): + email: EmailStr + password: str = Field(..., min_length=8) + + +# Properties to receive via API on update +class UserUpdate(UserBase): + password: Optional[str] = Field(None, min_length=8) + + +class UserInDBBase(UserBase): + id: Optional[int] = None + + class Config: + from_attributes = True + + +# Additional properties to return via API +class User(UserInDBBase): + pass + + +# Additional properties stored in DB +class UserInDB(UserInDBBase): + hashed_password: str \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..e5036c6 --- /dev/null +++ b/main.py @@ -0,0 +1,49 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from pathlib import Path + +from app.api.routes import api_router +from app.core.config import settings + +app = FastAPI( + title=settings.PROJECT_NAME, + description="E-commerce API for managing products, users, and orders", + version="0.1.0", + openapi_url="/openapi.json", + docs_url="/docs", + redoc_url="/redoc", +) + +# Set up CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include the API router +app.include_router(api_router) + +@app.get("/") +async def root(): + """ + Root endpoint returning basic information about the API. + """ + return { + "title": settings.PROJECT_NAME, + "docs": f"{settings.API_V1_STR}/docs", + "health": "/health" + } + +@app.get("/health", status_code=200) +async def health_check(): + """ + Health check endpoint. + """ + return {"status": "ok"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cd5720a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +fastapi>=0.100.0 +uvicorn>=0.23.0 +pydantic>=2.0.0 +pydantic-settings>=2.0.0 +python-jose[cryptography]>=3.3.0 +passlib[bcrypt]>=1.7.4 +alembic>=1.11.0 +sqlalchemy>=2.0.0 +python-multipart>=0.0.5 +email-validator>=2.0.0 +python-dotenv>=1.0.0 +ruff>=0.0.270 \ No newline at end of file