[
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014-2018 Paul Sokolovsky and contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "PREFIX = ~/.micropython/lib\n\nall:\n\ninstall:\n\tcp -a picoweb $(PREFIX)\n"
  },
  {
    "path": "README.rst",
    "content": "picoweb\n=======\n\npicoweb is a \"micro\" web micro-framework (thus, \"pico-framework\") for\nradically unbloated web applications using radically unbloated Python\nimplementation, Pycopy, https://github.com/pfalcon/pycopy .\n\nFeatures:\n\n* Asynchronous from the start, using unbloated asyncio-like library\n  for Pycopy (`uasyncio <https://github.com/pfalcon/pycopy-lib/tree/master/uasyncio>`_).\n  This means that ``picoweb`` can process multiple concurrent requests\n  at the same time (using I/O and/or CPU multiplexing).\n* Small memory usage. Initial version required about 64K of heap for\n  a trivial web app, and since then, it was optimized to allow run\n  more or less realistic web app in ~36K of heap. More optimizations\n  on all the levels (Pycopy and up) are planned (but may lead to\n  API changes).\n* Has API affinity with some well-known Python web micro-framework(s),\n  thus it should be an easy start if you have experience with them, and\n  existing applications can be potentially ported, instead of requiring\n  complete rewrite.\n\n\nRequirements and optional modules\n---------------------------------\n\n``picoweb`` depends on ``uasyncio`` for asynchronous networking\n(https://github.com/pfalcon/pycopy-lib/tree/master/uasyncio).\n``uasyncio`` itself requires `Pycopy <https://github.com/pfalcon/pycopy>`,\na minimalist, lightweight, and resource-efficient Python language\nimplementation.\n\nIt is also indended to be used with ``utemplate``\n(https://github.com/pfalcon/utemplate) for templating, but this is\na \"soft\" dependency - picoweb offers convenience functions to use\n``utemplate`` templates, but if you don't use them or will handle\ntemplating in your app (e.g. with a different library), it won't be\nimported.\n\nFor database access, there are following options (``picoweb`` does\nnot depend on any of them, up to your application to choose):\n\n* `btree <https://pycopy.readthedocs.io/en/latest/library/btree.html>`_\n  builtin Pycopy module. This is a recommended way to do a database\n  storage for `picoweb`, as it allows portability across all Pycopy\n  targets, starting with very memory- and storage-limited baremetal systems.\n* ``btreedb`` wrapper on top of ``btree`` builtin module. This may add some\n  overhead, but may allow to make an application portable between different\n  database backends (`filedb` and `uorm` below).\n  https://github.com/pfalcon/pycopy-btreedb\n* ``filedb``, for a simple database using files in a filesystem\n  https://github.com/pfalcon/filedb\n* ``uorm``, for Sqlite3 database access (works only with Pycopy\n  Unix port) https://github.com/pfalcon/uorm\n\nLast but not least, ``picoweb`` uses a standard ``logging``-compatible\nlogger for diagnostic output (like a connection opened, errors and debug\ninformation). However this output is optional, and otherwise you can use\na custom logging class instead of the standard ``logging``/``ulogging``\nmodule. Due to this, and to not put additional dependencies burden on\nthe small webapps for small systems, ``logging`` module is not included\nin ``picoweb``'s installation dependencies. Instead, a particular app\nusing ``picoweb`` should depend on ``pycopy-ulogging`` or\n``pycopy-logging`` package. Note that to disable use of logging,\nan application should start up using ``WebApp.run(debug=-1)``. The\ndefault value for ``debug`` parameter is 0 however, in which case\npicoweb will use ``ulogging`` module (on which your application needs\nto depend, again).\n\n\nDetails\n-------\n\npicoweb API is roughly based on APIs of other well-known Python web\nframeworks. The strongest affinity is Flask, http://flask.pocoo.org, as\narguably the most popular micro-framework. Some features are also based on\nBottle and Django. Note that this does not mean particular \"compatibility\"\nwith Flask, Bottle, or Django: most existing web frameworks are synchronous\n(and threaded), while picoweb is async framework, so its architecture is\nquite different. However, there is an aim to save porting efforts from\nrepetitive search & replace trials: for example, when methods do similar\nthings, they are likely named the same (but they may take slightly different\nparameters, return different values, and behave slightly differently).\n\nThe biggest difference is async, non-threaded nature of picoweb. That means\nthat the same code may handle multiple requests at the same time, but unlike\nthreaded environment, there's no external context (like thread and thread\nlocal storage) to associate with each request. Thus, there're no \"global\"\n(or thread-local \"global\") request and response objects, like Flask,\nBottle, Django have. Instead, all picoweb functions explicitly pass the\ncurrent request and response objects around.\n\nAlso, picoweb, being unbloated framework, tries to avoid avoidable\nabstractions. For example, HTTP at the lowest level has just read and write\nendpoints of a socket. To dispatch request, picoweb needs to pre-parse\nsome request data from input stream, and it saves that partially (sic!)\nparsed data as a \"request\" object, and that's what passed to application\nhandlers. However, there's no unavoidable need to have a \"response\"\nabstraction - the most efficient/lightweight application may want to\njust write raw HTTP status line, headers, and body to the socket. Thus,\nraw write stream is passed to application handlers as the \"response\" object.\n(But high-level convenience functions to construct an HTTP response are\nprovided).\n\n\nAPI reference\n-------------\n\nThe best API reference currently are examples (see below) and the ``picoweb``\nsource code itself. It's under 10K, so enjoy:\nhttps://github.com/pfalcon/picoweb/blob/master/picoweb/__init__.py\n\nNote that API is experimental and may undergo changes.\n\n\nExamples\n--------\n\n* `example_webapp.py <https://github.com/pfalcon/picoweb/blob/master/example_webapp.py>`_ -\n  A simple webapp showing you how to generate a complete HTTP response\n  yourself, use ``picoweb`` convenience functions for HTTP headers generation,\n  and use of templates. Mapping from URLs to webapp view functions (\"web\n  routes\" or just \"routes\") is done Django-style, using a centralized route\n  list.\n* `example_webapp2.py <https://github.com/pfalcon/picoweb/blob/master/example_webapp2.py>`_ -\n  Like above, but uses ``app.route()`` decorator for route specification,\n  Flask-style.\n* `examples/ <https://github.com/pfalcon/picoweb/tree/master/examples>`_ -\n  Additional examples for various features of picoweb. See comments in each\n  file for additional info. To run examples in this directory, you normally\n  would need to have picoweb installed (i.e. available in your ``MICROPYPATH``,\n  which defaults to ``~/.micropython/lib/``).\n* `notes-pico <https://github.com/pfalcon/notes-pico>`_ - A more realistic\n  example webapp, ported from the Flask original.\n\n\nRunning under CPython (regressed)\n---------------------------------\n\nInitial versions of picoweb could run under CPython, but later it was\nfurther optimized for Pycopy, and ability to run under CPython\nregressed. It's still on TODO to fix it, instructions below tell how\nit used to work.\n\nAt least CPython 3.4.2 is required (for asyncio loop.create_task() support).\nTo run under CPython, uasyncio compatibility module for CPython is required\n(pycopy-cpython-uasyncio). This and other dependencies can be installed\nusing requirements-cpython.txt::\n\n    pip install -r requirements-cpython.txt\n\nReporting Issues\n----------------\n\nHere are a few guidelines to make feedback more productive:\n\n1. Please be considerate of the overall best practices and common pitfalls in\n   reporting issues, this document gives a good overview:\n   `How to Report Bugs Effectively <https://www.chiark.greenend.org.uk/~sgtatham/bugs.html>`_.\n2. The reference platform for ``picoweb`` is the Unix port of Pycopy. All issues\n   reported must be validated against this version, to differentiate issues of\n   ``picoweb``/``uasyncio`` from the issues of your underlying platform.\n3. All reports must include version information of all components involved:\n   Pycopy, picoweb, uasyncio, uasyncio.core, any additional modules. Generally,\n   only the latest versions of the above are supported (this is what you get when\n   you install/reinstall components using the ``upip`` package manager). The\n   version information are thus first of all important for yourself, the issue\n   reporter, it allows you to double-check if you're using an outdated or\n   unsupported component.\n4. Feature requests: ``picoweb`` is by definition a pico-framework, and bound\n   to stay so. Feature requests are welcome, but please be considerate that\n   they may be outside the scope of core project. There's an easy way out\n   though: instead of putting more stuff *into* ``picoweb``, build new things\n   *on top* of it: via plugins, subclassing, additional modules etc. That's\n   how it was intended to be from the beginning!\n5. We would like to establish a dedicated QA team to support users of this\n   project better. If you would like to sponsor this effort, please let us\n   know.\n"
  },
  {
    "path": "example_webapp.py",
    "content": "#\n# This is a picoweb example showing a centralized web page route\n# specification (classical Django style).\n#\nimport ure as re\nimport picoweb\n\n\ndef index(req, resp):\n    # You can construct an HTTP response completely yourself, having\n    # a full control of headers sent...\n    yield from resp.awrite(\"HTTP/1.0 200 OK\\r\\n\")\n    yield from resp.awrite(\"Content-Type: text/html\\r\\n\")\n    yield from resp.awrite(\"\\r\\n\")\n    yield from resp.awrite(\"I can show you a table of <a href='squares'>squares</a>.<br/>\")\n    yield from resp.awrite(\"Or my <a href='file'>source</a>.\")\n\n\ndef squares(req, resp):\n    # Or can use a convenience function start_response() (see its source for\n    # extra params it takes).\n    yield from picoweb.start_response(resp)\n    yield from app.render_template(resp, \"squares.tpl\", (req,))\n\n\ndef hello(req, resp):\n    yield from picoweb.start_response(resp)\n    # Here's how you extract matched groups from a regex URI match\n    yield from resp.awrite(\"Hello \" + req.url_match.group(1))\n\n\nROUTES = [\n    # You can specify exact URI string matches...\n    (\"/\", index),\n    (\"/squares\", squares),\n    (\"/file\", lambda req, resp: (yield from app.sendfile(resp, \"example_webapp.py\"))),\n    # ... or match using a regex, the match result available as req.url_match\n    # for match group extraction in your view.\n    (re.compile(\"^/iam/(.+)\"), hello),\n]\n\n\nimport ulogging as logging\nlogging.basicConfig(level=logging.INFO)\n#logging.basicConfig(level=logging.DEBUG)\n\napp = picoweb.WebApp(__name__, ROUTES)\n# debug values:\n# -1 disable all logging\n# 0 (False) normal logging: requests and errors\n# 1 (True) debug logging\n# 2 extra debug logging\napp.run(debug=1)\n"
  },
  {
    "path": "example_webapp2.py",
    "content": "#\n# This is a picoweb example showing a web page route\n# specification using view decorators (Flask style).\n#\nimport picoweb\n\n\napp = picoweb.WebApp(__name__)\n\n@app.route(\"/\")\ndef index(req, resp):\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(\"I can show you a table of <a href='squares'>squares</a>.\")\n\n@app.route(\"/squares\")\ndef squares(req, resp):\n    yield from picoweb.start_response(resp)\n    yield from app.render_template(resp, \"squares.tpl\", (req,))\n\n\nimport ulogging as logging\nlogging.basicConfig(level=logging.INFO)\n\napp.run(debug=True)\n"
  },
  {
    "path": "examples/README.md",
    "content": "More picoweb examples\n=====================\n\nThis directory contains additional examples (beyond a couple available\nin the top-level directory) of picoweb usage. These examples are intended\nto serve as learn-by-example material, showing various features and\nusage patterns of picoweb. Each example starts with a comment header\ndescribing what the example does.\n\nTo run these examples, you normally need picoweb already installed (i.e.\navailable in your MICROPYPATH). If you want a quick start, you need to\ntry a couple of examples in the top-level dir mentioned above - those\ncan be run directly from the git checkout. (And as a final hint, you can\nalso copy any example to the top-level dir and run it from there too).\n"
  },
  {
    "path": "examples/app1.py",
    "content": "#\n# This is an example of a (sub)application, which can be made a part of\n# bigger site using \"app mount\" feature, see example_app_router.py.\n#\nimport picoweb\n\n\napp = picoweb.WebApp(__name__)\n\n@app.route(\"/\")\ndef index(req, resp):\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(\"This is webapp #1\")\n\n\nif __name__ == \"__main__\":\n    app.run(debug=True)\n"
  },
  {
    "path": "examples/app2.py",
    "content": "#\n# This is an example of a (sub)application, which can be made a part of\n# bigger site using \"app mount\" feature, see example_app_router.py.\n#\nimport picoweb\n\n\napp = picoweb.WebApp(__name__)\n\n@app.route(\"/\")\ndef index(req, resp):\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(\"This is webapp #2\")\n\n\nif __name__ == \"__main__\":\n    app.run(debug=True)\n"
  },
  {
    "path": "examples/example_app_router.py",
    "content": "#\n# This is an example of running several sub-applications in one bigger\n# application, by \"mounting\" them under specific URLs.\n#\nimport picoweb\nimport app1, app2\n\n\nsite = picoweb.WebApp(__name__)\nsite.mount(\"/app1\", app1.app)\nsite.mount(\"/app2\", app2.app)\n\n\n@site.route(\"/\")\ndef index(req, resp):\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(\"<a href='app1'>app1<a> or <a href='app2'>app2</a>\")\n\n\nsite.run(debug=True)\n"
  },
  {
    "path": "examples/example_basic_auth.py",
    "content": "#\n# This is a picoweb example showing handling of HTTP Basic authentication.\n#\nimport ubinascii\n\nimport picoweb\n\n\napp = picoweb.WebApp(__name__)\n\n@app.route(\"/\")\ndef index(req, resp):\n    if b\"Authorization\" not in req.headers:\n        yield from resp.awrite(\n            'HTTP/1.0 401 NA\\r\\n'\n            'WWW-Authenticate: Basic realm=\"Picoweb Realm\"\\r\\n'\n            '\\r\\n'\n        )\n        return\n\n    auth = req.headers[b\"Authorization\"].split(None, 1)[1]\n    auth = ubinascii.a2b_base64(auth).decode()\n    username, passwd = auth.split(\":\", 1)\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(\"You logged in with username: %s, password: %s\" % (username, passwd))\n\n\nimport ulogging as logging\nlogging.basicConfig(level=logging.INFO)\n\napp.run(debug=True)\n"
  },
  {
    "path": "examples/example_basic_auth_deco.py",
    "content": "#\n# This is a picoweb example showing handling of HTTP Basic authentication\n# using a decorator. Note: using decorator is cute, bit isn't the most\n# memory-efficient way. Prefer calling functions directly if you develop\n# for memory-constrained device.\n#\nimport ubinascii\n\nimport picoweb\n\n\napp = picoweb.WebApp(__name__)\n\n\ndef require_auth(func):\n\n    def auth(req, resp):\n        auth = req.headers.get(b\"Authorization\")\n        if not auth:\n            yield from resp.awrite(\n                'HTTP/1.0 401 NA\\r\\n'\n                'WWW-Authenticate: Basic realm=\"Picoweb Realm\"\\r\\n'\n                '\\r\\n'\n            )\n            return\n\n        auth = auth.split(None, 1)[1]\n        auth = ubinascii.a2b_base64(auth).decode()\n        req.username, req.passwd = auth.split(\":\", 1)\n        yield from func(req, resp)\n\n    return auth\n\n\n@app.route(\"/\")\n@require_auth\ndef index(req, resp):\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(\"You logged in with username: %s, password: %s\" % (req.username, req.passwd))\n\n\nimport ulogging as logging\nlogging.basicConfig(level=logging.INFO)\n\napp.run(debug=True)\n"
  },
  {
    "path": "examples/example_eventsource.py",
    "content": "#\n# This is a picoweb example showing a Server Side Events (SSE) aka\n# EventSource handling. Each connecting client gets its own events,\n# independent from other connected clients.\n#\nimport uasyncio\nimport picoweb\n\n\ndef index(req, resp):\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(\"\"\"\\\n<!DOCTYPE html>\n<html>\n<head>\n<script>\nvar source = new EventSource(\"events\");\nsource.onmessage = function(event) {\n    document.getElementById(\"result\").innerHTML += event.data + \"<br>\";\n}\nsource.onerror = function(error) {\n    console.log(error);\n    document.getElementById(\"result\").innerHTML += \"EventSource error:\" + error + \"<br>\";\n}\n</script>\n</head>\n<body>\n<div id=\"result\"></div>\n</body>\n</html>\n\"\"\")\n\ndef events(req, resp):\n    print(\"Event source connected\")\n    yield from resp.awrite(\"HTTP/1.0 200 OK\\r\\n\")\n    yield from resp.awrite(\"Content-Type: text/event-stream\\r\\n\")\n    yield from resp.awrite(\"\\r\\n\")\n    i = 0\n    try:\n        while True:\n            yield from resp.awrite(\"data: %d\\n\\n\" % i)\n            yield from uasyncio.sleep(1)\n            i += 1\n    except OSError:\n        print(\"Event source connection closed\")\n        yield from resp.aclose()\n\n\nROUTES = [\n    (\"/\", index),\n    (\"/events\", events),\n]\n\n\nimport ulogging as logging\nlogging.basicConfig(level=logging.INFO)\n#logging.basicConfig(level=logging.DEBUG)\n\napp = picoweb.WebApp(__name__, ROUTES)\napp.run(debug=True)\n"
  },
  {
    "path": "examples/example_eventsource_push.py",
    "content": "#\n# This is a picoweb example showing a Server Side Events (SSE) aka\n# EventSource handling. All connecting clients get the same events.\n# This is achieved by running a \"background service\" (a coroutine)\n# and \"pushing\" the same event to each connected client.\n#\nimport uasyncio\nimport picoweb\n\nevent_sinks = set()\n\n#\n# Webapp part\n#\n\ndef index(req, resp):\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(\"\"\"\\\n<!DOCTYPE html>\n<html>\n<head>\n<script>\nvar source = new EventSource(\"events\");\nsource.onmessage = function(event) {\n    document.getElementById(\"result\").innerHTML += event.data + \"<br>\";\n}\nsource.onerror = function(error) {\n    console.log(error);\n    document.getElementById(\"result\").innerHTML += \"EventSource error:\" + error + \"<br>\";\n}\n</script>\n</head>\n<body>\n<div id=\"result\"></div>\n</body>\n</html>\n\"\"\")\n\n\ndef events(req, resp):\n    global event_sinks\n    print(\"Event source %r connected\" % resp)\n    yield from resp.awrite(\"HTTP/1.0 200 OK\\r\\n\")\n    yield from resp.awrite(\"Content-Type: text/event-stream\\r\\n\")\n    yield from resp.awrite(\"\\r\\n\")\n    event_sinks.add(resp)\n    return False\n\n\nROUTES = [\n    (\"/\", index),\n    (\"/events\", events),\n]\n\n#\n# Background service part\n#\n\ndef push_event(ev):\n    global event_sinks\n    to_del = set()\n\n    for resp in event_sinks:\n        try:\n            await resp.awrite(\"data: %s\\n\\n\" % ev)\n        except OSError as e:\n            print(\"Event source %r disconnected (%r)\" % (resp, e))\n            await resp.aclose()\n            # Can't remove item from set while iterating, have to have\n            # second pass for that (not very efficient).\n            to_del.add(resp)\n\n    for resp in to_del:\n        event_sinks.remove(resp)\n\n\ndef push_count():\n    i = 0\n    while 1:\n        await push_event(\"%s\" % i)\n        i += 1\n        await uasyncio.sleep(1)\n\n\nimport ulogging as logging\nlogging.basicConfig(level=logging.INFO)\n#logging.basicConfig(level=logging.DEBUG)\n\nloop = uasyncio.get_event_loop()\nloop.create_task(push_count())\n\napp = picoweb.WebApp(__name__, ROUTES)\napp.run(debug=True)\n"
  },
  {
    "path": "examples/example_extra_headers.py",
    "content": "#\n# This is a picoweb example showing the usage of\n# extra headers in responses.\n#\nimport picoweb\nimport ure as re\n\napp = picoweb.WebApp(__name__)\n\n# Shows sending extra headers specified as a dictionary.\n@app.route(\"/\")\ndef index(req, resp):\n\n    headers = {\"X-MyHeader1\": \"foo\", \"X-MyHeader2\": \"bar\"}\n\n    # Passing headers as a positional param is more efficient,\n    # but we pass by keyword here ;-)\n    yield from picoweb.start_response(resp, headers=headers)\n    yield from resp.awrite(b\"\"\"\\\n<!DOCTYPE html>\n<html>\n<head>\n<link href=\"style.css\" rel=\"stylesheet\">\n</head>\n<body>\n<p>The style.css should be cached and might be encoded.</p>\n<p class=\"green\">Check out your webdev tool!</p>\n</body>\n</html>\"\"\")\n\n\n# Send gzipped content if supported by client.\n# Shows specifying headers as a flat binary string -\n# more efficient if such headers are static.\n@app.route(re.compile('^\\/(.+\\.css)$'))\ndef styles(req, resp):\n    file_path = req.url_match.group(1)\n    headers = b\"Cache-Control: max-age=86400\\r\\n\"\n\n    if b\"gzip\" in req.headers.get(b\"Accept-Encoding\", b\"\"):\n        file_path += \".gz\"\n        headers += b\"Content-Encoding: gzip\\r\\n\"\n\n    print(\"sending \" + file_path)\n    yield from app.sendfile(resp, \"static/\" + file_path, \"text/css\", headers)\n\n\nimport ulogging as logging\nlogging.basicConfig(level=logging.INFO)\n\napp.run(debug=True)\n"
  },
  {
    "path": "examples/example_form.py",
    "content": "#\n# This is a picoweb example showing how to handle form data.\n#\nimport picoweb\n\n\napp = picoweb.WebApp(__name__)\n\n\n@app.route(\"/form_url\")\ndef index(req, resp):\n    if req.method == \"POST\":\n        yield from req.read_form_data()\n    else:  # GET, apparently\n        # Note: parse_qs() is not a coroutine, but a normal function.\n        # But you can call it using yield from too.\n        req.parse_qs()\n\n    # Whether form data comes from GET or POST request, once parsed,\n    # it's available as req.form dictionary\n\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(\"Hello %s!\" % req.form[\"name\"])\n\n\n@app.route(\"/\")\ndef index(req, resp):\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(\"POST form:<br>\")\n    yield from resp.awrite(\"<form action='form_url' method='POST'>\"\n        \"What is your name? <input name='name' /> \"\n        \"<input type='submit'></form>\")\n\n    yield from resp.awrite(\"GET form:<br>\")\n    # GET is the default\n    yield from resp.awrite(\"<form action='form_url'>\"\n        \"What is your name? <input name='name' /> \"\n        \"<input type='submit'></form>\")\n\n\nimport ulogging as logging\nlogging.basicConfig(level=logging.INFO)\n\napp.run(debug=True)\n"
  },
  {
    "path": "examples/example_global_exc.py",
    "content": "#\n# This is a picoweb example showing a how to \"globally\" handle exceptions\n# during request processing. Note that you can always handle\n# exceptions in a particular request using normal try/except/finally.\n# That's actually the recommended way. Of course, if you have a\n# webapp with many request handlers, that becomes less practical\n# and global exception handler may make sense. A common action of\n# global handler would be to send a \"500\" page, but mind the cuprit\n# shown below.\n#\nimport picoweb\n\n\nclass ExcWebApp(picoweb.WebApp):\n\n    async def handle_exc(self, req, resp, exc):\n        try:\n            # Do you already see a problem - what happens if your action\n            # already started output before exception happened? Resolving\n            # that issue is wholy up to your webapp, picoweb doesn't limit\n            # you to any particular method, use whatever suits you better!\n            await picoweb.start_response(resp, status=\"500\")\n            await resp.awrite(\"We've got 500, cap!\")\n        except Exception as e:\n            # Per API contract, handle_exc() must not raise exceptions\n            # (unless we want the whole webapp to terminate).\n            print(repr(e))\n\n\napp = ExcWebApp(__name__)\n\n\n@app.route(\"/\")\ndef index(req, resp):\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(\n        \"<a href='case1'>good exception case</a> \"\n        \"<a href='case2'>less good exception case</a>\"\n    )\n\n\n@app.route(\"/case1\")\ndef case1(req, resp):\n    1/0\n\n\n@app.route(\"/case2\")\ndef case2(req, resp):\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(\n        \"Here, I started to write something to response, and suddenly...\"\n    )\n    1/0\n\n\nimport ulogging as logging\nlogging.basicConfig(level=logging.INFO)\n\napp.run(debug=True)\n"
  },
  {
    "path": "examples/example_header_modes.py",
    "content": "#\n# This is a picoweb example showing various header parsing modes.\n#\nimport ure as re\nimport picoweb\n\n\ndef index(req, resp):\n    yield from resp.awrite(\"HTTP/1.0 200 OK\\r\\n\")\n    yield from resp.awrite(\"Content-Type: text/html\\r\\n\")\n    yield from resp.awrite(\"\\r\\n\")\n    yield from resp.awrite('<li><a href=\"mode_parse\">header_mode=\"parse\"</a>')\n    yield from resp.awrite('<li><a href=\"mode_skip\">header_mode=\"skip\"</a>')\n    yield from resp.awrite('<li><a href=\"mode_leave\">header_mode=\"leave\"</a>')\n\n\ndef headers_parse(req, resp):\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(\"<table border='1'>\")\n    for h, v in req.headers.items():\n        yield from resp.awrite(\"<tr><td>%s</td><td>%s</td></tr>\\r\\n\" % (h, v))\n    yield from resp.awrite(\"</table>\")\n\ndef headers_skip(req, resp):\n    yield from picoweb.start_response(resp)\n    assert not hasattr(req, \"headers\")\n    yield from resp.awrite(\"No <tt>req.headers</tt>.\")\n\ndef headers_leave(req, resp):\n    yield from picoweb.start_response(resp)\n    assert not hasattr(req, \"headers\")\n    yield from resp.awrite(\"Reading headers directly from input request:\")\n    yield from resp.awrite(\"<pre>\")\n    while True:\n        l = yield from req.reader.readline()\n        if l == b\"\\r\\n\":\n            break\n        yield from resp.awrite(l)\n    yield from resp.awrite(\"</pre>\")\n\n\nROUTES = [\n    (\"/\", index),\n    (\"/mode_parse\", headers_parse, {\"headers\": \"parse\"}),\n    (\"/mode_skip\", headers_skip, {\"headers\": \"skip\"}),\n    (\"/mode_leave\", headers_leave, {\"headers\": \"leave\"}),\n]\n\n\nimport ulogging as logging\nlogging.basicConfig(level=logging.INFO)\n#logging.basicConfig(level=logging.DEBUG)\n\napp = picoweb.WebApp(__name__, ROUTES)\n# You could set the default header parsing mode here like this:\n# app.headers_mode = \"skip\"\napp.run(debug=True)\n"
  },
  {
    "path": "examples/example_img.py",
    "content": "#\n# This is a picoweb example showing how to serve images - both static\n# image from webapp's static/ dir, and dynamically-generated image.\n#\nimport picoweb\n\n\napp = picoweb.WebApp(__name__)\n\n@app.route(\"/\")\ndef index(req, resp):\n    yield from picoweb.start_response(resp)\n    yield from resp.awrite(b\"Static image: <img src='static/logo.png'><br />\")\n    yield from resp.awrite(b\"Dynamic image: <img src='dyna-logo.png'><br />\")\n\n@app.route(\"/dyna-logo.png\")\ndef squares(req, resp):\n    yield from picoweb.start_response(resp, \"image/png\")\n    yield from resp.awrite(\n        b\"\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x000\\x00\\x00\\x000\\x01\\x03\\x00\\x00\\x00m\\xcck\"\n        b\"\\xc4\\x00\\x00\\x00\\x06PLTE\\xff\\xff\\xff\\x00\\x00\\x00U\\xc2\\xd3~\\x00\\x00\\x00\\xd2IDAT\\x18\\xd3c`\"\n        b\"\\x80\\x01\\xc6\\x06\\x08-\\x00\\xe1u\\x80)\\xa6\\x040\\xc5\\x12\\x00\\xa18\\xc0\\x14+\\x84\\xe2\\xf5\\x00Sr\"\n        b\"\\x10J~\\x0e\\x98\\xb2\\xff\\x83\\x85\\xb2\\x86P\\xd2\\x15\\x10\\x95\\x10\\x8a\\xff\\x07\\x98b\\xff\\x80L\\xb1\"\n        b\"\\xa5\\x83-?\\x95\\xff\\x00$\\xf6\\xeb\\x7f\\x01\\xc8\\x9eo\\x7f@\\x92r)\\x9fA\\x94\\xfc\\xc4\\xf3/\\x80\\x94\"\n        b\"\\xf8\\xdb\\xff'@F\\x1e\\xfcg\\x01\\xa4\\xac\\x1e^\\xaa\\x01R6\\xb1\\x8f\\xff\\x01);\\xc7\\xff \\xca\\xfe\\xe1\"\n        b\"\\xff_@\\xea\\xff\\xa7\\xff\\x9f\\x81F\\xfe\\xfe\\x932\\xbd\\x81\\x81\\xb16\\xf0\\xa4\\x1d\\xd0\\xa3\\xf3\\xfb\"\n        b\"\\xba\\x7f\\x02\\x05\\x97\\xff\\xff\\xff\\x14(\\x98\\xf9\\xff\\xff\\xb4\\x06\\x06\\xa6\\xa8\\xfa\\x7fQ\\x0e\\x0c\"\n        b\"\\x0c\\xd3\\xe6\\xff\\xcc\\x04\\xeaS]\\xfet\\t\\x90\\xe2\\xcc\\x9c6\\x01\\x14\\x10Q )\\x06\\x86\\xe9/\\xc1\\xee\"\n        b\"T]\\x02\\xa68\\x04\\x18\\xd0\\x00\\x00\\xcb4H\\xa2\\x8c\\xbd\\xc0j\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\"\n    )\n\n\nimport ulogging as logging\nlogging.basicConfig(level=logging.INFO)\n\napp.run(debug=True)\n"
  },
  {
    "path": "examples/example_unicode.py",
    "content": "#\n# This is a picoweb example showing rendering of template\n# with Unicode (UTF-8) characters.\n#\nimport picoweb\n\n\napp = picoweb.WebApp(__name__)\n\n@app.route(\"/\")\ndef index(req, resp):\n    yield from picoweb.start_response(resp)\n    data = {\"chars\": \"абвгд\", \"var1\": \"α\", \"var2\": \"β\", \"var3\": \"γ\"}\n    yield from app.render_template(resp, \"unicode.tpl\", (data,))\n\nimport ulogging as logging\nlogging.basicConfig(level=logging.INFO)\n\napp.run(debug=True)\n"
  },
  {
    "path": "examples/static/style.css",
    "content": ".green {\n    color: darkgreen;\n}\n"
  },
  {
    "path": "examples/templates/unicode.tpl",
    "content": "{% args d %}\n<html>\nSome Cyrillic characters: {{d[\"chars\"]}}<br>\nSome Greek characters: {{d[\"var1\"]}} = {{d[\"var2\"]}} + {{d[\"var3\"]}}<br>\n</html>\n"
  },
  {
    "path": "picoweb/__init__.py",
    "content": "# Picoweb web pico-framework for Pycopy, https://github.com/pfalcon/pycopy\n# Copyright (c) 2014-2020 Paul Sokolovsky\n# SPDX-License-Identifier: MIT\nimport sys\nimport gc\nimport micropython\nimport utime\nimport uio\nimport ure as re\nimport uerrno\nimport uasyncio as asyncio\nimport pkg_resources\n\nfrom .utils import parse_qs\n\nSEND_BUFSZ = 128\n\n\ndef get_mime_type(fname):\n    # Provide minimal detection of important file\n    # types to keep browsers happy\n    if fname.endswith(\".html\"):\n        return \"text/html\"\n    if fname.endswith(\".css\"):\n        return \"text/css\"\n    if fname.endswith(\".png\") or fname.endswith(\".jpg\"):\n        return \"image\"\n    return \"text/plain\"\n\ndef sendstream(writer, f):\n    buf = bytearray(SEND_BUFSZ)\n    while True:\n        l = f.readinto(buf)\n        if not l:\n            break\n        yield from writer.awrite(buf, 0, l)\n\n\ndef jsonify(writer, dict):\n    import ujson\n    yield from start_response(writer, \"application/json\")\n    yield from writer.awrite(ujson.dumps(dict))\n\ndef start_response(writer, content_type=\"text/html; charset=utf-8\", status=\"200\", headers=None):\n    yield from writer.awrite(\"HTTP/1.0 %s NA\\r\\n\" % status)\n    yield from writer.awrite(\"Content-Type: \")\n    yield from writer.awrite(content_type)\n    if not headers:\n        yield from writer.awrite(\"\\r\\n\\r\\n\")\n        return\n    yield from writer.awrite(\"\\r\\n\")\n    if isinstance(headers, bytes) or isinstance(headers, str):\n        yield from writer.awrite(headers)\n    else:\n        for k, v in headers.items():\n            yield from writer.awrite(k)\n            yield from writer.awrite(\": \")\n            yield from writer.awrite(v)\n            yield from writer.awrite(\"\\r\\n\")\n    yield from writer.awrite(\"\\r\\n\")\n\ndef http_error(writer, status):\n    yield from start_response(writer, status=status)\n    yield from writer.awrite(status)\n\n\nclass HTTPRequest:\n\n    def __init__(self):\n        pass\n\n    def read_form_data(self):\n        size = int(self.headers[b\"Content-Length\"])\n        data = yield from self.reader.readexactly(size)\n        form = parse_qs(data.decode())\n        self.form = form\n\n    def parse_qs(self):\n        form = parse_qs(self.qs)\n        self.form = form\n\n\nclass WebApp:\n\n    def __init__(self, pkg, routes=None, serve_static=True):\n        if routes:\n            self.url_map = routes\n        else:\n            self.url_map = []\n        if pkg and pkg != \"__main__\":\n            self.pkg = pkg.split(\".\", 1)[0]\n        else:\n            self.pkg = None\n        if serve_static:\n            self.url_map.append((re.compile(\"^/(static/.+)\"), self.handle_static))\n        self.mounts = []\n        self.inited = False\n        # Instantiated lazily\n        self.template_loader = None\n        self.headers_mode = \"parse\"\n\n    def parse_headers(self, reader):\n        headers = {}\n        while True:\n            l = yield from reader.readline()\n            if l == b\"\\r\\n\":\n                break\n            k, v = l.split(b\":\", 1)\n            headers[k] = v.strip()\n        return headers\n\n    def _handle(self, reader, writer):\n        if self.debug > 1:\n            micropython.mem_info()\n\n        close = True\n        req = None\n        try:\n            request_line = yield from reader.readline()\n            if request_line == b\"\":\n                if self.debug >= 0:\n                    self.log.error(\"%s: EOF on request start\" % reader)\n                yield from writer.aclose()\n                return\n            req = HTTPRequest()\n            # TODO: bytes vs str\n            request_line = request_line.decode()\n            method, path, proto = request_line.split()\n            if self.debug >= 0:\n                self.log.info('%.3f %s %s \"%s %s\"' % (utime.time(), req, writer, method, path))\n            path = path.split(\"?\", 1)\n            qs = \"\"\n            if len(path) > 1:\n                qs = path[1]\n            path = path[0]\n\n            #print(\"================\")\n            #print(req, writer)\n            #print(req, (method, path, qs, proto), req.headers)\n\n            # Find which mounted subapp (if any) should handle this request\n            app = self\n            while True:\n                found = False\n                for subapp in app.mounts:\n                    root = subapp.url\n                    #print(path, \"vs\", root)\n                    if path[:len(root)] == root:\n                        app = subapp\n                        found = True\n                        path = path[len(root):]\n                        if not path.startswith(\"/\"):\n                            path = \"/\" + path\n                        break\n                if not found:\n                    break\n\n            # We initialize apps on demand, when they really get requests\n            if not app.inited:\n                app.init()\n\n            # Find handler to serve this request in app's url_map\n            found = False\n            for e in app.url_map:\n                pattern = e[0]\n                handler = e[1]\n                extra = {}\n                if len(e) > 2:\n                    extra = e[2]\n\n                if path == pattern:\n                    found = True\n                    break\n                elif not isinstance(pattern, str):\n                    # Anything which is non-string assumed to be a ducktype\n                    # pattern matcher, whose .match() method is called. (Note:\n                    # Django uses .search() instead, but .match() is more\n                    # efficient and we're not exactly compatible with Django\n                    # URL matching anyway.)\n                    m = pattern.match(path)\n                    if m:\n                        req.url_match = m\n                        found = True\n                        break\n\n            if not found:\n                headers_mode = \"skip\"\n            else:\n                headers_mode = extra.get(\"headers\", self.headers_mode)\n\n            if headers_mode == \"skip\":\n                while True:\n                    l = yield from reader.readline()\n                    if l == b\"\\r\\n\":\n                        break\n            elif headers_mode == \"parse\":\n                req.headers = yield from self.parse_headers(reader)\n            else:\n                assert headers_mode == \"leave\"\n\n            if found:\n                req.method = method\n                req.path = path\n                req.qs = qs\n                req.reader = reader\n                close = yield from handler(req, writer)\n            else:\n                yield from start_response(writer, status=\"404\")\n                yield from writer.awrite(\"404\\r\\n\")\n            #print(req, \"After response write\")\n        except Exception as e:\n            if self.debug >= 0:\n                self.log.exc(e, \"%.3f %s %s %r\" % (utime.time(), req, writer, e))\n            yield from self.handle_exc(req, writer, e)\n\n        if close is not False:\n            yield from writer.aclose()\n        if __debug__ and self.debug > 1:\n            self.log.debug(\"%.3f %s Finished processing request\", utime.time(), req)\n\n    def handle_exc(self, req, resp, e):\n        # Can be overriden by subclasses. req may be not (fully) initialized.\n        # resp may already have (partial) content written.\n        # NOTE: It's your responsibility to not throw exceptions out of\n        # handle_exc(). If exception is thrown, it will be propagated, and\n        # your webapp will terminate.\n        # This method is a coroutine.\n        return\n        yield\n\n    def mount(self, url, app):\n        \"Mount a sub-app at the url of current app.\"\n        # Inspired by Bottle. It might seem that dispatching to\n        # subapps would rather be handled by normal routes, but\n        # arguably, that's less efficient. Taking into account\n        # that paradigmatically there's difference between handing\n        # an action and delegating responisibilities to another\n        # app, Bottle's way was followed.\n        app.url = url\n        self.mounts.append(app)\n        # TODO: Consider instead to do better subapp prefix matching\n        # in _handle() above.\n        self.mounts.sort(key=lambda app: len(app.url), reverse=True)\n\n    def route(self, url, **kwargs):\n        def _route(f):\n            self.url_map.append((url, f, kwargs))\n            return f\n        return _route\n\n    def add_url_rule(self, url, func, **kwargs):\n        # Note: this method skips Flask's \"endpoint\" argument,\n        # because it's alleged bloat.\n        self.url_map.append((url, func, kwargs))\n\n    def _load_template(self, tmpl_name):\n        if self.template_loader is None:\n            import utemplate.source\n            self.template_loader = utemplate.source.Loader(self.pkg, \"templates\")\n        return self.template_loader.load(tmpl_name)\n\n    def render_template(self, writer, tmpl_name, args=()):\n        tmpl = self._load_template(tmpl_name)\n        for s in tmpl(*args):\n            yield from writer.awritestr(s)\n\n    def render_str(self, tmpl_name, args=()):\n        #TODO: bloat\n        tmpl = self._load_template(tmpl_name)\n        return ''.join(tmpl(*args))\n\n    def sendfile(self, writer, fname, content_type=None, headers=None):\n        if not content_type:\n            content_type = get_mime_type(fname)\n        try:\n            with pkg_resources.resource_stream(self.pkg, fname) as f:\n                yield from start_response(writer, content_type, \"200\", headers)\n                yield from sendstream(writer, f)\n        except OSError as e:\n            if e.args[0] == uerrno.ENOENT:\n                yield from http_error(writer, \"404\")\n            else:\n                raise\n\n    def handle_static(self, req, resp):\n        path = req.url_match.group(1)\n        print(path)\n        if \"..\" in path:\n            yield from http_error(resp, \"403\")\n            return\n        yield from self.sendfile(resp, path)\n\n    def init(self):\n        \"\"\"Initialize a web application. This is for overriding by subclasses.\n        This is good place to connect to/initialize a database, for example.\"\"\"\n        self.inited = True\n\n    def serve(self, loop, host, port):\n        # Actually serve client connections. Subclasses may override this\n        # to e.g. catch and handle exceptions when dealing with server socket\n        # (which are otherwise unhandled and will terminate a Picoweb app).\n        # Note: name and signature of this method may change.\n        loop.create_task(asyncio.start_server(self._handle, host, port))\n        loop.run_forever()\n\n    def run(self, host=\"127.0.0.1\", port=8081, debug=False, lazy_init=False, log=None):\n        if log is None and debug >= 0:\n            import ulogging\n            log = ulogging.getLogger(\"picoweb\")\n            if debug > 0:\n                log.setLevel(ulogging.DEBUG)\n        self.log = log\n        gc.collect()\n        self.debug = int(debug)\n        self.init()\n        if not lazy_init:\n            for app in self.mounts:\n                app.init()\n        loop = asyncio.get_event_loop()\n        if debug > 0:\n            print(\"* Running on http://%s:%s/\" % (host, port))\n        self.serve(loop, host, port)\n        loop.close()\n"
  },
  {
    "path": "picoweb/utils.py",
    "content": "def unquote_plus(s):\n    # TODO: optimize\n    s = s.replace(\"+\", \" \")\n    arr = s.split(\"%\")\n    arr2 = [chr(int(x[:2], 16)) + x[2:] for x in arr[1:]]\n    return arr[0] + \"\".join(arr2)\n\ndef parse_qs(s):\n    res = {}\n    if s:\n        pairs = s.split(\"&\")\n        for p in pairs:\n            vals = [unquote_plus(x) for x in p.split(\"=\", 1)]\n            if len(vals) == 1:\n                vals.append(True)\n            old = res.get(vals[0])\n            if old is not None:\n                if not isinstance(old, list):\n                    old = [old]\n                    res[vals[0]] = old\n                old.append(vals[1])\n            else:\n                res[vals[0]] = vals[1]\n    return res\n\n#print(parse_qs(\"foo\"))\n#print(parse_qs(\"fo%41o+bar=+++1\"))\n#print(parse_qs(\"foo=1&foo=2\"))\n"
  },
  {
    "path": "requirements-cpython.txt",
    "content": "pycopy-cpython-uasyncio\nutemplate\n"
  },
  {
    "path": "sdist_upip.py",
    "content": "#\n# This module overrides distutils (also compatible with setuptools) \"sdist\"\n# command to perform pre- and post-processing as required for MicroPython's\n# upip package manager.\n#\n# Preprocessing steps:\n#  * Creation of Python resource module (R.py) from each top-level package's\n#    resources.\n# Postprocessing steps:\n#  * Removing metadata files not used by upip (this includes setup.py)\n#  * Recompressing gzip archive with 4K dictionary size so it can be\n#    installed even on low-heap targets.\n#\nimport sys\nimport os\nimport zlib\nfrom subprocess import Popen, PIPE\nimport glob\nimport tarfile\nimport re\nimport io\n\nfrom distutils.filelist import FileList\nfrom setuptools.command.sdist import sdist as _sdist\n\n\ndef gzip_4k(inf, fname):\n    comp = zlib.compressobj(level=9, wbits=16 + 12)\n    with open(fname + \".out\", \"wb\") as outf:\n        while 1:\n            data = inf.read(1024)\n            if not data:\n                break\n            outf.write(comp.compress(data))\n        outf.write(comp.flush())\n    os.rename(fname, fname + \".orig\")\n    os.rename(fname + \".out\", fname)\n\n\nFILTERS = [\n    # include, exclude, repeat\n    (r\".+\\.egg-info/(PKG-INFO|requires\\.txt)\", r\"setup.py$\"),\n    (r\".+\\.py$\", r\"[^/]+$\"),\n    (None, r\".+\\.egg-info/.+\"),\n]\n\n\noutbuf = io.BytesIO()\n\ndef filter_tar(name):\n    fin = tarfile.open(name, \"r:gz\")\n    fout = tarfile.open(fileobj=outbuf, mode=\"w\")\n    for info in fin:\n#        print(info)\n        if not \"/\" in info.name:\n            continue\n        fname = info.name.split(\"/\", 1)[1]\n        include = None\n\n        for inc_re, exc_re in FILTERS:\n            if include is None and inc_re:\n                if re.match(inc_re, fname):\n                    include = True\n\n            if include is None and exc_re:\n                if re.match(exc_re, fname):\n                    include = False\n\n        if include is None:\n            include = True\n\n        if include:\n            print(\"including:\", fname)\n        else:\n            print(\"excluding:\", fname)\n            continue\n\n        farch = fin.extractfile(info)\n        fout.addfile(info, farch)\n    fout.close()\n    fin.close()\n\n\ndef make_resource_module(manifest_files):\n        resources = []\n        # Any non-python file included in manifest is resource\n        for fname in manifest_files:\n            ext = fname.rsplit(\".\", 1)[1]\n            if ext != \"py\":\n                resources.append(fname)\n\n        if resources:\n            print(\"creating resource module R.py\")\n            resources.sort()\n            last_pkg = None\n            r_file = None\n            for fname in resources:\n                try:\n                    pkg, res_name = fname.split(\"/\", 1)\n                except ValueError:\n                    print(\"not treating %s as a resource\" % fname)\n                    continue\n                if last_pkg != pkg:\n                    last_pkg = pkg\n                    if r_file:\n                        r_file.write(\"}\\n\")\n                        r_file.close()\n                    r_file = open(pkg + \"/R.py\", \"w\")\n                    r_file.write(\"R = {\\n\")\n\n                with open(fname, \"rb\") as f:\n                    r_file.write(\"%r: %r,\\n\" % (res_name, f.read()))\n\n            if r_file:\n                r_file.write(\"}\\n\")\n                r_file.close()\n\n\nclass sdist(_sdist):\n\n    def run(self):\n        self.filelist = FileList()\n        self.get_file_list()\n        make_resource_module(self.filelist.files)\n\n        r = super().run()\n\n        assert len(self.archive_files) == 1\n        print(\"filtering files and recompressing with 4K dictionary\")\n        filter_tar(self.archive_files[0])\n        outbuf.seek(0)\n        gzip_4k(outbuf, self.archive_files[0])\n\n        return r\n\n\n# For testing only\nif __name__ == \"__main__\":\n    filter_tar(sys.argv[1])\n    outbuf.seek(0)\n    gzip_4k(outbuf, sys.argv[1])\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup\nimport sdist_upip\n\n\nsetup(name='picoweb',\n      version='1.8.2',\n      description=\"A very lightweight, memory-efficient async web framework \\\nfor Pycopy (https://github.com/pfalcon/pycopy) and its uasyncio module.\",\n      long_description=open('README.rst').read(),\n      url='https://github.com/pfalcon/picoweb',\n      author='Paul Sokolovsky',\n      author_email='pfalcon@users.sourceforge.net',\n      license='MIT',\n      cmdclass={'sdist': sdist_upip.sdist},\n      packages=['picoweb'],\n      # Note: no explicit dependency on 'utemplate', if a specific app uses\n      # templates, it must depend on it. Likewise, don't depend on\n      # pycopy-ulogging as application might not use logging.\n      install_requires=['pycopy-uasyncio', 'pycopy-pkg_resources'])\n"
  },
  {
    "path": "templates/squares.tpl",
    "content": "{% args req %}\n<html>\nRequest path: '{{req.path}}'<br>\n<table border=\"1\">\n{% for i in range(5) %}\n<tr><td> {{i}} </td><td> {{\"%2d\" % i ** 2}} </td></tr>\n{% endfor %}\n</table>\n</html>\n"
  }
]