Thunderargs: practice of use. Part 1
- Tutorial
I recently wrote a post about how thunderargs was invented and written . Today I will talk about how it can be applied.
Let me remind you that this thing is designed to process function parameters using annotations. For example, like this:
We will try to solve quite specific problems during the tutorial, and not some ephemeral problems. Well, now - to the point.
Today in all examples (or almost all) we will use Flask. Those who are even a little familiar with this framework are well aware that the problem of extracting arguments from forms is pain and humiliation. Well, in addition, in the last topic, I already wrote a piece that allows you to use thunderargs in conjunction with flask without unnecessary troubles.
By the way, you can pick up all the code in the examples from here . You need the flask-example file.
Read more about annotation syntax here .
And here we look only at what we really need: the syntax for describing the arguments. It is done like this:
After that, we can access the description of the arguments through the
As we can see, here we have the names of annotated variables and the calculated expressions as dictionary values. This means that we can shove in the annotation any arbitrary expressions that will be calculated during the declaration of the function. This is the chip we use. If you want to know exactly how - you are welcome to read the post, a link to which is given at the beginning of this.
I did upload this thing to PyPI at the request of some dude, so you can safely install it through pip. The only correction: some features that we will touch on in the manual are only in the alpha version, so I advise you to use
And do not forget to put flask! In theory, it is not required for thunderargs to work, but in the current manual we will use it.
The simplest use case for thunderargs is type casting. Flask has such an unpleasant feature: it does not have any means for preprocessing arguments, and they have to be processed directly in the body of endpoint functions.
Suppose we want to write simple pagination. We will have two parameters: offset and limit.
All that is needed for this is to indicate the type of data to which these arguments should be given:
Please note that hereinafter I use not the classic Flask, but the version with the replaced function
So, we were able to cram type casts into annotations, and now we no longer have to do stupid operations like these:
in the body of the function. Already not bad. But the trouble is: there are still a huge number of unaccounted for probabilities. What if someone guesses to enter a negative limit value? What if someone doesn't indicate any value at all? What if someone enters not a number? Don’t worry, there are tools to deal with these exceptions, and we will consider them.
As long as our current example suits us, we will not come up with anything new, we will just complement it.
The default values, in my opinion, are set very intuitively:
We will not dwell on this in more detail yet. Except, perhaps, one fact: the default value should be an instance of the specified class. In our case, for example, it will not work
I think everything is clear with the code. But I have to clarify something: you cannot use default and required at the same time. Such an attempt will raise an error. This is a kind of guard against a possible logical error, which then will be very difficult to find.
And if you don’t give the server the argument you need, you will get an error
Everything here is also quite obvious. Except, perhaps, that as a parameter to our function will come
If someone has already forgotten, such arguments are thrown to us when the user specifies many values for one name. Request example:
Of course, the default value in this case should be presented in the form of a list, and each argument of this list must satisfy all conditions.
Let us return to our example with the paginator, which we promised to bring to mind.
Validators are created on a factory farm called validfarm. There are now only the most primitive options, like len_gt, val_neq and so on, but in the future, I think the list will be updated.
But no one bothers to make us your validator. It should be simply a function that receives a value and returns a Boolean response whether this value satisfies it or not.
Or even like this:
In general, absolutely any thing that can be called, which can work when only one argument is passed to it, and which returns a Boolean answer, will do for the validator.
Very often it happens that we need to get a key from the user that tells us which argument we have to work with. It is for this case that we need a scan.
To demonstrate, I will again give the function that I already mentioned at the beginning. I think this time everything will be clearer here.
As you can see,
Today we’ll finish with a functional review. The only thing I would like to make a couple of extra-manual comments.
In principle, I did not use anything that would make it impossible to reverse transfer this software to the second python. Need to replace string formatting. And, perhaps, that's all. To emulate annotations in the module
In theory, it should work, although in practice it has not been tested.
In the article, we considered only the GET method, but this does not mean that other methods are not supported. I chose it just so as not to bother. But there is one subtlety: I believe that multiple methods for one objective function are not needed, and therefore, now each function can only be responsible for one method. In my opinion, this makes the code more readable.
If you really need a native flask routing, use it
In theory, interactions with other flask modules should not break. But practice will show.
You can use path variables and parameters from annotations at the same time. They do not conflict with each other if any of the parameters is not both.
It should be remembered that thunderargs works fine even without a flask. To do this, you need to use the decorator
However, do not abuse it. Actually, hardcore argument processing is needed only on controllers.
Do not forget that you are easier than easy to create your descendants from
Most of the code was drunk. Some are in a very sleepy state. The code needs optimization, but it already works somehow. Development and running is ongoing, so if you suddenly decide to help, join in. Any help will go, and especially testing. I, as in the old joke about Chukchi, not a reader, but a writer. For now.
You can write any suggestions, wishes and criticism here, in the comments, or here .
By the way, much to my regret, May English from the notes of sow gud es ay nid that translate text is written, so if anyone does this, I will be very grateful :)
The second part will be slightly esoteric, but, as I hope, very interesting. But, unfortunately, it will not be very soon.
Let me remind you that this thing is designed to process function parameters using annotations. For example, like this:
OPERATION = {'+': lambda x, y: x+y,
'-': lambda x, y: x-y,
'*': lambda x, y: x*y,
'/': lambda x, y: x/y,
'^': lambda x, y: pow(x,y)}
@Endpoint
def calculate(x:Arg(int), y:Arg(int),
op:Arg(str, default='+', expander=OPERATION)):
return str(op(x,y))
We will try to solve quite specific problems during the tutorial, and not some ephemeral problems. Well, now - to the point.
Today in all examples (or almost all) we will use Flask. Those who are even a little familiar with this framework are well aware that the problem of extracting arguments from forms is pain and humiliation. Well, in addition, in the last topic, I already wrote a piece that allows you to use thunderargs in conjunction with flask without unnecessary troubles.
By the way, you can pick up all the code in the examples from here . You need the flask-example file.
Go
Step 0: annotation syntax, or get rid of the magic effect
Read more about annotation syntax here .
And here we look only at what we really need: the syntax for describing the arguments. It is done like this:
def foo(a: expression, b: expression):
...
After that, we can access the description of the arguments through the
__annotations__function field foo:>>> def foo(a: "bar", b: 5+3):
pass
>>> foo.__annotations__
{'b': 8, 'a': 'bar'}
As we can see, here we have the names of annotated variables and the calculated expressions as dictionary values. This means that we can shove in the annotation any arbitrary expressions that will be calculated during the declaration of the function. This is the chip we use. If you want to know exactly how - you are welcome to read the post, a link to which is given at the beginning of this.
Step 0.5: installation
I did upload this thing to PyPI at the request of some dude, so you can safely install it through pip. The only correction: some features that we will touch on in the manual are only in the alpha version, so I advise you to use
--pre:sudo pip install thunderargs --pre
And do not forget to put flask! In theory, it is not required for thunderargs to work, but in the current manual we will use it.
Step 1: Elementary Type Casting
The simplest use case for thunderargs is type casting. Flask has such an unpleasant feature: it does not have any means for preprocessing arguments, and they have to be processed directly in the body of endpoint functions.
Suppose we want to write simple pagination. We will have two parameters: offset and limit.
All that is needed for this is to indicate the type of data to which these arguments should be given:
from random import randrange
from thunderargs.flask import Flask
app = Flask()
# Just a random sequence
elements = list(map(lambda x: randrange(1000000), range(100)))
@app.route('/step1')
def step1(offset: Arg(int), limit: Arg(int)):
return str(elements[offset:offset+limit])
if __name__ == '__main__':
app.run(debug=True)
Please note that hereinafter I use not the classic Flask, but the version with the replaced function
routethat I import from thunderargs.flask. So, we were able to cram type casts into annotations, and now we no longer have to do stupid operations like these:
offset = int(request.args.get('offset'))
limit = int(request.args.get('limit'))
in the body of the function. Already not bad. But the trouble is: there are still a huge number of unaccounted for probabilities. What if someone guesses to enter a negative limit value? What if someone doesn't indicate any value at all? What if someone enters not a number? Don’t worry, there are tools to deal with these exceptions, and we will consider them.
Step 2: default value
As long as our current example suits us, we will not come up with anything new, we will just complement it.
The default values, in my opinion, are set very intuitively:
@app.route('/step2')
def step2(offset: Arg(int, default=0),
limit: Arg(int, default=20)):
return str(elements[offset:offset+limit])
We will not dwell on this in more detail yet. Except, perhaps, one fact: the default value should be an instance of the specified class. In our case, for example, it will not work
[0,2,5]as a default value.Step 3: required argument
@app.route('/step3')
def step3(username: Arg(required=True)):
return "Hello, {}!".format(username)
I think everything is clear with the code. But I have to clarify something: you cannot use default and required at the same time. Such an attempt will raise an error. This is a kind of guard against a possible logical error, which then will be very difficult to find.
And if you don’t give the server the argument you need, you will get an error
thunderargs.errors.ArgumentRequired.Step 4: multiple argument
Everything here is also quite obvious. Except, perhaps, that as a parameter to our function will come
map object, not a list.@app.route('/step4')
def step4(username: Arg(required=True, multiple=True)):
return "Hello, {}!".format(" and ".join(", ".join(username).rsplit(', ', 1)))
If someone has already forgotten, such arguments are thrown to us when the user specifies many values for one name. Request example:
?username=John&username=Adam&username=Lucas
Of course, the default value in this case should be presented in the form of a list, and each argument of this list must satisfy all conditions.
Step 5: validators
Let us return to our example with the paginator, which we promised to bring to mind.
from thunderargs.validfarm import val_gt, val_lt
@app.route('/step5')
def step5(offset: Arg(int, default=0, validators=[val_gt(-1),
val_lt(len(elements))]),
limit: Arg(int, default=20, validators=[val_lt(21)])):
return str(elements[offset:offset+limit])
Validators are created on a factory farm called validfarm. There are now only the most primitive options, like len_gt, val_neq and so on, but in the future, I think the list will be updated.
But no one bothers to make us your validator. It should be simply a function that receives a value and returns a Boolean response whether this value satisfies it or not.
def step5(offset: Arg(int, default=0, validators=[lambda x: x >= 0 and
x < len(elements)]),
limit: Arg(int, default=20, validators=[val_lt(21)])):
...
Or even like this:
def less_than_21(x):
return x < 21
@app.route('/step5_5')
def step5_5(offset: Arg(int, default=0, validators=[lambda x: x >= 0 and
x < len(elements)]),
limit: Arg(int, default=20, validators=[less_than_21])):
...
In general, absolutely any thing that can be called, which can work when only one argument is passed to it, and which returns a Boolean answer, will do for the validator.
Step 6: expand the arguments
Very often it happens that we need to get a key from the user that tells us which argument we have to work with. It is for this case that we need a scan.
To demonstrate, I will again give the function that I already mentioned at the beginning. I think this time everything will be clearer here.
OPERATION = {'+': lambda x, y: x+y,
'-': lambda x, y: x-y,
'*': lambda x, y: x*y,
'^': lambda x, y: pow(x,y)}
@app.route('/step6')
def step6(x:Arg(int), y:Arg(int),
op:Arg(str, default='+', expander=OPERATION)):
return str(op(x,y))
As you can see,
opwe are pulled out by the key that we received from the user. expandermay be a dictionary or a called object. There you can shove a function that, for example, will extract the desired object for us from the database using the specified key. Today we’ll finish with a functional review. The only thing I would like to make a couple of extra-manual comments.
Extra comments
Python 2 or option for fossils
In principle, I did not use anything that would make it impossible to reverse transfer this software to the second python. Need to replace string formatting. And, perhaps, that's all. To emulate annotations in the module
thunderargs.endpointthere is a simple decorator called annotate. In short, use it like this:@annotate(username=Arg())
def foo(username):
...
In theory, it should work, although in practice it has not been tested.
Flask small notes
In the article, we considered only the GET method, but this does not mean that other methods are not supported. I chose it just so as not to bother. But there is one subtlety: I believe that multiple methods for one objective function are not needed, and therefore, now each function can only be responsible for one method. In my opinion, this makes the code more readable.
If you really need a native flask routing, use it
app.froute. But do not forget that annotation tokens do not work there. In theory, interactions with other flask modules should not break. But practice will show.
You can use path variables and parameters from annotations at the same time. They do not conflict with each other if any of the parameters is not both.
Small notes on non-flask
It should be remembered that thunderargs works fine even without a flask. To do this, you need to use the decorator
Endpointfrom functions thunderargs.Endpoint. However, do not abuse it. Actually, hardcore argument processing is needed only on controllers.
Do not forget that you are easier than easy to create your descendants from
Arg. IntArg, StringArg, BoolArg and so on. Such optimization can significantly reduce the number of characters in a function declaration and increase code readability.We are working on it
Most of the code was drunk. Some are in a very sleepy state. The code needs optimization, but it already works somehow. Development and running is ongoing, so if you suddenly decide to help, join in. Any help will go, and especially testing. I, as in the old joke about Chukchi, not a reader, but a writer. For now.
You can write any suggestions, wishes and criticism here, in the comments, or here .
By the way, much to my regret, May English from the notes of sow gud es ay nid that translate text is written, so if anyone does this, I will be very grateful :)
The second part will be slightly esoteric, but, as I hope, very interesting. But, unfortunately, it will not be very soon.