Skip to content

Route

Routes can be defined using different ways:

  • using the decorator @route
  • within a function/method decorated using @route and using the with statement
  • within a function/method decorated using @route and using the for statement

@route decorator

One can use the @route decorator on a function or a class method which has as parameter a request object.

For instance:

from pytcher import App, Request, route

@route
def route(request: Request):
    return 'Hello world!'

app = App([route])
app.run()

In this example, all the requests will return the string Hello world!. By default the HTTP status code returned is http.HTTPStatus.OK (200). One can return a different code using a tuple instead (data, HTTP status code) as follows:

return'Hello world', http.HTTPStatus.CREATED

Also, like Flask, one can also pass the expected HTTP method (default to GET) and the path (that can contain binding variables) that defaults to /.

In this case, the parameters of the function/method will be:

  • request object
  • binding variables

For instance:

from pytcher import App, Request, route

@route(path='/items/<int:item_id>', method='GET')
def route(request: Request, item_id: int):
    return {
        'item': item_id
    }

Routing tree

Inside a function or a method decorated with @route The routing tree can be defined using with statements or for loops.

The following example using with statements:

from pytcher import route, Request, Integer

@route
def route(r: Request):
    with r / 'v1':
        with r.get / 'items' / Integer as item_id:
            return {
                'Item': item_id
            }    

Can be rewritten with for loops as follows:

from pytcher import route, Request, Integer

@route
def route(r: Request):
    for _ in  r / 'v1':
        for item_id in r.get / 'items' / Integer:
            return {
                'Item': item_id
            }    

Let's consider an example with 2 bindings. In this case, the binding will return a list instead of a single value. For example, the URL path /books/2/pages/3 will match the route r / 'books' / Integer / 'pages' / Integer and binds the 2 int to a list of 2 elements:

with r / 'books' / Integer / 'pages' / Integer as [book_id, page]:
    return {
        'book_id': book_id,
        'page': page
    }

or using a for loop construction:

for book_id, page in  r / 'books' / Integer / 'pages' / Integer:
    return {
        'book_id': book_id,
        'page': page
    }

Method matcher

The HTTP methods can be matched as follows:

Method route
GET with r.get:
PUT with r.put:
POST with r.post:
DELETE with r.delete:
PATCH with r.patch:
HEAD with r.head:

Path matcher

Paths can be defined using path elements separated by /. For example with r / 'v2' / 'items:' will match the path /v2/items.

One can also use the following matchers:

Matcher Description Example
Integer(min=None, max=None) Match an Integer between min and max with r / 'books' / Integer() / 'pages' / Integer() as [book_id, page]:
Float(min=None, max=None) Match a float between min and max with r / 'values' / Float() as [price]:
Date(format='YYYY-MM-DD') Match a date with r / 'data' / Date() as [date]:
Choice(value1, value2, ..., ignore_case=True]) Match different strings with r / Choice(['books', 'novels]) as [book_type]:
str Match a string with r / 'data':
Regex(regex, flags, data_types) Match a regex with r / 'data' / Regex('(.*)-(.*)') as [[a, b]]:
None Match the end of the path with r / 'items' / None:
request.end match the end of the path with r.end:

Info

If you use default parameters, you can use the matcher class or the instance. For example Integer or Integer()

Integer matcher

Integer(min: int = None, max: int = None): Matches if the path element is an integer. Optionally you can provide the boundaries (inclusive).

Float matcher

Float(min: float = None, max: float = None): Matches if the path element is a float. Optionally you can provide the boundaries (inclusive).

Date matcher

Date(format='%Y-%m-%d'): Matches if the path element is a date. By default the format is %Y-%m-%d (e.g., 2019-03-02). For the format see the Python datetime page.

Choice matcher

Choice(choice1: str, choice2: str, ..., ignore_case: bool = True): Matches if the path element is one of strings provided. By default it is case insensitive but one can set ignore_case to False to be case sensitive.

str matcher

str: Simply use a string for exact match, for example r / 'items' will match if the path element matches items

None: Indicate the end of the request URL. For example r / 'items' / None indicates that the URL ended at items

end: Indicate the end of the request URL. For example r.end indicates that the URL ended

Regex matcher

If no matching group is provided to the Regex matcher, it will return the whole string that matches. If a single capturing group is provided, it will return the string that matches the group If multiple capturing groups are provided, it will return an array of strings that matches the groups.

It can also take care of type conversion by providing data_types which is an array of types the groups are supposed to be. For example:

with r / Regex('^fruit-(?P<name>.*)-(?P<size>\d+)$', data_types=[str, int]) as [name, size]:
    return {
        'fruit': name,
        'size': size
    }

will bind the first group as a str and size as an int. The URL /fruit-orange-15 will result in the following result:

{
  "fruit": "orange",
  "size": 15
}

None matcher

Example using path matchers

Here is an example of routing tree:

from pytcher import route, Request

@route
def route(r: Request):
    with r / 'books':  # If path starts with '/books'
        with r.end:  # If path matches exactly `/books`
            return {"message": "something"}

        with r / 'info' / None:  # If path matches exactly `/books/info`:
            return {"info": "nothing"}

        with r / String() / 'page' / Integer() / None as [book_id, page]:  # For example /books/test/page/10
            return {
                "book": book_id,
                "page": page
            }

Parameter matcher

Conditions can be put on parameter values (for example http://localhost/items?token=45ab).

This can be done as follows:

from pytcher import route, Request

@route
def route(r: Request):
    with r.p['token'] == '45ab':
        return {
            'message': 'Hello!'
        }

    return {
        'message': "Bye!"
    }

It can also use operators such as > or < for numeric values.

Below is the full list of supported operators:

Operator Description Example
== equal r.p['token'] == 'secret-token'
!= not equal r.p['type'] != 'fruit'
> greater than r.p['price'] > 100
< less than r.p['price'] < 100
>= greater or equal r.p['price'] >= 100
<= less or equal r.p['price'] <= 100
in contains 'apple' in r.p['fruits']

Header matcher

Info

HTTP Headers are not case-sensitive (so X-Organization will be treated the same as x-organization)

Similarly to parameter matchers, conditions can be put on header values. For example one can check if X-Organization is set to my-company. This can be done as follows:

from pytcher import route, Request

@route
def route(r: Request):
    with r.h['X-Organization'] == 'my-company':
        return {
            'message': 'Hello my-company employee!'
        }

    return {
        'message': "Go away!"
    }

It can also use operators such as > or < for numeric values.

Below is the full list of supported operators:

Operator Description Example
== equal r.h['Token'] == 'secret-token'
!= not equal r.h['X-Organization'] != 'my-company'
> greater than r.h['money'] > 100
< less than r.h['money'] < 100
>= greater or equal r.h['money'] >= 100
<= less or equal r.h['money'] <= 100
in contains 'apple' in r.h['fruits']

Combining matchers

One can use boolean expressions with & (and) and | (or). For example:

from pytcher import route, Request

@route
def route(r: Request):
    with (r / 'items') & r.h['X-Organization'] == 'my-company':
        return {
            "items": [
                {"name": "pear"},
                {"name": "apple"}
            ]
        }