Package: bobtail
A little Python http framework⚠️Ready to use inv0.1.0, production ready inv1.0.0Read thedocsInstallpipenv install bobtail
pipenv install gunicornGetting StartedAn example of the smallest Bobtail appfromtypingimportTuple,Optional,DictfrombobtailimportBobTail,AbstractRoute,Request,ResponseclassImages:defget(self,req,res):res.set_body({id:1})routes=[(Images(),"/images")]app=BobTail(routes=routes)Runpipenv run  gunicorn api:appMiddlewareBobtail middlewareUsing third party middlewarefrombobttail_loggerimportBobtailLoggerapp=Bobtail(routes=routes)# Here we are using `bobtail-logger` logging middlewareapp.use(BobtailLogger())Middleware currently availablebobtail-corsbobtail-loggerbobtail-uploadbobtail-jinja2Creating custom middleware example. A Middleware object must implementAbstractMiddleware.frombobtailimportRequest,Responsefrombobtail.middlewareimportAbstractMiddleware,TailclassBobtailCors(AbstractMiddleware):defrun(self,req:Request,res:Response,tail:Tail)->None:res.set_headers({"Access-Control-Allow-Origin":"*",})tail(req,res)HTML TemplatesBobtail does not ship with a templating engine directly, but you can install and use
a templating engine with ease via middleware.Currently, there is middleware support for Jinja2, for examplefrombobtail_jinja2importBobtailJinja2blog=BobTail(routes=routes)blog.use(BobtailJinja2(template_dir="templates"))Then to use in a request handlerdefget(self,req:Request,res:Response)->None:res.jinja2.render(res,"layout.jinja2",data={"name":"joe"})Set the HeadersYou can set the headers with theResponseobject'sset_headersmethod. The default headers
areContent-Type: application/json.classImages:defget(self,req,res):res.set_headers({"Content-type":"text/plain"})Set the response statusYou can set the status with theResponseobject'sset_statusmethod. The default status
is always set to200if there are no errors.classImages:defget(self,req,res):res.set_status(202)RequestRequest ArgsYou can specify the type ofRequestarguments using curly braces & within the name & type seperated
by a colon, for example:/images/{id:int}/{name:str}/{is_raining:bool}To access request arguments inside a route handler, use theRequestobject'sget_argmethod, for example:defget(self,req,res):id=req.get_args("id")# intname=req.get_args("name")# stris_raining=req.get_args("is_raining")# boolRequest BodyJSON# marshals json to a python dictreq.get_json()Plain Text# returns a stringreq.get_body()Urlencoded form data# returns a pyton dictreq.get_form_data()Multipart form data# returns a pyton dictreq.get_multipart_data()TheRequestobject provides methods to easily get form values. By default, if a form value
doesn't exist, then eitherFormDataErrororMultipartFormDataErrorexceptions will be raised.Get Form Field Valuefrombobtail.exceptionsimportFormDataErrortry:email=req.form.get_field("email")exceptFormDataError:pass# handle no form valueGet Multipart Form Field Valuefrombobtail.exceptionsimportMultipartFormDataErrortry:email=req.multipart.get_field("email")exceptMultipartFormDataError:pass# handle no multipart form valueGet Multipart Form File Valuefrombobtail.exceptionsimportMultipartFormDataErrortry:email=req.multipart.get_file("image")exceptMultipartFormDataError:pass# handle no multipart form valueGet Multipart Form File Namefrombobtail.exceptionsimportMultipartFormDataErrortry:email=req.multipart.get_name("image")exceptMultipartFormDataError:pass# handle no multipart form valueGet Multipart Form File Datafrombobtail.exceptionsimportMultipartFormDataErrortry:email=req.multipart.get_data("image")exceptMultipartFormDataError:pass# handle no multipart form valueGet Multipart Form File Mimetypefrombobtail.exceptionsimportMultipartFormDataErrortry:email=req.multipart.get_mimetype("image")exceptMultipartFormDataError:pass# handle no multipart form valueQuery ParamsThis method returns a dict og query params where the key
is on the left side of the=sign & the value is pn the right.
For example:# for route "/images?name=joe&age=48"defget(self,req:Request,res:Response):result=req.get_params()# {"name": "joe", "age": "48"}Static FilesTo declare a static route postfix a*to the route's path::frombobtailimportBobTailAbstractRoute,BaseOptionsfrombobtail_jinja2importBobtailJinja2routes=[(Static(),"/static/*"),]classOptions(BaseOptions):STATIC_DIR="app/static"TEMPLATE_DIR="app/templates"blog=BobTail(routes=routes,options=Options())blog.use(BobtailJinja2(template_dir="app/templates"))Callingset_staticfrom within a route method will render a static
file such as a .css, .js or a media type file. The :class:~BaseOptionsclass sets theSTATIC_DIRdirectory.classStatic(AbstractRoute):defget(self,req:Request,res:Response)->None:res.set_static("/static/imgs/cat1.jpg")You can set the static file path using the :class:~BaseOptions.classOptions(BaseOptions):STATIC_DIR="/static"# Now in a route handler we can access static directory the via optionsclassStatic(AbstractRoute):defget(self,req:Request,res:Response)->None:res.set_static(f"{res.options.STATIC_DIR}/imgs/cat1.jpg")By default,STATIC_DIRis set to/static, if your static file is nested
within a Python package, for exampleapp/staticthe set asSTATIC_DIR = "app/static"To render an image from within a Jinja2 template include the full path including the
static directory name or path. For example::<!-- if STATIC_DIR = "/static" --><body><imgsrc="/static/imgs/cat1.jpg"/></body>OR without the first forward slash::<body><imgsrc="static/imgs/cat1.jpg"/></body>OOP ApproachIf you prefer to organise your routes in a more OOP approach, you can implement theAbstractRouteabstract class. It's especially useful when using an IDE like Pycharm
where the IDE will generate automatically all the require methods.frombobtailimportAbstractRoute,Request,Response# (Pycharm) - right click over the `Image` class name & select `Show context actions`# then click `implement abstract methods`, then select all and click ok.classImages(AbstractRoute):passWhich will generate the following:frombobtailimportAbstractRoute,Request,ResponseclassImages(AbstractRoute):defget(self,req:Request,res:Response):passdefpost(self,req:Request,res:Response):passdefput(self,req:Request,res:Response):passdefdelete(self,req:Request,res:Response):pass