From 4ae03284a3c8c0320f65f0085b7befd00f60da68 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 1 Sep 2024 01:05:11 +0300 Subject: [PATCH] feat: a lot of a lot --- backend/config.py | 5 ++ background/__init__.py | 0 background/celery_app.py | 14 ++++ background/tasks/__init__.py | 1 + background/tasks/marketplace.py | 20 +++++ barcodes/generator/default_generator.py | 2 +- decorators/__init__.py | 0 decorators/async_utils.py | 19 +++++ external/marketplace/__init__.py | 4 + external/marketplace/base/__init__.py | 1 + external/marketplace/base/core.py | 28 +++++++ external/marketplace/factory.py | 19 +++++ external/marketplace/ozon/__init__.py | 1 + external/marketplace/ozon/core.py | 15 ++++ external/marketplace/wildberries/__init__.py | 1 + external/marketplace/wildberries/core.py | 86 ++++++++++++++++++++ external/marketplace/wildberries/schemas.py | 0 external/marketplace/yandex/__init__.py | 1 + external/marketplace/yandex/core.py | 15 ++++ main.py | 3 +- marketplaces/__init__.py | 4 + marketplaces/base/__init__.py | 1 + marketplaces/base/core.py | 25 ++++++ marketplaces/factory.py | 28 +++++++ marketplaces/ozon/__init__.py | 1 + marketplaces/ozon/core.py | 6 ++ marketplaces/wildberries/__init__.py | 1 + marketplaces/wildberries/core.py | 76 +++++++++++++++++ marketplaces/yandex/__init__.py | 1 + marketplaces/yandex/core.py | 6 ++ models/__init__.py | 1 + models/marketplace.py | 21 ++++- models/marketplace_products.py | 39 +++++++++ models/product.py | 13 ++- requirements.txt | 3 +- routers/__init__.py | 1 + routers/auth.py | 10 ++- routers/client.py | 4 +- routers/marketplace.py | 48 +++++++++++ routers/task.py | 37 +++++++++ schemas/marketplace.py | 52 +++++++++++- schemas/task.py | 14 ++++ services/marketplace.py | 86 +++++++++++++++++++- 43 files changed, 700 insertions(+), 13 deletions(-) create mode 100644 background/__init__.py create mode 100644 background/celery_app.py create mode 100644 background/tasks/__init__.py create mode 100644 background/tasks/marketplace.py create mode 100644 decorators/__init__.py create mode 100644 decorators/async_utils.py create mode 100644 external/marketplace/__init__.py create mode 100644 external/marketplace/base/__init__.py create mode 100644 external/marketplace/base/core.py create mode 100644 external/marketplace/factory.py create mode 100644 external/marketplace/ozon/__init__.py create mode 100644 external/marketplace/ozon/core.py create mode 100644 external/marketplace/wildberries/__init__.py create mode 100644 external/marketplace/wildberries/core.py create mode 100644 external/marketplace/wildberries/schemas.py create mode 100644 external/marketplace/yandex/__init__.py create mode 100644 external/marketplace/yandex/core.py create mode 100644 marketplaces/__init__.py create mode 100644 marketplaces/base/__init__.py create mode 100644 marketplaces/base/core.py create mode 100644 marketplaces/factory.py create mode 100644 marketplaces/ozon/__init__.py create mode 100644 marketplaces/ozon/core.py create mode 100644 marketplaces/wildberries/__init__.py create mode 100644 marketplaces/wildberries/core.py create mode 100644 marketplaces/yandex/__init__.py create mode 100644 marketplaces/yandex/core.py create mode 100644 models/marketplace_products.py create mode 100644 routers/task.py create mode 100644 schemas/task.py diff --git a/backend/config.py b/backend/config.py index 6b8051a..e3fd5e2 100644 --- a/backend/config.py +++ b/backend/config.py @@ -18,3 +18,8 @@ SECRET_KEY = os.environ.get('SECRET_KEY') S3_API_KEY = os.environ.get('S3_API_KEY') BILLING_API_KEY = os.environ.get('BILLING_API_KEY') + +# Celery +CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL') +CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND') + diff --git a/background/__init__.py b/background/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/background/celery_app.py b/background/celery_app.py new file mode 100644 index 0000000..dc25262 --- /dev/null +++ b/background/celery_app.py @@ -0,0 +1,14 @@ +from celery import Celery + +from backend.config import CELERY_BROKER_URL, CELERY_RESULT_BACKEND + +celery = Celery( + __name__, + broker=CELERY_BROKER_URL, + backend=CELERY_RESULT_BACKEND +) +celery.conf.result_backend_transport_options = { + 'global_keyprefix': 'crm_' +} + +from .tasks import * diff --git a/background/tasks/__init__.py b/background/tasks/__init__.py new file mode 100644 index 0000000..8170180 --- /dev/null +++ b/background/tasks/__init__.py @@ -0,0 +1 @@ +from .marketplace import synchronize_marketplace diff --git a/background/tasks/marketplace.py b/background/tasks/marketplace.py new file mode 100644 index 0000000..b4985dc --- /dev/null +++ b/background/tasks/marketplace.py @@ -0,0 +1,20 @@ +import time +from random import randint + +from background.celery_app import celery + + +@celery.task(name='synchronize_marketplace') +def synchronize_marketplace(marketplace_id: int): + time.sleep(10) + if randint(0,10) % 2 == 0: + return 1 + else: + raise Exception('Some error') + # async with session_maker() as session: + # session: AsyncSession + # marketplace: Optional[Marketplace] = await session.get(Marketplace, marketplace_id) + # if not marketplace: + # return + # controller = MarketplaceControllerFactory.get_controller(session, marketplace) + # await controller.synchronize_products() diff --git a/barcodes/generator/default_generator.py b/barcodes/generator/default_generator.py index d9d091a..d07b8d2 100644 --- a/barcodes/generator/default_generator.py +++ b/barcodes/generator/default_generator.py @@ -17,7 +17,7 @@ class DefaultBarcodeGenerator(BaseBarcodeGenerator): if not attribute_getter: continue value = attribute_getter.get_value(product) - if not value: + if not value or not value.strip(): continue attributes[attribute.name] = value for additional_attribute in template.additional_attributes: diff --git a/decorators/__init__.py b/decorators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/decorators/async_utils.py b/decorators/async_utils.py new file mode 100644 index 0000000..26b0304 --- /dev/null +++ b/decorators/async_utils.py @@ -0,0 +1,19 @@ +import asyncio +from functools import wraps + + +def async_to_sync(func): + @wraps(func) + def wrapper(*args, **kwargs): + # Get the current event loop + loop = asyncio.get_event_loop() + + # If there is no current event loop, create a new one + if loop.is_closed(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # Run the async function until complete and return the result + return loop.run_until_complete(func(*args, **kwargs)) + + return wrapper diff --git a/external/marketplace/__init__.py b/external/marketplace/__init__.py new file mode 100644 index 0000000..845a4db --- /dev/null +++ b/external/marketplace/__init__.py @@ -0,0 +1,4 @@ +from .wildberries import WildberriesMarketplaceApi +from .ozon import OzonMarketplaceApi +from .yandex import YandexMarketplaceApi +from .factory import MarketplaceApiFactory diff --git a/external/marketplace/base/__init__.py b/external/marketplace/base/__init__.py new file mode 100644 index 0000000..1710e9d --- /dev/null +++ b/external/marketplace/base/__init__.py @@ -0,0 +1 @@ +from .core import BaseMarketplaceApi diff --git a/external/marketplace/base/core.py b/external/marketplace/base/core.py new file mode 100644 index 0000000..c89ac6c --- /dev/null +++ b/external/marketplace/base/core.py @@ -0,0 +1,28 @@ +from abc import ABC, abstractmethod + +import aiohttp + +from models import Marketplace + + +class BaseMarketplaceApi(ABC): + marketplace: Marketplace + + @abstractmethod + def __init__(self, marketplace: Marketplace): + pass + + async def _method(self, http_method, method, **kwargs): + async with aiohttp.ClientSession(headers=self.get_headers) as session: + async with session.request(http_method, self.base_url + method, **kwargs) as response: + return await response.json() + + @property + @abstractmethod + def get_headers(self) -> dict: + pass + + @property + @abstractmethod + def base_url(self) -> str: + pass diff --git a/external/marketplace/factory.py b/external/marketplace/factory.py new file mode 100644 index 0000000..e5a400c --- /dev/null +++ b/external/marketplace/factory.py @@ -0,0 +1,19 @@ +from enums.base_marketplace import BaseMarketplace +from external.marketplace.ozon.core import OzonMarketplaceApi +from external.marketplace.wildberries.core import WildberriesApiUrl, WildberriesMarketplaceApi +from external.marketplace.yandex.core import YandexMarketplaceApi +from models import Marketplace + + +class MarketplaceApiFactory: + @staticmethod + def get_marketplace_api(marketplace: Marketplace): + match marketplace.base_marketplace_key: + case BaseMarketplace.WILDBERRIES: + return WildberriesMarketplaceApi(marketplace) + case BaseMarketplace.OZON: + return OzonMarketplaceApi(marketplace) + case BaseMarketplace.YANDEX_MARKET: + return YandexMarketplaceApi(marketplace) + case _: + raise ValueError(f"Unsupported marketplace: {marketplace.base_marketplace_key}") diff --git a/external/marketplace/ozon/__init__.py b/external/marketplace/ozon/__init__.py new file mode 100644 index 0000000..edfd54f --- /dev/null +++ b/external/marketplace/ozon/__init__.py @@ -0,0 +1 @@ +from .core import OzonMarketplaceApi diff --git a/external/marketplace/ozon/core.py b/external/marketplace/ozon/core.py new file mode 100644 index 0000000..f51d011 --- /dev/null +++ b/external/marketplace/ozon/core.py @@ -0,0 +1,15 @@ +from external.marketplace.base.core import BaseMarketplaceApi +from models import Marketplace + + +class OzonMarketplaceApi(BaseMarketplaceApi): + def __init__(self, marketplace: Marketplace): + pass + + @property + def get_headers(self) -> dict: + return {} + + @property + def base_url(self) -> str: + return "" diff --git a/external/marketplace/wildberries/__init__.py b/external/marketplace/wildberries/__init__.py new file mode 100644 index 0000000..490d812 --- /dev/null +++ b/external/marketplace/wildberries/__init__.py @@ -0,0 +1 @@ +from .core import WildberriesMarketplaceApi diff --git a/external/marketplace/wildberries/core.py b/external/marketplace/wildberries/core.py new file mode 100644 index 0000000..6ff5058 --- /dev/null +++ b/external/marketplace/wildberries/core.py @@ -0,0 +1,86 @@ +import time +from enum import StrEnum +from typing import AsyncIterator + +from async_timeout import timeout + +from external.marketplace.base.core import BaseMarketplaceApi +from models import Marketplace + + +class WildberriesApiUrl(StrEnum): + CONTENT = 'https://content-api.wildberries.ru' + DISCOUNTS_PRICES = 'https://discounts-prices-api.wildberries.ru' + SUPPLIES = 'https://supplies-api.wildberries.ru' + MARKETPLACE = 'https://marketplace-api.wildberries.ru' + STATISTICS = 'https://statistics-api.wildberries.ru' + SELLER_ANALYTICS = 'https://seller-analytics-api.wildberries.ru' + ADVERT = 'https://advert-api.wildberries.ru' + RECOMMEND = 'https://recommend-api.wildberries.ru' + FEEDBACKS = 'https://feedbacks-api.wildberries.ru' + COMMON = 'https://common-api.wildberries.ru' + BUYER_CHAT = 'https://buyer-chat-api.wildberries.ru' + RETURNS = 'https://returns-api.wildberries.ru' + DOCUMENTS = 'https://documents-api.wildberries.ru' + + +class WildberriesMarketplaceApi(BaseMarketplaceApi): + + def __init__(self, marketplace: Marketplace): + token = marketplace.auth_data.get('Authorization') + if not token: + raise ValueError( + f"Authorization token is missing for Marketplace ID: {marketplace.id}. " + "Please check the marketplace credentials." + ) + self.token = token + self.headers = {'Authorization': token} + self.marketplace = marketplace + + @property + def get_headers(self) -> dict: + return self.headers + + @property + def base_url(self) -> str: + return "" + + async def get_products(self, data: dict) -> dict: + method = WildberriesApiUrl.CONTENT + '/content/v2/get/cards/list' + response = await self._method('POST', method, json=data) + return response + + async def get_all_products(self) -> AsyncIterator[dict]: + limit = 100 + updated_at = None + nm_id = None + while True: + data = { + 'settings': { + 'cursor': { + 'limit': limit, + 'updatedAt': updated_at, + 'nmID': nm_id + }, + 'filter': { + 'withPhoto': -1 + } + } + } + start = time.time() + response = await self.get_products(data) + print(f'Request elapsed: {round(time.time() - start, 2)}') + if not response: + break + cards = response.get('cards') + if not cards: + break + for card in cards: + yield card + + cursor = response.get('cursor') + total = cursor.get('total') + if total < limit: + break + updated_at = cursor.get('updatedAt') + nm_id = cursor.get('nmID') diff --git a/external/marketplace/wildberries/schemas.py b/external/marketplace/wildberries/schemas.py new file mode 100644 index 0000000..e69de29 diff --git a/external/marketplace/yandex/__init__.py b/external/marketplace/yandex/__init__.py new file mode 100644 index 0000000..0457905 --- /dev/null +++ b/external/marketplace/yandex/__init__.py @@ -0,0 +1 @@ +from .core import YandexMarketplaceApi diff --git a/external/marketplace/yandex/core.py b/external/marketplace/yandex/core.py new file mode 100644 index 0000000..fc839fc --- /dev/null +++ b/external/marketplace/yandex/core.py @@ -0,0 +1,15 @@ +from external.marketplace.base.core import BaseMarketplaceApi +from models import Marketplace + + +class YandexMarketplaceApi(BaseMarketplaceApi): + def __init__(self, marketplace: Marketplace): + pass + + @property + def get_headers(self) -> dict: + return {} + + @property + def base_url(self) -> str: + return "" diff --git a/main.py b/main.py index 12b3bcb..5c9395b 100644 --- a/main.py +++ b/main.py @@ -42,7 +42,8 @@ routers_list = [ routers.marketplace_router, routers.payroll_router, routers.time_tracking_router, - routers.billing_router + routers.billing_router, + routers.task_router, ] for router in routers_list: app.include_router(router) diff --git a/marketplaces/__init__.py b/marketplaces/__init__.py new file mode 100644 index 0000000..8c15df4 --- /dev/null +++ b/marketplaces/__init__.py @@ -0,0 +1,4 @@ +from .wildberries import WildberriesController +from .ozon import OzonController +from .yandex import YandexController +from .factory import MarketplaceControllerFactory diff --git a/marketplaces/base/__init__.py b/marketplaces/base/__init__.py new file mode 100644 index 0000000..09246b6 --- /dev/null +++ b/marketplaces/base/__init__.py @@ -0,0 +1 @@ +from .core import BaseMarketplaceController diff --git a/marketplaces/base/core.py b/marketplaces/base/core.py new file mode 100644 index 0000000..f50f8e1 --- /dev/null +++ b/marketplaces/base/core.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod +from typing import Union + +from sqlalchemy.ext.asyncio import AsyncSession + +from external.marketplace.factory import MarketplaceApiFactory +from external.marketplace.ozon.core import OzonMarketplaceApi +from external.marketplace.wildberries.core import WildberriesMarketplaceApi +from external.marketplace.yandex.core import YandexMarketplaceApi +from models import Marketplace + + +class BaseMarketplaceController(ABC): + api: Union[WildberriesMarketplaceApi, OzonMarketplaceApi, YandexMarketplaceApi] + marketplace: Marketplace + session: AsyncSession + + def __init__(self, session: AsyncSession, marketplace: Marketplace): + self.api = MarketplaceApiFactory.get_marketplace_api(marketplace) + self.marketplace = marketplace + self.session = session + + @abstractmethod + async def synchronize_products(self): + pass diff --git a/marketplaces/factory.py b/marketplaces/factory.py new file mode 100644 index 0000000..270dc28 --- /dev/null +++ b/marketplaces/factory.py @@ -0,0 +1,28 @@ +from audioop import ratecv +from typing import Union + +from sqlalchemy.ext.asyncio import AsyncSession + +from enums.base_marketplace import BaseMarketplace +from marketplaces.ozon.core import OzonController +from marketplaces.wildberries.core import WildberriesController +from marketplaces.yandex.core import YandexController +from models import Marketplace + + +class MarketplaceControllerFactory: + @staticmethod + def get_controller(session: AsyncSession, marketplace: Marketplace) -> Union[ + WildberriesController, + OzonController, + YandexController + ]: + match marketplace.base_marketplace_key: + case BaseMarketplace.WILDBERRIES: + return WildberriesController(session, marketplace) + case BaseMarketplace.OZON: + return OzonController(session, marketplace) + case BaseMarketplace.YANDEX_MARKET: + return YandexController(session, marketplace) + case _: + raise ValueError(f"Unsupported marketplace: {marketplace.base_marketplace_key}") diff --git a/marketplaces/ozon/__init__.py b/marketplaces/ozon/__init__.py new file mode 100644 index 0000000..fa4755c --- /dev/null +++ b/marketplaces/ozon/__init__.py @@ -0,0 +1 @@ +from .core import OzonController \ No newline at end of file diff --git a/marketplaces/ozon/core.py b/marketplaces/ozon/core.py new file mode 100644 index 0000000..d1c370f --- /dev/null +++ b/marketplaces/ozon/core.py @@ -0,0 +1,6 @@ +from marketplaces.base.core import BaseMarketplaceController + + +class OzonController(BaseMarketplaceController): + async def synchronize_products(self): + pass diff --git a/marketplaces/wildberries/__init__.py b/marketplaces/wildberries/__init__.py new file mode 100644 index 0000000..ab63dfc --- /dev/null +++ b/marketplaces/wildberries/__init__.py @@ -0,0 +1 @@ +from .core import WildberriesController diff --git a/marketplaces/wildberries/core.py b/marketplaces/wildberries/core.py new file mode 100644 index 0000000..d419e04 --- /dev/null +++ b/marketplaces/wildberries/core.py @@ -0,0 +1,76 @@ +import time + +from sqlalchemy import select + +from external.marketplace.wildberries.core import WildberriesMarketplaceApi +from marketplaces.base.core import BaseMarketplaceController +from models import Product, ProductBarcode, ProductImage, WildberriesProduct + + +class WildberriesController(BaseMarketplaceController): + api: WildberriesMarketplaceApi + + async def synchronize_products(self): + products = [] + barcodes = [] + images = [] + wildberries_products = [] + + marketplace_id: int = self.marketplace.id + synchronized_nm_uuids = set( + ( + await self.session.scalars( + select( + WildberriesProduct.nm_uuid + ) + .where( + WildberriesProduct.marketplace_id == marketplace_id + ) + ) + ).all() + ) + + async for card in self.api.get_all_products(): + nm_uuid = card['nmUUID'] + if nm_uuid in synchronized_nm_uuids: + continue + sizes: list[dict] = card.get('sizes') or [] + for size in sizes: + tech_size = size.get('techSize') + wb_size = size.get('wbSize') + size_value = tech_size or wb_size + product = Product( + client_id=self.marketplace.client_id, + name=card['title'], + article=card['vendorCode'], + size=size_value + ) + skus = size.get('skus') or [] + for sku in skus: + barcode = ProductBarcode( + product=product, + barcode=sku + ) + barcodes.append(barcode) + photos = card.get('photos') or [] + for photo in photos: + image = ProductImage( + product=product, + image_url=photo['big'] + ) + images.append(image) + break + wildberries_product = WildberriesProduct( + marketplace_id=self.marketplace.id, + product=product, + nm_uuid=nm_uuid + ) + wildberries_products.append( + wildberries_product + ) + products.append(product) + instances = products + wildberries_products + barcodes + images + start = time.time() + self.session.add_all(instances) + await self.session.commit() + print(f'Add and commit elapsed: {time.time() - start}') diff --git a/marketplaces/yandex/__init__.py b/marketplaces/yandex/__init__.py new file mode 100644 index 0000000..1216b8f --- /dev/null +++ b/marketplaces/yandex/__init__.py @@ -0,0 +1 @@ +from .core import YandexController \ No newline at end of file diff --git a/marketplaces/yandex/core.py b/marketplaces/yandex/core.py new file mode 100644 index 0000000..057b1e7 --- /dev/null +++ b/marketplaces/yandex/core.py @@ -0,0 +1,6 @@ +from marketplaces.base.core import BaseMarketplaceController + + +class YandexController(BaseMarketplaceController): + async def synchronize_products(self): + pass diff --git a/models/__init__.py b/models/__init__.py index 0a59eeb..7e80116 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -11,5 +11,6 @@ from .shipping_warehouse import * from .marketplace import * from .payroll import * from .billing import * +from .marketplace_products import * configure_mappers() diff --git a/models/marketplace.py b/models/marketplace.py index e5d6329..62c2de8 100644 --- a/models/marketplace.py +++ b/models/marketplace.py @@ -1,7 +1,12 @@ -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import ForeignKey, JSON +from sqlalchemy.orm import Mapped, mapped_column, relationship +from typing_extensions import TYPE_CHECKING from models import BaseModel +if TYPE_CHECKING: + from models import Client + class BaseMarketplace(BaseModel): __tablename__ = 'base_marketplaces' @@ -9,3 +14,17 @@ class BaseMarketplace(BaseModel): name: Mapped[str] = mapped_column() icon_url: Mapped[str] = mapped_column() + + +class Marketplace(BaseModel): + __tablename__ = 'marketplaces' + id: Mapped[int] = mapped_column(primary_key=True) + + base_marketplace_key: Mapped[str] = mapped_column(ForeignKey("base_marketplaces.key"), nullable=False) + base_marketplace: Mapped["BaseMarketplace"] = relationship(lazy="joined") + + client_id: Mapped[int] = mapped_column(ForeignKey('clients.id'), nullable=False, comment='ID клиента') + client: Mapped["Client"] = relationship('Client') + + name: Mapped[str] = mapped_column(nullable=False) + auth_data: Mapped[dict] = mapped_column(type_=JSON) diff --git a/models/marketplace_products.py b/models/marketplace_products.py new file mode 100644 index 0000000..ec2c5a3 --- /dev/null +++ b/models/marketplace_products.py @@ -0,0 +1,39 @@ +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, relationship, mapped_column + +from models import BaseModel +from models import Marketplace +from models import Product + + +class WildberriesProduct(BaseModel): + __tablename__ = 'wildberries_products' + + marketplace_id: Mapped[int] = mapped_column(ForeignKey('marketplaces.id'), primary_key=True) + marketplace: Mapped["Marketplace"] = relationship() + + product_id: Mapped[int] = mapped_column(ForeignKey('products.id'), primary_key=True) + product: Mapped["Product"] = relationship() + + nm_uuid: Mapped[str] = mapped_column(nullable=False) + + +class OzonProduct(BaseModel): + __tablename__ = 'ozon_products' + + marketplace_id: Mapped[int] = mapped_column(ForeignKey('marketplaces.id'), primary_key=True) + marketplace: Mapped["Marketplace"] = relationship() + + product_id: Mapped[int] = mapped_column(ForeignKey('products.id'), primary_key=True) + product: Mapped["Product"] = relationship() + + + +class YandexProduct(BaseModel): + __tablename__ = 'yandex_products' + + marketplace_id: Mapped[int] = mapped_column(ForeignKey('marketplaces.id'), primary_key=True) + marketplace: Mapped["Marketplace"] = relationship() + + product_id: Mapped[int] = mapped_column(ForeignKey('products.id'), primary_key=True) + product: Mapped["Product"] = relationship() diff --git a/models/product.py b/models/product.py index 08b044d..a61a140 100644 --- a/models/product.py +++ b/models/product.py @@ -1,8 +1,14 @@ +from typing import TYPE_CHECKING + from sqlalchemy import Column, Integer, String, ForeignKey, Sequence -from sqlalchemy.orm import relationship +from sqlalchemy.orm import relationship, Mapped +from sqlalchemy.testing.schema import mapped_column from models import BaseModel +if TYPE_CHECKING: + from models import Marketplace + class Product(BaseModel): __tablename__ = 'products' @@ -30,11 +36,12 @@ class Product(BaseModel): 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') + product: Mapped["Product"] = relationship(back_populates='images') image_url = Column(String, nullable=False) @@ -42,6 +49,6 @@ class ProductImage(BaseModel): class ProductBarcode(BaseModel): __tablename__ = 'product_barcodes' product_id = Column(Integer, ForeignKey('products.id'), nullable=False, comment='ID товара', primary_key=True) - product = relationship('Product', back_populates='barcodes') + product: Mapped["Product"] = relationship(back_populates='barcodes') barcode = Column(String, nullable=False, index=True, comment='ШК товара', primary_key=True) diff --git a/requirements.txt b/requirements.txt index 5154b29..81abdf7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,7 @@ aiohttp aiohttp[speedups] openpyxl lexorank-py - +celery[redis] +celery # PDF reportlab \ No newline at end of file diff --git a/routers/__init__.py b/routers/__init__.py index f8bbea3..000a693 100644 --- a/routers/__init__.py +++ b/routers/__init__.py @@ -12,3 +12,4 @@ from .marketplace import marketplace_router from .payroll import payroll_router from .time_tracking import time_tracking_router from .billing import billing_router +from .task import task_router diff --git a/routers/auth.py b/routers/auth.py index 1f99985..d7af2dd 100644 --- a/routers/auth.py +++ b/routers/auth.py @@ -1,6 +1,6 @@ from typing import Annotated -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Request from sqlalchemy.ext.asyncio import AsyncSession from backend.session import get_session @@ -16,3 +16,11 @@ auth_router = APIRouter( @auth_router.post('/login', response_model=AuthLoginResponse) async def login(request: AuthLoginRequest, session: Annotated[AsyncSession, Depends(get_session)]): return await AuthService(session).authenticate(request) + + +@auth_router.post('/test') +async def test( + request: Request +): + print(request.headers) + return {'a': "a"} diff --git a/routers/client.py b/routers/client.py index 0f93140..924f935 100644 --- a/routers/client.py +++ b/routers/client.py @@ -16,6 +16,8 @@ client_router = APIRouter( ) + + @client_router.get('/search', operation_id='search_clients') async def search_clients( name: str, @@ -52,7 +54,7 @@ async def get_all_clients( @client_router.post( '/create', - operation_id='create_client', + operation_id='create_client_api', response_model=ClientCreateResponse ) async def create_client( diff --git a/routers/marketplace.py b/routers/marketplace.py index 66c5a81..bfe5f05 100644 --- a/routers/marketplace.py +++ b/routers/marketplace.py @@ -21,3 +21,51 @@ async def get_all( session: SessionDependency ): return await MarketplaceService(session).get_all_base_marketplaces() + + +@marketplace_router.post( + '/get', + operation_id='get_client_marketplaces', + response_model=GetClientMarketplacesResponse +) +async def get( + session: SessionDependency, + request: GetClientMarketplacesRequest +): + return await MarketplaceService(session).get_client_marketplaces(request) + + +@marketplace_router.post( + '/create', + operation_id='create_marketplace', + response_model=CreateMarketplaceResponse +) +async def create( + session: SessionDependency, + request: CreateMarketplaceRequest +): + return await MarketplaceService(session).create_marketplace(request) + + +@marketplace_router.post( + '/delete', + operation_id='delete_marketplace', + response_model=DeleteMarketplaceResponse +) +async def delete( + session: SessionDependency, + request: DeleteMarketplaceRequest +): + return await MarketplaceService(session).delete_marketplace(request) + + +@marketplace_router.post( + '/update', + operation_id='update_marketplace', + response_model=UpdateMarketplaceResponse +) +async def update( + session: SessionDependency, + request: UpdateMarketplaceRequest +): + return await MarketplaceService(session).update_marketplace(request) diff --git a/routers/task.py b/routers/task.py new file mode 100644 index 0000000..03258ef --- /dev/null +++ b/routers/task.py @@ -0,0 +1,37 @@ +from celery.result import AsyncResult +from fastapi import APIRouter + +import background.tasks.marketplace +from background.celery_app import celery +from schemas.task import * + +task_router = APIRouter( + prefix='/task', + tags=["task"], +) + + +@task_router.post( + '/synchronize-marketplace', + operation_id='create_synchronize_marketplace_task', + response_model=CreateTaskResponse +) +async def synchronize_marketplace( + request: SynchronizeMarketplaceRequest +): + marketplace_id = request.marketplace_id + task: AsyncResult = background.tasks.marketplace.synchronize_marketplace.delay(marketplace_id) + return CreateTaskResponse(task_id=task.id) + + +@task_router.get( + '/info/{task_id}', + operation_id='get_task_info', + response_model=TaskInfoResponse +) +def task_info(task_id: str): + task = AsyncResult(task_id, app=celery) + return TaskInfoResponse( + task_id=task_id, + status=task.status + ) diff --git a/schemas/marketplace.py b/schemas/marketplace.py index 64b3ad7..3ef5ead 100644 --- a/schemas/marketplace.py +++ b/schemas/marketplace.py @@ -1,6 +1,7 @@ from typing import List -from schemas.base import BaseSchema +from schemas.base import BaseSchema, OkMessageSchema +from schemas.client import ClientSchema # region Entities @@ -10,13 +11,62 @@ class BaseMarketplaceSchema(BaseSchema): icon_url: str +class MarketplaceMixin(BaseSchema): + name: str + base_marketplace: BaseMarketplaceSchema + client: ClientSchema + auth_data: dict + + +class MarketplaceCreateSchema(BaseSchema): + name: str + client_id: int + base_marketplace_key: str + auth_data: dict + + +class MarketplaceSchema(MarketplaceMixin): + id: int + + # endregion # region Requests +class GetClientMarketplacesRequest(BaseSchema): + client_id: int + + +class CreateMarketplaceRequest(BaseSchema): + marketplace: MarketplaceCreateSchema + + +class DeleteMarketplaceRequest(BaseSchema): + marketplace_id: int + + +class UpdateMarketplaceRequest(BaseSchema): + marketplace: MarketplaceSchema + + # endregion # region Responses class GetAllBaseMarketplacesResponse(BaseSchema): base_marketplaces: List[BaseMarketplaceSchema] + +class GetClientMarketplacesResponse(BaseSchema): + marketplaces: List[MarketplaceSchema] + + +class CreateMarketplaceResponse(OkMessageSchema): + pass + + +class DeleteMarketplaceResponse(OkMessageSchema): + pass + +class UpdateMarketplaceResponse(OkMessageSchema): + pass + # endregion diff --git a/schemas/task.py b/schemas/task.py new file mode 100644 index 0000000..7a6b3c5 --- /dev/null +++ b/schemas/task.py @@ -0,0 +1,14 @@ +from schemas.base import BaseSchema + + +class CreateTaskResponse(BaseSchema): + task_id: str + + +class TaskInfoResponse(BaseSchema): + task_id: str + status: str + + +class SynchronizeMarketplaceRequest(BaseSchema): + marketplace_id: int diff --git a/services/marketplace.py b/services/marketplace.py index ebdca40..c3d15ca 100644 --- a/services/marketplace.py +++ b/services/marketplace.py @@ -1,7 +1,9 @@ -from sqlalchemy import select +from pyexpat.errors import messages +from sqlalchemy import select, insert, delete, update +from sqlalchemy.orm import joinedload -from models import BaseMarketplace -from schemas.marketplace import GetAllBaseMarketplacesResponse +from models import BaseMarketplace, Marketplace +from schemas.marketplace import * from services.base import BaseService @@ -12,3 +14,81 @@ class MarketplaceService(BaseService): return GetAllBaseMarketplacesResponse( base_marketplaces=base_marketplaces ) + + async def get_client_marketplaces(self, request: GetClientMarketplacesRequest) -> GetClientMarketplacesResponse: + stmt = ( + select( + Marketplace + ) + .options( + joinedload(Marketplace.base_marketplace), + joinedload(Marketplace.client) + ) + .where( + Marketplace.client_id == request.client_id + ) + ) + marketplaces = (await self.session.scalars(stmt)).all() + return GetClientMarketplacesResponse( + marketplaces=marketplaces + ) + + async def create_marketplace(self, request: CreateMarketplaceRequest) -> CreateMarketplaceResponse: + try: + marketplace = request.marketplace + marketplace_dict = marketplace.dict() + stmt = ( + insert(Marketplace) + .values( + **marketplace_dict + ) + ) + await self.session.execute(stmt) + await self.session.commit() + return CreateMarketplaceResponse(ok=True, message='Маркетплейс успешно создан') + except Exception as e: + await self.session.rollback() + return CreateMarketplaceResponse(ok=False, message=str(e)) + + async def delete_marketplace(self, request: DeleteMarketplaceRequest) -> DeleteMarketplaceResponse: + try: + stmt = ( + delete(Marketplace) + .where( + Marketplace.id == request.marketplace_id + ) + ) + await self.session.execute(stmt) + await self.session.commit() + return DeleteMarketplaceResponse(ok=True, message='Маркетплейс успешно удален') + except Exception as e: + await self.session.rollback() + return DeleteMarketplaceResponse(ok=False, message=str(e)) + + async def update_marketplace(self, request: UpdateMarketplaceRequest) -> UpdateMarketplaceResponse: + try: + marketplace = request.marketplace + marketplace_dict = marketplace.dict() + del marketplace_dict['id'] + del marketplace_dict['base_marketplace'] + del marketplace_dict['client'] + marketplace_dict['base_marketplace_key'] = marketplace.base_marketplace.key + marketplace_dict['client_id'] = marketplace.client.id + + stmt = ( + update( + Marketplace + ) + .values( + **marketplace_dict + ) + .where( + Marketplace.id == marketplace.id + ) + ) + await self.session.execute(stmt) + await self.session.commit() + return UpdateMarketplaceResponse(ok=True, message='Маркетплейс успешно обновлен') + except Exception as e: + await self.session.rollback() + return UpdateMarketplaceResponse(ok=False, message=str(e))