]> git.zarvox.org Git - shortlog.git/commitdiff
Rewrite in python3 without web.py
authorDrew Fisher <drew.m.fisher@gmail.com>
Sun, 2 Oct 2022 03:28:51 +0000 (20:28 -0700)
committerDrew Fisher <drew.m.fisher@gmail.com>
Sun, 2 Oct 2022 03:28:51 +0000 (20:28 -0700)
shortlog.py

index f7b62b3ddab48297f133a3db64ef47e31e09b1da..0a41f3f8c030ca492e93f601c2425469e200cf64 100755 (executable)
@@ -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()