fred-client 0.1.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.
@@ -0,0 +1,89 @@
1
+ """fred-client -- read economic data from the FRED / ALFRED / GeoFRED API.
2
+
3
+ from fred_client import FRED
4
+
5
+ fred = FRED() # or set FRED_API_KEY
6
+ rows = fred.series.observations("GNPCA") # a series' observations
7
+ hit = fred.series.search("unemployment rate") # series matching text
8
+
9
+ Single lookups return one raw dictionary; collections return ``list[dict]`` with the
10
+ vendor's own field names. The offline :mod:`fred_client.catalog` describes every service
11
+ without a call.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from importlib.metadata import PackageNotFoundError, version
17
+
18
+ from . import catalog
19
+ from .client import FRED
20
+ from .errors import (
21
+ FREDAuthError,
22
+ FREDConfigError,
23
+ FREDError,
24
+ FREDNetworkError,
25
+ FREDProtocolError,
26
+ FREDRateLimitError,
27
+ FREDResponseError,
28
+ )
29
+ from .types import (
30
+ AggregationMethod,
31
+ Frequency,
32
+ OutputType,
33
+ RegionType,
34
+ ReleaseDateOrder,
35
+ ReleaseOrder,
36
+ Row,
37
+ SearchOrder,
38
+ SearchType,
39
+ Season,
40
+ SeriesFilter,
41
+ SeriesOrder,
42
+ Shape,
43
+ SortOrder,
44
+ SourceOrder,
45
+ TagGroup,
46
+ TagOrder,
47
+ Transformation,
48
+ Units,
49
+ UpdateFilter,
50
+ VintageDates,
51
+ )
52
+
53
+ try:
54
+ __version__ = version("fred-client") # single source of truth: pyproject.toml
55
+ except PackageNotFoundError: # running from source without an install
56
+ __version__ = "0.0.0+unknown"
57
+
58
+ __all__ = [
59
+ "FRED",
60
+ "AggregationMethod",
61
+ "FREDAuthError",
62
+ "FREDConfigError",
63
+ "FREDError",
64
+ "FREDNetworkError",
65
+ "FREDProtocolError",
66
+ "FREDRateLimitError",
67
+ "FREDResponseError",
68
+ "Frequency",
69
+ "OutputType",
70
+ "RegionType",
71
+ "ReleaseDateOrder",
72
+ "ReleaseOrder",
73
+ "Row",
74
+ "SearchOrder",
75
+ "Season",
76
+ "SearchType",
77
+ "SeriesFilter",
78
+ "SeriesOrder",
79
+ "Shape",
80
+ "SourceOrder",
81
+ "SortOrder",
82
+ "TagGroup",
83
+ "TagOrder",
84
+ "Transformation",
85
+ "Units",
86
+ "UpdateFilter",
87
+ "VintageDates",
88
+ "catalog",
89
+ ]
fred_client/_config.py ADDED
@@ -0,0 +1,92 @@
1
+ """Resolve the FRED API key from the caller, the environment, or the config file.
2
+
3
+ The key is looked up in a fixed order, so an explicit value always wins and a set
4
+ environment variable beats a file on disk:
5
+
6
+ 1. the ``api_key`` passed to ``FRED(...)``
7
+ 2. the ``FRED_API_KEY`` environment variable
8
+ 3. ``"FRED_API_KEY"`` in ``$XDG_CONFIG_HOME/fred-client/credentials.json``
9
+ (``$XDG_CONFIG_HOME`` defaults to ``~/.config``)
10
+
11
+ The file is optional -- its absence just means "no key here." But a file that is present
12
+ and unreadable, not JSON, or not a JSON object is an error, because a caller who wrote one
13
+ meant it to be used and a silent skip would hide the mistake.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import re
21
+ from pathlib import Path
22
+
23
+ from .errors import FREDConfigError
24
+
25
+ _ENV_VAR = "FRED_API_KEY"
26
+ _CONFIG_DIR = "fred-client"
27
+ _CONFIG_FILE = "credentials.json"
28
+ _API_KEY_PATTERN = re.compile(r"[a-z0-9]{32}")
29
+
30
+
31
+ def resolve_api_key(explicit: str | None) -> str:
32
+ """Return the first key found across the three sources, or raise if none exists."""
33
+ # Surrounding whitespace is harmless copy/paste residue and is normalized. Control
34
+ # characters that remain inside the key are rejected below.
35
+ key = (explicit or "").strip() or os.environ.get(_ENV_VAR, "").strip() or _key_from_file()
36
+ if not key:
37
+ raise FREDConfigError(
38
+ f"no FRED API key: pass api_key=, set the {_ENV_VAR} environment "
39
+ f"variable, or put it in {credentials_path()}"
40
+ )
41
+ if _API_KEY_PATTERN.fullmatch(key) is None:
42
+ # FRED keys use only lowercase ASCII letters and digits. Enforcing that wire
43
+ # contract also means query encoding has exactly one representation.
44
+ raise FREDConfigError("the FRED API key must be 32 lowercase letters or digits")
45
+ return key
46
+
47
+
48
+ def credentials_path() -> Path:
49
+ """The path fred-client reads a stored key from (honoring ``$XDG_CONFIG_HOME``)."""
50
+ configured_home = os.environ.get("XDG_CONFIG_HOME")
51
+ if configured_home:
52
+ config_home = Path(configured_home)
53
+ if not config_home.is_absolute():
54
+ raise FREDConfigError("XDG_CONFIG_HOME must be an absolute path")
55
+ else:
56
+ config_home = Path.home() / ".config"
57
+ return config_home / _CONFIG_DIR / _CONFIG_FILE
58
+
59
+
60
+ def _key_from_file() -> str:
61
+ path = credentials_path()
62
+ failure: FREDConfigError | None = None
63
+ try:
64
+ text = path.read_text(encoding="utf-8")
65
+ except FileNotFoundError:
66
+ return ""
67
+ except UnicodeDecodeError:
68
+ failure = FREDConfigError(f"{path} is not valid UTF-8")
69
+ except OSError as err:
70
+ raise FREDConfigError(f"could not read {path}: {err}") from err
71
+ if failure is not None:
72
+ # A UnicodeDecodeError carries the offending bytes, and this file can hold the API
73
+ # key -- from None keeps those bytes out of any traceback.
74
+ raise failure from None
75
+
76
+ try:
77
+ data = json.loads(text)
78
+ except json.JSONDecodeError:
79
+ failure = FREDConfigError(f"{path} is not valid JSON")
80
+ if failure is not None:
81
+ # A JSONDecodeError retains the parsed text in .doc, and this file can hold the API
82
+ # key -- from None keeps the key out of any traceback.
83
+ raise failure from None
84
+ if not isinstance(data, dict):
85
+ raise FREDConfigError(f"{path} must contain a JSON object")
86
+
87
+ key = data.get(_ENV_VAR)
88
+ if key is None:
89
+ return ""
90
+ if not isinstance(key, str):
91
+ raise FREDConfigError(f"{path} property {_ENV_VAR} must be a string")
92
+ return key.strip()
@@ -0,0 +1,105 @@
1
+ """The :class:`_FREDEndpoint` value object and the registry of every FRED service.
2
+
3
+ Each endpoint is data: which API namespace it lives in, its path, the *shape* of the
4
+ response, and -- for the array-shaped responses -- the JSON key that holds the array.
5
+ ``url`` resolves from namespace + path, so the wire location lives in exactly one place.
6
+
7
+ The FRED API answers most requests as a top-level object with one named array whose key
8
+ differs per endpoint (``seriess`` with two s's, ``observations``, ``categories``, ...),
9
+ but two core endpoints and the whole GeoFRED maps surface break that mould: a vintage-date
10
+ list is an array of bare strings, a release table is a nested tree, and the maps responses
11
+ are GeoJSON or doubly-nested objects. ``shape`` records which, so the session extracts each
12
+ correctly instead of assuming one form.
13
+
14
+ ``_ENDPOINTS`` is kept read-only (a ``MappingProxyType``): it is package dispatch state
15
+ that every accessor reads, so a caller must not be able to mutate it.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Mapping
21
+ from dataclasses import dataclass
22
+ from types import MappingProxyType
23
+ from typing import Literal
24
+
25
+ # How the session turns a parsed body into the caller's return value.
26
+ # objects -> body[key] is a list[dict] (the common case)
27
+ # strings -> body[key] is a list[str] (series/vintagedates)
28
+ # tree -> the whole body dict (release/tables, a nested element tree)
29
+ # geojson -> the whole body dict (maps shapes, a GeoJSON FeatureCollection)
30
+ # single -> the whole body dict (maps series/group, one meta object)
31
+ # nested -> the whole body dict (maps series/data & regional/data)
32
+ # bulk -> cursor-paged v2 release histories, merged into one whole object
33
+ _ResponseShape = Literal["objects", "strings", "tree", "geojson", "single", "nested", "bulk"]
34
+ _APINamespace = Literal["fred", "geofred"]
35
+
36
+ _HOST = "https://api.stlouisfed.org"
37
+ _BASE_URL_BY_API_NAMESPACE: Mapping[_APINamespace, str] = MappingProxyType(
38
+ {"fred": f"{_HOST}/fred", "geofred": f"{_HOST}/geofred"}
39
+ )
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class _FREDEndpoint:
44
+ """One FRED service: where its data lives and how its response is shaped."""
45
+
46
+ path: str # e.g. "series/observations"
47
+ shape: _ResponseShape # how to extract the return value
48
+ response_array_key: str = "" # "" for whole-dict response shapes
49
+ api_namespace: _APINamespace = "fred"
50
+
51
+ @property
52
+ def url(self) -> str:
53
+ return f"{_BASE_URL_BY_API_NAMESPACE[self.api_namespace]}/{self.path}"
54
+
55
+
56
+ def _registry(*endpoints: _FREDEndpoint) -> Mapping[str, _FREDEndpoint]:
57
+ return MappingProxyType({endpoint.path: endpoint for endpoint in endpoints})
58
+
59
+
60
+ # Every service, path -> _FREDEndpoint. Read-only. Grouped by the FRED doc's own sections,
61
+ # mirroring the vendor's taxonomy (the module lays out the same way).
62
+ _ENDPOINTS: Mapping[str, _FREDEndpoint] = _registry(
63
+ # -- Categories --------------------------------------------------------
64
+ _FREDEndpoint("category", "objects", "categories"),
65
+ _FREDEndpoint("category/children", "objects", "categories"),
66
+ _FREDEndpoint("category/related", "objects", "categories"),
67
+ _FREDEndpoint("category/series", "objects", "seriess"),
68
+ _FREDEndpoint("category/tags", "objects", "tags"),
69
+ _FREDEndpoint("category/related_tags", "objects", "tags"),
70
+ # -- Releases ----------------------------------------------------------
71
+ _FREDEndpoint("releases", "objects", "releases"),
72
+ _FREDEndpoint("releases/dates", "objects", "release_dates"),
73
+ _FREDEndpoint("release", "objects", "releases"),
74
+ _FREDEndpoint("release/dates", "objects", "release_dates"),
75
+ _FREDEndpoint("release/series", "objects", "seriess"),
76
+ _FREDEndpoint("release/sources", "objects", "sources"),
77
+ _FREDEndpoint("release/tags", "objects", "tags"),
78
+ _FREDEndpoint("release/related_tags", "objects", "tags"),
79
+ _FREDEndpoint("release/tables", "tree"),
80
+ _FREDEndpoint("v2/release/observations", "bulk"),
81
+ # -- Series ------------------------------------------------------------
82
+ _FREDEndpoint("series", "objects", "seriess"),
83
+ _FREDEndpoint("series/categories", "objects", "categories"),
84
+ _FREDEndpoint("series/observations", "objects", "observations"),
85
+ _FREDEndpoint("series/release", "objects", "releases"),
86
+ _FREDEndpoint("series/search", "objects", "seriess"),
87
+ _FREDEndpoint("series/search/tags", "objects", "tags"),
88
+ _FREDEndpoint("series/search/related_tags", "objects", "tags"),
89
+ _FREDEndpoint("series/tags", "objects", "tags"),
90
+ _FREDEndpoint("series/updates", "objects", "seriess"),
91
+ _FREDEndpoint("series/vintagedates", "strings", "vintage_dates"),
92
+ # -- Sources -----------------------------------------------------------
93
+ _FREDEndpoint("sources", "objects", "sources"),
94
+ _FREDEndpoint("source", "objects", "sources"),
95
+ _FREDEndpoint("source/releases", "objects", "releases"),
96
+ # -- Tags --------------------------------------------------------------
97
+ _FREDEndpoint("tags", "objects", "tags"),
98
+ _FREDEndpoint("related_tags", "objects", "tags"),
99
+ _FREDEndpoint("tags/series", "objects", "seriess"),
100
+ # -- Maps (GeoFRED) ----------------------------------------------------
101
+ _FREDEndpoint("shapes/file", "geojson", api_namespace="geofred"),
102
+ _FREDEndpoint("series/group", "single", api_namespace="geofred"),
103
+ _FREDEndpoint("series/data", "nested", api_namespace="geofred"),
104
+ _FREDEndpoint("regional/data", "nested", api_namespace="geofred"),
105
+ )
fred_client/catalog.py ADDED
@@ -0,0 +1,34 @@
1
+ """Offline catalog of the FRED services -- browse the surface without an API key.
2
+
3
+ Every service is data already in the internal endpoint registry. This module presents it
4
+ in a read-friendly form so a caller (or the CLI) can answer "what endpoints exist, in which
5
+ API namespace, returning what shape?" with no network call and no key.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ._endpoint import _ENDPOINTS
11
+
12
+
13
+ def endpoints() -> list[dict[str, str]]:
14
+ """Every service as a plain dict: path, API namespace, response shape, and array key."""
15
+ return [
16
+ {
17
+ "path": endpoint.path,
18
+ "api_namespace": endpoint.api_namespace,
19
+ "shape": endpoint.shape,
20
+ "response_array_key": endpoint.response_array_key,
21
+ }
22
+ for endpoint in _ENDPOINTS.values()
23
+ ]
24
+
25
+
26
+ def paths() -> list[str]:
27
+ """The sorted list of every service path."""
28
+ return sorted(_ENDPOINTS)
29
+
30
+
31
+ def find(text: str) -> list[dict[str, str]]:
32
+ """The services whose path contains ``text`` (case-insensitive)."""
33
+ needle = text.lower()
34
+ return [entry for entry in endpoints() if needle in entry["path"].lower()]
fred_client/cli.py ADDED
@@ -0,0 +1,221 @@
1
+ """Command-line access to curated indicators and arbitrary FRED series.
2
+
3
+ ``list`` browses the curated accessor groups offline. ``fetch`` mirrors either a
4
+ curated accessor (``fred fetch growth gdp``) or a raw series ID (``fred fetch GDP``).
5
+ ``metadata`` describes one series, and ``search`` finds series IDs by text. Data
6
+ commands print aligned output by default and the raw response as JSON under ``--json``;
7
+ all data-shape knowledge stays in the library -- this only formats what it returns.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import sys
15
+ from collections.abc import Callable, Sequence
16
+ from typing import get_args
17
+
18
+ from . import __version__
19
+ from .client import FRED, _list_indicator_groups, _list_indicators
20
+ from .errors import FREDError
21
+ from .types import AggregationMethod, Frequency, OutputType, Row, SortOrder, Units
22
+
23
+ __all__ = ["main"]
24
+
25
+ _PROG = "fred"
26
+ _ERROR_PREFIX = f"{_PROG}: "
27
+ # How many rows the table view prints; the full result is always available with --json.
28
+ _MAX_SHOWN_ROWS = 20
29
+
30
+
31
+ def _cell(value: object) -> str:
32
+ """A value as a single-line table cell (FRED metadata ``notes`` can hold newlines)."""
33
+ return str(value).replace("\n", " ")
34
+
35
+
36
+ def _render(headers: Sequence[str], rows: Sequence[Sequence[object]]) -> str:
37
+ """Rows as an aligned table over ``headers``, one row per line.
38
+
39
+ Widths are code-point counts, which assume the effectively-ASCII FRED payloads; a
40
+ wide or combining character would misalign a column.
41
+ """
42
+ cells = [[_cell(value) for value in row] for row in rows]
43
+ if cells:
44
+ columns = list(zip(headers, *cells, strict=True))
45
+ else:
46
+ columns = [(header,) for header in headers]
47
+ widths = [max(len(cell) for cell in column) for column in columns]
48
+ header_line = " ".join(
49
+ header.ljust(width) for header, width in zip(headers, widths, strict=True)
50
+ )
51
+ body = "\n".join(
52
+ " ".join(cell.ljust(width) for cell, width in zip(row, widths, strict=True))
53
+ for row in cells
54
+ )
55
+ return f"{header_line}\n{body}" if body else header_line
56
+
57
+
58
+ def _table(rows: Sequence[Row]) -> tuple[list[str], list[list[object]]]:
59
+ """A row list as ``(headers, cell-rows)``, unioning keys across the rows.
60
+
61
+ The key union (not just the first row's keys) keeps an ALFRED vintage view
62
+ (output_type 2/3), whose rows are keyed by different vintage dates, from silently
63
+ dropping columns the table should show.
64
+ """
65
+ headers = list(dict.fromkeys(key for row in rows for key in row))
66
+ body: list[list[object]] = [[row.get(key, "") for key in headers] for row in rows]
67
+ return headers, body
68
+
69
+
70
+ def _render_rows(rows: Sequence[Row]) -> str:
71
+ """Rows as an aligned table then a total count. Over ``_MAX_SHOWN_ROWS`` rows, show
72
+ the first and last halves split by a gap marker so both ends of the series stay
73
+ visible (use ``--json`` for everything). Empty -> ``(no rows)``."""
74
+ if not rows:
75
+ return "(no rows)"
76
+ noun = "row" if len(rows) == 1 else "rows"
77
+ if len(rows) <= _MAX_SHOWN_ROWS:
78
+ headers, body = _table(rows)
79
+ return f"{_render(headers, body)}\n({len(rows)} {noun})"
80
+ head = _MAX_SHOWN_ROWS // 2
81
+ tail = _MAX_SHOWN_ROWS - head
82
+ headers, body = _table([*rows[:head], *rows[-tail:]])
83
+ lines = _render(headers, body).splitlines() # header line + head rows + tail rows
84
+ # A bare "..." marks the omitted middle; the footer already gives the omitted count.
85
+ table = "\n".join([*lines[: head + 1], "...", *lines[head + 1 :]])
86
+ return f"{table}\n({len(rows)} {noun} total, first {head} and last {tail} shown)"
87
+
88
+
89
+ def _render_mapping(mapping: Row) -> str:
90
+ """One object as aligned ``key value`` lines. Empty -> ``(no fields)``."""
91
+ if not mapping:
92
+ return "(no fields)"
93
+ width = max(len(key) for key in mapping)
94
+ return "\n".join(f"{key.ljust(width)} {_cell(value)}" for key, value in mapping.items())
95
+
96
+
97
+ def _emit_rows(rows: Sequence[Row], as_json: bool) -> None:
98
+ if as_json:
99
+ print(json.dumps(list(rows), ensure_ascii=False, indent=2))
100
+ else:
101
+ print(_render_rows(rows))
102
+
103
+
104
+ def _emit_mapping(mapping: Row, as_json: bool) -> None:
105
+ if as_json:
106
+ print(json.dumps(mapping, ensure_ascii=False, indent=2))
107
+ else:
108
+ print(_render_mapping(mapping))
109
+
110
+
111
+ def _add_observation_options(parser: argparse.ArgumentParser) -> None:
112
+ parser.add_argument("--realtime-start")
113
+ parser.add_argument("--realtime-end")
114
+ parser.add_argument("--observation-start")
115
+ parser.add_argument("--observation-end")
116
+ parser.add_argument("--units", choices=get_args(Units))
117
+ parser.add_argument("--frequency", choices=get_args(Frequency))
118
+ parser.add_argument("--aggregation-method", choices=get_args(AggregationMethod))
119
+ parser.add_argument("--output-type", type=int, choices=get_args(OutputType))
120
+ parser.add_argument("--vintage-dates")
121
+ parser.add_argument("--limit", type=int)
122
+ parser.add_argument("--offset", type=int)
123
+ parser.add_argument("--sort-order", choices=get_args(SortOrder))
124
+
125
+
126
+ def _make_parser() -> argparse.ArgumentParser:
127
+ parser = argparse.ArgumentParser(prog=_PROG, description="Read FRED economic data.")
128
+ parser.add_argument("--version", action="version", version=f"fred-client {__version__}")
129
+ commands = parser.add_subparsers(required=True)
130
+
131
+ list_command = commands.add_parser("list", help="list curated indicator accessors (offline)")
132
+ list_command.add_argument("group", nargs="?", choices=_list_indicator_groups())
133
+ list_command.set_defaults(run=_run_list)
134
+
135
+ fetch = commands.add_parser("fetch", help="fetch observations by accessor or series ID")
136
+ fetch.add_argument("group_or_series", help="an accessor group or a FRED series ID")
137
+ fetch.add_argument("indicator", nargs="?", help="an indicator in the accessor group")
138
+ _add_observation_options(fetch)
139
+ fetch.add_argument("--json", action="store_true", help="emit JSON rows instead of a table")
140
+ fetch.set_defaults(run=_run_fetch)
141
+
142
+ metadata = commands.add_parser("metadata", help="show one series' metadata")
143
+ metadata.add_argument("series_id")
144
+ metadata.add_argument("--json", action="store_true", help="emit JSON instead of a table")
145
+ metadata.set_defaults(run=_run_metadata)
146
+
147
+ search = commands.add_parser("search", help="find series matching search text")
148
+ search.add_argument("text")
149
+ search.add_argument("--limit", type=int)
150
+ search.add_argument("--json", action="store_true", help="emit JSON rows instead of a table")
151
+ search.set_defaults(run=_run_search)
152
+
153
+ return parser
154
+
155
+
156
+ def main(argv: list[str] | None = None) -> int:
157
+ args = _make_parser().parse_args(argv)
158
+ run: Callable[[argparse.Namespace], int] = args.run
159
+ try:
160
+ return run(args)
161
+ except FREDError as err:
162
+ print(f"{_ERROR_PREFIX}{err}", file=sys.stderr)
163
+ return 1
164
+ except ValueError as err:
165
+ print(f"{_ERROR_PREFIX}{err}", file=sys.stderr)
166
+ return 2
167
+
168
+
169
+ def _run_list(args: argparse.Namespace) -> int:
170
+ groups = (args.group,) if args.group else _list_indicator_groups()
171
+ lines: list[str] = []
172
+ for group in groups:
173
+ lines.append(group)
174
+ lines.extend(f" {group} {indicator}" for indicator in _list_indicators(group))
175
+ print("\n".join(lines))
176
+ return 0
177
+
178
+
179
+ def _run_fetch(args: argparse.Namespace) -> int:
180
+ params = {
181
+ "realtime_start": args.realtime_start,
182
+ "realtime_end": args.realtime_end,
183
+ "observation_start": args.observation_start,
184
+ "observation_end": args.observation_end,
185
+ "units": args.units,
186
+ "frequency": args.frequency,
187
+ "aggregation_method": args.aggregation_method,
188
+ "output_type": args.output_type,
189
+ "vintage_dates": args.vintage_dates,
190
+ "limit": args.limit,
191
+ "offset": args.offset,
192
+ "sort_order": args.sort_order,
193
+ }
194
+ if args.indicator is None:
195
+ fred = FRED()
196
+ payload = fred.series.observations(args.group_or_series, **params)
197
+ else:
198
+ group = args.group_or_series.lower()
199
+ indicator = args.indicator.lower()
200
+ if group not in _list_indicator_groups():
201
+ raise ValueError(f"unknown indicator group {group!r} (try `{_PROG} list`)")
202
+ if indicator not in _list_indicators(group):
203
+ raise ValueError(f"unknown indicator {group}.{indicator} (try `{_PROG} list {group}`)")
204
+ fred = FRED()
205
+ payload = getattr(getattr(fred, group), indicator)(**params)
206
+ _emit_rows(payload, args.json)
207
+ return 0
208
+
209
+
210
+ def _run_metadata(args: argparse.Namespace) -> int:
211
+ _emit_mapping(FRED().series.get(args.series_id), args.json)
212
+ return 0
213
+
214
+
215
+ def _run_search(args: argparse.Namespace) -> int:
216
+ _emit_rows(FRED().series.search(args.text, limit=args.limit), args.json)
217
+ return 0
218
+
219
+
220
+ if __name__ == "__main__":
221
+ raise SystemExit(main())