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