Python Forum
Thread Rating:
  • 1 Vote(s) - 5 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Mini-Web Framework
#1
So I've had an idea running around my head for a while, and I decided to finally do something about it and try it out.  The basics of the mini framework are simple: async by default, and routing is handled via annotations.  I like Flask a lot, but using decorators for url routing is exceptionally ugly to me.  You're applying meta-information in a decorator... the only reason people have done that in the past, was because it was the most practical method available.  Now we have a better alternative, and I wanted to explore that to see if it could actually work as nicely as I thought it could.

While I'm at it, I thought setting this up to be namespaced by default would help to encourage modular design, such that each group of related controllers is within its own namespace (ie: /user/edit, /user/add, /user/list).

This is just an experiment.  It is obviously not meant for production usage.  It's a toy.  Play with it if you want to.
def index() -> "/":
   # routing is handled through an annotation
   # you can read it "function_name 'maps to' url_route"
   return "hi"


async def user(id: int = None) -> "/user/{id}":
   # annotations in arguments causes web.py to auto-cast url args
   # cast failrues (id: int = "bob") get redirected to the namespaced 404
   # urls are written in pseudo-string format syntax...
   # ...web.py converts the pseudo-string to a regex for speed
   return repr(id)


async def user_error(id: None = None) -> "/user/{id}":
   # this will work, but PLEASE don't do that.  You'll confuse anyone reading your code
   # that said, any int id will be routed to user(), while anything else goes
   # to user_error()
   return "you wrong, sucka"


async def all_users() -> "/users":
   # Controllers (I refer to them as Handlers a lot) can be plain functions, or coroutines
   # plain functions will be wrapped into coroutines by asyncio.
   # probably best to just define your controllers as async, unless that's too ugly for you
   # TODO: test if async has any speed impact, or if wrapping native
   # functions is Actively Bad
   return "admin users"


async def not_found():
   return "derp"

if __name__ == "__main__":
   import web

   # make a website!
   site = web.site()

   # / -> index()
   site.route(index)
   # /user/12345 -> user(12345)
   site.route(user)
   # /does-not-exist -> not_found()
   site._404(not_found)

   # bind some handlers to a "namespace"
   # ...all routes will be attached under this top-level-uri
   with site.namespace("/admin") as admin_area:
       # /admin/users -> all_users()
       admin_area.route(all_users)

   # run the website!
   site.run()
That works, though without templating or a decent web server, it's still just a half-baked toy.  Here's the actual web.py:
import asyncio
import re


class Route:
   def __init__(self, path="/", handler=None, formatters={}):
       self.path = path
       self.formatters = formatters
       self.regex = self.compile(path)
       handler = handler if handler else lambda: None
       # fake async for non-async handlers
       self.handler = asyncio.coroutine(handler)

   def compile(self, path="", formatters={}):
       if not isinstance(path, str):
           # feel free to write your own regex
           return path

       # TODO: uri type-intelligent arguments
       path = re.sub(r"\{([^/]+)\}", r"([^/]+)", path)

       path = "^{0}$".format(path)
       return re.compile(path)

   def __call__(self):
       return self.handler()

   #... probably not useful
   def __contains__(self, uri="/"):
       return self.regex.match(uri) is not None

   def __getitem__(self, uri):
       match = self.regex.match(uri)
       if not match:
           return None
       # format data
       return match

   def __repr__(self):
       return self.path


class Namespace:
   # what we call a namespace, other frameworks refer to as "apps" or "modules"
   # a namespace is a self-contained collection of endpoints, bound to a url
   # path
   def __init__(self, site=None, attached="/"):
       self.site = site
       self.attachment_point = attached
       self.routes = []

   def __enter__(self):
       return self

   def __exit__(self, *args):
       self.site.register_namespace(self.attachment_point, self.routes)

   def route(self, route):
       path = None
       anno = route.__annotations__
       if "return" in anno:
           path = anno["return"]
           del anno["return"]

       # TODO: lists should be allowed in annotations so a controller can
       # listen to multiple uris
       self.routes.append(Route(path=path, handler=route, formatters=anno))

       # allow for route chaining
       return self

   def _404(self, handler):
       self.errors = {"404": Route(handler=handler)}

   def namespace(self, base):
       return Namespace(self, base)

   def register_namespace(self, attachment_point, routes):
       for route in routes:
           new_path = "{0}{1}".format(attachment_point, route.path)
           new_route = Route(
               path=new_path,
               handler=route.handler,
               formatters=route.formatters
           )
           self.routes.append(new_route)


class Site(Namespace):
   # a site is just the root node of a namespace heirarchy
   # only the root node (ie: the Site) can actually be run
   # sub-namespaces are dependent upon their parent
   def run(self):
       import http.server as http

       class Handler(http.BaseHTTPRequestHandler):
           def do_GET(server):
               content = None
               for route in self.routes:
                   data = route[server.path]
                   if data:
                       content = self.handle(route, data)
                       break
               else:
                   if "404" in self.errors:
                       content = self.handle(self.errors["404"])
               if content:
                   server.wfile.write(content.encode())

       with http.HTTPServer(("", 8080), Handler) as httpd:
           print("Starting test server...")
           httpd.serve_forever()

   # TODO: have an event loop that processes incomplete requests
   # TODO: handle() should just add things to the event loop
   # TODO: ...actually pass url parameters to controller
   def handle(self, route, data={}):
       runner = route()
       try:
           while True:
               runner.send(None)
       except StopIteration as resp:
           return resp.value


# nice things should look nice :)
site = Site
To me, the most interesting thing currently is the namespacing.  For example, this snippet (already listed above):
   # bind some handlers to a "namespace"
  # ...all routes will be attached under this top-level-uri
  with site.namespace("/admin") as admin_area:
      # /admin/users -> all_users()
      admin_area.route(all_users)
The admin_area controller only lists it's route as "/users", but because it's routed to the /admin namespace, it will only respond to "/admin/users"... the controller doesn't have any concept of where it's actually mounted, it only knows what it needs to.  The idea here, is to have a class of controllers that only refer to things like /edit or /add, so you can share that module easily, and it can be mounted easily anywhere.  The request object which will get passed to each controller will then contain the current namespace, so the controller's view can build accurate urls.

Roughly, something like this:
class User:
   def index(self) -> "/":
       pass

   def add(self) -> "/add":
       pass

   def edit(self, id) -> "/edit/{id}":
       pass

   def route(self, mount):
       mount.route(self.index).route(self.add).route(self.edit)

site = web.site()
with site.namespace("/user") as ns_user:
   user = User()
   user.route(ns_user)
So anyway, here's a little something I've been messing around with.  The routing works, and a server starts up to serve content based on the routing, but that's all that's done.
I'd still like to do:
-type-dependent uri routing, based on annotations (and auto-casting to those types before dispatching)
-actually using asyncio.  I mostly just faked it long enough to have the test script work, but it's still trash.
-move the http.server.HTTPServer to a backend module that handles the binding to a port and communicating with a socket
-create another backend using Twisted using the same interface, except... actually usable for production environments
-wrap jinja in a nice interface for simple templating
Reply


Messages In This Thread
Mini-Web Framework - by nilamo - May-25-2017, 03:34 AM
RE: Mini-Web Framework - by nilamo - May-25-2017, 03:28 PM
RE: Mini-Web Framework - by wavic - May-25-2017, 04:05 PM
RE: Mini-Web Framework - by nilamo - May-25-2017, 05:05 PM
RE: Mini-Web Framework - by micseydel - May-25-2017, 08:55 PM
RE: Mini-Web Framework - by nilamo - May-25-2017, 09:19 PM
RE: Mini-Web Framework - by nilamo - Jun-06-2017, 01:23 AM
RE: Mini-Web Framework - by wavic - Jun-06-2017, 03:32 AM
RE: Mini-Web Framework - by nilamo - Jun-06-2017, 03:42 PM
RE: Mini-Web Framework - by wavic - Jun-06-2017, 05:00 PM
RE: Mini-Web Framework - by Skaperen - Jun-14-2017, 06:19 AM
RE: Mini-Web Framework - by nilamo - Jun-14-2017, 11:16 PM
RE: Mini-Web Framework - by Skaperen - Jun-15-2017, 05:32 AM

Possibly Related Threads…
Thread Author Replies Views Last Post
  Guess the dice roll mini-game tawnnx 6 7,439 May-22-2018, 02:12 PM
Last Post: malonn
  5 mini programming projects for the python beginner kerzol81 4 36,303 Sep-26-2017, 02:36 PM
Last Post: VeenaReddy

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020