annotate myrss/myrss_app.py @ 74:d6acf8b093b7

myrss: update user-agent string to "Mozilla/5.0" to fix servers that insist on one
author paulo
date Wed, 27 Jan 2016 01:50:15 -0700
parents c7bbd3805509
children 51f0da3da721
rev   line source
paulo@39 1 import os
paulo@40 2 import sys
paulo@39 3 import re
paulo@40 4 import urllib2
paulo@40 5 import threading
paulo@40 6 import Queue
paulo@41 7 import datetime
paulo@41 8 import time
paulo@70 9 import traceback
paulo@47 10
paulo@42 11 import logging
paulo@68 12 logging.basicConfig(
paulo@70 13 #level=logging.DEBUG,
paulo@68 14 #filename="_LOG",
paulo@68 15 #format="%(asctime)s %(levelname)-8s %(message)s",
paulo@68 16 )
paulo@39 17
paulo@47 18 import xml.etree.ElementTree
paulo@47 19 import HTMLParser
paulo@47 20
paulo@39 21 import html
paulo@39 22
paulo@39 23
paulo@41 24 FEEDS_FILE = "FEEDS"
paulo@41 25 CACHE_HTML_FILE = "__cache__.html"
paulo@41 26
paulo@44 27 CACHE_LIFE = 1200 # [seconds]
paulo@47 28 MAX_ITEMS = 50
paulo@39 29 MAX_LINK_Z = 4
paulo@40 30 MAX_THREADS = 20
paulo@46 31 URLOPEN_TIMEOUT = 60 # [seconds]
paulo@39 32
paulo@39 33
paulo@39 34 _PARSE_ROOT_TAG_RE = re.compile(r"(\{(.+)\})?(.+)")
paulo@39 35
paulo@39 36 def _parse_root_tag(root_tag):
paulo@39 37 re_match = _PARSE_ROOT_TAG_RE.match(root_tag)
paulo@39 38
paulo@39 39 if re_match is None:
paulo@39 40 return (None, None)
paulo@39 41 else:
paulo@39 42 return re_match.group(2, 3)
paulo@39 43
paulo@39 44
paulo@47 45 def _strip_if_not_none(txt):
paulo@47 46 return txt.strip() if txt is not None else ''
paulo@47 47
paulo@47 48
paulo@39 49 def _go_rss(elementTree):
paulo@47 50 title = _strip_if_not_none(elementTree.find("channel/title").text)
paulo@39 51 link = elementTree.find("channel/link").text
paulo@39 52
paulo@39 53 items = []
paulo@39 54
paulo@39 55 for i in elementTree.findall("channel/item")[:MAX_ITEMS]:
paulo@47 56 it_title = _strip_if_not_none(i.find("title").text)
paulo@39 57 it_link = i.find("link").text
paulo@39 58
paulo@39 59 items.append((it_title, it_link))
paulo@39 60
paulo@39 61 return (title, link, items)
paulo@39 62
paulo@39 63
paulo@39 64 def _go_atom(elementTree):
paulo@39 65 ns = "http://www.w3.org/2005/Atom"
paulo@39 66
paulo@47 67 title = _strip_if_not_none(elementTree.find("{%s}title" % ns).text)
paulo@39 68 link = ''
paulo@39 69
paulo@39 70 for i in elementTree.findall("{%s}link" % ns):
paulo@39 71 if i.get("type") == "text/html" and i.get("rel") == "alternate":
paulo@39 72 link = i.get("href")
paulo@39 73 break
paulo@39 74
paulo@39 75 items = []
paulo@39 76
paulo@39 77 for i in elementTree.findall("{%s}entry" % ns)[:MAX_ITEMS]:
paulo@47 78 it_title = _strip_if_not_none(i.find("{%s}title" % ns).text)
paulo@39 79 it_link = ''
paulo@39 80
paulo@39 81 for j in i.findall("{%s}link" % ns):
paulo@39 82 if j.get("type") == "text/html" and j.get("rel") == "alternate":
paulo@39 83 it_link = j.get("href")
paulo@39 84 break
paulo@39 85
paulo@39 86 items.append((it_title, it_link))
paulo@39 87
paulo@39 88 return (title, link, items)
paulo@39 89
paulo@39 90
paulo@69 91 def _go_purl_rss(elementTree):
paulo@69 92 ns = "http://purl.org/rss/1.0/"
paulo@69 93
paulo@69 94 title = _strip_if_not_none(elementTree.find("{%s}channel/{%s}title" % (ns, ns)).text)
paulo@69 95 link = elementTree.find("{%s}channel/{%s}link" % (ns, ns)).text
paulo@69 96
paulo@69 97 items = []
paulo@69 98
paulo@69 99 for i in elementTree.findall("{%s}item" % ns)[:MAX_ITEMS]:
paulo@69 100 it_title = _strip_if_not_none(i.find("{%s}title" % ns).text)
paulo@69 101 it_link = i.find("{%s}link" % ns).text
paulo@69 102
paulo@69 103 items.append((it_title, it_link))
paulo@69 104
paulo@69 105 return (title, link, items)
paulo@69 106
paulo@69 107
paulo@47 108 _STRIP_HTML_RE = re.compile(r"<.*?>")
paulo@47 109 _htmlParser = HTMLParser.HTMLParser()
paulo@47 110
paulo@47 111 def _strip_html(txt):
paulo@47 112 return _htmlParser.unescape(_STRIP_HTML_RE.sub('', txt))
paulo@47 113
paulo@47 114
paulo@41 115 def _to_html(dtnow, docstruct):
paulo@41 116 datetime_str = dtnow.strftime("%Y-%m-%d %H:%M %Z")
paulo@41 117 page_title = "myrss -- %s" % datetime_str
paulo@41 118
paulo@42 119 root = html.HTML("html")
paulo@39 120
paulo@39 121 header = root.header
paulo@41 122 header.title(page_title)
paulo@39 123 header.link(rel="stylesheet", type="text/css", href="index.css")
paulo@39 124
paulo@41 125 body = root.body
paulo@41 126 body.h1(page_title)
paulo@41 127
paulo@39 128 link_z = 0
paulo@39 129
paulo@39 130 for feed in docstruct:
paulo@40 131 if feed is None:
paulo@40 132 continue
paulo@40 133
paulo@39 134 (title, link, items) = feed
paulo@39 135
paulo@47 136 body.h2.a(_strip_html(title), href=link, klass="z%d" % (link_z % MAX_LINK_Z))
paulo@39 137 link_z += 1
paulo@41 138 p = body.p
paulo@39 139
paulo@39 140 for (i, (it_title, it_link)) in enumerate(items):
paulo@39 141 if i > 0:
paulo@39 142 p += " - "
paulo@39 143
paulo@72 144 if not it_title:
paulo@72 145 it_title = "(missing title)"
paulo@72 146 if it_link is not None:
paulo@72 147 p.a(_strip_html(it_title), href=it_link, klass="z%d" % (link_z % MAX_LINK_Z))
paulo@72 148 else:
paulo@72 149 p += _strip_html(it_title)
paulo@72 150
paulo@39 151 link_z += 1
paulo@39 152
paulo@46 153 dtdelta = datetime.datetime.now() - dtnow
paulo@46 154 root.div("%.3f" % (dtdelta.days*86400 + dtdelta.seconds + dtdelta.microseconds/1e6), klass="debug")
paulo@46 155
paulo@39 156 return unicode(root).encode("utf-8")
paulo@39 157
paulo@39 158
paulo@47 159 def _fetch_url(url):
paulo@40 160 try:
paulo@42 161 logging.info("processing %s" % url)
paulo@74 162 feed = urllib2.urlopen(urllib2.Request(url, headers={"User-Agent": "Mozilla/5.0"}), timeout=URLOPEN_TIMEOUT)
paulo@40 163 except urllib2.HTTPError as e:
paulo@42 164 logging.info("(%s) %s" % (url, e))
paulo@47 165 return None
paulo@47 166
paulo@47 167 return feed
paulo@47 168
paulo@47 169
paulo@47 170 def _process_feed(feed):
paulo@47 171 ret = None
paulo@40 172
paulo@40 173 elementTree = xml.etree.ElementTree.parse(feed)
paulo@40 174 root = elementTree.getroot()
paulo@40 175
paulo@40 176 parsed_root_tag = _parse_root_tag(root.tag)
paulo@40 177
paulo@40 178 if parsed_root_tag == (None, "rss"):
paulo@40 179 version = float(root.get("version", 0.0))
paulo@40 180 if version >= 2.0:
paulo@40 181 ret = _go_rss(elementTree)
paulo@40 182 else:
paulo@40 183 raise NotImplementedError("Unsupported rss version")
paulo@40 184 elif parsed_root_tag == ("http://www.w3.org/2005/Atom", "feed"):
paulo@40 185 ret = _go_atom(elementTree)
paulo@69 186 elif parsed_root_tag == ("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "RDF"):
paulo@69 187 ret = _go_purl_rss(elementTree)
paulo@40 188 else:
paulo@40 189 raise NotImplementedError("Unknown root tag")
paulo@40 190
paulo@40 191 return ret
paulo@40 192
paulo@40 193
paulo@40 194 class WorkerThread(threading.Thread):
paulo@40 195 def __init__(self, *args, **kwargs):
paulo@40 196 self._input_queue = kwargs.pop("input_queue")
paulo@40 197 self._output_queue = kwargs.pop("output_queue")
paulo@40 198 threading.Thread.__init__(self, *args, **kwargs)
paulo@40 199 self.daemon = True
paulo@40 200
paulo@40 201 def run(self):
paulo@40 202 while True:
paulo@40 203 (idx, url) = self._input_queue.get()
paulo@40 204 docfeed = None
paulo@40 205 try:
paulo@47 206 feed = _fetch_url(url)
paulo@47 207 if feed is not None:
paulo@47 208 docfeed = _process_feed(feed)
paulo@40 209 except Exception as e:
paulo@42 210 logging.info("(%s) exception: %s" % (url, e))
paulo@40 211 self._output_queue.put((idx, docfeed))
paulo@40 212
paulo@40 213
paulo@44 214 def main(input_queue, output_queue, lock):
paulo@41 215 ret = ''
paulo@41 216
paulo@44 217 with lock:
paulo@44 218 epoch_now = time.time()
paulo@44 219 dtnow = datetime.datetime.fromtimestamp(epoch_now)
paulo@41 220
paulo@44 221 if os.path.exists(CACHE_HTML_FILE) and (epoch_now - os.stat(CACHE_HTML_FILE).st_mtime) < float(CACHE_LIFE):
paulo@44 222 with open(CACHE_HTML_FILE) as cache_html_file:
paulo@44 223 ret = cache_html_file.read()
paulo@41 224
paulo@44 225 else:
paulo@44 226 with open(FEEDS_FILE) as feeds_file:
paulo@44 227 feedlines = feeds_file.readlines()
paulo@41 228
paulo@44 229 docstruct = [None]*len(feedlines)
paulo@44 230 num_input = 0
paulo@44 231 for (i, l) in enumerate(feedlines):
paulo@44 232 if l[0] != '#':
paulo@44 233 l = l.strip()
paulo@44 234 input_queue.put((i, l))
paulo@44 235 num_input += 1
paulo@41 236
paulo@44 237 for _ in range(num_input):
paulo@44 238 (idx, docfeed) = output_queue.get()
paulo@44 239 docstruct[idx] = docfeed
paulo@41 240
paulo@44 241 ret = _to_html(dtnow, docstruct)
paulo@41 242
paulo@44 243 with open(CACHE_HTML_FILE, 'w') as cache_html_file:
paulo@44 244 cache_html_file.write(ret)
paulo@41 245
paulo@41 246 return ret
paulo@41 247
paulo@41 248
paulo@42 249 class MyRssApp:
paulo@42 250 def __init__(self):
paulo@42 251 self._iq = Queue.Queue(MAX_THREADS)
paulo@42 252 self._oq = Queue.Queue(MAX_THREADS)
paulo@44 253 self._main_lock = threading.Lock()
paulo@39 254
paulo@42 255 for _ in range(MAX_THREADS):
paulo@42 256 WorkerThread(input_queue=self._iq, output_queue=self._oq).start()
paulo@42 257
paulo@42 258 def __call__(self, environ, start_response):
paulo@70 259 response_code = "500 Internal Server Error"
paulo@70 260 response_type = "text/plain; charset=UTF-8"
paulo@70 261
paulo@70 262 try:
paulo@70 263 response_body = main(self._iq, self._oq, self._main_lock)
paulo@70 264 response_code = "200 OK"
paulo@70 265 response_type = "text/html; charset=UTF-8"
paulo@70 266 except:
paulo@70 267 response_body = traceback.format_exc()
paulo@70 268
paulo@42 269 response_headers = [
paulo@70 270 ("Content-Type", response_type),
paulo@42 271 ("Content-Length", str(len(response_body))),
paulo@42 272 ]
paulo@70 273 start_response(response_code, response_headers)
paulo@42 274
paulo@42 275 return [response_body]