feat(core): 初始化核心配置和部署文件
- 添加 .env.example 环境变量配置示例 - 添加 .gitignore 忽略文件配置 - 添加 core/config.py 配置管理模块 - 添加 deployments/k8s/configmap.yaml Kubernetes 配置 - 添加 core/database.py 数据库连接管理模块 - 添加 core/dependencies.py 全局依赖模块 - 添加 DEPENDENCIES_UPDATED.md 依赖更新记录 - 添加 deployments/k8s/deployment.yaml Kubernetes 部署配置- 添加 deployments/swarm/docker-compose.swarm.yml Docker Swarm 部署配置 - 添加 deployments/docker/docker-compose.yml Docker 部署配置 - 添加 deployments/docker/Dockerfile 应用镜像构建文件 - 添加 middleware/error_handler.py 全局异常处理中间件
This commit is contained in:
0
apps/__init__.py
Normal file
0
apps/__init__.py
Normal file
0
apps/app_a/__init__.py
Normal file
0
apps/app_a/__init__.py
Normal file
41
apps/app_a/models.py
Normal file
41
apps/app_a/models.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
App A Models - Database models using SQLModel.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from sqlmodel import SQLModel, Field
|
||||
|
||||
|
||||
class UserBase(SQLModel):
|
||||
"""Base user model with shared fields."""
|
||||
|
||||
username: str = Field(index=True, max_length=50)
|
||||
email: str = Field(index=True, max_length=100)
|
||||
full_name: Optional[str] = Field(default=None, max_length=100)
|
||||
is_active: bool = Field(default=True)
|
||||
|
||||
|
||||
class User(UserBase, table=True):
|
||||
"""
|
||||
User table model.
|
||||
|
||||
Represents users in the system.
|
||||
"""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
hashed_password: str = Field(max_length=255)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"username": "john_doe",
|
||||
"email": "john@example.com",
|
||||
"full_name": "John Doe",
|
||||
"is_active": True,
|
||||
}
|
||||
}
|
||||
139
apps/app_a/router.py
Normal file
139
apps/app_a/router.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
App A Router - API endpoints for user management.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from core.database import get_session
|
||||
from core.responses import ApiResponse, success, paginated, ERROR_RESPONSES
|
||||
from core.dependencies import get_trace_id
|
||||
from apps.app_a.schemas import UserCreate, UserUpdate, UserResponse
|
||||
from apps.app_a.services import UserService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/app-a",
|
||||
tags=["App A - Users"]
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/users",
|
||||
response_model=ApiResponse[UserResponse],
|
||||
status_code=201,
|
||||
summary="Create a new user",
|
||||
description="Create a new user with username, email, and password",
|
||||
responses={
|
||||
201: {"description": "User created successfully"},
|
||||
400: ERROR_RESPONSES[400],
|
||||
409: ERROR_RESPONSES[409],
|
||||
422: ERROR_RESPONSES[422],
|
||||
500: ERROR_RESPONSES[500],
|
||||
}
|
||||
)
|
||||
async def create_user(
|
||||
user_data: UserCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
trace_id: str = Depends(get_trace_id)
|
||||
) -> ApiResponse[UserResponse]:
|
||||
"""Create a new user."""
|
||||
user = await UserService.create_user(session, user_data)
|
||||
user_response = UserResponse.model_validate(user)
|
||||
return success(data=user_response, message="User created successfully", trace_id=trace_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/users/{user_id}",
|
||||
response_model=ApiResponse[UserResponse],
|
||||
summary="Get user by ID",
|
||||
description="Retrieve a user by their ID",
|
||||
responses={
|
||||
200: {"description": "User retrieved successfully"},
|
||||
404: ERROR_RESPONSES[404],
|
||||
500: ERROR_RESPONSES[500],
|
||||
}
|
||||
)
|
||||
async def get_user(
|
||||
user_id: int,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
trace_id: str = Depends(get_trace_id)
|
||||
) -> ApiResponse[UserResponse]:
|
||||
"""Get user by ID."""
|
||||
user = await UserService.get_user(session, user_id)
|
||||
user_response = UserResponse.model_validate(user)
|
||||
return success(data=user_response, trace_id=trace_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/users",
|
||||
response_model=ApiResponse,
|
||||
summary="List all users",
|
||||
description="List all users with pagination support",
|
||||
responses={
|
||||
200: {"description": "Users retrieved successfully"},
|
||||
400: ERROR_RESPONSES[400],
|
||||
500: ERROR_RESPONSES[500],
|
||||
}
|
||||
)
|
||||
async def list_users(
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
trace_id: str = Depends(get_trace_id)
|
||||
) -> ApiResponse:
|
||||
"""List all users with pagination."""
|
||||
skip = (page - 1) * page_size
|
||||
users, total = await UserService.list_users(session, skip, page_size)
|
||||
user_responses = [UserResponse.model_validate(user) for user in users]
|
||||
return paginated(
|
||||
items=user_responses,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
trace_id=trace_id
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/users/{user_id}",
|
||||
response_model=ApiResponse[UserResponse],
|
||||
summary="Update user",
|
||||
description="Update user information",
|
||||
responses={
|
||||
200: {"description": "User updated successfully"},
|
||||
400: ERROR_RESPONSES[400],
|
||||
404: ERROR_RESPONSES[404],
|
||||
422: ERROR_RESPONSES[422],
|
||||
500: ERROR_RESPONSES[500],
|
||||
}
|
||||
)
|
||||
async def update_user(
|
||||
user_id: int,
|
||||
user_data: UserUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
trace_id: str = Depends(get_trace_id)
|
||||
) -> ApiResponse[UserResponse]:
|
||||
"""Update user."""
|
||||
user = await UserService.update_user(session, user_id, user_data)
|
||||
user_response = UserResponse.model_validate(user)
|
||||
return success(data=user_response, message="User updated successfully", trace_id=trace_id)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/users/{user_id}",
|
||||
response_model=ApiResponse[None],
|
||||
summary="Delete user",
|
||||
description="Delete a user by ID",
|
||||
responses={
|
||||
200: {"description": "User deleted successfully"},
|
||||
404: ERROR_RESPONSES[404],
|
||||
500: ERROR_RESPONSES[500],
|
||||
}
|
||||
)
|
||||
async def delete_user(
|
||||
user_id: int,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
trace_id: str = Depends(get_trace_id)
|
||||
) -> ApiResponse[None]:
|
||||
"""Delete user."""
|
||||
await UserService.delete_user(session, user_id)
|
||||
return success(data=None, message="User deleted successfully", trace_id=trace_id)
|
||||
69
apps/app_a/schemas.py
Normal file
69
apps/app_a/schemas.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
App A Schemas - Pydantic schemas for request/response validation.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field, EmailStr
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
"""Schema for creating a new user."""
|
||||
|
||||
username: str = Field(..., min_length=3, max_length=50)
|
||||
email: EmailStr
|
||||
password: str = Field(..., min_length=6)
|
||||
full_name: Optional[str] = Field(None, max_length=100)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"username": "john_doe",
|
||||
"email": "john@example.com",
|
||||
"password": "secret123",
|
||||
"full_name": "John Doe",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
"""Schema for updating a user."""
|
||||
|
||||
email: Optional[EmailStr] = None
|
||||
full_name: Optional[str] = Field(None, max_length=100)
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"email": "newemail@example.com",
|
||||
"full_name": "John Smith",
|
||||
"is_active": True,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""Schema for user response."""
|
||||
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
full_name: Optional[str]
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"id": 1,
|
||||
"username": "john_doe",
|
||||
"email": "john@example.com",
|
||||
"full_name": "John Doe",
|
||||
"is_active": True,
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"updated_at": "2024-01-15T10:30:00Z",
|
||||
}
|
||||
}
|
||||
187
apps/app_a/services.py
Normal file
187
apps/app_a/services.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
App A Services - Business logic layer.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from apps.app_a.models import User
|
||||
from apps.app_a.schemas import UserCreate, UserUpdate
|
||||
from core.exceptions import NotFoundException, AlreadyExistsException
|
||||
|
||||
|
||||
class UserService:
|
||||
"""Service for user business logic."""
|
||||
|
||||
@staticmethod
|
||||
async def create_user(
|
||||
session: AsyncSession,
|
||||
user_data: UserCreate
|
||||
) -> User:
|
||||
"""
|
||||
Create a new user.
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
user_data: User creation data
|
||||
|
||||
Returns:
|
||||
User: Created user
|
||||
|
||||
Raises:
|
||||
AlreadyExistsException: If username or email already exists
|
||||
"""
|
||||
# Check if username exists
|
||||
result = await session.execute(
|
||||
select(User).where(User.username == user_data.username)
|
||||
)
|
||||
if result.scalar_one_or_none():
|
||||
raise AlreadyExistsException(
|
||||
message=f"Username '{user_data.username}' already exists",
|
||||
resource="User"
|
||||
)
|
||||
|
||||
# Check if email exists
|
||||
result = await session.execute(
|
||||
select(User).where(User.email == user_data.email)
|
||||
)
|
||||
if result.scalar_one_or_none():
|
||||
raise AlreadyExistsException(
|
||||
message=f"Email '{user_data.email}' already exists",
|
||||
resource="User"
|
||||
)
|
||||
|
||||
# Create user (in production, hash the password)
|
||||
user = User(
|
||||
username=user_data.username,
|
||||
email=user_data.email,
|
||||
full_name=user_data.full_name,
|
||||
hashed_password=f"hashed_{user_data.password}", # TODO: Use proper hashing
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
async def get_user(
|
||||
session: AsyncSession,
|
||||
user_id: int
|
||||
) -> User:
|
||||
"""
|
||||
Get user by ID.
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
user_id: User ID
|
||||
|
||||
Returns:
|
||||
User: User instance
|
||||
|
||||
Raises:
|
||||
NotFoundException: If user not found
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
raise NotFoundException(
|
||||
message=f"User with ID {user_id} not found",
|
||||
resource="User"
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
async def list_users(
|
||||
session: AsyncSession,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> tuple[list[User], int]:
|
||||
"""
|
||||
List all users with pagination.
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
skip: Number of records to skip
|
||||
limit: Maximum number of records to return
|
||||
|
||||
Returns:
|
||||
tuple: (list of users, total count)
|
||||
"""
|
||||
# Get total count
|
||||
count_result = await session.execute(
|
||||
select(User)
|
||||
)
|
||||
total = len(count_result.all())
|
||||
|
||||
# Get paginated results
|
||||
result = await session.execute(
|
||||
select(User).offset(skip).limit(limit)
|
||||
)
|
||||
users = result.scalars().all()
|
||||
|
||||
return list(users), total
|
||||
|
||||
@staticmethod
|
||||
async def update_user(
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
user_data: UserUpdate
|
||||
) -> User:
|
||||
"""
|
||||
Update user.
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
user_id: User ID
|
||||
user_data: Update data
|
||||
|
||||
Returns:
|
||||
User: Updated user
|
||||
|
||||
Raises:
|
||||
NotFoundException: If user not found
|
||||
"""
|
||||
user = await UserService.get_user(session, user_id)
|
||||
|
||||
# Update fields
|
||||
if user_data.email is not None:
|
||||
user.email = user_data.email
|
||||
if user_data.full_name is not None:
|
||||
user.full_name = user_data.full_name
|
||||
if user_data.is_active is not None:
|
||||
user.is_active = user_data.is_active
|
||||
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
async def delete_user(
|
||||
session: AsyncSession,
|
||||
user_id: int
|
||||
) -> None:
|
||||
"""
|
||||
Delete user.
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
user_id: User ID
|
||||
|
||||
Raises:
|
||||
NotFoundException: If user not found
|
||||
"""
|
||||
user = await UserService.get_user(session, user_id)
|
||||
await session.delete(user)
|
||||
await session.commit()
|
||||
0
apps/app_b/__init__.py
Normal file
0
apps/app_b/__init__.py
Normal file
45
apps/app_b/models.py
Normal file
45
apps/app_b/models.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
App B Models - Database models for products.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from sqlmodel import SQLModel, Field
|
||||
|
||||
|
||||
class ProductBase(SQLModel):
|
||||
"""Base product model with shared fields."""
|
||||
|
||||
name: str = Field(index=True, max_length=100)
|
||||
description: Optional[str] = Field(default=None, max_length=500)
|
||||
price: Decimal = Field(decimal_places=2)
|
||||
stock: int = Field(default=0, ge=0)
|
||||
is_available: bool = Field(default=True)
|
||||
|
||||
|
||||
class Product(ProductBase, table=True):
|
||||
"""
|
||||
Product table model.
|
||||
|
||||
Represents products in the system.
|
||||
"""
|
||||
|
||||
__tablename__ = "products"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
sku: str = Field(unique=True, index=True, max_length=50)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"name": "Laptop",
|
||||
"description": "High-performance laptop",
|
||||
"price": "999.99",
|
||||
"stock": 10,
|
||||
"sku": "LAP-001",
|
||||
"is_available": True,
|
||||
}
|
||||
}
|
||||
93
apps/app_b/router.py
Normal file
93
apps/app_b/router.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
App B Router - API endpoints for product management.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from core.database import get_session
|
||||
from core.responses import ApiResponse, success, paginated, ERROR_RESPONSES
|
||||
from core.dependencies import get_trace_id
|
||||
from apps.app_b.schemas import ProductCreate, ProductResponse
|
||||
from apps.app_b.services import ProductService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/app-b",
|
||||
tags=["App B - Products"]
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/products",
|
||||
response_model=ApiResponse[ProductResponse],
|
||||
status_code=201,
|
||||
summary="Create a new product",
|
||||
description="Create a new product with SKU, name, price, and stock",
|
||||
responses={
|
||||
201: {"description": "Product created successfully"},
|
||||
400: ERROR_RESPONSES[400],
|
||||
409: ERROR_RESPONSES[409],
|
||||
422: ERROR_RESPONSES[422],
|
||||
500: ERROR_RESPONSES[500],
|
||||
}
|
||||
)
|
||||
async def create_product(
|
||||
product_data: ProductCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
trace_id: str = Depends(get_trace_id)
|
||||
) -> ApiResponse[ProductResponse]:
|
||||
"""Create a new product."""
|
||||
product = await ProductService.create_product(session, product_data)
|
||||
product_response = ProductResponse.model_validate(product)
|
||||
return success(data=product_response, message="Product created successfully", trace_id=trace_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/products/{product_id}",
|
||||
response_model=ApiResponse[ProductResponse],
|
||||
summary="Get product by ID",
|
||||
description="Retrieve a product by its ID",
|
||||
responses={
|
||||
200: {"description": "Product retrieved successfully"},
|
||||
404: ERROR_RESPONSES[404],
|
||||
500: ERROR_RESPONSES[500],
|
||||
}
|
||||
)
|
||||
async def get_product(
|
||||
product_id: int,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
trace_id: str = Depends(get_trace_id)
|
||||
) -> ApiResponse[ProductResponse]:
|
||||
"""Get product by ID."""
|
||||
product = await ProductService.get_product(session, product_id)
|
||||
product_response = ProductResponse.model_validate(product)
|
||||
return success(data=product_response, trace_id=trace_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/products",
|
||||
response_model=ApiResponse,
|
||||
summary="List all products",
|
||||
description="List all products with pagination support",
|
||||
responses={
|
||||
200: {"description": "Products retrieved successfully"},
|
||||
400: ERROR_RESPONSES[400],
|
||||
500: ERROR_RESPONSES[500],
|
||||
}
|
||||
)
|
||||
async def list_products(
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
trace_id: str = Depends(get_trace_id)
|
||||
) -> ApiResponse:
|
||||
"""List all products with pagination."""
|
||||
skip = (page - 1) * page_size
|
||||
products, total = await ProductService.list_products(session, skip, page_size)
|
||||
product_responses = [ProductResponse.model_validate(product) for product in products]
|
||||
return paginated(
|
||||
items=product_responses,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
trace_id=trace_id
|
||||
)
|
||||
80
apps/app_b/schemas.py
Normal file
80
apps/app_b/schemas.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
App B Schemas - Pydantic schemas for products.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ProductCreate(BaseModel):
|
||||
"""Schema for creating a new product."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
price: Decimal = Field(..., gt=0, decimal_places=2)
|
||||
stock: int = Field(default=0, ge=0)
|
||||
sku: str = Field(..., min_length=1, max_length=50)
|
||||
is_available: bool = Field(default=True)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"name": "Laptop",
|
||||
"description": "High-performance laptop",
|
||||
"price": "999.99",
|
||||
"stock": 10,
|
||||
"sku": "LAP-001",
|
||||
"is_available": True,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ProductUpdate(BaseModel):
|
||||
"""Schema for updating a product."""
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
price: Optional[Decimal] = Field(None, gt=0, decimal_places=2)
|
||||
stock: Optional[int] = Field(None, ge=0)
|
||||
is_available: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"name": "Updated Laptop",
|
||||
"price": "899.99",
|
||||
"stock": 15,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ProductResponse(BaseModel):
|
||||
"""Schema for product response."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str]
|
||||
price: Decimal
|
||||
stock: int
|
||||
sku: str
|
||||
is_available: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"id": 1,
|
||||
"name": "Laptop",
|
||||
"description": "High-performance laptop",
|
||||
"price": "999.99",
|
||||
"stock": 10,
|
||||
"sku": "LAP-001",
|
||||
"is_available": True,
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"updated_at": "2024-01-15T10:30:00Z",
|
||||
}
|
||||
}
|
||||
78
apps/app_b/services.py
Normal file
78
apps/app_b/services.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
App B Services - Business logic for products.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from apps.app_b.models import Product
|
||||
from apps.app_b.schemas import ProductCreate, ProductUpdate
|
||||
from core.exceptions import NotFoundException, AlreadyExistsException
|
||||
|
||||
|
||||
class ProductService:
|
||||
"""Service for product business logic."""
|
||||
|
||||
@staticmethod
|
||||
async def create_product(
|
||||
session: AsyncSession,
|
||||
product_data: ProductCreate
|
||||
) -> Product:
|
||||
"""Create a new product."""
|
||||
# Check if SKU exists
|
||||
result = await session.execute(
|
||||
select(Product).where(Product.sku == product_data.sku)
|
||||
)
|
||||
if result.scalar_one_or_none():
|
||||
raise AlreadyExistsException(
|
||||
message=f"Product with SKU '{product_data.sku}' already exists",
|
||||
resource="Product"
|
||||
)
|
||||
|
||||
product = Product(
|
||||
**product_data.model_dump(),
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
session.add(product)
|
||||
await session.commit()
|
||||
await session.refresh(product)
|
||||
|
||||
return product
|
||||
|
||||
@staticmethod
|
||||
async def get_product(
|
||||
session: AsyncSession,
|
||||
product_id: int
|
||||
) -> Product:
|
||||
"""Get product by ID."""
|
||||
result = await session.execute(
|
||||
select(Product).where(Product.id == product_id)
|
||||
)
|
||||
product = result.scalar_one_or_none()
|
||||
|
||||
if not product:
|
||||
raise NotFoundException(
|
||||
message=f"Product with ID {product_id} not found",
|
||||
resource="Product"
|
||||
)
|
||||
|
||||
return product
|
||||
|
||||
@staticmethod
|
||||
async def list_products(
|
||||
session: AsyncSession,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> tuple[list[Product], int]:
|
||||
"""List all products with pagination."""
|
||||
count_result = await session.execute(select(Product))
|
||||
total = len(count_result.all())
|
||||
|
||||
result = await session.execute(
|
||||
select(Product).offset(skip).limit(limit)
|
||||
)
|
||||
products = result.scalars().all()
|
||||
|
||||
return list(products), total
|
||||
0
apps/shared/__init__.py
Normal file
0
apps/shared/__init__.py
Normal file
Reference in New Issue
Block a user