From 6d5a19c3b09eab4095abd22cacfc6c9630b0acd7 Mon Sep 17 00:00:00 2001 From: Pavel Sinitsin Date: Sat, 22 Jun 2024 18:46:21 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D1=8B=20endpoint=20=D0=B8=D0=B7=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20BP=20IEK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + README.md | 0 bpiek/__init__.py | 1 + bpiek/api.py | 125 ++++++++++++++++++++++++++++++ bpiek/models.py | 192 ++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 21 +++++ tests/__init__.py | 0 7 files changed, 343 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bpiek/__init__.py create mode 100644 bpiek/api.py create mode 100644 bpiek/models.py create mode 100644 pyproject.toml create mode 100644 tests/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5208b7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.venv/ +__pycache__/ +.env +main.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/bpiek/__init__.py b/bpiek/__init__.py new file mode 100644 index 0000000..2e977b3 --- /dev/null +++ b/bpiek/__init__.py @@ -0,0 +1 @@ +from .api import (BPIekApi, AUTH_URL, API_URL) diff --git a/bpiek/api.py b/bpiek/api.py new file mode 100644 index 0000000..5a2184d --- /dev/null +++ b/bpiek/api.py @@ -0,0 +1,125 @@ + +import requests +from bpiek import models + +AUTH_URL = "https://bp.iek.ru/oauth/login" +API_URL = "https://bp.iek.ru/api/catalog/v1/" + +class BPIekApi: + def __init__(self, username, password) -> None: + self.session = requests.Session() + self.username = username + self.password = password + + self._login() + + def _login(self) -> None: + auth = self.session.post( + url=f"{AUTH_URL}", + headers={ + "Content-Type": "application/x-www-form-urlencoded" + }, + data={ + "username": self.username, + "password": self.password + } + ) + + def _instance(self, endpoint, params: dict = {}): + response = self.session.get( + url=API_URL + endpoint, + headers={ + "Content-Type": "application/json" + }, + params={ + "format": "json", + **params + } + ) + return response.json() + + def get_parent_categories(self) -> list[models.Category] | models.Error: + response = self._instance("client/catalog") + + try: + result: models.ParentCategoriesResponse = ( + models + .ParentCategoriesResponse + .model_validate(response) + ) + + return result.categories + + except Exception as e: + return models.Error(code=400, message=str(e)) + + def get_product_by_article(self, article: str) -> models.Product | models.Error: + response = self._instance(f"client/products/{article}") + + try: + result: models.Product = ( + models + .Product + .model_validate(response) + ) + + return result + + except Exception as e: + return models.Error(code=400, message=str(e)) + + def get_categories_and_products_by_slug_parent_category(self, slug) -> models.CategoriesAndProductsBySlugParentCategory | models.Error: + response = self._instance(f"client/category/{slug}/json") + + try: + result: models.CategoriesAndProductsBySlugParentCategory = ( + models + .CategoriesAndProductsBySlugParentCategory + .model_validate(response) + ) + + return result + + except Exception as e: + return models.Error(code=400, message=str(e)) + + def get_new_products( + self, + sortBy: str = "article", + sortOrder: str = "asc", + pageSize: int = 10, + page: int = 1 + ) -> models.NewProductsResponse | models.Error: + response = self._instance("new-products", { + sortBy: sortBy, + sortOrder: sortOrder, + pageSize: pageSize, + page: page + }) + + try: + result: models.NewProductsResponse = ( + models + .NewProductsResponse + .model_validate(response) + ) + + return result + + except Exception as e: + return models.Error(code=400, message=str(e)) + + def get_remains_and_planresidues(self, slug) -> models.RemainsAndPlanresiduesResponse | models.Error: + response = self._instance(f"client/category/{slug}/balances-json") + + try: + result: models.RemainsAndPlanresiduesResponse = ( + models + .RemainsAndPlanresiduesResponse + .model_validate(response) + ) + + return result + + except Exception as e: + return models.Error(code=400, message=str(e)) diff --git a/bpiek/models.py b/bpiek/models.py new file mode 100644 index 0000000..907f378 --- /dev/null +++ b/bpiek/models.py @@ -0,0 +1,192 @@ +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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..98e4e5e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[tool.poetry] +name = "iek-python" +version = "0.1.0" +description = "" +authors = ["Pavel Sinitsin "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.12" +requests = "^2.32.3" +loguru = "^0.7.2" +pydantic = "^2.7.4" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.pyright] +venvPath = "." +venv = ".venv" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29