{ "info": { "author": "Peter Peter", "author_email": "dev.peterpeter5@gmail.com", "bugtrack_url": null, "classifiers": [ "Programming Language :: Python :: 3" ], "description": "# shallot - a plugable \"webframework\"\n\n## What is a shallot?\n\nIt is a small onion. It has only small and few layers. When you use it (cut it for cocking), it does not make \nyou cry (that much).\n\nThe above description of the vegetable, is a good misson-statement for what `shallot` (the [micro-] \"webframework\") tries to be. \n\n`shallot` is a small layer on top of an ASGI - compatible server, like: uvicorn, hypercorn, ... It is haveliy inspired \nby [ring](https://github.com/ring-clojure/ring). The main differnce to other webframeworks is, that `shallot` is easly pugable and extensible. Every component can be switched and new features can be added without touching `shallot`s source-code. That is accomplished by using middlewares for every functionality in `shallot`.\n\n## Architecture\n\n`shallot` is an [ASGI](https://asgi.readthedocs.io/en/latest/index.html) - compatible webframework. \n\n### Basic-Concepts\n\n`shallot` models a http-request-response-cycle as single function call. It treats `request` and `response` as `dict`s. The request get passed to a `handler` (which itself can be \"middleware-decorated\") and the `handler` produces a response.\nBasically `shallot` works like this:\n1. take the ASGI [connection-scope](https://asgi.readthedocs.io/en/latest/specs/www.html#connection-scope) (`dict`)\n2. read the body of the request and attach the body (`bytes`) to scope-dict \n3. pass the request-`dict` (scope + attached body) to a user-defined function (called `handler`)\n4. the result (`response`) of a handler has to be a `dict`. The response must at least provide a `status`-key with an integer. If provided a `body`-key for the response is provided, than the value must be of type `bytes` and to will be transfered to the client. \n\n### data-flow\n\n```\n+----------+ +----------+ +------------+\n| | | | | |\n| +-----------> request +-------------> middlewares+-----------+\n| | | | | (enter) | |\n| | +----------+ +------------+ |\n| A | |\n| S | |\n| G | |\n| I | |\n| | +---------v--------+\n| | | | |\n| | | handler |\n| S | | |\n| E | +---------+--------+\n| R | |\n| V | |\n| E | |\n| R | +----------+ +------------+ |\n| | | | | | |\n| <-----------+ response <-------------+ middlewares<-----------+\n| | | | | (leave) |\n+----------+ +----------+ +------------+\n```\n\n### request\n\nThe `request` is always the first argument that gets passed to your `handler`-function. It is of type `dict`. It has basically the same content as the [ASGI-connection-scope](https://asgi.readthedocs.io/en/latest/specs/www.html#connection-scope). \n\nA request will at least have the following structure:\n\n- `type`: http [string]\n- `http_version`: one of `1.0`, `1.1` or `2` [`string`]\n- `method`: the http-verb in uppercase (for example: \"GET\", \"PUT\", \"POST\", ...) [`string`]\n- `scheme` [optional, but not empty]: the url-scheme (for example: \"http\", \"https\") [`string`]\n- `query_string`: Byte-string with the url-query-path content (everything after the first `?`) [`bytes`]\n- `root_path`: mounting-point of your application [`string`]\n- `client`: A two-item iterable of `[host, port]`, where host is a unicode string of the remote host\u2019s IPv4 or IPv6 address, and port is the remote port as an integer. Optional, defaults to None. [`list`|`tuple`]\n- `server`: A two-item iterable of `[host, port]`, where host is the listening address for this server as a unicode string, and port is the integer listening port. Optional, defaults to None. [`list`|`tuple`]\n- `headers`: a `dict` with all header-names as `keys` and the corresponding-values as `values` of the dict. Duplicated `headers` will be joined \"comma-separated\". All header-names are lower-cased. [`dict`]\n- `headers_list`: the original `headers`-data-structure form the ASGI-connection-scope. This is a `list` containing `tuples` in the form: `[(header-name1, header-value1), ...]`. The header-names can be duplicated. [This is the basis for `headers`]\n- `body`: The body of the http-request as `bytes`. `shallot` always read the entire body and then calls the `handler`-function. [`bytes`] \n\n### response\n\nThe `response` is the result of the function-call to the handler (with the `request` as first argument). The `response` has to be a `dict`. The reponse must have the following structure:\n\n- `status`: the http-return-code [`int`]\n- `body` [optional]: the body of the http-response [`bytes`]\n- `headers` [optional]: the http-response-headers to be used. The value is a `dict` (for example: `{\"header1-name\": \"header1-value\", ...}`)\n- `stream` [optional]: this must be an `async-iterable` yielding `bytes`. When the `response` contains a key named `stream`, than `shallot` will consume the `iterable` and will stream the provided data to the client. This is specially usefull for large response-bodies.\n\n### handler\n\n`shallot` assembles a request-dict and calls a user-provided handler. A `handler` is an async-function that takes a request and returns a response (`dict`). \n\n```python\nasync def handler(request):\n return {\"status\": 200}\n```\n\n### middleware\n\nMost of `shallot`s functionality is implemented via middlewares. That makes it possible to easily extend, configure or change `shallot`s behaviour. In fact: if you don't like the implementation of a certain middleware, just write your own and use it insetad (or better: enhance `shallot` via PR)!\n\nThe general functionality of a middleware is, that it wraps a handler-function-call. Middlewares are designed that way, that they can be composed / chained together. So for a middleware-chain with 3 different middlewares, a call chain might look like:\n\n```\n|-> middleware 1 (enter)\n |-> middleware 2 (enter)\n |-> middleware 3 (enter)\n |-> handler (execute)\n |<- middleware 3 (leave)\n |<- middleware 2 (leave)\n|<- middleware 1 (leave)\n```\n\nA good analogy for a middleware is a python-decorator. A decorator wraps a function and returns another function to provide extended functionality.\n\n#### middleware signature\n\nin order to make middlewares composeable / work together, thy must implement the following signature:\n\n```python\ndef wrap_print_logging(next_middleware):\n async def _log_request_response(handler, request):\n print(f\"Request to the handler: {request}\")\n\n response = await next_middleware(handler, request) # IMPORTANT: here we call the middlewares and wait for them to run\n\n print(f\"Response from the handler: {response}\")\n return response\n return _log_request_response\n```\n\nThe above example shows a middleware that would simply printout the request and the reponse from the handler. Every middleware will run for EVERY request that comes to your application!\n\n#### composing middlewares together\n\n`middlewares` are great because they can be composed/chained together. In that way every `middleware` can enhance the `request` / `response` or choose a different `handler` to add functionality. Chaining middlewares is done via the `apply_middleware` - function provided by shallot:\n\n```python\nfrom shallot.middlewares import chain_middleware\nmiddlewares = chain_middleware(middleware1, middleware2, middleware3)\n\nenhanced_handler = middlewares(default_handler)\n```\n\nThe result of `chain_middleware` is a middleware-chain. A middleware-chain is a function that accepts another function, the `default_handler`. This is the handler-function that gets called after the request is passed through all middlewares. After instantiating the middleware-chain with a handler, the result is another-function. The function behaves just like a normal `handler`-function and can be used with `build_server`\n\n#### differences to ring-middleware\nWhile the function-signature of a `shallot`-handler is the same as with [ring](https://github.com/ring-clojure/ring), the middleware-signature is different and slitely more complex. This is, to support \"request-routing\" as a middleware. This way, the router can be just another middleware choosing a new handler, instead of enhancing the request. This way, other middlewares (possible type-annotation-aware middlewares) can be chained after the router and have access to the handler-function. \n\n### run an application\n\nthe minimal deployable thing one can build is this:\n\n```python\nasync def minimal(request):\n \"\"\"\n answer EVERY request with 200 and NO body \n \"\"\"\n return {\"status\": 200}\n\nserver = build_server(minimal)\n\nif __name__ == \"__main__\":\n import uvicorn # shallot is not tied to uvicorn, its just fast\n uvicorn.run(\"127.0.0.1\", 5000, log_level=\"info\", debug=True)\n```\n\nto configure/run a real application, one would typically chain/apply a pile of middlewares and a handler:\n\n```python\n\nasync def handle_404(request):\n return {\"status\": 404}\n\nmiddleware_pile = apply_middleware(\n wrap_cors(),\n wrap_content_type(),\n wrap_static(\"/static/data\"),\n wrap_routes(routes),\n wrap_cookies,\n wrap_json,\n)\n\nserver = build_server(middlewre_pile(handle_404))\n```\n\n## Features\n\nNothing is enabled by default. Every functionality has its own middleware. \n\n### Routing\nTo include `shallot` builtin routing use the routing-middleware: `wrap_routes`:\n```python\nbuild_server(apply_middleware(wrap_routes(routes))(default_handler))\n```\nThe routing-middleware is somewhat special, to other middlewares. It does not enhance the request/response, but chooses a new handler for the specific request. If the router can't find a matching handler for the route, then the `default_handler` will be transfered into the next middleware(s).\n\nrouting is one essential and by far, the most opinonated part of any webframeworks-api. `shallot` is there no exception. Routing is defined completely via a data-structure:\n\n```python\nasync def hello_world(request):\n return text(\"hi user!\")\n\n# is attached to a \"dynamic\"-route with one parseable url-part\nasync def handle_index(request, idx):\n return text(f\"hi user number: {idx}\")\n\n\nroutes = [\n (\"/\", [\"GET\"], hello_world),\n (\"/hello\", [\"GET\"], hello_world),\n (\"/hello/{index}\", [\"GET\"], handle_index),\n (\"/echo\", [\"GET\", \"PUT\", \"POST\"], post_echo),\n (\"/json\", [\"GET\", \"PUT\"], show_and_accept_json),\n]\n\n```\nas shown above, `routes` is a list of tuples with:\n\n 1. the (potentially dynamic) route\n 2. the allowed methods\n 3. the handler\n\nroutes with an `{tag}` in it, are considered dynamic-routes. The router will parse the value from the url and transfered it (as string) to the handler-function. Therfore the handler function must accept the `request` and as many arguments as there are `{tag}`s.\n\nmaybe one controversial one upfront: trailing slashes are ignored. In the defined routes and in the matching of requests too.\n\n\n\n### JSON\nto easily work with json-data, use the json-middleware:\n```python\nbuild_server(apply_middleware(wrap_json)(handler))\n```\nevery request, that contains a content-type `application/json` will be parsed and the result will be attached to the request under the key `json`. \nWhen data body is not parseable as json, the middleware will respond with `{\"status\": 400, \"body\": \"Malformed JSON\"}`.\n\nwhen you want to return json-data as your response, use the `shallot.response` - function `json`:\n\n```python\nfrom shallot.response import json\n\nasync def json_handler(request):\n return json({\"hello\": \"world\"})\n```\n\n### Parameters\nparameters are url-encoded query-strings or bodies. To automatically parse this data use the `wrap_parameters` - middleware\n```python\n\nfrom shallot.middlewars import wrap_parameters\nbuild_server(apply_middleware(\n wrap_parameters(keep_blank_values=False, strict_parsing=False, encoding='utf-8')\n) (handler))\n```\nParameters (url-query or form-body-data) is parsed to a `dict`. The value(s) are added to a list. The middleware is mostly a wrapper to the python-builtin [urllib.parse.parse_qs](https://docs.python.org/3/library/urllib.parse.html#urllib.parse.parse_qs). All parameters to `wrap_parameters` are passed to `parse_qs`. \n\nThis middleware will add 3 keys to the request. URL-query-strings will be parsed and added to `query_params`. If the body is sent with the content-type `application/x-www-form-urlencoded`, the body will parsed and added to `form_params`. The result of merging `query_params` and `form_params` will be added to the `params`-key.\n\n```python\nasync def return_request(request):\n return request\n\n\nhandle_request = apply_middleware(wrap_parameters())(return_request)\n\nurl_request = {\"query_string\": b\"key1=0&p2=val&p2=9\"}\nprint(handle_request(url_request))\n>> {\n \"query_string\": b\"p2=9&key1=0&p2=val\",\n \"query_params\": {\n \"key1\": [\"0\"],\n \"p2\": [\"9\", \"val\"]\n },\n \"form_params\": {},\n \"params\": {\n \"key1\": [\"0\"],\n \"p2\": [\"9\", \"val\"]\n },\n}\n\nform_request = {\n \"headers\": {\"content-type\": \"application/x-www-form-urlencoded\"},\n \"body\": b\"p2=9&key1=0&p2=val\" \n}\n\nprint(handle_request(form_request))\n>> {\n \"body\": b\"p2=9&key1=0&p2=val\",\n \"query_params\": {},\n \"form_params\": {\n \"key1\": [\"0\"],\n \"p2\": [\"9\", \"val\"]\n },\n \"params\": {\n \"key1\": [\"0\"],\n \"p2\": [\"9\", \"val\"]\n },\n}\n\n\nmixed_request = {\n \"query_string\": b\"u1=0&u8=3\",\n \"headers\": {\"content-type\": \"application/x-www-form-urlencoded\"},\n \"body\": b\"p2=9&key1=0&p2=val\",\n} \nprint(handle_request(mixed_request))\n>> {\n \"body\": b\"p2=9&key1=0&p2=val\",\n \"query_string\": b\"u1=0&u8=3\",\n\n \"query_params\": {\n \"u1\": [\"0\"],\n \"u8\": [\"val\"],\n },\n \"form_params\": {\n \"key1\": [\"0\"],\n \"p2\": [\"9\", \"val\"]\n },\n \"params\": {\n \"key1\": [\"0\"],\n \"p2\": [\"9\", \"val\"],\n \"u1\": [\"0\"],\n \"u8\": [\"val\"],\n },\n}\n```\n\n*The values are treated as list on purpose. Because every key can be sent multiple-times, it is better to consequently deal with lists. Otherwise an application would have to handle 2 different types (which both support iterating/indexing), rather than with different-length lists.*\n\n\n### Static-Files\n\n`shallot` is not optimized to work as static-file-server. Altough it goes to great length, to provide a solid experience for serving static content.\n\nto work with static-files use the `wrap_static` - middleware:\n```python\nrel_path_to_folder_to_serve_from = \"/static/data\"\nbuild_server(apply_middleware(wrap_static(rel_path_to_folder_to_serve_from))(handler))\n```\n\nThis middleware depends on `aiofiles`. It will try to match the path of an request, to files in the folder `/static/data` relative to your current $PWD / %CWD%. To provide a `cwd` indepented path, call `wrap_static` with a root-path:\n\n```python\nimport os\nhere = os.path.dirname(__file__)\nwrap_static(\"/static/data\", root_path=here) # will always assume the folder is located : .py/static/data\n```\n\nBrowser-caches will be honored. For that, `last-modified` and `etag` - headers will be send accordingly. When the browser requests a already-cached resource (`if-none-match` or/and `if-modified-since`), this middleware will reply with a `304-Not Modified`.\nFor further information about browser-file-caches: [MDN:Cache validation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#Cache_validation)\n\nRequests with a path containing \"../\" will be automatically responded with `404-Not Found`.\n\n### Content-Types\n\nfor static-files it can be convenient to use the content-type-middleware: `wrap_content_type`\nSo when a resource is requested, for example: \"/static/index.html\", then this middleware will set the `content-type`-header to `text/html`\n\n```python\n\nserver = build_server(apply_middleware(\n wrap_content_type())(handler)\n)\n```\nBy defalut it will guess the content-type based on the python-builtin `mimetypes`. The default is to use `mimetypes` with non-strict evaluation. To change this behaviour one can provide a `strict=True` falg to `wrap_content`.\n\nWhen the content-type can not be guessed, \"application/octet-stream\" is used. This can be overriden via `wrap_content_type`.\n\nThis middleware will only add a `content-type`-header when none is provided in the response. \n\nAdditional type->extension-mappings can be provided to `wrap_content_type` via dict:\n\n```python\nadd_mapping = {\"application/fruit\": [\".apple\", \"orange\"]}\napply_middleware(wrap_content_type(additional_content_types=add_mapping))\n```\n\nthe key is the content-type to map to, and the value is a list of extensions (with or without leading-dot)\n\n\n### Cookies\n\nCookies are handled as dicts. To use cookie-handling one must include `wrap_cookies` in the middleware-chain.\n\n```python\nbuild_server(apply_middleware(wrap_cookies)(handler))\n```\n\n#### Receive Cookies\n\nCookies send with the request, are parsed and attached to the request-object with the key `cookies`. For further information about cookies and how to use them: [MDN:cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)\n\n```python\nasync def handler(request):\n print(request[\"cookies\"]) # prints {a-cookie-name: value, b-cookie-name: value}\n\n return {\n \"status\": 200, \n \"cookies\": {\n \"first\": {\n \"value\": 3.4,\n \"expires\": 1545335438.5059335,\n \"path\": \"/some/path\",\n \"comment\": \"usefull comment\",\n \"domain\": \"my.domain.zz\",\n \"max-age\" 3600,\n \"secure\": True,\n \"version\": 2,\n \"httponly\": True,\n },\n \"second\": {\"value\": \"value-asdf\", \"expires\": \"Thu, 20 Dec 2018 19:50:38 GMT\"},\n \"minimal\": {\"value\": 0}.\n \"to-delete\": None,\n },\n }\n```\n\n#### Set Cookies\n\nCookies are send to the client, when the response contains a `cookies`-key. The `cookies`- value is a dict, with the minimal structure:\n```python\n{\"cookie-name\": {\"value\": 0}}\n```\n\nThis will result in a *session-cookie* : `{\"cookie-name\": 0}`, which will be sent with the next request. Further data can be attached to the cookie. The supported keys are, all names that are supported by [python-std-lib:morsel](\"https://docs.python.org/3/library/http.cookies.html#http.cookies.Morsel\"):\n\n - expires\n - path\n - comment\n - domain\n - max-age\n - secure\n - version\n - httponly\n\nThe `expires` value can be set in two diffrent fashions: \n\n 1. string: the value will be sent *as-is* without further checking, whether it complies to a date-format.\n 2. int|float: the value will be interpreted as a timestamp and will be converted to a date-string\n\n#### Deleting Cookies\n\nTo delete a cookie you will need to set the cookie-value to None:\n\n```python\n{\"cookie-name\": None}\n```\nThen a cookie will be send, with an `expires`-value in the past.\n\n\n\n\n\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/peterpeter5/shallot", "keywords": "", "license": "", "maintainer": "", "maintainer_email": "", "name": "shallot", "package_url": "https://pypi.org/project/shallot/", "platform": "", "project_url": "https://pypi.org/project/shallot/", "project_urls": { "Homepage": "https://github.com/peterpeter5/shallot" }, "release_url": "https://pypi.org/project/shallot/0.1.0/", "requires_dist": [ "aiofiles", "sphinx ; extra == 'docs'", "recommonmark ; extra == 'docs'", "uvicorn ; extra == 'full'", "pytest ; extra == 'test'", "hypothesis ; extra == 'test'", "requests ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "uvicorn ; extra == 'test'" ], "requires_python": ">=3.6", "summary": "Fast, small ASGI-compliant webframework", "version": "0.1.0" }, "last_serial": 4669567, "releases": { "0.0.1": [ { "comment_text": "", "digests": { "md5": "c59abbee24e5446b97b53ce25b86663e", "sha256": "4f5b8b5226cfe2c4c356824283e641ea4d2fe7ab4c6f834c1f00200daa27cd27" }, "downloads": -1, "filename": "shallot-0.0.1-py3-none-any.whl", "has_sig": false, "md5_digest": "c59abbee24e5446b97b53ce25b86663e", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 19304, "upload_time": "2019-01-05T16:09:00", "url": "https://files.pythonhosted.org/packages/56/0e/2279fcc54fae4eb2339712e7d82db8a37afd7943f9c57325839554a529b5/shallot-0.0.1-py3-none-any.whl" } ], "0.1.0": [ { "comment_text": "", "digests": { "md5": "f761773f2ce753c2fbe6d9abf7820a44", "sha256": "dc55131c140e09b7870c68ef28f4cbe1b68a8c7c362a5b7814f0435fca50070c" }, "downloads": -1, "filename": "shallot-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "f761773f2ce753c2fbe6d9abf7820a44", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 19318, "upload_time": "2019-01-07T17:34:09", "url": "https://files.pythonhosted.org/packages/42/98/bfbead1a6435b1a1f869f462a9fce641067d12acfa013925398db7b1178a/shallot-0.1.0-py3-none-any.whl" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "f761773f2ce753c2fbe6d9abf7820a44", "sha256": "dc55131c140e09b7870c68ef28f4cbe1b68a8c7c362a5b7814f0435fca50070c" }, "downloads": -1, "filename": "shallot-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "f761773f2ce753c2fbe6d9abf7820a44", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 19318, "upload_time": "2019-01-07T17:34:09", "url": "https://files.pythonhosted.org/packages/42/98/bfbead1a6435b1a1f869f462a9fce641067d12acfa013925398db7b1178a/shallot-0.1.0-py3-none-any.whl" } ] }