Spaces:
Sleeping
Sleeping
update config and wildberries_client
Browse files- config.py +25 -11
- wildberries_client.py +119 -0
config.py
CHANGED
|
@@ -15,12 +15,17 @@ class Config:
|
|
| 15 |
|
| 16 |
def __init__(self):
|
| 17 |
self.wildberries_api_token = os.getenv("WILDBERRIES_API_TOKEN")
|
|
|
|
|
|
|
| 18 |
self.wildberries_base_url = "https://statistics-api.wildberries.ru"
|
| 19 |
self.wildberries_content_url = "https://content-api.wildberries.ru"
|
| 20 |
self.wildberries_analytics_url = "https://seller-analytics-api.wildberries.ru"
|
| 21 |
self.wildberries_common_url = "https://common-api.wildberries.ru"
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
# Rate limiting settings (
|
|
|
|
| 24 |
self.rate_limit_requests = 300 # requests per minute
|
| 25 |
self.rate_limit_window = 60 # seconds
|
| 26 |
|
|
@@ -58,7 +63,6 @@ class Config:
|
|
| 58 |
return {
|
| 59 |
"requests_per_minute": self.rate_limit_requests,
|
| 60 |
"window_seconds": self.rate_limit_window,
|
| 61 |
-
"burst_allowance": self.rate_limit_burst,
|
| 62 |
"backoff_factor": self.retry_backoff_factor
|
| 63 |
}
|
| 64 |
|
|
@@ -67,37 +71,47 @@ class Config:
|
|
| 67 |
return self.wildberries_api_token is not None and len(self.wildberries_api_token) > 0
|
| 68 |
|
| 69 |
def get_endpoints(self) -> Dict[str, str]:
|
| 70 |
-
"""Get API endpoint configurations"""
|
| 71 |
return {
|
|
|
|
| 72 |
"sales": f"{self.wildberries_base_url}/api/v1/supplier/sales",
|
| 73 |
-
"orders": f"{self.wildberries_base_url}/api/v1/supplier/orders",
|
| 74 |
"stocks": f"{self.wildberries_base_url}/api/v1/supplier/stocks",
|
| 75 |
"incomes": f"{self.wildberries_base_url}/api/v1/supplier/incomes",
|
| 76 |
"reportDetailByPeriod": f"{self.wildberries_base_url}/api/v1/supplier/reportDetailByPeriod",
|
|
|
|
|
|
|
| 77 |
"analytics": f"{self.wildberries_analytics_url}/api/v2/nm-report/detail",
|
|
|
|
|
|
|
| 78 |
"content": f"{self.wildberries_content_url}/content/v1/cards/cursor/list",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
"ping_statistics": f"{self.wildberries_base_url}/ping",
|
| 80 |
-
"ping_content": f"{self.wildberries_content_url}/ping",
|
| 81 |
"ping_analytics": f"{self.wildberries_analytics_url}/ping",
|
| 82 |
"ping_common": f"{self.wildberries_common_url}/ping",
|
| 83 |
-
"
|
| 84 |
-
"
|
| 85 |
}
|
| 86 |
|
| 87 |
def validate_token(self, token: str) -> bool:
|
| 88 |
-
"""Validate the format of a Wildberries API token"""
|
| 89 |
if not token:
|
| 90 |
return False
|
| 91 |
|
| 92 |
-
#
|
| 93 |
-
# This is a simple check, not comprehensive
|
| 94 |
try:
|
| 95 |
# Check if it looks like a JWT (three parts separated by dots)
|
| 96 |
parts = token.split('.')
|
| 97 |
if len(parts) != 3:
|
| 98 |
return False
|
| 99 |
|
| 100 |
-
# Check minimum length
|
| 101 |
if len(token) < 50:
|
| 102 |
return False
|
| 103 |
|
|
|
|
| 15 |
|
| 16 |
def __init__(self):
|
| 17 |
self.wildberries_api_token = os.getenv("WILDBERRIES_API_TOKEN")
|
| 18 |
+
|
| 19 |
+
# Official Wildberries API URLs based on documentation
|
| 20 |
self.wildberries_base_url = "https://statistics-api.wildberries.ru"
|
| 21 |
self.wildberries_content_url = "https://content-api.wildberries.ru"
|
| 22 |
self.wildberries_analytics_url = "https://seller-analytics-api.wildberries.ru"
|
| 23 |
self.wildberries_common_url = "https://common-api.wildberries.ru"
|
| 24 |
+
self.wildberries_marketplace_url = "https://marketplace-api.wildberries.ru"
|
| 25 |
+
self.wildberries_supplies_url = "https://supplies-api.wildberries.ru"
|
| 26 |
|
| 27 |
+
# Rate limiting settings (based on official documentation)
|
| 28 |
+
# Statistics API: Maximum of 300 requests per minute
|
| 29 |
self.rate_limit_requests = 300 # requests per minute
|
| 30 |
self.rate_limit_window = 60 # seconds
|
| 31 |
|
|
|
|
| 63 |
return {
|
| 64 |
"requests_per_minute": self.rate_limit_requests,
|
| 65 |
"window_seconds": self.rate_limit_window,
|
|
|
|
| 66 |
"backoff_factor": self.retry_backoff_factor
|
| 67 |
}
|
| 68 |
|
|
|
|
| 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 official documentation"""
|
| 75 |
return {
|
| 76 |
+
# Statistics API endpoints
|
| 77 |
"sales": f"{self.wildberries_base_url}/api/v1/supplier/sales",
|
| 78 |
+
"orders": f"{self.wildberries_base_url}/api/v1/supplier/orders",
|
| 79 |
"stocks": f"{self.wildberries_base_url}/api/v1/supplier/stocks",
|
| 80 |
"incomes": f"{self.wildberries_base_url}/api/v1/supplier/incomes",
|
| 81 |
"reportDetailByPeriod": f"{self.wildberries_base_url}/api/v1/supplier/reportDetailByPeriod",
|
| 82 |
+
|
| 83 |
+
# Analytics API endpoints
|
| 84 |
"analytics": f"{self.wildberries_analytics_url}/api/v2/nm-report/detail",
|
| 85 |
+
|
| 86 |
+
# Content API endpoints
|
| 87 |
"content": f"{self.wildberries_content_url}/content/v1/cards/cursor/list",
|
| 88 |
+
|
| 89 |
+
# Common API endpoints
|
| 90 |
+
"news": f"{self.wildberries_common_url}/api/communications/v2/news",
|
| 91 |
+
"seller_info": f"{self.wildberries_common_url}/api/v1/seller-info",
|
| 92 |
+
|
| 93 |
+
# Connection check endpoints for each service
|
| 94 |
"ping_statistics": f"{self.wildberries_base_url}/ping",
|
| 95 |
+
"ping_content": f"{self.wildberries_content_url}/ping",
|
| 96 |
"ping_analytics": f"{self.wildberries_analytics_url}/ping",
|
| 97 |
"ping_common": f"{self.wildberries_common_url}/ping",
|
| 98 |
+
"ping_marketplace": f"{self.wildberries_marketplace_url}/ping",
|
| 99 |
+
"ping_supplies": f"{self.wildberries_supplies_url}/ping"
|
| 100 |
}
|
| 101 |
|
| 102 |
def validate_token(self, token: str) -> bool:
|
| 103 |
+
"""Validate the format of a Wildberries API token (JWT format)"""
|
| 104 |
if not token:
|
| 105 |
return False
|
| 106 |
|
| 107 |
+
# Wildberries tokens are JWT format based on official documentation
|
|
|
|
| 108 |
try:
|
| 109 |
# Check if it looks like a JWT (three parts separated by dots)
|
| 110 |
parts = token.split('.')
|
| 111 |
if len(parts) != 3:
|
| 112 |
return False
|
| 113 |
|
| 114 |
+
# Check minimum length (JWT tokens are typically longer)
|
| 115 |
if len(token) < 50:
|
| 116 |
return False
|
| 117 |
|
wildberries_client.py
CHANGED
|
@@ -389,4 +389,123 @@ class WildberriesAPI:
|
|
| 389 |
"message": f"API connection failed: {str(e)}",
|
| 390 |
"records_count": 0,
|
| 391 |
"rate_limit_remaining": 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
}
|
|
|
|
| 389 |
"message": f"API connection failed: {str(e)}",
|
| 390 |
"records_count": 0,
|
| 391 |
"rate_limit_remaining": 0
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
def ping(self, service: str = "statistics") -> Dict[str, Any]:
|
| 395 |
+
"""
|
| 396 |
+
Test connection to specific WB API service
|
| 397 |
+
|
| 398 |
+
Args:
|
| 399 |
+
service: API service to ping (statistics, content, analytics, common, marketplace, supplies)
|
| 400 |
+
|
| 401 |
+
Returns:
|
| 402 |
+
Dict with ping result
|
| 403 |
+
|
| 404 |
+
Note: Rate limit is 3 requests every 30 seconds per service
|
| 405 |
+
"""
|
| 406 |
+
endpoint_key = f"ping_{service}"
|
| 407 |
+
if endpoint_key not in self.config.get_endpoints():
|
| 408 |
+
raise WildberriesAPIError(f"Unknown service: {service}")
|
| 409 |
+
|
| 410 |
+
endpoint = self.config.get_endpoints()[endpoint_key]
|
| 411 |
+
|
| 412 |
+
try:
|
| 413 |
+
# Simple request without using main rate limiter (ping has separate limits)
|
| 414 |
+
response = self.session.get(endpoint, timeout=10)
|
| 415 |
+
|
| 416 |
+
if response.ok:
|
| 417 |
+
data = response.json()
|
| 418 |
+
return {
|
| 419 |
+
"status": "success",
|
| 420 |
+
"service": service,
|
| 421 |
+
"timestamp": data.get("TS"),
|
| 422 |
+
"api_status": data.get("Status"),
|
| 423 |
+
"message": f"Connection to {service} API successful"
|
| 424 |
+
}
|
| 425 |
+
else:
|
| 426 |
+
return {
|
| 427 |
+
"status": "error",
|
| 428 |
+
"service": service,
|
| 429 |
+
"message": f"HTTP {response.status_code}: {response.text}"
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
except Exception as e:
|
| 433 |
+
return {
|
| 434 |
+
"status": "error",
|
| 435 |
+
"service": service,
|
| 436 |
+
"message": f"Connection failed: {str(e)}"
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
def get_seller_info(self) -> Dict[str, Any]:
|
| 440 |
+
"""
|
| 441 |
+
Get seller information including name and account ID
|
| 442 |
+
|
| 443 |
+
Returns:
|
| 444 |
+
Dict with seller information
|
| 445 |
+
|
| 446 |
+
Note: Maximum 1 request per minute per seller account
|
| 447 |
+
"""
|
| 448 |
+
endpoint = self.config.get_endpoints()["seller_info"]
|
| 449 |
+
|
| 450 |
+
try:
|
| 451 |
+
response = self._make_request("GET", endpoint)
|
| 452 |
+
|
| 453 |
+
return {
|
| 454 |
+
"name": response.get("name"),
|
| 455 |
+
"seller_id": response.get("sid"),
|
| 456 |
+
"trade_mark": response.get("tradeMark"),
|
| 457 |
+
"status": "success"
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
except Exception as e:
|
| 461 |
+
logger.error(f"Error fetching seller info: {str(e)}")
|
| 462 |
+
return {
|
| 463 |
+
"status": "error",
|
| 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 = 100) -> Dict[str, Any]:
|
| 468 |
+
"""
|
| 469 |
+
Get seller portal news
|
| 470 |
+
|
| 471 |
+
Args:
|
| 472 |
+
from_date: Date from which to get news (YYYY-MM-DD format)
|
| 473 |
+
from_id: News ID to start from (including it)
|
| 474 |
+
limit: Maximum number of news items (up to 100)
|
| 475 |
+
|
| 476 |
+
Returns:
|
| 477 |
+
Dict with news data
|
| 478 |
+
|
| 479 |
+
Note: Maximum 10 requests per 10 minutes per seller account
|
| 480 |
+
"""
|
| 481 |
+
endpoint = self.config.get_endpoints()["news"]
|
| 482 |
+
|
| 483 |
+
params = {}
|
| 484 |
+
if from_date:
|
| 485 |
+
params["from"] = from_date
|
| 486 |
+
if from_id:
|
| 487 |
+
params["fromID"] = from_id
|
| 488 |
+
|
| 489 |
+
if not from_date and not from_id:
|
| 490 |
+
# Default to last 7 days if no parameters specified
|
| 491 |
+
from_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
|
| 492 |
+
params["from"] = from_date
|
| 493 |
+
|
| 494 |
+
try:
|
| 495 |
+
response = self._make_request("GET", endpoint, params=params)
|
| 496 |
+
|
| 497 |
+
news_items = response.get("data", [])
|
| 498 |
+
|
| 499 |
+
return {
|
| 500 |
+
"status": "success",
|
| 501 |
+
"count": len(news_items),
|
| 502 |
+
"news": news_items[:limit] if len(news_items) > limit else news_items
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
except Exception as e:
|
| 506 |
+
logger.error(f"Error fetching news: {str(e)}")
|
| 507 |
+
return {
|
| 508 |
+
"status": "error",
|
| 509 |
+
"message": f"Failed to fetch news: {str(e)}",
|
| 510 |
+
"news": []
|
| 511 |
}
|