google-news-trends-mcp 0.1.10__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.
@@ -1,2 +1,3 @@
1
1
  import logging
2
+
2
3
  logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -7,6 +7,7 @@ from google_news_trends_mcp.news import (
7
7
  get_trending_terms,
8
8
  get_top_news,
9
9
  save_article_to_json,
10
+ BrowserManager,
10
11
  )
11
12
 
12
13
 
@@ -27,9 +28,13 @@ def cli():
27
28
  )
28
29
  @click.option("--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles.")
29
30
  def keyword(keyword, period, max_results, no_nlp):
30
- articles = asyncio.run(get_news_by_keyword(keyword, period=period, max_results=max_results, nlp=not no_nlp))
31
- # asyncio.run(articles) # Ensure the articles are fetched asynchronously
32
- print_articles(articles)
31
+ @BrowserManager()
32
+ async def _keyword():
33
+ articles = await get_news_by_keyword(keyword, period=period, max_results=max_results, nlp=not no_nlp)
34
+ print_articles(articles)
35
+ print(f"Found {len(articles)} articles for keyword '{keyword}'.")
36
+
37
+ asyncio.run(_keyword())
33
38
 
34
39
 
35
40
  @cli.command(help=get_news_by_location.__doc__)
@@ -44,8 +49,13 @@ def keyword(keyword, period, max_results, no_nlp):
44
49
  )
45
50
  @click.option("--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles.")
46
51
  def location(location, period, max_results, no_nlp):
47
- articles = asyncio.run(get_news_by_location(location, period=period, max_results=max_results, nlp=not no_nlp))
48
- print_articles(articles)
52
+ @BrowserManager()
53
+ async def _location():
54
+ articles = await get_news_by_location(location, period=period, max_results=max_results, nlp=not no_nlp)
55
+ print_articles(articles)
56
+ print(f"Found {len(articles)} articles for location '{location}'.")
57
+
58
+ asyncio.run(_location())
49
59
 
50
60
 
51
61
  @cli.command(help=get_news_by_topic.__doc__)
@@ -60,8 +70,13 @@ def location(location, period, max_results, no_nlp):
60
70
  )
61
71
  @click.option("--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles.")
62
72
  def topic(topic, period, max_results, no_nlp):
63
- articles = asyncio.run(get_news_by_topic(topic, period=period, max_results=max_results, nlp=not no_nlp))
64
- print_articles(articles)
73
+ @BrowserManager()
74
+ async def _topic():
75
+ articles = await get_news_by_topic(topic, period=period, max_results=max_results, nlp=not no_nlp)
76
+ print_articles(articles)
77
+ print(f"Found {len(articles)} articles for topic '{topic}'.")
78
+
79
+ asyncio.run(_topic())
65
80
 
66
81
 
67
82
  @cli.command(help=get_trending_terms.__doc__)
@@ -75,16 +90,20 @@ def topic(topic, period, max_results, no_nlp):
75
90
  help="Maximum number of results to return.",
76
91
  )
77
92
  def trending(geo, full_data, max_results):
78
- trending_terms = asyncio.run(get_trending_terms(geo=geo, full_data=full_data, max_results=max_results))
79
- if trending_terms:
80
- print("Trending terms:")
81
- for term in trending_terms:
82
- if isinstance(term, dict):
83
- print(f"{term['keyword']:<40} - {term['volume']}")
84
- else:
85
- print(term)
86
- else:
87
- print("No trending terms found.")
93
+ # Browser not used for Google Trends
94
+ async def _trending():
95
+ trending_terms = await get_trending_terms(geo=geo, full_data=full_data, max_results=max_results)
96
+ if trending_terms:
97
+ print("Trending terms:")
98
+ for term in trending_terms:
99
+ if isinstance(term, dict):
100
+ print(f"{term['keyword']:<40} - {term['volume']}")
101
+ else:
102
+ print(term)
103
+ else:
104
+ print("No trending terms found.")
105
+
106
+ asyncio.run(_trending())
88
107
 
89
108
 
90
109
  @cli.command(help=get_top_news.__doc__)
@@ -98,9 +117,13 @@ def trending(geo, full_data, max_results):
98
117
  )
99
118
  @click.option("--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles.")
100
119
  def top(period, max_results, no_nlp):
101
- articles = asyncio.run(get_top_news(max_results=max_results, period=period, nlp=not no_nlp))
102
- print_articles(articles)
103
- print(f"Found {len(articles)} top articles.")
120
+ @BrowserManager()
121
+ async def _top():
122
+ articles = await get_top_news(max_results=max_results, period=period, nlp=not no_nlp)
123
+ print_articles(articles)
124
+ print(f"Found {len(articles)} top articles.")
125
+
126
+ asyncio.run(_top())
104
127
 
105
128
 
106
129
  def print_articles(articles):
@@ -16,8 +16,7 @@ import cloudscraper
16
16
  from playwright.async_api import async_playwright, Browser, Playwright
17
17
  from trendspy import Trends, TrendKeyword
18
18
  from typing import Optional, cast, overload, Literal, Awaitable
19
- import atexit
20
- from contextlib import asynccontextmanager
19
+ from contextlib import asynccontextmanager, AsyncContextDecorator
21
20
  import logging
22
21
  from collections.abc import Callable
23
22
 
@@ -55,34 +54,56 @@ browser: Optional[Browser] = None
55
54
  ProgressCallback = Callable[[float, Optional[float]], Awaitable[None]]
56
55
 
57
56
 
58
- async def startup_browser():
59
- global playwright, browser
60
- playwright = await async_playwright().start()
61
- browser = await playwright.chromium.launch(headless=True)
62
-
63
-
64
- @atexit.register
65
- def shutdown_browser():
66
- if browser:
67
- asyncio.run(browser.close())
68
- if playwright:
69
- asyncio.run(playwright.stop())
70
-
71
-
72
- async def get_browser() -> Browser:
73
- if browser is None:
74
- await startup_browser()
75
- return cast(Browser, browser)
76
-
77
-
78
- @asynccontextmanager
79
- async def browser_context():
80
- context = await (await get_browser()).new_context()
81
- try:
82
- yield context
83
- finally:
84
- logging.debug("Closing browser context...")
85
- await context.close()
57
+ class BrowserManager(AsyncContextDecorator):
58
+ playwright: Optional[Playwright] = None
59
+ browser: Optional[Browser] = None
60
+ _lock = asyncio.Lock()
61
+
62
+ @classmethod
63
+ async def _get_browser(cls) -> Browser:
64
+ if cls.browser is None:
65
+ async with cls._lock:
66
+ if cls.browser is None:
67
+ await cls._startup()
68
+ return cast(Browser, cls.browser)
69
+
70
+ @classmethod
71
+ async def _startup(cls):
72
+ logger.info("Starting browser...")
73
+ cls.playwright = await async_playwright().start()
74
+ cls.browser = await cls.playwright.chromium.launch(headless=True)
75
+
76
+ @classmethod
77
+ async def _shutdown(cls):
78
+ logger.info("Shutting down browser...")
79
+ if cls.browser:
80
+ await cls.browser.close()
81
+ cls.browser = None
82
+ if cls.playwright:
83
+ await cls.playwright.stop()
84
+ cls.playwright = None
85
+
86
+ @classmethod
87
+ def browser_context(cls):
88
+ @asynccontextmanager
89
+ async def _browser_context_cm():
90
+ browser_inst = await cls._get_browser()
91
+ context = await browser_inst.new_context()
92
+ logging.debug("Created browser context...")
93
+ try:
94
+ yield context
95
+ finally:
96
+ logging.debug("Closing browser context...")
97
+ await context.close()
98
+
99
+ return _browser_context_cm()
100
+
101
+ async def __aenter__(self):
102
+ return self
103
+
104
+ async def __aexit__(self, *exc):
105
+ await self._shutdown()
106
+ return False
86
107
 
87
108
 
88
109
  async def download_article_with_playwright(url) -> newspaper.Article | None:
@@ -90,7 +111,7 @@ async def download_article_with_playwright(url) -> newspaper.Article | None:
90
111
  Download an article using Playwright to handle complex websites (async).
91
112
  """
92
113
  try:
93
- async with browser_context() as context:
114
+ async with BrowserManager.browser_context() as context:
94
115
  page = await context.new_page()
95
116
  await page.goto(url, wait_until="domcontentloaded")
96
117
  await asyncio.sleep(2) # Wait for the page to load completely
@@ -144,6 +165,7 @@ async def download_article(url: str) -> newspaper.Article | None:
144
165
  return None
145
166
  article = download_article_with_scraper(url)
146
167
  if article is None or not article.text:
168
+ logger.debug("Attempting to download article with playwright")
147
169
  article = await download_article_with_playwright(url)
148
170
  return article
149
171
 
@@ -217,8 +239,7 @@ async def get_news_by_location(
217
239
  nlp: bool = True,
218
240
  report_progress: Optional[ProgressCallback] = None,
219
241
  ) -> list[newspaper.Article]:
220
- """Find articles by location using Google News.
221
- """
242
+ """Find articles by location using Google News."""
222
243
  google_news.period = f"{period}d"
223
244
  google_news.max_results = max_results
224
245
  gnews_articles = google_news.get_news_by_location(location)
@@ -7,8 +7,10 @@ from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
7
7
  from mcp.types import TextContent
8
8
  from pydantic import BaseModel, Field, model_serializer
9
9
  from google_news_trends_mcp import news
10
+ from google_news_trends_mcp.news import BrowserManager
10
11
  from newspaper import settings as newspaper_settings
11
12
  from newspaper.article import Article
13
+ from contextlib import asynccontextmanager
12
14
 
13
15
 
14
16
  class BaseModelClean(BaseModel):
@@ -82,9 +84,16 @@ class TrendingTermOut(BaseModelClean):
82
84
  normalized_keyword: Annotated[Optional[str], Field(description="Normalized form of the keyword.")] = None
83
85
 
84
86
 
87
+ @asynccontextmanager
88
+ async def lifespan(app: FastMCP):
89
+ async with BrowserManager():
90
+ yield
91
+
92
+
85
93
  mcp = FastMCP(
86
94
  name="google-news-trends",
87
95
  instructions="This server provides tools to search, analyze, and summarize Google News articles and Google Trends",
96
+ lifespan=lifespan,
88
97
  on_duplicate_tools="replace",
89
98
  )
90
99
 
@@ -328,7 +337,6 @@ async def get_trending_terms(
328
337
  ] = False,
329
338
  max_results: Annotated[int, Field(description="Maximum number of results to return.", ge=1)] = 100,
330
339
  ) -> list[TrendingTermOut]:
331
-
332
340
  if not full_data:
333
341
  trends = await news.get_trending_terms(geo=geo, full_data=False, max_results=max_results)
334
342
  return [TrendingTermOut(keyword=str(tt["keyword"]), volume=tt["volume"]) for tt in trends]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-news-trends-mcp
3
- Version: 0.1.10
3
+ Version: 0.2.0
4
4
  Summary: An MCP server to access Google News and Google Trends.
5
5
  Author-email: Jesse Manek <jesse.manek@gmail.com>
6
6
  License-Expression: MIT
@@ -22,8 +22,6 @@ Requires-Dist: newspaper4k>=0.9.3.1
22
22
  Requires-Dist: nltk>=3.9.1
23
23
  Requires-Dist: playwright>=1.53.0
24
24
  Requires-Dist: pydantic>=2.11.7
25
- Requires-Dist: pytest>=8.4.1
26
- Requires-Dist: pytest-asyncio>=1.0.0
27
25
  Requires-Dist: trendspy>=0.1.6
28
26
  Dynamic: license-file
29
27
 
@@ -0,0 +1,11 @@
1
+ google_news_trends_mcp/__init__.py,sha256=nDWNd6_TSf4vDQuHVBoAf4QfZCB3ZUFQ0M7XvifNJ-g,78
2
+ google_news_trends_mcp/__main__.py,sha256=ysiAk_xpnnW3lrLlzdIQQa71tuGBRT8WocbecBsY2Fs,87
3
+ google_news_trends_mcp/cli.py,sha256=3Z916898HXTigmQYEfvb7ybfbuUE7bjMC6yjT5-l6u0,4558
4
+ google_news_trends_mcp/news.py,sha256=MPNZlzI7KXkhQ2uj7233N2i9kFHGUgGMdRBCAbj-B44,12471
5
+ google_news_trends_mcp/server.py,sha256=S-tlFY1wiFm9VPeb4NDnV0NGtczaQDmx20kIrZZQHto,15031
6
+ google_news_trends_mcp-0.2.0.dist-info/licenses/LICENSE,sha256=5dsv2ZI5EZIer0a9MktVmILVrlp5vqH_0tPIe3bRLgE,1067
7
+ google_news_trends_mcp-0.2.0.dist-info/METADATA,sha256=rok_3L-eDVXQJSuG6ze1Vuicnh-kpcWyJxaF2DbqZ1s,4454
8
+ google_news_trends_mcp-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ google_news_trends_mcp-0.2.0.dist-info/entry_points.txt,sha256=eVT3xd6YJQgsWAUBwhnffuwhXNF7yyt_uco6fjBy-1o,130
10
+ google_news_trends_mcp-0.2.0.dist-info/top_level.txt,sha256=RFheDbzhNnEV_Y3iFNm7jhRhY1P1wQgfiYqVpXCTD_U,23
11
+ google_news_trends_mcp-0.2.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- google_news_trends_mcp/__init__.py,sha256=NkmudPEEuKk8Geah4EtzeEHQ-ChqR66lZEO5VrMwXNo,77
2
- google_news_trends_mcp/__main__.py,sha256=ysiAk_xpnnW3lrLlzdIQQa71tuGBRT8WocbecBsY2Fs,87
3
- google_news_trends_mcp/cli.py,sha256=-Cith02x6-9o91rXpgMM0lrhArPDMB9d3h8AAE1rimw,3959
4
- google_news_trends_mcp/news.py,sha256=wmFzikDEx_HNVO0vm84gnkgV-LZBOAuW7mNr2uhurEE,11524
5
- google_news_trends_mcp/server.py,sha256=h8GP_XUPqiPw4vFu1jy9MFv0i384rBARePvm15YOZJo,14807
6
- google_news_trends_mcp-0.1.10.dist-info/licenses/LICENSE,sha256=5dsv2ZI5EZIer0a9MktVmILVrlp5vqH_0tPIe3bRLgE,1067
7
- google_news_trends_mcp-0.1.10.dist-info/METADATA,sha256=rbYqd15smnZA0sgOU4Fk6iqvsr8h638U69Ki4VRMddI,4521
8
- google_news_trends_mcp-0.1.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- google_news_trends_mcp-0.1.10.dist-info/entry_points.txt,sha256=eVT3xd6YJQgsWAUBwhnffuwhXNF7yyt_uco6fjBy-1o,130
10
- google_news_trends_mcp-0.1.10.dist-info/top_level.txt,sha256=RFheDbzhNnEV_Y3iFNm7jhRhY1P1wQgfiYqVpXCTD_U,23
11
- google_news_trends_mcp-0.1.10.dist-info/RECORD,,