Skip to content

Commit 7a8ae40

Browse files
authored
Feature: Command shortcuts (#36)
* 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
1 parent ee26cc5 commit 7a8ae40

13 files changed

+349
-119
lines changed

.github/workflows/fred-py-api-package.yml

+3-5
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,14 @@ jobs:
3333
- name: Install dependencies
3434
run: |
3535
python -m pip install --upgrade pip
36-
python -m pip install requests coverage black==22.6.0
37-
python -m pip install -e .
38-
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
36+
python -m pip install -e .[ci]
3937
- name: Lint with black
4038
run: |
4139
# Run black on all Python files
42-
black --line-length 120 --check ./
40+
black --check ./
4341
- name: Test with coverage
4442
run: |
45-
coverage run --source="src" --omit="src/fred/cli/__main__.py","src/fred/cli/__init__.py" -m unittest
43+
coverage run -m unittest
4644
coverage report -m
4745
- name: Upload coverage report
4846
uses: codecov/codecov-action@v2

pyproject.toml

+37-6
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "fred-py-api"
7-
version = "1.1.1"
7+
version = "1.1.2"
88
authors = [
9-
{ name="Zachary Spar", email="[email protected]" },
10-
{ name="Prasiddha Parthsarthy", email="[email protected]" },
9+
{ name="Zachary Spar", email="[email protected]" },
10+
{ name="Prasiddha Parthsarthy", email="[email protected]" },
1111
]
1212
description = "A fully featured FRED Command Line Interface & Python API client library."
1313
readme = "README.md"
@@ -26,13 +26,44 @@ classifiers = [
2626
"Operating System :: OS Independent",
2727
]
2828
dependencies = [
29-
"requests>=2.17.3",
30-
"click>=7.0",
29+
"requests>=2.17.3",
30+
"click>=7.0",
31+
]
32+
33+
[project.optional-dependencies]
34+
ci = [
35+
"black==22.6.0",
36+
"coverage==6.4.2",
37+
]
38+
dev = [
39+
"black==22.6.0",
40+
"coverage==6.4.2",
41+
"tox==3.25.1",
3142
]
3243

3344
[project.scripts]
34-
fred = "fred.cli:__main__.run_cli"
45+
fred = "fred.cli:__main__.run_fred_cli"
46+
categories = "fred.cli:categories.run_categories_cli"
47+
releases = "fred.cli:releases.run_releases_cli"
48+
series = "fred.cli:series.run_series_cli"
49+
sources = "fred.cli:sources.run_sources_cli"
50+
tags = "fred.cli:tags.run_tags_cli"
3551

3652
[project.urls]
3753
"Homepage" = "https://github.com/zachspar/fred_py_api"
3854
"Bug Tracker" = "https://github.com/zachspar/fred_py_api/issues"
55+
56+
[tool.coverage.run]
57+
branch = true
58+
source = ["src"]
59+
60+
[tool.coverage.report]
61+
exclude_lines = [
62+
"if __name__ == .__main__.:",
63+
]
64+
65+
[tool.coverage.html]
66+
directory = "coverage_html_report"
67+
68+
[tool.black]
69+
line-length = 120

requirements.txt

-25
This file was deleted.

src/fred/_util/cli.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
FRED CLI Utilities.
44
"""
55
from json import dumps
6-
from typing import Union
6+
from typing import Union, Callable
77
from xml.etree import ElementTree as ET
88

9+
import click
10+
911

1012
__all__ = [
1113
"generate_api_kwargs",
1214
"serialize",
15+
"run_cli_callable",
16+
"init_cli_context",
1317
]
1418

1519

@@ -32,3 +36,26 @@ def serialize(response_obj: Union[dict, ET.Element]) -> str:
3236
return ET.tostring(response_obj, encoding="unicode", method="xml")
3337
else:
3438
raise TypeError("response_obj must be a dict or xml.etree.ElementTree.Element")
39+
40+
41+
def run_cli_callable(cli_callable: Callable) -> None:
42+
"""
43+
Run a CLI callable.
44+
"""
45+
try:
46+
cli_callable(auto_envvar_prefix="FRED")
47+
except AssertionError:
48+
click.echo(click.style("Error: FRED_API_KEY is not set!", fg="red"))
49+
50+
51+
def init_cli_context(ctx: click.Context, api_key: str, api_client_class: Callable) -> None:
52+
"""
53+
Initialize a CLI context.
54+
"""
55+
ctx.ensure_object(dict)
56+
57+
if "api_key" not in ctx.obj:
58+
ctx.obj["api_key"] = api_key
59+
60+
if "client" not in ctx.obj:
61+
ctx.obj["client"] = api_client_class(api_key=api_key)

src/fred/cli/__init__.py

+13-14
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
from click import group
55

66
from fred import FredAPI
7-
from .categories import categories
8-
from .releases import releases
9-
from .series import series
10-
from .sources import sources
11-
from .tags import tags
7+
from .categories import categories as categories_cli_callable
8+
from .releases import releases as releases_cli_callable
9+
from .series import series as series_cli_callable
10+
from .sources import sources as sources_cli_callable
11+
from .tags import tags as tags_cli_callable
12+
from .._util.cli import init_cli_context
1213

1314
__all__ = [
1415
"fred_cli",
@@ -18,16 +19,14 @@
1819
@group()
1920
@click.option("--api-key", type=click.STRING, required=False, help="FRED API key.")
2021
@click.pass_context
21-
def fred_cli(ctx, api_key: str):
22+
def fred_cli(ctx: click.Context, api_key: str):
2223
"""CLI for the Federal Reserve Economic Data (FRED)."""
23-
ctx.ensure_object(dict)
24-
ctx.obj["api_key"] = api_key
25-
ctx.obj["client"] = FredAPI(api_key=api_key)
24+
init_cli_context(ctx, api_key, FredAPI)
2625

2726

2827
# add each FRED command group
29-
fred_cli.add_command(categories)
30-
fred_cli.add_command(releases)
31-
fred_cli.add_command(series)
32-
fred_cli.add_command(sources)
33-
fred_cli.add_command(tags)
28+
fred_cli.add_command(categories_cli_callable)
29+
fred_cli.add_command(releases_cli_callable)
30+
fred_cli.add_command(series_cli_callable)
31+
fred_cli.add_command(sources_cli_callable)
32+
fred_cli.add_command(tags_cli_callable)

src/fred/cli/__main__.py

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
#!/usr/bin/env python3
2-
import click
3-
42
from . import fred_cli
3+
from .._util import run_cli_callable
4+
5+
6+
__all__ = [
7+
"run_fred_cli",
8+
]
59

610

7-
def run_cli():
8-
"""Run the FRED CLI."""
9-
try:
10-
fred_cli(auto_envvar_prefix="FRED")
11-
except AssertionError:
12-
click.echo(click.style("Error: FRED_API_KEY is not set!", fg="red"))
11+
def run_fred_cli():
12+
"""Run the CLI."""
13+
run_cli_callable(cli_callable=fred_cli)
1314

1415

1516
if __name__ == "__main__":
16-
run_cli()
17+
run_fred_cli()

src/fred/cli/categories.py

+21-10
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,30 @@
44
"""
55
import click
66

7-
from .. import BaseFredAPIError
8-
from .._util import generate_api_kwargs, serialize
7+
from .. import BaseFredAPIError, FredAPICategories
8+
from .._util import generate_api_kwargs, serialize, run_cli_callable, init_cli_context
99

1010
__all__ = [
1111
"categories",
12+
"run_categories_cli",
1213
]
1314

1415

1516
@click.group()
17+
@click.option("--api-key", type=click.STRING, required=False, help="FRED API key.")
1618
@click.pass_context
17-
def categories(ctx):
19+
def categories(ctx: click.Context, api_key: str):
1820
"""
1921
Categories CLI Namespace.
2022
"""
21-
pass
23+
init_cli_context(ctx, api_key, FredAPICategories)
2224

2325

2426
@categories.command()
2527
@click.option("--category-id", "-i", required=True, type=click.STRING, help="Category ID.")
2628
@click.argument("args", nargs=-1)
2729
@click.pass_context
28-
def get_category(ctx, category_id: int, args: tuple):
30+
def get_category(ctx: click.Context, category_id: int, args: tuple):
2931
"""Get a category."""
3032
try:
3133
click.echo(serialize(ctx.obj["client"].get_category(category_id, **generate_api_kwargs(args))))
@@ -37,7 +39,7 @@ def get_category(ctx, category_id: int, args: tuple):
3739
@click.option("--category-id", "-i", required=True, type=click.STRING, help="Category ID.")
3840
@click.argument("args", nargs=-1)
3941
@click.pass_context
40-
def get_category_children(ctx, category_id: int, args: tuple):
42+
def get_category_children(ctx: click.Context, category_id: int, args: tuple):
4143
"""Get the child categories."""
4244
try:
4345
click.echo(serialize(ctx.obj["client"].get_category_children(category_id, **generate_api_kwargs(args))))
@@ -49,7 +51,7 @@ def get_category_children(ctx, category_id: int, args: tuple):
4951
@click.option("--category-id", "-i", required=True, type=click.STRING, help="Category ID.")
5052
@click.argument("args", nargs=-1)
5153
@click.pass_context
52-
def get_category_related(ctx, category_id: int, args: tuple):
54+
def get_category_related(ctx: click.Context, category_id: int, args: tuple):
5355
"""Get related categories."""
5456
try:
5557
click.echo(serialize(ctx.obj["client"].get_category_related(category_id, **generate_api_kwargs(args))))
@@ -61,7 +63,7 @@ def get_category_related(ctx, category_id: int, args: tuple):
6163
@click.option("--category-id", "-i", required=True, type=click.STRING, help="Category ID.")
6264
@click.argument("args", nargs=-1)
6365
@click.pass_context
64-
def get_category_series(ctx, category_id: int, args: tuple):
66+
def get_category_series(ctx: click.Context, category_id: int, args: tuple):
6567
"""Get series in a category."""
6668
try:
6769
click.echo(serialize(ctx.obj["client"].get_category_series(category_id, **generate_api_kwargs(args))))
@@ -73,7 +75,7 @@ def get_category_series(ctx, category_id: int, args: tuple):
7375
@click.option("--category-id", "-i", required=True, type=click.STRING, help="Category ID.")
7476
@click.argument("args", nargs=-1)
7577
@click.pass_context
76-
def get_category_tags(ctx, category_id: int, args: tuple):
78+
def get_category_tags(ctx: click.Context, category_id: int, args: tuple):
7779
"""Get FRED tags for a category."""
7880
try:
7981
click.echo(serialize(ctx.obj["client"].get_category_tags(category_id, **generate_api_kwargs(args))))
@@ -86,11 +88,20 @@ def get_category_tags(ctx, category_id: int, args: tuple):
8688
@click.option("--tag-names", "-t", required=True, type=click.STRING, help="Tag Names.")
8789
@click.argument("args", nargs=-1)
8890
@click.pass_context
89-
def get_category_related_tags(ctx, category_id: int, tag_names: str, args: tuple):
91+
def get_category_related_tags(ctx: click.Context, category_id: int, tag_names: str, args: tuple):
9092
"""Get related FRED tags for a category."""
9193
try:
9294
click.echo(
9395
serialize(ctx.obj["client"].get_category_related_tags(category_id, tag_names, **generate_api_kwargs(args)))
9496
)
9597
except (ValueError, BaseFredAPIError) as e:
9698
raise click.UsageError(click.style(e, fg="red"), ctx)
99+
100+
101+
def run_categories_cli():
102+
"""Run the CLI for Categories namespace."""
103+
run_cli_callable(cli_callable=categories)
104+
105+
106+
if __name__ == "__main__":
107+
run_categories_cli()

0 commit comments

Comments
 (0)