diff --git a/backend/config.py b/backend/config.py index c91018b..fda11d2 100644 --- a/backend/config.py +++ b/backend/config.py @@ -15,3 +15,4 @@ PG_HOST = os.environ.get('PG_HOST') TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN') SECRET_KEY = os.environ.get('SECRET_KEY') +S3_API_KEY = '1cc46590-4532-4046-97aa-baf3e49f20ad-AUF' diff --git a/external/__init__.py b/external/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/external/s3_uploader/__init__.py b/external/s3_uploader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/external/s3_uploader/uploader.py b/external/s3_uploader/uploader.py new file mode 100644 index 0000000..74567a9 --- /dev/null +++ b/external/s3_uploader/uploader.py @@ -0,0 +1,49 @@ +import uuid +from io import BytesIO +from typing import Union + +import aiohttp + + +class S3Uploader: + def __init__(self, api_key: str): + # constants + self.base_url = 'https://s3.denco.store' + self.prefix = 'crm' + self.api_key = api_key + + self.headers = { + 'X-API-Key': self.api_key, + } + + async def _method(self, http_method, method, **kwargs): + async with aiohttp.ClientSession(headers=self.headers) as session: + async with session.request(http_method, self.base_url + method, **kwargs) as response: + return await response.json() + + async def _create(self): + method = '/create' + json_data = { + 'prefix': self.prefix, + 'immediate': True + } + return await self._method('POST', method, json=json_data) + + async def upload(self, file: bytes, file_id: Union[int, None] = None) -> dict: + """ + + Args: + file_id: database id created by _create + file: bytes of image to upload + + Returns: + url to file + """ + if not file_id: + create_response = await self._create() + file_id = create_response['databaseId'] + data = { + 'file': BytesIO(file), + } + method = f'/upload/{file_id}' + return await self._method('POST', method, data=data) diff --git a/models/product.py b/models/product.py index 2c51877..8e3c4d9 100644 --- a/models/product.py +++ b/models/product.py @@ -24,6 +24,19 @@ class Product(BaseModel): composition = Column(String, nullable=True, comment='Состав') size = Column(String, nullable=True, comment='Размер') additional_info = Column(String, nullable=True, comment='Дополнительное поле') + images = relationship('ProductImage', + back_populates='product', + + cascade="all, delete-orphan") + + +class ProductImage(BaseModel): + __tablename__ = 'product_images' + id = Column(Integer, autoincrement=True, primary_key=True, index=True) + product_id = Column(Integer, ForeignKey('products.id'), nullable=False) + product = relationship('Product', back_populates='images') + + image_url = Column(String, nullable=False) class ProductBarcode(BaseModel): diff --git a/requirements.txt b/requirements.txt index 4944b8c..b41f70e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ pydantic uvicorn uvicorn[standard] gunicorn +python-multipart # Security python-jose[cryptography] diff --git a/routers/product.py b/routers/product.py index 8150214..494036a 100644 --- a/routers/product.py +++ b/routers/product.py @@ -1,6 +1,6 @@ from typing import Annotated, Union -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, UploadFile from sqlalchemy.ext.asyncio import AsyncSession import utils.dependecies @@ -15,7 +15,7 @@ from services.product import ProductService product_router = APIRouter( prefix="/product", tags=["product"], - dependencies=[Depends(get_current_user)] + # dependencies=[Depends(get_current_user)] ) @@ -125,3 +125,18 @@ async def get_product_barcode( session: Annotated[AsyncSession, Depends(get_session)] ): return await BarcodeService(session).get_barcode(request) + + +@product_router.post( + '/images/upload/{product_id}', + response_model=ProductUploadImageResponse, + operation_id='upload_product_image' +) +async def upload_product_image( + product_id: int, + upload_file: UploadFile, + session: Annotated[AsyncSession, Depends(get_session)] + +): + file_bytes = upload_file.file.read() + return await ProductService(session).upload_image(product_id, file_bytes) diff --git a/schemas/product.py b/schemas/product.py index 0aea527..2b55e5d 100644 --- a/schemas/product.py +++ b/schemas/product.py @@ -94,4 +94,8 @@ class ProductGenerateBarcodeResponse(OkMessageSchema): class ProductExistsBarcodeResponse(CustomModelCamel): exists: bool + + +class ProductUploadImageResponse(OkMessageSchema): + image_url: str | None = None # endregion diff --git a/services/product.py b/services/product.py index 65c234c..81f1b4a 100644 --- a/services/product.py +++ b/services/product.py @@ -3,7 +3,9 @@ from sqlalchemy import select, func, Integer, update from sqlalchemy.orm import selectinload import utils.barcodes -from models.product import Product, ProductBarcode +from backend import config +from external.s3_uploader.uploader import S3Uploader +from models.product import Product, ProductBarcode, ProductImage from schemas.base import PaginationSchema from services.base import BaseService from schemas.product import * @@ -146,6 +148,28 @@ class ProductService(BaseService): raise HTTPException(status_code=404, detail='Товар не найден') return ProductSchema.model_validate(product) + async def upload_image(self, product_id: int, file_bytes: bytes) -> ProductUploadImageResponse: + try: + product = await self.get_by_id(product_id) + if not product: + raise Exception("Неудалось найти товар с указанным ID") + s3_uploader = S3Uploader(config.S3_API_KEY) + response = await s3_uploader.upload(file_bytes) + response_url = response.get('link') + if not response_url: + raise Exception("Неудалось загрузить изображение") + product_image = ProductImage( + product_id=product_id, + image_url=response_url, + ) + self.session.add(product_image) + await self.session.commit() + return ProductUploadImageResponse(ok=True, + message='Изображение успешно загружено', + image_url=response_url) + except Exception as e: + return ProductUploadImageResponse(ok=False, message=str(e)) + # region Barcodes async def add_barcode(self, request: ProductAddBarcodeRequest): try: diff --git a/test/test.py b/test/test.py index 39417d1..e6af690 100644 --- a/test/test.py +++ b/test/test.py @@ -5,6 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload from backend.session import session_maker +from external.s3_uploader.uploader import S3Uploader from models import Deal, DealProduct, Service import models @@ -12,34 +13,10 @@ import models.secondary async def main(session: AsyncSession): - deal_services_subquery = ( - select( - models.secondary.DealService.deal_id, - func.sum(models.secondary.DealService.quantity * Service.price).label('total_price') - ) - .join(Service) - .group_by(models.secondary.DealService.deal_id) - ) - product_services_subquery = select( - select( - models.secondary.DealProductService.deal_id, - func.sum(models.DealProduct.quantity * models.secondary.DealProductService.price).label('total_price') - ) - .join(models.secondary.DealProduct) - .group_by(models.secondary.DealProductService.deal_id) - .subquery() - ) - union_subqueries = deal_services_subquery.union(product_services_subquery).subquery() - final_subquery = ( - select( - union_subqueries.c.deal_id, - func.sum(union_subqueries.c.total_price).label('total_sum') - ) - .group_by(union_subqueries.c.deal_id) - .subquery() - ) - - print(final_subquery) + file_bytes = open('photo_2024-04-01 10.26.39.jpeg', 'rb').read() + uploader = S3Uploader('1cc46590-4532-4046-97aa-baf3e49f20ad-AUF') + response = await uploader.upload(file_bytes) + print(response) async def preload(): diff --git a/utils/services.xlsx b/utils/services.xlsx deleted file mode 100644 index 8553cc7..0000000 Binary files a/utils/services.xlsx and /dev/null differ