bakyt92 commited on
Commit
a3a4e3f
·
1 Parent(s): a758bd8

update config and wildberries_client

Browse files
Files changed (2) hide show
  1. config.py +25 -11
  2. 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 (matches official documentation)
 
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
- "news": f"{self.wildberries_common_url}/api/communications/v2/news",
84
- "seller_info": f"{self.wildberries_common_url}/api/v1/seller-info"
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
- # Basic validation - Wildberries tokens are typically base64-encoded JWTs
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
  }