123
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
import json
|
import asyncio
|
||||||
|
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
|
||||||
from background import celery
|
from background import celery
|
||||||
|
import background.update
|
||||||
|
|
||||||
|
|
||||||
@celery.task(name='test')
|
@celery.task(name='process_update')
|
||||||
def test_task():
|
def process_update(product_ids: list[int]):
|
||||||
with open('test.json', 'a') as tf:
|
loop = asyncio.get_event_loop()
|
||||||
tf.write(json.dumps({'ok': True}))
|
return loop.run_until_complete(background.update.process_update(product_ids))
|
||||||
|
|||||||
14
background/update.py
Normal file
14
background/update.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from backend.session import get_session
|
||||||
|
from schemas.general import StockUpdate
|
||||||
|
from updaters.stocks_updater import StocksUpdater
|
||||||
|
|
||||||
|
|
||||||
|
async def process_update(product_ids: list[int]):
|
||||||
|
async for session in get_session():
|
||||||
|
updates = [StockUpdate(product_id=product_id) for product_id in product_ids]
|
||||||
|
updater = StocksUpdater(session)
|
||||||
|
await updater.update(updates)
|
||||||
|
await session.close()
|
||||||
|
return {'message': f'Stocks for [{",".join(map(str, product_ids))}] successfully updated'}
|
||||||
@@ -51,6 +51,7 @@ class Marketplace(BaseSiproModel):
|
|||||||
sell_from_price: Mapped[bool] = mapped_column()
|
sell_from_price: Mapped[bool] = mapped_column()
|
||||||
|
|
||||||
warehouses: Mapped[List["Warehouse"]] = relationship(secondary=marketplace_warehouses)
|
warehouses: Mapped[List["Warehouse"]] = relationship(secondary=marketplace_warehouses)
|
||||||
|
warehouse_id: Mapped[str] = mapped_column()
|
||||||
|
|
||||||
company_id: Mapped[int] = mapped_column(ForeignKey('companies.id'))
|
company_id: Mapped[int] = mapped_column(ForeignKey('companies.id'))
|
||||||
company: Mapped["Company"] = relationship()
|
company: Mapped["Company"] = relationship()
|
||||||
|
|||||||
@@ -24,11 +24,14 @@ class MarketplaceProduct(BaseSiproModel):
|
|||||||
product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
|
product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
|
||||||
product: Mapped["Product"] = relationship()
|
product: Mapped["Product"] = relationship()
|
||||||
|
|
||||||
|
third_additional_article: Mapped[str] = mapped_column()
|
||||||
|
|
||||||
|
|
||||||
class SupplierProduct(BaseSiproModel):
|
class SupplierProduct(BaseSiproModel):
|
||||||
__tablename__ = 'supplier_products'
|
__tablename__ = 'supplier_products'
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
supplier_stock: Mapped[int] = mapped_column()
|
supplier_stock: Mapped[int] = mapped_column()
|
||||||
|
sold_today: Mapped[int] = mapped_column()
|
||||||
supplier_id: Mapped[int] = mapped_column()
|
supplier_id: Mapped[int] = mapped_column()
|
||||||
|
|
||||||
product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
|
product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
|
||||||
|
|||||||
54
main.py
54
main.py
@@ -1,43 +1,37 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from celery.result import AsyncResult
|
from celery.result import AsyncResult
|
||||||
from fastapi import FastAPI, Depends, Body
|
from fastapi import FastAPI, Depends, HTTPException
|
||||||
from sqlalchemy import select
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
from sqlalchemy.dialects.postgresql import insert
|
from starlette import status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy.orm import joinedload
|
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
from backend.session import get_session
|
import background.tasks
|
||||||
from database import DailyStock
|
|
||||||
from database.sipro import *
|
|
||||||
from queries.general import get_stocks_data
|
|
||||||
from background.tasks import *
|
from background.tasks import *
|
||||||
from updaters.stocks_updater import StockUpdate
|
from schemas.general import UpdateRequest, UpdateResponse
|
||||||
|
|
||||||
app = FastAPI()
|
auth_schema = HTTPBearer()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
async def check_auth(token: Annotated[HTTPAuthorizationCredentials, Depends(auth_schema)]):
|
||||||
async def root(
|
if token.credentials != 'vvHh1QNl7lS6c7OVwmxU1TVNd7DLlc9W810csZGf4rkqOrBy6fQwlhIDZsQZd9hQYZYK47yWv33aCq':
|
||||||
session: Annotated[AsyncSession, Depends(get_session)],
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid credentials')
|
||||||
marketplace_id: int
|
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
dependencies=[Depends(check_auth)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
'/update',
|
||||||
|
response_model=UpdateResponse
|
||||||
|
)
|
||||||
|
async def update(
|
||||||
|
request: UpdateRequest
|
||||||
):
|
):
|
||||||
marketplace = await session.get(Marketplace, marketplace_id, options=[
|
task = background.tasks.process_update.delay(request.product_ids)
|
||||||
joinedload(Marketplace.warehouses).joinedload(Warehouse.suppliers),
|
return UpdateResponse(task_id=task.id)
|
||||||
joinedload(Marketplace.warehouses).joinedload(Warehouse.company_warehouses),
|
|
||||||
joinedload(Marketplace.company).joinedload(Company.warehouse)
|
|
||||||
])
|
|
||||||
data = await get_stocks_data(session, marketplace)
|
|
||||||
data = sorted(data, key=lambda x: x['denco_article'])
|
|
||||||
return {"message": data}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/tasks", status_code=201)
|
|
||||||
def run_task(payload=Body(...)):
|
|
||||||
task_type = payload["type"]
|
|
||||||
task = test_task.delay()
|
|
||||||
return JSONResponse({"task_id": task.id})
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/tasks/{task_id}")
|
@app.get("/tasks/{task_id}")
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
from .ozon import OzonMarketplace
|
from .ozon import OzonMarketplaceApi
|
||||||
from .wildberries import WildberriesMarketplace
|
from .wildberries import WildberriesMarketplaceApi
|
||||||
from .factory import MarketplaceFactory
|
from .factory import MarketplaceApiFactory
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from aiohttp import ClientResponse
|
|||||||
from database import Marketplace
|
from database import Marketplace
|
||||||
|
|
||||||
|
|
||||||
class BaseJsonMarketplace(ABC):
|
class BaseMarketplaceApi(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __init__(self, marketplace: Marketplace):
|
def __init__(self, marketplace: Marketplace):
|
||||||
pass
|
pass
|
||||||
@@ -20,18 +20,17 @@ class BaseJsonMarketplace(ABC):
|
|||||||
def get_headers(self):
|
def get_headers(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
@property
|
@property
|
||||||
|
@abstractmethod
|
||||||
def api_url(self):
|
def api_url(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def _method(self, http_method: Literal['POST', 'GET', 'PATCH', 'PUT', 'DELETE'],
|
async def _method(self, http_method: Literal['POST', 'GET', 'PATCH', 'PUT', 'DELETE'],
|
||||||
method: str,
|
method: str,
|
||||||
data: dict) -> ClientResponse:
|
data: dict) -> ClientResponse:
|
||||||
async with aiohttp.ClientSession as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.request(http_method,
|
return await session.request(http_method,
|
||||||
f'{self.api_url}{method}',
|
f'{self.api_url}{method}',
|
||||||
json=data,
|
json=data,
|
||||||
headers=self.get_headers()
|
headers=self.get_headers()
|
||||||
) as response:
|
)
|
||||||
return response
|
|
||||||
|
|||||||
@@ -2,18 +2,18 @@ from typing import Union
|
|||||||
|
|
||||||
from database import Marketplace
|
from database import Marketplace
|
||||||
from database.sipro.enums.general import BaseMarketplace
|
from database.sipro.enums.general import BaseMarketplace
|
||||||
from .wildberries import WildberriesMarketplace
|
from .wildberries import WildberriesMarketplaceApi
|
||||||
from .ozon import OzonMarketplace
|
from .ozon import OzonMarketplaceApi
|
||||||
|
|
||||||
|
|
||||||
class MarketplaceFactory:
|
class MarketplaceApiFactory:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_marketplace(marketplace: Marketplace) -> Union[
|
def get_marketplace_api(marketplace: Marketplace) -> Union[
|
||||||
WildberriesMarketplace,
|
WildberriesMarketplaceApi,
|
||||||
OzonMarketplace,
|
OzonMarketplaceApi,
|
||||||
]:
|
]:
|
||||||
match marketplace.base_marketplace:
|
match marketplace.base_marketplace:
|
||||||
case BaseMarketplace.OZON:
|
case BaseMarketplace.OZON:
|
||||||
return OzonMarketplace(marketplace)
|
return OzonMarketplaceApi(marketplace)
|
||||||
case BaseMarketplace.WILDBERRIES:
|
case BaseMarketplace.WILDBERRIES:
|
||||||
return WildberriesMarketplace(marketplace)
|
return WildberriesMarketplaceApi(marketplace)
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from aiolimiter import AsyncLimiter
|
|
||||||
from asynciolimiter import StrictLimiter
|
|
||||||
|
|
||||||
import utils
|
import utils
|
||||||
from database import Marketplace
|
from database import Marketplace
|
||||||
from limiter import BatchLimiter
|
from limiter import BatchLimiter
|
||||||
from marketplaces.base import BaseJsonMarketplace
|
from marketplaces.base import BaseMarketplaceApi
|
||||||
|
|
||||||
|
|
||||||
class OzonMarketplace(BaseJsonMarketplace):
|
class OzonMarketplaceApi(BaseMarketplaceApi):
|
||||||
|
|
||||||
def __init__(self, marketplace: Marketplace):
|
def __init__(self, marketplace: Marketplace):
|
||||||
self.marketplace = marketplace
|
self.marketplace = marketplace
|
||||||
@@ -25,6 +23,7 @@ class OzonMarketplace(BaseJsonMarketplace):
|
|||||||
def get_headers(self):
|
def get_headers(self):
|
||||||
return self.headers
|
return self.headers
|
||||||
|
|
||||||
|
@property
|
||||||
def api_url(self):
|
def api_url(self):
|
||||||
return 'https://api-seller.ozon.ru'
|
return 'https://api-seller.ozon.ru'
|
||||||
|
|
||||||
@@ -33,22 +32,23 @@ class OzonMarketplace(BaseJsonMarketplace):
|
|||||||
return
|
return
|
||||||
max_stocks = 100
|
max_stocks = 100
|
||||||
chunks = utils.chunk_list(data, max_stocks)
|
chunks = utils.chunk_list(data, max_stocks)
|
||||||
limiter = BatchLimiter(max_requests=80,
|
limiter = BatchLimiter(max_requests=80, period=60)
|
||||||
period=60)
|
|
||||||
for chunk in chunks:
|
async def send_stock_chunk(chunk):
|
||||||
try:
|
try:
|
||||||
await limiter.acquire()
|
await limiter.acquire()
|
||||||
response = await self._method('POST',
|
request_data = {'stocks': chunk}
|
||||||
'/v2/products/stocks',
|
response = await self._method('POST', '/v2/products/stocks', data=request_data)
|
||||||
data=chunk)
|
print(request_data)
|
||||||
response = await response.json()
|
response = await response.json()
|
||||||
# response = await
|
|
||||||
error_message = response.get('message')
|
error_message = response.get('message')
|
||||||
error_code = response.get('code')
|
error_code = response.get('code')
|
||||||
if error_message:
|
if error_message:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f'Error occurred when sending stocks to [{self.marketplace.id}]: {error_message} ({error_code})')
|
f'Error occurred when sending stocks to [{self.marketplace.id}]: {error_message} ({error_code})')
|
||||||
break
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
f'Exception occurred while sending stocks to marketplace ID [{self.marketplace.id}]: {str(e)}')
|
f'Exception occurred while sending stocks to marketplace ID [{self.marketplace.id}]: {str(e)}')
|
||||||
|
|
||||||
|
tasks = [send_stock_chunk(chunk) for chunk in chunks]
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Union
|
from typing import Union
|
||||||
@@ -5,10 +6,10 @@ from typing import Union
|
|||||||
import utils
|
import utils
|
||||||
from database import Marketplace
|
from database import Marketplace
|
||||||
from limiter import BatchLimiter
|
from limiter import BatchLimiter
|
||||||
from marketplaces.base import BaseJsonMarketplace
|
from marketplaces.base import BaseMarketplaceApi
|
||||||
|
|
||||||
|
|
||||||
class WildberriesMarketplace(BaseJsonMarketplace):
|
class WildberriesMarketplaceApi(BaseMarketplaceApi):
|
||||||
def __init__(self, marketplace: Marketplace):
|
def __init__(self, marketplace: Marketplace):
|
||||||
self.marketplace = marketplace
|
self.marketplace = marketplace
|
||||||
auth_data = json.loads(marketplace.auth_data)
|
auth_data = json.loads(marketplace.auth_data)
|
||||||
@@ -21,6 +22,7 @@ class WildberriesMarketplace(BaseJsonMarketplace):
|
|||||||
def get_headers(self):
|
def get_headers(self):
|
||||||
return self.headers
|
return self.headers
|
||||||
|
|
||||||
|
@property
|
||||||
def api_url(self):
|
def api_url(self):
|
||||||
return 'https://suppliers-api.wildberries.ru'
|
return 'https://suppliers-api.wildberries.ru'
|
||||||
|
|
||||||
@@ -29,21 +31,24 @@ class WildberriesMarketplace(BaseJsonMarketplace):
|
|||||||
return
|
return
|
||||||
max_stocks = 1000
|
max_stocks = 1000
|
||||||
chunks = utils.chunk_list(data, max_stocks)
|
chunks = utils.chunk_list(data, max_stocks)
|
||||||
limiter = BatchLimiter(max_requests=300,
|
limiter = BatchLimiter(max_requests=300, period=60)
|
||||||
period=60)
|
|
||||||
for chunk in chunks:
|
async def send_stock_chunk(chunk):
|
||||||
try:
|
try:
|
||||||
await limiter.acquire()
|
await limiter.acquire()
|
||||||
response = await self._method('PUT',
|
request_data = {'stocks': chunk}
|
||||||
'/api/v3/stocks/{warehouseId}',
|
response = await self._method('PUT', f'/api/v3/stocks/{self.marketplace.warehouse_id}',
|
||||||
chunk)
|
data=request_data)
|
||||||
if response.status != 204:
|
print(request_data)
|
||||||
|
if response.status not in [204, 409]:
|
||||||
response = await response.json()
|
response = await response.json()
|
||||||
error_message = response.get('message')
|
error_message = response.get('message')
|
||||||
error_code = response.get('code')
|
error_code = response.get('code')
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f'Error occurred when sending stocks to [{self.marketplace.id}]: {error_message} ({error_code})')
|
f'Error occurred when sending stocks to [{self.marketplace.id}]: {error_message} ({error_code})')
|
||||||
break
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
f'Exception occurred while sending stocks to marketplace ID [{self.marketplace.id}]: {str(e)}')
|
f'Exception occurred while sending stocks to marketplace ID [{self.marketplace.id}]: {str(e)}')
|
||||||
|
|
||||||
|
tasks = [send_stock_chunk(chunk) for chunk in chunks]
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from typing import Union
|
from dataclasses import dataclass
|
||||||
|
from typing import Union, TypedDict
|
||||||
|
|
||||||
from sqlalchemy import select, func, and_, cast, String, case, or_
|
from sqlalchemy import select, func, and_, cast, String, case, or_
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -9,6 +10,12 @@ from database.sipro import *
|
|||||||
from database.sipro.enums.product import ProductRelationType
|
from database.sipro.enums.product import ProductRelationType
|
||||||
|
|
||||||
|
|
||||||
|
class StockData(TypedDict):
|
||||||
|
full_stock: int
|
||||||
|
article: Union[str, int]
|
||||||
|
marketplace_product: MarketplaceProduct
|
||||||
|
|
||||||
|
|
||||||
def get_marketplace_suppliers_and_company_warehouses(marketplace: Marketplace):
|
def get_marketplace_suppliers_and_company_warehouses(marketplace: Marketplace):
|
||||||
company = marketplace.company
|
company = marketplace.company
|
||||||
suppliers = set()
|
suppliers = set()
|
||||||
@@ -30,7 +37,7 @@ async def get_stocks_data(
|
|||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
marketplace: Marketplace,
|
marketplace: Marketplace,
|
||||||
product_ids: Union[list[int], None] = None
|
product_ids: Union[list[int], None] = None
|
||||||
):
|
) -> List[StockData]:
|
||||||
if not product_ids:
|
if not product_ids:
|
||||||
product_ids = []
|
product_ids = []
|
||||||
company = marketplace.company
|
company = marketplace.company
|
||||||
@@ -46,7 +53,7 @@ async def get_stocks_data(
|
|||||||
supplier_stock_subquery = (
|
supplier_stock_subquery = (
|
||||||
select(
|
select(
|
||||||
func.greatest(
|
func.greatest(
|
||||||
func.sum(SupplierProduct.supplier_stock) - func.coalesce(DailyStock.sold_today, 0),
|
func.sum(SupplierProduct.supplier_stock - SupplierProduct.sold_today),
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
.label('supplier_stock'),
|
.label('supplier_stock'),
|
||||||
@@ -58,10 +65,6 @@ async def get_stocks_data(
|
|||||||
.join(
|
.join(
|
||||||
Product
|
Product
|
||||||
)
|
)
|
||||||
.outerjoin(
|
|
||||||
DailyStock,
|
|
||||||
DailyStock.product_id == SupplierProduct.product_id
|
|
||||||
)
|
|
||||||
.where(
|
.where(
|
||||||
SupplierProduct.supplier_id.in_(supplier_ids)
|
SupplierProduct.supplier_id.in_(supplier_ids)
|
||||||
)
|
)
|
||||||
@@ -286,9 +289,13 @@ async def get_stocks_data(
|
|||||||
slaves_stock_subquery.c.product_id == MarketplaceProduct.product_id
|
slaves_stock_subquery.c.product_id == MarketplaceProduct.product_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
print('-------------------------')
|
||||||
|
print(stmt.compile(compile_kwargs={
|
||||||
|
'literal_binds': True
|
||||||
|
}))
|
||||||
result = await session.execute(stmt)
|
result = await session.execute(stmt)
|
||||||
marketplace_products = result.all()
|
marketplace_products = result.all()
|
||||||
result = []
|
response: List[StockData] = []
|
||||||
for (marketplace_product,
|
for (marketplace_product,
|
||||||
denco_article,
|
denco_article,
|
||||||
price_purchase,
|
price_purchase,
|
||||||
@@ -301,8 +308,8 @@ async def get_stocks_data(
|
|||||||
price_recommended,
|
price_recommended,
|
||||||
is_archived) in marketplace_products:
|
is_archived) in marketplace_products:
|
||||||
if is_archived or (sell_from_price > price_recommended):
|
if is_archived or (sell_from_price > price_recommended):
|
||||||
result.append({
|
response.append({
|
||||||
'denco_article': denco_article,
|
'article': denco_article,
|
||||||
'full_stock': 0,
|
'full_stock': 0,
|
||||||
'marketplace_product': marketplace_product,
|
'marketplace_product': marketplace_product,
|
||||||
})
|
})
|
||||||
@@ -328,10 +335,10 @@ async def get_stocks_data(
|
|||||||
full_stock = 0
|
full_stock = 0
|
||||||
full_stock = max([0, full_stock])
|
full_stock = max([0, full_stock])
|
||||||
|
|
||||||
result.append({
|
response.append({
|
||||||
'denco_article': denco_article,
|
'article': denco_article,
|
||||||
'full_stock': full_stock,
|
'full_stock': full_stock,
|
||||||
'marketplace_product': marketplace_product,
|
'marketplace_product': marketplace_product,
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return response
|
||||||
|
|||||||
0
schemas/__init__.py
Normal file
0
schemas/__init__.py
Normal file
20
schemas/general.py
Normal file
20
schemas/general.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StockUpdate:
|
||||||
|
product_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSchema(BaseModel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateRequest(BaseSchema):
|
||||||
|
product_ids: list[int]
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateResponse(BaseSchema):
|
||||||
|
task_id: str
|
||||||
@@ -1,15 +1,41 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
import queries.general
|
||||||
from database import Marketplace
|
from database import Marketplace
|
||||||
from updaters.stocks_updater import StockUpdate
|
from marketplaces import MarketplaceApiFactory
|
||||||
|
from marketplaces.base import BaseMarketplaceApi
|
||||||
|
from queries.general import StockData
|
||||||
|
from schemas.general import StockUpdate
|
||||||
|
|
||||||
|
|
||||||
class BaseMarketplaceUpdater(ABC):
|
class BaseMarketplaceUpdater(ABC):
|
||||||
@abstractmethod
|
marketplace: Marketplace
|
||||||
def __init__(self, marketplace: Marketplace):
|
marketplace_api: BaseMarketplaceApi
|
||||||
pass
|
session: AsyncSession
|
||||||
|
|
||||||
|
def __init__(self, marketplace: Marketplace, session: AsyncSession):
|
||||||
|
self.marketplace = marketplace
|
||||||
|
self.session = session
|
||||||
|
self.marketplace_api = MarketplaceApiFactory.get_marketplace_api(marketplace)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def update(self, updates: List[StockUpdate]):
|
def get_update_for_marketplace(self,
|
||||||
|
stock_data: StockData) -> dict:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def update(self, updates: List[StockUpdate]):
|
||||||
|
product_ids = list(set([update.product_id for update in updates]))
|
||||||
|
stock_data_list = await queries.general.get_stocks_data(
|
||||||
|
session=self.session,
|
||||||
|
marketplace=self.marketplace,
|
||||||
|
product_ids=product_ids
|
||||||
|
)
|
||||||
|
return
|
||||||
|
marketplace_updates = []
|
||||||
|
for stock_data in stock_data_list:
|
||||||
|
marketplace_update = self.get_update_for_marketplace(stock_data)
|
||||||
|
marketplace_updates.append(marketplace_update)
|
||||||
|
await self.marketplace_api.update_stocks(marketplace_updates)
|
||||||
|
|||||||
18
updaters/factory.py
Normal file
18
updaters/factory.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from database import Marketplace
|
||||||
|
from database.sipro.enums.general import BaseMarketplace
|
||||||
|
from updaters.ozon_updater import OzonUpdater
|
||||||
|
from updaters.wildberries_updater import WildberriesUpdater
|
||||||
|
|
||||||
|
|
||||||
|
class UpdaterFactory:
|
||||||
|
@staticmethod
|
||||||
|
def get_updater(session: AsyncSession, marketplace: Marketplace) -> Union[OzonUpdater, WildberriesUpdater]:
|
||||||
|
match marketplace.base_marketplace:
|
||||||
|
case BaseMarketplace.WILDBERRIES:
|
||||||
|
return WildberriesUpdater(marketplace, session)
|
||||||
|
case BaseMarketplace.OZON:
|
||||||
|
return OzonUpdater(marketplace, session)
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
from typing import List
|
from queries.general import StockData
|
||||||
|
|
||||||
from database import Marketplace
|
|
||||||
from marketplaces import MarketplaceFactory, OzonMarketplace
|
|
||||||
from updaters.base import BaseMarketplaceUpdater
|
from updaters.base import BaseMarketplaceUpdater
|
||||||
from updaters.stocks_updater import StockUpdate
|
|
||||||
|
|
||||||
|
|
||||||
class OzonUpdater(BaseMarketplaceUpdater):
|
class OzonUpdater(BaseMarketplaceUpdater):
|
||||||
def __init__(self, marketplace: Marketplace):
|
def get_update_for_marketplace(self, data: StockData) -> dict:
|
||||||
self.ozon_marketplace: OzonMarketplace = MarketplaceFactory.get_marketplace(marketplace)
|
return {
|
||||||
|
'offer_id': str(data['article']),
|
||||||
async def update(self, updates: List[StockUpdate]):
|
'stock': 0, # $data['full_stock'],
|
||||||
pass
|
'warehouse_id': self.marketplace.warehouse_id
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,53 +1,47 @@
|
|||||||
|
import asyncio
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import unique, IntEnum
|
from enum import unique, IntEnum
|
||||||
from typing import List, Union
|
from typing import List
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.dialects.postgresql import insert
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
import database
|
from database import Marketplace, MarketplaceProduct, Warehouse, Company
|
||||||
from database import Marketplace, MarketplaceProduct, DailyStock
|
from schemas.general import StockUpdate
|
||||||
|
from updaters.factory import UpdaterFactory
|
||||||
|
|
||||||
@unique
|
|
||||||
class StockUpdateType(IntEnum):
|
|
||||||
SALE = 0
|
|
||||||
SUPPLIER_UPDATE = 1
|
|
||||||
WAREHOUSE_UPDATE = 2
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class StockUpdate:
|
|
||||||
product_id: int
|
|
||||||
type: StockUpdateType
|
|
||||||
quantity: int
|
|
||||||
|
|
||||||
|
|
||||||
class StocksUpdater:
|
class StocksUpdater:
|
||||||
def __init__(self, session: AsyncSession):
|
def __init__(self, session: AsyncSession):
|
||||||
self.session = session
|
self.session = session
|
||||||
|
|
||||||
|
async def get_marketplace(self, marketplace_id: int):
|
||||||
|
marketplace = await self.session.get(Marketplace, marketplace_id, options=[
|
||||||
|
joinedload(Marketplace.warehouses).joinedload(Warehouse.suppliers),
|
||||||
|
joinedload(Marketplace.warehouses).joinedload(Warehouse.company_warehouses),
|
||||||
|
joinedload(Marketplace.company).joinedload(Company.warehouse)
|
||||||
|
])
|
||||||
|
return marketplace
|
||||||
|
|
||||||
async def update_marketplace(self, marketplace_id: int, updates: List[StockUpdate]):
|
async def update_marketplace(self, marketplace_id: int, updates: List[StockUpdate]):
|
||||||
pass
|
marketplace = await self.get_marketplace(marketplace_id)
|
||||||
|
updater = UpdaterFactory.get_updater(self.session, marketplace)
|
||||||
|
if not updater:
|
||||||
|
return
|
||||||
|
await updater.update(updates)
|
||||||
|
|
||||||
async def update(self, updates: list[StockUpdate]):
|
async def update(self, updates: list[StockUpdate]):
|
||||||
updates_dict = defaultdict(list)
|
updates_dict = defaultdict(list)
|
||||||
stock_update_values = []
|
|
||||||
for update in updates:
|
for update in updates:
|
||||||
# Working with sold today
|
|
||||||
if update.type == StockUpdateType.SALE:
|
|
||||||
stock_update_values.append({
|
|
||||||
'product_id': update.product_id,
|
|
||||||
'sold_today': update.quantity
|
|
||||||
})
|
|
||||||
# Working with marketplaces
|
# Working with marketplaces
|
||||||
stmt = (
|
stmt = (
|
||||||
select(
|
select(
|
||||||
MarketplaceProduct.marketplace_id.distinct()
|
MarketplaceProduct.marketplace_id.distinct()
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
MarketplaceProduct.product_id == update.product_id
|
MarketplaceProduct.product_id == update.product_id,
|
||||||
|
MarketplaceProduct.marketplace_id.in_([9, 41])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
stmt_result = await self.session.execute(stmt)
|
stmt_result = await self.session.execute(stmt)
|
||||||
@@ -57,27 +51,9 @@ class StocksUpdater:
|
|||||||
for marketplace_id in marketplace_ids:
|
for marketplace_id in marketplace_ids:
|
||||||
updates_dict[marketplace_id].append(update)
|
updates_dict[marketplace_id].append(update)
|
||||||
updates_list = list(updates_dict.items())
|
updates_list = list(updates_dict.items())
|
||||||
updates_list = sorted(updates_list, key=lambda x: x[1])
|
updates_list = sorted(updates_list, key=lambda x: len(x[1]))
|
||||||
|
|
||||||
# Updating DailyStock-s
|
|
||||||
insert_stmt = (
|
|
||||||
insert(
|
|
||||||
DailyStock
|
|
||||||
)
|
|
||||||
.values(
|
|
||||||
stock_update_values
|
|
||||||
)
|
|
||||||
)
|
|
||||||
insert_stmt = (
|
|
||||||
insert_stmt.on_conflict_do_update(
|
|
||||||
index_elements=['product_id'],
|
|
||||||
set_={
|
|
||||||
'sold_today': DailyStock.sold_today + insert_stmt.excluded.sold_today
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await self.session.execute(insert_stmt)
|
|
||||||
await self.session.commit()
|
|
||||||
|
|
||||||
|
tasks = []
|
||||||
for marketplace_id, marketplace_updates in updates_list:
|
for marketplace_id, marketplace_updates in updates_list:
|
||||||
await self.update_marketplace(marketplace_id, marketplace_updates)
|
tasks.append(self.update_marketplace(marketplace_id, marketplace_updates))
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|||||||
@@ -1 +1,11 @@
|
|||||||
|
from queries.general import StockData
|
||||||
|
from updaters.base import BaseMarketplaceUpdater
|
||||||
|
|
||||||
|
|
||||||
|
class WildberriesUpdater(BaseMarketplaceUpdater):
|
||||||
|
|
||||||
|
def get_update_for_marketplace(self, stock_data: StockData) -> dict:
|
||||||
|
return {
|
||||||
|
'sku': stock_data['marketplace_product'].third_additional_article,
|
||||||
|
'amount': 0 # stock_data['full_stock']
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user