Package: tokamak
TokamakTokamak is a pure-Python router based on Radix trees intended for ASGI Python applications.[Read the documentation]Primary Project GoalsThere are many HTTP routers based on radix trees available in other language communities, including Go, Javascript, Rust, and others. In Python, however, most open-source Python web frameworks instead utilize lists to store and look up HTTP routes.The primary goal for this project is to provide a radix-tree-based router for Python web frameworks (or any custom ASGI or WSGI implementation).This library isexperimental. Use at your own risk.Other GoalsAs a secondary goal, a minimal and highly experimental web framework is included an optional install in this library.The reasons for including this web framework are as follows:It provides a convenient way to test theAsgiRouterclass, andIt allows the authors of this library to explore experimental ASGI-framework features, including request-cancellation, background task time-limits, and background task cancellation.Developers should consider more fully-featured web frameworksbeforethis one and there are many to choose from:DjangoFlaskQuartStarletteTornadoInstallationYou can installtokamakwith:pipinstalltokamakBy default tokamak has no dependencies.If you would like to try out the experimental web framework, you can install with optional extrasweb, which will includetrio:$pipinstall"tokamak[web]"...UsageThis library provides a radix tree implementation and a basicAsgiRouterrouter implementation for low-level ASGI applications. You can use theAsgiRouterclass as follows.First, we start with some with some imports and some fallback handlers:fromhypercorn.configimportConfigfromhypercorn.trioimportserveimporttriofromtokamakimportAsgiRouter,Routefromtokamak.routerimportMethodNotAllowed,UnknownEndpoint# # Fallback Handlers # #asyncdefmethod_not_allowed(scope,receive,send):awaitsend({"type":"http.response.start","status":405,"headers":[(b"Content-Type",b"text/html; charset=UTF-8")],})awaitsend({"type":"http.response.body","body":b"<html><body><h1>405 Method not allowed</h1></body></html>",})asyncdefunknown_handler(scope,receive,send):awaitsend({"type":"http.response.start","status":404,"headers":[(b"Content-Type",b"text/html; charset=UTF-8")],})awaitsend({"type":"http.response.body","body":b"<html><body><h1>404 Not Found!</h1></body></html>",})Next we'll build two different application endpoint handlers. These do roughly the same thing, so this is purely for demonstration purposes:asyncdefindex(path_context,scope,receive,send):message=awaitreceive()ifmessage["type"]=="http.request":body=message.get("body",b"")# here's our response:awaitsend({"type":"http.response.start","status":200})awaitsend({"type":"http.response.body","body":bodyifbodyelseb"OK"})elifmessage["type"]=="http.disconnect":print("Disconnected! ")asyncdefother_handler(path_context,scope,receive,send):context=bytes(json.dumps(path_context),encoding="utf-8")message=awaitreceive()ifmessage["type"]=="http.request":body=message.get("body",b"")awaitsend({"type":"http.response.start","status":200})awaitsend({"type":"http.response.body","body":bodyifbodyelsecontext})elifmessage["type"]=="http.disconnect":print("Disconnected! ")Finally, we can build anAsgiRouterand a working ASGI app, like this:# `AsgiRouter` and `Route` class provided by this libraryROUTER=AsgiRouter(routes=[Route("/",handler=index,methods=["GET"]),# Routes will match on regexes and bind to variables# given on the left side of the colonRoute("/other_handler/{name:[a-z1-9]+}",handler=other_handler,methods=["POST"],),])# This is a basic implementation of the ASGI spec# See: https://asgi.readthedocs.io/en/latest/specs/main.htmlasyncdefasgi_app(scope,receive,send):path=scope.get("path","")try:# Routers provider a `get_route` method# If no route is matched, they throw `UnknownEndpoint`# If a route is matched, we'll get path context and a handlerhandler,context=ROUTER.get_route(path)exceptUnknownEndpoint:awaitunknown_handler(scope,receive,send)returnNonetry:# If a matched router doesn't handle this method# it will throw `MethodNotAllowed`awaithandler(context,scope,receive,send,method=scope.get("method"))exceptMethodNotAllowed:awaitmethod_not_allowed(scope,receive,send)returnNoneasyncdefapp_with_lifespan(scope,receive,send):ifscope["type"]=="lifespan":whileTrue:message=awaitreceive()ifmessage["type"]=="lifespan.startup":awaitsend({"type":"lifespan.startup.complete"})elifmessage["type"]=="lifespan.shutdown":awaitsend({"type":"lifespan.shutdown.complete"})returnifscope["type"]=="http":returnawaitasgi_app(scope,receive,send)Finally, to run our ASGI app, we'll add the following:if__name__=="__main__":config=Config()config.bind=["localhost:8000"]trio.run(partial(serve,app_with_lifespan,config))This example relies on the following dependencies:hypercorntrioIf we have these dependencies in our Python environment, we can run this simple script:$poetryrunpythonexamples/asgi_minimal.py[2022-03-2016:59:58-0700][91988][INFO]Runningonhttp://127.0.0.1:8000(CTRL+Ctoquit)In a separate terminal, we can try it out like so:❯curlhttp://localhost:8000/
OK# No capital letters matched❯curl-XPOSThttp://localhost:8000/other_handler/bla1AA
<html><body><h1>404NotFound!</h1></body></html># GET not POST -> 405❯curlhttp://localhost:8000/other_handler/bla
<html><body><h1>405Methodnotallowed</h1></body></html># Success❯curl-XPOSThttp://localhost:8000/other_handler/bla1{"name":"bla1"}Note: that our regex pathdoes notmatch capital letters, so that request 404s.ExamplesRunnable examples are provided in theexamplesdirectory. For instance, you can run the experimentaltokamakapplication withtrioandhypercornlike so:$poetryinstall-E"full"Installingdependenciesfromlockfile

Packageoperations:8installs,0updates,0removals•Installingh11(0.13.0)•Installinghpack(4.0.0)•Installinghyperframe(6.0.1)•Installingh2(4.1.0)•Installingpriority(2.0.0)•Installingtoml(0.10.2)•Installingwsproto(1.1.0)•Installinghypercorn(0.13.2)Installingthecurrentproject:tokamak(0.2.1)❯poetryrunpythonexamples/tokamak_app.py========·°·°~>Startingtokamak°°···°°🚀···°°[2022-03-2011:05:01-0700][63023][INFO]Runningonhttp://127.0.0.1:8000(CTRL+Ctoquit)In a separate terminal, you can make various requests, such as the following:❯curlhttp://localhost:8000{"received":{}}❯curlhttp://localhost:8000/info/erik-d'{"some_data": "something"}'{"received":{"some_data":"something"}}Back in the first terminal, where you launched the exampletokamakapplication, you should see the following:❯poetryrunpythonexamples/tokamak_app.py========·°·°~>Startingtokamak°°···°°🚀···°°[2022-03-2011:05:01-0700][63023][INFO]Runningonhttp://127.0.0.1:8000(CTRL+Ctoquit){}{'type':'http','http_version':'1.1','asgi':{'spec_version':'2.1','version':'3.0'},'method':'GET','scheme':'http','path':'/','raw_path':b'/','query_string':b'','root_path':'','headers':<Headers([(b'host',b'localhost:8000'),(b'user-agent',b'curl/7.81.0'),(b'accept',b'*/*')])>,'client':('127.0.0.1',55379),'server':('127.0.0.1',8000),'extensions':{}}<Headers([(b'host',b'localhost:8000'),(b'user-agent',b'curl/7.81.0'),(b'accept',b'*/*')])>b''1.1GET
Sleeping1sfortotalseconds:0Sleeping1sfortotalseconds:1Sleeping1sfortotalseconds:2Sleeping1sfortotalseconds:3Sleeping1sfortotalseconds:4{'user':'erik'}{'type':'http','http_version':'1.1','asgi':{'spec_version':'2.1','version':'3.0'},'method':'POST','scheme':'http','path':'/info/erik','raw_path':b'/info/erik','query_string':b'','root_path':'','headers':<Headers([(b'host',b'localhost:8000'),(b'user-agent',b'curl/7.81.0'),(b'accept',b'*/*'),(b'content-length',b'26'),(b'content-type',b'application/x-www-form-urlencoded')])>,'client':('127.0.0.1',55386),'server':('127.0.0.1',8000),'extensions':{}}<Headers([(b'host',b'localhost:8000'),(b'user-agent',b'curl/7.81.0'),(b'accept',b'*/*'),(b'content-length',b'26'),(b'content-type',b'application/x-www-form-urlencoded')])>b''1.1POST
Sleeping1sfortotalseconds:0Sleeping1sfortotalseconds:5Sleeping1sfortotalseconds:1Sleeping1sfortotalseconds:6Sleeping1sfortotalseconds:2Sleeping1sfortotalseconds:7Sleeping1sfortotalseconds:3Sleeping1sfortotalseconds:8