Spaces:
Running
Running
update config and wildberries_client
Browse files- config.py +7 -7
- wildberries_client.py +150 -19
config.py
CHANGED
|
@@ -71,14 +71,14 @@ class Config:
|
|
| 71 |
return self.wildberries_api_token is not None and len(self.wildberries_api_token) > 0
|
| 72 |
|
| 73 |
def get_endpoints(self) -> Dict[str, str]:
|
| 74 |
-
"""Get API endpoint configurations based on
|
| 75 |
return {
|
| 76 |
-
# Statistics API endpoints
|
| 77 |
-
"sales": f"{self.wildberries_base_url}/api/
|
| 78 |
-
"orders": f"{self.wildberries_base_url}/api/
|
| 79 |
-
"stocks": f"{self.wildberries_base_url}/api/
|
| 80 |
-
"incomes": f"{self.wildberries_base_url}/api/
|
| 81 |
-
"reportDetailByPeriod": f"{self.wildberries_base_url}/api/
|
| 82 |
|
| 83 |
# Analytics API endpoints
|
| 84 |
"analytics": f"{self.wildberries_analytics_url}/api/v2/nm-report/detail",
|
|
|
|
| 71 |
return self.wildberries_api_token is not None and len(self.wildberries_api_token) > 0
|
| 72 |
|
| 73 |
def get_endpoints(self) -> Dict[str, str]:
|
| 74 |
+
"""Get API endpoint configurations based on working API calls"""
|
| 75 |
return {
|
| 76 |
+
# Statistics API endpoints - Updated to working v5 version
|
| 77 |
+
"sales": f"{self.wildberries_base_url}/api/v5/supplier/reportDetailByPeriod",
|
| 78 |
+
"orders": f"{self.wildberries_base_url}/api/v5/supplier/reportDetailByPeriod",
|
| 79 |
+
"stocks": f"{self.wildberries_base_url}/api/v5/supplier/reportDetailByPeriod",
|
| 80 |
+
"incomes": f"{self.wildberries_base_url}/api/v5/supplier/reportDetailByPeriod",
|
| 81 |
+
"reportDetailByPeriod": f"{self.wildberries_base_url}/api/v5/supplier/reportDetailByPeriod",
|
| 82 |
|
| 83 |
# Analytics API endpoints
|
| 84 |
"analytics": f"{self.wildberries_analytics_url}/api/v2/nm-report/detail",
|
wildberries_client.py
CHANGED
|
@@ -153,18 +153,24 @@ class WildberriesAPI:
|
|
| 153 |
try:
|
| 154 |
response = self._make_request("GET", endpoint, params=params)
|
| 155 |
|
| 156 |
-
if not response
|
| 157 |
logger.warning("No sales data returned from API")
|
| 158 |
return pd.DataFrame()
|
| 159 |
|
| 160 |
-
#
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
if sales_data.empty:
|
| 164 |
return sales_data
|
| 165 |
|
| 166 |
# Process and clean the data
|
| 167 |
-
sales_data = self.
|
| 168 |
|
| 169 |
return sales_data
|
| 170 |
|
|
@@ -187,23 +193,29 @@ class WildberriesAPI:
|
|
| 187 |
if not date_from:
|
| 188 |
date_from = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
| 189 |
|
| 190 |
-
params = {"dateFrom": date_from}
|
| 191 |
|
| 192 |
try:
|
| 193 |
response = self._make_request("GET", endpoint, params=params)
|
| 194 |
|
| 195 |
-
if not response
|
| 196 |
logger.warning("No stock data returned from API")
|
| 197 |
return pd.DataFrame()
|
| 198 |
|
| 199 |
-
#
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
if stock_data.empty:
|
| 203 |
return stock_data
|
| 204 |
|
| 205 |
# Process and clean the data
|
| 206 |
-
stock_data = self.
|
| 207 |
|
| 208 |
return stock_data
|
| 209 |
|
|
@@ -224,25 +236,31 @@ class WildberriesAPI:
|
|
| 224 |
"""
|
| 225 |
endpoint = self.config.get_endpoints()["orders"]
|
| 226 |
|
| 227 |
-
params = {"dateFrom": date_from}
|
| 228 |
if date_to:
|
| 229 |
params["dateTo"] = date_to
|
| 230 |
|
| 231 |
try:
|
| 232 |
response = self._make_request("GET", endpoint, params=params)
|
| 233 |
|
| 234 |
-
if not response
|
| 235 |
logger.warning("No orders data returned from API")
|
| 236 |
return pd.DataFrame()
|
| 237 |
|
| 238 |
-
#
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
if orders_data.empty:
|
| 242 |
return orders_data
|
| 243 |
|
| 244 |
# Process and clean the data
|
| 245 |
-
orders_data = self.
|
| 246 |
|
| 247 |
return orders_data
|
| 248 |
|
|
@@ -362,13 +380,126 @@ class WildberriesAPI:
|
|
| 362 |
|
| 363 |
return df
|
| 364 |
|
| 365 |
-
def
|
| 366 |
-
"""Process and clean
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
|
| 368 |
-
# Similar processing to sales data
|
| 369 |
-
# This would be implemented based on the specific orders API response format
|
| 370 |
return df
|
| 371 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
def test_connection(self) -> Dict[str, Any]:
|
| 373 |
"""Test API connection and return status"""
|
| 374 |
try:
|
|
@@ -464,7 +595,7 @@ class WildberriesAPI:
|
|
| 464 |
"message": f"Failed to fetch seller info: {str(e)}"
|
| 465 |
}
|
| 466 |
|
| 467 |
-
def get_news(self, from_date: str = None, from_id: int = None, limit: int =
|
| 468 |
"""
|
| 469 |
Get seller portal news
|
| 470 |
|
|
|
|
| 153 |
try:
|
| 154 |
response = self._make_request("GET", endpoint, params=params)
|
| 155 |
|
| 156 |
+
if not response:
|
| 157 |
logger.warning("No sales data returned from API")
|
| 158 |
return pd.DataFrame()
|
| 159 |
|
| 160 |
+
# Handle direct array response (v5 API format)
|
| 161 |
+
if isinstance(response, list):
|
| 162 |
+
sales_data = pd.DataFrame(response)
|
| 163 |
+
elif isinstance(response, dict) and "data" in response:
|
| 164 |
+
sales_data = pd.DataFrame(response["data"])
|
| 165 |
+
else:
|
| 166 |
+
logger.warning("Unexpected API response format")
|
| 167 |
+
return pd.DataFrame()
|
| 168 |
|
| 169 |
if sales_data.empty:
|
| 170 |
return sales_data
|
| 171 |
|
| 172 |
# Process and clean the data
|
| 173 |
+
sales_data = self._process_reportdetail_data(sales_data)
|
| 174 |
|
| 175 |
return sales_data
|
| 176 |
|
|
|
|
| 193 |
if not date_from:
|
| 194 |
date_from = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
| 195 |
|
| 196 |
+
params = {"dateFrom": date_from, "limit": 100}
|
| 197 |
|
| 198 |
try:
|
| 199 |
response = self._make_request("GET", endpoint, params=params)
|
| 200 |
|
| 201 |
+
if not response:
|
| 202 |
logger.warning("No stock data returned from API")
|
| 203 |
return pd.DataFrame()
|
| 204 |
|
| 205 |
+
# Handle direct array response (v5 API format)
|
| 206 |
+
if isinstance(response, list):
|
| 207 |
+
stock_data = pd.DataFrame(response)
|
| 208 |
+
elif isinstance(response, dict) and "data" in response:
|
| 209 |
+
stock_data = pd.DataFrame(response["data"])
|
| 210 |
+
else:
|
| 211 |
+
logger.warning("Unexpected API response format")
|
| 212 |
+
return pd.DataFrame()
|
| 213 |
|
| 214 |
if stock_data.empty:
|
| 215 |
return stock_data
|
| 216 |
|
| 217 |
# Process and clean the data
|
| 218 |
+
stock_data = self._process_reportdetail_data(stock_data)
|
| 219 |
|
| 220 |
return stock_data
|
| 221 |
|
|
|
|
| 236 |
"""
|
| 237 |
endpoint = self.config.get_endpoints()["orders"]
|
| 238 |
|
| 239 |
+
params = {"dateFrom": date_from, "limit": 100}
|
| 240 |
if date_to:
|
| 241 |
params["dateTo"] = date_to
|
| 242 |
|
| 243 |
try:
|
| 244 |
response = self._make_request("GET", endpoint, params=params)
|
| 245 |
|
| 246 |
+
if not response:
|
| 247 |
logger.warning("No orders data returned from API")
|
| 248 |
return pd.DataFrame()
|
| 249 |
|
| 250 |
+
# Handle direct array response (v5 API format)
|
| 251 |
+
if isinstance(response, list):
|
| 252 |
+
orders_data = pd.DataFrame(response)
|
| 253 |
+
elif isinstance(response, dict) and "data" in response:
|
| 254 |
+
orders_data = pd.DataFrame(response["data"])
|
| 255 |
+
else:
|
| 256 |
+
logger.warning("Unexpected API response format")
|
| 257 |
+
return pd.DataFrame()
|
| 258 |
|
| 259 |
if orders_data.empty:
|
| 260 |
return orders_data
|
| 261 |
|
| 262 |
# Process and clean the data
|
| 263 |
+
orders_data = self._process_reportdetail_data(orders_data)
|
| 264 |
|
| 265 |
return orders_data
|
| 266 |
|
|
|
|
| 380 |
|
| 381 |
return df
|
| 382 |
|
| 383 |
+
def _process_reportdetail_data(self, df: pd.DataFrame) -> pd.DataFrame:
|
| 384 |
+
"""Process and clean reportDetailByPeriod data from API response (v5)"""
|
| 385 |
+
|
| 386 |
+
# Column mapping based on actual API response structure
|
| 387 |
+
column_mapping = {
|
| 388 |
+
'realizationreport_id': 'report_id',
|
| 389 |
+
'date_from': 'date_from',
|
| 390 |
+
'date_to': 'date_to',
|
| 391 |
+
'create_dt': 'create_date',
|
| 392 |
+
'currency_name': 'currency',
|
| 393 |
+
'suppliercontract_code': 'contract_code',
|
| 394 |
+
'rrd_id': 'record_id',
|
| 395 |
+
'gi_id': 'goods_id',
|
| 396 |
+
'subject_name': 'category',
|
| 397 |
+
'nm_id': 'product_id',
|
| 398 |
+
'brand_name': 'brand',
|
| 399 |
+
'sa_name': 'supplier_article',
|
| 400 |
+
'ts_name': 'tech_size',
|
| 401 |
+
'barcode': 'barcode',
|
| 402 |
+
'doc_type_name': 'document_type',
|
| 403 |
+
'quantity': 'quantity',
|
| 404 |
+
'retail_price': 'retail_price',
|
| 405 |
+
'retail_amount': 'retail_amount',
|
| 406 |
+
'sale_percent': 'sale_percent',
|
| 407 |
+
'commission_percent': 'commission_percent',
|
| 408 |
+
'office_name': 'office_name',
|
| 409 |
+
'supplier_oper_name': 'operation_name',
|
| 410 |
+
'order_dt': 'order_date',
|
| 411 |
+
'sale_dt': 'sale_date',
|
| 412 |
+
'rr_dt': 'report_date',
|
| 413 |
+
'shk_id': 'warehouse_code',
|
| 414 |
+
'retail_price_withdisc_rub': 'discounted_price',
|
| 415 |
+
'delivery_amount': 'delivery_amount',
|
| 416 |
+
'return_amount': 'return_amount',
|
| 417 |
+
'delivery_rub': 'delivery_cost',
|
| 418 |
+
'gi_box_type_name': 'box_type',
|
| 419 |
+
'product_discount_for_report': 'product_discount',
|
| 420 |
+
'supplier_promo': 'supplier_promo',
|
| 421 |
+
'rid': 'rid',
|
| 422 |
+
'ppvz_spp_prc': 'spp_percent',
|
| 423 |
+
'ppvz_kvw_prc_base': 'kvw_percent_base',
|
| 424 |
+
'ppvz_kvw_prc': 'kvw_percent',
|
| 425 |
+
'sup_rating_prc_up': 'rating_bonus',
|
| 426 |
+
'is_kgvp_v2': 'is_kgvp',
|
| 427 |
+
'ppvz_sales_commission': 'sales_commission',
|
| 428 |
+
'ppvz_for_pay': 'amount_for_pay',
|
| 429 |
+
'ppvz_reward': 'reward',
|
| 430 |
+
'acquiring_fee': 'acquiring_fee',
|
| 431 |
+
'acquiring_percent': 'acquiring_percent',
|
| 432 |
+
'payment_processing': 'payment_processing',
|
| 433 |
+
'acquiring_bank': 'acquiring_bank',
|
| 434 |
+
'ppvz_vw': 'warehouse_cost',
|
| 435 |
+
'ppvz_vw_nds': 'warehouse_cost_vat',
|
| 436 |
+
'ppvz_office_name': 'pickup_office',
|
| 437 |
+
'ppvz_office_id': 'pickup_office_id',
|
| 438 |
+
'ppvz_supplier_id': 'supplier_id',
|
| 439 |
+
'ppvz_supplier_name': 'supplier_name',
|
| 440 |
+
'ppvz_inn': 'supplier_inn',
|
| 441 |
+
'declaration_number': 'declaration_number',
|
| 442 |
+
'sticker_id': 'sticker_id',
|
| 443 |
+
'site_country': 'site_country',
|
| 444 |
+
'penalty': 'penalty',
|
| 445 |
+
'additional_payment': 'additional_payment',
|
| 446 |
+
'rebill_logistic_cost': 'logistics_cost',
|
| 447 |
+
'rebill_logistic_org': 'logistics_company',
|
| 448 |
+
'storage_fee': 'storage_fee',
|
| 449 |
+
'deduction': 'deduction',
|
| 450 |
+
'acceptance': 'acceptance',
|
| 451 |
+
'assembly_id': 'assembly_id',
|
| 452 |
+
'srid': 'unique_id',
|
| 453 |
+
'report_type': 'report_type'
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
# Rename columns that exist
|
| 457 |
+
for old_name, new_name in column_mapping.items():
|
| 458 |
+
if old_name in df.columns:
|
| 459 |
+
df = df.rename(columns={old_name: new_name})
|
| 460 |
+
|
| 461 |
+
# Convert date columns
|
| 462 |
+
date_columns = ['create_date', 'order_date', 'sale_date', 'report_date']
|
| 463 |
+
for col in date_columns:
|
| 464 |
+
if col in df.columns:
|
| 465 |
+
df[col] = pd.to_datetime(df[col], errors='coerce')
|
| 466 |
+
|
| 467 |
+
# Convert numeric columns
|
| 468 |
+
numeric_columns = [
|
| 469 |
+
'quantity', 'retail_price', 'retail_amount', 'sale_percent', 'commission_percent',
|
| 470 |
+
'discounted_price', 'delivery_amount', 'return_amount', 'delivery_cost',
|
| 471 |
+
'product_discount', 'supplier_promo', 'spp_percent', 'kvw_percent_base',
|
| 472 |
+
'kvw_percent', 'rating_bonus', 'sales_commission', 'amount_for_pay',
|
| 473 |
+
'reward', 'acquiring_fee', 'acquiring_percent', 'warehouse_cost',
|
| 474 |
+
'warehouse_cost_vat', 'penalty', 'additional_payment', 'logistics_cost',
|
| 475 |
+
'storage_fee', 'deduction', 'acceptance'
|
| 476 |
+
]
|
| 477 |
+
|
| 478 |
+
for col in numeric_columns:
|
| 479 |
+
if col in df.columns:
|
| 480 |
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 481 |
+
|
| 482 |
+
# Add product name (use supplier_article if available)
|
| 483 |
+
if 'product_name' not in df.columns:
|
| 484 |
+
if 'supplier_article' in df.columns:
|
| 485 |
+
df['product_name'] = df['supplier_article']
|
| 486 |
+
elif 'category' in df.columns:
|
| 487 |
+
df['product_name'] = df['category']
|
| 488 |
+
else:
|
| 489 |
+
df['product_name'] = 'Unknown Product'
|
| 490 |
+
|
| 491 |
+
# Filter out non-sales records (keep only actual sales)
|
| 492 |
+
if 'operation_name' in df.columns:
|
| 493 |
+
# Keep records that are actual sales or returns
|
| 494 |
+
df = df[~df['operation_name'].str.contains('Возмещение', na=False)]
|
| 495 |
|
|
|
|
|
|
|
| 496 |
return df
|
| 497 |
|
| 498 |
+
def _process_orders_data(self, df: pd.DataFrame) -> pd.DataFrame:
|
| 499 |
+
"""Process and clean orders data from API response (legacy method)"""
|
| 500 |
+
# This is now handled by _process_reportdetail_data
|
| 501 |
+
return self._process_reportdetail_data(df)
|
| 502 |
+
|
| 503 |
def test_connection(self) -> Dict[str, Any]:
|
| 504 |
"""Test API connection and return status"""
|
| 505 |
try:
|
|
|
|
| 595 |
"message": f"Failed to fetch seller info: {str(e)}"
|
| 596 |
}
|
| 597 |
|
| 598 |
+
def get_news(self, from_date: str = None, from_id: int = None, limit: int = 500) -> Dict[str, Any]:
|
| 599 |
"""
|
| 600 |
Get seller portal news
|
| 601 |
|