pytrends-modern 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {pytrends_modern-0.2.0/pytrends_modern.egg-info → pytrends_modern-0.2.2}/PKG-INFO +9 -4
  2. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/README.md +8 -3
  3. pytrends_modern-0.2.2/examples/example_docker_usage.py +50 -0
  4. pytrends_modern-0.2.2/examples/test_async_integration.py +78 -0
  5. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pyproject.toml +1 -1
  6. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/__init__.py +3 -1
  7. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/browser_config_camoufox.py +5 -3
  8. pytrends_modern-0.2.2/pytrends_modern/request_async.py +342 -0
  9. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2/pytrends_modern.egg-info}/PKG-INFO +9 -4
  10. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern.egg-info/SOURCES.txt +3 -0
  11. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/LICENSE +0 -0
  12. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/MANIFEST.in +0 -0
  13. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/examples/advanced_usage.py +0 -0
  14. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/examples/basic_usage.py +0 -0
  15. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/examples/example_browser_mode.py +0 -0
  16. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/browser_config.py +0 -0
  17. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/camoufox_setup.py +0 -0
  18. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/cli.py +0 -0
  19. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/config.py +0 -0
  20. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/exceptions.py +0 -0
  21. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/proxy_extension.py +0 -0
  22. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/py.typed +0 -0
  23. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/request.py +0 -0
  24. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/rss.py +0 -0
  25. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/scraper.py +0 -0
  26. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/utils.py +0 -0
  27. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern.egg-info/dependency_links.txt +0 -0
  28. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern.egg-info/entry_points.txt +0 -0
  29. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern.egg-info/requires.txt +0 -0
  30. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern.egg-info/top_level.txt +0 -0
  31. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/setup.cfg +0 -0
  32. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/tests/conftest.py +0 -0
  33. {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/tests/test_basic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrends-modern
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Modern Google Trends API - Combining the best of pytrends, with RSS feeds, Selenium scraping, DrissionPage browser automation, and enhanced features
5
5
  Author: pytrends-modern contributors
6
6
  License: MIT
@@ -175,11 +175,16 @@ RUN mkdir -p /root/.config && \
175
175
  cd /root/.config && \
176
176
  tar -xzf /tmp/profile.tar.gz
177
177
 
178
- # 3. Use headless mode in container
179
- config = BrowserConfig(headless=True)
178
+ # 3. Use headless="virtual" in container
179
+ config = BrowserConfig(headless="virtual") # Use Xvfb for Docker
180
180
  ```
181
181
 
182
- See `Dockerfile.example` for complete Docker setup.
182
+ **Headless Options:**
183
+ - `headless=False` - Show browser window (local development)
184
+ - `headless=True` - Standard headless (servers with display)
185
+ - `headless="virtual"` - Xvfb virtual display (Docker containers)
186
+
187
+ See `Dockerfile.example` and `examples/example_docker_usage.py` for complete Docker setup.
183
188
 
184
189
  ⚠️ **Security**: Profile contains Google session - keep secure, don't commit to git!
185
190
 
@@ -125,11 +125,16 @@ RUN mkdir -p /root/.config && \
125
125
  cd /root/.config && \
126
126
  tar -xzf /tmp/profile.tar.gz
127
127
 
128
- # 3. Use headless mode in container
129
- config = BrowserConfig(headless=True)
128
+ # 3. Use headless="virtual" in container
129
+ config = BrowserConfig(headless="virtual") # Use Xvfb for Docker
130
130
  ```
131
131
 
132
- See `Dockerfile.example` for complete Docker setup.
132
+ **Headless Options:**
133
+ - `headless=False` - Show browser window (local development)
134
+ - `headless=True` - Standard headless (servers with display)
135
+ - `headless="virtual"` - Xvfb virtual display (Docker containers)
136
+
137
+ See `Dockerfile.example` and `examples/example_docker_usage.py` for complete Docker setup.
133
138
 
134
139
  ⚠️ **Security**: Profile contains Google session - keep secure, don't commit to git!
135
140
 
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Example: Using pytrends-modern in Docker containers
4
+
5
+ When running in Docker, use headless="virtual" to enable Xvfb virtual display.
6
+ This prevents display errors in containerized environments.
7
+ """
8
+
9
+ import asyncio
10
+ from pytrends_modern import AsyncTrendReq, BrowserConfig
11
+
12
+
13
+ async def main():
14
+ """Example async usage in Docker container"""
15
+
16
+ # Configure for Docker environment
17
+ config = BrowserConfig(
18
+ headless="virtual", # Use Xvfb virtual display for Docker
19
+ humanize=True,
20
+ os='linux',
21
+ geoip=True
22
+ )
23
+
24
+ # For local development, use:
25
+ # config = BrowserConfig(headless=False) # Show browser window
26
+ # config = BrowserConfig(headless=True) # Standard headless
27
+
28
+ print("🐳 Running pytrends-modern in Docker container...")
29
+ print(f"📁 Profile: {config.user_data_dir}")
30
+
31
+ async with AsyncTrendReq(browser_config=config) as pytrends:
32
+ pytrends.kw_list = ["Docker"]
33
+
34
+ # Get interest over time
35
+ print("\n📊 Fetching interest_over_time...")
36
+ df = await pytrends.interest_over_time()
37
+ print(f"✓ Got {len(df)} rows")
38
+ print(df.head())
39
+
40
+ # Get interest by region
41
+ print("\n🌍 Fetching interest_by_region...")
42
+ df_region = await pytrends.interest_by_region()
43
+ print(f"✓ Got {len(df_region)} rows")
44
+ print(df_region.head())
45
+
46
+ print("\n✅ Complete!")
47
+
48
+
49
+ if __name__ == "__main__":
50
+ asyncio.run(main())
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test AsyncTrendReq with Camoufox
4
+ """
5
+
6
+ import asyncio
7
+ from pytrends_modern import AsyncTrendReq, BrowserConfig
8
+
9
+ async def test_async_browser_mode():
10
+ """Test async browser mode with all 4 APIs"""
11
+ print("=" * 70)
12
+ print("Testing AsyncTrendReq with Camoufox")
13
+ print("=" * 70)
14
+
15
+ # Configure browser
16
+ config = BrowserConfig(
17
+ headless=False,
18
+ humanize=True,
19
+ os='linux',
20
+ geoip=True
21
+ )
22
+
23
+ keyword = "Python"
24
+ print(f"\n🔍 Testing with keyword: {keyword}")
25
+
26
+ # Use async context manager
27
+ async with AsyncTrendReq(browser_config=config) as pytrends:
28
+ pytrends.kw_list = [keyword]
29
+
30
+ # Test interest_over_time
31
+ print("\n📊 Testing interest_over_time()...")
32
+ try:
33
+ df_iot = await pytrends.interest_over_time()
34
+ print(f"✓ interest_over_time: {len(df_iot)} rows")
35
+ print(df_iot.head())
36
+ except Exception as e:
37
+ print(f"✗ Error: {e}")
38
+
39
+ # Test interest_by_region
40
+ print("\n🌍 Testing interest_by_region()...")
41
+ try:
42
+ df_ibr = await pytrends.interest_by_region()
43
+ print(f"✓ interest_by_region: {len(df_ibr)} rows")
44
+ print(df_ibr.head())
45
+ except Exception as e:
46
+ print(f"✗ Error: {e}")
47
+
48
+ # Test related_topics
49
+ print("\n🔗 Testing related_topics()...")
50
+ try:
51
+ topics = await pytrends.related_topics()
52
+ if keyword in topics and topics[keyword].get('top') is not None:
53
+ print(f"✓ related_topics: {len(topics[keyword]['top'])} topics")
54
+ print(topics[keyword]['top'].head())
55
+ else:
56
+ print("✓ related_topics: No data (might be normal)")
57
+ except Exception as e:
58
+ print(f"✗ Error: {e}")
59
+
60
+ # Test related_queries
61
+ print("\n❓ Testing related_queries()...")
62
+ try:
63
+ queries = await pytrends.related_queries()
64
+ if keyword in queries and queries[keyword].get('top') is not None:
65
+ print(f"✓ related_queries: {len(queries[keyword]['top'])} queries")
66
+ print(queries[keyword]['top'].head())
67
+ else:
68
+ print("✓ related_queries: No data (might be normal)")
69
+ except Exception as e:
70
+ print(f"✗ Error: {e}")
71
+
72
+ print("\n" + "=" * 70)
73
+ print("✅ Async test complete!")
74
+ print("=" * 70)
75
+
76
+ if __name__ == "__main__":
77
+ # Run async test
78
+ asyncio.run(test_async_browser_mode())
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pytrends-modern"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Modern Google Trends API - Combining the best of pytrends, with RSS feeds, Selenium scraping, DrissionPage browser automation, and enhanced features"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -2,11 +2,12 @@
2
2
  pytrends-modern: Modern Google Trends API
3
3
  """
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "0.2.2"
6
6
  __author__ = "pytrends-modern contributors"
7
7
  __license__ = "MIT"
8
8
 
9
9
  from pytrends_modern.request import TrendReq
10
+ from pytrends_modern.request_async import AsyncTrendReq
10
11
  from pytrends_modern.rss import TrendsRSS
11
12
  from pytrends_modern.scraper import TrendsScraper
12
13
  from pytrends_modern.browser_config_camoufox import BrowserConfig
@@ -21,6 +22,7 @@ from pytrends_modern.exceptions import (
21
22
 
22
23
  __all__ = [
23
24
  "TrendReq",
25
+ "AsyncTrendReq",
24
26
  "TrendsRSS",
25
27
  "TrendsScraper",
26
28
  "BrowserConfig",
@@ -1,6 +1,6 @@
1
1
  """Browser configuration for Camoufox automation"""
2
2
 
3
- from typing import Optional
3
+ from typing import Optional, Union
4
4
  import os as os_module
5
5
 
6
6
 
@@ -18,7 +18,9 @@ class BrowserConfig:
18
18
 
19
19
  Args:
20
20
  headless: Run browser in headless mode (default: False)
21
- Set to 'virtual' on Linux to use Xvfb
21
+ - False: Show browser window (for local development)
22
+ - True: Standard headless mode (for servers with display)
23
+ - 'virtual': Use Xvfb virtual display (for Docker containers)
22
24
  proxy_server: Proxy server URL (e.g., 'http://proxy.com:8080')
23
25
  proxy_username: Proxy username (for authenticated proxies)
24
26
  proxy_password: Proxy password (for authenticated proxies)
@@ -47,7 +49,7 @@ class BrowserConfig:
47
49
 
48
50
  def __init__(
49
51
  self,
50
- headless: bool = False,
52
+ headless: Union[bool, str] = False,
51
53
  proxy_server: Optional[str] = None,
52
54
  proxy_username: Optional[str] = None,
53
55
  proxy_password: Optional[str] = None,
@@ -0,0 +1,342 @@
1
+ """
2
+ Async Google Trends API request module with Camoufox support
3
+ """
4
+
5
+ import json
6
+ from typing import Dict, Optional
7
+ from urllib.parse import quote
8
+
9
+ import pandas as pd
10
+
11
+ from pytrends_modern import exceptions
12
+ from pytrends_modern.browser_config_camoufox import BrowserConfig
13
+
14
+
15
+ class AsyncTrendReq:
16
+ """
17
+ Async Google Trends API with Camoufox browser mode support
18
+
19
+ This class provides async methods for fetching Google Trends data
20
+ using Camoufox's async API for better performance in async applications.
21
+
22
+ Example:
23
+ >>> import asyncio
24
+ >>> from pytrends_modern import AsyncTrendReq, BrowserConfig
25
+ >>>
26
+ >>> async def main():
27
+ ... config = BrowserConfig(headless=True)
28
+ ... async with AsyncTrendReq(browser_config=config) as pytrends:
29
+ ... pytrends.kw_list = ['Python']
30
+ ... df = await pytrends.interest_over_time()
31
+ ... print(df.head())
32
+ >>>
33
+ >>> asyncio.run(main())
34
+ """
35
+
36
+ def __init__(self, browser_config: BrowserConfig):
37
+ """
38
+ Initialize async Google Trends request
39
+
40
+ Args:
41
+ browser_config: BrowserConfig instance for Camoufox
42
+ """
43
+ self.browser_config = browser_config
44
+ self.browser = None
45
+ self.browser_context = None
46
+ self.browser_page = None
47
+ self.browser_responses_cache = {}
48
+ self.kw_list = []
49
+
50
+ async def __aenter__(self):
51
+ """Async context manager entry"""
52
+ await self._init_camoufox()
53
+ return self
54
+
55
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
56
+ """Async context manager exit"""
57
+ await self._close_browser()
58
+
59
+ async def _init_camoufox(self) -> None:
60
+ """Initialize Camoufox browser with persistent context (async)"""
61
+ try:
62
+ from camoufox.async_api import AsyncCamoufox
63
+ except ImportError:
64
+ raise ImportError(
65
+ "Camoufox is required for async browser mode. "
66
+ "Install with: pip install pytrends-modern[browser]"
67
+ )
68
+
69
+ # Prepare browser options
70
+ import os
71
+ user_data_dir = os.path.expanduser(
72
+ self.browser_config.user_data_dir or "~/.config/camoufox-pytrends-profile"
73
+ )
74
+
75
+ # Check if profile is configured (has Google login)
76
+ from pytrends_modern.camoufox_setup import is_profile_configured
77
+ if not is_profile_configured(user_data_dir):
78
+ raise exceptions.BrowserError(
79
+ f"Camoufox profile not configured at: {user_data_dir}\n"
80
+ "You must set up your Google account login first:\n\n"
81
+ " from pytrends_modern.camoufox_setup import setup_profile\n"
82
+ " setup_profile()\n\n"
83
+ "Or run from command line:\n"
84
+ " python -m pytrends_modern.camoufox_setup\n\n"
85
+ "This will open a browser for you to log in to Google."
86
+ )
87
+
88
+ # Proxy configuration (if provided)
89
+ proxy_config = None
90
+ if self.browser_config.proxy_server:
91
+ proxy_config = {
92
+ "server": self.browser_config.proxy_server,
93
+ }
94
+ if self.browser_config.proxy_username:
95
+ proxy_config["username"] = self.browser_config.proxy_username
96
+ if self.browser_config.proxy_password:
97
+ proxy_config["password"] = self.browser_config.proxy_password
98
+
99
+ # Initialize AsyncCamoufox with persistent context
100
+ try:
101
+ # AsyncCamoufox() returns a context manager
102
+ camoufox_manager = AsyncCamoufox(
103
+ persistent_context=True,
104
+ user_data_dir=user_data_dir,
105
+ headless=self.browser_config.headless,
106
+ humanize=self.browser_config.humanize if hasattr(self.browser_config, 'humanize') else True,
107
+ os=self.browser_config.os if hasattr(self.browser_config, 'os') else 'linux',
108
+ geoip=self.browser_config.geoip if hasattr(self.browser_config, 'geoip') else True,
109
+ proxy=proxy_config
110
+ )
111
+
112
+ # Enter the context manager to get the browser context
113
+ self.browser = camoufox_manager
114
+ self.browser_context = await camoufox_manager.__aenter__()
115
+
116
+ # Use existing page if available (avoid opening 2 tabs)
117
+ if self.browser_context.pages:
118
+ self.browser_page = self.browser_context.pages[0]
119
+ else:
120
+ self.browser_page = await self.browser_context.new_page()
121
+
122
+ # Set up network interception
123
+ self.browser_page.on("response", self._handle_network_response)
124
+
125
+ except Exception as e:
126
+ raise exceptions.BrowserError(f"Failed to initialize AsyncCamoufox: {e}")
127
+
128
+ async def _close_browser(self) -> None:
129
+ """Close browser if open (async)"""
130
+ if self.browser:
131
+ try:
132
+ # Exit the context manager
133
+ await self.browser.__aexit__(None, None, None)
134
+ except Exception:
135
+ pass
136
+ self.browser = None
137
+ self.browser_context = None
138
+ self.browser_page = None
139
+
140
+ async def _handle_network_response(self, response) -> None:
141
+ """
142
+ Handle network responses and cache Google Trends API data (async)
143
+
144
+ Args:
145
+ response: Playwright response object
146
+ """
147
+ url = response.url
148
+
149
+ # Only process Google Trends API responses
150
+ if '/trends/api/widgetdata/' not in url:
151
+ return
152
+
153
+ try:
154
+ # Get response body (async in AsyncPlaywright)
155
+ body = await response.body()
156
+
157
+ # Parse the response (remove Google's JSONP prefix - exactly 5 bytes)
158
+ if body.startswith(b")]}'\n"):
159
+ body = body[5:]
160
+ elif body.startswith(b")]}'"):
161
+ body = body[5:]
162
+
163
+ data = json.loads(body)
164
+
165
+ # Cache by URL pattern
166
+ if '/widgetdata/multiline' in url:
167
+ self.browser_responses_cache['interest_over_time'] = data
168
+ elif '/widgetdata/comparedgeo' in url:
169
+ self.browser_responses_cache['interest_by_region'] = data
170
+ elif '/widgetdata/relatedsearches' in url:
171
+ # keywordType is URL-encoded inside the req parameter
172
+ import urllib.parse
173
+ decoded_url = urllib.parse.unquote(url)
174
+ if 'keywordType":"ENTITY' in decoded_url:
175
+ self.browser_responses_cache['related_topics'] = data
176
+ elif 'keywordType":"QUERY' in decoded_url:
177
+ self.browser_responses_cache['related_queries'] = data
178
+
179
+ except Exception:
180
+ pass # Silently ignore parsing errors
181
+
182
+ async def _capture_all_api_responses(self, keyword: str) -> None:
183
+ """
184
+ Navigate once and capture ALL API responses via network interception (async)
185
+
186
+ Args:
187
+ keyword: Search keyword to use
188
+ """
189
+ if not self.browser_page:
190
+ raise exceptions.BrowserError("Browser not initialized")
191
+
192
+ # Clear cache
193
+ self.browser_responses_cache.clear()
194
+
195
+ # Build URL
196
+ import urllib.parse
197
+ encoded_keyword = urllib.parse.quote(keyword)
198
+ url = f"https://trends.google.com/trends/explore?date=today%201-m&q={encoded_keyword}&hl=en-GB"
199
+
200
+ try:
201
+ # Navigate and wait for network idle
202
+ await self.browser_page.goto(url, wait_until='networkidle', timeout=60000)
203
+
204
+ # Give extra time for any delayed API calls
205
+ import asyncio
206
+ await asyncio.sleep(2)
207
+
208
+ except Exception as e:
209
+ raise exceptions.BrowserError(f"Failed to navigate to Google Trends: {e}")
210
+
211
+ def _parse_multiline_response(self, data: Dict) -> pd.DataFrame:
212
+ """Parse multiline (interest over time) API response"""
213
+ # Import from sync version to reuse parsing logic
214
+ from pytrends_modern.request import TrendReq
215
+ temp = TrendReq(hl='en-US', tz=360)
216
+ return temp._parse_multiline_response(data)
217
+
218
+ def _parse_comparedgeo_response(self, data: Dict, inc_geo_code: bool = False) -> pd.DataFrame:
219
+ """Parse comparedgeo (interest by region) API response"""
220
+ from pytrends_modern.request import TrendReq
221
+ temp = TrendReq(hl='en-US', tz=360)
222
+ return temp._parse_comparedgeo_response(data, inc_geo_code)
223
+
224
+ def _parse_relatedsearches_response(self, data: Dict) -> Dict[str, Optional[pd.DataFrame]]:
225
+ """Parse relatedsearches (related topics/queries) API response"""
226
+ from pytrends_modern.request import TrendReq
227
+ temp = TrendReq(hl='en-US', tz=360)
228
+ return temp._parse_relatedsearches_response(data)
229
+
230
+ async def interest_over_time(self) -> pd.DataFrame:
231
+ """
232
+ Get interest over time data (async)
233
+
234
+ Returns:
235
+ DataFrame with date index and columns for each keyword
236
+ """
237
+ if len(self.kw_list) != 1:
238
+ raise exceptions.InvalidParameterError(
239
+ "Async browser mode only supports 1 keyword. You provided: "
240
+ + str(len(self.kw_list))
241
+ )
242
+
243
+ keyword = self.kw_list[0]
244
+
245
+ # Capture all responses if not already cached
246
+ if not self.browser_responses_cache:
247
+ await self._capture_all_api_responses(keyword)
248
+
249
+ # Get cached response
250
+ response_data = self.browser_responses_cache.get('interest_over_time')
251
+
252
+ if not response_data:
253
+ # Try one more navigation if cache is empty
254
+ await self._capture_all_api_responses(keyword)
255
+ response_data = self.browser_responses_cache.get('interest_over_time')
256
+
257
+ if not response_data:
258
+ raise exceptions.ResponseError("Failed to capture interest_over_time API response")
259
+
260
+ # Parse browser response to DataFrame
261
+ return self._parse_multiline_response(response_data)
262
+
263
+ async def interest_by_region(self, inc_geo_code: bool = False) -> pd.DataFrame:
264
+ """
265
+ Get interest by region data (async)
266
+
267
+ Args:
268
+ inc_geo_code: Include geographic codes in results
269
+
270
+ Returns:
271
+ DataFrame with region index
272
+ """
273
+ if len(self.kw_list) != 1:
274
+ raise exceptions.InvalidParameterError(
275
+ "Async browser mode only supports 1 keyword"
276
+ )
277
+
278
+ keyword = self.kw_list[0]
279
+
280
+ # Capture all responses if not already cached
281
+ if not self.browser_responses_cache:
282
+ await self._capture_all_api_responses(keyword)
283
+
284
+ # Get cached response
285
+ response_data = self.browser_responses_cache.get('interest_by_region')
286
+
287
+ if not response_data:
288
+ raise exceptions.ResponseError("Failed to capture interest_by_region API response")
289
+
290
+ return self._parse_comparedgeo_response(response_data, inc_geo_code)
291
+
292
+ async def related_topics(self) -> Dict[str, Dict[str, Optional[pd.DataFrame]]]:
293
+ """
294
+ Get related topics (async)
295
+
296
+ Returns:
297
+ Dict with keyword as key and dict of 'top'/'rising' DataFrames as value
298
+ """
299
+ if len(self.kw_list) != 1:
300
+ raise exceptions.InvalidParameterError(
301
+ "Async browser mode only supports 1 keyword"
302
+ )
303
+
304
+ keyword = self.kw_list[0]
305
+
306
+ # Capture all responses if not already cached
307
+ if not self.browser_responses_cache:
308
+ await self._capture_all_api_responses(keyword)
309
+
310
+ # Get cached response
311
+ response_data = self.browser_responses_cache.get('related_topics')
312
+
313
+ if not response_data:
314
+ raise exceptions.ResponseError("Failed to capture related_topics API response")
315
+
316
+ return {keyword: self._parse_relatedsearches_response(response_data)}
317
+
318
+ async def related_queries(self) -> Dict[str, Dict[str, Optional[pd.DataFrame]]]:
319
+ """
320
+ Get related queries (async)
321
+
322
+ Returns:
323
+ Dict with keyword as key and dict of 'top'/'rising' DataFrames as value
324
+ """
325
+ if len(self.kw_list) != 1:
326
+ raise exceptions.InvalidParameterError(
327
+ "Async browser mode only supports 1 keyword"
328
+ )
329
+
330
+ keyword = self.kw_list[0]
331
+
332
+ # Capture all responses if not already cached
333
+ if not self.browser_responses_cache:
334
+ await self._capture_all_api_responses(keyword)
335
+
336
+ # Get cached response
337
+ response_data = self.browser_responses_cache.get('related_queries')
338
+
339
+ if not response_data:
340
+ raise exceptions.ResponseError("Failed to capture related_queries API response")
341
+
342
+ return {keyword: self._parse_relatedsearches_response(response_data)}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrends-modern
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Modern Google Trends API - Combining the best of pytrends, with RSS feeds, Selenium scraping, DrissionPage browser automation, and enhanced features
5
5
  Author: pytrends-modern contributors
6
6
  License: MIT
@@ -175,11 +175,16 @@ RUN mkdir -p /root/.config && \
175
175
  cd /root/.config && \
176
176
  tar -xzf /tmp/profile.tar.gz
177
177
 
178
- # 3. Use headless mode in container
179
- config = BrowserConfig(headless=True)
178
+ # 3. Use headless="virtual" in container
179
+ config = BrowserConfig(headless="virtual") # Use Xvfb for Docker
180
180
  ```
181
181
 
182
- See `Dockerfile.example` for complete Docker setup.
182
+ **Headless Options:**
183
+ - `headless=False` - Show browser window (local development)
184
+ - `headless=True` - Standard headless (servers with display)
185
+ - `headless="virtual"` - Xvfb virtual display (Docker containers)
186
+
187
+ See `Dockerfile.example` and `examples/example_docker_usage.py` for complete Docker setup.
183
188
 
184
189
  ⚠️ **Security**: Profile contains Google session - keep secure, don't commit to git!
185
190
 
@@ -5,6 +5,8 @@ pyproject.toml
5
5
  examples/advanced_usage.py
6
6
  examples/basic_usage.py
7
7
  examples/example_browser_mode.py
8
+ examples/example_docker_usage.py
9
+ examples/test_async_integration.py
8
10
  pytrends_modern/__init__.py
9
11
  pytrends_modern/browser_config.py
10
12
  pytrends_modern/browser_config_camoufox.py
@@ -15,6 +17,7 @@ pytrends_modern/exceptions.py
15
17
  pytrends_modern/proxy_extension.py
16
18
  pytrends_modern/py.typed
17
19
  pytrends_modern/request.py
20
+ pytrends_modern/request_async.py
18
21
  pytrends_modern/rss.py
19
22
  pytrends_modern/scraper.py
20
23
  pytrends_modern/utils.py
File without changes