weppy provides several instruments to help you dealing with requests in your application. Let's see them.
When a request comes from a client, weppy binds useful informations about it
within the request
object, which can be accessed just with an import:
from weppy import request
It contains useful information about the current processing request, in particular:
attribute | description |
---|---|
scheme | could be http or https |
method | the request HTTP method |
now | a Pendulum (datetime) object created with request |
headers | the headers of the request |
cookies | the cookies passed with the request |
client | the IP Address of the client doing the request (if available) |
environ | contains raw environment variables of the request |
isajax | boolean which states if the request was made in AJAX (check for xmlhttprequest presence in headers) |
Please keep in mind that the now
attribute will always use the UTC timezone, by default.
If you need to access the local time of the request you can use now_local
:
# request datetime in local machine timezone
request.now_local
Note: since
now
is a Pendulum object, you can easily change the timezone using thein_timezone
method, likerequest.now.in_timezone('Europe/Berlin')
.
Now, let's see how to deal with request variables.
weppy's request
object also provides three important attributes about the
active request:
attribute | description |
---|---|
query_params | contains the URL query parameters |
body_params | contains parameters passed into the request body |
params | contains both the query parameters and the body parameters |
All three attributes work in the same way, and an example may help you understand their dynamic:
from weppy import App, request
app = App(__name__)
@app.route("/post/<int:id>")
def post(id):
editor = request.params.editor
if editor == "markdown":
# code
elif editor == "html":
# code
#..
Now, when a client calls the URL /post/123?editor=markdown, the editor
parameter
will be mapped into request.params
and we can access its value simply calling
the parameter name as an attribute.
When the URL doesn't contain the query parameter you're trying to look at, this
will be None
, so it's completely safe to call it. It wont raise an exception.
Now, what happens when the client does a POST request with the following body on the URL /post/123?editor=markdown?
{
"text": "this is an example post",
"date": "2014-10-15"
}
Simple: the three request
attributes will look like this:
>>> request.params
<sdict {'date': '2014-10-15', 'text': 'this is a sample post', 'editor': 'markdown'}>
>>> request.query_params
<sdict {'editor': 'markdown'}>
>>> request.body_params
<sdict {'date': '2014-10-15', 'text': 'this is a sample post'}>
You can always access the variables you need.
Changed in 1.0
Quite often, you will need to perform operations during the request flow, for example you might need to verify certain authorization conditions before your exposed method is invoked by weppy when the request is routed trough it, or you may want to close a database connection once the request flow is ended and the response is ready to be transmitted to the client.
weppy uses a pipeline to handle the request flow trough your application, and like a water pipeline is composed of several pipes. You've already encountered some of them in the tutorial, the database and the auth ones. But how this pipes works inside the pipeline?
You can imagine the pipeline as a real water pipeline, composed of several pipes one after another:
|<---- pipeline ---->|
|-------|-------|-------|
[request] -> pipe -> pipe -> pipe -> [route]
|-------|-------|-------|
The request will flow trough the pipeline, which means will flow trough every pipe in the pipeline, and will reach the method you've exposed for that specific route.
– Ok dude. So the pipeline is just an array of functions that will perform some actions on the request?
– Not really.
The pipes are not just functions but actually objects. In fact, these pipes won't just pipe the request trough the pipeline flow, but will have several options and responsibilities on the pipeline. weppy will use several functions on these objects during the request, so the application can customize the request flow based on its needs.
Any pipe can, in fact, perform operations before the request will be piped, during the flow, or after the flow has been completed. This is because any request in weppy can be sketched in several steps above the pipeline.
First of all, weppy will open up all the pipes in the pipeline:
open open open
|------|------|------|
[request] | pipe | pipe | pipe | [route]
|------|------|------|
You can imagine the pipes have bulkheads on their entrance and before the request is actually processed all these bulkheads are opened so that the request can flow trough the pipes.
After this first step, weppy will push the request trough the pipeline:
-> -> -> ->
|------|------|------|
[request] | pipe | pipe | pipe | [route]
|------|------|------|
During this step, all the pipes will pipe the request trough the next step in the pipeline. Every step in the pipeline will compose the final flow of the request from the client to the routed method.
Then the route method will compose a response, that will flow back trough the pipeline:
<- <- <- <-
|------|------|------|
[response] | pipe | pipe | pipe | [route]
|------|------|------|
So the pipeline is actually walked by both sides and the pipes have access to the response as well.
Finally, weppy will close all the pipes and send the response to the client:
close close close
|------|------|------|
[response] | pipe | pipe | pipe | [route]
|------|------|------|
The bulkheads we imagined on the first step will be closed since the request flow is ended and nothing have to pass trough the pipeline.
All these steps lead to have this Pipe
class that you can extend to build your custom pipelines:
from weppy import Pipe
class MyPipe(Pipe):
def open(self):
pass
def close(self):
pass
def pipe(self, next_pipe, **kwargs):
return next_pipe(**kwargs)
def on_pipe_success(self):
pass
def on_pipe_failure(self):
pass
As we seen in the steps, the open
and close
method will be called before the request will flow trough the pipeline and after the response is built.
The pipe
method is the one called by weppy to pipe the request trough the pipeline: this means that every pipe is actually responsible to build the flow to the next pipe. And, this also means every pipe can alter the normal flow of the pipeline if needed.
The on_pipe_success
and on_pipe_failure
will be called as soon as the flow gets back to the pipe: the failure one will be invoked in case of an exception in any subsequent point of the pipeline, otherwise the success one will be invoked.
Notice that the close
method will be always invoked on the pipeline, even if an exception occurred.
So how you can use these pipes functions? Let's see some examples.
A pipe responsible of connecting to the database will need to open the connection on a new request, and close the connection when the request flow is ended. Then we can write a pipe like this:
class DBPipe(Pipe):
def __init__(self, db):
self.db = db
def open(self):
self.db.open_connection()
def close(self):
self.db.close_connection()
But we also can make it more smart, and have it commit what happened on the database when everything went right, or rollback the changes if something wrong happened:
class DBPipe(Pipe):
def __init__(self, db):
self.db = db
def open(self):
self.db.open_connection()
def close(self):
self.db.close_connection()
def on_pipe_success(self):
self.db.commit()
def on_pipe_failure(self):
self.db.rollback()
Then we can add this pipe to a single route:
@app.route(pipeline=[DBPipe(db)])
def foo():
#code
or on every route of our application:
app.pipeline = [DBPipe(db)]
This makes you sure every request that have this pipeline will have a correct behaviour with the database. And this is more or less what the Database.pipe
attribute does.
A second example could be a pipe that verifies the request authorization checking the value of a specific header. In this case we want to break the request flow if the client is not authorized and return an error instead of the content of the route. We can modify the pipe
method for this:
from weppy import Pipe, request, abort
class AuthPipe(Pipe):
def pipe(self, next_pipe, **kwargs):
if self.valid_header():
return next_pipe(**kwargs)
return "Bad auth"
def valid_header(self):
return request.environ.get("HTTP_MY_HEADER", "") == "MY_KEY"
Adding this pipe to a route pipeline will make you sure the request will never flow trough the next pipe unless the condition is verified. In case of an abort the response that will be available to the pipes before the one interrupting the flow will be the content returned by this pipe.
For example, in case the AuthPipe
is in the second position of our pipeline and the request is not authorized, the flow can be sketched like this:
1)
open open open
|------|------|------|
[request] | pipe | pipe | pipe | [route]
|------|------|------|
2)
-> ->
|------|------|------|
[request] | pipe | pipe | pipe | [route]
|------|------|------|
3)
<- <-
|------|------|------|
[response] | pipe | pipe | pipe | [route]
|------|------|------|
4)
close close close
|------|------|------|
[response] | pipe | pipe | pipe | [route]
|------|------|------|
The last example is about the use of a pipe in order to change the parameters passed to the routed method. Let's say, for example that you have some routes that accept a date
variable:
@app.route("/foo/<date:start>")
def foo(start):
# code
and you often need to build a strict period starting from the start
parameter, so your code looks like this:
from datetime import timedelta
@app.route("/foo/<date:start>")
def foo(start):
end = start + timedelta(days=7)
Then you can easily inject this to your routes writing a pipe:
class PeriodPipe(Pipe):
def __init__(self, days):
self.dt = timedelta(days=days)
def pipe(self, next_pipe, **kwargs):
kwargs['end'] = kwargs['start'] + self.dt
return next_pipe(**kwargs)
and using it on every route you need:
@app.route("/foo/daily/<date:start>", pipeline=[PeriodPipe(1)])
def foo_daily(start, end):
# code
@app.route("/foo/weekly/<date:start>", pipeline=[PeriodPipe(7)])
def foo_weekly(start, end):
# code
@app.route("/foo/monthly/<date:start>", pipeline=[PeriodPipe(30)])
def foo_monthly(start, end):
# code
Another common scenario you may encounter while building your application is when you need to add the same contents to your exposed functions' outputs, to make them available for the templates.
For example, let's say you have a function that makes your datetimes objects prettier:
>>> prettydate(datetime.now()-timedelta(days=1))
'One day ago'
And you want to use it in your templates:
{{for post in posts:}}
<div class="post">
<div class="post-date">{{=prettydate(post.date)}}</div>
<div class="post-content">{{=post.text}}</div>
</div>
{{pass}}
Instead of adding prettydate
to every exposed function, you can write down an injector:
from weppy import Injector
class DateInjector(Injector):
@staticmethod
def prettydate(d):
# your prettydate code
app.injectors = [DateInjector()]
and you can access your prettydate
function in every template.
So, basically, the Injector
class of weppy adds everything you define inside it (functions and attributes) into your exposed functions' returning dictionary.
Speaking of handling requests, you would like to perform specific actions on errors.
If we look at the given example for the request.params
again, what happens when
the user calls the URL without passing the editor
query parameter?
Maybe you want to redirect the client with a default parameter:
from weppy import redirect, url
@app.route("/post/<int:id>")
def post(id):
editor = request.params.editor
if editor == "markdown":
# code
elif editor == "html":
# code
else:
redirect(url('post', id, params={'editor': 'markdown'}))
which means that, when the editor
var is missing, we force the user to markdown.
The redirect
function of weppy accepts a string for the URL, and acts like
an exception, interrupting the execution of your code.
Maybe, you prefer to show your 404 page:
from weppy import abort
@app.on_error(404)
def not_found():
return app.render_template("404.html")
@app.route("/post/<int:id>")
def post(id):
editor = request.params.editor
if editor == "markdown":
# code
elif editor == "html":
# code
else:
abort(404)
That's all it takes.
So you've just learned three handy aspects of weppy:
redirect
and abort
allow you to stop the execution of your code;app.on_error()
;app.render_template()
to render a specific template without the presence of an exposed function or a specific context.