fix(dataflows): trim global news to the window before the limit (#1356)

- out-of-window articles no longer spend the article budget or cut the remaining searches short
This commit is contained in:
Yijia-Xiao
2026-09-17 01:10:48 +00:00
parent c4152d6946
commit 8d64416436
2 changed files with 64 additions and 30 deletions

View File

@@ -213,3 +213,47 @@ def test_coverage_gap_future_window_is_unavailable():
today = datetime.now(timezone.utc).date() today = datetime.now(timezone.utc).date()
out = coverage_gap([], str(today), str(today + timedelta(days=3)), "Feed", "items") out = coverage_gap([], str(today), str(today + timedelta(days=3)), "Feed", "items")
assert out is not None and "past today" in out assert out is not None and "past today" in out
@pytest.mark.unit
def test_out_of_window_articles_do_not_consume_the_article_budget(monkeypatch):
"""The limit counts articles the run may see, not candidates fetched (#1356).
Out-of-window items were counted first, so they filled the budget, stopped
the remaining searches, and the in-window news was reported as absent.
"""
stale = [{"title": f"OLD {i}", "publisher": "P", "link": "l",
"providerPublishTime": _epoch("2025-01-01")} for i in range(2)]
wanted = {"title": "IN WINDOW", "publisher": "P", "link": "l",
"providerPublishTime": _epoch("2025-05-08")}
pages = [stale, [wanted]]
class FakeSearch:
def __init__(self, *a, **k):
self.news = pages.pop(0) if pages else []
monkeypatch.setattr(ynews.yf, "Search", FakeSearch)
monkeypatch.setattr(ynews, "get_config", lambda: {
"global_news_lookback_days": 7, "global_news_article_limit": 2,
"global_news_queries": ["markets", "economy"],
})
out = ynews.get_global_news_yfinance("2025-05-09")
assert "IN WINDOW" in out
assert "OLD 0" not in out
@pytest.mark.unit
def test_the_article_limit_still_caps_what_is_returned(monkeypatch):
articles = [{"title": f"NEWS {i}", "publisher": "P", "link": "l",
"providerPublishTime": _epoch("2025-05-08")} for i in range(5)]
class FakeSearch:
def __init__(self, *a, **k):
self.news = articles
monkeypatch.setattr(ynews.yf, "Search", FakeSearch)
out = ynews.get_global_news_yfinance("2025-05-09", look_back_days=7, limit=3)
assert out.count("### ") == 3

View File

@@ -146,7 +146,11 @@ def get_global_news_yfinance(
limit = config["global_news_article_limit"] limit = config["global_news_article_limit"]
search_queries = config["global_news_queries"] search_queries = config["global_news_queries"]
all_news = [] curr_dt = datetime.strptime(curr_date, "%Y-%m-%d")
start_dt = curr_dt - relativedelta(days=look_back_days)
start_date = start_dt.strftime("%Y-%m-%d")
in_window_news = []
seen_titles = set() seen_titles = set()
try: try:
@@ -157,47 +161,33 @@ def get_global_news_yfinance(
enable_fuzzy_query=True, enable_fuzzy_query=True,
)) ))
if search.news: for article in search.news or []:
for article in search.news: # Window first: the limit counts what the run may read, so an
# Handle both flat and nested structures # out-of-window article must not spend the budget or cut the
if "content" in article: # remaining searches short (#1356). Flat articles are filtered
data = _extract_article_data(article) # on the same rule, so none can leak future news (#1007).
title = data["title"]
else:
title = article.get("title", "")
# Deduplicate by title
if title and title not in seen_titles:
seen_titles.add(title)
all_news.append(article)
if len(all_news) >= limit:
break
# Calculate date range
curr_dt = datetime.strptime(curr_date, "%Y-%m-%d")
start_dt = curr_dt - relativedelta(days=look_back_days)
start_date = start_dt.strftime("%Y-%m-%d")
news_str = ""
kept = 0
for article in all_news[:limit]:
# Extract uniformly (flat + nested) and apply the same look-ahead-safe
# window filter, so flat articles can't leak future news (#1007).
data = _extract_article_data(article) data = _extract_article_data(article)
if not in_window(data["pub_date"], start_dt, curr_dt): if not in_window(data["pub_date"], start_dt, curr_dt):
continue continue
if data["title"] and data["title"] not in seen_titles:
seen_titles.add(data["title"])
in_window_news.append(data)
if len(in_window_news) >= limit:
break
news_str = ""
for data in in_window_news[:limit]:
news_str += f"### {data['title']} (source: {data['publisher']})\n" news_str += f"### {data['title']} (source: {data['publisher']})\n"
if data["summary"]: if data["summary"]:
news_str += f"{data['summary']}\n" news_str += f"{data['summary']}\n"
if data["link"]: if data["link"]:
news_str += f"Link: {data['link']}\n" news_str += f"Link: {data['link']}\n"
news_str += "\n" news_str += "\n"
kept += 1
# All candidates fell outside the window -> say so rather than return an # Nothing fell inside the window -> say so rather than return an
# empty-bodied report (#993). # empty-bodied report (#993).
if kept == 0: if not news_str:
# Results merge several fuzzy searches, so their timestamps prove no # Results merge several fuzzy searches, so their timestamps prove no
# continuous coverage; judge the window against the present only. # continuous coverage; judge the window against the present only.
gap = coverage_gap((), start_date, curr_date, "Yahoo Finance global news", "market news") gap = coverage_gap((), start_date, curr_date, "Yahoo Finance global news", "market news")