Skip to content

Commit

Permalink
Feature/cli (#26)
Browse files Browse the repository at this point in the history
* 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 <zachspar@gmail.com>
Co-authored-by: Zachary Spar <41600414+zachspar@users.noreply.github.com>
  • Loading branch information
3 people committed Oct 8, 2022
1 parent c15b84d commit 3513e02
Show file tree
Hide file tree
Showing 32 changed files with 1,624 additions and 62 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/fred-py-api-package.yml
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -31,14 +34,15 @@ 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: |
# Run black on all Python files
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
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
[![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)

### FRED References:
- [Create an API Key Here](https://fredaccount.stlouisfed.org/apikey)
- [API Documentation](https://fred.stlouisfed.org/docs/api/fred/)

10 changes: 7 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -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"
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions src/fred/_util/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env python3
from .cli import *
from .decorators import *
34 changes: 34 additions & 0 deletions src/fred/_util/cli.py
Original file line number Diff line number Diff line change
@@ -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")
8 changes: 8 additions & 0 deletions src/fred/_util/decorators.py
Original file line number Diff line number Diff line change
@@ -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."""

Expand Down
2 changes: 1 addition & 1 deletion src/fred/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions src/fred/api/_fred_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
Fred API Client.
"""
from http import HTTPStatus
from os import environ
from typing import Optional, Dict, TypeVar
from xml.etree import ElementTree as ET

import requests

from .exceptions import FredAPIRequestError


__all__ = [
"FredClient",
"JsonOrXml",
Expand All @@ -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"

Expand Down
33 changes: 33 additions & 0 deletions src/fred/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
16 changes: 16 additions & 0 deletions src/fred/cli/__main__.py
Original file line number Diff line number Diff line change
@@ -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()
96 changes: 96 additions & 0 deletions src/fred/cli/categories.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 3513e02

Please sign in to comment.