Skip to content

Commit

Permalink
Feature: Command shortcuts (#36)
Browse files Browse the repository at this point in the history
* include optional deps, remove requirements file

* add command shortcuts for each namespace

* update pyproject to include new script shortcuts

* fix failing entrypoint test

* add types to all cli sub-commands

* add more utility tests

* update ci and pyproject tool configuration

* add test

* add type hint

* add more cli entrypoint tests for each subcommand

* final test
  • Loading branch information
zachspar authored Jan 5, 2024
1 parent ee26cc5 commit 7a8ae40
Show file tree
Hide file tree
Showing 13 changed files with 349 additions and 119 deletions.
8 changes: 3 additions & 5 deletions .github/workflows/fred-py-api-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,14 @@ jobs:
- name: Install dependencies
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
python -m pip install -e .[ci]
- name: Lint with black
run: |
# Run black on all Python files
black --line-length 120 --check ./
black --check ./
- name: Test with coverage
run: |
coverage run --source="src" --omit="src/fred/cli/__main__.py","src/fred/cli/__init__.py" -m unittest
coverage run -m unittest
coverage report -m
- name: Upload coverage report
uses: codecov/codecov-action@v2
Expand Down
43 changes: 37 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"

[project]
name = "fred-py-api"
version = "1.1.1"
version = "1.1.2"
authors = [
{ name="Zachary Spar", email="[email protected]" },
{ name="Prasiddha Parthsarthy", email="[email protected]" },
{ name="Zachary Spar", email="[email protected]" },
{ name="Prasiddha Parthsarthy", email="[email protected]" },
]
description = "A fully featured FRED Command Line Interface & Python API client library."
readme = "README.md"
Expand All @@ -26,13 +26,44 @@ classifiers = [
"Operating System :: OS Independent",
]
dependencies = [
"requests>=2.17.3",
"click>=7.0",
"requests>=2.17.3",
"click>=7.0",
]

[project.optional-dependencies]
ci = [
"black==22.6.0",
"coverage==6.4.2",
]
dev = [
"black==22.6.0",
"coverage==6.4.2",
"tox==3.25.1",
]

[project.scripts]
fred = "fred.cli:__main__.run_cli"
fred = "fred.cli:__main__.run_fred_cli"
categories = "fred.cli:categories.run_categories_cli"
releases = "fred.cli:releases.run_releases_cli"
series = "fred.cli:series.run_series_cli"
sources = "fred.cli:sources.run_sources_cli"
tags = "fred.cli:tags.run_tags_cli"

[project.urls]
"Homepage" = "https://github.com/zachspar/fred_py_api"
"Bug Tracker" = "https://github.com/zachspar/fred_py_api/issues"

[tool.coverage.run]
branch = true
source = ["src"]

[tool.coverage.report]
exclude_lines = [
"if __name__ == .__main__.:",
]

[tool.coverage.html]
directory = "coverage_html_report"

[tool.black]
line-length = 120
25 changes: 0 additions & 25 deletions requirements.txt

This file was deleted.

29 changes: 28 additions & 1 deletion src/fred/_util/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
FRED CLI Utilities.
"""
from json import dumps
from typing import Union
from typing import Union, Callable
from xml.etree import ElementTree as ET

import click


__all__ = [
"generate_api_kwargs",
"serialize",
"run_cli_callable",
"init_cli_context",
]


Expand All @@ -32,3 +36,26 @@ def serialize(response_obj: Union[dict, ET.Element]) -> str:
return ET.tostring(response_obj, encoding="unicode", method="xml")
else:
raise TypeError("response_obj must be a dict or xml.etree.ElementTree.Element")


def run_cli_callable(cli_callable: Callable) -> None:
"""
Run a CLI callable.
"""
try:
cli_callable(auto_envvar_prefix="FRED")
except AssertionError:
click.echo(click.style("Error: FRED_API_KEY is not set!", fg="red"))


def init_cli_context(ctx: click.Context, api_key: str, api_client_class: Callable) -> None:
"""
Initialize a CLI context.
"""
ctx.ensure_object(dict)

if "api_key" not in ctx.obj:
ctx.obj["api_key"] = api_key

if "client" not in ctx.obj:
ctx.obj["client"] = api_client_class(api_key=api_key)
27 changes: 13 additions & 14 deletions src/fred/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
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
from .categories import categories as categories_cli_callable
from .releases import releases as releases_cli_callable
from .series import series as series_cli_callable
from .sources import sources as sources_cli_callable
from .tags import tags as tags_cli_callable
from .._util.cli import init_cli_context

__all__ = [
"fred_cli",
Expand All @@ -18,16 +19,14 @@
@group()
@click.option("--api-key", type=click.STRING, required=False, help="FRED API key.")
@click.pass_context
def fred_cli(ctx, api_key: str):
def fred_cli(ctx: click.Context, 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)
init_cli_context(ctx, api_key, FredAPI)


# 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)
fred_cli.add_command(categories_cli_callable)
fred_cli.add_command(releases_cli_callable)
fred_cli.add_command(series_cli_callable)
fred_cli.add_command(sources_cli_callable)
fred_cli.add_command(tags_cli_callable)
19 changes: 10 additions & 9 deletions src/fred/cli/__main__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
#!/usr/bin/env python3
import click

from . import fred_cli
from .._util import run_cli_callable


__all__ = [
"run_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"))
def run_fred_cli():
"""Run the CLI."""
run_cli_callable(cli_callable=fred_cli)


if __name__ == "__main__":
run_cli()
run_fred_cli()
31 changes: 21 additions & 10 deletions src/fred/cli/categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,30 @@
"""
import click

from .. import BaseFredAPIError
from .._util import generate_api_kwargs, serialize
from .. import BaseFredAPIError, FredAPICategories
from .._util import generate_api_kwargs, serialize, run_cli_callable, init_cli_context

__all__ = [
"categories",
"run_categories_cli",
]


@click.group()
@click.option("--api-key", type=click.STRING, required=False, help="FRED API key.")
@click.pass_context
def categories(ctx):
def categories(ctx: click.Context, api_key: str):
"""
Categories CLI Namespace.
"""
pass
init_cli_context(ctx, api_key, FredAPICategories)


@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):
def get_category(ctx: click.Context, category_id: int, args: tuple):
"""Get a category."""
try:
click.echo(serialize(ctx.obj["client"].get_category(category_id, **generate_api_kwargs(args))))
Expand All @@ -37,7 +39,7 @@ def get_category(ctx, category_id: int, args: tuple):
@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):
def get_category_children(ctx: click.Context, 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))))
Expand All @@ -49,7 +51,7 @@ def get_category_children(ctx, category_id: int, args: tuple):
@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):
def get_category_related(ctx: click.Context, category_id: int, args: tuple):
"""Get related categories."""
try:
click.echo(serialize(ctx.obj["client"].get_category_related(category_id, **generate_api_kwargs(args))))
Expand All @@ -61,7 +63,7 @@ def get_category_related(ctx, category_id: int, args: tuple):
@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):
def get_category_series(ctx: click.Context, 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))))
Expand All @@ -73,7 +75,7 @@ def get_category_series(ctx, category_id: int, args: tuple):
@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):
def get_category_tags(ctx: click.Context, 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))))
Expand All @@ -86,11 +88,20 @@ def get_category_tags(ctx, category_id: int, args: tuple):
@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):
def get_category_related_tags(ctx: click.Context, 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)


def run_categories_cli():
"""Run the CLI for Categories namespace."""
run_cli_callable(cli_callable=categories)


if __name__ == "__main__":
run_categories_cli()
Loading

0 comments on commit 7a8ae40

Please sign in to comment.