From 3513e025f7a934bc5c68aadc237f13b7f821e94a Mon Sep 17 00:00:00 2001 From: prez2307 <86853355+prez2307@users.noreply.github.com> Date: Sat, 8 Oct 2022 17:32:46 -0400 Subject: [PATCH] Feature/cli (#26) * boilerplate cli * wrap sources cli * Feature/required args (#10) * create api decorator to check valid args * format files * add args validation for releases * add api arg validator for categories * update required args for categories namespace * add required args to tags namespace * add validate args to sources apis * add arg validation to series namespace * add required args for series apis * finish base implementation of all APIs, closes issue #4 * bump fred-py-api to 0.0.5 and update readme to point to wiki * Create dependabot.yml * Delete dependabot.yml * Feature/test suite (#11) * basic tests for categories namespace * add basic tests for tags api * add basic tests for sources api namespace * create basic tests for releases api namespace * add basic tests for series api namespace * create test for decorators * use src for tests * first full test-suite of basic API functionality, closes issue #8 * new CI for lint and test package * upgrade black in CI, use fred api key from GH secrets * reformat files * update CI only test for 3.9 & 3.10 * only run CI with python 3.10.x * run ci using coverage * add coverage upload * add new job skeleton to CI * upload to PyPI when publishing new release/prerelease * Feature/test suite (#14) * basic tests for categories namespace * add basic tests for tags api * add basic tests for sources api namespace * create basic tests for releases api namespace * add basic tests for series api namespace * create test for decorators * use src for tests * first full test-suite of basic API functionality, closes issue #8 * new CI for lint and test package * upgrade black in CI, use fred api key from GH secrets * reformat files * update CI only test for 3.9 & 3.10 * only run CI with python 3.10.x * run ci using coverage * add coverage upload * add new job skeleton to CI * upload to PyPI when publishing new release/prerelease * update readme * 0.0.7-Alpha (#16) * add FRED urls to python docstrings * update readme * bump lib * fix issue #15 by enforcing API length * check alnum * update tests * reformat file * rename top level packagename to fred * bump lib (#17) * Add support for XML `file_type` (#18) * add XML support, and associated test * bump lib version to v0.1.1 * move cli library * cli WIP * general idea of FRED cli, WIP * variadic api arguments and kwargs generator, import cleanups * re-organize internal util module * full CLI project skeleton with some working examples * json dump string in sources CLI * restructure tests module * adding dummy test case for codecov * adding dummy test case for codecov * reformat file * update ci to run coverage with src * add test for generate_api_kwargs cli utility * Added JSON CLI commands for each command * update tests module to use installed fred library * update local requirements * update cli code slightly * update pyproject.toml to include new deps and script entrypoints * update README to include CLI * update github ci workflow to include editable install * Fixed many issues in code review, still need 4 utility functions, few changes needed * update a few args, docstrings, and file format * Adding basic testing structure for sources cli * wip series test cases * refactor fred cli entry point * refactor common test case logic * refactor and fix existing api tests * fix api key mock for cli tests * update ci workflow * slight refactor to base fred client test setup * allow for more testing constraints in base framework * wip series cli tests * omit test * add all basic tests for releases namespace * add missing test to releases namespace * finish up all basic tests for series cli * add support for XML output in CLI * Added CLI test cases for sources, tags, categories * reformat files * fix up some broken tests * add new test for CLI entry point * optimize imports * update codecoverage omit files, and CI * fix CI * ignore unnecessary files in codecov * update pyproject.toml description Co-authored-by: Zachary Spar Co-authored-by: Zachary Spar <41600414+zachspar@users.noreply.github.com> --- .github/workflows/fred-py-api-package.yml | 8 +- README.md | 3 +- pyproject.toml | 10 +- requirements.txt | 2 +- src/fred/_util/__init__.py | 3 + src/fred/_util/cli.py | 34 +++ src/fred/_util/decorators.py | 8 + src/fred/api/__init__.py | 2 +- src/fred/api/_fred_client.py | 4 - src/fred/cli/__init__.py | 33 +++ src/fred/cli/__main__.py | 16 ++ src/fred/cli/categories.py | 96 ++++++++ src/fred/cli/releases.py | 131 ++++++++++ src/fred/cli/series.py | 150 ++++++++++++ src/fred/cli/sources.py | 61 +++++ src/fred/cli/tags.py | 62 +++++ tests/api/__init__.py | 20 ++ tests/{ => api}/test_base_client.py | 17 +- tests/{ => api}/test_categories.py | 14 +- tests/{ => api}/test_decorators.py | 2 +- tests/{ => api}/test_releases.py | 14 +- tests/{ => api}/test_series.py | 14 +- tests/{ => api}/test_sources.py | 14 +- tests/{ => api}/test_tags.py | 14 +- tests/cli/__init__.py | 67 +++++ tests/cli/test_categories_cli.py | 157 ++++++++++++ tests/cli/test_cli_entry_point.py | 16 ++ tests/cli/test_cli_utils.py | 36 +++ tests/cli/test_releases_cli.py | 226 +++++++++++++++++ tests/cli/test_series_cli.py | 286 ++++++++++++++++++++++ tests/cli/test_sources_cli.py | 84 +++++++ tests/cli/test_tags_cli.py | 82 +++++++ 32 files changed, 1624 insertions(+), 62 deletions(-) create mode 100644 src/fred/_util/cli.py create mode 100644 src/fred/cli/__init__.py create mode 100644 src/fred/cli/__main__.py create mode 100644 src/fred/cli/categories.py create mode 100644 src/fred/cli/releases.py create mode 100644 src/fred/cli/series.py create mode 100644 src/fred/cli/sources.py create mode 100644 src/fred/cli/tags.py create mode 100644 tests/api/__init__.py rename tests/{ => api}/test_base_client.py (88%) rename tests/{ => api}/test_categories.py (90%) rename tests/{ => api}/test_decorators.py (92%) rename tests/{ => api}/test_releases.py (92%) rename tests/{ => api}/test_series.py (93%) rename tests/{ => api}/test_sources.py (80%) rename tests/{ => api}/test_tags.py (81%) create mode 100644 tests/cli/__init__.py create mode 100644 tests/cli/test_categories_cli.py create mode 100644 tests/cli/test_cli_entry_point.py create mode 100644 tests/cli/test_cli_utils.py create mode 100644 tests/cli/test_releases_cli.py create mode 100644 tests/cli/test_series_cli.py create mode 100644 tests/cli/test_sources_cli.py create mode 100644 tests/cli/test_tags_cli.py diff --git a/.github/workflows/fred-py-api-package.yml b/.github/workflows/fred-py-api-package.yml index e0312f8..d621d88 100644 --- a/.github/workflows/fred-py-api-package.yml +++ b/.github/workflows/fred-py-api-package.yml @@ -1,4 +1,5 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# This workflow will install Python dependencies, run tests and lint +# On publish event, it will build and publish a package to PyPI name: Lint, Test & Upload fred-py-api @@ -20,6 +21,8 @@ jobs: python-version: ["3.10"] env: FRED_API_KEY: ${{ secrets.FRED_API_KEY }} + TEST_FRED_API_KEY__API: ${{ secrets.TEST_FRED_API_KEY__API }} + TEST_FRED_API_KEY__CLI: ${{ secrets.TEST_FRED_API_KEY__CLI }} steps: - uses: actions/checkout@v3 @@ -31,6 +34,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install requests coverage black==22.6.0 + python -m pip install -e . if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with black run: | @@ -38,7 +42,7 @@ jobs: black --line-length 120 --check ./ - name: Test with coverage run: | - coverage run -m unittest discover -s tests + coverage run --source="src" --omit="src/fred/cli/__main__.py","src/fred/cli/__init__.py" -m unittest coverage report -m - name: Upload coverage report uses: codecov/codecov-action@v2 diff --git a/README.md b/README.md index 4b789b2..8a7227c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![codecov](https://codecov.io/gh/zachspar/fred-py-api/branch/main/graph/badge.svg?token=BG1948D8Y7)](https://codecov.io/gh/zachspar/fred-py-api) # Fred Python API -A fully-featured FRED Python Wrapper. +A fully-featured FRED Command Line Interface & Python API Wrapper. ## Documentation: - [fred-py-api Wiki](https://github.com/zachspar/fred-py-api/wiki) @@ -11,4 +11,3 @@ A fully-featured FRED Python Wrapper. ### FRED References: - [Create an API Key Here](https://fredaccount.stlouisfed.org/apikey) - [API Documentation](https://fred.stlouisfed.org/docs/api/fred/) - diff --git a/pyproject.toml b/pyproject.toml index 005f541..aecf802 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,15 @@ [build-system] -requires = ["setuptools>=61.0", "requests>=2.17.3",] +requires = ["setuptools>=61.0",] build-backend = "setuptools.build_meta" [project] name = "fred-py-api" -version = "1.0.1" +version = "1.1.0" authors = [ { name="Zachary Spar", email="zachspar@gmail.com" }, { name="Prasiddha Parthsarthy", email="prasiddha@gmail.com" }, ] -description = "A fully featured FRED API client library for Python." +description = "A fully featured FRED Command Line Interface & Python API client library." readme = "README.md" requires-python = ">=3.6" classifiers = [ @@ -27,8 +27,12 @@ classifiers = [ ] dependencies = [ "requests>=2.17.3", + "click>=7.0", ] +[project.scripts] +fred = "fred.cli:__main__.run_cli" + [project.urls] "Homepage" = "https://github.com/zachspar/fred_py_api" "Bug Tracker" = "https://github.com/zachspar/fred_py_api/issues" diff --git a/requirements.txt b/requirements.txt index f35cda7..8484503 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,5 @@ toml==0.10.2 tomli==2.0.1 tox==3.25.1 typing_extensions==4.3.0 -urllib3==1.26.10 +urllib3==1.26.11 virtualenv==20.15.1 diff --git a/src/fred/_util/__init__.py b/src/fred/_util/__init__.py index e69de29..79810ec 100644 --- a/src/fred/_util/__init__.py +++ b/src/fred/_util/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +from .cli import * +from .decorators import * diff --git a/src/fred/_util/cli.py b/src/fred/_util/cli.py new file mode 100644 index 0000000..667291f --- /dev/null +++ b/src/fred/_util/cli.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +""" +FRED CLI Utilities. +""" +from json import dumps +from typing import Union +from xml.etree import ElementTree as ET + + +__all__ = [ + "generate_api_kwargs", + "serialize", +] + + +def generate_api_kwargs(arguments: tuple) -> dict: + """Generate API keyword arguments from CLI variadic arguments.""" + api_kwargs = {} + for arg in arguments: + try: + api_kwargs[arg.split("=")[0]] = arg.split("=")[1] + except IndexError: + pass + return api_kwargs + + +def serialize(response_obj: Union[dict, ET.Element]) -> str: + """Serialize a FRED response object to a string.""" + if isinstance(response_obj, dict): + return dumps(response_obj, indent=4) + elif isinstance(response_obj, ET.Element): + return ET.tostring(response_obj, encoding="unicode", method="xml") + else: + raise TypeError("response_obj must be a dict or xml.etree.ElementTree.Element") diff --git a/src/fred/_util/decorators.py b/src/fred/_util/decorators.py index 6d3b443..724fef5 100644 --- a/src/fred/_util/decorators.py +++ b/src/fred/_util/decorators.py @@ -1,7 +1,15 @@ #!/usr/bin/env python3 +""" +FRED API Decorators. +""" from functools import wraps +__all__ = [ + "validate_api_args", +] + + def validate_api_args(*valid_args): """Class method decorator to validate API arguments.""" diff --git a/src/fred/api/__init__.py b/src/fred/api/__init__.py index fca4f69..fd9cc82 100644 --- a/src/fred/api/__init__.py +++ b/src/fred/api/__init__.py @@ -2,9 +2,9 @@ """ Fred API core. """ +from ._fred_client import JsonOrXml from .categories import FredAPICategories from .exceptions import BaseFredAPIError, FredAPIRequestError -from ._fred_client import JsonOrXml from .releases import FredAPIReleases from .series import FredAPISeries from .sources import FredAPISources diff --git a/src/fred/api/_fred_client.py b/src/fred/api/_fred_client.py index ed2118e..e22e7b9 100644 --- a/src/fred/api/_fred_client.py +++ b/src/fred/api/_fred_client.py @@ -3,7 +3,6 @@ Fred API Client. """ from http import HTTPStatus -from os import environ from typing import Optional, Dict, TypeVar from xml.etree import ElementTree as ET @@ -11,7 +10,6 @@ from .exceptions import FredAPIRequestError - __all__ = [ "FredClient", "JsonOrXml", @@ -26,8 +24,6 @@ class FredClient(object): def __init__(self, api_key: str = None, base_client: Optional["FredClient"] = None): """Init client.""" - if not base_client and not api_key: - api_key = environ.get("FRED_API_KEY", None) assert api_key or base_client, "Fred API Client or API Key required to use FredAPI" diff --git a/src/fred/cli/__init__.py b/src/fred/cli/__init__.py new file mode 100644 index 0000000..1cfabe9 --- /dev/null +++ b/src/fred/cli/__init__.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""CLI Core.""" +import click +from click import group + +from fred import FredAPI +from .categories import categories +from .releases import releases +from .series import series +from .sources import sources +from .tags import tags + +__all__ = [ + "fred_cli", +] + + +@group() +@click.option("--api-key", type=click.STRING, required=False, help="FRED API key.") +@click.pass_context +def fred_cli(ctx, api_key: str): + """CLI for the Federal Reserve Economic Data (FRED).""" + ctx.ensure_object(dict) + ctx.obj["api_key"] = api_key + ctx.obj["client"] = FredAPI(api_key=api_key) + + +# add each FRED command group +fred_cli.add_command(categories) +fred_cli.add_command(releases) +fred_cli.add_command(series) +fred_cli.add_command(sources) +fred_cli.add_command(tags) diff --git a/src/fred/cli/__main__.py b/src/fred/cli/__main__.py new file mode 100644 index 0000000..22a9c28 --- /dev/null +++ b/src/fred/cli/__main__.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +import click + +from . import fred_cli + + +def run_cli(): + """Run the FRED CLI.""" + try: + fred_cli(auto_envvar_prefix="FRED") + except AssertionError: + click.echo(click.style("Error: FRED_API_KEY is not set!", fg="red")) + + +if __name__ == "__main__": + run_cli() diff --git a/src/fred/cli/categories.py b/src/fred/cli/categories.py new file mode 100644 index 0000000..2014a86 --- /dev/null +++ b/src/fred/cli/categories.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +FRED CLI - Categories Namespace. +""" +import click + +from .. import BaseFredAPIError +from .._util import generate_api_kwargs, serialize + +__all__ = [ + "categories", +] + + +@click.group() +@click.pass_context +def categories(ctx): + """ + Categories CLI Namespace. + """ + pass + + +@categories.command() +@click.option("--category-id", "-i", required=True, type=click.STRING, help="Category ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_category(ctx, category_id: int, args: tuple): + """Get a category.""" + try: + click.echo(serialize(ctx.obj["client"].get_category(category_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@categories.command() +@click.option("--category-id", "-i", required=True, type=click.STRING, help="Category ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_category_children(ctx, category_id: int, args: tuple): + """Get the child categories.""" + try: + click.echo(serialize(ctx.obj["client"].get_category_children(category_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@categories.command() +@click.option("--category-id", "-i", required=True, type=click.STRING, help="Category ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_category_related(ctx, category_id: int, args: tuple): + """Get related categories.""" + try: + click.echo(serialize(ctx.obj["client"].get_category_related(category_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@categories.command() +@click.option("--category-id", "-i", required=True, type=click.STRING, help="Category ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_category_series(ctx, category_id: int, args: tuple): + """Get series in a category.""" + try: + click.echo(serialize(ctx.obj["client"].get_category_series(category_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@categories.command() +@click.option("--category-id", "-i", required=True, type=click.STRING, help="Category ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_category_tags(ctx, category_id: int, args: tuple): + """Get FRED tags for a category.""" + try: + click.echo(serialize(ctx.obj["client"].get_category_tags(category_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@categories.command() +@click.option("--category-id", "-i", required=True, type=click.STRING, help="Category ID.") +@click.option("--tag-names", "-t", required=True, type=click.STRING, help="Tag Names.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_category_related_tags(ctx, category_id: int, tag_names: str, args: tuple): + """Get related FRED tags for a category.""" + try: + click.echo( + serialize(ctx.obj["client"].get_category_related_tags(category_id, tag_names, **generate_api_kwargs(args))) + ) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) diff --git a/src/fred/cli/releases.py b/src/fred/cli/releases.py new file mode 100644 index 0000000..281d1f2 --- /dev/null +++ b/src/fred/cli/releases.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +""" +FRED CLI - Releases Namespace. +""" +import click + +from .. import BaseFredAPIError +from .._util import generate_api_kwargs, serialize + +__all__ = [ + "releases", +] + + +@click.group() +@click.pass_context +def releases(ctx): + """ + Releases CLI Namespace. + """ + pass + + +@releases.command() +@click.argument("args", nargs=-1) +@click.pass_context +def get_releases(ctx, args: tuple): + """Get all releases of economic data.""" + try: + click.echo(serialize(ctx.obj["client"].get_releases(**generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@releases.command() +@click.argument("args", nargs=-1) +@click.pass_context +def get_releases_dates(ctx, args: tuple): + """Get release dates for all releases of economic data.""" + try: + click.echo(serialize(ctx.obj["client"].get_releases_dates(**generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@releases.command() +@click.option("--release-id", "-i", required=True, type=click.INT, help="Release ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_release(ctx, release_id: int, args: tuple): + """Get a release of economic data.""" + try: + click.echo(serialize(ctx.obj["client"].get_release(release_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@releases.command() +@click.option("--release-id", "-i", required=True, type=click.INT, help="Release ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_release_dates(ctx, release_id: int, args: tuple): + """Get release dates for a release of economic data.""" + try: + click.echo(serialize(ctx.obj["client"].get_release_dates(release_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@releases.command() +@click.option("--release-id", "-i", required=True, type=click.INT, help="Release ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_release_series(ctx, release_id: int, args: tuple): + """Get the series on a release of economic data.""" + try: + click.echo(serialize(ctx.obj["client"].get_release_series(release_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@releases.command() +@click.option("--release-id", "-i", required=True, type=click.INT, help="Release ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_release_sources(ctx, release_id: int, args: tuple): + """Get the sources for a release of economic data.""" + try: + click.echo(serialize(ctx.obj["client"].get_release_sources(release_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@releases.command() +@click.option("--release-id", "-i", required=True, type=click.INT, help="Release ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_release_tags(ctx, release_id: int, args: tuple): + """Get the tags for a release.""" + try: + click.echo(serialize(ctx.obj["client"].get_release_tags(release_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@releases.command() +@click.option("--release-id", "-i", required=True, type=click.INT, help="Release ID.") +@click.option("--tag-names", "-t", required=True, type=click.STRING, help="Tag Names.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_release_related_tags(ctx, release_id: int, tag_names: str, args: tuple): + """Get the related tags for a release.""" + try: + click.echo( + serialize(ctx.obj["client"].get_release_related_tags(release_id, tag_names, **generate_api_kwargs(args))) + ) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@releases.command() +@click.option("--release-id", "-i", required=True, type=click.INT, help="Release ID.") +@click.option("--element-id", "-e", required=False, type=click.INT, help="Element ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_release_tables(ctx, release_id: int, element_id: int, args: tuple): + """Get the release table for a given release.""" + try: + click.echo(serialize(ctx.obj["client"].get_release_tables(release_id, element_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) diff --git a/src/fred/cli/series.py b/src/fred/cli/series.py new file mode 100644 index 0000000..bbe0d35 --- /dev/null +++ b/src/fred/cli/series.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +""" +FRED CLI - Series Namespace. +""" +import click + +from .. import BaseFredAPIError +from .._util import generate_api_kwargs, serialize + +__all__ = [ + "series", +] + + +@click.group() +@click.pass_context +def series(ctx): + """ + Series CLI Namespace. + """ + pass + + +@series.command() +@click.option("--series-id", "-i", required=True, type=click.STRING, help="Series ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_series(ctx, series_id: str, args: tuple): + """Get series.""" + try: + click.echo(serialize(ctx.obj["client"].get_series(series_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@series.command() +@click.option("--series-id", "-i", required=True, type=click.STRING, help="Series ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_series_categories(ctx, series_id: str, args: tuple): + """Get series categories.""" + try: + click.echo(serialize(ctx.obj["client"].get_series_categories(series_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@series.command() +@click.option("--series-id", "-i", required=True, type=click.STRING, help="Series ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_series_observations(ctx, series_id: str, args: tuple): + """Get series observations.""" + try: + click.echo(serialize(ctx.obj["client"].get_series_observations(series_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@series.command() +@click.option("--series-id", "-i", required=True, type=click.STRING, help="Series ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_series_release(ctx, series_id: str, args: tuple): + """Get series release.""" + try: + click.echo(serialize(ctx.obj["client"].get_series_release(series_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@series.command() +@click.option("--search-text", "-t", required=True, type=click.STRING, help="Search text.") +@click.option("--search-type", "-s", required=False, type=click.STRING, help="Search type.", default="full_text") +@click.argument("args", nargs=-1) +@click.pass_context +def get_series_search(ctx, search_text: str, search_type: str, args: tuple): + """Get series search.""" + try: + click.echo( + serialize(ctx.obj["client"].get_series_search(search_text, search_type, **generate_api_kwargs(args))) + ) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@series.command() +@click.option("--series-search-text", "-t", required=True, type=click.STRING, help="Series search text.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_series_search_tags(ctx, series_search_text: str, args: tuple): + """Get series search tags.""" + try: + click.echo(serialize(ctx.obj["client"].get_series_search_tags(series_search_text, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@series.command() +@click.option("--series-search-text", "-t", required=True, type=click.STRING, help="Series search text.") +@click.option("--tag-names", "-n", required=True, type=click.STRING, help="Tag names.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_series_search_related_tags(ctx, series_search_text: str, tag_names: str, args: tuple): + """Get series search related tags.""" + try: + click.echo( + serialize( + ctx.obj["client"].get_series_search_related_tags( + series_search_text, tag_names, **generate_api_kwargs(args) + ) + ) + ) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@series.command() +@click.option("--series-id", "-i", required=True, type=click.STRING, help="Series ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_series_tags(ctx, series_id: str, args: tuple): + """Get series tags.""" + try: + click.echo(serialize(ctx.obj["client"].get_series_tags(series_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@series.command() +@click.argument("args", nargs=-1) +@click.pass_context +def get_series_updates(ctx, args: tuple): + """Get series updates.""" + try: + click.echo(serialize(ctx.obj["client"].get_series_updates(**generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@series.command() +@click.option("--series-id", "-i", required=True, type=click.STRING, help="Series ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_series_vintagedates(ctx, series_id: str, args: tuple): + """Get series vintage dates.""" + try: + click.echo(serialize(ctx.obj["client"].get_series_vintagedates(series_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) diff --git a/src/fred/cli/sources.py b/src/fred/cli/sources.py new file mode 100644 index 0000000..c19e41e --- /dev/null +++ b/src/fred/cli/sources.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +FRED CLI - Sources Namespace. +""" +import click + +from .._util import generate_api_kwargs, serialize +from ..api import BaseFredAPIError + +__all__ = [ + "sources", +] + + +@click.group() +def sources(): + """ + Sources CLI Namespace. + """ + pass + + +@sources.command() +@click.argument("args", nargs=-1) +@click.pass_context +def get_sources(ctx, args: tuple): + """ + Get sources. + """ + try: + click.echo(serialize(ctx.obj["client"].get_sources(**generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@sources.command() +@click.option("--source-id", "-i", type=click.INT, required=True, help="Source ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_source(ctx, source_id: int, args: tuple): + """ + Get source by ID. + """ + try: + click.echo(serialize(ctx.obj["client"].get_source(source_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@sources.command() +@click.option("--source-id", "-i", type=click.INT, required=True, help="Source ID.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_source_releases(ctx, source_id: int, args: tuple): + """ + Get source releases by ID. + """ + try: + click.echo(serialize(ctx.obj["client"].get_source_releases(source_id, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) diff --git a/src/fred/cli/tags.py b/src/fred/cli/tags.py new file mode 100644 index 0000000..3738a1c --- /dev/null +++ b/src/fred/cli/tags.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +FRED CLI - Tags Namespace. +""" +import click + +from .. import BaseFredAPIError +from .._util import generate_api_kwargs, serialize + +__all__ = [ + "tags", +] + + +@click.group() +@click.pass_context +def tags(ctx): + """ + Tags CLI Namespace. + """ + pass + + +@tags.command() +@click.argument("args", nargs=-1) +@click.pass_context +def get_tags(ctx, args: tuple): + """ + Get tags. + """ + try: + click.echo(serialize(ctx.obj["client"].get_tags(**generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@tags.command() +@click.option("--tag-names", "-t", required=True, type=click.STRING, help="Tag Names.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_related_tags(ctx, tag_names: str, args: tuple): + """ + Get related tags. + """ + try: + click.echo(serialize(ctx.obj["client"].get_related_tags(tag_names, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) + + +@tags.command() +@click.option("--tag-names", "-t", required=True, type=click.STRING, help="Tag Names.") +@click.argument("args", nargs=-1) +@click.pass_context +def get_tags_series(ctx, tag_names: str, args: tuple): + """ + Get tag series. + """ + try: + click.echo(serialize(ctx.obj["client"].get_tags_series(tag_names, **generate_api_kwargs(args)))) + except (ValueError, BaseFredAPIError) as e: + raise click.UsageError(click.style(e, fg="red"), ctx) diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000..9a696ce --- /dev/null +++ b/tests/api/__init__.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +import os +from unittest import TestCase, mock + + +class BaseAPITest(TestCase): + """Base API Test Case.""" + + def setUp(self) -> None: + self.patcher = mock.patch.dict(os.environ, {"FRED_API_KEY": os.environ.get("TEST_FRED_API_KEY__API")}) + self.patcher.start() + self.base_params = { + "api_key": os.environ.get("FRED_API_KEY"), + "file_type": "json", + } + self.client = None + + def tearDown(self) -> None: + super().tearDown() + self.patcher.stop() diff --git a/tests/test_base_client.py b/tests/api/test_base_client.py similarity index 88% rename from tests/test_base_client.py rename to tests/api/test_base_client.py index f48d34a..23aa118 100644 --- a/tests/test_base_client.py +++ b/tests/api/test_base_client.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 import os -import unittest +from unittest import TestCase from xml.dom.minidom import parseString from xml.etree import ElementTree as ET from requests import get -from src.fred import FredAPIRequestError, BaseFredAPIError -from src.fred.api._fred_client import FredClient +from fred import FredAPIRequestError, BaseFredAPIError +from fred.api._fred_client import FredClient -class TestBaseFredClient(unittest.TestCase): +class TestBaseFredClient(TestCase): """Test functionality of base FredClient.""" @staticmethod @@ -20,13 +20,14 @@ def _unset_env_var(): def _set_env_var(self): """Set environment variable.""" - os.environ["FRED_API_KEY"] = self.client.get_api_key() + os.environ["FRED_API_KEY"] = self.api_key def setUp(self) -> None: """Setup the test.""" - self.client = FredClient() + self.api_key = os.environ.get("FRED_API_KEY") + self.client = FredClient(api_key=self.api_key) self.base_params = { - "api_key": self.client.get_api_key(), + "api_key": self.api_key, "file_type": "json", } @@ -89,7 +90,7 @@ def test_init_mechanism(self): finally: self._set_env_var() - og_client = FredClient() + og_client = FredClient(api_key=os.environ.get("FRED_API_KEY")) self._unset_env_var() try: FredClient(base_client=og_client) diff --git a/tests/test_categories.py b/tests/api/test_categories.py similarity index 90% rename from tests/test_categories.py rename to tests/api/test_categories.py index cd89b34..a6ffbee 100644 --- a/tests/test_categories.py +++ b/tests/api/test_categories.py @@ -2,24 +2,22 @@ """ Test the Fred API Categories Namespace. """ -import unittest +import os from requests import get -from src.fred import FredAPICategories +from fred import FredAPICategories +from tests.api import BaseAPITest from tests.const import BASE_FRED_URL -class TestFredAPICategories(unittest.TestCase): +class TestFredAPICategories(BaseAPITest): """Test the Fred API Categories Namespace.""" def setUp(self): """Setup the test.""" - self.client = FredAPICategories() - self.base_params = { - "api_key": self.client.get_api_key(), - "file_type": "json", - } + super().setUp() + self.client = FredAPICategories(api_key=os.environ.get("FRED_API_KEY")) def test_get_category(self): """Test the get_category method.""" diff --git a/tests/test_decorators.py b/tests/api/test_decorators.py similarity index 92% rename from tests/test_decorators.py rename to tests/api/test_decorators.py index 2375a22..169f788 100644 --- a/tests/test_decorators.py +++ b/tests/api/test_decorators.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import unittest -from src.fred._util.decorators import validate_api_args +from fred._util.decorators import validate_api_args class TestDecorators(unittest.TestCase): diff --git a/tests/test_releases.py b/tests/api/test_releases.py similarity index 92% rename from tests/test_releases.py rename to tests/api/test_releases.py index 16d9992..c89a7d6 100644 --- a/tests/test_releases.py +++ b/tests/api/test_releases.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 -import unittest +import os from requests import get -from src.fred import FredAPIReleases +from fred import FredAPIReleases +from tests.api import BaseAPITest from tests.const import BASE_FRED_URL -class TestFredAPIReleases(unittest.TestCase): +class TestFredAPIReleases(BaseAPITest): """Test Fred API Releases Namespace.""" def setUp(self) -> None: """Setup the test.""" - self.client = FredAPIReleases() - self.base_params = { - "api_key": self.client.get_api_key(), - "file_type": "json", - } + super().setUp() + self.client = FredAPIReleases(api_key=os.environ.get("FRED_API_KEY")) def test_get_releases(self): """Test the get_releases method.""" diff --git a/tests/test_series.py b/tests/api/test_series.py similarity index 93% rename from tests/test_series.py rename to tests/api/test_series.py index 815d245..3231b88 100644 --- a/tests/test_series.py +++ b/tests/api/test_series.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 -import unittest +import os from requests import get -from src.fred import FredAPISeries +from fred import FredAPISeries +from tests.api import BaseAPITest from tests.const import BASE_FRED_URL -class TestFredAPISeries(unittest.TestCase): +class TestFredAPISeries(BaseAPITest): """Test the Fred API Series Namespace.""" def setUp(self): """Setup the test.""" - self.client = FredAPISeries() - self.base_params = { - "api_key": self.client.get_api_key(), - "file_type": "json", - } + super().setUp() + self.client = FredAPISeries(api_key=os.environ.get("FRED_API_KEY")) def test_get_series(self): """Test the get_series method.""" diff --git a/tests/test_sources.py b/tests/api/test_sources.py similarity index 80% rename from tests/test_sources.py rename to tests/api/test_sources.py index 78e9175..49c7468 100644 --- a/tests/test_sources.py +++ b/tests/api/test_sources.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 -import unittest +import os from requests import get -from src.fred import FredAPISources +from fred import FredAPISources +from tests.api import BaseAPITest from tests.const import BASE_FRED_URL -class TestFredAPISources(unittest.TestCase): +class TestFredAPISources(BaseAPITest): """Test the Fred API Sources Namespace.""" def setUp(self): """Setup the test.""" - self.client = FredAPISources() - self.base_params = { - "api_key": self.client.get_api_key(), - "file_type": "json", - } + super().setUp() + self.client = FredAPISources(api_key=os.environ.get("FRED_API_KEY")) def test_get_sources(self): """Test the get_sources method.""" diff --git a/tests/test_tags.py b/tests/api/test_tags.py similarity index 81% rename from tests/test_tags.py rename to tests/api/test_tags.py index 7152a34..123db43 100644 --- a/tests/test_tags.py +++ b/tests/api/test_tags.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 -import unittest +import os from requests import get -from src.fred import FredAPITags +from fred import FredAPITags +from tests.api import BaseAPITest from tests.const import BASE_FRED_URL -class TestFredAPITags(unittest.TestCase): +class TestFredAPITags(BaseAPITest): """Test the Fred API Tags Namespace.""" def setUp(self): """Setup the test.""" - self.client = FredAPITags() - self.base_params = { - "api_key": self.client.get_api_key(), - "file_type": "json", - } + super().setUp() + self.client = FredAPITags(api_key=os.environ.get("FRED_API_KEY")) def test_get_tags(self): """Test the get_tags method.""" diff --git a/tests/cli/__init__.py b/tests/cli/__init__.py new file mode 100644 index 0000000..51c296c --- /dev/null +++ b/tests/cli/__init__.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +import os +from json import loads +from typing import List, Dict +from unittest import TestCase, mock + +from click.testing import CliRunner + +from fred import FredAPI + + +class BaseCLITest(TestCase): + """Base CLI Test Case.""" + + def setUp(self) -> None: + """Set up.""" + self.runner = CliRunner() + self.patcher = mock.patch.dict(os.environ, {"FRED_API_KEY": os.environ.get("TEST_FRED_API_KEY__CLI")}) + self.patcher.start() + self.client = FredAPI(api_key=os.environ.get("FRED_API_KEY")) + + # set basic context object for all cli tests + self.obj = { + "api_key": self.client.get_api_key(), + "client": self.client, + } + + # set basic params for all requests + self.base_params = { + "api_key": self.client.get_api_key(), + "file_type": "json", + } + + def tearDown(self) -> None: + """Clean up.""" + super().tearDown() + # stop mocking API key + self.patcher.stop() + + def run_test_cases(self, cli, tests: List[Dict]): + """Common logic for running a list of CLI test cases.""" + for case in tests: + + case_keys = set(case.keys()) + command = case.get("command", None) + exit_code = case.get("exit_code", None) + + with self.subTest(case["msg"], exit_code=exit_code, command=command): + + # get cli result + result = self.runner.invoke(cli, command, obj=self.obj) + + # assert output + if "output" in case_keys: + if isinstance(case["output"], dict): + comparison_keys = set(case["output"].keys()) + if "dict" in comparison_keys: + self.assertDictEqual(case["output"]["dict"], loads(result.output)) + elif "contains" in comparison_keys: + self.assertIn(case["output"]["contains"], result.output) + else: + self.assertEqual(case["output"], result.output) + else: + self.assertEqual(case["output"], result.output) + + # assert exit code + self.assertEqual(case["exit_code"], result.exit_code) diff --git a/tests/cli/test_categories_cli.py b/tests/cli/test_categories_cli.py new file mode 100644 index 0000000..ab6766d --- /dev/null +++ b/tests/cli/test_categories_cli.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +from requests import get + +from fred.cli.categories import categories +from tests.cli import BaseCLITest + + +class TestCLICategories(BaseCLITest): + def test_get_category(self): + """CLI test for get-category.""" + tests = [ + { + "msg": "Basic get-category test Ok", + "exit_code": 0, + "command": ["get-category", "-i", "125"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/category", + params={"category_id": "125", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-category test Fail", + "exit_code": 2, + "command": ["get-category", "-i", "125", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_category.", + }, + }, + ] + self.run_test_cases(categories, tests) + + def test_get_category_children(self): + """CLI test for get-category-children.""" + tests = [ + { + "msg": "Basic get-category test Ok", + "exit_code": 0, + "command": ["get-category-children", "-i", "13"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/category/children", + params={"category_id": "13", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-category test Fail", + "exit_code": 2, + "command": ["get-category-children", "-i", "13", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_category_children.", + }, + }, + ] + self.run_test_cases(categories, tests) + + def test_get_category_related(self): + """CLI test for get-category-related.""" + tests = [ + { + "msg": "Basic get-category-related test Ok", + "exit_code": 0, + "command": ["get-category-related", "-i", "32073"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/category/related", + params={"category_id": "32073", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-category-related test Fail", + "exit_code": 2, + "command": ["get-category-related", "-i", "32073", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_category_related.", + }, + }, + ] + self.run_test_cases(categories, tests) + + def test_get_category_series(self): + """CLI test for get-category-series.""" + tests = [ + { + "msg": "Basic get-category-series test Ok", + "exit_code": 0, + "command": ["get-category-series", "-i", "125"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/category/series", + params={"category_id": "125", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-category-series test Fail", + "exit_code": 2, + "command": ["get-category-series", "-i", "125", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_category_series.", + }, + }, + ] + self.run_test_cases(categories, tests) + + def test_get_category_tags(self): + """CLI test for get-category-tags.""" + tests = [ + { + "msg": "Basic get-category-tags test Ok", + "exit_code": 0, + "command": ["get-category-tags", "-i", "125"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/category/tags", + params={"category_id": "125", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-category-tags test Fail", + "exit_code": 2, + "command": ["get-category-tags", "-i", "125", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_category_tags.", + }, + }, + ] + self.run_test_cases(categories, tests) + + def test_get_category_related_tags(self): + """CLI test for get-category-related-tags.""" + tests = [ + { + "msg": "Basic get-category-related-tags test Ok", + "exit_code": 0, + "command": ["get-category-related-tags", "-i", "125", "-t", "services;quarterly"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/category/related_tags", + params={"category_id": "125", "tag_names": "services;quarterly", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-category-related-tags test Fail", + "exit_code": 2, + "command": ["get-category-related-tags", "-i", "125", "-t", "services;quarterly", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_category_related_tags.", + }, + }, + ] + self.run_test_cases(categories, tests) diff --git a/tests/cli/test_cli_entry_point.py b/tests/cli/test_cli_entry_point.py new file mode 100644 index 0000000..2146022 --- /dev/null +++ b/tests/cli/test_cli_entry_point.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +from tests.cli import BaseCLITest + + +class TestCLIEntryPoint(BaseCLITest): + """Test CLI entry point.""" + + def test_cli_entry_point_import_and_run(self): + """Test the CLI entry point imports properly.""" + from fred.cli.__main__ import run_cli + + self.assertIsNotNone(run_cli) + + # make sure CLI exits with sys.exit() + with self.assertRaises(SystemExit): + run_cli() diff --git a/tests/cli/test_cli_utils.py b/tests/cli/test_cli_utils.py new file mode 100644 index 0000000..27a34da --- /dev/null +++ b/tests/cli/test_cli_utils.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import unittest +from xml.etree import ElementTree as ET + +from fred._util import generate_api_kwargs, serialize + + +class TestCLIUtils(unittest.TestCase): + """Test CLI utilities.""" + + def test_generate_api_kwargs(self): + """Test the generate_api_kwargs function.""" + arguments = ("arg1=1", "arg2=2") + api_kwargs = generate_api_kwargs(arguments) + self.assertEqual(api_kwargs, {"arg1": "1", "arg2": "2"}) + + arguments = ("arg1=1", "arg2=2", "arg3-3", "arg4=", "arg5") + api_kwargs = generate_api_kwargs(arguments) + self.assertEqual(api_kwargs, {"arg1": "1", "arg2": "2", "arg4": ""}) + + def test_serialize_dict(self): + """Test the serialize function with a dict.""" + response_obj = {"key": "value"} + self.assertEqual(serialize(response_obj), '{\n "key": "value"\n}') + + def test_serialize_xml(self): + """Test the serialize function with an xml.etree.ElementTree.Element.""" + response_obj = ET.Element("root") + ET.SubElement(response_obj, "child") + self.assertEqual(serialize(response_obj), "") + + def test_serialize_error(self): + """Test the serialize function with an invalid response object.""" + response_obj = "invalid" + with self.assertRaises(TypeError): + serialize(response_obj) diff --git a/tests/cli/test_releases_cli.py b/tests/cli/test_releases_cli.py new file mode 100644 index 0000000..a51f724 --- /dev/null +++ b/tests/cli/test_releases_cli.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +from requests import get + +from fred.cli.releases import releases +from tests.cli import BaseCLITest + + +class TestCLIReleases(BaseCLITest): + """Test cases for Releases CLI.""" + + def test_get_releases(self): + """CLI test for get-releases.""" + tests = [ + { + "msg": "Basic get-releases test Ok", + "exit_code": 0, + "command": ["get-releases"], + "output": { + "dict": get("https://api.stlouisfed.org/fred/releases", params=self.base_params).json(), + }, + }, + { + "msg": "Basic get-releases test Fail", + "exit_code": 2, + "command": ["get-releases", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_releases.", + }, + }, + ] + self.run_test_cases(releases, tests) + + def test_get_releases_dates(self): + """CLI test for get-releases-dates.""" + tests = [ + { + "msg": "Basic get-releases-dates test Ok", + "exit_code": 0, + "command": ["get-releases-dates"], + "output": { + "dict": get("https://api.stlouisfed.org/fred/releases/dates", params=self.base_params).json(), + }, + }, + { + "msg": "Basic get-releases-dates test Fail", + "exit_code": 2, + "command": ["get-releases-dates", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_releases_dates.", + }, + }, + ] + self.run_test_cases(releases, tests) + + def test_get_release_dates(self): + """CLI test for get-release-dates.""" + tests = [ + { + "msg": "Basic get-release-dates test Ok", + "exit_code": 0, + "command": ["get-release-dates", "-i", "53"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/release/dates", + params={"release_id": "53", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-release-dates test Fail", + "exit_code": 2, + "command": ["get-release-dates", "-i", "53", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_release_dates.", + }, + }, + ] + self.run_test_cases(releases, tests) + + def test_get_release(self): + """CLI test for get-release.""" + tests = [ + { + "msg": "Basic get-release test Ok", + "exit_code": 0, + "command": ["get-release", "-i", "53"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/release", params={"release_id": "53", **self.base_params} + ).json(), + }, + }, + { + "msg": "Basic get-release test Fail", + "exit_code": 2, + "command": ["get-release", "-i", "53", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_release.", + }, + }, + ] + self.run_test_cases(releases, tests) + + def test_get_release_series(self): + """CLI test for get-release-series.""" + tests = [ + { + "msg": "Basic get-release-series test Ok", + "exit_code": 0, + "command": ["get-release-series", "-i", "53"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/release/series", + params={"release_id": "53", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-release-series test Fail", + "exit_code": 2, + "command": ["get-release-series", "-i", "53", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_release_series.", + }, + }, + ] + self.run_test_cases(releases, tests) + + def test_get_release_sources(self): + """CLI test for get-release-sources.""" + tests = [ + { + "msg": "Basic get-release-sources test Ok", + "exit_code": 0, + "command": ["get-release-sources", "-i", "53"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/release/sources", + params={"release_id": "53", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-release-sources test Fail", + "exit_code": 2, + "command": ["get-release-sources", "-i", "53", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_release_sources.", + }, + }, + ] + self.run_test_cases(releases, tests) + + def test_get_release_tags(self): + """CLI test for get-release-tags.""" + tests = [ + { + "msg": "Basic get-release-tags test Ok", + "exit_code": 0, + "command": ["get-release-tags", "-i", "53"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/release/tags", params={"release_id": "53", **self.base_params} + ).json(), + }, + }, + { + "msg": "Basic get-release-tags test Fail", + "exit_code": 2, + "command": ["get-release-tags", "-i", "53", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_release_tags.", + }, + }, + ] + self.run_test_cases(releases, tests) + + def test_get_release_related_tags(self): + """CLI test for get-release-related-tags.""" + tests = [ + { + "msg": "Basic get-release-related-tags test Ok", + "exit_code": 0, + "command": ["get-release-related-tags", "-i", "86", "-t", "sa;foreign"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/release/related_tags", + params={"release_id": "86", "tag_names": "sa;foreign", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-release-related-tags test Fail", + "exit_code": 2, + "command": ["get-release-related-tags", "-i", "86", "-t", "sa;foreign", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_release_related_tags.", + }, + }, + ] + self.run_test_cases(releases, tests) + + def test_get_release_tables(self): + """CLI test for get-release-tables.""" + tests = [ + { + "msg": "Basic get-release-tables test Ok", + "exit_code": 0, + "command": ["get-release-tables", "-i", "53", "-e", "12886"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/release/tables", + params={"release_id": "53", "element_id": "12886", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-release-tables test Fail", + "exit_code": 2, + "command": ["get-release-tables", "-i", "53", "-e", "12886", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_release_tables.", + }, + }, + ] + self.run_test_cases(releases, tests) diff --git a/tests/cli/test_series_cli.py b/tests/cli/test_series_cli.py new file mode 100644 index 0000000..b3e1fe3 --- /dev/null +++ b/tests/cli/test_series_cli.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +from requests import get + +from fred.cli.series import series +from tests.cli import BaseCLITest + + +class TestCLISeries(BaseCLITest): + """Test cases for Series CLI.""" + + def test_get_series(self): + """CLI test for get-series.""" + tests = [ + { + "msg": "Basic get-series test Ok", + "exit_code": 0, + "command": ["get-series", "-i", "dff"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/series", params={"series_id": "dff", **self.base_params} + ).json(), + }, + }, + { + "msg": "Basic get-series test Fail", + "exit_code": 2, + "command": ["get-series", "-i", "dff", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_series.", + }, + }, + ] + self.run_test_cases(series, tests) + + def test_get_series_observations(self): + """CLI test for get-series-observations.""" + tests = [ + { + "msg": "Basic get-series-observations test Ok", + "exit_code": 0, + "command": ["get-series-observations", "-i", "dff"], + "output": { + "dict": get( + f"https://api.stlouisfed.org/fred/series/observations", + params={"series_id": "dff", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-series-observations test Fail", + "exit_code": 2, + "command": ["get-series-observations", "-i", "dff", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_series_observations.", + }, + }, + { + "msg": "Test get-series-observations with limit=1 sort_order=desc Ok", + "exit_code": 0, + "command": ["get-series-observations", "-i", "dff", "limit=1", "sort_order=desc"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/series/observations", + params={"series_id": "dff", "limit": 1, "sort_order": "desc", **self.base_params}, + ).json(), + }, + }, + ] + self.run_test_cases(series, tests) + + def test_get_series_release(self): + """CLI test for get-series-releases.""" + tests = [ + { + "msg": "Basic get-series-release test Ok", + "exit_code": 0, + "command": ["get-series-release", "-i", "dff"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/series/release", + params={"series_id": "dff", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-series-release test Fail", + "exit_code": 2, + "command": ["get-series-release", "-i", "dff", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_series_release.", + }, + }, + ] + self.run_test_cases(series, tests) + + def test_get_series_categories(self): + """CLI test for get-categories.""" + tests = [ + { + "msg": "Basic get-series-categories test Ok", + "exit_code": 0, + "command": ["get-series-categories", "-i", "dff"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/series/categories", + params={"series_id": "dff", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-series-categories test Fail", + "exit_code": 2, + "command": ["get-series-categories", "-i", "dff", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_series_categories.", + }, + }, + ] + self.run_test_cases(series, tests) + + def test_get_series_search(self): + """CLI test for get-series-search.""" + tests = [ + { + "msg": "Basic get-series-search test Ok", + "exit_code": 0, + "command": ["get-series-search", "--search-text", "monetary+service+index"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/series/search", + params={"search_text": "monetary+service+index", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-series-search test Fail", + "exit_code": 2, + "command": ["get-series-search", "-t", "monetary+service+index", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_series_search.", + }, + }, + ] + self.run_test_cases(series, tests) + + def test_get_series_search_tags(self): + """CLI test for get-series-search-tags.""" + tests = [ + { + "msg": "Basic get-series-search-tags test Ok", + "exit_code": 0, + "command": ["get-series-search-tags", "-t", "monetary+service+index"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/series/search/tags", + params={"series_search_text": "monetary+service+index", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-series-search-tags test Fail", + "exit_code": 2, + "command": ["get-series-search-tags", "-t", "monetary+service+index", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_series_search_tags.", + }, + }, + ] + self.run_test_cases(series, tests) + + def test_get_series_search_related_tags(self): + """CLI test for get-series-search-related-tags using series_search_text=mortgage+rate&tag_names=30-year;frb""" + tests = [ + { + "msg": "Basic get-series-search-related-tags test Ok", + "exit_code": 0, + "command": [ + "get-series-search-related-tags", + "-t", + "mortgage+rate", + "-n", + "30-year;frb", + ], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/series/search/related_tags", + params={ + "series_search_text": "mortgage+rate", + "tag_names": "30-year;frb", + **self.base_params, + }, + ).json(), + }, + }, + { + "msg": "Basic get-series-search-related-tags test Fail", + "exit_code": 2, + "command": [ + "get-series-search-related-tags", + "-t", + "mortgage+rate", + "-n", + "30-year;frb", + "asdf=asdf", + ], + "output": { + "contains": "Error: asdf is not a valid argument for get_series_search_related_tags.", + }, + }, + ] + self.run_test_cases(series, tests) + + def test_get_series_tags(self): + """CLI test for get-series-tags.""" + tests = [ + { + "msg": "Basic get-series-tags test Ok", + "exit_code": 0, + "command": ["get-series-tags", "-i", "dff"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/series/tags", + params={"series_id": "dff", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-series-tags test Fail", + "exit_code": 2, + "command": ["get-series-tags", "-i", "dff", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_series_tags.", + }, + }, + ] + self.run_test_cases(series, tests) + + def test_get_series_updates(self): + """CLI test for get-series-updates.""" + tests = [ + { + "msg": "Basic get-series-updates test Ok", + "exit_code": 0, + "command": ["get-series-updates"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/series/updates", + params=self.base_params, + ).json(), + }, + }, + { + "msg": "Basic get-series-updates test Fail", + "exit_code": 2, + "command": ["get-series-updates", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_series_updates.", + }, + }, + ] + self.run_test_cases(series, tests) + + def test_get_series_vintagedates(self): + """CLI test for get-series-vintagedates.""" + tests = [ + { + "msg": "Basic get-series-vintagedates test Ok", + "exit_code": 0, + "command": ["get-series-vintagedates", "-i", "GNPCA"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/series/vintagedates", + params={"series_id": "GNPCA", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-series-vintagedates test Fail", + "exit_code": 2, + "command": ["get-series-vintagedates", "-i", "GNPCA", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_series_vintagedates.", + }, + }, + ] + self.run_test_cases(series, tests) diff --git a/tests/cli/test_sources_cli.py b/tests/cli/test_sources_cli.py new file mode 100644 index 0000000..8036b5a --- /dev/null +++ b/tests/cli/test_sources_cli.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +from requests import get + +from fred.cli.sources import sources +from tests.cli import BaseCLITest + + +class TestCLISources(BaseCLITest): + """Test CLI utilities.""" + + def test_get_sources(self): + """CLI test for get-sources.""" + tests = [ + { + "msg": "Basic get-sources test Ok", + "exit_code": 0, + "command": ["get-sources"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/sources", + params={**self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-sources test Fail", + "exit_code": 2, + "command": ["get-sources", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_sources.", + }, + }, + ] + self.run_test_cases(sources, tests) + + def test_get_source(self): + """CLI test for get-source.""" + tests = [ + { + "msg": "Basic get-source test Ok", + "exit_code": 0, + "command": ["get-source", "-i", "1"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/source", + params={"source_id": "1", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-source test Fail", + "exit_code": 2, + "command": ["get-source", "-i", "1", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_source.", + }, + }, + ] + self.run_test_cases(sources, tests) + + def test_get_source_releases(self): + """CLI test for get-source-releases.""" + tests = [ + { + "msg": "Basic get-source-releases test Ok", + "exit_code": 0, + "command": ["get-source-releases", "-i", "1"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/source/releases", + params={"source_id": "1", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-source-releases test Fail", + "exit_code": 2, + "command": ["get-source-releases", "-i", "1", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_source_releases.", + }, + }, + ] + self.run_test_cases(sources, tests) diff --git a/tests/cli/test_tags_cli.py b/tests/cli/test_tags_cli.py new file mode 100644 index 0000000..efe528e --- /dev/null +++ b/tests/cli/test_tags_cli.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +from requests import get + +from fred.cli.tags import tags +from tests.cli import BaseCLITest + + +class TestCLITags(BaseCLITest): + def test_get_tags(self): + """CLI test for get-tags.""" + tests = [ + { + "msg": "Basic get-tags test Ok", + "exit_code": 0, + "command": ["get-tags"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/tags", + params={**self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-tags test Fail", + "exit_code": 2, + "command": ["get-tags", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_tags.", + }, + }, + ] + self.run_test_cases(tags, tests) + + def test_get_related_tags(self): + """CLI test for get-related-tags.""" + tests = [ + { + "msg": "Basic get-related-tags test Ok", + "exit_code": 0, + "command": ["get-related-tags", "-t", "slovenia;food;oecd"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/related_tags", + params={"tag_names": "slovenia;food;oecd", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-related_tags test Fail", + "exit_code": 2, + "command": ["get-related-tags", "-t", "slovenia;food;oecd", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_related_tags.", + }, + }, + ] + self.run_test_cases(tags, tests) + + def test_get_tags_series(self): + """CLI test for get-tags-series.""" + tests = [ + { + "msg": "Basic get-tags-series test Ok", + "exit_code": 0, + "command": ["get-tags-series", "-t", "slovenia;food;oecd"], + "output": { + "dict": get( + "https://api.stlouisfed.org/fred/tags/series", + params={"tag_names": "slovenia;food;oecd", **self.base_params}, + ).json(), + }, + }, + { + "msg": "Basic get-tags-series test Fail", + "exit_code": 2, + "command": ["get-tags-series", "-t", "slovenia;food;oecd", "asdf=asdf"], + "output": { + "contains": "Error: asdf is not a valid argument for get_tags_series.", + }, + }, + ] + self.run_test_cases(tags, tests)