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

pymlconf

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:

These are some builtin HTTP status factory functions:

statuses.badrequest()

statuses.unauthorized()

statuses.forbidden()

statuses.notfound()

statuses.methodnotallowed()

statuses.conflict()

statuses.gone()

statuses.preconditionfailed()

statuses.notmodified()

statuses.internalservererror()

statuses.badgateway()

statuses.movedpermanently()

statuses.found()

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'