Добавил новый метод get_tree_categories. Возвращает дерево категорий.

This commit is contained in:
Павел Синицин 2024-06-23 17:18:53 +03:00
parent c16785509e
commit 4cc7a53bc9
Signed by: spavelit
GPG Key ID: 2FEC8CEAE5A95DD1
7 changed files with 338 additions and 217 deletions

View File

@ -1,5 +1,14 @@
import requests
from bpiek import models
from bpiek.models.product import Product
from bpiek.models.category import Category
from bpiek.models.response import (
ParentCategoriesResponse,
CategoriesAndProductsBySlugParentCategory,
NewProductsResponse,
RemainsAndPlanresiduesResponse,
TreeCategoriesResponse,
)
AUTH_URL = "https://bp.iek.ru/oauth/login"
API_URL = "https://bp.iek.ru/api/catalog/v1/"
@ -9,7 +18,7 @@ class BPIekApi:
def __init__(self) -> None:
self.session = requests.Session()
def _instance(self, endpoint, params: dict = {}):
def _instance(self, endpoint: str, params: dict = {}):
response = self.session.get(
url=API_URL + endpoint,
headers={"Content-Type": "application/json"},
@ -17,53 +26,54 @@ class BPIekApi:
)
return response.json()
def login(self, username, password) -> None:
def login(self, username: str, password: str) -> None:
auth = self.session.post(
url=f"{AUTH_URL}",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={"username": username, "password": password},
)
def get_parent_categories(self) -> list[models.Category] | models.Error:
if len(auth.cookies) == 0:
raise Exception("Invalid username or password")
def get_parent_categories(self) -> list[Category]:
response = self._instance("client/catalog")
try:
result: models.ParentCategoriesResponse = (
models.ParentCategoriesResponse.model_validate(response)
result: ParentCategoriesResponse = ParentCategoriesResponse.model_validate(
response
)
return result.categories
except Exception as e:
return models.Error(code=400, message=str(e))
raise Exception(e)
def get_product_by_article(self, article: str) -> models.Product | models.Error:
def get_product_by_article(self, article: str) -> Product:
response = self._instance(f"client/products/{article}")
try:
result: models.Product = models.Product.model_validate(response)
result: Product = Product.model_validate(response)
return result
except Exception as e:
return models.Error(code=400, message=str(e))
raise Exception(e)
def get_categories_and_products_by_slug_parent_category(
self, slug
) -> models.CategoriesAndProductsBySlugParentCategory | models.Error:
self, slug: str
) -> CategoriesAndProductsBySlugParentCategory:
response = self._instance(f"client/category/{slug}/json")
try:
result: models.CategoriesAndProductsBySlugParentCategory = (
models.CategoriesAndProductsBySlugParentCategory.model_validate(
response
)
result: CategoriesAndProductsBySlugParentCategory = (
CategoriesAndProductsBySlugParentCategory.model_validate(response)
)
return result
except Exception as e:
return models.Error(code=400, message=str(e))
raise Exception(e)
def get_new_products(
self,
@ -71,33 +81,112 @@ class BPIekApi:
sortOrder: str = "asc",
pageSize: int = 10,
page: int = 1,
) -> models.NewProductsResponse | models.Error:
) -> NewProductsResponse:
response = self._instance(
"new-products",
{sortBy: sortBy, sortOrder: sortOrder, pageSize: pageSize, page: page},
)
try:
result: models.NewProductsResponse = (
models.NewProductsResponse.model_validate(response)
)
result: NewProductsResponse = NewProductsResponse.model_validate(response)
return result
except Exception as e:
return models.Error(code=400, message=str(e))
raise Exception(e)
def get_remains_and_planresidues(
self, slug
) -> models.RemainsAndPlanresiduesResponse | models.Error:
def get_remains_and_planresidues(self, slug: str) -> RemainsAndPlanresiduesResponse:
response = self._instance(f"client/category/{slug}/balances-json")
try:
result: models.RemainsAndPlanresiduesResponse = (
models.RemainsAndPlanresiduesResponse.model_validate(response)
result: RemainsAndPlanresiduesResponse = (
RemainsAndPlanresiduesResponse.model_validate(response)
)
return result
except Exception as e:
return models.Error(code=400, message=str(e))
raise Exception(e)
def get_tree_categories(self) -> list[Category]:
tree = {}
def convert_to_nested_list(nested_dict: dict) -> list:
result = []
for key, value in nested_dict.items():
if isinstance(value, dict):
try:
result.append(
{
"name": value["name"],
"slug": value["slug"],
"url": value["url"],
"childs": convert_to_nested_list(value["childs"]),
}
)
except:
continue
else:
result.append(value)
return result
def convert_list_to_nested_dict(_list: list):
def update_nested(d, keys, value):
for key in keys[:-1]:
d = d.setdefault(key, {})
d[keys[-1]] = value
tmp_categories = _list.copy()
for idx, category in enumerate(tmp_categories):
paths = category.url[1:][:-1].split("/")
_str = ""
for _idx, path in enumerate(paths):
if len(paths) == _idx + 1:
_str += f"{path}"
break
_str += f"{path},childs,"
try:
update_nested(
tree,
_str.split(","),
{
"name": category.name,
"slug": category.slug,
"url": category.url,
"childs": {},
},
)
tmp_categories.pop(idx)
except Exception as e:
print(e)
continue
return tmp_categories
for parent_category in self.get_parent_categories():
tree[parent_category.slug] = {
"name": parent_category.name,
"slug": parent_category.slug,
"url": parent_category.url,
"childs": {},
}
categories_and_products = (
self.get_categories_and_products_by_slug_parent_category(
slug=parent_category.slug
)
)
tmp_categories = convert_list_to_nested_dict(
categories_and_products.categories
)
while len(tmp_categories) != 0:
tmp_categories = convert_list_to_nested_dict(tmp_categories)
break
return TreeCategoriesResponse.model_validate(
{"tree": convert_to_nested_list(tree)}
).tree

View File

@ -1,188 +0,0 @@
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from pydantic.types import UUID1
class Category(BaseModel):
slug: str # Слаг категории
name: str # Название категории
url: str # Относительный адрес категории
apiUrl: str # Ссылка на скачивание файла с содержимым категории
class ProductShort(BaseModel):
class WarehouseData(BaseModel):
class Incoming(BaseModel):
dateBegan: str | None
dateEnd: str | None
amount: int
type: str # Enum: "production" "shipping" Тип поступления, production - поступление после производства, shipping - доставка на склад
warehouseId: UUID1
warehouseName: str
availableAmount: int
incoming: list[Incoming]
article: str # Артикул товара
name: str # Полное наименование товара
multiplicity: int | None # Кратность продажи
priceBase: float | None # Базовая цена с НДС
priceRrc: float | None # Рекомендованная розничная цена (РРЦ) с НДС
available: int | None # Значение остатка
units: str | None # Единицы измерения
warehouseData: list[WarehouseData]
class Product(ProductShort):
class ImageVariant(BaseModel):
url: str # Ссылка
ext: str # Расширение
width: int # Ширина
class Etim(BaseModel):
class EtimClass(BaseModel):
id: str
name: str # Название класса
class EtimFeatures(BaseModel):
id: str
name: str # Название свойства
sort: int | None # Порядок сортировки по умолчанию
unit: str | None # Единицы измерения
value: str # Значение свойства
value_union: str # Код значения
etim_class: EtimClass = Field(alias="class")
features: list[EtimFeatures]
class Complects(BaseModel):
article: str # Артикул
name: str # Наименование
quantity: int # Количество
class LeftPeriod(BaseModel):
name: str # Название характеристики
value: str # Значение характеристики
class LeftPeriodRaw(BaseModel):
class Lifespan(BaseModel):
limit: str | None
value: str | None
units: str | None
class Warranty(BaseModel):
value: str | None
units: str | None
lifespan: Lifespan
warranty: Warranty
class LogisticParams(BaseModel):
class Value(BaseModel):
group: str | None
individual: str | None
transport: str | None
name: str
nameOrig: str
value: Value
class LogisticParamsData(BaseModel):
class SinglePackage(BaseModel):
multiplicity: int | None
unit: str | None
singlePackage: SinglePackage
class DesignFeatures(BaseModel):
imageUrl: str
description: str
class Videos(BaseModel):
name: str
description: str
url: str
type: str # Enum: "url" "file"
class Software(BaseModel):
name: str
description: str
url: str
size: str
shortName: str # Краткое название
description: str | None # Описание
categoryName: str | None # Название категории
category: str # Относительный путь до категории в каталоге
slug: str # Слаг товара
tm: str # Торговая марка
url: str # Ссылка на товар
isArchived: bool # Архивный или нет
imageUrl: str # Фото товара (основное)
imageUrls: list[str] # Все фото товара
imageVariants: list[ImageVariant] # Все вариации изображений
advantages: str | None # Преимущества
etim: Etim # EIM характеристики товара
complects: list[Complects] # Комплектация и сопутствующие товары
complectations: str | None # Комплектация
files: list[Any] # Список файлов, относящихся к товару (ГЧ, КД, CAD-модели и т.д.)
leftPeriod: list[LeftPeriod] | None # Характеристики срока службы
leftPeriodRaw: LeftPeriodRaw # Гарантийные показатели
logisticParams: list[LogisticParams] # Логистические характеристики
logisticParamsData: (
LogisticParamsData | None
) # Подробные логистические характеристики
novelty: bool # Новинка или нет
designFeatures: list[DesignFeatures] # Отличительные особенности
videos: list[Videos] # Видео по товару
software: list[Software] # ПО по товару
banner: str | None # Текст баннера
lastModified: str | None # Дата последнего изменения
countryOfProduction: str | None # Страна производства
firstSaleDate: str | None # Дата начала продаж
feacn: str | None # Код ТН ВЭД
family: str | Any | None
series: str | Any | None
indPacking: list[str] # Ссылки на фото упаковки
analogs: list["Product"] # Аналоги
related: list["Product"] # Совместно применяемые изделия
qrCode: str | None = Field(default=None)
isOutOfAssortment: bool
isOutOfProduction: bool
class ParentCategoriesResponse(BaseModel):
categories: list[Category]
class CategoriesAndProductsBySlugParentCategory(BaseModel):
date: str
slug: str
name: str
url: str
categories: list[Category]
products: list[Product]
class NewProductsResponse(BaseModel):
class Data(BaseModel):
products: list[Product]
class Meta(BaseModel):
page: str
totalPages: int
totalCount: int
pageSize: int
data: Data
_meta: Meta
class RemainsAndPlanresiduesResponse(BaseModel):
date: str
products: list[ProductShort]
class Error(BaseModel):
code: int
message: str

13
bpiek/models/category.py Normal file
View File

@ -0,0 +1,13 @@
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from pydantic.types import UUID1
class Category(BaseModel):
slug: str # Слаг категории
name: str # Название категории
url: str # Относительный адрес категории
apiUrl: str | None = Field(
default=None
) # Ссылка на скачивание файла с содержимым категории
childs: list["Category"] | None = Field(default=[])

167
bpiek/models/product.py Normal file
View File

@ -0,0 +1,167 @@
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from pydantic.types import UUID1
class WarehouseData(BaseModel):
class Incoming(BaseModel):
dateBegan: str | None
dateEnd: str | None
amount: int | float
type: str # Enum: "production" "shipping" Тип поступления, production - поступление после производства, shipping - доставка на склад
warehouseId: UUID1
warehouseName: str
availableAmount: int
incoming: list[Incoming]
class ImageVariant(BaseModel):
url: str # Ссылка
ext: str # Расширение
width: int # Ширина
class Etim(BaseModel):
class EtimClass(BaseModel):
id: str
name: str # Название класса
class EtimFeatures(BaseModel):
id: str
name: str # Название свойства
sort: int | None # Порядок сортировки по умолчанию
unit: str | None # Единицы измерения
value: str # Значение свойства
value_union: str # Код значения
etim_class: EtimClass = Field(alias="class")
features: list[EtimFeatures]
class Complects(BaseModel):
article: str # Артикул
name: str # Наименование
quantity: int # Количество
class LeftPeriod(BaseModel):
name: str # Название характеристики
value: str | None # Значение характеристики
class LeftPeriodRaw(BaseModel):
class Lifespan(BaseModel):
limit: str | None
value: str | None
units: str | None
class Warranty(BaseModel):
value: str | None
units: str | None
lifespan: Lifespan
warranty: Warranty
class LogisticParams(BaseModel):
class Value(BaseModel):
group: str | None
individual: str | None
transport: str | None
name: str
nameOrig: str
value: Value
class LogisticParamsData(BaseModel):
class SinglePackage(BaseModel):
multiplicity: int | None
unit: str | None
singlePackage: SinglePackage
class DesignFeatures(BaseModel):
imageUrl: str
description: str
class Videos(BaseModel):
name: str
description: str | None
url: str
type: str # Enum: "url" "file"
class Software(BaseModel):
name: str
description: str
url: str
size: int
class AnalogProduct(BaseModel):
article: str # Артикул товара
name: str # Полное наименование товара
shortName: str # Краткое название
description: str | None # Описание
imageUrl: str # Фото товара (основное)
imageUrls: list[str] # Все фото товара
imageVariants: list[ImageVariant] # Все вариации изображений
isArchived: bool # Архивный или нет
tm: str # Торговая марка
class ShortProduct(BaseModel):
article: str # Артикул товара
name: str # Полное наименование товара
multiplicity: int | None # Кратность продажи
priceBase: float | None # Базовая цена с НДС
priceRrc: float | None # Рекомендованная розничная цена (РРЦ) с НДС
available: int | None # Значение остатка
units: str | None # Единицы измерения
warehouseData: list[WarehouseData]
class Product(ShortProduct):
shortName: str # Краткое название
description: str | None # Описание
categoryName: str | None # Название категории
category: str # Относительный путь до категории в каталоге
slug: str # Слаг товара
tm: str # Торговая марка
url: str # Ссылка на товар
isArchived: bool # Архивный или нет
imageUrl: str # Фото товара (основное)
imageUrls: list[str] # Все фото товара
imageVariants: list[ImageVariant] # Все вариации изображений
advantages: str | None # Преимущества
etim: Etim | None # EIM характеристики товара
complects: list[Complects] # Комплектация и сопутствующие товары
complectations: str | None # Комплектация
files: list[Any] # Список файлов, относящихся к товару (ГЧ, КД, CAD-модели и т.д.)
leftPeriod: list[LeftPeriod] | None # Характеристики срока службы
leftPeriodRaw: LeftPeriodRaw | None # Гарантийные показатели
logisticParams: list[LogisticParams] # Логистические характеристики
logisticParamsData: (
LogisticParamsData | None
) # Подробные логистические характеристики
novelty: bool # Новинка или нет
designFeatures: list[DesignFeatures] # Отличительные особенности
videos: list[Videos] # Видео по товару
software: list[Software] # ПО по товару
banner: str | None # Текст баннера
lastModified: str | None # Дата последнего изменения
countryOfProduction: str | None # Страна производства
firstSaleDate: str | None # Дата начала продаж
feacn: str | None # Код ТН ВЭД
family: str | Any | None = Field(default=None)
series: str | Any | None = Field(default=None)
indPacking: list[str] # Ссылки на фото упаковки
analogs: list[AnalogProduct] # Аналоги
related: list[AnalogProduct] # Совместно применяемые изделия
qrCode: str | None = Field(default=None)
isOutOfAssortment: bool
isOutOfProduction: bool

39
bpiek/models/response.py Normal file
View File

@ -0,0 +1,39 @@
from pydantic import BaseModel, ConfigDict, Field
from .category import Category
from .product import Product, ShortProduct
class ParentCategoriesResponse(BaseModel):
categories: list[Category]
class CategoriesAndProductsBySlugParentCategory(BaseModel):
date: str
slug: str
name: str
url: str
categories: list[Category]
products: list[Product]
class NewProductsResponse(BaseModel):
class Data(BaseModel):
products: list[Product]
class Meta(BaseModel):
page: str
totalPages: int
totalCount: int
pageSize: int
data: Data
_meta: Meta
class RemainsAndPlanresiduesResponse(BaseModel):
date: str
products: list[ShortProduct]
class TreeCategoriesResponse(BaseModel):
tree: list[Category]

1
bpiek/utils.py Normal file
View File

@ -0,0 +1 @@

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "bpiek"
version = "1.0.0"
version = "1.1.0"
description = "API клиент для Бизнес-платформа IEK"
authors = ["Pavel Sinitsin <spavelit@list.ru>"]
readme = "README.md"