Package: micropython-ccrighton-phew
phew!the Pico (or Python) HTTP Endpoint WranglerA small webserver and templating library specifically designed for MicroPython
on the Pico W. It aims to provide a complete toolkit for easily creating high
quality web based interfaces for your projects.phew!is ideal for creating web based provisioning interfaces for connected projects
using theRaspberry Pi Pico W.phew!the Pico (or Python) HTTP Endpoint WranglerWhatphew!does:How to useBasic exampleRunning multiple web applicationsFunction referenceserver moduleadd_routeset_catchallrunTypesRequestResponseShorthandTemplatesrender_templateTemplate expressionsVariablesConditional displayIncludeslogging modulelog(level, text)debug(*items)info(*items)warn(*items)error(*items)set_truncate_thresholds(truncate_at, truncate_to)dns modulerun_catchallHelper functionsconnect_to_wifiaccess_pointis_connected_to_wifiget_ip_addressOther ResourcesWhatphew!does:a basic web serveroptimised for speed (atimportand during execution)minimal use of memoryparameterised routing rules/greet/<name>templating engine that allows inline python expressions{{name.lower()}}GET,POSTrequest methodsquery string decoding and parsingcatchall handler for unrouted requestsmultipart/form-data,x-www-form-urlencoded, and JSONPOSTbodiesstring, byte, or generator based responsesconnect_to_wifiandaccess_pointconvenience methodsWhere possiblephew!tries to minimise the amount of code and setup that you,
the developer, has to do in favour of picking sane defaults and hiding away bits
of minutiae that rarely needs to be tweaked.How to usephew!can be installed usingpipfrom the command line or from your favourite IDE. In Thonny this can be achieved by clickingTools->Manage packagesand searching formicropython-phew.Basic exampleAn example web server that returns a random number between 1 and 100 (or optionally
the range specified by the callee) when requested:fromphewimportserver,connect_to_wificonnect_to_wifi("<ssid>","<password>")phew_app=server.Phew()@phew_app.route("/random",methods=["GET"])defrandom_number(request):importrandommin=int(request.query.get("min",0))max=int(request.query.get("max",100))returnstr(random.randint(min,max))@phew_app.catchall()defcatchall(request):return"Not found",404phew_app.run()phewis designed specifically with performance and minimal resource use in mind.
Generally this means it will prioritise doing as little work as possible including
assuming the correctness of incoming requests.Running multiple web applicationsA device may require multiple web apps.  For instance, a setup web app for the access point
and a configuration web app for normal operation.  Phew supports the creation of many apps
with registration of routes per app. To create a new app, just create anotherserver.Phewinstance.For concurrent execute of apps, each must be configured to connect to a different port and be
run in the same uasyncio loop as tasks.importuasynciofromphewimportserverphew_app1=server.Phew()phew_app2=server.Phew()# route methods declared here for both appsloop=uasyncio.get_event_loop()phew_app1.run_as_task(loop,host="0.0.0.0",port=80)phew_app2.run_as_task(loop,host="0.0.0.0",port=8080)loop.run_forever()Function referenceserver moduleTheservermodule provides all functionality for running a web server with
route handlers.add_routeserver.add_route(path,handler,methods=["GET"])Adds a new route into the routing table. When an incoming request is received the server checks each route to find the most specific one that matches the request based on the path and method. If a route is found then thehandlerfunction is called with arequestparameter
that contains details about the request.defmy_handler(request):return"I got it!",200phew_app.add_route("/testpath",my_handler,methods=["GET"])Or, alternatively, using a decorator:@phew_app.route("/testpath",methods=["GET"])defmy_handler(request):return"I got it!",200set_catchallphew_app.set_catchall(handler)Provide a catchall method for requests that didn't match a route.defmy_catchall(request):return"No matching route",404phew_app.set_catchall(my_catchall)Or, alternatively, using a decorator:@phew_app.catchall()defmy_catchall(request):return"No matching route",404runphew_app.run(host="0.0.0.0",port=80)Starts up the web server and begins handling incoming requests.phew_app.run()TypesRequestTheRequestobject contains all of the information that was parsed out of the
incoming request including form data, query string parameters, HTTP method, path,
and more.Handler functions provided toadd_routeandset_catchallwill recieve aRequestobject as their first parameter.memberexampletypedescriptionprotocol"HTTP/1.1"stringprotocol versionmethod"GET"or"POST"stringHTTP method used for this requesturi"/path/to/page?parameter=foo"stringfull URI of the requestpath"/path/to/page"stringjust the path part of the URIquery_string"parameter=foo"stringjust the query string part of the URIform{"foo": "bar", "name": "geoff"}dictPOSTbody parsed asmultipart/form-datadata[{"name": "jenny"}, {"name": "geoff"}]anyPOSTbody parsed as JSONquery{"parameter": "foo"}dictresult of parsing the query stringAt the time your route handler is being called the request has been fully parsed and you can access any properties that are relevant to the request (e.g. theformdictionary for amultipart/form-datarequest) any irrelevant properties will be set toNone.@phew_app.route("/login",["POST"])deflogin_form(request):username=request.form.get("username",None)password=request.form.get("password",None)# check the user credentials with your own code# for example:## logged_in = authenticate_user(username, password)ifnotlogged_in:return"Username or password not recognised",401return"Logged in!",200ResponseTheResponseobject encapsulates all of the attributes of your programs response
to an incoming request. This include the status code of the result (e.g. 200 OK!)
, the data to return, and any associated headers.Handler functions can create and return aResponseobject explicitly or use a couple
of shorthand forms to avoid writing the boilerplate needed.memberexampletypedescriptionstatus200intHTTP status codeheaders{"Content-Type": "text/html"}dictdictionary of headers to returnbody"this is the response body"string or generatorthe content to be returned@phew_app.route("/greeting/<name>",["GET"])defuser_details(request):returnResponse(f"Hello,{name}",status=200,{"Content-Type":"text/html"})ShorthandAs shorthand instead of returning aResponseobject the handle may also return atuplewith between
one and three values:body - either a string or generator methodstatus code - defaults to200if not providedheaders - defaults to{"Content-Type": "text/html"}if not providedFor example:@phew_app.route("/greeting/<name>",["GET"])defuser_details(request,name):returnf"Hello,{name}",200TemplatesA web server isn't much use without something to serve. While it's straightforward
to serve the contents of a file or some generated JSON things get more complicated
when we want to present a dynamically generated web page to the user.phew!provides a templating engine which allows you to write normal HTML with
fragments of Python code embedded to output variable values, parse input, or dynamically
load assets.render_templaterender_template(template,param1="foo",param2="bar",...):Therender_templatemethod takes a path to a template file on the filesystem and
a list of named paramaters which will be passed into the template when parsing.The method is a generator which yields the parsing result in chunks, minimising the
amount of memory used to hold the results as they can be streamed directly out rather
than having to build the entire result as a string first.Generally you will callrender_templateto create the body of aResponsein one
of your handler methods.Template expressionsTemplates are not much use if you can't inject dynamic data into them. Withphew!you can embed Python expressions with{{<expression here>}}which will be evaluated
during parsing.VariablesIn the simplest form you can embed a simple value by just enclosing it in double curly braces.
It's also possible to perform more complicated transformations using any built in Python method.<divid="name">{{name}}</div><divid="name">{{name.upper()}}</div><divid="name">{{"/".join(name.split(" "))}}</div>ListsIt is possible to perform operations on lists using str.join and list comprehension as in the following table example.<table>{{"".join([f"<tr><td>item</td></tr>\r\n" for item in mylist])}}</table>Conditional displayIf you want to show a value only if some other condition is met then you can use the
(slightly clunky) Python tenary operator.<div>You won
  {{"1st" if prize == 1 else ""}}
  {{"2nd" if prize == 2 else ""}}
  {{"3rd" if prize == 3 else ""}}
  prize!</div>or<div>You won
  {{["1st", "2nd", "3rd"][prize]}}
  prize!</div>While a bit unwieldy this methods works. An alternative would be to select the appropriate
value in your handler and simply pass it into the template as a parameter however that
would mean having some of your copy embedded into your Python code rather than all of it
in one place in the template file.IncludesYou can include another template by callingrender_template()again within your outer template.include.htmlHello there {{name}}!main.html<!DOCTYPE html><body>{{render_template("include.html", name=name)}}</body>:warning: Note: you need to explicitly pass through template parameters into the included template - they are not available by default.logging modulelog(level, text)Add a new entry into the log file.log("info","> i'd like to take a minute, just sit right there")log("error","> the license plate said 'Fresh' and it had dice in the mirror")The entry will automatically have the current date and time, thelevelvalue, and the amount of free memory in kB prepended:2022-09-04 15:29:20 [debug    / 110kB] > performing startup
2022-09-04 15:30:42 [info     / 113kB]   - wake reason: rtc_alarm
2022-09-04 15:30:42 [debug    / 112kB]   - turn on activity led
2022-09-04 15:30:43 [info     / 102kB] > running pump 1 for 0.4 second
2022-09-04 15:30:46 [info     / 110kB] > 5 cache files need uploading
2022-09-04 15:30:46 [info     / 107kB] > connecting to wifi network 'yourssid'
2022-09-04 15:30:48 [debug    / 100kB]   - connecting
2022-09-04 15:30:51 [info     /  87kB]   - ip address:  192.168.x.x
2022-09-04 15:30:57 [info     /  79kB]   - uploaded 2022-09-04T15:19:03Z.json 2022-09-04 15:31:01 [info     /  82kB]   - uploaded 2022-09-04T15:28:17Z.json 2022-09-04 15:31:06 [info     /  88kB]   - uploaded 2022-09-04T15:30:43Z.json 2022-09-04 15:31:11 [info     /  95kB]   - uploaded 2022-09-04T15:29:00Z.json 2022-09-04 15:31:16 [info     / 100kB]   - uploaded 2022-09-04T15:29:21Z.json 2022-09-04 15:31:16 [info     /  98kB] > going to sleepdebug(*items)Shorthand method for writing debug messages to the log.warn("> this is a story")info(*items)Shorthand method for writing information to the log.num=123info("> all about how",num,time.time())warn(*items)Shorthand method for writing warnings to the log.warn("> my life got flipped")error(*items)Shorthand method for writing errors to the log.warn("> turned upside down")set_truncate_thresholds(truncate_at, truncate_to)Will automatically truncate the log file totruncate_tobytes long when it reachestruncate_atbytes in length.# automatically truncate when we're closed to the# filesystem block size to keep to a single blockset_truncate_thresholds(3.5*1024,2*1.024)Truncation always happens on the nearest line ending boundary so the truncated file may not exactly match the size specified.dns moduleTo make implementing device provisioning interfaces (via captive portal) simplephew!provides a catchall DNS server.If you put the Pico W into access point mode and then run the catchall DNS server it will route all DNS requests back to the local device so that they can be handled.run_catchalldns.run_catchall(ip_address)Pass in the IP address of your device once in access point mode.Helper functionsconnect_to_wificonnect_to_wifi(ssid,password,timeout=30)Connects to the network specified byssidwith the provided password.Returns the device IP address on success orNoneon failure.access_pointaccess_point(ssid,password=None)Create an access point with the specified SSID. Optionally password protected if provided.is_connected_to_wifiresult=is_connected_to_wifi()ReturnsTrueif there is an active WiFi connection.get_ip_addressget_ip_address()Returns the current IP address if connected to a network or acting as an access point orNoneotherwise.Other ResourcesHere are some Phew! community projects and guides that you might find useful. Note that code at the links below has not been tested by us and we're not able to offer support with it.:link:Hacking Big Mouth Billy Bass:link:How to set up a Phew! Access Point:link:Wireless Networking Setup Example for Raspberry Pi Pico W