Package: uhttp
µHTTP - Pythonic Web DevelopmentWhyEasy: intuitive, clear logicSimple: small code base, no external dependenciesModular: application mounting, custom route behaviorFlexible: unopinionated, paradigm-freeFast: minimal overheadSafe: small attack surfaceInstallationµHTTP is onPyPI.pipinstalluhttpAlso, anASGIserver might be needed.pipinstalluvicornHello, world!fromuhttpimportApplicationapp=Application()@app.get('/')defhello(request):returnf'Hello,{request.ip}!'if__name__=='__main__':importuvicornuvicorn.run('__main__:app')DocumentationApplicationAnASGIapplication. Called once per request by the server.Application(*,routes=None,startup=None,shutdown=None,before=None,after=None,max_content=1048576)E.g.:app=Application(startup=[open_db],before=[counter,auth],routes={'/':{'GET':lambdarequest:'HI!','POST':new},'/users/':{'GET':users,'PUT':users}},after=[logger],shutdown=[close_db])Application MountingMounts another application at the specified prefix.app.mount(another,prefix='')E.g.:utils=Application()@utils.beforedefincoming(request):print('Incoming from{request.ip}')app.mount(utils)Application Lifespan (Startup)Append the decorated function to the list of functions called at the beginning of theLifespanprotocol.@app.startup[async]deffunc(state)E.g.:@app.startupasyncdefopen_db(state):state['db']=awaitaiosqlite.connect('db.sqlite')Application Lifespan (Shutdown)Appends the decorated function to the list of functions called at the end of the Lifespan protocol.@app.shutdown[async]deffunc(state)E.g.:@app.shutdownasyncdefclose_db(state):awaitstate['db'].close()Application Middleware (Before)Appends the decorated function to the list of functions called before a response is made.@app.before[async]deffunc(request)E.g.:@app.beforedefrestricted(request):user=request.state['session'].get('user')ifuser!='admin':raiseResponse(401)Application Middleware (After)Appends the decorated function to the list of functions called after a response is made.@app.after[async]deffunc(request,response)E.g.:@app.afterdeflogger(request,response):print(request,'-->',response)Application RoutingInserts the decorated function to the routing table.@app.route(path,methods=('GET',))[async]deffunc(request)Paths are compiled at startup as regular expression patterns. Named groups define path parameters.If the request path doesn't match any route pattern, a404 Not Foundresponse is returned.If the request method isn't in the route methods, a405 Method Not Allowedresponse is returned.Decorators for the standard methods are also available:@app.get(path)@app.head(path)@app.post(path)@app.put(path)@app.delete(path)@app.connect(path)@app.options(path)@app.trace(path)@app.patch(path)E.g.:@app.route('/',methods=('GET','POST'))defindex(request):returnf'{request.method}ing from{request.ip}'@app.get(r'/user/(?P<id>\d+)')defprofile(request):user=request.state['db'].get_or_404(request.params['id'])return'{user.name}has{user.friends}friends and lives in{user.location}'RequestAn HTTP request. Created every time the application is called on the HTTP protocol with a shallow copy of the state.Request(method,path,*,ip='',params=None,args=None,headers=None,cookies=None,body=b'',json=None,form=None,state=None)ResponseAn HTTP Response.Response(status,*,headers=None,cookies=None,body=b'')E.g.:@app.startupdefopen_db(state):state['db']={1:{'name':'admin','likes':['terminal','old computers']},2:{'name':'john','likes':['animals']}}defget_or_404(db,id):ifuser:=db.get(id):returnuserelse:raiseResponse(404)@app.get(r'/user/(?P<id>\d+)')defprofile(request):user=get_or_404(request.state['db'],request.params['id'])ifrequest.args.get('json'):returnuserelse:return"{user['name']}likes {', '.join(user['likes'])}"PatternsSessionsSession implementation based onJavaScript Web Signatures. Sessions are stored in the client's browser as a tamper-proof cookie. Depends onPyJWT.importosimporttimeimportjwtfromuhttpimportApplication,Responseapp=Application()secret=os.getenv('APP_SECRET','dev')@app.beforedefget_token(request):session=request.cookies.get('session')ifsessionandsession.value:try:request.state['session']=jwt.decode(jwt=session.value,key=secret,algorithms=['HS256'])exceptjwt.exceptions.PyJWTError:request.state['session']={'exp':0}raiseResponse(400)else:request.state['session']={}@app.afterdefset_token(request,response):ifsession:=request.state.get('session'):session.setdefault('exp',int(time.time())+604800)response.cookies['session']=jwt.encode(payload=session,key=secret,algorithm='HS256')response.cookies['session']['expires']=time.strftime('%a,%d%b %Y %T GMT',time.gmtime(session['exp']))response.cookies['session']['samesite']='Lax'response.cookies['session']['httponly']=Trueresponse.cookies['session']['secure']=TrueMultipart FormsSupport for multipart forms. Depends onpython-multipart.frommultipart.multipartimportFormParser,parse_options_headerfrommultipart.exceptionsimportFormParserErrorfromuhttpimportApplication,MultiDict,Responseapp=Application()defparse_form(request):form=MultiDict()defon_field(field):form[field.field_name.decode()]=field.value.decode()defon_file(file):iffile.field_name:form[file.field_name.decode()]=file.file_objectcontent_type,options=parse_options_header(request.headers.get('content-type',''))try:parser=FormParser(content_type.decode(),on_field,on_file,boundary=options.get(b'boundary'),config={'MAX_MEMORY_FILE_SIZE':float('inf')}# app._max_content)parser.write(request.body)parser.finalize()exceptFormParserError:raiseResponse(400)returnform@app.beforedefhandle_multipart(request):if'multipart/form-data'inrequest.headers.get('content-type'):request.form=parse_form(request)Static FilesStatic files for development.importosfrommimetypesimportguess_typefromuhttpimportApplication,Responseapp=Application()defsend_file(path):ifnotos.path.isfile(path):raiseRuntimeError('Invalid file')mime_type=guess_type(path)[0]or'application/octet-stream'withopen(path,'rb')asfile:content=file.read()returnResponse(status=200,headers={'content-type':mime_type},body=content)@app.get('/assets/(?P<path>.*)')defassets(request):directory='assets'path=os.path.realpath(os.path.join(directory,request.params['path']))ifos.path.commonpath([directory,path])==directory:ifos.path.isfile(path):returnsend_file(path)ifos.path.isdir(path):index=os.path.join(path,'index.html')ifos.path.isfile(index):returnsend_file(index)return404ContributingAll contributions are welcomed.LicenseReleased under the MIT license.