diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 503272c..38d268e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 + rev: v2.5.0 hooks: - id: check-added-large-files - id: check-ast diff --git a/README.md b/README.md index e333aa4..8605f50 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ Hot deals on the command line. [![Build Status](https://travis-ci.org/davegallant/rfd.svg?branch=master)](https://travis-ci.org/davegallant/rfd) [![PyPI version](https://badge.fury.io/py/rfd.svg)](https://badge.fury.io/py/rfd) [![Dependabot](https://badgen.net/badge/Dependabot/enabled/green?icon=dependabot)](https://dependabot.com/) -[![Total alerts](https://img.shields.io/lgtm/alerts/g/davegallant/rfd.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/davegallant/rfd/alerts/) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/davegallant/rfd.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/davegallant/rfd/context:python) @@ -37,29 +36,46 @@ Commands: ## Examples -### view hot deals -```shell -rfd threads +### View Hot Deals +```console +$ rfd threads ``` -### search for pizza -```shell -rfd search 'pizza' +### View and Sort Hot Deals + +```console +$ rfd threads --sort-by score ``` -## Tab Completion +```console +$ rfd threads --sort-by total_views --limit 40 +``` -To enable: +### Simple Search +```console +$ rfd search 'pizza' +``` + +### RegEx Search + +Regular expressions can be used for search. + +```console +$ rfd search '(coffee|starbucks)' --num-pages 100 +``` + +## Shell Completion + +Completion can be enabled if using `bash` or `zsh`. ### bash -```bash -echo 'eval "$(_RFD_COMPLETE=source rfd)"' >> ~/.profile +```console +$ echo 'eval "$(_RFD_COMPLETE=source rfd)"' >> ~/.profile ``` ### zsh - -```zsh -echo 'eval "$(_RFD_COMPLETE=source_zsh rfd)"' >> ~/.zshrc +```console +$ echo 'eval "$(_RFD_COMPLETE=source_zsh rfd)"' >> ~/.zshrc ``` diff --git a/rfd/VERSION b/rfd/VERSION index 449d7e7..1d0ba9e 100644 --- a/rfd/VERSION +++ b/rfd/VERSION @@ -1 +1 @@ -0.3.6 +0.4.0 diff --git a/rfd/__init__.py b/rfd/__init__.py index dd67195..d058ab2 100644 --- a/rfd/__init__.py +++ b/rfd/__init__.py @@ -6,3 +6,5 @@ __title__ = "RFD CLI" __author__ = "Dave Gallant" __license__ = "Apache 2.0" __copyright__ = "(c) 2018 Dave Gallant" + +API_BASE_URL = "https://forums.redflagdeals.com" diff --git a/rfd/api.py b/rfd/api.py index 329aca4..817400e 100644 --- a/rfd/api.py +++ b/rfd/api.py @@ -6,11 +6,10 @@ except ImportError: JSONDecodeError = ValueError import logging import requests -from .constants import API_BASE_URL -from .format import strip_html, is_valid_url -from .models import Post +from . import API_BASE_URL +from .posts import Post from .scores import calculate_score -from .utils import is_int +from .utils import is_int, strip_html, is_valid_url def extract_post_id(url): diff --git a/rfd/cli.py b/rfd/cli.py index 0e65b77..26ef501 100644 --- a/rfd/cli.py +++ b/rfd/cli.py @@ -7,8 +7,7 @@ import sys import click from colorama import init, Fore, Style from .api import get_threads, get_posts -from .search import search_threads -from .parsing import parse_threads +from .threads import parse_threads, search_threads, sort_threads from .__version__ import version as current_version init() @@ -20,7 +19,7 @@ logging.getLogger().addHandler(logging.StreamHandler()) def get_version(): - return "rfd " + current_version + return "rfd v" + current_version def get_terminal_width(): @@ -39,7 +38,7 @@ def get_vote_color(score): def print_version(ctx, value): if not value or ctx.resilient_parsing: return - click.echo(get_version()) + click.echo(get_version(), nl=False) ctx.exit() @@ -115,9 +114,10 @@ def posts(post_id): @cli.command(short_help="Displays threads in the forum. Defaults to hot deals.") -@click.option("--limit", default=10, help="Number of topics.") @click.option("--forum-id", default=9, help="The forum id number") -def threads(limit, forum_id): +@click.option("--limit", default=10, help="Number of threads.") +@click.option("--sort-by", default=None, help="Sort threads by") +def threads(limit, forum_id, sort_by): """Display threads in the specified forum id. Defaults to 9 (hot deals). Popular forum ids: @@ -134,7 +134,9 @@ def threads(limit, forum_id): 74 \t shopping discussion 88 \t cell phones """ - _threads = parse_threads(get_threads(forum_id, limit), limit) + _threads = sort_threads( + parse_threads(get_threads(forum_id, limit), limit), sort_by=sort_by + ) for count, thread in enumerate(_threads, 1): display_thread(click, thread, count) diff --git a/rfd/constants.py b/rfd/constants.py deleted file mode 100644 index 2186548..0000000 --- a/rfd/constants.py +++ /dev/null @@ -1 +0,0 @@ -API_BASE_URL = "https://forums.redflagdeals.com" diff --git a/rfd/format.py b/rfd/format.py deleted file mode 100644 index a5113cf..0000000 --- a/rfd/format.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Formatting utils""" - -try: - from urllib.parse import urlparse # python 3 -except ImportError: - from urlparse import urlparse # python 2 -from bs4 import BeautifulSoup - - -def strip_html(text): - return BeautifulSoup(text, "html.parser").get_text() - - -def is_valid_url(url): - result = urlparse(url) - return all([result.scheme, result.netloc, result.path]) diff --git a/rfd/models.py b/rfd/models.py deleted file mode 100644 index 5d68a66..0000000 --- a/rfd/models.py +++ /dev/null @@ -1,18 +0,0 @@ -# pylint: disable=old-style-class -class Thread: - def __init__(self, title, dealer_name, score, url, total_views): - self.dealer_name = dealer_name - self.score = score - self.title = title - self.url = url - self.total_views = total_views - - def __repr__(self): - return "Thread(%s)" % self.title - - -class Post: - def __init__(self, body, score, user): - self.body = body - self.score = score - self.user = user diff --git a/rfd/parsing.py b/rfd/parsing.py deleted file mode 100644 index c5d85d7..0000000 --- a/rfd/parsing.py +++ /dev/null @@ -1,42 +0,0 @@ -from .constants import API_BASE_URL -from .scores import calculate_score -from .models import Thread - - -def build_web_path(slug): - return "{}{}".format(API_BASE_URL, slug) - - -def get_dealer(topic): - dealer = None - if topic.get("offer"): - dealer = topic.get("offer").get("dealer_name") - return dealer - - -def parse_threads(threads, limit): - """parse topics list api response into digestible list. - - Arguments: - threads {dict} -- topics response from rfd api - limit {int} -- limit number of threads returned - - Returns: - list(dict) -- digestible list of threads - """ - parsed_threads = [] - if threads is None: - return [] - for count, topic in enumerate(threads.get("topics"), start=1): - if count > limit: - break - parsed_threads.append( - Thread( - title=topic.get("title"), - dealer_name=get_dealer(topic), - score=calculate_score(topic), - url=build_web_path(topic.get("web_path")), - total_views=topic.get("total_views"), - ) - ) - return parsed_threads diff --git a/rfd/posts.py b/rfd/posts.py new file mode 100644 index 0000000..c00d346 --- /dev/null +++ b/rfd/posts.py @@ -0,0 +1,8 @@ +# pylint: disable=old-style-class + + +class Post: + def __init__(self, body, score, user): + self.body = body + self.score = score + self.user = user diff --git a/rfd/search.py b/rfd/search.py deleted file mode 100644 index 507f0c1..0000000 --- a/rfd/search.py +++ /dev/null @@ -1,14 +0,0 @@ -import re - - -def search_threads(threads, regex): - """Match deal title and dealer names with regex specified.""" - - regexp = re.compile(str(regex).lower()) - - for deal in threads: - - if regexp.search(deal.title.lower()) or ( - deal.dealer_name and regexp.search(deal.dealer_name.lower()) - ): - yield deal diff --git a/rfd/threads.py b/rfd/threads.py new file mode 100644 index 0000000..58f2158 --- /dev/null +++ b/rfd/threads.py @@ -0,0 +1,76 @@ +import re +from . import API_BASE_URL +from .scores import calculate_score + +# pylint: disable=old-style-class +class Thread: + def __init__(self, title, dealer_name, score, url, total_views): + self.dealer_name = dealer_name + self.score = score + self.title = title + self.url = url + self.total_views = total_views + + def __repr__(self): + return "Thread(%s)" % self.title + + +def build_web_path(slug): + return "{}{}".format(API_BASE_URL, slug) + + +def get_dealer(topic): + dealer = None + if topic.get("offer"): + dealer = topic.get("offer").get("dealer_name") + return dealer + + +def parse_threads(threads, limit): + """Parse topics list api response into digestible list. + + Arguments: + threads {dict} -- topics response from rfd api + limit {int} -- limit number of threads returned + + Returns: + list(dict) -- digestible list of threads + """ + parsed_threads = [] + if threads is None: + return [] + for count, topic in enumerate(threads.get("topics"), start=1): + if count > limit: + break + parsed_threads.append( + Thread( + title=topic.get("title"), + dealer_name=get_dealer(topic), + score=calculate_score(topic), + url=build_web_path(topic.get("web_path")), + total_views=topic.get("total_views"), + ) + ) + return parsed_threads + + +def sort_threads(threads, sort_by): + """Sort threads by an attribute""" + if sort_by is None: + return threads + assert sort_by in ["total_views", "score", "title"] + threads = sorted(threads, key=lambda x: getattr(x, sort_by), reverse=True) + return threads + + +def search_threads(threads, regex): + """Match deal title and dealer names with regex specified.""" + + regexp = re.compile(str(regex).lower()) + + for deal in threads: + + if regexp.search(deal.title.lower()) or ( + deal.dealer_name and regexp.search(deal.dealer_name.lower()) + ): + yield deal diff --git a/rfd/utils.py b/rfd/utils.py index 935606b..5805300 100644 --- a/rfd/utils.py +++ b/rfd/utils.py @@ -1,4 +1,18 @@ """This module provides utility functions that are used within rfd""" +try: + from urllib.parse import urlparse # python 2 +except ImportError: + from urlparse import urlparse # python 1 +from bs4 import BeautifulSoup + + +def strip_html(text): + return BeautifulSoup(text, "html.parser").get_text() + + +def is_valid_url(url): + result = urlparse(url) + return all([result.scheme, result.netloc, result.path]) def is_int(number): diff --git a/tests/test_api.py b/tests/test_api.py index b5dbe38..ad86437 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,5 +1,5 @@ from rfd.api import extract_post_id -from rfd.parsing import build_web_path, parse_threads +from rfd.threads import build_web_path, parse_threads def test_build_web_path(): diff --git a/tox.ini b/tox.ini index 81d8bd6..a72cb78 100644 --- a/tox.ini +++ b/tox.ini @@ -9,3 +9,8 @@ envlist = py{27, passenv = SSH_AUTH_SOCK commands = make ci + rfd --version + rfd threads + rfd threads --sort-by score + rfd search 'pizza' + rfd search '(coffee|starbucks)'