Skip to content

Commit

Permalink
Merge pull request #8 from crossjam/feature-implement-entries-cli-sub…
Browse files Browse the repository at this point in the history
…command-2

Complete implementation of entries subcommand
Closes issue: #2
  • Loading branch information
crossjam authored Oct 18, 2023
2 parents 18430ee + 3b53654 commit 9abbc6f
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 16 deletions.
44 changes: 37 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# feedbin-tools

[![PyPI](https://img.shields.io/pypi/v/feedbin-tools.svg)](https://pypi.org/project/feedbin-tools/)
[![Changelog](https://img.shields.io/github/v/release/crossjam/feedbin-tools?include_prereleases&label=changelog)](https://github.com/crossjam/feedbin-tools/releases)
<!--[![PyPI](https://img.shields.io/pypi/v/feedbin-tools.svg)](https://pypi.org/project/feedbin-tools/) --->
<!--- -->
<!--[![Changelog](https://img.shields.io/github/v/release/crossjam/feedbin-tools?include_prereleases&label=changelog)](https://github.com/crossjam/feedbin-tools/releases) --->
[![Tests](https://github.com/crossjam/feedbin-tools/workflows/Test/badge.svg)](https://github.com/crossjam/feedbin-tools/actions?query=workflow%3ATest)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/crossjam/feedbin-tools/blob/master/LICENSE)

Tools for Working with the Feedbin API
A command line toolkit for working with the [Feedbin HTTP API](https://github.com/feedbin/feedbin-api/)

## Installation

Install this tool using `pip`:
Install this tool using `pip`. Even better use
[`pipx`](https://pypa.github.io/pipx/) which should work perfectly
fine with the URL below:

pip install feedbin-tools
pip install git+https://github.com/crossjam/feedbin-tools.git

## Usage

Expand All @@ -23,11 +26,38 @@ You can also use:

python -m feedbin_tools --help

```
Usage: feedbin-tools [OPTIONS] COMMAND [ARGS]...
A command line toolkit for working with the Feedbin HTTP API
https://github.com/feedbin/feedbin-api/
Due to the use of the requests library for HTTP, .netrc is honored which is
another means of setting the HTTP Basic Auth user and password for the
feedbin endpoints
Options:
--version Show the version and exit.
--log-format TEXT Python logging format string
--log-level TEXT Python logging level [default: ERROR]
--log-file FILE Python log output file
--user TEXT feedbin user, also via FEEDBIN_USER envvar
--password TEXT feedbin password, also via FEEDBIN_PASSWORD envvar
--help Show this message and exit.
Commands:
entries Fetch entries for the authed feedbin user and emit as JSON
feed Fetch entries for feedbin feed FEED_ID and emit as JSON
starred Fetch feedbin starred entries for the authed feedbin...
subscriptions Fetch feedbin subscriptions for the authed feedbin user...
```

## Development

To contribute to this tool, first checkout the code. Then create a new virtual environment:
To develop this module further, first checkout the code. Then create a new virtual environment:

cd feedbin-tools
git clone +https://github.com/crossjam/feedbin-tools.git
cd feedbin-tools
python -m venv venv
source venv/bin/activate

Expand Down
122 changes: 113 additions & 9 deletions feedbin_tools/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@


import click
from dateparser import parse as dtparse

from .logconfig import DEFAULT_LOG_FORMAT, logging_config

Expand All @@ -28,6 +29,10 @@ def batched(iterable, n):
yield batch


def json_bool(val):
return str(bool(val)).lower()


def paginated_request(request_url, auth=None, params={}):
logging.debug("requesting with potential pagination: %s", request_url)

Expand Down Expand Up @@ -102,11 +107,28 @@ def paginated_request(request_url, auth=None, params={}):
type=click.Path(dir_okay=False, writable=True, resolve_path=True),
default=None,
)
@click.option("--user", type=click.STRING, envvar="FEEDBIN_USER")
@click.option("--password", type=click.STRING, envvar="FEEDBIN_PASSWORD")
@click.option(
"--user",
type=click.STRING,
envvar="FEEDBIN_USER",
help="feedbin user, also via FEEDBIN_USER envvar",
)
@click.option(
"--password",
type=click.STRING,
envvar="FEEDBIN_PASSWORD",
help="feedbin password, also via FEEDBIN_PASSWORD envvar",
)
@click.pass_context
def cli(ctx, log_format, log_level, log_file, user=None, password=None):
"Tools for Working with the Feedbin API"
"""A command line toolkit for working with the Feedbin HTTP API
https://github.com/feedbin/feedbin-api/
Due to the use of the requests library for HTTP, .netrc is honored
which is another means of setting the HTTP Basic Auth user and
password for the feedbin endpoints
"""

ctx.ensure_object(dict)
ctx.obj["feedbin_password"] = password
Expand All @@ -128,20 +150,34 @@ def auth_from_context(ctx):


@cli.command(name="subscriptions")
@click.option("--extended/--no-extended", default=False)
@click.pass_context
def subscriptions(ctx):
def subscriptions(ctx, extended):
"""
Fetch feedbin subscriptions and emit as JSON
Fetch feedbin subscriptions for the authed feedbin user and emit as JSON
"""

session = requests_cache.CachedSession()
auth = auth_from_context(ctx)

resp = session.get("https://api.feedbin.com/v2/subscriptions.json", auth=auth)
params = {"mode": "extended"} if extended else {}

logging.info("Request params: %s", params)
resp = session.get(
"https://api.feedbin.com/v2/subscriptions.json", auth=auth, params=params
)
resp.raise_for_status()

for item in resp.json():
sys.stdout.write(json.dumps(item) + "\n")
try:
for item in resp.json():
sys.stdout.write(json.dumps(item) + "\n")
except IOError:
logging.info("Output endpoint closed, exiting")

try:
sys.stdout.close()
except IOError:
pass


@cli.command(name="starred")
Expand All @@ -151,7 +187,9 @@ def subscriptions(ctx):
@click.option("--limit", type=click.INT, default=-1)
@click.pass_context
def starred(ctx, chunk_size, extended, ids, limit):
"Command description goes here"
"""
Fetch feedbin starred entries for the authed feedbin user and emit as JSON
"""

chunk_size = min(chunk_size, 100)
logging.info("Chunk size: %d", chunk_size)
Expand Down Expand Up @@ -242,3 +280,69 @@ def feed(ctx, feed_id, extended, limit):

sys.stdout.write(json.dumps(item) + "\n")
total_emitted += 1


@cli.command(name="entries")
@click.option("--read/--unread", default=False)
@click.option("--starred/--no-starred", default=False)
@click.option("--extended/--no-extended", default=False)
@click.option("--limit", type=click.INT, default=-1)
@click.option("-b", "--per-page", type=click.INT, default=75)
@click.option("--since", type=click.STRING, default="")
@click.option("--include-original/--no-include-original", default=False)
@click.option("--include-enclosure/--no-include-enclosure", default=False)
@click.option("--include-content-diff/--no-include-content-diff", default=False)
@click.pass_context
def entries(
ctx,
read,
starred,
extended,
limit,
per_page,
since,
include_original,
include_enclosure,
include_content_diff,
):
"""
Fetch entries for the authed feedbin user and emit as JSON
"""

auth = auth_from_context(ctx)

entries_url = f"https://api.feedbin.com/v2/entries.json"

params = {"mode": "extended"} if extended else {}
params["read"] = json_bool(read) if not starred else "starred"
params["starred"] = json_bool(starred)
params["per_page"] = per_page
params["include_original"] = json_bool(include_original)
params["include_enclosure"] = json_bool(include_enclosure)
params["include_content_diff"] = json_bool(include_content_diff)

if since:
dt = dtparse(
since, settings={"TO_TIMEZONE": "UTC", "RETURN_AS_TIMEZONE_AWARE": True}
)
if not dt:
logging.error("Failed to parse since %s as a date time", since)
raise ValueError(f"Unrecognized dateparser input string: '{since}'")
else:
logging.info("Retrieving entries after: %s", dt.isoformat())

logging.info("Request params: %s", params)

total_emitted = 0
for item in paginated_request(entries_url, auth=auth, params=params):
if 0 <= limit <= total_emitted:
logging.info("Reached limit of %d, completed", limit)
return

current_utc = datetime.now(timezone.utc)
iso_format = current_utc.isoformat()

item["x-retrieved-at"] = iso_format
item["x-read-status"] = params["read"]
sys.stdout.write(json.dumps(item) + "\n")
total_emitted += 1
5 changes: 5 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ zip_safe = True
include_package_data = True
install_requires =
click
dateparser
pyrate-limiter
requests
requests-cache
stamina
tenacity
keyring

[options.extras_require]
test = pytest
requests-mock


[options.entry_points]
Expand Down

0 comments on commit 9abbc6f

Please sign in to comment.