Repository: pfalcon/picoweb Branch: master Commit: b74428ebdde9 Files: 27 Total size: 44.0 KB Directory structure: gitextract_h09r58tg/ ├── LICENSE ├── Makefile ├── README.rst ├── example_webapp.py ├── example_webapp2.py ├── examples/ │ ├── README.md │ ├── app1.py │ ├── app2.py │ ├── example_app_router.py │ ├── example_basic_auth.py │ ├── example_basic_auth_deco.py │ ├── example_eventsource.py │ ├── example_eventsource_push.py │ ├── example_extra_headers.py │ ├── example_form.py │ ├── example_global_exc.py │ ├── example_header_modes.py │ ├── example_img.py │ ├── example_unicode.py │ ├── static/ │ │ └── style.css │ └── templates/ │ └── unicode.tpl ├── picoweb/ │ ├── __init__.py │ └── utils.py ├── requirements-cpython.txt ├── sdist_upip.py ├── setup.py └── templates/ └── squares.tpl ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014-2018 Paul Sokolovsky and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ PREFIX = ~/.micropython/lib all: install: cp -a picoweb $(PREFIX) ================================================ FILE: README.rst ================================================ picoweb ======= picoweb is a "micro" web micro-framework (thus, "pico-framework") for radically unbloated web applications using radically unbloated Python implementation, Pycopy, https://github.com/pfalcon/pycopy . Features: * Asynchronous from the start, using unbloated asyncio-like library for Pycopy (`uasyncio `_). This means that ``picoweb`` can process multiple concurrent requests at the same time (using I/O and/or CPU multiplexing). * Small memory usage. Initial version required about 64K of heap for a trivial web app, and since then, it was optimized to allow run more or less realistic web app in ~36K of heap. More optimizations on all the levels (Pycopy and up) are planned (but may lead to API changes). * Has API affinity with some well-known Python web micro-framework(s), thus it should be an easy start if you have experience with them, and existing applications can be potentially ported, instead of requiring complete rewrite. Requirements and optional modules --------------------------------- ``picoweb`` depends on ``uasyncio`` for asynchronous networking (https://github.com/pfalcon/pycopy-lib/tree/master/uasyncio). ``uasyncio`` itself requires `Pycopy `, a minimalist, lightweight, and resource-efficient Python language implementation. It is also indended to be used with ``utemplate`` (https://github.com/pfalcon/utemplate) for templating, but this is a "soft" dependency - picoweb offers convenience functions to use ``utemplate`` templates, but if you don't use them or will handle templating in your app (e.g. with a different library), it won't be imported. For database access, there are following options (``picoweb`` does not depend on any of them, up to your application to choose): * `btree `_ builtin Pycopy module. This is a recommended way to do a database storage for `picoweb`, as it allows portability across all Pycopy targets, starting with very memory- and storage-limited baremetal systems. * ``btreedb`` wrapper on top of ``btree`` builtin module. This may add some overhead, but may allow to make an application portable between different database backends (`filedb` and `uorm` below). https://github.com/pfalcon/pycopy-btreedb * ``filedb``, for a simple database using files in a filesystem https://github.com/pfalcon/filedb * ``uorm``, for Sqlite3 database access (works only with Pycopy Unix port) https://github.com/pfalcon/uorm Last but not least, ``picoweb`` uses a standard ``logging``-compatible logger for diagnostic output (like a connection opened, errors and debug information). However this output is optional, and otherwise you can use a custom logging class instead of the standard ``logging``/``ulogging`` module. Due to this, and to not put additional dependencies burden on the small webapps for small systems, ``logging`` module is not included in ``picoweb``'s installation dependencies. Instead, a particular app using ``picoweb`` should depend on ``pycopy-ulogging`` or ``pycopy-logging`` package. Note that to disable use of logging, an application should start up using ``WebApp.run(debug=-1)``. The default value for ``debug`` parameter is 0 however, in which case picoweb will use ``ulogging`` module (on which your application needs to depend, again). Details ------- picoweb API is roughly based on APIs of other well-known Python web frameworks. The strongest affinity is Flask, http://flask.pocoo.org, as arguably the most popular micro-framework. Some features are also based on Bottle and Django. Note that this does not mean particular "compatibility" with Flask, Bottle, or Django: most existing web frameworks are synchronous (and threaded), while picoweb is async framework, so its architecture is quite different. However, there is an aim to save porting efforts from repetitive search & replace trials: for example, when methods do similar things, they are likely named the same (but they may take slightly different parameters, return different values, and behave slightly differently). The biggest difference is async, non-threaded nature of picoweb. That means that the same code may handle multiple requests at the same time, but unlike threaded environment, there's no external context (like thread and thread local storage) to associate with each request. Thus, there're no "global" (or thread-local "global") request and response objects, like Flask, Bottle, Django have. Instead, all picoweb functions explicitly pass the current request and response objects around. Also, picoweb, being unbloated framework, tries to avoid avoidable abstractions. For example, HTTP at the lowest level has just read and write endpoints of a socket. To dispatch request, picoweb needs to pre-parse some request data from input stream, and it saves that partially (sic!) parsed data as a "request" object, and that's what passed to application handlers. However, there's no unavoidable need to have a "response" abstraction - the most efficient/lightweight application may want to just write raw HTTP status line, headers, and body to the socket. Thus, raw write stream is passed to application handlers as the "response" object. (But high-level convenience functions to construct an HTTP response are provided). API reference ------------- The best API reference currently are examples (see below) and the ``picoweb`` source code itself. It's under 10K, so enjoy: https://github.com/pfalcon/picoweb/blob/master/picoweb/__init__.py Note that API is experimental and may undergo changes. Examples -------- * `example_webapp.py `_ - A simple webapp showing you how to generate a complete HTTP response yourself, use ``picoweb`` convenience functions for HTTP headers generation, and use of templates. Mapping from URLs to webapp view functions ("web routes" or just "routes") is done Django-style, using a centralized route list. * `example_webapp2.py `_ - Like above, but uses ``app.route()`` decorator for route specification, Flask-style. * `examples/ `_ - Additional examples for various features of picoweb. See comments in each file for additional info. To run examples in this directory, you normally would need to have picoweb installed (i.e. available in your ``MICROPYPATH``, which defaults to ``~/.micropython/lib/``). * `notes-pico `_ - A more realistic example webapp, ported from the Flask original. Running under CPython (regressed) --------------------------------- Initial versions of picoweb could run under CPython, but later it was further optimized for Pycopy, and ability to run under CPython regressed. It's still on TODO to fix it, instructions below tell how it used to work. At least CPython 3.4.2 is required (for asyncio loop.create_task() support). To run under CPython, uasyncio compatibility module for CPython is required (pycopy-cpython-uasyncio). This and other dependencies can be installed using requirements-cpython.txt:: pip install -r requirements-cpython.txt Reporting Issues ---------------- Here are a few guidelines to make feedback more productive: 1. Please be considerate of the overall best practices and common pitfalls in reporting issues, this document gives a good overview: `How to Report Bugs Effectively `_. 2. The reference platform for ``picoweb`` is the Unix port of Pycopy. All issues reported must be validated against this version, to differentiate issues of ``picoweb``/``uasyncio`` from the issues of your underlying platform. 3. All reports must include version information of all components involved: Pycopy, picoweb, uasyncio, uasyncio.core, any additional modules. Generally, only the latest versions of the above are supported (this is what you get when you install/reinstall components using the ``upip`` package manager). The version information are thus first of all important for yourself, the issue reporter, it allows you to double-check if you're using an outdated or unsupported component. 4. Feature requests: ``picoweb`` is by definition a pico-framework, and bound to stay so. Feature requests are welcome, but please be considerate that they may be outside the scope of core project. There's an easy way out though: instead of putting more stuff *into* ``picoweb``, build new things *on top* of it: via plugins, subclassing, additional modules etc. That's how it was intended to be from the beginning! 5. We would like to establish a dedicated QA team to support users of this project better. If you would like to sponsor this effort, please let us know. ================================================ FILE: example_webapp.py ================================================ # # This is a picoweb example showing a centralized web page route # specification (classical Django style). # import ure as re import picoweb def index(req, resp): # You can construct an HTTP response completely yourself, having # a full control of headers sent... yield from resp.awrite("HTTP/1.0 200 OK\r\n") yield from resp.awrite("Content-Type: text/html\r\n") yield from resp.awrite("\r\n") yield from resp.awrite("I can show you a table of squares.
") yield from resp.awrite("Or my source.") def squares(req, resp): # Or can use a convenience function start_response() (see its source for # extra params it takes). yield from picoweb.start_response(resp) yield from app.render_template(resp, "squares.tpl", (req,)) def hello(req, resp): yield from picoweb.start_response(resp) # Here's how you extract matched groups from a regex URI match yield from resp.awrite("Hello " + req.url_match.group(1)) ROUTES = [ # You can specify exact URI string matches... ("/", index), ("/squares", squares), ("/file", lambda req, resp: (yield from app.sendfile(resp, "example_webapp.py"))), # ... or match using a regex, the match result available as req.url_match # for match group extraction in your view. (re.compile("^/iam/(.+)"), hello), ] import ulogging as logging logging.basicConfig(level=logging.INFO) #logging.basicConfig(level=logging.DEBUG) app = picoweb.WebApp(__name__, ROUTES) # debug values: # -1 disable all logging # 0 (False) normal logging: requests and errors # 1 (True) debug logging # 2 extra debug logging app.run(debug=1) ================================================ FILE: example_webapp2.py ================================================ # # This is a picoweb example showing a web page route # specification using view decorators (Flask style). # import picoweb app = picoweb.WebApp(__name__) @app.route("/") def index(req, resp): yield from picoweb.start_response(resp) yield from resp.awrite("I can show you a table of squares.") @app.route("/squares") def squares(req, resp): yield from picoweb.start_response(resp) yield from app.render_template(resp, "squares.tpl", (req,)) import ulogging as logging logging.basicConfig(level=logging.INFO) app.run(debug=True) ================================================ FILE: examples/README.md ================================================ More picoweb examples ===================== This directory contains additional examples (beyond a couple available in the top-level directory) of picoweb usage. These examples are intended to serve as learn-by-example material, showing various features and usage patterns of picoweb. Each example starts with a comment header describing what the example does. To run these examples, you normally need picoweb already installed (i.e. available in your MICROPYPATH). If you want a quick start, you need to try a couple of examples in the top-level dir mentioned above - those can be run directly from the git checkout. (And as a final hint, you can also copy any example to the top-level dir and run it from there too). ================================================ FILE: examples/app1.py ================================================ # # This is an example of a (sub)application, which can be made a part of # bigger site using "app mount" feature, see example_app_router.py. # import picoweb app = picoweb.WebApp(__name__) @app.route("/") def index(req, resp): yield from picoweb.start_response(resp) yield from resp.awrite("This is webapp #1") if __name__ == "__main__": app.run(debug=True) ================================================ FILE: examples/app2.py ================================================ # # This is an example of a (sub)application, which can be made a part of # bigger site using "app mount" feature, see example_app_router.py. # import picoweb app = picoweb.WebApp(__name__) @app.route("/") def index(req, resp): yield from picoweb.start_response(resp) yield from resp.awrite("This is webapp #2") if __name__ == "__main__": app.run(debug=True) ================================================ FILE: examples/example_app_router.py ================================================ # # This is an example of running several sub-applications in one bigger # application, by "mounting" them under specific URLs. # import picoweb import app1, app2 site = picoweb.WebApp(__name__) site.mount("/app1", app1.app) site.mount("/app2", app2.app) @site.route("/") def index(req, resp): yield from picoweb.start_response(resp) yield from resp.awrite("app1 or app2") site.run(debug=True) ================================================ FILE: examples/example_basic_auth.py ================================================ # # This is a picoweb example showing handling of HTTP Basic authentication. # import ubinascii import picoweb app = picoweb.WebApp(__name__) @app.route("/") def index(req, resp): if b"Authorization" not in req.headers: yield from resp.awrite( 'HTTP/1.0 401 NA\r\n' 'WWW-Authenticate: Basic realm="Picoweb Realm"\r\n' '\r\n' ) return auth = req.headers[b"Authorization"].split(None, 1)[1] auth = ubinascii.a2b_base64(auth).decode() username, passwd = auth.split(":", 1) yield from picoweb.start_response(resp) yield from resp.awrite("You logged in with username: %s, password: %s" % (username, passwd)) import ulogging as logging logging.basicConfig(level=logging.INFO) app.run(debug=True) ================================================ FILE: examples/example_basic_auth_deco.py ================================================ # # This is a picoweb example showing handling of HTTP Basic authentication # using a decorator. Note: using decorator is cute, bit isn't the most # memory-efficient way. Prefer calling functions directly if you develop # for memory-constrained device. # import ubinascii import picoweb app = picoweb.WebApp(__name__) def require_auth(func): def auth(req, resp): auth = req.headers.get(b"Authorization") if not auth: yield from resp.awrite( 'HTTP/1.0 401 NA\r\n' 'WWW-Authenticate: Basic realm="Picoweb Realm"\r\n' '\r\n' ) return auth = auth.split(None, 1)[1] auth = ubinascii.a2b_base64(auth).decode() req.username, req.passwd = auth.split(":", 1) yield from func(req, resp) return auth @app.route("/") @require_auth def index(req, resp): yield from picoweb.start_response(resp) yield from resp.awrite("You logged in with username: %s, password: %s" % (req.username, req.passwd)) import ulogging as logging logging.basicConfig(level=logging.INFO) app.run(debug=True) ================================================ FILE: examples/example_eventsource.py ================================================ # # This is a picoweb example showing a Server Side Events (SSE) aka # EventSource handling. Each connecting client gets its own events, # independent from other connected clients. # import uasyncio import picoweb def index(req, resp): yield from picoweb.start_response(resp) yield from resp.awrite("""\
""") def events(req, resp): print("Event source connected") yield from resp.awrite("HTTP/1.0 200 OK\r\n") yield from resp.awrite("Content-Type: text/event-stream\r\n") yield from resp.awrite("\r\n") i = 0 try: while True: yield from resp.awrite("data: %d\n\n" % i) yield from uasyncio.sleep(1) i += 1 except OSError: print("Event source connection closed") yield from resp.aclose() ROUTES = [ ("/", index), ("/events", events), ] import ulogging as logging logging.basicConfig(level=logging.INFO) #logging.basicConfig(level=logging.DEBUG) app = picoweb.WebApp(__name__, ROUTES) app.run(debug=True) ================================================ FILE: examples/example_eventsource_push.py ================================================ # # This is a picoweb example showing a Server Side Events (SSE) aka # EventSource handling. All connecting clients get the same events. # This is achieved by running a "background service" (a coroutine) # and "pushing" the same event to each connected client. # import uasyncio import picoweb event_sinks = set() # # Webapp part # def index(req, resp): yield from picoweb.start_response(resp) yield from resp.awrite("""\
""") def events(req, resp): global event_sinks print("Event source %r connected" % resp) yield from resp.awrite("HTTP/1.0 200 OK\r\n") yield from resp.awrite("Content-Type: text/event-stream\r\n") yield from resp.awrite("\r\n") event_sinks.add(resp) return False ROUTES = [ ("/", index), ("/events", events), ] # # Background service part # def push_event(ev): global event_sinks to_del = set() for resp in event_sinks: try: await resp.awrite("data: %s\n\n" % ev) except OSError as e: print("Event source %r disconnected (%r)" % (resp, e)) await resp.aclose() # Can't remove item from set while iterating, have to have # second pass for that (not very efficient). to_del.add(resp) for resp in to_del: event_sinks.remove(resp) def push_count(): i = 0 while 1: await push_event("%s" % i) i += 1 await uasyncio.sleep(1) import ulogging as logging logging.basicConfig(level=logging.INFO) #logging.basicConfig(level=logging.DEBUG) loop = uasyncio.get_event_loop() loop.create_task(push_count()) app = picoweb.WebApp(__name__, ROUTES) app.run(debug=True) ================================================ FILE: examples/example_extra_headers.py ================================================ # # This is a picoweb example showing the usage of # extra headers in responses. # import picoweb import ure as re app = picoweb.WebApp(__name__) # Shows sending extra headers specified as a dictionary. @app.route("/") def index(req, resp): headers = {"X-MyHeader1": "foo", "X-MyHeader2": "bar"} # Passing headers as a positional param is more efficient, # but we pass by keyword here ;-) yield from picoweb.start_response(resp, headers=headers) yield from resp.awrite(b"""\

The style.css should be cached and might be encoded.

Check out your webdev tool!

""") # Send gzipped content if supported by client. # Shows specifying headers as a flat binary string - # more efficient if such headers are static. @app.route(re.compile('^\/(.+\.css)$')) def styles(req, resp): file_path = req.url_match.group(1) headers = b"Cache-Control: max-age=86400\r\n" if b"gzip" in req.headers.get(b"Accept-Encoding", b""): file_path += ".gz" headers += b"Content-Encoding: gzip\r\n" print("sending " + file_path) yield from app.sendfile(resp, "static/" + file_path, "text/css", headers) import ulogging as logging logging.basicConfig(level=logging.INFO) app.run(debug=True) ================================================ FILE: examples/example_form.py ================================================ # # This is a picoweb example showing how to handle form data. # import picoweb app = picoweb.WebApp(__name__) @app.route("/form_url") def index(req, resp): if req.method == "POST": yield from req.read_form_data() else: # GET, apparently # Note: parse_qs() is not a coroutine, but a normal function. # But you can call it using yield from too. req.parse_qs() # Whether form data comes from GET or POST request, once parsed, # it's available as req.form dictionary yield from picoweb.start_response(resp) yield from resp.awrite("Hello %s!" % req.form["name"]) @app.route("/") def index(req, resp): yield from picoweb.start_response(resp) yield from resp.awrite("POST form:
") yield from resp.awrite("
" "What is your name? " "
") yield from resp.awrite("GET form:
") # GET is the default yield from resp.awrite("
" "What is your name? " "
") import ulogging as logging logging.basicConfig(level=logging.INFO) app.run(debug=True) ================================================ FILE: examples/example_global_exc.py ================================================ # # This is a picoweb example showing a how to "globally" handle exceptions # during request processing. Note that you can always handle # exceptions in a particular request using normal try/except/finally. # That's actually the recommended way. Of course, if you have a # webapp with many request handlers, that becomes less practical # and global exception handler may make sense. A common action of # global handler would be to send a "500" page, but mind the cuprit # shown below. # import picoweb class ExcWebApp(picoweb.WebApp): async def handle_exc(self, req, resp, exc): try: # Do you already see a problem - what happens if your action # already started output before exception happened? Resolving # that issue is wholy up to your webapp, picoweb doesn't limit # you to any particular method, use whatever suits you better! await picoweb.start_response(resp, status="500") await resp.awrite("We've got 500, cap!") except Exception as e: # Per API contract, handle_exc() must not raise exceptions # (unless we want the whole webapp to terminate). print(repr(e)) app = ExcWebApp(__name__) @app.route("/") def index(req, resp): yield from picoweb.start_response(resp) yield from resp.awrite( "good exception case " "less good exception case" ) @app.route("/case1") def case1(req, resp): 1/0 @app.route("/case2") def case2(req, resp): yield from picoweb.start_response(resp) yield from resp.awrite( "Here, I started to write something to response, and suddenly..." ) 1/0 import ulogging as logging logging.basicConfig(level=logging.INFO) app.run(debug=True) ================================================ FILE: examples/example_header_modes.py ================================================ # # This is a picoweb example showing various header parsing modes. # import ure as re import picoweb def index(req, resp): yield from resp.awrite("HTTP/1.0 200 OK\r\n") yield from resp.awrite("Content-Type: text/html\r\n") yield from resp.awrite("\r\n") yield from resp.awrite('
  • header_mode="parse"') yield from resp.awrite('
  • header_mode="skip"') yield from resp.awrite('
  • header_mode="leave"') def headers_parse(req, resp): yield from picoweb.start_response(resp) yield from resp.awrite("") for h, v in req.headers.items(): yield from resp.awrite("\r\n" % (h, v)) yield from resp.awrite("
    %s%s
    ") def headers_skip(req, resp): yield from picoweb.start_response(resp) assert not hasattr(req, "headers") yield from resp.awrite("No req.headers.") def headers_leave(req, resp): yield from picoweb.start_response(resp) assert not hasattr(req, "headers") yield from resp.awrite("Reading headers directly from input request:") yield from resp.awrite("
    ")
        while True:
            l = yield from req.reader.readline()
            if l == b"\r\n":
                break
            yield from resp.awrite(l)
        yield from resp.awrite("
    ") ROUTES = [ ("/", index), ("/mode_parse", headers_parse, {"headers": "parse"}), ("/mode_skip", headers_skip, {"headers": "skip"}), ("/mode_leave", headers_leave, {"headers": "leave"}), ] import ulogging as logging logging.basicConfig(level=logging.INFO) #logging.basicConfig(level=logging.DEBUG) app = picoweb.WebApp(__name__, ROUTES) # You could set the default header parsing mode here like this: # app.headers_mode = "skip" app.run(debug=True) ================================================ FILE: examples/example_img.py ================================================ # # This is a picoweb example showing how to serve images - both static # image from webapp's static/ dir, and dynamically-generated image. # import picoweb app = picoweb.WebApp(__name__) @app.route("/") def index(req, resp): yield from picoweb.start_response(resp) yield from resp.awrite(b"Static image:
    ") yield from resp.awrite(b"Dynamic image:
    ") @app.route("/dyna-logo.png") def squares(req, resp): yield from picoweb.start_response(resp, "image/png") yield from resp.awrite( b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x000\x00\x00\x000\x01\x03\x00\x00\x00m\xcck" b"\xc4\x00\x00\x00\x06PLTE\xff\xff\xff\x00\x00\x00U\xc2\xd3~\x00\x00\x00\xd2IDAT\x18\xd3c`" b"\x80\x01\xc6\x06\x08-\x00\xe1u\x80)\xa6\x040\xc5\x12\x00\xa18\xc0\x14+\x84\xe2\xf5\x00Sr" b"\x10J~\x0e\x98\xb2\xff\x83\x85\xb2\x86P\xd2\x15\x10\x95\x10\x8a\xff\x07\x98b\xff\x80L\xb1" b"\xa5\x83-?\x95\xff\x00$\xf6\xeb\x7f\x01\xc8\x9eo\x7f@\x92r)\x9fA\x94\xfc\xc4\xf3/\x80\x94" b"\xf8\xdb\xff'@F\x1e\xfcg\x01\xa4\xac\x1e^\xaa\x01R6\xb1\x8f\xff\x01);\xc7\xff \xca\xfe\xe1" b"\xff_@\xea\xff\xa7\xff\x9f\x81F\xfe\xfe\x932\xbd\x81\x81\xb16\xf0\xa4\x1d\xd0\xa3\xf3\xfb" b"\xba\x7f\x02\x05\x97\xff\xff\xff\x14(\x98\xf9\xff\xff\xb4\x06\x06\xa6\xa8\xfa\x7fQ\x0e\x0c" b"\x0c\xd3\xe6\xff\xcc\x04\xeaS]\xfet\t\x90\xe2\xcc\x9c6\x01\x14\x10Q )\x06\x86\xe9/\xc1\xee" b"T]\x02\xa68\x04\x18\xd0\x00\x00\xcb4H\xa2\x8c\xbd\xc0j\x00\x00\x00\x00IEND\xaeB`\x82" ) import ulogging as logging logging.basicConfig(level=logging.INFO) app.run(debug=True) ================================================ FILE: examples/example_unicode.py ================================================ # # This is a picoweb example showing rendering of template # with Unicode (UTF-8) characters. # import picoweb app = picoweb.WebApp(__name__) @app.route("/") def index(req, resp): yield from picoweb.start_response(resp) data = {"chars": "абвгд", "var1": "α", "var2": "β", "var3": "γ"} yield from app.render_template(resp, "unicode.tpl", (data,)) import ulogging as logging logging.basicConfig(level=logging.INFO) app.run(debug=True) ================================================ FILE: examples/static/style.css ================================================ .green { color: darkgreen; } ================================================ FILE: examples/templates/unicode.tpl ================================================ {% args d %} Some Cyrillic characters: {{d["chars"]}}
    Some Greek characters: {{d["var1"]}} = {{d["var2"]}} + {{d["var3"]}}
    ================================================ FILE: picoweb/__init__.py ================================================ # Picoweb web pico-framework for Pycopy, https://github.com/pfalcon/pycopy # Copyright (c) 2014-2020 Paul Sokolovsky # SPDX-License-Identifier: MIT import sys import gc import micropython import utime import uio import ure as re import uerrno import uasyncio as asyncio import pkg_resources from .utils import parse_qs SEND_BUFSZ = 128 def get_mime_type(fname): # Provide minimal detection of important file # types to keep browsers happy if fname.endswith(".html"): return "text/html" if fname.endswith(".css"): return "text/css" if fname.endswith(".png") or fname.endswith(".jpg"): return "image" return "text/plain" def sendstream(writer, f): buf = bytearray(SEND_BUFSZ) while True: l = f.readinto(buf) if not l: break yield from writer.awrite(buf, 0, l) def jsonify(writer, dict): import ujson yield from start_response(writer, "application/json") yield from writer.awrite(ujson.dumps(dict)) def start_response(writer, content_type="text/html; charset=utf-8", status="200", headers=None): yield from writer.awrite("HTTP/1.0 %s NA\r\n" % status) yield from writer.awrite("Content-Type: ") yield from writer.awrite(content_type) if not headers: yield from writer.awrite("\r\n\r\n") return yield from writer.awrite("\r\n") if isinstance(headers, bytes) or isinstance(headers, str): yield from writer.awrite(headers) else: for k, v in headers.items(): yield from writer.awrite(k) yield from writer.awrite(": ") yield from writer.awrite(v) yield from writer.awrite("\r\n") yield from writer.awrite("\r\n") def http_error(writer, status): yield from start_response(writer, status=status) yield from writer.awrite(status) class HTTPRequest: def __init__(self): pass def read_form_data(self): size = int(self.headers[b"Content-Length"]) data = yield from self.reader.readexactly(size) form = parse_qs(data.decode()) self.form = form def parse_qs(self): form = parse_qs(self.qs) self.form = form class WebApp: def __init__(self, pkg, routes=None, serve_static=True): if routes: self.url_map = routes else: self.url_map = [] if pkg and pkg != "__main__": self.pkg = pkg.split(".", 1)[0] else: self.pkg = None if serve_static: self.url_map.append((re.compile("^/(static/.+)"), self.handle_static)) self.mounts = [] self.inited = False # Instantiated lazily self.template_loader = None self.headers_mode = "parse" def parse_headers(self, reader): headers = {} while True: l = yield from reader.readline() if l == b"\r\n": break k, v = l.split(b":", 1) headers[k] = v.strip() return headers def _handle(self, reader, writer): if self.debug > 1: micropython.mem_info() close = True req = None try: request_line = yield from reader.readline() if request_line == b"": if self.debug >= 0: self.log.error("%s: EOF on request start" % reader) yield from writer.aclose() return req = HTTPRequest() # TODO: bytes vs str request_line = request_line.decode() method, path, proto = request_line.split() if self.debug >= 0: self.log.info('%.3f %s %s "%s %s"' % (utime.time(), req, writer, method, path)) path = path.split("?", 1) qs = "" if len(path) > 1: qs = path[1] path = path[0] #print("================") #print(req, writer) #print(req, (method, path, qs, proto), req.headers) # Find which mounted subapp (if any) should handle this request app = self while True: found = False for subapp in app.mounts: root = subapp.url #print(path, "vs", root) if path[:len(root)] == root: app = subapp found = True path = path[len(root):] if not path.startswith("/"): path = "/" + path break if not found: break # We initialize apps on demand, when they really get requests if not app.inited: app.init() # Find handler to serve this request in app's url_map found = False for e in app.url_map: pattern = e[0] handler = e[1] extra = {} if len(e) > 2: extra = e[2] if path == pattern: found = True break elif not isinstance(pattern, str): # Anything which is non-string assumed to be a ducktype # pattern matcher, whose .match() method is called. (Note: # Django uses .search() instead, but .match() is more # efficient and we're not exactly compatible with Django # URL matching anyway.) m = pattern.match(path) if m: req.url_match = m found = True break if not found: headers_mode = "skip" else: headers_mode = extra.get("headers", self.headers_mode) if headers_mode == "skip": while True: l = yield from reader.readline() if l == b"\r\n": break elif headers_mode == "parse": req.headers = yield from self.parse_headers(reader) else: assert headers_mode == "leave" if found: req.method = method req.path = path req.qs = qs req.reader = reader close = yield from handler(req, writer) else: yield from start_response(writer, status="404") yield from writer.awrite("404\r\n") #print(req, "After response write") except Exception as e: if self.debug >= 0: self.log.exc(e, "%.3f %s %s %r" % (utime.time(), req, writer, e)) yield from self.handle_exc(req, writer, e) if close is not False: yield from writer.aclose() if __debug__ and self.debug > 1: self.log.debug("%.3f %s Finished processing request", utime.time(), req) def handle_exc(self, req, resp, e): # Can be overriden by subclasses. req may be not (fully) initialized. # resp may already have (partial) content written. # NOTE: It's your responsibility to not throw exceptions out of # handle_exc(). If exception is thrown, it will be propagated, and # your webapp will terminate. # This method is a coroutine. return yield def mount(self, url, app): "Mount a sub-app at the url of current app." # Inspired by Bottle. It might seem that dispatching to # subapps would rather be handled by normal routes, but # arguably, that's less efficient. Taking into account # that paradigmatically there's difference between handing # an action and delegating responisibilities to another # app, Bottle's way was followed. app.url = url self.mounts.append(app) # TODO: Consider instead to do better subapp prefix matching # in _handle() above. self.mounts.sort(key=lambda app: len(app.url), reverse=True) def route(self, url, **kwargs): def _route(f): self.url_map.append((url, f, kwargs)) return f return _route def add_url_rule(self, url, func, **kwargs): # Note: this method skips Flask's "endpoint" argument, # because it's alleged bloat. self.url_map.append((url, func, kwargs)) def _load_template(self, tmpl_name): if self.template_loader is None: import utemplate.source self.template_loader = utemplate.source.Loader(self.pkg, "templates") return self.template_loader.load(tmpl_name) def render_template(self, writer, tmpl_name, args=()): tmpl = self._load_template(tmpl_name) for s in tmpl(*args): yield from writer.awritestr(s) def render_str(self, tmpl_name, args=()): #TODO: bloat tmpl = self._load_template(tmpl_name) return ''.join(tmpl(*args)) def sendfile(self, writer, fname, content_type=None, headers=None): if not content_type: content_type = get_mime_type(fname) try: with pkg_resources.resource_stream(self.pkg, fname) as f: yield from start_response(writer, content_type, "200", headers) yield from sendstream(writer, f) except OSError as e: if e.args[0] == uerrno.ENOENT: yield from http_error(writer, "404") else: raise def handle_static(self, req, resp): path = req.url_match.group(1) print(path) if ".." in path: yield from http_error(resp, "403") return yield from self.sendfile(resp, path) def init(self): """Initialize a web application. This is for overriding by subclasses. This is good place to connect to/initialize a database, for example.""" self.inited = True def serve(self, loop, host, port): # Actually serve client connections. Subclasses may override this # to e.g. catch and handle exceptions when dealing with server socket # (which are otherwise unhandled and will terminate a Picoweb app). # Note: name and signature of this method may change. loop.create_task(asyncio.start_server(self._handle, host, port)) loop.run_forever() def run(self, host="127.0.0.1", port=8081, debug=False, lazy_init=False, log=None): if log is None and debug >= 0: import ulogging log = ulogging.getLogger("picoweb") if debug > 0: log.setLevel(ulogging.DEBUG) self.log = log gc.collect() self.debug = int(debug) self.init() if not lazy_init: for app in self.mounts: app.init() loop = asyncio.get_event_loop() if debug > 0: print("* Running on http://%s:%s/" % (host, port)) self.serve(loop, host, port) loop.close() ================================================ FILE: picoweb/utils.py ================================================ def unquote_plus(s): # TODO: optimize s = s.replace("+", " ") arr = s.split("%") arr2 = [chr(int(x[:2], 16)) + x[2:] for x in arr[1:]] return arr[0] + "".join(arr2) def parse_qs(s): res = {} if s: pairs = s.split("&") for p in pairs: vals = [unquote_plus(x) for x in p.split("=", 1)] if len(vals) == 1: vals.append(True) old = res.get(vals[0]) if old is not None: if not isinstance(old, list): old = [old] res[vals[0]] = old old.append(vals[1]) else: res[vals[0]] = vals[1] return res #print(parse_qs("foo")) #print(parse_qs("fo%41o+bar=+++1")) #print(parse_qs("foo=1&foo=2")) ================================================ FILE: requirements-cpython.txt ================================================ pycopy-cpython-uasyncio utemplate ================================================ FILE: sdist_upip.py ================================================ # # This module overrides distutils (also compatible with setuptools) "sdist" # command to perform pre- and post-processing as required for MicroPython's # upip package manager. # # Preprocessing steps: # * Creation of Python resource module (R.py) from each top-level package's # resources. # Postprocessing steps: # * Removing metadata files not used by upip (this includes setup.py) # * Recompressing gzip archive with 4K dictionary size so it can be # installed even on low-heap targets. # import sys import os import zlib from subprocess import Popen, PIPE import glob import tarfile import re import io from distutils.filelist import FileList from setuptools.command.sdist import sdist as _sdist def gzip_4k(inf, fname): comp = zlib.compressobj(level=9, wbits=16 + 12) with open(fname + ".out", "wb") as outf: while 1: data = inf.read(1024) if not data: break outf.write(comp.compress(data)) outf.write(comp.flush()) os.rename(fname, fname + ".orig") os.rename(fname + ".out", fname) FILTERS = [ # include, exclude, repeat (r".+\.egg-info/(PKG-INFO|requires\.txt)", r"setup.py$"), (r".+\.py$", r"[^/]+$"), (None, r".+\.egg-info/.+"), ] outbuf = io.BytesIO() def filter_tar(name): fin = tarfile.open(name, "r:gz") fout = tarfile.open(fileobj=outbuf, mode="w") for info in fin: # print(info) if not "/" in info.name: continue fname = info.name.split("/", 1)[1] include = None for inc_re, exc_re in FILTERS: if include is None and inc_re: if re.match(inc_re, fname): include = True if include is None and exc_re: if re.match(exc_re, fname): include = False if include is None: include = True if include: print("including:", fname) else: print("excluding:", fname) continue farch = fin.extractfile(info) fout.addfile(info, farch) fout.close() fin.close() def make_resource_module(manifest_files): resources = [] # Any non-python file included in manifest is resource for fname in manifest_files: ext = fname.rsplit(".", 1)[1] if ext != "py": resources.append(fname) if resources: print("creating resource module R.py") resources.sort() last_pkg = None r_file = None for fname in resources: try: pkg, res_name = fname.split("/", 1) except ValueError: print("not treating %s as a resource" % fname) continue if last_pkg != pkg: last_pkg = pkg if r_file: r_file.write("}\n") r_file.close() r_file = open(pkg + "/R.py", "w") r_file.write("R = {\n") with open(fname, "rb") as f: r_file.write("%r: %r,\n" % (res_name, f.read())) if r_file: r_file.write("}\n") r_file.close() class sdist(_sdist): def run(self): self.filelist = FileList() self.get_file_list() make_resource_module(self.filelist.files) r = super().run() assert len(self.archive_files) == 1 print("filtering files and recompressing with 4K dictionary") filter_tar(self.archive_files[0]) outbuf.seek(0) gzip_4k(outbuf, self.archive_files[0]) return r # For testing only if __name__ == "__main__": filter_tar(sys.argv[1]) outbuf.seek(0) gzip_4k(outbuf, sys.argv[1]) ================================================ FILE: setup.py ================================================ from setuptools import setup import sdist_upip setup(name='picoweb', version='1.8.2', description="A very lightweight, memory-efficient async web framework \ for Pycopy (https://github.com/pfalcon/pycopy) and its uasyncio module.", long_description=open('README.rst').read(), url='https://github.com/pfalcon/picoweb', author='Paul Sokolovsky', author_email='pfalcon@users.sourceforge.net', license='MIT', cmdclass={'sdist': sdist_upip.sdist}, packages=['picoweb'], # Note: no explicit dependency on 'utemplate', if a specific app uses # templates, it must depend on it. Likewise, don't depend on # pycopy-ulogging as application might not use logging. install_requires=['pycopy-uasyncio', 'pycopy-pkg_resources']) ================================================ FILE: templates/squares.tpl ================================================ {% args req %} Request path: '{{req.path}}'
    {% for i in range(5) %} {% endfor %}
    {{i}} {{"%2d" % i ** 2}}