This script is meant to do the tedious work involved in a monthly event I run on a subreddit. It does a search for all posts relevant to the event since the last posting, and creates the bulk of the next month's post.
I would most like criticism at an organizational level. My functions ramble together, and it's hard to keep track of what I have, so I'd like suggestions for a better to do that.
In the problem domain, the name Piece
is not as horribly vague as it seems. Of course, if you're aware of this and still think it's an awful name, I welcome your thoughts.
import configparser
import datetime
import logging
import re
import pickle
from typing import Optional
import praw
import praw.models
DELIMITER = '---' # type: str
REDDIT = None
JAM_MAINTAINER = 'G01denW01f11'
def init_reddit(config_pathname: str) -> praw.Reddit:
"""Create global Reddit object from config file"""
config = configparser.ConfigParser()
config.read(config_pathname)
return praw.Reddit(client_id=config['RedditParams']['client_id'],
client_secret=config['RedditParams']['client_secret'],
user_agent=config['RedditParams']['user_agent'])
def get_reddit() -> praw.Reddit:
"""Get the global Reddit object. Create it if it hasn't been created"""
global REDDIT
if not REDDIT:
REDDIT = init_reddit('config.ini')
return REDDIT
class Piece(object):
"""A piece to be listed in the piano jam"""
def __init__(self, composer: str = None, title: str = None, video_url: str = None, score_url: str = None,
category: str = None):
self.composer = composer # type: str
self.title = title # type: str
self.video_url = video_url # type: str
self.score_url = score_url # type: str
self.category = category # type: str
def __eq__(self, other: 'Piece') -> bool:
return self.composer == other.composer and self.title == other.title
def __ne__(self, other: 'Piece') -> bool:
return not self == other
def __str__(self) -> str:
return '{}: [{}]({}) | [Sheet Music]({})'.format(self.composer, self.title, self.video_url.replace(')', '\)'),
self.score_url.replace(')', '\)'))
class Submission(object):
"""A submission to the month's Jam"""
def __init__(self, username: str = None, url: str = None, title: str = None, piece: Piece = None):
self.username = username # type: str
self.url = url # type: str
self.title = title # type: str
self.piece = piece # type: Piece
def __eq__(self, other: 'Submission') -> bool:
return self.username == other.username and self.piece == other.piece
def __ne__(self, other: 'Submission') -> bool:
return not self == other
def __str__(self) -> str:
return '{}\'s {} by [/u/{}]({})'.format(self.piece.composer, self.piece.title, self.username, self.url)
def set_piece(self, pieces: [Piece]) -> None:
"""
From a list of valid pieces, set the one that matches
:param pieces: A list of pieces to choose from
"""
self.piece = find_piece_matching_title(pieces, self.title)
if not self.piece:
logging.warning('Could not find piece for {} | {}'.format(self.title, self.url))
def find_piece_matching_title(pieces: [Piece], title: str) -> Optional[Piece]:
"""
Use a simple heuristic to tell which piece a submission is from the title
:param pieces: Pieces to choose from
:param title: Submission title
:return: Appropriate piece, if any
"""
for piece in pieces:
if biggest_word_in_line(piece.title).lower() in title.lower():
return piece
return None
def format_title(section_title: str) -> str:
"""
Apply proper formatting to the title of a section
:param section_title: The title of a section to be formatted
:return: Formatted title
"""
return '**{}**'.format(section_title)
class Jam(object):
"""A Piano Jam posting"""
CATEGORIES = ['Jazz', 'Classical', 'Ragtime', 'Video Game / Anime / Film'] # type: [str]
def __init__(self, outline_pathname: str = 'jam_outline.txt'):
"""
Create a Piano Jam instance from a given outline file
:param outline_pathname: pathname to file with default jam contents
"""
self.filename = '' # type: str
self.submissions = [] # type: [Submission]
self.pieces = [] # type: [Piece]
with open(outline_pathname, 'r') as f:
self.text = f.read()
def __str__(self):
submissions_str = ''
for submission in self.submissions:
submissions_str += str(submission) + '\n\n'
pieces_str = ''
for piece in self.pieces:
pieces_str += str(piece) + '\n\n'
return self.text.format(submissions_str, pieces_str)
def add_submission(self, submission: Submission):
"""
Add a submission to the Jam. Multiple submissions do not get added
:param submission: Submission to the Piano Jam
:return: None
"""
for prior_submission in self.submissions:
if submission.username == prior_submission.username and submission.piece == submission.piece:
if submission.url != prior_submission.url:
logging.warning('User {0} attempted to submit a piece multiple times'.format(submission.username))
return
self.submissions.append(submission)
def add_piece(self, piece: Piece):
if piece not in self.pieces:
self.pieces.append(piece)
def save(self, filename: str='') -> None:
if filename:
self.filename = filename
if not self.filename:
raise ValueError('No filename to save to!')
with open(self.filename, 'wb') as f:
pickle.dump(self, f)
@classmethod
def load(cls, filename: str) -> 'Jam':
with open(filename, 'rb') as f:
jam = pickle.load(f) # type: Jam
if type(jam) != Jam:
raise TypeError('Tried to load a Jam. Got {}'.format(type(jam)))
assert jam.filename == filename
return jam
def parse_piece(piece_text: str) -> Piece:
"""
Construct a Piece from its string representation.
Expected format: Composer: [Title](url) | [Sheet Music](sheetUrl)
:param piece_text: Line from Piano Jam specifying a Piece to learn
"""
piece = Piece()
piece.composer = piece_text[:piece_text.index(':')]
piece.title = re.findall(re.compile('\[(.*?)\]'), piece_text)[0] # type: str
urls = re.findall(re.compile('\((.*?)\)'), piece_text)
piece.video_url = urls[0] # type: str
piece.score_url = urls[1] # type: str
return piece
def parse_pieces(section_text: str) -> [Piece]:
"""Parse all the pieces in a given section"""
pieces = section_text.split('\n')[1:] # First line is the category; discard
return (parse_piece(piece_text) for piece_text in pieces if piece_text.strip() != '')
def get_pieces_from_jam(jam_text: str) -> [Piece]:
"""
Parse all the pieces from a Jam, given the contents of a post
:param jam_text: The contents of a Piano Jam posting
:return: List of pieces to be used for the Jam
"""
sections = jam_text.split(DELIMITER)
sections = (section.strip() for section in sections)
filtered_sections = []
for section in sections:
section = section.strip()
for category in Jam.CATEGORIES:
category = format_title(category)
if section.startswith(category):
filtered_sections.append(section)
break
pieces = []
for section in filtered_sections:
pieces.extend(parse_pieces(section))
return pieces
def get_selections_from_url(url: str) -> [Piece]:
"""
Parse all the pieces from a jam, given its url
:param url: URL to a Piano Jam post
:return: List of pieces to be used for the Jam
"""
try:
post = praw.models.Submission(get_reddit(), url=url)
except KeyError:
raise KeyError('Could not recognize url {0}'.format(url))
return get_pieces_from_jam(post.selftext)
def search_for_submissions():
"""
Search Reddit for posts with [Piano Jam] in title within past month
:return: List of urls to posts
"""
subreddit = get_reddit().subreddit('piano')
results = subreddit.search('[Piano Jam]', sort='new', time_filter='month')
return (result for result in results)
def filter_submissions(submissions: [praw.models.Submission], jam: praw.models.Submission):
return [submission for submission in submissions
if '[piano jam]' in submission.title.lower() and
datetime.datetime.fromtimestamp(submission.created) >
datetime.datetime.fromtimestamp(jam.created)]
def find_last_jam() -> praw.models.Submission:
candidates = search_for_submissions()
for candidate in candidates:
if candidate.author == JAM_MAINTAINER and '[' not in candidate.title:
return candidate
raise ValueError('Could not find last Piano Jam')
def biggest_word_in_line(line: str) -> str:
words = line.split()
length = 0
biggest_word = None
for word in words:
if len(word) > length:
length = len(word)
biggest_word = word
assert biggest_word
return biggest_word
def create_jam() -> [Submission]:
"""
Find all Piano Jam submissions since the last posting
Log a warning if there are submissions not in the previous Jam.
Create Jam from submissions and pickle it for later use.
"""
previous_jam = find_last_jam()
entries = filter_submissions(search_for_submissions(), previous_jam)
submissions = [Submission(entry.author, entry.shortlink, entry.title) for entry in entries]
pieces = get_pieces_from_jam(previous_jam.selftext)
new_jam = Jam()
for submission in submissions:
submission.set_piece(pieces)
if submission.piece:
new_jam.add_submission(submission)
new_jam.save('current_jam.txt')