trymonolith commited on
Commit
923fc35
·
verified ·
1 Parent(s): 7130d2e

Add 429 error handling and retry logic with exponential backoff

Browse files
Files changed (1) hide show
  1. app.py +151 -69
app.py CHANGED
@@ -4,6 +4,8 @@ from fastapi.responses import JSONResponse
4
  from typing import List, Optional
5
  import pandas as pd
6
  import json
 
 
7
 
8
  app = FastAPI(
9
  title="PyTrends API",
@@ -14,18 +16,45 @@ app = FastAPI(
14
  # Initialize pytrends
15
  pytrends = TrendReq(hl="en-US", tz=360)
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  @app.get("/")
18
  def root():
19
  return {
20
  "message": "PyTrends API",
21
  "endpoints": {
 
22
  "/interest_over_time": "Get interest over time for keywords",
23
  "/interest_by_region": "Get interest by region for keywords",
24
  "/trending_searches": "Get trending searches for a country",
25
  "/related_queries": "Get related queries for keywords"
26
- }
 
27
  }
28
 
 
 
 
 
29
  @app.get("/interest_over_time")
30
  def get_interest_over_time(
31
  kw: List[str] = Query(..., description="Keywords to search"),
@@ -35,30 +64,44 @@ def get_interest_over_time(
35
  cat: int = Query(0, description="Category")
36
  ):
37
  try:
38
- pytrends.build_payload(
39
- kw_list=kw,
40
- timeframe=timeframe,
41
- geo=geo,
42
- gprop=gprop,
43
- cat=cat
44
- )
45
- df = pytrends.interest_over_time()
46
- df = df.drop('isPartial', axis=1)
47
- data = df.reset_index().to_dict('records')
48
- # Convert datetime to string for JSON serialization
49
- for record in data:
50
- if 'date' in record:
51
- record['date'] = str(record['date'])
52
- return {
53
- "data": data,
54
- "keywords": kw,
55
- "timeframe": timeframe,
56
- "geo": geo
57
- }
 
 
 
 
 
 
 
 
58
  except Exception as e:
 
 
 
 
 
 
59
  return JSONResponse(
60
  status_code=400,
61
- content={"error": str(e)}
62
  )
63
 
64
  @app.get("/interest_by_region")
@@ -70,40 +113,70 @@ def get_interest_by_region(
70
  resolution: str = Query("country", description="Resolution (country, region, metro, city)")
71
  ):
72
  try:
73
- pytrends.build_payload(
74
- kw_list=kw,
75
- timeframe=timeframe,
76
- geo=geo,
77
- gprop=gprop
78
- )
79
- df = pytrends.interest_by_region(resolution=resolution)
80
- data = df.reset_index().to_dict('records')
81
- return {
82
- "data": data,
83
- "keywords": kw,
84
- "resolution": resolution
85
- }
 
 
 
 
 
 
 
 
 
86
  except Exception as e:
 
 
 
 
 
 
87
  return JSONResponse(
88
  status_code=400,
89
- content={"error": str(e)}
90
  )
91
 
92
  @app.get("/trending_searches")
93
  def get_trending_searches(
94
- country: str = Query("united_states", description="Country code (e.g., united_states, india, etc.)")
95
  ):
96
  try:
97
- df = pytrends.trending_searches(pn=country)
98
- data = df.values.tolist()
99
- return {
100
- "trending": data,
101
- "country": country
102
- }
 
 
 
 
 
 
 
 
 
103
  except Exception as e:
 
 
 
 
 
 
104
  return JSONResponse(
105
  status_code=400,
106
- content={"error": str(e)}
107
  )
108
 
109
  @app.get("/related_queries")
@@ -113,31 +186,40 @@ def get_related_queries(
113
  geo: str = Query("", description="Geographic location")
114
  ):
115
  try:
116
- pytrends.build_payload(
117
- kw_list=kw,
118
- timeframe=timeframe,
119
- geo=geo
120
- )
121
- related = pytrends.related_queries()
122
- result = {}
123
- for kw_item in kw:
124
- if kw_item in related:
125
- top_queries = related[kw_item]['top']
126
- rising_queries = related[kw_item]['rising']
127
- result[kw_item] = {
128
- "top": top_queries.reset_index().to_dict('records') if top_queries is not None else [],
129
- "rising": rising_queries.reset_index().to_dict('records') if rising_queries is not None else []
130
- }
131
- return {
132
- "related_queries": result,
133
- "keywords": kw
134
- }
 
 
 
 
 
 
 
135
  except Exception as e:
 
 
 
 
 
 
136
  return JSONResponse(
137
  status_code=400,
138
- content={"error": str(e)}
139
- )
140
-
141
- @app.get("/health")
142
- def health_check():
143
- return {"status": "healthy"}
 
4
  from typing import List, Optional
5
  import pandas as pd
6
  import json
7
+ import time
8
+ from functools import lru_cache
9
 
10
  app = FastAPI(
11
  title="PyTrends API",
 
16
  # Initialize pytrends
17
  pytrends = TrendReq(hl="en-US", tz=360)
18
 
19
+ # Helper function for retry logic
20
+ def retry_with_backoff(func, max_retries=3, initial_delay=1, backoff_factor=2):
21
+ """Retry function with exponential backoff for handling rate limits"""
22
+ for attempt in range(max_retries):
23
+ try:
24
+ return func()
25
+ except Exception as e:
26
+ error_str = str(e)
27
+ # Check if it's a rate limit error (429)
28
+ if "429" in error_str or "rate" in error_str.lower():
29
+ if attempt < max_retries - 1:
30
+ wait_time = initial_delay * (backoff_factor ** attempt)
31
+ print(f"Rate limited. Waiting {wait_time} seconds before retry...")
32
+ time.sleep(wait_time)
33
+ continue
34
+ else:
35
+ return {"error": f"Service temporarily unavailable. Please try again in a few moments. Details: {error_str}"}
36
+ else:
37
+ raise
38
+ return {"error": "Max retries exceeded"}
39
+
40
  @app.get("/")
41
  def root():
42
  return {
43
  "message": "PyTrends API",
44
  "endpoints": {
45
+ "/health": "Health check endpoint",
46
  "/interest_over_time": "Get interest over time for keywords",
47
  "/interest_by_region": "Get interest by region for keywords",
48
  "/trending_searches": "Get trending searches for a country",
49
  "/related_queries": "Get related queries for keywords"
50
+ },
51
+ "note": "If you get a 429 error, Google Trends is rate limiting. Wait a few minutes and try again."
52
  }
53
 
54
+ @app.get("/health")
55
+ def health_check():
56
+ return {"status": "healthy"}
57
+
58
  @app.get("/interest_over_time")
59
  def get_interest_over_time(
60
  kw: List[str] = Query(..., description="Keywords to search"),
 
64
  cat: int = Query(0, description="Category")
65
  ):
66
  try:
67
+ def fetch_data():
68
+ pytrends.build_payload(
69
+ kw_list=kw,
70
+ timeframe=timeframe,
71
+ geo=geo,
72
+ gprop=gprop,
73
+ cat=cat
74
+ )
75
+ time.sleep(0.5) # Small delay to avoid rate limits
76
+ df = pytrends.interest_over_time()
77
+ if df is None or df.empty:
78
+ return {"error": "No data available for this query"}
79
+ df = df.drop('isPartial', axis=1)
80
+ data = df.reset_index().to_dict('records')
81
+ for record in data:
82
+ if 'date' in record:
83
+ record['date'] = str(record['date'])
84
+ return {
85
+ "data": data,
86
+ "keywords": kw,
87
+ "timeframe": timeframe,
88
+ "geo": geo
89
+ }
90
+
91
+ result = retry_with_backoff(fetch_data)
92
+ if isinstance(result, dict) and "error" in result:
93
+ return JSONResponse(status_code=429, content=result)
94
+ return result
95
  except Exception as e:
96
+ error_msg = str(e)
97
+ if "429" in error_msg or "rate" in error_msg.lower():
98
+ return JSONResponse(
99
+ status_code=429,
100
+ content={"error": "Too many requests to Google Trends. Please wait a few minutes before trying again."}
101
+ )
102
  return JSONResponse(
103
  status_code=400,
104
+ content={"error": error_msg}
105
  )
106
 
107
  @app.get("/interest_by_region")
 
113
  resolution: str = Query("country", description="Resolution (country, region, metro, city)")
114
  ):
115
  try:
116
+ def fetch_data():
117
+ pytrends.build_payload(
118
+ kw_list=kw,
119
+ timeframe=timeframe,
120
+ geo=geo,
121
+ gprop=gprop
122
+ )
123
+ time.sleep(0.5)
124
+ df = pytrends.interest_by_region(resolution=resolution)
125
+ if df is None or df.empty:
126
+ return {"error": "No regional data available"}
127
+ data = df.reset_index().to_dict('records')
128
+ return {
129
+ "data": data,
130
+ "keywords": kw,
131
+ "resolution": resolution
132
+ }
133
+
134
+ result = retry_with_backoff(fetch_data)
135
+ if isinstance(result, dict) and "error" in result:
136
+ return JSONResponse(status_code=429, content=result)
137
+ return result
138
  except Exception as e:
139
+ error_msg = str(e)
140
+ if "429" in error_msg or "rate" in error_msg.lower():
141
+ return JSONResponse(
142
+ status_code=429,
143
+ content={"error": "Too many requests to Google Trends. Please wait a few minutes before trying again."}
144
+ )
145
  return JSONResponse(
146
  status_code=400,
147
+ content={"error": error_msg}
148
  )
149
 
150
  @app.get("/trending_searches")
151
  def get_trending_searches(
152
+ country: str = Query("united_states", description="Country code")
153
  ):
154
  try:
155
+ def fetch_data():
156
+ time.sleep(0.5)
157
+ df = pytrends.trending_searches(pn=country)
158
+ if df is None or df.empty:
159
+ return {"error": "No trending data available for this country"}
160
+ data = df.values.tolist()
161
+ return {
162
+ "trending": data,
163
+ "country": country
164
+ }
165
+
166
+ result = retry_with_backoff(fetch_data)
167
+ if isinstance(result, dict) and "error" in result:
168
+ return JSONResponse(status_code=429, content=result)
169
+ return result
170
  except Exception as e:
171
+ error_msg = str(e)
172
+ if "429" in error_msg or "rate" in error_msg.lower():
173
+ return JSONResponse(
174
+ status_code=429,
175
+ content={"error": "Too many requests to Google Trends. Please wait a few minutes before trying again."}
176
+ )
177
  return JSONResponse(
178
  status_code=400,
179
+ content={"error": error_msg}
180
  )
181
 
182
  @app.get("/related_queries")
 
186
  geo: str = Query("", description="Geographic location")
187
  ):
188
  try:
189
+ def fetch_data():
190
+ pytrends.build_payload(
191
+ kw_list=kw,
192
+ timeframe=timeframe,
193
+ geo=geo
194
+ )
195
+ time.sleep(0.5)
196
+ related = pytrends.related_queries()
197
+ result = {}
198
+ for kw_item in kw:
199
+ if kw_item in related:
200
+ top_queries = related[kw_item]['top']
201
+ rising_queries = related[kw_item]['rising']
202
+ result[kw_item] = {
203
+ "top": top_queries.reset_index().to_dict('records') if top_queries is not None else [],
204
+ "rising": rising_queries.reset_index().to_dict('records') if rising_queries is not None else []
205
+ }
206
+ return {
207
+ "related_queries": result,
208
+ "keywords": kw
209
+ }
210
+
211
+ result = retry_with_backoff(fetch_data)
212
+ if isinstance(result, dict) and "error" in result:
213
+ return JSONResponse(status_code=429, content=result)
214
+ return result
215
  except Exception as e:
216
+ error_msg = str(e)
217
+ if "429" in error_msg or "rate" in error_msg.lower():
218
+ return JSONResponse(
219
+ status_code=429,
220
+ content={"error": "Too many requests to Google Trends. Please wait a few minutes before trying again."}
221
+ )
222
  return JSONResponse(
223
  status_code=400,
224
+ content={"error": error_msg}
225
+ )