Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
145
bpiek/api.py
145
bpiek/api.py
|
@ -1,14 +1,5 @@
|
|||
import requests
|
||||
from bpiek.models.product import Product
|
||||
from bpiek.models.category import Category
|
||||
from bpiek.models.response import (
|
||||
ParentCategoriesResponse,
|
||||
CategoriesAndProductsBySlugParentCategory,
|
||||
NewProductsResponse,
|
||||
RemainsAndPlanresiduesResponse,
|
||||
TreeCategoriesResponse,
|
||||
)
|
||||
|
||||
from bpiek import models
|
||||
|
||||
AUTH_URL = "https://bp.iek.ru/oauth/login"
|
||||
API_URL = "https://bp.iek.ru/api/catalog/v1/"
|
||||
|
@ -18,7 +9,7 @@ class BPIekApi:
|
|||
def __init__(self) -> None:
|
||||
self.session = requests.Session()
|
||||
|
||||
def _instance(self, endpoint: str, params: dict = {}):
|
||||
def _instance(self, endpoint, params: dict = {}):
|
||||
response = self.session.get(
|
||||
url=API_URL + endpoint,
|
||||
headers={"Content-Type": "application/json"},
|
||||
|
@ -26,54 +17,53 @@ class BPIekApi:
|
|||
)
|
||||
return response.json()
|
||||
|
||||
def login(self, username: str, password: str) -> None:
|
||||
def login(self, username, password) -> None:
|
||||
auth = self.session.post(
|
||||
url=f"{AUTH_URL}",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
data={"username": username, "password": password},
|
||||
)
|
||||
|
||||
if len(auth.cookies) == 0:
|
||||
raise Exception("Invalid username or password")
|
||||
|
||||
def get_parent_categories(self) -> list[Category]:
|
||||
def get_parent_categories(self) -> list[models.Category] | models.Error:
|
||||
response = self._instance("client/catalog")
|
||||
|
||||
try:
|
||||
result: ParentCategoriesResponse = ParentCategoriesResponse.model_validate(
|
||||
response
|
||||
result: models.ParentCategoriesResponse = (
|
||||
models.ParentCategoriesResponse.model_validate(response)
|
||||
)
|
||||
|
||||
return result.categories
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(e)
|
||||
return models.Error(code=400, message=str(e))
|
||||
|
||||
def get_product_by_article(self, article: str) -> Product:
|
||||
def get_product_by_article(self, article: str) -> models.Product | models.Error:
|
||||
response = self._instance(f"client/products/{article}")
|
||||
|
||||
try:
|
||||
result: Product = Product.model_validate(response)
|
||||
result: models.Product = models.Product.model_validate(response)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(e)
|
||||
return models.Error(code=400, message=str(e))
|
||||
|
||||
def get_categories_and_products_by_slug_parent_category(
|
||||
self, slug: str
|
||||
) -> CategoriesAndProductsBySlugParentCategory:
|
||||
self, slug
|
||||
) -> models.CategoriesAndProductsBySlugParentCategory | models.Error:
|
||||
response = self._instance(f"client/category/{slug}/json")
|
||||
|
||||
try:
|
||||
result: CategoriesAndProductsBySlugParentCategory = (
|
||||
CategoriesAndProductsBySlugParentCategory.model_validate(response)
|
||||
result: models.CategoriesAndProductsBySlugParentCategory = (
|
||||
models.CategoriesAndProductsBySlugParentCategory.model_validate(
|
||||
response
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(e)
|
||||
return models.Error(code=400, message=str(e))
|
||||
|
||||
def get_new_products(
|
||||
self,
|
||||
|
@ -81,112 +71,33 @@ class BPIekApi:
|
|||
sortOrder: str = "asc",
|
||||
pageSize: int = 10,
|
||||
page: int = 1,
|
||||
) -> NewProductsResponse:
|
||||
) -> models.NewProductsResponse | models.Error:
|
||||
response = self._instance(
|
||||
"new-products",
|
||||
{sortBy: sortBy, sortOrder: sortOrder, pageSize: pageSize, page: page},
|
||||
)
|
||||
|
||||
try:
|
||||
result: NewProductsResponse = NewProductsResponse.model_validate(response)
|
||||
result: models.NewProductsResponse = (
|
||||
models.NewProductsResponse.model_validate(response)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(e)
|
||||
return models.Error(code=400, message=str(e))
|
||||
|
||||
def get_remains_and_planresidues(self, slug: str) -> RemainsAndPlanresiduesResponse:
|
||||
def get_remains_and_planresidues(
|
||||
self, slug
|
||||
) -> models.RemainsAndPlanresiduesResponse | models.Error:
|
||||
response = self._instance(f"client/category/{slug}/balances-json")
|
||||
|
||||
try:
|
||||
result: RemainsAndPlanresiduesResponse = (
|
||||
RemainsAndPlanresiduesResponse.model_validate(response)
|
||||
result: models.RemainsAndPlanresiduesResponse = (
|
||||
models.RemainsAndPlanresiduesResponse.model_validate(response)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as 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
|
||||
return models.Error(code=400, message=str(e))
|
||||
|
|
188
bpiek/models.py
Normal file
188
bpiek/models.py
Normal file
|
@ -0,0 +1,188 @@
|
|||
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
|
|
@ -1,13 +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 | None = Field(
|
||||
default=None
|
||||
) # Ссылка на скачивание файла с содержимым категории
|
||||
childs: list["Category"] | None = Field(default=[])
|
|
@ -1,167 +0,0 @@
|
|||
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
|
|
@ -1,39 +0,0 @@
|
|||
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 +0,0 @@
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "bpiek"
|
||||
version = "1.1.0"
|
||||
version = "1.0.0"
|
||||
description = "API клиент для Бизнес-платформа IEK"
|
||||
authors = ["Pavel Sinitsin <spavelit@list.ru>"]
|
||||
readme = "README.md"
|
||||
|
|
Loading…
Reference in New Issue
Block a user