I've taken a dive into a number of different methods to serve content with python and I have to say the ASGI standard is by far the most useful and well thought out way to do things, to the point that I would go as far to say that it makes things easy enough that the convenience (and sometimes burden!) of a framework can be replace by a simple handful of functions. This also gives us the perfect opportunity to get to grips with the foundations that many modern frameworks sit on.
Now is a good time to download and open up the source code for the example (see below), we'll start with main.py which is the initial entry point for each request where we decide how we're going to handle it.
Initially lifespan messages are used to signal when the application serving the requests is started, using this we can create (and later destroy) an instance of an SqlAlchemy engine, this is shared amongst all the requests the application deals with.
For this simple example each "scope" (an individual request) is using a single database connection, but there is no reason why an application couldn't make multiple asynchronous connections to different data sources, and build separate parts of a page in parallel.
Once actually dealing with a HTTP request, the first thing to deal with is routing, matching up a URL with the appropriate code. While a third party library could be used for this, if your requirements are simple, and in order to concentrate solely on ASGI, its as easy to roll our own. Note that this routing system deliberately doesn’t handle GET variable as they are almost tantamount to an invitation to tinker, and I know POST variables can be tinkered with but there are ways to mitigate this that I’ll go into in a later post, suffice to say the “invitation” to tinker is less visible and tempting to novice users.
If there is no corresponding code for a path (in the pages directory) then a check is made for “static” files, if one can’t be served then a 404 response is sent instead.
Given an authenticated user, the session ID is refreshed (a new cookie is sent, and the users record is updated). For pages that are not in the PUBLIC_PAGES setting, if the user isn’t logged in a redirect is made to the login page, here the only place a URL variable is used, a traditional GET parameter redirects the user back to where they were once logged in.
Now the application is ready to import the code for the requested endpoint, the path separators are substituted with periods (“.”). Should the code not import an error page is displayed and the database connection is closed. This is also where a users sessions idle or expiry time is reset.
The imported page is executed and again if there is an issue an error page is presented, in either case the end point has been satisfied and the scopes database connection is closed.
Now that the applications usual execution has been described we can look at how to implement an endpoint. In this example we'll look at a simple page that maintains a single changeable and persistent variable (pagevars.py in the example code) It’s worth noting at this point that I've decided to create pages as an XML structure that is used when the page is “rendered” as the last act of the endpoint. I’ll go into more detail in subsequent posts but suffice to say it make creating a page easier and less error prone, the whole page can be entirely imported from a template and programmatically added to, allowing in effect the whole structure of the page to be treated as a template if required.
As the entrance point, all endpoints must provide an asynchronous index function, accepting the standard scope, receive and send parameters as you’d normally expect for the ASGI application itself.
A utilities module has a function to create an initial XML page structure, consisting of just a head tag and empty body tag, as well as returning the body tag this function also returns the DOM object (Document Object Model) which is used later. While not obligatory I tend to add to the body a single DIV tag (with a CSS class) to contain all the main content for the page.
The get_page_globals utility function is used to retrieve a set of values from the database that are associated with this page. initially the page is accessed via a GET request, so the value used for demonstration is set to its default value at this point. All subsequent access to the page where persistent values are needed are done via POST requests (a single button form can be very easily styled to look and behave like an anchor link if needed)
A POST request is first handled by parsing the received body of the request, with the first job being to test a previously saved CSRF token. Assuming the CSRF token is okay then the posted value is used to update the value in the globals. Having parsed the POST body, a new CSRF token is stored in the globals, the token will be embedded in any forms the page uses for a subsequent request.
The main content of the page is created including a form which is created with the create_form utility function this creates the tags needed to embed the CSRF token and provide the form with a name value to identify it.
Just prior to handling a POST request the page globals stored in the database are retrieved, this uses the "scope" object which by now has the user information for our logged in user. When the HTTP request method is GET, the "var1" variable in the page globals is set to a default value. In the case of a POST request, the request body is parsed and the submitted CSRF token is verified against the one stored in the page globals. If the tokens match, the user-submitted value for "var1" is saved to the page globals. If the CSRF tokens don't match, an error message is displayed.
Ready for the next request, a new CSRF token is generated and saved to the page globals. At this point having delt with the requests specifics we can save the page globals for this users session with the page and create the content.
The form could as easily be part of a template with the CSRF token inserted but as there is only a tiny bit of content its as easy to create the page programmatically.
A form element is created using the "util.create_form" function this inserts the supplied CSRF token in the form for us. then an input field for "var1" and a submit button, are added to the forms object.
There is a "util.dump_scope" function for development use, really we should check the value of IS_DEV setting before using this, but in this example it is done just to show the wealth of information that our the "scope" object contains for each request.
Once we're happy with the content assembled in the DOM object we can "render" it in the form of a HTML response.
Well the example covers quite a lot, so I'll leave it there for now, but I'll likely detail further uses for it in future posts, including detailing the paginated grid and how to mitigate form tampering. Even as it stands the example provides a nice vehicle to detail some of the many advantage for using Python and specifically ASGI for creating web content.
You can grab the example code here.