Package: vilo
ViloVilo is a WSGI micro-framework for building web apps with Python. Inspired byExpressandBottle, Vilo is lightweight, unopinionated, and flexible.InstallationInstall Vilo via pip:pip install viloWaitressis a recommended for running Vilo apps, paired withHupper. Install via:pip install waitress hupperAlternativelyGunicornis a popular option, installable via:pip install gunicornHello, World!Createhello.py:# Define App: ##################################importvilo;# Import viloapp=vilo.buildApp();# Build (blank) app@app.route("GET","/")# Add routedefget_homepage(req,res):return"Hello, World!";wsgi=app.wsgi;# WSGI callable# Run App: #####################################if__name__=="__main__":importwaitress;waitress.serve(wsgi);Runpython hello.pyto start the app. Once the app is running,visitlocalhost:8080in your favorite browser! You should see a familiar greeting. (To stop running the app, press:Ctrl+C)From The Command LineContinuing with thehello.pyexample above, instead of callingwaitress.serve(.)from Python, you can run the app from the command line as follows:With Waitress:Using waitress-serve:waitress-serve hello:wsgiWith Hupper for dev:hupper -m waitress hello:wsgiWaitress includes thewaitress-servecommand, using which, we're asking Waitress to runwsgifrom thehellomodule. Hupper is useful during development, for hot-reloads.Kindly refer to Waitress/Hupper's docs for more.Using Gunicorn:Without hot-reloads:gunicorn hello:wsgiWith--reloadfor dev:gunicorn hello:wsgi --reloadKindly refer to Gunicorn's docs for more.Vilo vs Flask/BottleNo Magic Globals:Flask and Bottle both employ globals likerequest,gand/orresponse. Vilo instead, like Express, suppliesreqandresas arguments to each route handler.If you're building a single app, globals can be very handy. But if you're looking to build and deploy multiple (entirely isolated) apps, having to deal with automatically context-aware globals can seemeerily magical.With Vilo,reqandresarguments passed to route handlers areentirely isolated. Even with regard to a single app, each route handler receives fully isolatedreqandresobjects.No Built-In Templating:Bottle and Flask both include built-in templating engines. As an un-opinionated framework, Vilo doesn't. You may pick any templating engine you want. For illustration only, our examples will useQree(read 'Curie'), installable viapip install qree.No Built-In Development Server:Bottle and Flask include a development server with hot-reloads for local testing and development, but recommendagainstusing it in production. Instead of pre-packaging a development server, we recommend using Gunicorn with the--reloadflag, or Waitress atop Hupper.Basic Routes & Static FilesWorking with fixed routes is fairly straightforward. To handle the route"/foo/bar", just supply that path toapp.route(.)For example:# import vilo, define app etc.@app.route("GET","/foo/bar")defget_foo_bar(req,res):return"You sent a GET request to path: /foo/bar";@app.route("GET","/task/1")defget_listTasks(req,res):returnyourLogic_showTask(1);@app.route("GET","/task/2")defget_listTasks(req,res):returnyourLogic_showTask(2);@app.route("POST","/newTask")defpost_newTask(req,res):return"You sent a POST request at path: /newTask";If you have static CSS, JS, image or other files, serve them usingres.staticFile(filepath, [mimeType]). For example, let's say you have the following directory structure:- app.py
- config.py
- utils.py
- static/
        - jquery.js
        - logo.pngIn such a case, you could serve jquery and your logo as follows:# import vilo, define app etc.@app.route("GET","/static/jquery.js")defget_jquery(req,res):returnres.staticFile("./static/jquery.js");@app.route("GET","/static/logo.png")defget_bootstrap(req,res):returnres.staticFile("./static/logo.png");Optionally, you may passmimeTypetores.staticFile(.). If not passed, it'll be guessed.DRY= Don't Repeat YourselfYou would've noticed that routes/task/1and/task/2, and similarly the routes/static/jquery.jsand/static/logo.pngare essentially the same. Repeating them twice not very DRY.Right now, there seem to be just two tasks and two static files. But what if there were 10 each? 100 each? Instead of tediously repeating ourselves, we can use wildcard routes. That's next.Wildcard RoutesThe URL path has multiple slash-separatedsegments. Use*for wildcard segment matching.Exclusively at the endof the URL, using**instead of a single*will match multiple segments, including slashes therebetween.With the sole exception of**at the end of the URL, each*matches asingle but complete segment. Thus, except at the end, each*must be sandwiched between two slashes:/*/.The list of matched wildcards is available viareq.wildcards.Examples:/category/*/page/*/editWILL match/category/Food/page/Pasta/editwithreq.wildcards = ['Food', 'Pasta'].but will NOT match/category/Fo/od/page/Pasta/editasFo/odis not a single segment./cart/add-item/*will match/cart/add-item/123withreq.wildcards = ['123'].but will NOT match/cart/add-item/12/3as12/3is not a single segment./static/**will match/static/lib/js/jquery.jswithreq.wildcards = 'lib/javascript/jquery.jsbut will NOT match/StaTiC/lib/js/jquery.jsbecauseStaTiCandstaticdon't match.Now, using wildcard routes, we could write:# import vilo, define app etc.@app.route("GET","/task/*")defget_task(req,res):taskId=int(req.wildcards[0]);returnyourLogic_showTask(1)@app.route("GET","/static/**")defget_static(req,res):"Serves static files from the `static/` dir.";relpath=req.wildcards[0];returnres.staticFile("./static/"+relpath);@app.route("GET","/category/*/season/*")defget_category_season(req,res):"Serve product info by category and season."cateogry,season=req.wildcards;productsPage=doSomething(category,season);returnproductsPage;Regular Expression ModeWildcard patterns are powerful, but don't necessarily cover all use cases. For greater flexibility, you can rely on regular expressions.Let's say we want to match routes like the following:"/show-posts/from-2018-to-2020""/show-posts/from-1915-to-2015"As each*matches an entire segment, weCANNOTuse:"/show-posts/from-*-to-*"Instead, we can use the following regular expression:r"/show-posts/from-(\d+)-to-(\d+)"For using regular expression mode, passmode="re"toapp.route(.). The resultant match-object is available asreq.matched. For example:# import vilo, define app, etc.@app.route("GET",r"/show-posts/from-(\d+)-to-(\d+)",mode="re")defget_showPosts(req,res):fromYear,toYear=req.matched.groups();resultPage=yourLogic(fromYear,toYear);returnresultPage;Themodeparameter toapp.route(.):Generally speaking, there'sno needto explicitly passmode, asapp.route(.)can auto-detect it. If passed explicitly, it accepts one of three values:["re", "wildcard", "exact"]."re": regular expression matching, based onre.match(.)."wildcard":wildcard segment matching, explained above."exact": exact path matching, based on==operator.Dot-Accessible Dictionary (dotsi.Dict)Vilo usesDotsifor dot-accessible dictionaries. In fact, in all previous examples,app,reqandresare all dot-accessible dictionaries!dotsiUsage:importdotsid=dotsi.fy({"foo":"bar"})print(d.foo);# Same as d['foo']# Output: bard.baz=[{"key":"a"},{"key":"b"}]# Like d['baz'] = ..print(d.baz[0].key)# Output: aFor more examples, check out Dotsi's docs, linked above.Sidebar: Functional vs OOPThe Polydojo team strongly favours functional programming over classical OOP.As far as possible, we avoid writing classes. This is super-easy in JavaScript, especially because object properties are dot-accessible; Dotsi allows us to bring the same ease to Python.HTML Escaping &%s-FormattingUsevilo.esc(string)for escaping HTML:vilo.esc('foo')=>'foo'vilo.esc('<b> Hi </b>')=>'&lt;b&gt; Hi &lt;/b&gt;'vilo.esc('<script> xss() </script>')=>'&lt;script&gt; xss() &lt;/script&gt;'Usevilo.escfmt(string, data)for escape-wrapped,%s-based formatting. Or better yet, tryQree, our tiny but might templating engine.Working With FormsUsereq.qdatato accessquery string parameters.Usereq.fdatato access POSTedform data.POSTed multipart/form-data is also available viareq.fdata.Bothreq.qdataandreq.fdataare of typedotsi.Dict.Factorial Form Example:importvilo;app=vilo.buildApp();wsgi=app.wsgi;@app.route("GET","/")defget_homepage(req,res):return"""<form method="GET" action="/factorial"><input type="text" name="n" placeholder="Enter N:"><button>Submit</button></form>""";# Factorial helper:facto=lambdan:1ifn==0elsen*facto(n-1);@app.route("GET","/factorial")defget_factorial(req,res):n=int(req.qdata.get("n")or1);returnvilo.escfmt("""<h2>%s! =%s</h2><a href="javascript: history.back();">Back</a>""",[n,facto(n)]);Errors & RedirectsRedirects:Returnres.redirect(.)for redirecting to another URL:@app.route("GET","/foo")defredirect_from_foo_to_bar(req,res):returnres.redirect("/bar");@app.route("GET","/go/to/boardbell")defredirect_to_boardbell_dot_com(req,res):returnres.redirect("https://www.boardbell.com/");NB:Please be sure toreturn res.staticFile(.). Skippingreturnwill likely produce unintended consequences.Errors:Raisevilo.HttpError(.)to produce a non-200 response. (Or usevilo.error(.), which is a short alias for the same.)@app.route("GET","/post/*")defget_post(req,res):postId=req.wildcards[0];ifnotyourLogic_postId_found(postId):raisevilo.error("<h2>No such post.</h2>");# otherwise ...returnyourLogic_showPost(postId);vilo.error(.)takes two parameters:body(required): The response body. Similar to the return value for non-error responses. If it's adictorlist, a JSON response is produced.statusLine(optional): A status line like "404 Not Found" or "403 Forbidden"; alternatively, an integer code like 404 or 405. (Defaults to 404.)NB:Please be sure toraise vilo.error(.). Ifraiseisn't used, no error will be produced; it'll be as if no error ever occurred.Framework-Level Errors:When you explicitly raise avilo.HttpError(.), it is sent directly to the client, as described above. But sometimes, Vilo itself needs to raise errors; for example, when a route is not found. Other error-types are caught, producing a 500-response.Framework-level error-handling can be customized viaapp.frameworkError(.), which is a bit likeapp.route(.).To override the default route-not-found behavior:@app.frameworkError("route_not_found")deferror_routeNotFound(req,res,err):return"Custom Msg: That route wasn't found.";Comparingapp.frameworkError(.)withapp.route(.):Instead of passing a route, you pass a framework-error-code, such as"route_not_found". (Codes are listed below.)The handler accepts an extra parameter,err, which is simply the exception raised (or caught) by Vilo.Framework-error codes:route_not_found(illustrated above)file_not_found(with regard tores.staticFile(.))request_too_large(this is yet to be documented.)unexpected_error(more on this below)Handling Unexpected Errors:When your application produces an unexpected error, by default, Vilo produces a simple500 Internal Server Errorresponse. The default error response may be customized as follows:# Sample error producer:@app.get("/err")defget_zeroDivErr(req,res):return1/0;# Unexpected error handler:@app.frameworkError("unexpected_error")defhandle_unexpectedError(req,res,err):returnvilo.esc(repr(err));On navigating to"/err", instead of the default error message, you should see:ZeroDivisionError('division by zero')Debug Mode:In debug mode, which can be enabled viaapp.setDebug(True), the default 500-error handler includes a stack traceback. While this obviously helps with debugging, it can cause security concerns in production.Caution:Do not enable debug-mode in production.Quick PlugVilo is built and maintained by the folks atPolydojo, Inc., led bySumukh Barve. If your team is looking for a simple project management tool, please check out our latest product:BoardBell.com.HeadersSet Response Headers:Useres.setHeader(name, value)for setting a response header. (Paramsnameandvaluerespectively correspond to the name and value of the header.)Or useres.setHeaders(.)for setting multiple headers at once by passing either a dict or a list of(name, value)pairs.@app.route("GET","/foo.txt")defget_fooTxt(req,res):res.setHeaders({"Content-Type":"text/plain","Cache-Control":"no-store",});return"Here's some foo text.";Header Shortcuts:Useres.contentType = someValueinstead ofres.setHeader("Content-Type", someValue).Useres.setCookie(.)for setting cookies, instead of setting the"Set-Cookie"header. More on this below.Get Request Headers:Usereq.getHeader(name)to get a request header. (Paramnamecorresponds to the header's name.) If there's no such header,Noneis returned.@app.route("GET","/api/foo")defget_apiFoo(req,res):reqType=req.getHeader("Content-Type");ifreqType!="application/json":raisevilo.error("Only JSON requsts are valid.");# otherwise ...returnyourLogic_doFoo();Note:Usereq.getCookie(.)(documented below) for getting request cookies. No need to deal with the"Cookie"header directly.CookiesSet Response Cookies:Useres.setCookie(name, value, [secret, opt])for setting response cookies. Paramsnameandvaluerespectively correspond to the name and value of the cookie.Optional paramsecretmay be a string; and is used to sign the cookie, if passed.Optional paramoptmay be a dict of cookie options compatible with Python'shttp.cookies.Morsel.@app.route("GET","/home")defget_home(req,res):res.setCookie("visitedHome","Yes");return"<h1>You're Home!</h1>";Note:By default, Vilo setsHttpOnlycookies withPath="/". For custom behavior, passopt={"httponly": False, "path": "/custom"}. Of course, you may also pass other Morsel-compatible options.Warning:Even whensecretis passed, it is only used tosignthe cookie. The cookie isNOTencrypted. SigningDOES NOThide or obscure the cookie in any way. As such, one mustNEVERstore confidential information in cookies.Get Request Cookies:Usereq.getCookie(name, [secret])for getting request cookies. Paramnameis the cookie name whilesecretshould match the secret used while setting the cookie.If the named cookie exists (and is valid), it's value is returned, elseNone.Ifsecretis passed but the signature-check fails,Noneis returned (regardless of whether the cookie exists).@app.route("GET","/visitCounter")defget_visitCounter(req,res):count=int(req.getCookie("visitCount")or0);res.setCookie("visitCount",str(count+1))return"Number of visits:%s"%(count+1);Working With JSONVilo makes it easy to consume and produce JSON. In route handlers:JSON Requests:POSTedapplication/jsondata is available asreq.fdata.JSON Responses:Returning adictorlistproduces anapplication/jsonresponse.@app.route("GET","/hello_json")defget_sample_json(req,res):return{"hello":"json"};# Will produce JSON response, with:#	Content-Type: application/jsonPlugins (Universal Route Decorators)If you'd like certain action to be performed acrossallroutes, it's best to define a plugin for such behavior. (Plugins are just decorators that are applied to all routes.)Example:X-Exec-TimeHeaderTo add theX-Exec-Timeheader to each response, we can write a plugin that computes the execution time and adds the header.importtime;defplugin_xExecTime(fn):defwrapper(req,res,*args,**kwargs):t0=time.time();result=fn(req,res,*args,**kwargs);tDiff=time.time()-t0;res.setHeader("X-Exec-Time",str(tDiff));returnresult;returnwrapper;Then, install the plugin onto your app via:app.install(plugin_xExecTime)Example: Enforcing HTTPSdefplugin_enforceHttps(fn):defwrapper(req,res,*args,**kwargs):scheme=req.splitUrl.scheme;ifscheme!="https":newUrl=req.url.replace(scheme,"https",1);returnres.redirect(newUrl);# otherwise ...returnfn(req,res,*args,**kwargs);Then, as before, install onto you app via:app.install(plugin_enforceHttps)Plugin Application OrderIdeally, plugins should be writtenindependentlyof all other plugins. That is, each plugin should assume that other plugins may exist, and that plugins may be applied in any order.However, it may be useful to know that plugins that are installed first are applied first by Vilo. That is if youapp.install(X), then installY, and thenZ; then effectivelyZ(Y(X(.)))should be expected. That is,X(.)completes first, thenY(.), and thenZ(.).TestBin: In-Memory Pastebin AppCheck outtestbin.pya super-simple, in-memorypastebinapp. Instead of connecting to a database or file-storage system, the app uses adictfor (temporarily) storing pastes.Running:Thetestbin.pymodule is included in Vilo's GitHub repo. The app may be run as follows:gunicorn testbin:wsgi --reloadThen, head over tolocalhost:8080in your favorite browser.ViloLog: DB-Backed Blogging EngineFor a bit more in-depth example, check outViloLog: a blogging engine built atop Vilo andPogoDB.LicensingCopyright (c) 2020 Polydojo, Inc.Software Licensing:The software is released "AS IS" under theApache License 2.0, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Kindly seeLICENSE.txtfor more details.No Trademark Rights:The above software licensing termsdo notgrant any right in the trademarks, service marks, brand names or logos of Polydojo, Inc.