From: Drew Fisher <drew.m.fisher@gmail.com> Date: Sun, 2 Oct 2022 03:28:51 +0000 (-0700) Subject: Rewrite in python3 without web.py X-Git-Url: http://git.zarvox.org/shortlog/week?a=commitdiff_plain;h=af35de78d1c7db97c00fb16560888082779550a5;p=shortlog.git Rewrite in python3 without web.py --- diff --git a/shortlog.py b/shortlog.py index f7b62b3..0a41f3f 100755 --- a/shortlog.py +++ b/shortlog.py @@ -1,11 +1,9 @@ -#!/usr/bin/env python2 - -import web -import os +#!/usr/bin/python3 +from collections import namedtuple import datetime +import mimetypes +import os import re -import smtplib -import hashlib import sys from wsgiref.handlers import CGIHandler @@ -16,8 +14,7 @@ import markdoku entriesdir = os.path.join(pwd, "entries") commentsdir = os.path.join(pwd, "comments") templatesdir = os.path.join(pwd, "templates") - -web.config.debug = True +staticsdir = os.path.join(pwd, "static") monthnames = {"01": "January", "02": "February", @@ -32,29 +29,52 @@ monthnames = {"01": "January", "11": "November", "12": "December"} -urls = ( - '/', 'pastweek', - '/week', 'pastweek' , - '/month', 'pastmonth' , - '/year', 'pastyear' , - '/feed', 'feed' , - '/(\d\d\d\d)/(\d\d)/(\d\d)', 'day' , - '/(\d\d\d\d)-(\d\d)-(\d\d)', 'day' , - '/(\d\d\d\d)/(\d\d)', 'month' , - '/(\d\d\d\d)', 'year', - '', 'redir', - ) - -## This is the way to do it in web.py 0.3+ -app = web.application(urls, globals()) - -## Workaround for autoreloading and sessions when using web.config.debug = True -## Note: I don't actually use sessions -#if web.config.get("_session") is None: -# session = web.session.Session(app, web.session.DiskStore('sessions'), initializer={'count': 0}) -# web.config._session = session -#else: -# session = web.config._session +STATUS_CODES = { + 200: "OK", + 302: "Found", + 404: "Not Found", + 500: "Internal Server Error", +} + +class Response: + def __init__(self, status, headers, body): + self.status = status + self.headers = headers + self.body = body + + def send(self, start_response): + status_str = STATUS_CODES[self.status] + resp_line = f"{self.status} {status_str}" + start_response(resp_line, self.headers) + return self.body + +class Route: + def __init__(self, path_re, fn, methods=["GET"], prefix_match=False): + self.path_re = re.compile(path_re) + self.fn = fn + self.methods = methods + self.prefix_match = prefix_match + + def handle(self, match, env): + return self.fn(match, env) + +class Router: + def __init__(self, routes): + self.routes = routes + + def match(self, env): + for route in self.routes: + if env["REQUEST_METHOD"] not in route.methods: + continue + if route.prefix_match: + m = route.path_re.match(env["PATH_INFO"]) + else: + m = route.path_re.fullmatch(env["PATH_INFO"]) + if m: + return (route, m) + return (None, None) + +# Helper functions # read the paragraphs from a file def readFile(filepath): @@ -76,6 +96,10 @@ def loadComment(filepath): #timestamp = datetime.datetime.fromtimestamp(os.stat(filepath).st_mtime).isoformat() return (nameline, emailline, websiteline, commentbody, timestamp, gravatar) +def most_recent_entries_filelist(count): + files = sorted(os.listdir(entriesdir)) + return files[-count:] + def getCommentFiles(date): # Returns a list of paths to comment files associated with date filelist = [x for x in os.listdir(commentsdir) if re.match(date, x)] return filelist @@ -97,7 +121,6 @@ def makeParas(lines): #return map( lambda x: x if "<ol>" in x or "<ul>" in x or "<pre>" in x or "<table>" in x or "<video>" in x else "<p>" + x + "</p>", paragraphs) return [x if "<ol>" in x or "<ul>" in x or "<pre>" in x or "<table>" in x or "<video>" in x else "<p>" + x + "</p>" for x in paragraphs] - def getFileList(numdays=7): files = sorted(os.listdir(entriesdir)) today = datetime.date.today() @@ -105,143 +128,194 @@ def getFileList(numdays=7): retval = [ x for x in sorted(files) if x > then.isoformat() ] return retval -class year: - def GET(self, theyear): - files = sorted(os.listdir(entriesdir)) - filelist = [ os.path.join(entriesdir, x) for x in files if re.match(theyear,x) ] - # It makes more sense to sort fixed chunks of time in chronological order, rather than feed-style - #filelist.reverse() - dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ] - entries = [markdoku.markdown(readFile(x)) for x in filelist] - commentcounts = map(len, map( getCommentFiles, dates)) - render = web.template.render(templatesdir) - web.header("Content-Type","text/html; charset=utf-8") - return render.multiday(zip(dates, entries, commentcounts), "Entries from %s" % theyear) - -class month: - def GET(self, theyear, themonth): - files = sorted(os.listdir(entriesdir)) - filelist = [ os.path.join(entriesdir, x) for x in files if re.match("%s-%s" % (theyear, themonth), x) ] - #filelist.reverse() - dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ] - entries = [markdoku.markdown(readFile(x)) for x in filelist] - commentcounts = map(len, map( getCommentFiles, dates)) - render = web.template.render(templatesdir) - web.header("Content-Type","text/html; charset=utf-8") - return render.multiday(zip(dates, entries, commentcounts), "Entries from %s, %s" % (monthnames[themonth], theyear)) - -class day: - def GET(self, year, month, day): - filename = os.path.join(entriesdir, "%s-%s-%s.txt" % (year, month, day)) - web.header("Content-Type","text/html; charset=utf-8") - if os.path.isfile(filename): - entry = markdoku.markdown(readFile(filename)) - commentfilelist = os.listdir(commentsdir) - files = sorted([os.path.join(commentsdir, filename) for filename in commentfilelist if re.match("(.*)%s-%s-%s_(\d\d\d\d\d\d).txt$" % (year, month, day), filename) ]) - comments = [] - for f in files: - comments.append(loadComment(f)) - render = web.template.render(templatesdir) - return render.day("%s-%s-%s" % (year, month, day), entry, comments) - else: - render = web.template.render(templatesdir) - return render.noentry("%s-%s-%s" % (year, month, day)) - - def verify(self, test): - if len(test) != 4: - return False - try: - x = int(test,10) - except ValueError: - return False - return True - - def POST(self, year, month, day): - i = web.input() - bottest = i.bottest.strip() - if not self.verify(bottest): - web.redirect("http://127.0.0.1/go_away_bot.html") - return - name = i.name.strip() - email = i.email.strip() - wantsreply = True - if 'wantsreply' in i: - wantsreply = False - website = i.website.strip() - if website != "" and not website.startswith("http"): - website = "http://" + website - comment = i.comment.strip() - timestamp = datetime.datetime.today().isoformat() - # find a file name that isn't taken - fname = "" - for i in xrange(0, 1000000): - fname = os.path.join(pwd, "pending", "%s-%s-%s_%06d.txt" % (year, month, day, i)) - if not os.path.exists(fname): - break - f = open(fname, "a+") - f.write(name + "\n") - if wantsreply: - f.write(email + "\n") - else: - f.write(email + " noreply\n") - f.write(website + "\n") - f.write(timestamp + "\n") - f.write(comment + "\n") - f.close() - - s = smtplib.SMTP("127.0.0.1") - s.sendmail("shortlog@zarvox.org", ["drew.m.fisher@gmail.com"], "To: Drew Fisher <drew.m.fisher@gmail.com>\nFrom: Shortlog <shortlog@zarvox.org>\nSubject: [shortlog] new comment on %s-%s-%s\n\nName: %s\nEmail: %s\nWebsite: %s\nComment: %s" % (year, month, day, name, email, website, comment )) - s.quit() - web.header('Content-Type', 'text/html') - return "<html><body>Thanks! Your comment has been submitted and will be posted pending review.</body></html>" - -class pastyear: - def GET(self): - filelist = [ os.path.join(entriesdir, x) for x in getFileList(365)] - filelist.reverse() - dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ] - entries = [markdoku.markdown(readFile(x)) for x in filelist] - commentcounts = map(len, map( getCommentFiles, dates)) - render = web.template.render(templatesdir) - web.header("Content-Type","text/html; charset=utf-8") - return render.multiday(zip(dates, entries, commentcounts)) - -class pastmonth: - def GET(self): - filelist = [ os.path.join(entriesdir, x) for x in getFileList(30)] - filelist.reverse() - dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ] - entries = [markdoku.markdown(readFile(x)) for x in filelist] - commentcounts = map(len, map( getCommentFiles, dates)) - render = web.template.render(templatesdir) - web.header("Content-Type","text/html; charset=utf-8") - return render.multiday(zip(dates, entries, commentcounts)) - -class pastweek: - def GET(self): - filelist = [ os.path.join(entriesdir, x) for x in getFileList(7)] - filelist.reverse() - dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ] - entries = [markdoku.markdown(readFile(x)) for x in filelist] - commentcounts = map(len, map( getCommentFiles, dates)) - render = web.template.render(templatesdir) - web.header("Content-Type","text/html; charset=utf-8") - return render.multiday(zip(dates, entries, commentcounts)) - -class redir: - def GET(self): - web.redirect("/") - -class feed: - def GET(self): - web.header('Content-Type', 'application/atom+xml') - filelist = os.listdir(entriesdir) - files = [os.path.join(entriesdir, f) for f in filelist if re.match("(.*)(\d\d\d\d-\d\d-\d\d).txt$", f)] - stats = [os.stat(f) for f in files] - decorated = [(stat.st_mtime, filename) for (filename, stat) in zip(files, stats)] - decorated.sort(reverse=True) - if len(decorated) == 0: - return render.feed(datetime.datetime.today(), [] ) +# Templates + +def render_no_entry(date): + return f"""<!DOCTYPE HTML> +<html> +<head> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8" > +<title>Shortlog - a log of everyday things</title> +</head> +<body> +<h1>Shortlog - a log of everyday things</h1> +<h3><a href="/shortlog/">Home</a></h3> +<p>There's no entry for {date}. Perhaps you reached this page by mistake?</p> +</body> +</html> +""" + +def render_comments(comments): + rendered_comments = [] + for comment in comments: + name = comment[0] + email = comment[1] + website = comment[2] + body = comment[3] + timestamp = comment[4] + gravatar = comment[5] + writer_line = None + name_block = f'<a href="{website}">{name}</a>' if website else name + rendered_paras = "\n".join(body) + rendered_comment = "\n".join([ + '<div class="comment">', + f'<p><span style="float: right;"><img src="{gravatar}" alt="avatar from Gravatar"></span>', + f'<p>{name_block} | {timestamp}</p', + rendered_paras + ]) + rendered_comments.append(rendered_comment) + else: + return "" + + return "\n".join([ + "<hr><h3>Comments:</h3>", + "\n".join(rendered_comments), + ]) + +def render_page(content): + return f"""<!DOCTYPE HTML> +<html> +<head> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8" > +<title>Shortlog - a log of everyday things</title> +<link rel="stylesheet" type="text/css" href="/static/style.css"> +<link rel="alternate" type="application/atom+xml" href="/shortlog/feed"> +</head> +<body> +{content} +</body> +</html> +""" + +def render_multiday(entry_tuples, note): + entries = [] + for entry in entry_tuples: + (date, entrytext, commentcount) = entry + commentcount_suffix = "s" if commentcount != 1 else "" + commentcount_label = f"{commentcount} comment{commentcount_suffix}" + entries.append("\n".join([ + "<div>", + f'<h3><a href="/shortlog/{date}">{date}</a> | <a href="/shortlog/{date}#comments">{commentcount_label}</a></h3>', + entrytext, + ])) + all_entry_text = "\n".join(entries) + content = f"""<h1>Shortlog - a log of everyday things</h1> +<h3>View the past: <a href="/shortlog/2013">2013</a> <a href="/shortlog/2012">2012</a> <a href="/shortlog/2011">2011</a> <a href="/shortlog/2010">2010</a> </h3> +<h4><a href="/shortlog/feed">Atom Feed</a></h4> +{note} +{all_entry_text} +""" + return render_page(content) + +def render_day(date, entry, comments): + comment_markup = render_comments(comments) + content = f"""<h1>Shortlog - a log of everyday things</h1> +<h3><a href="/shortlog/">Home</a></h3> +<div> + <h3><a href="/shortlog/{date}">{date}</a></h3> + {entry} +</div> +<div id="comments"> +{comment_markup} +</div>""" + return render_page(content) + +def render_feed(timestamp, entries): + rendered_entries = [] + for entry in entries: + rendered_entries.append(f""" <entry> + <title>{entry["date"]}</title> + <link href="https://zarvox.org/shortlog/{entry["date"]}" rel="alternate"></link> + <id>https://zarvox.org/shortlog/{entry["date"]}</id> + <updated>{entry["timestamp"]}</updated> + <summary type="html">{entry["content"]}</summary> + </entry>""") + entries_text = "\n".join(rendered_entries) + content = f"""<?xml version="1.0" encoding="utf-8"?> +<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"> + <title>Shortlog</title> + <subtitle>A log of everyday things</subtitle> + <link href="https://zarvox.org/shortlog/feed rel="self"></link> + <link href="https://zarvox.org/shortlog/" rel="alternate"></link> + <id>https://zarvox.org/shortlog</id> + <updated>{timestamp}</updated> + <author> + <name>Drew Fisher</name> + <email>zarvox@zarvox.org</email> + </author> +{entries_text} +</feed> +""" + return content + +# Route definitions + +def serve_static(match, env): + requested_filename = env["PATH_INFO"][len("/static/"):] + path = os.path.normpath(os.path.join(staticsdir, requested_filename)) + if os.path.commonprefix([path, staticsdir]) == staticsdir: + content_type = mimetypes.guess_type(path) + with open(path, "rb") as f: + content = f.read() + return Response(200, [("Content-Type", content_type[0])], [content]) + return Response(404, [("Content-Type", "text/html")], + [b"<h1>404 Asset " + requested_filename.encode('utf-8') + b" Not Found :(</h1>"]) + +def serve_test_page(match, env): + return Response(200, [("Content-Type", "text/html")], + [b"<h1>It works!</h1>"]) + +def serve_year(match, env): + (year,) = match.groups() + files = sorted(os.listdir(entriesdir)) + filelist = [ os.path.join(entriesdir, x) for x in files if re.match(year,x) ] + # It makes more sense to sort fixed chunks of time in chronological order, rather than feed-style + #filelist.reverse() + dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ] + entries = [markdoku.markdown(readFile(x)) for x in filelist] + commentcounts = map(len, map( getCommentFiles, dates)) + note = f"Entries from {year}" + body = render_multiday(zip(dates, entries, commentcounts), note) + return Response(200, [('Content-Type', 'text/html; charset=utf-8')], [body.encode('utf-8')]) + +def serve_month(match, env): + (year, month) = match.groups() + files = sorted(os.listdir(entriesdir)) + filelist = [ os.path.join(entriesdir, x) for x in files if re.match(f"{year}-{month}", x) ] + #filelist.reverse() + dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ] + entries = [markdoku.markdown(readFile(x)) for x in filelist] + commentcounts = map(len, map( getCommentFiles, dates)) + note = f"Entries from {monthnames[month]}, {year}" + body = render_multiday(zip(dates, entries, commentcounts), note) + return Response(200, [('Content-Type', 'text/html; charset=utf-8')], [body.encode('utf-8')]) + +def serve_day(match, env): + (year, month, day) = match.groups() + datestr = f"{year}-{month}-{day}" + filename = os.path.join(entriesdir, f"{datestr}.txt") + if os.path.isfile(filename): + entry = markdoku.markdown(readFile(filename)) + commentfilelist = os.listdir(commentsdir) + files = sorted([os.path.join(commentsdir, filename) for filename in commentfilelist if re.match("(.*)%s-%s-%s_(\d\d\d\d\d\d).txt$" % (year, month, day), filename) ]) + comments = [] + for f in files: + comments.append(loadComment(f)) + body = render_day(datestr, entry, comments) + return Response(200, [('Content-Type', 'text/html; charset=utf-8')], [body.encode('utf-8')]) + else: + body = render_no_entry(datestr) + return Response(404, [('Content-Type', 'text/html; charset=utf-8')], [body.encode('utf-8')]) + +def serve_feed(match, env): + filelist = os.listdir(entriesdir) + files = [os.path.join(entriesdir, f) for f in filelist if re.match("(.*)(\d\d\d\d-\d\d-\d\d).txt$", f)] + stats = [os.stat(f) for f in files] + decorated = [(stat.st_mtime, filename) for (filename, stat) in zip(files, stats)] + decorated.sort(reverse=True) + if len(decorated) == 0: + body = render_feed(datetime.datetime.today(), []) + else: group_timestamp = datetime.datetime.fromtimestamp(decorated[0][0]).isoformat() + "-06:00" entries = [] for d in decorated[:10]: @@ -250,20 +324,80 @@ class feed: "content": markdoku.markdown(readFile(d[1])), "timestamp": (mtime.isoformat() + "-06:00") } entries.append(second) - render = web.template.render(templatesdir) - return render.feed(group_timestamp, entries) + body = render_feed(group_timestamp, entries) + return Response(200, [('Content-Type', 'application/atom+xml')], + [body.encode('utf-8')]) + +def serve_most_recent(match, env): + filelist = [ os.path.join(entriesdir, x) for x in most_recent_entries_filelist(20) ] + filelist.reverse() + dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ] + print(dates) + entries = [markdoku.markdown(readFile(x)) for x in filelist] + commentcounts = map(len, map( getCommentFiles, dates)) + note = "" + body = render_multiday(zip(dates, entries, commentcounts), note) + return Response(200, [('Content-Type', 'text/html; charset=utf-8')], [body.encode('utf-8')]) + +router = Router([ + Route("/static/", serve_static, prefix_match=True), + Route("/(\d\d\d\d)-(\d\d)-(\d\d)", serve_day), + Route("/(\d\d\d\d)/(\d\d)/(\d\d)", serve_day), + Route("/(\d\d\d\d)-(\d\d)", serve_month), + Route("/(\d\d\d\d)/(\d\d)", serve_month), + Route("/(\d\d\d\d)", serve_year), + Route("/feed", serve_feed), + Route("/", serve_most_recent), +]) + +#def redir(match, env): +# return Response(302, [("Location", "/")], []) + +#urls = ( +# '', 'redir', +# '/week', 'pastweek' , +# '/month', 'pastmonth' , +# '/year', 'pastyear' , +# '/feed', 'feed' , +# '/', 'pastweek', +# ) + +def application(env, start_response): + (route, match) = router.match(env) + + response = None + if route: + try: + response = route.handle(match, env) + except e: + print("EXCEPTION:", e) + pass + else: + print("No route matched", repr(env["PATH_INFO"])) + response = Response(404, [("Content-Type", "text/html")], + [b"<h1>404 Not Found :(</h1>"]) + + + if response is None: + response = Response(500, [("Content-Type", "text/html")], + [b"<h1>Internal Server Error</h1>"]) -# web.py 0.3+ way -application = web.application(urls, globals()).wsgifunc() + print(env["REQUEST_METHOD"], env["PATH_INFO"], response.status) + return response.send(start_response) -# web.py 0.2 way -#application = web.wsgifunc(web.webpyfunc(urls, globals())) if __name__ == "__main__": # CGI interface CGIHandler().run(application) - ## The web.py 0.3+ way - #app.run() - ## The web.py 0.2 way - #web.run(urls, globals()) + # cherrypy WSGI container + #import cherrypy + #cherrypy.tree.graft(application, "/") + #cherrypy.server.unsubscribe() + #server = cherrypy._cpserver.Server() + #server.socket_host = "127.0.0.1" + #server.socket_port= 5000 + #server.thread_pool = 4 + #server.subscribe() + #cherrypy.engine.start() + #cherrypy.engine.block()