{"id":18051780,"url":"https://github.com/joegasewicz/bobtail","last_synced_at":"2025-07-18T17:31:36.844Z","repository":{"id":62950699,"uuid":"560642037","full_name":"joegasewicz/bobtail","owner":"joegasewicz","description":"An OOP Python WSGI http framework","archived":false,"fork":false,"pushed_at":"2024-03-08T00:58:51.000Z","size":8729,"stargazers_count":20,"open_issues_count":12,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-06-11T18:18:23.908Z","etag":null,"topics":["http","http-server","python-web","restful","web","webframework","wsgi","wsgi-framework"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/bobtail/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joegasewicz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-11-02T00:05:00.000Z","updated_at":"2025-01-14T13:30:26.000Z","dependencies_parsed_at":"2025-04-10T18:15:31.640Z","dependency_job_id":"ff24d87d-1bf9-4423-9426-442de616eb73","html_url":"https://github.com/joegasewicz/bobtail","commit_stats":null,"previous_names":[],"tags_count":30,"template":false,"template_full_name":null,"purl":"pkg:github/joegasewicz/bobtail","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joegasewicz%2Fbobtail","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joegasewicz%2Fbobtail/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joegasewicz%2Fbobtail/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joegasewicz%2Fbobtail/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joegasewicz","download_url":"https://codeload.github.com/joegasewicz/bobtail/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joegasewicz%2Fbobtail/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265002725,"owners_count":23696091,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["http","http-server","python-web","restful","web","webframework","wsgi","wsgi-framework"],"created_at":"2024-10-30T22:55:56.135Z","updated_at":"2025-07-18T17:31:36.788Z","avatar_url":"https://github.com/joegasewicz.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Python package](https://github.com/joegasewicz/bobtail/actions/workflows/python-package.yml/badge.svg)](https://github.com/joegasewicz/bobtail/actions/workflows/python-package.yml)\n[![Upload Python Package](https://github.com/joegasewicz/bobtail/actions/workflows/python-publish.yml/badge.svg)](https://github.com/joegasewicz/bobtail/actions/workflows/python-publish.yml)\n[![GitHub license](https://img.shields.io/github/license/joegasewicz/bobtail)](https://github.com/joegasewicz/bobtail/blob/master/LICENSE.md)\n\n[//]: # (![PyPI - Python Version]\u0026#40;https://img.shields.io/pypi/pyversions/bobtail\u0026#41;)\n\n![Bobtail](bobtail.png?raw=true \"Bobtail\")\nA little Python http framework\n\n⚠️ *Ready to use in `v0.1.0`, production ready in `v1.0.0`*\n\nRead the [docs](https://bobtail.readthedocs.io/en/latest/)\n\n## Install\n```\npipenv install bobtail\npipenv install gunicorn\n```\n\n### Getting Started\nAn example of the smallest Bobtail app\n```python\nfrom bobtail import BobTail, AbstractRoute, Request, Response\n\nclass Images:\n\n    def get(self, req, res):\n        res.set_body({id: 1})\n\nroutes = [\n    (Images(), \"/images\")\n]\n\napp = BobTail(routes=routes)\n```\n\n### Run\n```\npipenv run  gunicorn api:app\n```\n### Options\nTo define port, static directory, template directory etc. you can\ncreate a concrete version of the BaseOptions abstract class. See the [docs](https://bobtail.readthedocs.io/en/latest/options.html) for more info.\n```python\nfrom bobtail.options import BaseOptions\n\nclass Options(BaseOptions):\n    PORT = 8001\n\napp = Bobtail(Options)\n```\n### Middleware\nBobtail middleware\n\n#### Using third party middleware\n```python\nfrom bobtail_logger import BobtailLogger\n\napp = Bobtail(routes=routes)\n\n# Here we are using `bobtail-logger` logging middleware\napp.use(BobtailLogger())\n```\n\nMiddleware currently available\n- [bobtail-cors](https://github.com/joegasewicz/bobtail-cors)\n- [bobtail-logger](https://github.com/joegasewicz/bobtail-logger)\n- [bobtail-upload](https://github.com/joegasewicz/bobtail-upload)\n- [bobtail-jinja2](https://github.com/joegasewicz/bobtail-jinja2)\n\n\nCreating custom middleware example. A Middleware object must implement `AbstractMiddleware`. \n\n```python\nfrom bobtail import Request, Response\nfrom bobtail.middleware import AbstractMiddleware, Tail\n\nclass BobtailCors(AbstractMiddleware):\n\n    def run(self, req: Request, res: Response, tail: Tail) -\u003e None:\n        res.set_headers({\n            \"Access-Control-Allow-Origin\": \"*\",\n        })\n        tail(req, res)\n```\n\n### HTML Templates\nBobtail does not ship with a templating engine directly, but you can install and use\na templating engine with ease via middleware.\n\nCurrently, there is middleware support for Jinja2, for example\n```python\nfrom bobtail_jinja2 import BobtailJinja2\n\nblog = BobTail(routes=routes)\nblog.use(BobtailJinja2(template_dir=\"templates\"))\n```\nThen to use in a request handler\n```python\ndef get(self, req: Request, res: Response) -\u003e None:\n    res.jinja2.render(res, \"layout.jinja2\", data={\"name\": \"joe\"})\n```\n\n### Set the Headers\nYou can set the headers with the `Response` object's `set_headers` method. The default headers\nare `Content-Type: application/json`.\n```python\nclass Images:\n\n    def get(self, req, res):\n        res.set_headers({\"Content-type\": \"text/plain\"})\n\n```\n\n### Set the response status\nYou can set the status with the `Response` object's `set_status` method. The default status\nis always set to `200` if there are no errors.\n```python\nclass Images:\n\n    def get(self, req, res):\n        res.set_status(202)\n\n```\n## Request\n\n### Request Args\nYou can specify the type of `Request` arguments using curly braces \u0026 within the name \u0026 type seperated \nby a colon, for example:\n```\n/images/{id:int}/{name:str}/{is_raining:bool}\n```\nTo access request arguments inside a route handler, use the `Request` object's `get_arg` method, for example:\n```python\ndef get(self, req, res):\n    id = req.get_args(\"id\") # int\n    name = req.get_args(\"name\") # str\n    is_raining = req.get_args(\"is_raining\") # bool\n```\n\n### Request Body\n- JSON\n ```python\n# marshals json to a python dict\nreq.get_json()\n```\n- Plain Text\n ```python\n# returns a string\nreq.get_body()\n```\n- Urlencoded form data\n ```python\n# returns a pyton dict\nreq.get_form_data()\n```\n- Multipart form data\n ```python\n# returns a pyton dict\nreq.get_multipart_data()\n```\n\nThe `Request` object provides methods to easily get form values. By default, if a form value\ndoesn't exist, then either `FormDataError` or `MultipartFormDataError` exceptions will be raised.\n\n- Get Form Field Value\n```python\nfrom bobtail.exceptions import FormDataError\ntry:\n    email = req.form.get_field(\"email\")\nexcept FormDataError:\n    pass # handle no form value\n```\n\n- Get Multipart Form Field Value\n```python\nfrom bobtail.exceptions import MultipartFormDataError\ntry:\n    email = req.multipart.get_field(\"email\")\nexcept MultipartFormDataError:\n    pass # handle no multipart form value\n```\n\n- Get Multipart Form File Value\n```python\nfrom bobtail.exceptions import MultipartFormDataError\ntry:\n    email = req.multipart.get_file(\"image\")\nexcept MultipartFormDataError:\n    pass # handle no multipart form value\n```\n\n- Get Multipart Form File Name\n```python\nfrom bobtail.exceptions import MultipartFormDataError\ntry:\n    email = req.multipart.get_name(\"image\")\nexcept MultipartFormDataError:\n    pass # handle no multipart form value\n```\n\n- Get Multipart Form File Data\n```python\nfrom bobtail.exceptions import MultipartFormDataError\ntry:\n    email = req.multipart.get_data(\"image\")\nexcept MultipartFormDataError:\n    pass # handle no multipart form value\n```\n\n- Get Multipart Form File Mimetype\n```python\nfrom bobtail.exceptions import MultipartFormDataError\ntry:\n    email = req.multipart.get_mimetype(\"image\")\nexcept MultipartFormDataError:\n    pass # handle no multipart form value\n```\n### Query Params\nThis method returns a dict og query params where the key\nis on the left side of the `=` sign \u0026 the value is pn the right.\nFor example:\n```python\n# for route \"/images?name=joe\u0026age=48\"\ndef get(self, req: Request, res: Response):\n    result = req.get_params() # {\"name\": \"joe\", \"age\": \"48\"}\n```\n\n### Static Files\nTo declare a static route postfix a `*` to the route's path::\n```python\nfrom bobtail import BobTail AbstractRoute, BaseOptions\nfrom bobtail_jinja2 import BobtailJinja2\n\nroutes = [\n    (Static(), \"/static/*\"),\n]\n\nclass Options(BaseOptions):\n    STATIC_DIR = \"app/static\"\n    TEMPLATE_DIR = \"app/templates\"\n\nblog = BobTail(routes=routes, options=Options())\nblog.use(BobtailJinja2(template_dir=\"app/templates\"))\n```\nCalling `set_static` from within a route method will render a static\nfile such as a .css, .js or a media type file. The :class:`~BaseOptions`\nclass sets the `STATIC_DIR` directory.\n```python\n\n    class Static(AbstractRoute):\n        def get(self, req: Request, res: Response) -\u003e None:\n            res.set_static(req.path)\n```\nYou can set the static file path using the :class:`~BaseOptions`.\n```python\n\nclass Options(BaseOptions):\n    STATIC_DIR = \"/static\"\n\n# Now in a route handler we can access static directory the via options\nclass Static(AbstractRoute):\n    def get(self, req: Request, res: Response) -\u003e None:\n        res.set_static(req.path)\n```\nBy default, `STATIC_DIR` is set to `/static`, if your static file is nested\nwithin a Python package, for example `app/static` the set as `STATIC_DIR = \"app/static\"`\n\nTo render an image from within a Jinja2 template include the full path including the\nstatic directory name or path. For example::\n```html\n\u003c!-- if STATIC_DIR = \"/static\" --\u003e\n\u003cbody\u003e\n    \u003cimg src=\"/static/imgs/cat1.jpg\" /\u003e\n\u003c/body\u003e\n```\nOR without the first forward slash::\n```html\n\u003cbody\u003e\n    \u003cimg src=\"static/imgs/cat1.jpg\" /\u003e\n\u003c/body\u003e\n```\n### OOP Approach\nIf you prefer to organise your routes in a more OOP approach, you can implement the\n`AbstractRoute` abstract class. It's especially useful when using an IDE like Pycharm\nwhere the IDE will generate automatically all the require methods.\n```python\nfrom bobtail import AbstractRoute, Request, Response\n\n# (Pycharm) - right click over the `Image` class name \u0026 select `Show context actions`\n# then click `implement abstract methods`, then select all and click ok.\nclass Images(AbstractRoute): \n    pass\n```\nWhich will generate the following:\n\n```python\nfrom bobtail import AbstractRoute, Request, Response\n\n\nclass Images(AbstractRoute):\n    def get(self, req: Request, res: Response):\n        pass\n    \n    def post(self, req: Request, res: Response):\n        pass\n\n    def put(self, req: Request, res: Response):\n        pass\n\n    def delete(self, req: Request, res: Response):\n        pass\n\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoegasewicz%2Fbobtail","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoegasewicz%2Fbobtail","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoegasewicz%2Fbobtail/lists"}