fred-loader 0.1.2__tar.gz → 0.1.4__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.
- {fred_loader-0.1.2/src/fred_loader.egg-info → fred_loader-0.1.4}/PKG-INFO +13 -1
- {fred_loader-0.1.2 → fred_loader-0.1.4}/pyproject.toml +18 -2
- {fred_loader-0.1.2 → fred_loader-0.1.4}/src/fred_loader/load.py +2 -2
- {fred_loader-0.1.2 → fred_loader-0.1.4}/src/fred_loader/macro_scores.py +1 -1
- {fred_loader-0.1.2 → fred_loader-0.1.4}/src/fred_loader/utils.py +69 -9
- {fred_loader-0.1.2 → fred_loader-0.1.4/src/fred_loader.egg-info}/PKG-INFO +13 -1
- {fred_loader-0.1.2 → fred_loader-0.1.4}/src/fred_loader.egg-info/SOURCES.txt +2 -0
- {fred_loader-0.1.2 → fred_loader-0.1.4}/tests/test_discovery_and_resolver.py +19 -0
- fred_loader-0.1.4/tests/test_output_encoding.py +84 -0
- fred_loader-0.1.4/tests/test_package_metadata.py +20 -0
- {fred_loader-0.1.2 → fred_loader-0.1.4}/tests/test_pull_loop.py +21 -0
- {fred_loader-0.1.2 → fred_loader-0.1.4}/LICENSE +0 -0
- {fred_loader-0.1.2 → fred_loader-0.1.4}/README.md +0 -0
- {fred_loader-0.1.2 → fred_loader-0.1.4}/setup.cfg +0 -0
- {fred_loader-0.1.2 → fred_loader-0.1.4}/src/fred_loader/__init__.py +0 -0
- {fred_loader-0.1.2 → fred_loader-0.1.4}/src/fred_loader/py.typed +0 -0
- {fred_loader-0.1.2 → fred_loader-0.1.4}/src/fred_loader/series.py +0 -0
- {fred_loader-0.1.2 → fred_loader-0.1.4}/src/fred_loader.egg-info/dependency_links.txt +0 -0
- {fred_loader-0.1.2 → fred_loader-0.1.4}/src/fred_loader.egg-info/requires.txt +0 -0
- {fred_loader-0.1.2 → fred_loader-0.1.4}/src/fred_loader.egg-info/top_level.txt +0 -0
- {fred_loader-0.1.2 → fred_loader-0.1.4}/tests/test_macro_scores.py +0 -0
- {fred_loader-0.1.2 → fred_loader-0.1.4}/tests/test_typed.py +0 -0
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fred-loader
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: A Python wrapper around the Federal Reserve FRED API that abstracts away series IDs, resampling rules, and macro scoring behind a clean, human-readable interface.
|
|
5
5
|
Author: Ian Solberg
|
|
6
6
|
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/iasolb/fred-loader
|
|
8
|
+
Project-URL: Repository, https://github.com/iasolb/fred-loader
|
|
9
|
+
Project-URL: Issues, https://github.com/iasolb/fred-loader/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
7
19
|
Classifier: Typing :: Typed
|
|
8
20
|
Requires-Python: >=3.10
|
|
9
21
|
Description-Content-Type: text/markdown
|
|
@@ -4,14 +4,25 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fred-loader"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "A Python wrapper around the Federal Reserve FRED API that abstracts away series IDs, resampling rules, and macro scoring behind a clean, human-readable interface."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
authors = [{ name = "Ian Solberg" }]
|
|
14
|
-
classifiers = [
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Scientific/Engineering",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
15
26
|
dependencies = [
|
|
16
27
|
"fredapi",
|
|
17
28
|
"pandas",
|
|
@@ -19,6 +30,11 @@ dependencies = [
|
|
|
19
30
|
"python-dotenv",
|
|
20
31
|
]
|
|
21
32
|
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/iasolb/fred-loader"
|
|
35
|
+
Repository = "https://github.com/iasolb/fred-loader"
|
|
36
|
+
Issues = "https://github.com/iasolb/fred-loader/issues"
|
|
37
|
+
|
|
22
38
|
[project.optional-dependencies]
|
|
23
39
|
dev = ["pytest"]
|
|
24
40
|
|
|
@@ -38,7 +38,7 @@ Available categories::
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
40
|
import pandas as pd
|
|
41
|
-
from .utils import Config, load_fred_master
|
|
41
|
+
from .utils import Config, _say, load_fred_master
|
|
42
42
|
from .macro_scores import score
|
|
43
43
|
|
|
44
44
|
# Re-export every category so users only need `from fred_loader import ...`
|
|
@@ -139,5 +139,5 @@ def pull_fred(config: Config, apply_scores: bool = False) -> pd.DataFrame | None
|
|
|
139
139
|
out_file = config.OUTPUT_PATH / config.FILENAME
|
|
140
140
|
output.to_csv(out_file)
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
_say(f"\nSaved -> {out_file}")
|
|
143
143
|
return output
|
|
@@ -772,7 +772,7 @@ def score(
|
|
|
772
772
|
meaningful = [s for s in resolved if s.requires]
|
|
773
773
|
if not meaningful:
|
|
774
774
|
print(
|
|
775
|
-
"\n
|
|
775
|
+
"\n! No scores can be computed from the columns present.\n"
|
|
776
776
|
" Scoring requires series from categories like INFLATION, LABOR,\n"
|
|
777
777
|
" RATES, or SEP. Returning raw data unchanged."
|
|
778
778
|
)
|
|
@@ -2,12 +2,60 @@ from fredapi import Fred
|
|
|
2
2
|
import pandas as pd
|
|
3
3
|
import os
|
|
4
4
|
from dotenv import load_dotenv
|
|
5
|
+
from pandas.tseries.frequencies import to_offset
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Optional
|
|
8
|
+
import sys
|
|
7
9
|
import time
|
|
8
10
|
from .series import ALL_SERIES, CATEGORIES, SUBCATEGORIES
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
def _say(line: str = "") -> None:
|
|
14
|
+
"""Print one progress line, never raising on the stream's encoding.
|
|
15
|
+
|
|
16
|
+
A Windows script whose stdout is piped or redirected writes cp1252, which
|
|
17
|
+
cannot encode every character an API error, a custom series name or a file
|
|
18
|
+
path may carry. The line reports on work that already happened, so it
|
|
19
|
+
degrades to '?' instead of crashing the pull it describes.
|
|
20
|
+
"""
|
|
21
|
+
encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
|
|
22
|
+
print(line.encode(encoding, errors="replace").decode(encoding))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# One row of the output stands for one of these, keyed by the offset's class
|
|
26
|
+
# name so the lookup does not depend on how a pandas version spells the alias.
|
|
27
|
+
_PERIOD_WORDS = {
|
|
28
|
+
"Week": "weeks",
|
|
29
|
+
"MonthEnd": "months",
|
|
30
|
+
"MonthBegin": "months",
|
|
31
|
+
"BusinessMonthEnd": "months",
|
|
32
|
+
"BusinessMonthBegin": "months",
|
|
33
|
+
"QuarterEnd": "quarters",
|
|
34
|
+
"QuarterBegin": "quarters",
|
|
35
|
+
"YearEnd": "years",
|
|
36
|
+
"YearBegin": "years",
|
|
37
|
+
"Day": "days",
|
|
38
|
+
"BusinessDay": "business days",
|
|
39
|
+
"Hour": "hours",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _period_word(rule: str) -> str:
|
|
44
|
+
"""Name the period one output row stands for under *rule*, else "rows".
|
|
45
|
+
|
|
46
|
+
The summary line said "weeks" whatever the rule, so a monthly pull
|
|
47
|
+
printed "56 weeks" (2026-10-17). A multiple such as "2W" says "rows",
|
|
48
|
+
since "weeks" would be wrong by the multiple.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
offset = to_offset(rule)
|
|
52
|
+
except ValueError:
|
|
53
|
+
return "rows"
|
|
54
|
+
if offset.n != 1:
|
|
55
|
+
return "rows"
|
|
56
|
+
return _PERIOD_WORDS.get(type(offset).__name__, "rows")
|
|
57
|
+
|
|
58
|
+
|
|
11
59
|
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
12
60
|
# ║ SERIES RESOLVER ║
|
|
13
61
|
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
@@ -108,19 +156,30 @@ def available(category: str | None = None) -> None:
|
|
|
108
156
|
print()
|
|
109
157
|
|
|
110
158
|
|
|
159
|
+
def _words(text: str) -> list[str]:
|
|
160
|
+
"""Lowercase words of *text*, with underscores read as spaces.
|
|
161
|
+
|
|
162
|
+
Friendly names are underscore_joined (``Headline_CPI``), so a reader
|
|
163
|
+
typing "headline cpi" has to land on them.
|
|
164
|
+
"""
|
|
165
|
+
return text.lower().replace("_", " ").split()
|
|
166
|
+
|
|
167
|
+
|
|
111
168
|
def search(keyword: str) -> list[str]:
|
|
112
169
|
"""Search the catalog by keyword, case-insensitive.
|
|
113
170
|
|
|
114
171
|
Matches the FRED series id AND the friendly name, so both
|
|
115
|
-
`search("CPIAUCSL")` and `search("inflation")` work.
|
|
172
|
+
`search("CPIAUCSL")` and `search("inflation")` work. Spaces and
|
|
173
|
+
underscores are the same thing, and a multi-word query matches only
|
|
174
|
+
series carrying every word, so `search("headline cpi")` works too.
|
|
116
175
|
|
|
117
176
|
Returns the list of matching series ids.
|
|
118
177
|
"""
|
|
119
|
-
|
|
178
|
+
words = _words(keyword)
|
|
120
179
|
hits = [
|
|
121
180
|
series_id
|
|
122
181
|
for series_id, (name, _freq) in ALL_SERIES.items()
|
|
123
|
-
if
|
|
182
|
+
if all(w in " ".join(_words(f"{series_id} {name}")) for w in words)
|
|
124
183
|
]
|
|
125
184
|
|
|
126
185
|
if not hits:
|
|
@@ -277,11 +336,11 @@ def load_fred_master(config: Config) -> pd.DataFrame:
|
|
|
277
336
|
|
|
278
337
|
frames[name] = s
|
|
279
338
|
agg = "mean" if native_freq in config.MEAN_FREQS else "last"
|
|
280
|
-
|
|
339
|
+
_say(f" ok {name:<40s} ({series_id:<18s} {native_freq} -> {agg})")
|
|
281
340
|
time.sleep(0.5) # be nice to the API
|
|
282
341
|
except Exception as e:
|
|
283
342
|
failed.append((series_id, name, str(e)))
|
|
284
|
-
|
|
343
|
+
_say(f" FAIL {name:<40s} ({series_id}): {e}")
|
|
285
344
|
time.sleep(0.5) # be nice to the API
|
|
286
345
|
|
|
287
346
|
df = pd.DataFrame(frames)
|
|
@@ -289,12 +348,13 @@ def load_fred_master(config: Config) -> pd.DataFrame:
|
|
|
289
348
|
df.index.name = "date"
|
|
290
349
|
|
|
291
350
|
if failed:
|
|
292
|
-
|
|
351
|
+
_say(f"\n! {len(failed)} series failed:")
|
|
293
352
|
for sid, nm, err in failed:
|
|
294
|
-
|
|
353
|
+
_say(f" {nm} ({sid}): {err}")
|
|
295
354
|
|
|
296
|
-
|
|
297
|
-
|
|
355
|
+
period = _period_word(config.RESAMPLE_RULE)
|
|
356
|
+
_say(
|
|
357
|
+
f"\nLoaded {len(frames)} series | {df.shape[0]} {period} | {df.columns.size} columns"
|
|
298
358
|
)
|
|
299
359
|
return df
|
|
300
360
|
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fred-loader
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: A Python wrapper around the Federal Reserve FRED API that abstracts away series IDs, resampling rules, and macro scoring behind a clean, human-readable interface.
|
|
5
5
|
Author: Ian Solberg
|
|
6
6
|
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/iasolb/fred-loader
|
|
8
|
+
Project-URL: Repository, https://github.com/iasolb/fred-loader
|
|
9
|
+
Project-URL: Issues, https://github.com/iasolb/fred-loader/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
7
19
|
Classifier: Typing :: Typed
|
|
8
20
|
Requires-Python: >=3.10
|
|
9
21
|
Description-Content-Type: text/markdown
|
|
@@ -14,5 +14,7 @@ src/fred_loader.egg-info/requires.txt
|
|
|
14
14
|
src/fred_loader.egg-info/top_level.txt
|
|
15
15
|
tests/test_discovery_and_resolver.py
|
|
16
16
|
tests/test_macro_scores.py
|
|
17
|
+
tests/test_output_encoding.py
|
|
18
|
+
tests/test_package_metadata.py
|
|
17
19
|
tests/test_pull_loop.py
|
|
18
20
|
tests/test_typed.py
|
|
@@ -141,6 +141,25 @@ def test_search_returns_empty_and_says_so_when_nothing_matches(capsys):
|
|
|
141
141
|
assert "No series matching" in capsys.readouterr().out
|
|
142
142
|
|
|
143
143
|
|
|
144
|
+
def test_search_reads_spaces_as_underscores(capsys):
|
|
145
|
+
"""Friendly names are underscore_joined, so the words a reader types must
|
|
146
|
+
still find them: "headline cpi" returned nothing until 2026-10-17."""
|
|
147
|
+
hits = search("headline cpi")
|
|
148
|
+
capsys.readouterr()
|
|
149
|
+
assert "CPIAUCSL" in hits
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_search_with_several_words_needs_every_word_in_any_order(capsys):
|
|
153
|
+
hits = search("cpi headline")
|
|
154
|
+
narrower_than = search("cpi")
|
|
155
|
+
capsys.readouterr()
|
|
156
|
+
assert "CPIAUCSL" in hits
|
|
157
|
+
for series_id in hits:
|
|
158
|
+
words = f"{series_id} {ALL_SERIES[series_id][0]}".lower().replace("_", " ")
|
|
159
|
+
assert "cpi" in words and "headline" in words
|
|
160
|
+
assert set(hits) < set(narrower_than)
|
|
161
|
+
|
|
162
|
+
|
|
144
163
|
def test_info_reports_the_series_with_its_frequency_and_lineage(capsys):
|
|
145
164
|
info("CPIAUCSL")
|
|
146
165
|
out = capsys.readouterr().out
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Printed output must never crash a stream that is not UTF-8.
|
|
2
|
+
|
|
3
|
+
A Windows script whose stdout is piped or redirected writes cp1252. The pull
|
|
4
|
+
loop printed check marks and arrows there and raised UnicodeEncodeError inside
|
|
5
|
+
pull_all after the data had already arrived, so a successful pull read as a
|
|
6
|
+
crash (2026-10-17). Each test runs its call under a strict cp1252 stdout,
|
|
7
|
+
which is what that script sees. The swap happens inside the test body because
|
|
8
|
+
pytest re-installs its own capture stream after fixture setup.
|
|
9
|
+
|
|
10
|
+
Offline: the client is faked by patching ``fred_loader.utils.Fred``.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import io
|
|
15
|
+
from contextlib import contextmanager, redirect_stdout
|
|
16
|
+
from unittest.mock import MagicMock, patch
|
|
17
|
+
|
|
18
|
+
import pandas as pd
|
|
19
|
+
|
|
20
|
+
import fred_loader.utils as utils
|
|
21
|
+
from fred_loader.load import pull_fred
|
|
22
|
+
from fred_loader.utils import Config, available
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _Cp1252Out:
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.raw = io.BytesIO()
|
|
28
|
+
self.stream = io.TextIOWrapper(
|
|
29
|
+
self.raw, encoding="cp1252", errors="strict", newline="\n"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def text(self) -> str:
|
|
33
|
+
self.stream.flush()
|
|
34
|
+
return self.raw.getvalue().decode("cp1252")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@contextmanager
|
|
38
|
+
def cp1252_stdout():
|
|
39
|
+
out = _Cp1252Out()
|
|
40
|
+
with redirect_stdout(out.stream):
|
|
41
|
+
yield out
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_say_degrades_a_character_the_stream_cannot_encode():
|
|
45
|
+
with cp1252_stdout() as out:
|
|
46
|
+
utils._say("before ✓ after")
|
|
47
|
+
assert out.text() == "before ? after\n"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_a_pull_with_a_failure_prints_cleanly_to_cp1252(tmp_path):
|
|
51
|
+
def _get(series_id, observation_start=None):
|
|
52
|
+
if series_id == "BAD_ID":
|
|
53
|
+
raise RuntimeError("upstream said → no")
|
|
54
|
+
return pd.Series(
|
|
55
|
+
[1.0, 2.0, 3.0],
|
|
56
|
+
index=pd.DatetimeIndex(pd.date_range("2020-01-01", periods=3, freq="MS")),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
mock_fred = MagicMock()
|
|
60
|
+
mock_fred.get_series.side_effect = _get
|
|
61
|
+
cfg = Config(
|
|
62
|
+
filename="enc.csv",
|
|
63
|
+
output_path=tmp_path,
|
|
64
|
+
start="2020-01-01",
|
|
65
|
+
series={"GOOD_ID": ("Good Series", "M"), "BAD_ID": ("Bad Series", "M")},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
with patch("fred_loader.utils.Fred", return_value=mock_fred), \
|
|
69
|
+
patch("fred_loader.utils.time.sleep"), \
|
|
70
|
+
cp1252_stdout() as out:
|
|
71
|
+
df = pull_fred(cfg, apply_scores=False)
|
|
72
|
+
|
|
73
|
+
assert list(df.columns) == ["Good Series"]
|
|
74
|
+
text = out.text()
|
|
75
|
+
assert "ok Good Series" in text
|
|
76
|
+
assert "FAIL Bad Series" in text
|
|
77
|
+
assert "upstream said ? no" in text
|
|
78
|
+
assert "Saved -> " in text
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_available_prints_cleanly_to_cp1252():
|
|
82
|
+
with cp1252_stdout() as out:
|
|
83
|
+
available()
|
|
84
|
+
assert "TOTAL" in out.text()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""What a stranger sees on PyPI, pinned so it cannot quietly regress.
|
|
2
|
+
|
|
3
|
+
Read as text, not with tomllib: the suite runs on Python 3.10, which has none.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
PYPROJECT = (Path(__file__).resolve().parent.parent / "pyproject.toml").read_text(
|
|
9
|
+
encoding="utf-8"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_the_pypi_page_links_back_to_the_repo():
|
|
14
|
+
assert "[project.urls]" in PYPROJECT
|
|
15
|
+
assert 'Repository = "https://github.com/iasolb/fred-loader"' in PYPROJECT
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_the_pypi_page_names_the_supported_pythons():
|
|
19
|
+
for version in ("3.10", "3.11", "3.12"):
|
|
20
|
+
assert f'"Programming Language :: Python :: {version}"' in PYPROJECT
|
|
@@ -276,6 +276,27 @@ def test_resample_rule_honoured(tmp_path):
|
|
|
276
276
|
assert len(df_w) >= len(df_m)
|
|
277
277
|
|
|
278
278
|
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
# Test 8b: the summary line names the period the rule actually produced
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
@pytest.mark.parametrize(
|
|
284
|
+
"rule, word",
|
|
285
|
+
[("W-FRI", "weeks"), ("ME", "months"), ("QE", "quarters"), ("YE", "years"), ("2W", "rows")],
|
|
286
|
+
)
|
|
287
|
+
def test_summary_line_names_the_real_period(tmp_path, capsys, rule, word):
|
|
288
|
+
"""It said "weeks" whatever the rule: a monthly pull printed "56 weeks"
|
|
289
|
+
(2026-10-17)."""
|
|
290
|
+
cfg = _config(tmp_path, resample_rule=rule)
|
|
291
|
+
mock_fred = _make_fred_mock(_fake_get_series())
|
|
292
|
+
|
|
293
|
+
with patch("fred_loader.utils.Fred", return_value=mock_fred):
|
|
294
|
+
df = load_fred_master(cfg)
|
|
295
|
+
|
|
296
|
+
summary = [ln for ln in capsys.readouterr().out.splitlines() if ln.startswith("Loaded ")]
|
|
297
|
+
assert summary == [f"Loaded 2 series | {len(df)} {word} | 2 columns"]
|
|
298
|
+
|
|
299
|
+
|
|
279
300
|
# ---------------------------------------------------------------------------
|
|
280
301
|
# Test 9: pull_fred end-to-end — writes CSV, returns frame; apply_scores paths
|
|
281
302
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|