Writing a Python

web framework
in 2021

By Emmanuelle Delescolle

Who am I?

Why?

  • The major Python webframeworks are over 10yo. Things have changed
  • Explore libraries unavailable/undocumented at the time
  • Build API and Websockets into the main code
  • Thought exercise

Then Vs Now

Source FreeIMG

Source WikiMedia

Then Vs Now

Poor documentation of most libraries

A lot of Python packages are well documented

Little sense of community

Friendly Python community

Rails was the "reference"

Many Python web frameworks to get inspiration from. But also more non-Python frameworks (Laravel, Spray, etc...)

Server-rendered-pages was the main thing to have in mind

Rest API's and websockets have become primary concerns

What about...

  • Sanic
  • FastAPI
  • Falcon
  • Quark
  • autobahn
  • Starlet
  • Tornado
  • ....

Let's go on a tour!

Source pixabay

Tour: Project Template

django-admin startproject splendid
./manage.py startapp core
cookiecutter cordy_project
cookiecutter cordy_app
cookiecutter \
gh:Pylons/pyramid-cookiecutter-starter \
--checkout 2.0-branch

Tour: ORM

class Deck(Model):
    level = models.IntegerField()
    cards = JSONField()

Deck.objects.filter(level=2)
class Deck(Model):
    level = pw.IntegerField()
    cards = JSONField()

Deck.select().filter(level=2)
Deck.select().where(Deck.level==2)
class Deck(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    level = db.Column(db.Integer)
    cards = db.Column(JSON)


session.query(Deck).filter(Deck.level==2)

Tour: Template engine

<p>{{object.level}}</p>
{%if object.level == 3 %}
  <span>Many points</span>
{%endif%}

Tour: Settings

from django.conf import settings

print(settings.SOME_VAR)
from simple_settings import settings

print(settings.SOME_VAR)

Tour: Routing

url_map = [
    Route('infos', '/deck/{id}/info', controller='myapp.DeckViewSet', action='info'),
    *MyController.get_routes(), 
]


class MyController(Controller):

    @action(needs_id=False)
    def sayhello(self):
        return Response('OK')
config.add_route('myroute', '/prefix/{one}/{two}')
config.scan('mypackage')

@view_config(route_name='myroute')
def myview(request):
    return Response('OK')
@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"
urlpatterns = [
    path('articles/2003/', views.special_case_2003),
    path('articles/<int:year>/', views.year_archive),
]

Tour: (De)Serialization

from marshmallow import fields, Schema

class UserCreateSerializer(Schema):
    username = fields.String()
    password = fields.String()

Tour: Request/Response

if request.method == 'GET':
    do_something()
elif request.method == 'POST':
    do_something_else()

return Response(text="Here's the text of the Web page."')

Tour: Command-Line

@click.command()
@click.argument('poll_ids', nargs=-1)
def hello(poll_ids=()):
    # do something

Tour: Middlewares

WSGI middlewares

IE: fancy name function wrappers

Tour: Special *SGI implementation

to support websockets

def application(env, start_response):
    uwsgi.websocket_handshake(env['HTTP_SEC_WEBSOCKET_KEY'], env.get('HTTP_ORIGIN', ''))
    while True:
        msg = uwsgi.websocket_recv()
        uwsgi.websocket_send(msg)

Tour: Form Builder

Put everything in the 

Almost... But not exactly!

blender
and
press power
?

Source flickr

Missing links

Source WikiMedia

Missing Links

  • CSRF
  • Authentication
  • Django-style Admin?
  • "glue"

Missing Links

  • CSRF
  • Authentication
  • Django-style Admin
  • "glue"

} ->

Copy from Django

->

->

Use "regular" form handling

...

Missing Links

Glue code: aka WSGI handler

class Handler:

  handler = None
  _after_response = None
  _after_request = None
class Handler:

  handler = None
  _after_response = None
  _after_request = None

 def __call__(self, enviro, start):
     to_call = Cordy.mapper.match(environ=enviro)
     request = Request(enviro)
class Handler:

  handler = None
  _after_response = None
  _after_request = None

 def __call__(self, enviro, start):
     to_call = Cordy.mapper.match(environ=enviro)
     request = Request(enviro)

     if to_call is None:
        response = HTTPException(404)
class Handler:

  handler = None
  _after_response = None
  _after_request = None

 def __call__(self, enviro, start):
     to_call = Cordy.mapper.match(environ=enviro)
     request = Request(enviro)

     if to_call is None:
        response = HTTPException(404)
     else:
        action = to_call.pop('action')
        controller_class = to_call.pop('controller')
        request.action = action

Missing Links

Glue code: aka WSGI handler

        action = to_call.pop('action')
        controller_class = to_call.pop('controller')
        request.action = action
        try:
            controller = import_string(controller_class)(request)
            method = getattr(controller, action)
            response = method(**to_call)
            response = _make_response(response, controller, action)
        action = to_call.pop('action')
        controller_class = to_call.pop('controller')
        request.action = action
        try:
            controller = import_string(controller_class)(request)
            method = getattr(controller, action)
            response = method(**to_call)
            response = _make_response(response, controller, action)
        action = to_call.pop('action')
        controller_class = to_call.pop('controller')
        request.action = action
        try:
            controller = import_string(controller_class)(request)
            method = getattr(controller, action)
            response = method(**to_call)
            response = _make_response(response, controller, action)
        except BaseHTTPException as http_e:
            response = http_e
        action = to_call.pop('action')
        controller_class = to_call.pop('controller')
        request.action = action
        try:
            controller = import_string(controller_class)(request)
            method = getattr(controller, action)
            response = method(**to_call)
            response = _make_response(response, controller, action)
        except BaseHTTPException as http_e:
            response = http_e
        except WSDisconnect:
            return []
        action = to_call.pop('action')
        controller_class = to_call.pop('controller')
        request.action = action
        try:
            controller = import_string(controller_class)(request)
            method = getattr(controller, action)
            response = method(**to_call)
            response = _make_response(response, controller, action)
        except BaseHTTPException as http_e:
            response = http_e
        except WSDisconnect:
            return []
        except Exception as e:
            response = HTTPException(500, e.args)          

Missing Links

Glue code: aka WSGI handler

  def __call__(self, enviro, start):
     to_call = Cordy.mapper.match(environ=enviro)
     request = Request(enviro)

     if to_call is None:
        ...
     else:
        try:
            response = method(**to_call)
            response = _make_response(response, controller, action)
        except:
            ...

      self.handler = response
      return response(enviro, start)

Missing Links

  • CSRF
  • Authentication
  • Django-style Admin
  • "glue"

} ->

Copy from Django

->

->

Use "regular" form handling

Cordy

What is Cordy?

Annie Cordy

Belgian actress and singer

 

Léonie, Baroness Cooreman, known by the stage name Annie Cordy, was a Belgian actress and singer. She appeared in more than 50 films from 1954. King Albert II of Belgium bestowed upon her the title of Baroness in recognition for her life's achievements.

What is Cordy?

Cordy is a way to rope-in all the libraries and components mentioned before.

 

It is a thought experiment

 

Hopefully it can serve as inspiration for the future of Python web frameworks

What is Cordy?

from cordy.auth.models import BaseUser, Group
from cordy.db.models import Model

import peewee as pw


class ToDo(Model):

    description = pw.TextField()
    is_done = pw.BooleanField(null=True)


class User(BaseUser):

    groups = pw.ManyToManyField(Group, backref='users')


UserGroup = User.groups.get_through_model() 

models.py example

What is Cordy?

class Controller(CordyController):

    @action(needs_id=False)
    def index(self):
        return HTMLResponse(
          content="<h1>Hello World</h1>"
        )

      
 class ToDoViewSet(CRUDViewSet):

    Model = ToDo
    pagination_class = PageNumberPagination
    page_size = 2
    filter_fields = ['is_done']
    search_fields = ['description', ]

controllers.py example 

class WSController(CordyWSController):

    def on_connect(self):
        print('WS Connect')

    def on_message(self, message):
        print('Received message:', message)

    def on_receive(self, data):
        self.send(data['data'])

    def on_disconnect(self):
        print('WS Disconnected')

What is Cordy?

urls.py example 

from routes.route import Route

from cordy.crud.controllers import OpenAPIView
from cordy.utils import include

from myapp.controllers import Controller, ToDoViewSet, ToDoHTML


url_map = [
    *Controller.get_routes(prefix=''),
    Route('websocket', '/ws/', controller='myapp.WSController', action='connect'),
    Route('static', "/public/{path_info:.*}", controller='cordy.base.StaticFiles',
          action='serve'),
    include(ToDoViewSet.get_routes(), '/api/v1'),
    include(OpenAPIView.get_routes(prefix='v1', path='/api/v1/'),
                '/apidocs'),
    include(ToDoHTML.get_routes(prefix='todo'), ''),
]

What is Cordy?

In Action 

What would be the
pros & cons?

People involved in Python web are already involved in those libraries

Loss of agency (dependent on library maintainers)

Maintaining a framework as a whole is easier

Resources can be dedicated to the core of the framework

Possible loss of backward compatibility with new library releases

Overall less work needed

Questions

Root

Shell

Curry

https://splendid.dev.levitnet.be

Writing a python web framework in 2021

By Emma

Writing a python web framework in 2021

EuroPython 2021

  • 1,161