Update code via agent code generation
This commit is contained in:
parent
a5fe775f3b
commit
cb90be1c1f
1
app/__init__.py
Normal file
1
app/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# E-commerce API application package
|
1
app/core/__init__.py
Normal file
1
app/core/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Core module for the application
|
39
app/core/config.py
Normal file
39
app/core/config.py
Normal file
@ -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)
|
33
app/core/security.py
Normal file
33
app/core/security.py
Normal file
@ -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)
|
1
app/db/__init__.py
Normal file
1
app/db/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Database module
|
5
app/db/base.py
Normal file
5
app/db/base.py
Normal file
@ -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
|
13
app/db/base_class.py
Normal file
13
app/db/base_class.py
Normal file
@ -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()
|
22
app/db/session.py
Normal file
22
app/db/session.py
Normal file
@ -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()
|
1
app/models/__init__.py
Normal file
1
app/models/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Models package
|
40
app/models/order.py
Normal file
40
app/models/order.py
Normal file
@ -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")
|
16
app/models/product.py
Normal file
16
app/models/product.py
Normal file
@ -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())
|
15
app/models/user.py
Normal file
15
app/models/user.py
Normal file
@ -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())
|
1
app/schemas/__init__.py
Normal file
1
app/schemas/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Schemas package
|
70
app/schemas/order.py
Normal file
70
app/schemas/order.py
Normal file
@ -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
|
47
app/schemas/product.py
Normal file
47
app/schemas/product.py
Normal file
@ -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
|
11
app/schemas/token.py
Normal file
11
app/schemas/token.py
Normal file
@ -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
|
38
app/schemas/user.py
Normal file
38
app/schemas/user.py
Normal file
@ -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
|
49
main.py
Normal file
49
main.py
Normal file
@ -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)
|
12
requirements.txt
Normal file
12
requirements.txt
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user