Refactoring PRAW generators.
This commit is contained in:
135
rtv/content_generators.py
Normal file
135
rtv/content_generators.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
def clean(unicode_string):
|
||||||
|
"""
|
||||||
|
Convert unicode string into ascii-safe characters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return unicode_string.encode('ascii', 'replace').replace('\\', '')
|
||||||
|
|
||||||
|
|
||||||
|
def humanize_timestamp(utc_timestamp, verbose=False):
|
||||||
|
"""
|
||||||
|
Convert a utc timestamp into a human readable relative-time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
timedelta = datetime.utcnow() - datetime.utcfromtimestamp(utc_timestamp)
|
||||||
|
|
||||||
|
seconds = int(timedelta.total_seconds())
|
||||||
|
if seconds < 60:
|
||||||
|
return 'moments ago' if verbose else '0min'
|
||||||
|
minutes = seconds / 60
|
||||||
|
if minutes < 60:
|
||||||
|
return ('%d minutes ago' % minutes) if verbose else ('%dmin' % minutes)
|
||||||
|
hours = minutes / 60
|
||||||
|
if hours < 24:
|
||||||
|
return ('%d hours ago' % hours) if verbose else ('%dhr' % hours)
|
||||||
|
days = hours / 24
|
||||||
|
if days < 30:
|
||||||
|
return ('%d days ago' % days) if verbose else ('%dday' % days)
|
||||||
|
months = days / 30.4
|
||||||
|
if months < 12:
|
||||||
|
return ('%d months ago' % months) if verbose else ('%dmonth' % months)
|
||||||
|
years = months / 12
|
||||||
|
return ('%d years ago' % years) if verbose else ('%dyr' % years)
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionGenerator(object):
|
||||||
|
"""
|
||||||
|
Facilitates navigating through the comments in a PRAW submission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, submission):
|
||||||
|
|
||||||
|
self.submission = submission
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def flatten_comments(submission):
|
||||||
|
"""
|
||||||
|
Flatten a PRAW comment tree while preserving the nested level of each
|
||||||
|
comment via the `nested_level` attribute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
stack = submission[:]
|
||||||
|
for item in stack:
|
||||||
|
item.nested_level = 0
|
||||||
|
|
||||||
|
retval = []
|
||||||
|
while stack:
|
||||||
|
item = stack.pop(0)
|
||||||
|
nested = getattr(item, 'replies', None)
|
||||||
|
if nested:
|
||||||
|
for n in nested:
|
||||||
|
n.nested_level = item.nested_level + 1
|
||||||
|
stack[0:0] = nested
|
||||||
|
retval.append(item)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
class SubredditGenerator(object):
|
||||||
|
"""
|
||||||
|
Grabs a subreddit from PRAW lazily and store in an internal list for repeat
|
||||||
|
access.
|
||||||
|
|
||||||
|
params:
|
||||||
|
session (praw.Reddit): Active reddit connection
|
||||||
|
subreddit (str): Subreddit to connect to, None defaults to front page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, reddit_session, subreddit=None):
|
||||||
|
|
||||||
|
self.r = reddit_session
|
||||||
|
self.r.config.decode_html_entities = True
|
||||||
|
|
||||||
|
if subreddit is None:
|
||||||
|
self._submissions = self.r.get_front_page(limit=None)
|
||||||
|
else:
|
||||||
|
self._submissions = self.r.get_subreddit(subreddit, limit=None)
|
||||||
|
|
||||||
|
self._submission_data = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def strip_praw_submission(sub):
|
||||||
|
"""
|
||||||
|
Parse through a submission and return a dict with data ready to be
|
||||||
|
displayed through the terminal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
is_selfpost = lambda s: s.startswith('http://www.reddit.com/r/')
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
data['title'] = clean(sub.title)
|
||||||
|
data['created'] = humanize_timestamp(sub.created_utc)
|
||||||
|
data['comments'] = '{} comments'.format(sub.num_comments)
|
||||||
|
data['score'] = '{} pts'.format(sub.score)
|
||||||
|
data['author'] = clean(sub.author.name)
|
||||||
|
data['subreddit'] = clean(sub.subreddit.url)
|
||||||
|
data['url'] = ('(selfpost)' if is_selfpost(sub.url) else clean(sub.url))
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get(self, index, n_cols):
|
||||||
|
"""
|
||||||
|
Grab the `i`th submission, with the title field formatted to fit inside
|
||||||
|
of a window of width `n`
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert(index >= 0)
|
||||||
|
|
||||||
|
while index >= len(self._submission_data):
|
||||||
|
data = self._strip_praw_submission(self._submissions.next())
|
||||||
|
self._submission_data.append(data)
|
||||||
|
|
||||||
|
# Modifies the original original dict, faster than copying
|
||||||
|
data = self._submission_data[index]
|
||||||
|
data['split_title'] = textwrap.wrap(data['title'], width=n_cols)
|
||||||
|
data['n_rows'] = len(data['split_title']) + 3
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def iterate(self, index, n_cols):
|
||||||
|
|
||||||
|
while True:
|
||||||
|
yield self.get(index, n_cols)
|
||||||
|
index += 1
|
||||||
@@ -2,22 +2,44 @@ import praw
|
|||||||
import textwrap
|
import textwrap
|
||||||
import curses
|
import curses
|
||||||
|
|
||||||
from utils import humanize_timestamp, flatten_tree, clean
|
from content_generators import SubredditGenerator
|
||||||
|
|
||||||
def strip_submission(sub):
|
class SubredditViewer(object):
|
||||||
"Grab info from a PRAW submission and prep for display."
|
|
||||||
|
|
||||||
out = {}
|
def __init__(self, stdscr):
|
||||||
out['title'] = clean(sub.title)
|
self.stdscr = stdscr
|
||||||
out['created'] = humanize_timestamp(sub.created_utc, long=False)
|
|
||||||
out['comments'] = '{} comments'.format(sub.num_comments)
|
|
||||||
out['score'] = '{} pts'.format(sub.score)
|
|
||||||
out['author'] = clean(sub.author.name)
|
|
||||||
out['subreddit'] = clean(sub.subreddit.url)
|
|
||||||
out['url'] = ('(selfpost)' if sub.url.startswith('http://www.reddit.com/r/')
|
|
||||||
else clean(sub.url))
|
|
||||||
|
|
||||||
return out
|
def loop(self):
|
||||||
|
|
||||||
|
while True:
|
||||||
|
cmd = self.stdscr.getch()
|
||||||
|
|
||||||
|
# Move cursor up one submission
|
||||||
|
if cmd == curses.KEY_UP:
|
||||||
|
self.move_cursor(-1)
|
||||||
|
|
||||||
|
# Move cursor down one submission
|
||||||
|
elif cmd == curses.KEY_DOWN:
|
||||||
|
self.move_cursor(1)
|
||||||
|
|
||||||
|
# View submission
|
||||||
|
elif cmd in (curses.KEY_RIGHT, ord(' ')):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Enter edit mode to change subreddit
|
||||||
|
elif cmd == ord('/'):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Refresh page
|
||||||
|
elif cmd in (curses.KEY_F5, ord('r')):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Quit
|
||||||
|
elif cmd == ord('q'):
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
curses.beep()
|
||||||
|
|
||||||
def draw_submission(win, data, top_down=True):
|
def draw_submission(win, data, top_down=True):
|
||||||
"Draw a submission in the given window."
|
"Draw a submission in the given window."
|
||||||
@@ -49,46 +71,26 @@ def draw_submission(win, data, top_down=True):
|
|||||||
win.addnstr(row, 1, '{author} {subreddit}'.format(**data), n_cols)
|
win.addnstr(row, 1, '{author} {subreddit}'.format(**data), n_cols)
|
||||||
|
|
||||||
|
|
||||||
class SubmissionGenerator(object):
|
def focus_submission(win):
|
||||||
"""
|
"Add a vertical column of reversed background on left side of the window"
|
||||||
Grab submissions from PRAW lazily and store in an internal list for repeat
|
|
||||||
access.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
n_rows, n_cols = win.getmaxyx()
|
||||||
|
for row in xrange(n_rows):
|
||||||
|
win.chgat(row, 0, 1, curses.A_REVERSE)
|
||||||
|
|
||||||
self.r = praw.Reddit(user_agent='reddit terminal viewer (rtv) v0.0')
|
|
||||||
self.r.config.decode_html_entities = True
|
|
||||||
|
|
||||||
self._submissions = self.r.get_front_page(limit=None)
|
def unfocus_submission(win):
|
||||||
self._submission_data = []
|
"Clear the vertical column"
|
||||||
|
|
||||||
def get(self, index, n_cols):
|
|
||||||
|
|
||||||
assert(index >= 0)
|
|
||||||
|
|
||||||
while index >= len(self._submission_data):
|
|
||||||
data = strip_submission(self._submissions.next())
|
|
||||||
self._submission_data.append(data)
|
|
||||||
|
|
||||||
# Modifies the original original dict, faster than copying
|
|
||||||
out = self._submission_data[index]
|
|
||||||
out['split_title'] = textwrap.wrap(out['title'], width=n_cols)
|
|
||||||
out['n_rows'] = len(out['split_title']) + 3
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
def iterate(self, index, n_cols):
|
|
||||||
|
|
||||||
while True:
|
|
||||||
yield self.get(index, n_cols)
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
|
n_rows, n_cols = win.getmaxyx()
|
||||||
|
for row in xrange(n_rows):
|
||||||
|
win.chgat(row, 0, 1, curses.A_NORMAL)
|
||||||
|
|
||||||
|
|
||||||
def draw_subreddit(stdscr):
|
def draw_subreddit(stdscr):
|
||||||
|
|
||||||
generator = SubmissionGenerator()
|
r = praw.Reddit(user_agent='reddit terminal viewer (rtv) v0.0')
|
||||||
|
generator = SubredditGenerator(r)
|
||||||
|
|
||||||
main_window = stdscr.derwin(1, 0)
|
main_window = stdscr.derwin(1, 0)
|
||||||
main_window.erase()
|
main_window.erase()
|
||||||
@@ -99,6 +101,7 @@ def draw_subreddit(stdscr):
|
|||||||
n_rows = min(max_rows-current_row, data['n_rows'])
|
n_rows = min(max_rows-current_row, data['n_rows'])
|
||||||
sub_window = main_window.derwin(n_rows, max_cols, current_row, 0)
|
sub_window = main_window.derwin(n_rows, max_cols, current_row, 0)
|
||||||
draw_submission(sub_window, data)
|
draw_submission(sub_window, data)
|
||||||
|
focus_submission(sub_window)
|
||||||
sub_window.refresh() # Debugging
|
sub_window.refresh() # Debugging
|
||||||
current_row += n_rows + 1
|
current_row += n_rows + 1
|
||||||
if current_row >= max_rows:
|
if current_row >= max_rows:
|
||||||
|
|||||||
50
rtv/utils.py
50
rtv/utils.py
@@ -1,4 +1,3 @@
|
|||||||
from datetime import datetime, timedelta
|
|
||||||
import curses
|
import curses
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
@@ -52,52 +51,3 @@ def curses_session():
|
|||||||
curses.nocbreak()
|
curses.nocbreak()
|
||||||
curses.endwin()
|
curses.endwin()
|
||||||
|
|
||||||
def humanize_timestamp(utc_timestamp, long=True):
|
|
||||||
"""
|
|
||||||
Convert a utc timestamp into a human readable time relative to now.
|
|
||||||
"""
|
|
||||||
timedelta = datetime.utcnow() - datetime.utcfromtimestamp(utc_timestamp)
|
|
||||||
seconds = int(timedelta.total_seconds())
|
|
||||||
if seconds < 60:
|
|
||||||
return 'moments ago' if long else '0min'
|
|
||||||
minutes = seconds / 60
|
|
||||||
if minutes < 60:
|
|
||||||
return '%d' % minutes + (' minutes ago' if long else 'min')
|
|
||||||
hours = minutes / 60
|
|
||||||
if hours < 24:
|
|
||||||
return '%d' % hours + (' hours ago' if long else 'hr')
|
|
||||||
days = hours / 24
|
|
||||||
if days < 30:
|
|
||||||
return '%d' % days + (' days ago' if long else 'day')
|
|
||||||
months = days / 30.4
|
|
||||||
if months < 12:
|
|
||||||
return '%d' % months + (' months ago' if long else 'month')
|
|
||||||
years = months / 12
|
|
||||||
return '%d' % years + (' years ago' if long else 'yr')
|
|
||||||
|
|
||||||
|
|
||||||
def flatten_tree(tree):
|
|
||||||
"""
|
|
||||||
Flatten a PRAW comment tree while preserving the nested level of each
|
|
||||||
comment via the `nested_level` attribute.
|
|
||||||
"""
|
|
||||||
|
|
||||||
stack = tree[:]
|
|
||||||
for item in stack:
|
|
||||||
item.nested_level = 0
|
|
||||||
|
|
||||||
retval = []
|
|
||||||
while stack:
|
|
||||||
item = stack.pop(0)
|
|
||||||
nested = getattr(item, 'replies', None)
|
|
||||||
if nested:
|
|
||||||
for n in nested:
|
|
||||||
n.nested_level = item.nested_level + 1
|
|
||||||
stack[0:0] = nested
|
|
||||||
retval.append(item)
|
|
||||||
return retval
|
|
||||||
|
|
||||||
|
|
||||||
def clean(unicode_string):
|
|
||||||
"Convert unicode string into ascii-safe characters."
|
|
||||||
return unicode_string.encode('ascii', 'replace').replace('\\', '')
|
|
||||||
Reference in New Issue
Block a user