Parse official Michigan survey archive reports

Extract original preliminary and final results with publication dates and validated table headers. Add annual report discovery and real report fixtures covering cross-month releases and PDF text layout differences.
This commit is contained in:
eezd committed 2026-10-26 17:19:37 +08:00
1 parent 0886e74e21
commit da289686be
7 files changed
+397 -3

No files matched your search

+32 -1
View File
@@ -1,7 +1,7 @@
"""官方当期公告解析;归档内容不被标为公布时就已采集的原始版本。"""
import re
from dataclasses import dataclass
from datetime import UTC, datetime
from datetime import UTC, datetime, timedelta
from parsel import Selector
@@ -20,6 +20,37 @@ class Announcement:
values: dict
def parse_umich_report(text: str) -> Announcement:
"""取当期 PDF 最后一列;前十二列只是报告当时所列历史值。"""
text = compact(text)
# 2026-02 PDF 的首字母 P 是单独的文字块,提取后与 reliminary 之间有换行。
heading = re.search(r"(?:P\s*reliminary results from the (" + MONTH_PATTERN + r") (20\d\d) survey|(" +
MONTH_PATTERN + r") (20\d\d) survey results)\s+(" + MONTH_PATTERN + r" \d{1,2}, 20\d\d)", text)
if heading is None:
raise ValueError("密歇根历史报告缺少统计期、发布阶段或正式日期")
month, year = (heading[1], heading[2]) if heading[1] else (heading[3], heading[4])
period = f"{year}-{MONTHS[month]:02d}"
stage = "preliminary" if heading[1] else "final"
at = datetime.strptime(heading[5], "%B %d, %Y").replace(hour=10, tzinfo=EASTERN).astimezone(UTC)
table = re.search(r"((?:[A-Z][a-z]{2}\s+){13})((?:(?:20\d\d|Prelim)\s+){13})"
r"Index of Consumer Sentiment\s+((?:\d+(?:\.\d+)?\s+){13})Current Economic Conditions", text)
if table is None:
raise ValueError("密歇根历史报告缺少完整的消费者信心表头或数值行")
months, years, values = (group.split() for group in table.groups())
current = datetime(int(year), MONTHS[month], 1)
previous = (current - timedelta(days=1)).strftime("%Y-%m")
# 初值最后一列表头用 Prelim 代替年份;其他十二列必须与统计月份逐列一致。
for index in range(13):
expected_month = (current.month - 1 + index) % 12 + 1
expected_year = current.year - 1 + (current.month - 1 + index) // 12
expected_label = "Prelim" if index == 12 and stage == "preliminary" else str(expected_year)
if months[index] != datetime(2000, expected_month, 1).strftime("%b") or years[index] != expected_label:
raise ValueError("密歇根历史报告表头与统计期或发布阶段不符")
return Announcement(at, period, stage, {"umich_sentiment": {
"actual": number(values[-1]), "previous": number(values[-2]), "previous_basis": f"{previous} 官方上月终值",
}})
def signed(match: re.Match) -> str:
sign = "-" if match[1].lower() in {"declined", "decreased", "fell", "edged down", "moved down"} else ""
return number(sign + match[2])
+22
View File
@@ -29,6 +29,28 @@ def document_links(body: str, base: str, labels: set[str]) -> dict[str, str]:
return {label: links[label] for label in labels if label in links}
def report_links(body: str, year: int) -> dict[tuple[str, str], str]:
"""年度索引只取初值、终值报告,不把数据手册或专题报告当作当期公告。"""
sel = Selector(text=body, type="html")
if compact(sel.css("#report_heading .title").xpath("string(.)").get()) != f"{year} Reports":
raise ValueError("密歇根历史报告索引年份不符")
reports = {}
for node in sel.css("a[href]"):
match = re.fullmatch(r"([A-Z][a-z]+) (Preliminary|Final) Results", compact(node.xpath("string(.)").get()))
if match is None:
continue
identity = (f"{year}-{MONTHS[match[1]]:02d}", "preliminary" if match[2] == "Preliminary" else "final")
url = urljoin("https://data.sca.isr.umich.edu/", node.attrib["href"])
if not re.fullmatch(r"https://data\.sca\.isr\.umich\.edu/fetchdoc\.php\?docid=\d+", url):
raise ValueError("密歇根历史报告链接不是官方文档地址")
if identity in reports:
raise ValueError("密歇根历史报告索引存在重复期次")
reports[identity] = url
if not reports:
raise ValueError("密歇根年度索引没有可识别的初值或终值报告")
return reports
def parse_schedule(text: str) -> list[Scheduled]:
events = []
# 历史 PDF 明确列出调查月份、初值日期和终值日期,包括跨月公布。
+78
View File
@@ -0,0 +1,78 @@
© The University of Michigan, 2025. All rights reserved.
Monitoring trends for over 75 years
January 2025 survey results January 24, 2025
Joanne Hsu, PhD, Director
Consumer sentiment fell for the first time in six months, edging down 4%
from December. While assessments of personal finances inched up for the
fifth consecutive month, all other index components pulled back. Indeed,
sentiment declines were broad based and seen across incomes, wealth, and
age groups. Buying conditions for durables softened but remained about
30% better than six months ago amid persistent views that purchasing now
would avoid future price increases. Despite reporting stronger incomes
this month, concerns about unemployment rose; about 47% of consumers
expect unemployment to rise in the year ahead, the highest since the
pandemic recession. January’s data closed on Inauguration Day, and
consumers of all political leanings will continue to refine their views as Trump’s policies are clarified and implemented.
Year-ahead inflation expectations soared from 2.8% last month to 3.3% this month. The current reading is the highest since
May 2024 and is above the 2.3-3.0% range seen in the two years prior to the pandemic. Long-run inflation expectations rose
from 3.0% last month to 3.2% this month, the same reading seen in November 2024. For both the short and long run,
inflation expectations rose across income and educational groups. Concerns over the future trajectory of inflation were
visible throughout the interviews and were tied to beliefs about anticipated policies like tariffs. Consumers continued to
spontaneously express motives for buying-in-advance to avoid future price increases, and robust auto and retail sales data
suggest that consumers are indeed acting on these views.
Consumers’ concerns over the path of inflation appear closely tied to their views on anticipated economic policies under
the new president. Since the election, inflation expectations have risen for Democrats and fallen for Republicans. These
trends are consistent with historical patterns. During the Trump to Biden transition, the inflation expectations of Republicans
and Democrats similarly rotated around the national average, albeit against a backdrop of sustained low inflation in the
2010s. This month’s interviews make it abundantly clear that the current divergence in inflation expectations is shaped by
beliefs about future policy, rather than a form of political expression divorced from economic factors.
In January, consumers were asked if they thought higher tariffs would be better for the economy, or if lower tariffs would
be better, or if tariff policy would make no difference for the economy. As seen in the chart, about 19% of consumers
reported higher tariffs would be better for the economy, while 62% favored lower tariffs. Strikingly, those favoring higher
tariffs had much lower inflation expectations, both year-ahead and long-run, than those favoring lower tariffs and those who
believed tariff policy has little economic impact. These patterns suggest that consumers expect Trump to hold true to his
plan to hike tariffs and have incorporated the anticipated policy change into their economic expectations.
Responses to the tariff question were not fully correlated with party affiliation; less than half of Republicans reported that
higher tariffs would be better for the economy. Younger, more-educated, or higher-income consumers were more likely to
favor lower tariffs for the economy. Note that the interview focused on the perceived impact of tariffs on the economy,
rather than the overall policy preferences of the consumer. Some consumers may believe that lower tariffs are better for the
economy but prefer higher tariffs for non-economic reasons, say as a negotiating strategy or to advance other goals.
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec Jan
2024 2024 2024 2024 2024 2024 2024 2024 2024 2024 2024 2024 2025
Index of Consumer Sentiment 79.0 76.9 79.4 77.2 69.1 68.2 66.4 67.9 70.1 70.5 71.8 74.0 71.1
Current Economic Conditions 81.9 79.4 82.5 79.0 69.6 65.9 62.7 61.3 63.3 64.9 63.9 75.1 74.0
Index of Consumer Expectations 77.1 75.2 77.4 76.0 68.8 69.6 68.8 72.1 74.4 74.1 76.9 73.3 69.3
Index Components
Personal Finances - Current 100 98 104 95 91 81 79 79 80 81 82 85 86
Personal Finances - Expected 122 116 122 116 111 106 105 108 111 110 116 111 108
Economic Outlook - 12 Months 93 93 95 92 79 84 86 88 93 92 97 88 81
Economic Outlook - 5 Years 94 92 94 96 84 88 84 92 94 95 96 94 88
Buying Conditions - Durables 111 106 109 109 88 88 81 78 82 85 82 108 104
-1
0
1
2
3
4
5
Higher Tariffs
Better for Economy
(19% of Consumers)
Lower Tariffs
Better for Economy
(62% of Consumers)
No Difference
for Economy
(19% of Consumers)
Median Inflation Expectations %
Inflation Expectations Reflect Views on Policies
Consumers who Favor Higher Tariffs Expect Lower Inflation
Year-Ahead Inflation
Expectations
Long-Run Inflation
Expectations
@@ -0,0 +1,68 @@
© The University of Michigan, 2025. All rights reserved.
Monitoring trends for over 75 years
Preliminary results from the January 2025 survey January 10, 2025
Joanne Hsu, PhD, Director
Consumer sentiment was essentially unchanged in January, inching down
less than one index point from December, well within the margin of error.
Assessments of personal finances improved about 5%, while the economic
outlook fell back 7% for the short run and 5% for the long r un. January’s
divergence in views of the present and the future reflects easing concerns
over the current cost of living this month, but surging worries over the future
path of inflation. Overall, this month’s deterioration in the expectations index
was se en acro ss political affiliation s, including decline s of about 3% for
Independents and 1.5% for Republicans.
Year-ahead inflation expectations soared from 2.8% last month to 3.3% this month. The current reading is the highest since
May 2024 and is above the 2.3-3.0% range seen in the two years prior to the pandemic. Long-run inflation expectations rose
from 3.0% last month to 3.3% this month. This is only the third time in the last four years that long-run expectations have
exhibited such a large one -month change. For both the short and long run , inflation expectations rose across multiple
demographic groups, with particularly strong increases among lower -income co nsumers and Independents. Note that
inflation uncertainty—as estimated using the interquartile range in inflation expectations —has climbed considerably over
the past year, though it remains well below levels seen in the 1970s (see chart). October 2024’s special report provides
additional context for understanding the path of long-run inflation expectations over the last seven years.
Inflation expectations are being shaped by beliefs about future policy. The share of consumers worried about the prospect
of tariff hikes continues to rise. Nearly one-third of consumers spontaneously mentioned tariffs, up from 24% in December
and less than 2% prior to the election. These consumers generally report that tariff hikes will pass through to consumers in
the form of higher prices, thus pushing up their inflation expectations.
Concerningly, recent data show an emergence of inflationary psychology —motives for buying-in-advance to avoid future
price increases, the proliferation of which would generate further momentum for inf lation. Like in December, 22% of
consumers this month reported that purchasing durables now would enable buyers to avoid future price hikes, the highest
since 1990. That said, these buy-in-advance price rationales were far more prevalent in the 1970s, averaging 41% in 1978.
Expectations for the future weakened on other dimensions as well. Half of consumers expect unemployment rates to rise in
the year ahead, up from 40% last month and 31% a year ago. This month’s worsening was concentrated among Independents
and Republicans. Consumers’ expectations over their own income growth also fell from last month in both nominal and real
terms. Only one-third of consumers expect interest rates to fall over the next year, down from 41% in December and falling
for the fourth straight month. All told, consumers perceive numerous sources of downside risk to the economy.
Despite their worries for the future, consumers are fully aware that current inflation has slowed over the past two or so
years. In fact, the 4% increase in Current Conditions this month was largely supported by softening worries over current
prices. About 32% of consumers spontaneously cited high prices as weighing down their personal finances, down from 47%
in August 2024 and the lowest seen since February 2022. For buying conditions for large purchases, spontaneous mentions
of high prices have fallen over the last five months for durable goods and are the lowest since May 2021 for vehicles.
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec Jan
2024 2024 2024 2024 2024 2024 2024 2024 2024 2024 2024 2024 Prelim
Index of Consumer Sentiment 79.0 76.9 79.4 77.2 69.1 68.2 66.4 67.9 70.1 70.5 71.8 74.0 73.2
Current Economic Conditions 81.9 79.4 82.5 79.0 69.6 65.9 62.7 61.3 63.3 64.9 63.9 75.1 77.9
Index of Consumer Expectations 77.1 75.2 77.4 76.0 68.8 69.6 68.8 72.1 74.4 74.1 76.9 73.3 70.2
Index Components
Personal Finances - Current 100 98 104 95 91 81 79 79 80 81 82 85 89
Personal Finances - Expected 122 116 122 116 111 106 105 108 111 110 116 111 110
Economic Outlook - 12 Months 93 93 95 92 79 84 86 88 93 92 97 88 82
Economic Outlook - 5 Years 94 92 94 96 84 88 84 92 94 95 96 94 89
Buying Conditions - Durables 111 106 109 109 88 88 81 78 82 85 82 108 112
0
2
4
6
8
10
1977 1982 1987 1992 1997 2002 2007 2012 2017 2022
Uncertainty - Year-Ahead Inflation Expectations
Uncertainty - 5-10 Year Inflation Expectations
Consumers Express Rising Uncertainty Over Path of Inflation
Uncertainty Estimated by 75th Percentile – 25th Percentile of Expectations
3-Month Moving Averages
University of Michigan, January 10, 2025
+77
View File
@@ -0,0 +1,77 @@
© The University of Michigan, 2025. All rights reserved.
Monitoring trends for over 75 years
July 2025 survey results August 1, 2025
Joanne Hsu, PhD, Director
Consumer sentiment improved for the second straight month, inching up
a scant single index point from June. Current conditions rose about 5% to
its highest reading since February 2025, while the expectations index fell
slightly. A rise in sentiment among stock holders was partially offset by a
decline among consumers who do not own stocks . Perceptions of this
month’s economic developments were similar across the political
spectrum; Republicans, Independents, and Democrats all saw some minor
increases in sentiment this month. Although recent trends show sentiment
moving in a favorable direction, sentiment remains broadly negative.
Consumers are hardly optimistic about the trajectory of the economy, even as their worries have softened since April 2025.
Year-ahead inflation expectations fell for a second straight month, plunging from 5.0% last month to 4.5% this month. This
is the lowest reading since February 2025 but above December 2024 just after the election. Long-run inflation expectations
receded for the third consecut ive month, falling back from 4.0% in June to 3.4% in July. This is the lowest reading since
January 2025 but, again, still considerably higher than the December 2024 reading.
Labor market expectations improved just a touch but remain considerably weaker than a year ago. About 57% of consumers
expect unemployment to rise in the year ahead, down from the two-thirds seen in March and April, but well above the 35%
seen a year ago (see chart, red solid line). Aside from the last five months, this is the highest reading since 2009. Moreover,
consumers continue to perceive elevated risks of personal job loss (see chart, black dotted line). Expectations of own
incomes remain extremely low and are less than half of what was seen in December 2024.
Sentiment this year has tracked the salience of tariffs to consumers. The share of consumers spontaneously offering
comments about tariffs has fallen from a height of two-thirds of consumers in May to 57%, still a solid majority, in July.
While tariff policy appears to be stabilizing below the levels threatened in April, recent announcements include tariff rates
that are still very high from a historical perspective. Tariff concerns are particularly strong among higher -income, high-
stock-wealth, and college-educated consumers. These consumers note that their personal finances have been boosted by
strong asset values, but their personal views of the economy are still relatively gloomy, which presents its own risks given
their large contribution to aggregate spending.
Overall, the survey reveals risks to both of the Fed’s mandates. While inflation expectations have fallen considerably from
the initial shock of the April tariff threats, they remain notably higher than the pre-pandemic period. Consumers clearly do
not believe that the risk of accelerating inflation has passed. Meanwhile, expectations remain poor for business conditions
and elevated for unemployment; critically, consumers anticipate they may be personally affected. Despite recent
improvements, this combination of views is consistent with a slowdown in spending, as consumers may respond to these
risks with more cautious financial behavior. At the same time, a cautionary pullback in spending could potentially mediate
any inflationary effects of tariffs.
Jul Aug Sep Oct Nov Dec Jan Feb Mar Apr May Jun Jul
2024 2024 2024 2024 2024 2024 2025 2025 2025 2025 2025 2025 2025
Index of Consumer Sentiment 66.4 67.9 70.1 70.5 71.8 74.0 71.7 64.7 57.0 52.2 52.2 60.7 61.7
Current Economic Conditions 62.7 61.3 63.3 64.9 63.9 75.1 75.1 65.7 63.8 59.8 58.9 64.8 68.0
Index of Consumer Expectations 68.8 72.1 74.4 74.1 76.9 73.3 69.5 64.0 52.6 47.3 47.9 58.1 57.7
Index Components
Personal Finances - Current 79 79 80 81 82 85 87 82 74 71 67 78 83
Personal Finances - Expected 105 108 111 110 116 111 109 99 86 78 79 94 91
Economic Outlook - 12 Months 86 88 93 92 97 88 81 73 56 48 50 63 66
Economic Outlook - 5 Years 84 92 94 95 96 94 88 83 66 60 60 74 72
Buying Conditions - Durables 81 78 82 85 82 108 106 86 89 82 83 88 91
13
15
17
19
21
23
25
0
10
20
30
40
50
60
70
80
1997 2001 2005 2009 2013 2017 2021 2025
Probability of Losing a Job During the
Next 5 Years (3-Month Movng Average)
% of Consumers Expecting More Unemployment
During the Next Year (3-Month Moving Average)
Despite Recent Improvements, Consumers Still
Anticipate Rising Unemployment and Elevated
Risks of Personal Job Loss
@@ -0,0 +1,73 @@
© The University of Michigan, 2026. All rights reserved.
Monitoring trends for over 75 years
P
reliminary results from the February 2026 survey February 6, 2026
Joanne Hsu, PhD, Director
Consumer sentiment was essentially unchanged, inching up less than
one index point from last month and sitting about 20% below January
2025. Sentiment surged for consumers with the largest stock
portfolios, while it stagnated and remained at dismal levels for
consumers without stock holdings . On net, modest increases in
current personal finances and buying conditions for durables were
offset by a small decline in long- run business conditions. W hile
sentiment is currently the highest since August 2025, recent monthly
increases have been small—well under the margin of error—and the
overall level of sentiment remains very low from a historical
Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec Jan Feb
2025 2025 2025 2025 2025 2025 2025 2025 2025 2025 2025 2026 Prelim
Index of Consumer Sentiment 64.7 57.0 52.2 52.2 60.7 61.7 58.2 55.1 53.6 51.0 52.9 56.4 57.3
Current Economic Conditions 65.7 63.8 59.8 58.9 64.8 68.0 61.7 60.4 58.6 51.1 50.4 55.4 58.3
Index of Consumer Expectations 64.0 52.6 47.3 47.9 58.1 57.7 55.9 51.7 50.3 51.0 54.6 57.0 56.6
Index Components
Personal Finances - Current 82 74 71 67 78 83 77 76 78 66 68 74 77
Personal Finances - Expected 99 86 78 79 94 91 91 86 82 85 93 95 95
Economic Outlook - 12 Months 73 56 48 50 63 66 61 57 57 56 59 63 64
Economic Outlook - 5 Years 83 66 60 60 74 72 70 62 59 61 64 68 65
Buying Conditions - Durables 86 89 82 83 88 91 80 78 72 64 60 67 72
40
50
60
70
80
90
100
110
2018 2019 2020 2021 2022 2023 2024 2025 2026
Index of Consumer Sentiment
Top Tercile of Stock
Holdings, 3mma
No Stock Holdings,
3mma
Wealth Gaps in Sentiment Continue to Widen,
Mirroring Trends Seen in 2023 and 2024
perspective. Concerns about the erosion of personal finances from high prices and elevated risk of job loss continue to
be
widespread. Interviews for this release cover the two-week period that ended this past Monday.
Year-ahead inflation expectations fell from 4.0% last month to 3.5 % this month, the lowest reading since January 2025.
This month’s reading still exceeds those seen in 2024 and remains well above the 2.3-3.0% range seen in the two years pre-
pandemic. Long-run inflation expectations inched up for the second straight month, from 3.3% last month to 3.4% this
month. In comparison, readings ranged between 2.8% and 3.2% in 2024, and were below 2.8% throughout 2019 and 2020.
Overall views of labor markets remain considerably weaker than a year ago, though changes from last month are a bit mixed.
This month, a substantial 60% of consumers expect unemployment to rise in the year ahead, down a touch from January
and the lowest share seen since July 2025. Meanwhile, the expected probability of own job loss climbed to its highest
reading since July 2020. Thus, consumers continue to perceive risks to both sides of the Fed’s dual mandate.
While views of the economy converged across population subgroups last spring amid announcements of the new tariff
regimes, the 10 or so months thereafter have been characterized by a sharp divergence between higher -wealth, higher-
income consumers and their lower-wealth, lower-income counterparts. As seen in the chart, sentiment for the largest
stockholders is currently nearly 50% higher than May 2025, when national sentiment reached a trough. In contrast,
sentiment for non-stockholders fell 6% during this period.
These trends are consistent with the fact that asset values have soared, which benefits asset owners but not others. Notably,
similar divergent patterns in sentiment were seen in 2023-2024 as well. Sentiment for high and low wealth groups converged
in 2022 when post -Covid inflation peaked. In the two years that followed , sentiment for the wealthy soared while it
languished for lower-wealth consumers. As such, the K-shape of sentiment seen over the past year is not a new phenomenon.
These patterns highlight the ongoing heterogeneity in experiences and expectations for the economy that are obscured by
headline aggregate statistics about the economy. Robust aggregate spending is consistent both with strength stemming from
the disproportionate share of spending generated by wealthier consumers who are more confident, as well as the fact that
less-confident, lower-wealth consumers may be exercising much more care over their spending amid pressures from high
prices and weakening income prospects.
Top Tercile of
Stock Holdings
Feb-2026 (monthly)
No Stock Holdings Feb-2026 (monthly)
+47 -2
View File
@@ -1,12 +1,57 @@
import asyncio
from datetime import UTC, date, datetime
from pathlib import Path
from types import SimpleNamespace
import pytest
from alphapulse.calendar.archives import parse_announcements, parse_bls_text
from alphapulse.calendar.archives import parse_announcements, parse_bls_text, parse_umich_report
from alphapulse.calendar.parsers import bls_periods, release_metadata
from alphapulse.calendar.umich import current_result, monthly_series, parse_schedule
from alphapulse.calendar.umich import current_result, monthly_series, parse_schedule, report_links
@pytest.mark.parametrize("name,stage,at,actual,previous", [
("2025-01-preliminary", "preliminary", datetime(2025, 1, 10, 15, tzinfo=UTC), "73.2", "74.0"),
("2025-01-final", "final", datetime(2025, 1, 24, 15, tzinfo=UTC), "71.1", "74.0"),
("2025-07-final", "final", datetime(2025, 8, 1, 14, tzinfo=UTC), "61.7", "60.7"),
("2026-02-preliminary", "preliminary", datetime(2026, 2, 6, 15, tzinfo=UTC), "57.3", "56.4"),
])
def test_umich_report_preserves_original_vintage_stage_and_real_publication_date(name, stage, at, actual, previous):
body = (Path(__file__).parent / "fixtures/calendar" / f"umich-{name}.txt").read_text()
result = parse_umich_report(body)
assert (result.period, result.stage, result.at) == (name[:7], stage, at)
assert result.values["umich_sentiment"]["actual"] == actual
assert result.values["umich_sentiment"]["previous"] == previous
@pytest.mark.parametrize("old,new", [
("January 2025 survey results", "February 2025 survey results"),
("2024 2024 2024 2025", "2024 2024 2024 Prelim"),
("74.0 71.1", "74.0"),
("January 24, 2025", "Date unavailable"),
])
def test_umich_report_rejects_inconsistent_header_missing_value_or_missing_date(old, new):
body = (Path(__file__).parent / "fixtures/calendar/umich-2025-01-final.txt").read_text()
assert old in body
with pytest.raises(ValueError):
parse_umich_report(body.replace(old, new))
def test_umich_index_only_accepts_official_stage_reports_for_the_requested_year():
body = '''<div id="report_heading"><span class="title">2025 Reports</span></div>
<a href="fetchdoc.php?docid=1">January Preliminary Results</a>
<a href="fetchdoc.php?docid=2">January Final Results</a>
<a href="fetchdoc.php?docid=3">January Data Booklet</a>
<a href="fetchdoc.php?docid=4">Revisions to Estimates</a>'''
assert report_links(body, 2025) == {
("2025-01", "preliminary"): "https://data.sca.isr.umich.edu/fetchdoc.php?docid=1",
("2025-01", "final"): "https://data.sca.isr.umich.edu/fetchdoc.php?docid=2",
}
for invalid in (body.replace("2025 Reports", "2026 Reports"),
body.replace("fetchdoc.php?docid=1", "https://example.com/foreign.pdf"),
body + '<a href="fetchdoc.php?docid=9">January Final Results</a>'):
with pytest.raises(ValueError):
report_links(invalid, 2025)
@pytest.mark.parametrize("title,period,stage", [