Refactoring work to make this a real program

master
bspeice 2016-05-17 10:40:23 -04:00
parent 5853c86a2d
commit fcc280b96c
13 changed files with 215 additions and 174 deletions

View File

@ -13,23 +13,7 @@
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="Python 3.5.1 (C:\Users\Bradlee Speice\Anaconda3\python.exe)" project-jdk-type="Python SDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" default="false" assert-keyword="true" jdk-15="true" project-jdk-name="Python 3.5.1 (C:\Users\Bradlee Speice\Anaconda3\python.exe)" project-jdk-type="Python SDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
<component name="masterDetails">
<states>
<state key="ProjectJDKs.UI">
<settings>
<last-edited>Python 3.5.1 (C:\Users\Bradlee Speice\Anaconda3\python.exe)</last-edited>
<splitter-proportions>
<option name="proportions">
<list>
<option value="0.2" />
</list>
</option>
</splitter-proportions>
</settings>
</state>
</states>
</component>
</project>

View File

@ -1,12 +1,15 @@
# Format: (all args are passed to __init__ as kwargs
# Format (all args are passed to __init__ as kwargs:
#
# <mountpoint>:
# class: <feed_class>
# args:
# key: value
subfactory-show:
package: bassdrive
class: BassdriveFeed
args:
url: http://archives.bassdrivearchive.com/1%20-%20Monday/Subfactory%20Show%20-%20DJ%20Spim/
logo: http://www.bassdrive.com/img/radio_schedule_entries/image/original/subfactory-web-add-56.jpg
server:
port: 10000
podcasts:
subfactory-show:
class: podcasters.BassdriveFeed
args:
url: http://archives.bassdrivearchive.com/1%20-%20Monday/Subfactory%20Show%20-%20DJ%20Spim/
logo: http://www.bassdrive.com/img/radio_schedule_entries/image/original/subfactory-web-add-56.jpg

View File

@ -2,7 +2,9 @@
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>

View File

@ -1,30 +0,0 @@
"""
Given a configuration file, set up everything needed to kick
off the server.
"""
from importlib import import_module
import yaml
from pyramid.config import Configurator
from os.path import expanduser, join
# Needed for import_module call
# noinspection PyUnresolvedReferences
import modules
def build_configuration(conf=None) -> Configurator:
if conf is None:
conf = join(expanduser('~'), '.repodrc')
with open(conf) as conf_file:
conf_dict = yaml.load(conf_file)
server_conf = Configurator()
for mountpoint, feed in conf_dict.items():
feed_package = import_module('modules.' + feed['package'])
feed_class = getattr(feed_package, feed['class'])
feed_instance = feed_class(**feed['args'])
server_conf.add_route(mountpoint, '/' + mountpoint + '/')
server_conf.add_view(feed_instance.view, route_name=mountpoint)
return server_conf

View File

@ -1,10 +0,0 @@
from wsgiref.simple_server import make_server
from conf_parser import build_configuration
def start_server():
app = build_configuration().make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever()
if __name__ == '__main__':
start_server()

38
src/conf_parser.py Normal file
View File

@ -0,0 +1,38 @@
"""
Given a configuration file, set up everything needed to kick
off the server.
"""
from importlib import import_module
import yaml
from pyramid.config import Configurator
def build_configurator(podcasts: dict) -> Configurator:
server_conf = Configurator()
for mountpoint, feed in podcasts:
package, class_name = feed['class'].rsplit('.', 1)
feed_package = import_module(package)
feed_class = getattr(feed_package, class_name)
feed_instance = feed_class(**feed['args'])
server_conf.add_route(mountpoint, '/' + mountpoint + '/')
server_conf.add_view(feed_instance.view, route_name=mountpoint)
def build_configuration_text(file_str: str) -> (dict, Configurator):
conf_dict = yaml.load(file_str)
server_opts = conf_dict.get('server', None)
podcasts = build_configurator(conf_dict['podcasts'])
return server_opts, podcasts
def build_configuration(file_name) -> (dict, Configurator):
try:
with open(file_name) as conf_file:
return build_configuration_text(conf_file.read())
except FileNotFoundError:
print("Could not locate configuration file " +
"(does {} exist?)".format(file_name))
raise

View File

@ -1,18 +1,19 @@
"""
Base skeleton for what needs to be implemented by a podcast provider
"""
from feedgen.feed import FeedGenerator
from pyramid.response import Response
class BasePodcast():
def build_feed(self) -> FeedGenerator:
"Return a list of all episodes, in descending date order"
pass
def view(self, request):
fg = self.build_feed()
response = Response(fg.rss_str(pretty=True))
response.content_type = 'application/rss+xml'
return response
"""
Base skeleton for what needs to be implemented by a podcast provider
"""
from feedgen.feed import FeedGenerator
from pyramid.response import Response
class BasePodcast():
def build_feed(self) -> FeedGenerator:
"Return a list of all episodes, in descending date order"
pass
# noinspection PyUnusedLocal
def view(self, request):
fg = self.build_feed()
response = Response(fg.rss_str(pretty=True))
response.content_type = 'application/rss+xml'
return response

View File

@ -1,91 +1,94 @@
"""
Podcast provider for the Bassdrive Archives
"""
from html.parser import HTMLParser
from urllib.parse import unquote
import requests
from feedgen.feed import FeedGenerator
from podcast import BasePodcast
from datetime import datetime
from pytz import UTC
class BassdriveParser(HTMLParser):
record_link_text = False
link_url = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.links = []
def handle_starttag(self, tag, attrs):
href = ''
for attr, val in attrs:
if attr == 'href':
href = val
if tag == 'a' and href.find('mp3') != -1:
self.record_link_text = True
self.link_url = href
def handle_data(self, data):
if self.record_link_text:
self.links.append((data, self.link_url))
self.record_link_text = False
def get_links(self):
# Reverse to sort in descending date order
return self.links
def clear_links(self):
self.links = []
class BassdriveFeed(BasePodcast):
def __init__(self, *args, **kwargs):
self.url = kwargs['url']
self.logo = kwargs['logo']
# Get the title and DJ while handling trailing slash
url_pretty = unquote(self.url)
elems = filter(lambda x: x, url_pretty.split('/'))
self.title, self.dj = list(elems)[-1].split(' - ')
def build_feed(self):
"Build the feed given our existing URL"
# Get all the episodes
page_content = str(requests.get(self.url).content)
parser = BassdriveParser()
parser.feed(page_content)
links = parser.get_links()
# And turn them into something usable
fg = FeedGenerator()
#fg.load_extension('podcast')
fg.id(self.url)
fg.title(self.title)
fg.description(self.title)
fg.author({'name': self.dj})
fg.language('en')
fg.link({'href': self.url, 'rel': 'alternate'})
fg.logo(self.logo)
for link in links:
fe = fg.add_entry()
fe.author({'name': self.dj})
fe.title(link[0])
fe.description(link[0])
fe.enclosure(self.url + link[1], 0, 'audio/mpeg')
# Bassdrive always uses date strings of
# [yyyy.mm.dd] with 0 padding, so that
# makes our lives easy
date_start = link[0].find('[')
date_str = link[0][date_start:date_start+12]
published = datetime.strptime(date_str, '[%Y.%m.%d]')
fe.pubdate(UTC.localize(published))
fe.guid((link[0]))
parser.clear_links()
return fg
"""
Podcast provider for the Bassdrive Archives
"""
from datetime import datetime
from html.parser import HTMLParser
from urllib.parse import unquote
import requests
from feedgen.feed import FeedGenerator
from pytz import UTC
from podcasters.base import BasePodcast
class BassdriveParser(HTMLParser):
def error(self, message):
return super().error(message)
record_link_text = False
link_url = ''
def __init__(self, *args, **kwargs):
# noinspection PyArgumentList
super().__init__(*args, **kwargs)
self.links = []
def handle_starttag(self, tag, attrs):
href = ''
for attr, val in attrs:
if attr == 'href':
href = val
if tag == 'a' and href.find('mp3') != -1:
self.record_link_text = True
self.link_url = href
def handle_data(self, data):
if self.record_link_text:
self.links.append((data, self.link_url))
self.record_link_text = False
def get_links(self):
# Reverse to sort in descending date order
return self.links
def clear_links(self):
self.links = []
class BassdriveFeed(BasePodcast):
def __init__(self, *args, **kwargs):
self.url = kwargs['url']
self.logo = kwargs['logo']
# Get the title and DJ while handling trailing slash
url_pretty = unquote(self.url)
elems = filter(lambda x: x, url_pretty.split('/'))
self.title, self.dj = list(elems)[-1].split(' - ')
def build_feed(self):
"Build the feed given our existing URL"
# Get all the episodes
page_content = str(requests.get(self.url).content)
parser = BassdriveParser()
parser.feed(page_content)
links = parser.get_links()
# And turn them into something usable
fg = FeedGenerator()
fg.id(self.url)
fg.title(self.title)
fg.description(self.title)
fg.author({'name': self.dj})
fg.language('en')
fg.link({'href': self.url, 'rel': 'alternate'})
fg.logo(self.logo)
for link in links:
fe = fg.add_entry()
fe.author({'name': self.dj})
fe.title(link[0])
fe.description(link[0])
fe.enclosure(self.url + link[1], 0, 'audio/mpeg')
# Bassdrive always uses date strings of
# [yyyy.mm.dd] with 0 padding on days and months,
# so that makes our lives easy
date_start = link[0].find('[')
date_str = link[0][date_start:date_start+12]
published = datetime.strptime(date_str, '[%Y.%m.%d]')
fe.pubdate(UTC.localize(published))
fe.guid((link[0]))
parser.clear_links()
return fg

38
src/server.py Normal file
View File

@ -0,0 +1,38 @@
import argparse
from wsgiref.simple_server import make_server
from conf_parser import build_configuration
from os.path import expanduser, join
# noinspection PyUnresolvedReferences
def start_server(cmd_args: dict):
try:
server_conf, configurator = build_configuration(cmd_args.configuration)
app = configurator.make_wsgi_app()
server = make_server(cmd_args.host, cmd_args.port, app)
server.serve_forever()
except FileNotFoundError:
print("Unable to find configuration file. Does {} exist?"
.format(cmd_args.configuration))
except AttributeError:
print("Unable to parse configuration file. Is {} a valid YML file?"
.format(cmd_args.configuration))
except KeyError:
print('Unable to parse configuration file. Is there a `podcasts`'
'section?')
if __name__ == '__main__':
default_rc = join(expanduser('~'), '.repodrc')
parser = argparse.ArgumentParser()
parser.add_argument('--verbose', help='Run server in verbose mode')
parser.add_argument('--port', type=int, default=10000,
help='Port to use when starting the server')
parser.add_argument('--host', type=str, default='0.0.0.0',
help='Host address to start the server')
parser.add_argument('--configuration', type=str, default=default_rc,
help='Configuration file to start the server')
args = parser.parse_args()
start_server(args)

0
src/tests/__init__.py Normal file
View File

View File

@ -0,0 +1,12 @@
from unittest import TestCase
import conf_parser
class TestBuild_configurator(TestCase):
def test_build_configurator(self):
try:
# noinspection PyTypeChecker
conf_parser.build_configurator(None)
self.fail("Must have dictionary to set up configurator")
except TypeError:
pass