feat: a lot of a lot
This commit is contained in:
@@ -18,3 +18,8 @@ SECRET_KEY = os.environ.get('SECRET_KEY')
|
|||||||
S3_API_KEY = os.environ.get('S3_API_KEY')
|
S3_API_KEY = os.environ.get('S3_API_KEY')
|
||||||
|
|
||||||
BILLING_API_KEY = os.environ.get('BILLING_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')
|
||||||
|
|
||||||
|
|||||||
0
background/__init__.py
Normal file
0
background/__init__.py
Normal file
14
background/celery_app.py
Normal file
14
background/celery_app.py
Normal file
@@ -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 *
|
||||||
1
background/tasks/__init__.py
Normal file
1
background/tasks/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .marketplace import synchronize_marketplace
|
||||||
20
background/tasks/marketplace.py
Normal file
20
background/tasks/marketplace.py
Normal file
@@ -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()
|
||||||
@@ -17,7 +17,7 @@ class DefaultBarcodeGenerator(BaseBarcodeGenerator):
|
|||||||
if not attribute_getter:
|
if not attribute_getter:
|
||||||
continue
|
continue
|
||||||
value = attribute_getter.get_value(product)
|
value = attribute_getter.get_value(product)
|
||||||
if not value:
|
if not value or not value.strip():
|
||||||
continue
|
continue
|
||||||
attributes[attribute.name] = value
|
attributes[attribute.name] = value
|
||||||
for additional_attribute in template.additional_attributes:
|
for additional_attribute in template.additional_attributes:
|
||||||
|
|||||||
0
decorators/__init__.py
Normal file
0
decorators/__init__.py
Normal file
19
decorators/async_utils.py
Normal file
19
decorators/async_utils.py
Normal file
@@ -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
|
||||||
4
external/marketplace/__init__.py
vendored
Normal file
4
external/marketplace/__init__.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .wildberries import WildberriesMarketplaceApi
|
||||||
|
from .ozon import OzonMarketplaceApi
|
||||||
|
from .yandex import YandexMarketplaceApi
|
||||||
|
from .factory import MarketplaceApiFactory
|
||||||
1
external/marketplace/base/__init__.py
vendored
Normal file
1
external/marketplace/base/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .core import BaseMarketplaceApi
|
||||||
28
external/marketplace/base/core.py
vendored
Normal file
28
external/marketplace/base/core.py
vendored
Normal file
@@ -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
|
||||||
19
external/marketplace/factory.py
vendored
Normal file
19
external/marketplace/factory.py
vendored
Normal file
@@ -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}")
|
||||||
1
external/marketplace/ozon/__init__.py
vendored
Normal file
1
external/marketplace/ozon/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .core import OzonMarketplaceApi
|
||||||
15
external/marketplace/ozon/core.py
vendored
Normal file
15
external/marketplace/ozon/core.py
vendored
Normal file
@@ -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 ""
|
||||||
1
external/marketplace/wildberries/__init__.py
vendored
Normal file
1
external/marketplace/wildberries/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .core import WildberriesMarketplaceApi
|
||||||
86
external/marketplace/wildberries/core.py
vendored
Normal file
86
external/marketplace/wildberries/core.py
vendored
Normal file
@@ -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')
|
||||||
0
external/marketplace/wildberries/schemas.py
vendored
Normal file
0
external/marketplace/wildberries/schemas.py
vendored
Normal file
1
external/marketplace/yandex/__init__.py
vendored
Normal file
1
external/marketplace/yandex/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .core import YandexMarketplaceApi
|
||||||
15
external/marketplace/yandex/core.py
vendored
Normal file
15
external/marketplace/yandex/core.py
vendored
Normal file
@@ -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 ""
|
||||||
3
main.py
3
main.py
@@ -42,7 +42,8 @@ routers_list = [
|
|||||||
routers.marketplace_router,
|
routers.marketplace_router,
|
||||||
routers.payroll_router,
|
routers.payroll_router,
|
||||||
routers.time_tracking_router,
|
routers.time_tracking_router,
|
||||||
routers.billing_router
|
routers.billing_router,
|
||||||
|
routers.task_router,
|
||||||
]
|
]
|
||||||
for router in routers_list:
|
for router in routers_list:
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
|
|||||||
4
marketplaces/__init__.py
Normal file
4
marketplaces/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .wildberries import WildberriesController
|
||||||
|
from .ozon import OzonController
|
||||||
|
from .yandex import YandexController
|
||||||
|
from .factory import MarketplaceControllerFactory
|
||||||
1
marketplaces/base/__init__.py
Normal file
1
marketplaces/base/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .core import BaseMarketplaceController
|
||||||
25
marketplaces/base/core.py
Normal file
25
marketplaces/base/core.py
Normal file
@@ -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
|
||||||
28
marketplaces/factory.py
Normal file
28
marketplaces/factory.py
Normal file
@@ -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}")
|
||||||
1
marketplaces/ozon/__init__.py
Normal file
1
marketplaces/ozon/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .core import OzonController
|
||||||
6
marketplaces/ozon/core.py
Normal file
6
marketplaces/ozon/core.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from marketplaces.base.core import BaseMarketplaceController
|
||||||
|
|
||||||
|
|
||||||
|
class OzonController(BaseMarketplaceController):
|
||||||
|
async def synchronize_products(self):
|
||||||
|
pass
|
||||||
1
marketplaces/wildberries/__init__.py
Normal file
1
marketplaces/wildberries/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .core import WildberriesController
|
||||||
76
marketplaces/wildberries/core.py
Normal file
76
marketplaces/wildberries/core.py
Normal file
@@ -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}')
|
||||||
1
marketplaces/yandex/__init__.py
Normal file
1
marketplaces/yandex/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .core import YandexController
|
||||||
6
marketplaces/yandex/core.py
Normal file
6
marketplaces/yandex/core.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from marketplaces.base.core import BaseMarketplaceController
|
||||||
|
|
||||||
|
|
||||||
|
class YandexController(BaseMarketplaceController):
|
||||||
|
async def synchronize_products(self):
|
||||||
|
pass
|
||||||
@@ -11,5 +11,6 @@ from .shipping_warehouse import *
|
|||||||
from .marketplace import *
|
from .marketplace import *
|
||||||
from .payroll import *
|
from .payroll import *
|
||||||
from .billing import *
|
from .billing import *
|
||||||
|
from .marketplace_products import *
|
||||||
|
|
||||||
configure_mappers()
|
configure_mappers()
|
||||||
|
|||||||
@@ -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
|
from models import BaseModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from models import Client
|
||||||
|
|
||||||
|
|
||||||
class BaseMarketplace(BaseModel):
|
class BaseMarketplace(BaseModel):
|
||||||
__tablename__ = 'base_marketplaces'
|
__tablename__ = 'base_marketplaces'
|
||||||
@@ -9,3 +14,17 @@ class BaseMarketplace(BaseModel):
|
|||||||
name: Mapped[str] = mapped_column()
|
name: Mapped[str] = mapped_column()
|
||||||
|
|
||||||
icon_url: 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)
|
||||||
|
|||||||
39
models/marketplace_products.py
Normal file
39
models/marketplace_products.py
Normal file
@@ -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()
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import Column, Integer, String, ForeignKey, Sequence
|
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
|
from models import BaseModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from models import Marketplace
|
||||||
|
|
||||||
|
|
||||||
class Product(BaseModel):
|
class Product(BaseModel):
|
||||||
__tablename__ = 'products'
|
__tablename__ = 'products'
|
||||||
@@ -30,11 +36,12 @@ class Product(BaseModel):
|
|||||||
cascade="all, delete-orphan")
|
cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ProductImage(BaseModel):
|
class ProductImage(BaseModel):
|
||||||
__tablename__ = 'product_images'
|
__tablename__ = 'product_images'
|
||||||
id = Column(Integer, autoincrement=True, primary_key=True, index=True)
|
id = Column(Integer, autoincrement=True, primary_key=True, index=True)
|
||||||
product_id = Column(Integer, ForeignKey('products.id'), nullable=False)
|
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)
|
image_url = Column(String, nullable=False)
|
||||||
|
|
||||||
@@ -42,6 +49,6 @@ class ProductImage(BaseModel):
|
|||||||
class ProductBarcode(BaseModel):
|
class ProductBarcode(BaseModel):
|
||||||
__tablename__ = 'product_barcodes'
|
__tablename__ = 'product_barcodes'
|
||||||
product_id = Column(Integer, ForeignKey('products.id'), nullable=False, comment='ID товара', primary_key=True)
|
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)
|
barcode = Column(String, nullable=False, index=True, comment='ШК товара', primary_key=True)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ aiohttp
|
|||||||
aiohttp[speedups]
|
aiohttp[speedups]
|
||||||
openpyxl
|
openpyxl
|
||||||
lexorank-py
|
lexorank-py
|
||||||
|
celery[redis]
|
||||||
|
celery
|
||||||
# PDF
|
# PDF
|
||||||
reportlab
|
reportlab
|
||||||
@@ -12,3 +12,4 @@ from .marketplace import marketplace_router
|
|||||||
from .payroll import payroll_router
|
from .payroll import payroll_router
|
||||||
from .time_tracking import time_tracking_router
|
from .time_tracking import time_tracking_router
|
||||||
from .billing import billing_router
|
from .billing import billing_router
|
||||||
|
from .task import task_router
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from backend.session import get_session
|
from backend.session import get_session
|
||||||
@@ -16,3 +16,11 @@ auth_router = APIRouter(
|
|||||||
@auth_router.post('/login', response_model=AuthLoginResponse)
|
@auth_router.post('/login', response_model=AuthLoginResponse)
|
||||||
async def login(request: AuthLoginRequest, session: Annotated[AsyncSession, Depends(get_session)]):
|
async def login(request: AuthLoginRequest, session: Annotated[AsyncSession, Depends(get_session)]):
|
||||||
return await AuthService(session).authenticate(request)
|
return await AuthService(session).authenticate(request)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_router.post('/test')
|
||||||
|
async def test(
|
||||||
|
request: Request
|
||||||
|
):
|
||||||
|
print(request.headers)
|
||||||
|
return {'a': "a"}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ client_router = APIRouter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@client_router.get('/search', operation_id='search_clients')
|
@client_router.get('/search', operation_id='search_clients')
|
||||||
async def search_clients(
|
async def search_clients(
|
||||||
name: str,
|
name: str,
|
||||||
@@ -52,7 +54,7 @@ async def get_all_clients(
|
|||||||
|
|
||||||
@client_router.post(
|
@client_router.post(
|
||||||
'/create',
|
'/create',
|
||||||
operation_id='create_client',
|
operation_id='create_client_api',
|
||||||
response_model=ClientCreateResponse
|
response_model=ClientCreateResponse
|
||||||
)
|
)
|
||||||
async def create_client(
|
async def create_client(
|
||||||
|
|||||||
@@ -21,3 +21,51 @@ async def get_all(
|
|||||||
session: SessionDependency
|
session: SessionDependency
|
||||||
):
|
):
|
||||||
return await MarketplaceService(session).get_all_base_marketplaces()
|
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)
|
||||||
|
|||||||
37
routers/task.py
Normal file
37
routers/task.py
Normal file
@@ -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
|
||||||
|
)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from schemas.base import BaseSchema
|
from schemas.base import BaseSchema, OkMessageSchema
|
||||||
|
from schemas.client import ClientSchema
|
||||||
|
|
||||||
|
|
||||||
# region Entities
|
# region Entities
|
||||||
@@ -10,13 +11,62 @@ class BaseMarketplaceSchema(BaseSchema):
|
|||||||
icon_url: str
|
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
|
# endregion
|
||||||
|
|
||||||
# region Requests
|
# 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
|
# endregion
|
||||||
|
|
||||||
# region Responses
|
# region Responses
|
||||||
class GetAllBaseMarketplacesResponse(BaseSchema):
|
class GetAllBaseMarketplacesResponse(BaseSchema):
|
||||||
base_marketplaces: List[BaseMarketplaceSchema]
|
base_marketplaces: List[BaseMarketplaceSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class GetClientMarketplacesResponse(BaseSchema):
|
||||||
|
marketplaces: List[MarketplaceSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateMarketplaceResponse(OkMessageSchema):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteMarketplaceResponse(OkMessageSchema):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class UpdateMarketplaceResponse(OkMessageSchema):
|
||||||
|
pass
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|||||||
14
schemas/task.py
Normal file
14
schemas/task.py
Normal file
@@ -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
|
||||||
@@ -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 models import BaseMarketplace, Marketplace
|
||||||
from schemas.marketplace import GetAllBaseMarketplacesResponse
|
from schemas.marketplace import *
|
||||||
from services.base import BaseService
|
from services.base import BaseService
|
||||||
|
|
||||||
|
|
||||||
@@ -12,3 +14,81 @@ class MarketplaceService(BaseService):
|
|||||||
return GetAllBaseMarketplacesResponse(
|
return GetAllBaseMarketplacesResponse(
|
||||||
base_marketplaces=base_marketplaces
|
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))
|
||||||
|
|||||||
Reference in New Issue
Block a user