Cookbook¶
Query String¶
yhttp
will dispatch query string to the python’s keywordonly
argument
if defined in handler, see foo
argument in the example below.
of-course, all query string will available as a dictionary via
req.query
.
@app.route()
@text
def get(req, *, foo=None):
return f'{foo} {req.query.get("bar")}'
app.ready()
A painless way to test our code is bddrest.
from bddrest import Given, response, when, given
with Given(app, '/?foo=foo&bar=bar'):
assert response.text == 'foo bar'
when(query=given - 'foo')
assert response.text == 'None bar'
Form¶
Use req.form
as a dictionary to access the submitted fields.
New in version 2.6: An easy way to get form values is:
req['field-name']
The above expression is the same as:
req.form['field-name']
from yhttp import Application, text, statuses
app = Application()
@app.route()
@text
def post(req):
try:
return f'{req["foo"]}'
except KeyError:
raise statuses.badrequest()
app.ready()
from bddrest import Given, response, when, given, status
with Given(app, verb='POST', form={'foo': 'bar'}):
assert response.text == 'bar'
when(form=given - 'foo')
assert status == 400
the form=
parameter of the Given
and when
functions will send the
given dictionary as a urlencoded
HTTP form, but you can try json
and
multipart
content types to ensure all API users will be happy!
with Given(app, verb='POST', json={'foo': 'bar'}):
assert response.text == 'bar'
with Given(app, verb='POST', multipart={'foo': 'bar'}):
assert response.text == 'bar'
Settings¶
Use app.settings
attribute to update global
settings instance for the application. this is an instance of
pymlconf.Root
.
To update configuration just use the pymlconf.Mergable.merge()
or
pymlconf.Root.loadfile()
methods of the Application.settings
Just remember configration format is yaml.
app.settings.merge('''
db:
url: postgres://user:pass@host/db
''')
app.settings.loadfile('path/to/conf.yml')
Then use your configration keys like:
url = app.settings.db.url
Note
Do not update the app.settings
instance
after the Application.ready()
is called.
See also
Debug Flag¶
You can do:
app.settings.debug = False
Or:
app.settings.merge('debug: false') # YAML syntax
To prevent write stacktrace on error responses.
HTTP Status¶
There are tree ways to set HTTP status code for response:
use
statuscode()
decorator.raise an instance of
HTTPStatus
classset
req.response.status
directly.
These are some builtin HTTP status factory functions:
statuses.internalservererror()
See the example below for usage:
from yhttp import statuses
@app.route()
def get(req):
raise statuses.notfound()
app.ready()
This is how to use statuscode()
decorator to specify response status
code for all requests.
from yhttp import statuscode
@app.route()
@statuscode('201 Created')
def get(req):
return b'Hello'
app.ready()
HTTP Redirect¶
To redirect the request to another location raise a
statuses.movedpermanently()
or statuses.found()
raise statuses.found('http://example.com')
Custom HTTP Status¶
Use statuses.status()
to raise your very own status code and text.
raise statuses.status(700, 'Custom Status Text')
Or set req.response.status
directly.
@app.route()
def get(req):
req.response.status = '201 Created'
return ...
Routing¶
the only way to register handler for http requests is
Application.route()
decorator factory.
@app.route() # Default route
def get(req):
...
@app.route('/foo') # Not match with: /foo/bar
def get(req):
...
@app.route('/books/(\d+)') # Match with: /books/1
def get(req, id):
...
Handler function’s name will be used as HTTP verb. so, the get
in the
example above stands for the HTTP GET
method.
Any Verb¶
Another approach is to us a single star *
to catch any verb.
@app.route(verb='*') # Match any HTTP verb
def any(req):
...
New in version 3.1.
Static Contents¶
Application
class has two methods: Application.staticfile()
and Application.staticdirectory()
to complete this mission!
app.staticfile(r'/a\.txt', 'path/to/a.txt')
app.staticdirectory(r'/foo/', 'path/to/foo/directory')
app.staticdirectory(r'/foo/', 'path/to/foo/directory', default='index.txt')
Note
Do not use any regular expression group inside
Application.staticdirectory()
’s pattern
parameter.
Request Valiation¶
yhttp
has a very flexible request validation system. these are some
examples:
required¶
from yhttp import validate
@app.route()
@validate(fields=dict(
bar=dict(required=True),
baz=dict(required='700 Please provide baz'),
))
def post(req):
...
with Given(app, verb='POST', form=dict(bar='bar', baz='baz')):
assert status == 200
when(form=given - 'bar')
assert status == '400 Field bar is required'
when(form=given - 'baz', query=dict(baz='baz'))
assert status == 200
when(form=given - 'baz')
assert status == '700 Please provide baz'
notnone¶
@app.route()
@validate(fields=dict(
bar=dict(notnone=True),
baz=dict(notnone='700 baz cannot be null')
))
def post(req):
...
with Given(app, verb='POST', json=dict(bar='bar', baz='baz')):
assert status == 200
when(json=given - 'bar')
assert status == 200
when(json=given | dict(bar=None))
assert status == '400 Field bar cannot be null'
when(json=given - 'baz')
assert status == 200
when(json=given | dict(baz=None))
assert status == '700 baz cannot be null'
nobody¶
Use nobody
validator when you need to prevent users to post any HTTP body
to the server.
@app.route()
@validate(nobody=True)
def foo(req):
assert req.form == {}
with Given(app, verb='FOO'):
assert status == 200
when(form=dict(bar='baz'))
assert status == '400 Body Not Allowed'
readonly¶
readonly
means the field should not exists on the request form.
@app.route()
@validate(fields=dict(
bar=dict(readonly=True),
))
def post(req):
pass
with Given(app, verb='POST'):
assert status == 200
when(form=dict(bar='bar'))
assert status == '400 Field bar is readonly'
pattern¶
You can use regular expression to validate request fields:
@app.route()
@validate(fields=dict(
bar=dict(pattern=r'^\d+$'),
))
def post(req):
pass
with Given(app, verb='POST', form=dict(bar='123')):
assert status == 200
when(form=given - 'bar')
assert status == 200
when(form=given | dict(bar='a'))
assert status == '400 Invalid format: bar'
type¶
Type validator gets a callable as the type
and tries to cast the field’s
value by form[field] = type(form[field])
.
@app.route()
@validate(fields=dict(
bar=dict(type_=int),
))
def post(req):
pass
with Given(app, verb='POST'):
assert status == 200
when(json=dict(bar='bar'))
assert status == '400 Invalid type: bar'
minimum/maximum¶
@app.route()
@validate(fields=dict(
bar=dict(
minimum=2,
maximum=9
),
))
def post(req):
pass
with Given(app, verb='POST', json=dict(bar=2)):
assert status == 200
when(json=dict(bar='bar'))
assert status == '400 Minimum allowed value for field bar is 2'
when(json=dict(bar=1))
assert status == '400 Minimum allowed value for field bar is 2'
when(json=dict(bar=10))
assert status == '400 Maximum allowed value for field bar is 9'
minlength/maxlength¶
@app.route()
@validate(fields=dict(
bar=dict(minlength=2, maxlength=5),
))
def post(req):
pass
with Given(app, verb='POST', form=dict(bar='123')):
assert status == 200
when(form=given - 'bar')
assert status == 200
when(form=given | dict(bar='1'))
assert status == '400 Minimum allowed length for field bar is 2'
when(form=given | dict(bar='123456'))
assert status == '400 Maximum allowed length for field bar is 5'
length¶
New in version 3.9.0.
@app.route()
@validate(fields=dict(
bar=dict(length=6),
))
def post(req):
pass
with Given(verb='post', json=dict(bar='123456')):
assert status == 200
when(json=given - 'bar')
assert status == 200
when(json=dict(bar='1'))
assert status == '400 Allowed length for field bar is 6'
when(json=dict(bar='12345678'))
assert status == '400 Allowed length for field bar is 6'
Custom Callback¶
You can use your very own callable as the request validator:
from yhttp.validation import Field
def customvalidator(value, container, field):
assert isinstance(field, Field)
if value not in 'ab':
raise statuses.status(400, 'Value must be either a or b')
@app.route()
@validate(fields=dict(
bar=dict(callback=customvalidator)
))
def post(req):
pass
with Given(app, verb='POST', form=dict(bar='a')):
assert status == 200
when(form=given - 'bar')
assert status == 200
when(form=given | dict(bar='c'))
assert status == '400 Value must be either a or b'
@app.route()
@validate(fields=dict(
bar=customvalidator
))
def post(req):
pass
with Given(app, verb='POST', form=dict(bar='a')):
assert status == 200
when(form=given - 'bar')
assert status == 200
when(form=given | dict(bar='c'))
assert status == '400 Value must be either a or b'