123
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
.venv/
|
.venv/
|
||||||
.idea/
|
.idea/
|
||||||
.env
|
.env
|
||||||
|
.linux_venv
|
||||||
alembic/versions/
|
alembic/versions/
|
||||||
@@ -8,3 +8,6 @@ PG_PASSWORD = os.environ.get('PG_PASSWORD')
|
|||||||
PG_PORT = os.environ.get('PG_PORT')
|
PG_PORT = os.environ.get('PG_PORT')
|
||||||
PG_HOST = os.environ.get('PG_HOST')
|
PG_HOST = os.environ.get('PG_HOST')
|
||||||
PG_DATABASE = os.environ.get('PG_DATABASE')
|
PG_DATABASE = os.environ.get('PG_DATABASE')
|
||||||
|
|
||||||
|
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL')
|
||||||
|
CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND')
|
||||||
|
|||||||
2
background/__init__.py
Normal file
2
background/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .worker import celery
|
||||||
|
from .tasks import *
|
||||||
9
background/tasks.py
Normal file
9
background/tasks.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from background import celery
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task(name='test')
|
||||||
|
def test_task():
|
||||||
|
with open('test.json', 'a') as tf:
|
||||||
|
tf.write(json.dumps({'ok': True}))
|
||||||
8
background/worker.py
Normal file
8
background/worker.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from celery import Celery
|
||||||
|
from backend.config import CELERY_RESULT_BACKEND, CELERY_BROKER_URL
|
||||||
|
|
||||||
|
celery = Celery(
|
||||||
|
__name__,
|
||||||
|
broker=CELERY_BROKER_URL,
|
||||||
|
backend=CELERY_RESULT_BACKEND
|
||||||
|
)
|
||||||
@@ -1 +1,2 @@
|
|||||||
from .models import *
|
from .models import *
|
||||||
|
from .enums import *
|
||||||
|
|||||||
0
database/sipro/enums/__init__.py
Normal file
0
database/sipro/enums/__init__.py
Normal file
8
database/sipro/enums/general.py
Normal file
8
database/sipro/enums/general.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from enum import unique, IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class BaseMarketplace(IntEnum):
|
||||||
|
WILDBERRIES = 0
|
||||||
|
OZON = 1
|
||||||
|
YANDEX_MARKET = 2
|
||||||
12
database/sipro/enums/product.py
Normal file
12
database/sipro/enums/product.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from enum import IntEnum, unique
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class ProductRelationType(IntEnum):
|
||||||
|
PRODUCT_SAME_COLOR = 2
|
||||||
|
RELATION_FIRST = 3
|
||||||
|
RELATION_SECOND = 4
|
||||||
|
MAIN_PRODUCT = 5
|
||||||
|
PRODUCT_DIFFERENT_SIZE = 6
|
||||||
|
SAME_MIX = 7
|
||||||
|
SAME_PRODUCT = 8
|
||||||
@@ -44,7 +44,7 @@ class Marketplace(BaseSiproModel):
|
|||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
name: Mapped[str] = mapped_column()
|
name: Mapped[str] = mapped_column()
|
||||||
auth_data: Mapped[str] = mapped_column()
|
auth_data: Mapped[str] = mapped_column()
|
||||||
|
base_marketplace: Mapped[int] = mapped_column()
|
||||||
sell_mixes: Mapped[bool] = mapped_column()
|
sell_mixes: Mapped[bool] = mapped_column()
|
||||||
sell_blocks: Mapped[bool] = mapped_column()
|
sell_blocks: Mapped[bool] = mapped_column()
|
||||||
sell_warehouse_products: Mapped[bool] = mapped_column()
|
sell_warehouse_products: Mapped[bool] = mapped_column()
|
||||||
|
|||||||
@@ -7,4 +7,3 @@ class DailyStock(BaseStocksModel):
|
|||||||
__tablename__ = 'daily_stocks'
|
__tablename__ = 'daily_stocks'
|
||||||
product_id: Mapped[int] = mapped_column(primary_key=True)
|
product_id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
sold_today: Mapped[int] = mapped_column()
|
sold_today: Mapped[int] = mapped_column()
|
||||||
test: Mapped[bool] = mapped_column()
|
|
||||||
|
|||||||
1
limiter/__init__.py
Normal file
1
limiter/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .batch_limiter import BatchLimiter
|
||||||
29
limiter/batch_limiter.py
Normal file
29
limiter/batch_limiter.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class BatchLimiter:
|
||||||
|
def __init__(self, max_requests, period):
|
||||||
|
self.max_requests = max_requests
|
||||||
|
self.period = period
|
||||||
|
self.current_requests = 0
|
||||||
|
self.start_time = None
|
||||||
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def acquire(self):
|
||||||
|
async with self.lock:
|
||||||
|
if self.current_requests == 0:
|
||||||
|
self.start_time = datetime.now()
|
||||||
|
|
||||||
|
if self.current_requests < self.max_requests:
|
||||||
|
self.current_requests += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
elapsed_time = (datetime.now() - self.start_time).total_seconds()
|
||||||
|
if elapsed_time < self.period:
|
||||||
|
await asyncio.sleep(self.period - elapsed_time)
|
||||||
|
self.current_requests = 1
|
||||||
|
self.start_time = datetime.now()
|
||||||
|
else:
|
||||||
|
self.current_requests = 1
|
||||||
|
self.start_time = datetime.now()
|
||||||
316
main.py
316
main.py
@@ -1,302 +1,23 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import FastAPI, Depends
|
from celery.result import AsyncResult
|
||||||
from sqlalchemy import select, func, and_, cast, String
|
from fastapi import FastAPI, Depends, Body
|
||||||
|
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
|
from sqlalchemy.orm import joinedload
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
from backend.session import get_session
|
from backend.session import get_session
|
||||||
|
from database import DailyStock
|
||||||
from database.sipro import *
|
from database.sipro import *
|
||||||
|
from queries.general import get_stocks_data
|
||||||
|
from background.tasks import *
|
||||||
|
from updaters.stocks_updater import StockUpdate
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
def get_marketplace_suppliers_and_company_warehouses(marketplace: Marketplace):
|
|
||||||
company = marketplace.company
|
|
||||||
suppliers = set()
|
|
||||||
company_warehouses = set()
|
|
||||||
for warehouse in marketplace.warehouses:
|
|
||||||
for supplier in warehouse.suppliers:
|
|
||||||
if supplier.is_pseudo:
|
|
||||||
continue
|
|
||||||
suppliers.add(supplier)
|
|
||||||
company_warehouses.update(warehouse.company_warehouses)
|
|
||||||
if marketplace.sell_warehouse_products:
|
|
||||||
company_warehouse = company.warehouse
|
|
||||||
if company_warehouse and not company.is_denco:
|
|
||||||
company_warehouses.add(company_warehouse)
|
|
||||||
return suppliers, company_warehouses
|
|
||||||
|
|
||||||
|
|
||||||
# def get_stocks_data(marketplace: Marketplace):
|
|
||||||
# suppliers, company_warehouses = get_marketplace_suppliers_and_company_warehouses(marketplace)
|
|
||||||
# supplier_ids = [supplier.id for supplier in suppliers]
|
|
||||||
# company_warehouse_ids = [warehouse.id for warehouse in company_warehouses]
|
|
||||||
#
|
|
||||||
# sell_mixes: bool = marketplace.sell_mixes
|
|
||||||
# sell_blocks: bool = marketplace.sell_blocks
|
|
||||||
# sell_warehouse_products: bool = marketplace.sell_warehouse_products
|
|
||||||
# sell_from_price: int = marketplace.sell_from_price
|
|
||||||
#
|
|
||||||
# stmt = (
|
|
||||||
# select(
|
|
||||||
# MarketplaceProduct
|
|
||||||
# )
|
|
||||||
# .join(
|
|
||||||
# Product
|
|
||||||
# )
|
|
||||||
# .options(
|
|
||||||
# joinedload(MarketplaceProduct.product)
|
|
||||||
# )
|
|
||||||
# .where(
|
|
||||||
# MarketplaceProduct.marketplace_id == marketplace.id
|
|
||||||
# )
|
|
||||||
# )
|
|
||||||
# supplier_stock_subquery = (
|
|
||||||
# select(
|
|
||||||
# func.sum(SupplierProduct.supplier_stock).label('supplier_stock'),
|
|
||||||
# Product.id.label('product_id')
|
|
||||||
# )
|
|
||||||
# .join(
|
|
||||||
# Product
|
|
||||||
# )
|
|
||||||
# .where(
|
|
||||||
# SupplierProduct.supplier_id.in_(supplier_ids)
|
|
||||||
# )
|
|
||||||
# .group_by(
|
|
||||||
# Product.id
|
|
||||||
# )
|
|
||||||
# .subquery()
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
# warehouse_stock_subquery = (
|
|
||||||
# select(
|
|
||||||
# func.count(CompanyWarehouseProduct.is_sold).label('warehouse_stock'),
|
|
||||||
# CompanyWarehouseProduct.product_id.label('product_id')
|
|
||||||
# )
|
|
||||||
# .where(
|
|
||||||
# CompanyWarehouseProduct.is_sold == False,
|
|
||||||
# CompanyWarehouseProduct.company_warehouse_id.in_(company_warehouse_ids)
|
|
||||||
# )
|
|
||||||
# .group_by(
|
|
||||||
# CompanyWarehouseProduct.product_id
|
|
||||||
# )
|
|
||||||
# .subquery()
|
|
||||||
# )
|
|
||||||
|
|
||||||
async def get_stocks_data(session: AsyncSession, marketplace: Marketplace):
|
|
||||||
company = marketplace.company
|
|
||||||
suppliers, company_warehouses = get_marketplace_suppliers_and_company_warehouses(marketplace)
|
|
||||||
supplier_ids = [supplier.id for supplier in suppliers]
|
|
||||||
company_warehouse_ids = [warehouse.id for warehouse in company_warehouses]
|
|
||||||
|
|
||||||
sell_mixes: bool = marketplace.sell_mixes
|
|
||||||
sell_blocks: bool = marketplace.sell_blocks
|
|
||||||
sell_warehouse_products: bool = marketplace.sell_warehouse_products
|
|
||||||
sell_from_price: int = marketplace.sell_from_price
|
|
||||||
|
|
||||||
supplier_stock_subquery = (
|
|
||||||
select(
|
|
||||||
func.sum(SupplierProduct.supplier_stock).label('supplier_stock'),
|
|
||||||
SupplierProduct.product_id.label('product_id')
|
|
||||||
)
|
|
||||||
.select_from(SupplierProduct)
|
|
||||||
.join(Product)
|
|
||||||
.where(SupplierProduct.supplier_id.in_(supplier_ids))
|
|
||||||
.group_by(SupplierProduct.product_id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
warehouse_stock_subquery = (
|
|
||||||
select(
|
|
||||||
func.count(CompanyWarehouseProduct.is_sold).label('warehouse_stock'),
|
|
||||||
CompanyWarehouseProduct.product_id.label('product_id')
|
|
||||||
)
|
|
||||||
.select_from(CompanyWarehouseProduct)
|
|
||||||
.where(
|
|
||||||
CompanyWarehouseProduct.is_sold == False,
|
|
||||||
CompanyWarehouseProduct.company_warehouse_id.in_(company_warehouse_ids)
|
|
||||||
)
|
|
||||||
.group_by(CompanyWarehouseProduct.product_id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
mix_stock_first_subquery = (
|
|
||||||
select(
|
|
||||||
func.sum(SupplierProduct.supplier_stock).label('master_stock'),
|
|
||||||
SupplierProduct.product_id.label('product_id')
|
|
||||||
)
|
|
||||||
.select_from(SupplierProduct)
|
|
||||||
.where(SupplierProduct.supplier_id.in_(supplier_ids))
|
|
||||||
.group_by(SupplierProduct.product_id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
mix_stock_full_subquery = (
|
|
||||||
select(
|
|
||||||
func.min(SupplierProduct.in_block).label('mix_stock'),
|
|
||||||
Product.id.label('product_id')
|
|
||||||
)
|
|
||||||
.select_from(Product)
|
|
||||||
.join(SupplierProduct)
|
|
||||||
.join(ProductRelation, Product.id == ProductRelation.slave_product_id)
|
|
||||||
.join(mix_stock_first_subquery, mix_stock_first_subquery.c.product_id == ProductRelation.master_product_id)
|
|
||||||
.where(
|
|
||||||
ProductRelation.relation_type == 5,
|
|
||||||
mix_stock_first_subquery.c.master_stock > 0
|
|
||||||
)
|
|
||||||
.group_by(Product.id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
is_master_first_subquery = (
|
|
||||||
select(
|
|
||||||
ProductRelation.master_product_id,
|
|
||||||
(func.count(ProductRelation.master_product_id) > 0).label('is_master')
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
ProductRelation.relation_type == 5
|
|
||||||
)
|
|
||||||
.group_by(ProductRelation.master_product_id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
is_master_subquery = (
|
|
||||||
select(
|
|
||||||
Product.id.label('product_id'),
|
|
||||||
func.coalesce(is_master_first_subquery.c.is_master, False).label('is_master')
|
|
||||||
)
|
|
||||||
.select_from(
|
|
||||||
Product
|
|
||||||
)
|
|
||||||
.outerjoin(
|
|
||||||
is_master_first_subquery,
|
|
||||||
Product.id == is_master_first_subquery.c.master_product_id
|
|
||||||
)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
in_block_subquery = (
|
|
||||||
select(
|
|
||||||
Product.id.label('product_id'),
|
|
||||||
func.min(SupplierProduct.in_block).label('in_block_value')
|
|
||||||
)
|
|
||||||
.select_from(Product)
|
|
||||||
.join(SupplierProduct)
|
|
||||||
.where(
|
|
||||||
SupplierProduct.supplier_id.in_(supplier_ids),
|
|
||||||
SupplierProduct.supplier_stock > 0
|
|
||||||
)
|
|
||||||
.group_by(Product.id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
slaves_stock_first_subquery = (
|
|
||||||
select(
|
|
||||||
ProductRelation.master_product_id.label('product_id'),
|
|
||||||
func.sum(SupplierProduct.supplier_stock).label('slaves_stock')
|
|
||||||
)
|
|
||||||
.select_from(ProductRelation)
|
|
||||||
.join(SupplierProduct, and_(
|
|
||||||
ProductRelation.slave_product_id == SupplierProduct.product_id,
|
|
||||||
ProductRelation.relation_type == 7
|
|
||||||
))
|
|
||||||
.where(SupplierProduct.supplier_id.in_(supplier_ids))
|
|
||||||
.group_by(ProductRelation.master_product_id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
slaves_stock_subquery = (
|
|
||||||
select(
|
|
||||||
Product.id.label('product_id'),
|
|
||||||
slaves_stock_first_subquery.c.slaves_stock.label('slaves_stock')
|
|
||||||
)
|
|
||||||
.select_from(Product)
|
|
||||||
.join(slaves_stock_first_subquery, slaves_stock_first_subquery.c.product_id == Product.id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
stmt = (
|
|
||||||
select(
|
|
||||||
MarketplaceProduct,
|
|
||||||
func.coalesce(Product.article, cast(Product.denco_article, String)).label('denco_article'),
|
|
||||||
MarketplaceProduct.mp_price_bought.label('price_purchase'),
|
|
||||||
supplier_stock_subquery.c.supplier_stock.label('supplier_stock'),
|
|
||||||
warehouse_stock_subquery.c.warehouse_stock.label('warehouse_stock'),
|
|
||||||
mix_stock_full_subquery.c.mix_stock.label('mix_stock'),
|
|
||||||
in_block_subquery.c.in_block_value.label('in_block_value'),
|
|
||||||
is_master_subquery.c.is_master.label('is_master'),
|
|
||||||
slaves_stock_subquery.c.slaves_stock.label('slaves_stock'),
|
|
||||||
MarketplaceProduct.price_recommended.label('price_recommended'),
|
|
||||||
MarketplaceProduct.is_archived.label('is_archived')
|
|
||||||
)
|
|
||||||
.select_from(MarketplaceProduct)
|
|
||||||
.join(Product)
|
|
||||||
.options(joinedload(MarketplaceProduct.product))
|
|
||||||
.where(MarketplaceProduct.marketplace_id == marketplace.id)
|
|
||||||
.outerjoin(supplier_stock_subquery, supplier_stock_subquery.c.product_id == MarketplaceProduct.product_id)
|
|
||||||
.outerjoin(warehouse_stock_subquery, warehouse_stock_subquery.c.product_id == MarketplaceProduct.product_id)
|
|
||||||
.outerjoin(mix_stock_full_subquery, mix_stock_full_subquery.c.product_id == MarketplaceProduct.product_id)
|
|
||||||
.outerjoin(in_block_subquery, in_block_subquery.c.product_id == MarketplaceProduct.product_id)
|
|
||||||
.outerjoin(is_master_subquery, is_master_subquery.c.product_id == MarketplaceProduct.product_id)
|
|
||||||
.outerjoin(slaves_stock_subquery, slaves_stock_subquery.c.product_id == MarketplaceProduct.product_id)
|
|
||||||
)
|
|
||||||
print(stmt.compile(compile_kwargs={
|
|
||||||
'literal_binds': True
|
|
||||||
}))
|
|
||||||
|
|
||||||
result = await session.execute(stmt)
|
|
||||||
marketplace_products = result.all()
|
|
||||||
result = []
|
|
||||||
for marketplace_product, denco_article, price_purchase, supplier_stock, warehouse_stock, mix_stock, in_block_value, is_master, slaves_stock, price_recommended, is_archived in marketplace_products:
|
|
||||||
if is_archived or (sell_from_price > price_recommended):
|
|
||||||
result.append({
|
|
||||||
'denco_article': denco_article,
|
|
||||||
'full_stock': 0,
|
|
||||||
# 'marketplace_product': marketplace_product,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
|
|
||||||
is_mix = mix_stock is not None
|
|
||||||
|
|
||||||
in_block_value = in_block_value or 1
|
|
||||||
price_purchase = price_purchase or 0
|
|
||||||
supplier_stock = supplier_stock or 0
|
|
||||||
warehouse_stock = warehouse_stock or 0
|
|
||||||
mix_stock = mix_stock or 0
|
|
||||||
slaves_stock = slaves_stock or 0
|
|
||||||
|
|
||||||
if not sell_warehouse_products:
|
|
||||||
warehouse_stock = 0
|
|
||||||
|
|
||||||
if all([is_mix, slaves_stock > 0]):
|
|
||||||
mix_stock = 0
|
|
||||||
balance_limit = price_purchase > company.balance
|
|
||||||
|
|
||||||
if balance_limit:
|
|
||||||
supplier_stock = 0
|
|
||||||
full_stock = supplier_stock + warehouse_stock
|
|
||||||
|
|
||||||
if all([not is_mix, not sell_blocks, in_block_value > 1]):
|
|
||||||
full_stock = warehouse_stock
|
|
||||||
if sell_mixes and (not balance_limit):
|
|
||||||
full_stock += mix_stock
|
|
||||||
if (not sell_mixes) and is_master:
|
|
||||||
full_stock = warehouse_stock
|
|
||||||
if (not sell_mixes) and is_mix:
|
|
||||||
full_stock = warehouse_stock
|
|
||||||
if 45 > company.balance:
|
|
||||||
full_stock = 0
|
|
||||||
full_stock = max([0, full_stock])
|
|
||||||
|
|
||||||
result.append({
|
|
||||||
'denco_article': denco_article,
|
|
||||||
'full_stock': full_stock,
|
|
||||||
# 'marketplace_product': marketplace_product,
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root(
|
async def root(
|
||||||
session: Annotated[AsyncSession, Depends(get_session)],
|
session: Annotated[AsyncSession, Depends(get_session)],
|
||||||
@@ -312,6 +33,19 @@ async def root(
|
|||||||
return {"message": data}
|
return {"message": data}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/hello/{name}")
|
@app.post("/tasks", status_code=201)
|
||||||
async def say_hello(name: str):
|
def run_task(payload=Body(...)):
|
||||||
return {"message": f"Hello {name}"}
|
task_type = payload["type"]
|
||||||
|
task = test_task.delay()
|
||||||
|
return JSONResponse({"task_id": task.id})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/tasks/{task_id}")
|
||||||
|
def get_status(task_id):
|
||||||
|
task_result = AsyncResult(task_id)
|
||||||
|
result = {
|
||||||
|
"task_id": task_id,
|
||||||
|
"task_status": task_result.status,
|
||||||
|
"task_result": task_result.result
|
||||||
|
}
|
||||||
|
return JSONResponse(result)
|
||||||
|
|||||||
3
marketplaces/__init__.py
Normal file
3
marketplaces/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .ozon import OzonMarketplace
|
||||||
|
from .wildberries import WildberriesMarketplace
|
||||||
|
from .factory import MarketplaceFactory
|
||||||
37
marketplaces/base.py
Normal file
37
marketplaces/base.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Literal, Union
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from aiohttp import ClientResponse
|
||||||
|
|
||||||
|
from database import Marketplace
|
||||||
|
|
||||||
|
|
||||||
|
class BaseJsonMarketplace(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def __init__(self, marketplace: Marketplace):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def update_stocks(self, data: Union[list, dict]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_headers(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
@property
|
||||||
|
def api_url(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _method(self, http_method: Literal['POST', 'GET', 'PATCH', 'PUT', 'DELETE'],
|
||||||
|
method: str,
|
||||||
|
data: dict) -> ClientResponse:
|
||||||
|
async with aiohttp.ClientSession as session:
|
||||||
|
async with session.request(http_method,
|
||||||
|
f'{self.api_url}{method}',
|
||||||
|
json=data,
|
||||||
|
headers=self.get_headers()
|
||||||
|
) as response:
|
||||||
|
return response
|
||||||
19
marketplaces/factory.py
Normal file
19
marketplaces/factory.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from database import Marketplace
|
||||||
|
from database.sipro.enums.general import BaseMarketplace
|
||||||
|
from .wildberries import WildberriesMarketplace
|
||||||
|
from .ozon import OzonMarketplace
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceFactory:
|
||||||
|
@staticmethod
|
||||||
|
def get_marketplace(marketplace: Marketplace) -> Union[
|
||||||
|
WildberriesMarketplace,
|
||||||
|
OzonMarketplace,
|
||||||
|
]:
|
||||||
|
match marketplace.base_marketplace:
|
||||||
|
case BaseMarketplace.OZON:
|
||||||
|
return OzonMarketplace(marketplace)
|
||||||
|
case BaseMarketplace.WILDBERRIES:
|
||||||
|
return WildberriesMarketplace(marketplace)
|
||||||
54
marketplaces/ozon.py
Normal file
54
marketplaces/ozon.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from aiolimiter import AsyncLimiter
|
||||||
|
from asynciolimiter import StrictLimiter
|
||||||
|
|
||||||
|
import utils
|
||||||
|
from database import Marketplace
|
||||||
|
from limiter import BatchLimiter
|
||||||
|
from marketplaces.base import BaseJsonMarketplace
|
||||||
|
|
||||||
|
|
||||||
|
class OzonMarketplace(BaseJsonMarketplace):
|
||||||
|
|
||||||
|
def __init__(self, marketplace: Marketplace):
|
||||||
|
self.marketplace = marketplace
|
||||||
|
auth_data = json.loads(marketplace.auth_data)
|
||||||
|
|
||||||
|
self.headers = {
|
||||||
|
'Client-Id': auth_data.get('clientId'),
|
||||||
|
'Api-Key': auth_data.get('clientToken')
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_headers(self):
|
||||||
|
return self.headers
|
||||||
|
|
||||||
|
def api_url(self):
|
||||||
|
return 'https://api-seller.ozon.ru'
|
||||||
|
|
||||||
|
async def update_stocks(self, data: Union[list, dict]):
|
||||||
|
if type(data) is not list:
|
||||||
|
return
|
||||||
|
max_stocks = 100
|
||||||
|
chunks = utils.chunk_list(data, max_stocks)
|
||||||
|
limiter = BatchLimiter(max_requests=80,
|
||||||
|
period=60)
|
||||||
|
for chunk in chunks:
|
||||||
|
try:
|
||||||
|
await limiter.acquire()
|
||||||
|
response = await self._method('POST',
|
||||||
|
'/v2/products/stocks',
|
||||||
|
data=chunk)
|
||||||
|
response = await response.json()
|
||||||
|
# response = await
|
||||||
|
error_message = response.get('message')
|
||||||
|
error_code = response.get('code')
|
||||||
|
if error_message:
|
||||||
|
logging.warning(
|
||||||
|
f'Error occurred when sending stocks to [{self.marketplace.id}]: {error_message} ({error_code})')
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(
|
||||||
|
f'Exception occurred while sending stocks to marketplace ID [{self.marketplace.id}]: {str(e)}')
|
||||||
49
marketplaces/wildberries.py
Normal file
49
marketplaces/wildberries.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import utils
|
||||||
|
from database import Marketplace
|
||||||
|
from limiter import BatchLimiter
|
||||||
|
from marketplaces.base import BaseJsonMarketplace
|
||||||
|
|
||||||
|
|
||||||
|
class WildberriesMarketplace(BaseJsonMarketplace):
|
||||||
|
def __init__(self, marketplace: Marketplace):
|
||||||
|
self.marketplace = marketplace
|
||||||
|
auth_data = json.loads(marketplace.auth_data)
|
||||||
|
|
||||||
|
self.headers = {
|
||||||
|
'Authorization': auth_data.get('token'),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_headers(self):
|
||||||
|
return self.headers
|
||||||
|
|
||||||
|
def api_url(self):
|
||||||
|
return 'https://suppliers-api.wildberries.ru'
|
||||||
|
|
||||||
|
async def update_stocks(self, data: Union[list, dict]):
|
||||||
|
if type(data) is not list:
|
||||||
|
return
|
||||||
|
max_stocks = 1000
|
||||||
|
chunks = utils.chunk_list(data, max_stocks)
|
||||||
|
limiter = BatchLimiter(max_requests=300,
|
||||||
|
period=60)
|
||||||
|
for chunk in chunks:
|
||||||
|
try:
|
||||||
|
await limiter.acquire()
|
||||||
|
response = await self._method('PUT',
|
||||||
|
'/api/v3/stocks/{warehouseId}',
|
||||||
|
chunk)
|
||||||
|
if response.status != 204:
|
||||||
|
response = await response.json()
|
||||||
|
error_message = response.get('message')
|
||||||
|
error_code = response.get('code')
|
||||||
|
logging.warning(
|
||||||
|
f'Error occurred when sending stocks to [{self.marketplace.id}]: {error_message} ({error_code})')
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(
|
||||||
|
f'Exception occurred while sending stocks to marketplace ID [{self.marketplace.id}]: {str(e)}')
|
||||||
337
queries/general.py
Normal file
337
queries/general.py
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from sqlalchemy import select, func, and_, cast, String, case, or_
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
|
from database import DailyStock
|
||||||
|
from database.sipro import *
|
||||||
|
from database.sipro.enums.product import ProductRelationType
|
||||||
|
|
||||||
|
|
||||||
|
def get_marketplace_suppliers_and_company_warehouses(marketplace: Marketplace):
|
||||||
|
company = marketplace.company
|
||||||
|
suppliers = set()
|
||||||
|
company_warehouses = set()
|
||||||
|
for warehouse in marketplace.warehouses:
|
||||||
|
for supplier in warehouse.suppliers:
|
||||||
|
if supplier.is_pseudo:
|
||||||
|
continue
|
||||||
|
suppliers.add(supplier)
|
||||||
|
company_warehouses.update(warehouse.company_warehouses)
|
||||||
|
if marketplace.sell_warehouse_products:
|
||||||
|
company_warehouse = company.warehouse
|
||||||
|
if company_warehouse and not company.is_denco:
|
||||||
|
company_warehouses.add(company_warehouse)
|
||||||
|
return list(suppliers), list(company_warehouses)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_stocks_data(
|
||||||
|
session: AsyncSession,
|
||||||
|
marketplace: Marketplace,
|
||||||
|
product_ids: Union[list[int], None] = None
|
||||||
|
):
|
||||||
|
if not product_ids:
|
||||||
|
product_ids = []
|
||||||
|
company = marketplace.company
|
||||||
|
suppliers, company_warehouses = get_marketplace_suppliers_and_company_warehouses(marketplace)
|
||||||
|
supplier_ids = [supplier.id for supplier in suppliers]
|
||||||
|
company_warehouse_ids = [warehouse.id for warehouse in company_warehouses]
|
||||||
|
|
||||||
|
sell_mixes: bool = marketplace.sell_mixes
|
||||||
|
sell_blocks: bool = marketplace.sell_blocks
|
||||||
|
sell_warehouse_products: bool = marketplace.sell_warehouse_products
|
||||||
|
sell_from_price: int = marketplace.sell_from_price
|
||||||
|
|
||||||
|
supplier_stock_subquery = (
|
||||||
|
select(
|
||||||
|
func.greatest(
|
||||||
|
func.sum(SupplierProduct.supplier_stock) - func.coalesce(DailyStock.sold_today, 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
.label('supplier_stock'),
|
||||||
|
SupplierProduct.product_id.label('product_id')
|
||||||
|
)
|
||||||
|
.select_from(
|
||||||
|
SupplierProduct
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
Product
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
DailyStock,
|
||||||
|
DailyStock.product_id == SupplierProduct.product_id
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
SupplierProduct.supplier_id.in_(supplier_ids)
|
||||||
|
)
|
||||||
|
.group_by(
|
||||||
|
SupplierProduct.product_id
|
||||||
|
)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
|
||||||
|
warehouse_stock_subquery = (
|
||||||
|
select(
|
||||||
|
func.count(CompanyWarehouseProduct.is_sold).label('warehouse_stock'),
|
||||||
|
CompanyWarehouseProduct.product_id.label('product_id')
|
||||||
|
)
|
||||||
|
.select_from(
|
||||||
|
CompanyWarehouseProduct
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
CompanyWarehouseProduct.is_sold == False,
|
||||||
|
CompanyWarehouseProduct.company_warehouse_id.in_(company_warehouse_ids)
|
||||||
|
)
|
||||||
|
.group_by(
|
||||||
|
CompanyWarehouseProduct.product_id
|
||||||
|
)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
|
||||||
|
mix_stock_first_subquery = (
|
||||||
|
select(
|
||||||
|
func.sum(SupplierProduct.supplier_stock).label('master_stock'),
|
||||||
|
SupplierProduct.product_id.label('product_id')
|
||||||
|
)
|
||||||
|
.select_from(
|
||||||
|
SupplierProduct
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
SupplierProduct.supplier_id.in_(supplier_ids)
|
||||||
|
)
|
||||||
|
.group_by(
|
||||||
|
SupplierProduct.product_id
|
||||||
|
)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
|
||||||
|
mix_stock_full_subquery = (
|
||||||
|
select(
|
||||||
|
func.min(SupplierProduct.in_block).label('mix_stock'),
|
||||||
|
Product.id.label('product_id')
|
||||||
|
)
|
||||||
|
.select_from(
|
||||||
|
Product
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
SupplierProduct
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
ProductRelation,
|
||||||
|
Product.id == ProductRelation.slave_product_id
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
mix_stock_first_subquery,
|
||||||
|
mix_stock_first_subquery.c.product_id == ProductRelation.master_product_id
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
ProductRelation.relation_type == ProductRelationType.MAIN_PRODUCT,
|
||||||
|
mix_stock_first_subquery.c.master_stock > 0
|
||||||
|
)
|
||||||
|
.group_by(
|
||||||
|
Product.id
|
||||||
|
)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
|
||||||
|
is_master_first_subquery = (
|
||||||
|
select(
|
||||||
|
ProductRelation.master_product_id,
|
||||||
|
(func.count(ProductRelation.master_product_id) > 0).label('is_master')
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
ProductRelation.relation_type == ProductRelationType.MAIN_PRODUCT
|
||||||
|
)
|
||||||
|
.group_by(
|
||||||
|
ProductRelation.master_product_id
|
||||||
|
)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
|
||||||
|
is_master_subquery = (
|
||||||
|
select(
|
||||||
|
Product.id.label('product_id'),
|
||||||
|
func.coalesce(is_master_first_subquery.c.is_master, False).label('is_master')
|
||||||
|
)
|
||||||
|
.select_from(
|
||||||
|
Product
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
is_master_first_subquery,
|
||||||
|
Product.id == is_master_first_subquery.c.master_product_id
|
||||||
|
)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
|
||||||
|
in_block_subquery = (
|
||||||
|
select(
|
||||||
|
Product.id.label('product_id'),
|
||||||
|
func.min(SupplierProduct.in_block).label('in_block_value')
|
||||||
|
)
|
||||||
|
.select_from(
|
||||||
|
Product
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
SupplierProduct
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
SupplierProduct.supplier_id.in_(supplier_ids),
|
||||||
|
SupplierProduct.supplier_stock > 0
|
||||||
|
)
|
||||||
|
.group_by(
|
||||||
|
Product.id
|
||||||
|
)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
slaves_stock_first_subquery = (
|
||||||
|
select(
|
||||||
|
ProductRelation.master_product_id.label('product_id'),
|
||||||
|
func.sum(SupplierProduct.supplier_stock).label('slaves_stock')
|
||||||
|
)
|
||||||
|
.select_from(
|
||||||
|
ProductRelation
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
SupplierProduct,
|
||||||
|
and_(
|
||||||
|
ProductRelation.slave_product_id == SupplierProduct.product_id,
|
||||||
|
ProductRelation.relation_type == ProductRelationType.SAME_MIX
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
SupplierProduct.supplier_id.in_(
|
||||||
|
supplier_ids
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.group_by(
|
||||||
|
ProductRelation.master_product_id
|
||||||
|
)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
|
||||||
|
slaves_stock_subquery = (
|
||||||
|
select(
|
||||||
|
Product.id.label('product_id'),
|
||||||
|
slaves_stock_first_subquery.c.slaves_stock.label('slaves_stock')
|
||||||
|
)
|
||||||
|
.select_from(
|
||||||
|
Product
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
slaves_stock_first_subquery,
|
||||||
|
slaves_stock_first_subquery.c.product_id == Product.id
|
||||||
|
)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
|
||||||
|
stmt = (
|
||||||
|
select(
|
||||||
|
MarketplaceProduct,
|
||||||
|
func.coalesce(Product.article, cast(Product.denco_article, String)).label('denco_article'),
|
||||||
|
func.coalesce(MarketplaceProduct.mp_price_bought, 0).label('price_purchase'),
|
||||||
|
func.coalesce(supplier_stock_subquery.c.supplier_stock, 0).label('supplier_stock'),
|
||||||
|
case(
|
||||||
|
(
|
||||||
|
sell_warehouse_products,
|
||||||
|
func.coalesce(warehouse_stock_subquery.c.warehouse_stock, 0)
|
||||||
|
),
|
||||||
|
else_=0)
|
||||||
|
.label('warehouse_stock'),
|
||||||
|
func.coalesce(mix_stock_full_subquery.c.mix_stock, 0).label('mix_stock'),
|
||||||
|
func.coalesce(in_block_subquery.c.in_block_value, 1).label('in_block_value'),
|
||||||
|
is_master_subquery.c.is_master.label('is_master'),
|
||||||
|
func.coalesce(slaves_stock_subquery.c.slaves_stock, 0).label('slaves_stock'),
|
||||||
|
MarketplaceProduct.price_recommended.label('price_recommended'),
|
||||||
|
MarketplaceProduct.is_archived.label('is_archived')
|
||||||
|
)
|
||||||
|
.select_from(
|
||||||
|
MarketplaceProduct
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
Product
|
||||||
|
)
|
||||||
|
.options(
|
||||||
|
joinedload(MarketplaceProduct.product)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
MarketplaceProduct.marketplace_id == marketplace.id,
|
||||||
|
or_(
|
||||||
|
len(product_ids) == 0,
|
||||||
|
MarketplaceProduct.product_id.in_(product_ids)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
supplier_stock_subquery,
|
||||||
|
supplier_stock_subquery.c.product_id == MarketplaceProduct.product_id
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
warehouse_stock_subquery,
|
||||||
|
warehouse_stock_subquery.c.product_id == MarketplaceProduct.product_id
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
mix_stock_full_subquery,
|
||||||
|
mix_stock_full_subquery.c.product_id == MarketplaceProduct.product_id
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
in_block_subquery,
|
||||||
|
in_block_subquery.c.product_id == MarketplaceProduct.product_id
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
is_master_subquery,
|
||||||
|
is_master_subquery.c.product_id == MarketplaceProduct.product_id
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
slaves_stock_subquery,
|
||||||
|
slaves_stock_subquery.c.product_id == MarketplaceProduct.product_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
marketplace_products = result.all()
|
||||||
|
result = []
|
||||||
|
for (marketplace_product,
|
||||||
|
denco_article,
|
||||||
|
price_purchase,
|
||||||
|
supplier_stock,
|
||||||
|
warehouse_stock,
|
||||||
|
mix_stock,
|
||||||
|
in_block_value,
|
||||||
|
is_master,
|
||||||
|
slaves_stock,
|
||||||
|
price_recommended,
|
||||||
|
is_archived) in marketplace_products:
|
||||||
|
if is_archived or (sell_from_price > price_recommended):
|
||||||
|
result.append({
|
||||||
|
'denco_article': denco_article,
|
||||||
|
'full_stock': 0,
|
||||||
|
'marketplace_product': marketplace_product,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
is_mix = mix_stock is not None
|
||||||
|
if all([is_mix, slaves_stock > 0]):
|
||||||
|
mix_stock = 0
|
||||||
|
balance_limit = price_purchase > company.balance
|
||||||
|
|
||||||
|
if balance_limit:
|
||||||
|
supplier_stock = 0
|
||||||
|
full_stock = supplier_stock + warehouse_stock
|
||||||
|
|
||||||
|
if all([not is_mix, not sell_blocks, in_block_value > 1]):
|
||||||
|
full_stock = warehouse_stock
|
||||||
|
if sell_mixes and (not balance_limit):
|
||||||
|
full_stock += mix_stock
|
||||||
|
if (not sell_mixes) and is_master:
|
||||||
|
full_stock = warehouse_stock
|
||||||
|
if (not sell_mixes) and is_mix:
|
||||||
|
full_stock = warehouse_stock
|
||||||
|
if 45 > company.balance:
|
||||||
|
full_stock = 0
|
||||||
|
full_stock = max([0, full_stock])
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
'denco_article': denco_article,
|
||||||
|
'full_stock': full_stock,
|
||||||
|
'marketplace_product': marketplace_product,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -10,4 +10,4 @@ alembic
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
aiohttp
|
aiohttp
|
||||||
aiohttp[speedups]
|
aiohttp[speedups]
|
||||||
celery[redis]
|
celery[redis]
|
||||||
|
|||||||
1
start_celery.sh
Normal file
1
start_celery.sh
Normal file
@@ -0,0 +1 @@
|
|||||||
|
celery -A background.celery worker --loglevel=info
|
||||||
8
test.py
8
test.py
@@ -1,3 +1,7 @@
|
|||||||
from alembic.env import run_migrations_offline
|
a = [
|
||||||
|
(1, [1, 4, 5, ]),
|
||||||
|
(2, [1, 2, 3, 4, 5, ]),
|
||||||
|
]
|
||||||
|
|
||||||
run_migrations_offline()
|
updates_list = sorted(a, key=lambda x: x[1], reverse=True)
|
||||||
|
print(updates_list)
|
||||||
|
|||||||
0
updaters/__init__.py
Normal file
0
updaters/__init__.py
Normal file
15
updaters/base.py
Normal file
15
updaters/base.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from database import Marketplace
|
||||||
|
from updaters.stocks_updater import StockUpdate
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMarketplaceUpdater(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def __init__(self, marketplace: Marketplace):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def update(self, updates: List[StockUpdate]):
|
||||||
|
pass
|
||||||
14
updaters/ozon_updater.py
Normal file
14
updaters/ozon_updater.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from database import Marketplace
|
||||||
|
from marketplaces import MarketplaceFactory, OzonMarketplace
|
||||||
|
from updaters.base import BaseMarketplaceUpdater
|
||||||
|
from updaters.stocks_updater import StockUpdate
|
||||||
|
|
||||||
|
|
||||||
|
class OzonUpdater(BaseMarketplaceUpdater):
|
||||||
|
def __init__(self, marketplace: Marketplace):
|
||||||
|
self.ozon_marketplace: OzonMarketplace = MarketplaceFactory.get_marketplace(marketplace)
|
||||||
|
|
||||||
|
async def update(self, updates: List[StockUpdate]):
|
||||||
|
pass
|
||||||
83
updaters/stocks_updater.py
Normal file
83
updaters/stocks_updater.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import unique, IntEnum
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.dialects.postgresql import insert
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
import database
|
||||||
|
from database import Marketplace, MarketplaceProduct, DailyStock
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class StockUpdateType(IntEnum):
|
||||||
|
SALE = 0
|
||||||
|
SUPPLIER_UPDATE = 1
|
||||||
|
WAREHOUSE_UPDATE = 2
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StockUpdate:
|
||||||
|
product_id: int
|
||||||
|
type: StockUpdateType
|
||||||
|
quantity: int
|
||||||
|
|
||||||
|
|
||||||
|
class StocksUpdater:
|
||||||
|
def __init__(self, session: AsyncSession):
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
async def update_marketplace(self, marketplace_id: int, updates: List[StockUpdate]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def update(self, updates: list[StockUpdate]):
|
||||||
|
updates_dict = defaultdict(list)
|
||||||
|
stock_update_values = []
|
||||||
|
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
|
||||||
|
stmt = (
|
||||||
|
select(
|
||||||
|
MarketplaceProduct.marketplace_id.distinct()
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
MarketplaceProduct.product_id == update.product_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stmt_result = await self.session.execute(stmt)
|
||||||
|
marketplace_ids = stmt_result.scalars().all()
|
||||||
|
if not marketplace_ids:
|
||||||
|
continue
|
||||||
|
for marketplace_id in marketplace_ids:
|
||||||
|
updates_dict[marketplace_id].append(update)
|
||||||
|
updates_list = list(updates_dict.items())
|
||||||
|
updates_list = sorted(updates_list, key=lambda x: 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()
|
||||||
|
|
||||||
|
for marketplace_id, marketplace_updates in updates_list:
|
||||||
|
await self.update_marketplace(marketplace_id, marketplace_updates)
|
||||||
1
updaters/wildberries_updater.py
Normal file
1
updaters/wildberries_updater.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Reference in New Issue
Block a user