Skip to content

Getting started

First, make sure that you have Pytcher installed and up to date:

$ pip install pytcher --upgrade

Create a simple web service

Let's create a simple web service that returns Hello World!

from pytcher import App, Request, route

class MyRoute(object):
   @route
   def route(self, r: Request): 
       return 'Hello World!'

route = MyRoute()
App(route).start()

You can run it as follows:

$ python run_app.py

On another window:

$ curl localhost:8080

"Hello World!"

Creating a routing tree

Let's create a more complex CRUD service with the endpoints:

  • /items
    • GET: return the list of items
    • POST: add an item
  • /items/<number>
    • PUT: replace the item at position <number>
    • DELETE: delete the item at position <number>
# flake8: noqa: E999
import http

from pytcher import App, Integer, Request, route


class MyRouter(object):
    def __init__(self):
        self._items = ['pizza', 'cheese', 'ice-cream', 'butter']

    @route
    def route(self, r: Request):
        with r / 'items':
            with r.end:
                with r.get:
                    return self._items

                with r.post:
                    self._items.append(r.json)
                    return self._items[-1], http.HTTPStatus.CREATED

            with r / Integer as item_id:
                with r.get:
                    return self._items[item_id]

                with r.put:
                    self._items[item_id] = r.json
                    return self._items[item_id]

                with r.delete:
                    return self._items.pop(item_id)


app = App(MyRouter())

if __name__ == '__main__':
    app.start()

On another window, try the following commands:

$ curl localhost:8000/items

["pizza","cheese","ice-cream","butter"]
$ curl localhost:8000/items/1

"cheese"
$ curl localhost:8000/items -XPOST -d "ham"

"ham"

$ curl localhost:8000/items

["pizza","cheese","ice-cream","butter", "ham"]
$ curl localhost:8000/items/1 -XPUT -d "cucumber"

"cucumber"

$ curl localhost:8000/items

["pizza","cucumber","ice-cream","butter", "ham"]
$ curl localhost:8000/items/1 -XDELETE

"cucumber"

$ curl localhost:8000/items

["pizza","ice-cream","butter", "ham"]

As you notice, we use the with statement context. In this case, if the request matches the condition (e.g., starts with /items or is GET request), then the code inside the block is executed. This can also be achieved using a for loop instead.

For example:

using with using for
with r / 'items': for _ in r / 'items':
with r.get / 'items' / Integer() as item_id: for item_id in r.get / 'items' / 'Integer':

Info

The author of Python did not approve requests to use with statements to be conditional (i.e., execute the block if a certain condition occurs). We implemented it to make it work on cpython and possibly other implementations of Python. However using the for loop construction is perfectly fine and does not violate the Python standard.

Create a simple web service using dataclasses

In this example, we will take advantage of the data classes that were introduced in Python 3.7. The data type Item will be marshalled to JSON automatically and the JSON payload used to create a new Item will be unmarshalled automatically using the types specified in the data class Item.

# flake8: noqa: E999
from dataclasses import dataclass

from pytcher import Integer, Request, route
from pytcher.app import App


@dataclass
class InventoryItem(object):
    name: str
    unit_price: float
    quantity: int = 0


class MyRouter(object):
    def __init__(self):
        words = [
            'wine',
            'pizza',
            'cheese',
            'peanuts',
            'ice-cream'

        ]
        self._inventory = [
            InventoryItem(word, 10 + i, i + 1)
            for i in range(10)
            for word in words
        ]

    @route
    def route(self, r: Request):
        with r / None:
            return {"test": 1}

        with r / 'items':
            with r / Integer() as [item_index]:
                with r.get:
                    return self._inventory[item_index]

                with r.put:
                    item = r.entity(InventoryItem)
                    self._inventory[item_index] = item
                    return item

                with r.delete:
                    item = self._inventory[item_index]
                    del self._inventory[item_index]
                    return item

            with r.end:
                with r.get:
                    return self._inventory

                with r.post:
                    item = r.entity(InventoryItem)
                    self._inventory.append(item)
                    return item


app = App(MyRouter())

if __name__ == '__main__':
    app.start()

On another window, try the following commands:

$ curl localhost:8000/items

[
  {
    "name": "wine",
    "unit_price": 10,
    "quantity": 1
  },
  {
    "name": "pizza",
    "unit_price": 10,
    "quantity": 1
  },
  {
    "name": "cheese",
    "unit_price": 10,
    "quantity": 1
  },
  [...]
]  
$ curl localhost:8000/items/1

{
  "name": "pizza",
  "unit_price": 10,
  "quantity": 1
}
$ curl localhost:8000/items -H "Content-Type: application/json" -XPOST -d '{
  "name": "pizza",
  "unit_price": 10,
  "quantity": 1
}'

{
  "name": "pizza",
  "unit_price": 10,
  "quantity": 1
}
$ curl localhost:8000/items/1  -H "Content-Type: application/json" -XPUT -d '{
  "name": "corn",
  "unit_price": 1,
  "quantity": 2
}'

{
  "name": "corn",
  "unit_price": 1,
  "quantity": 2
}
$ curl localhost:8000/items/1 -XDELETE

{
  "name": "pizza",
  "unit_price": 10,
  "quantity": 1
}

Create a simple web service using annotation

For those more used to using decorators like in Flask, one can decorate multiple methods using a path and method.

# flake8: noqa: E999
import logging
from dataclasses import dataclass

from pytcher import Request, route
from pytcher.app import App

logger = logging.getLogger(__name__)


@dataclass
class InventoryItem(object):
    name: str
    unit_price: float
    quantity: int = 0


class MyRouter(object):

    def __init__(self):
        words = [
            'wine',
            'pizza',
            'cheese',
            'peanuts',
            'ice-cream'

        ]
        self._inventory = [
            InventoryItem(word, 10 + i, i + 1)
            for i in range(10)
            for word in words
        ]

    @route(path='/items/<int:id>', method='GET')
    def get_item(self, r: Request, id):
        return self._inventory[id]

    @route(path='/items', method='GET')
    def list_items(self, request):
        return self._inventory

    @route(path='/items', method='POST')
    def route(self, r: Request):
        with r.post:
            item = r.entity(InventoryItem)
            self._inventory.append(item)
            return item


app = App(MyRouter(), debug=True)

if __name__ == '__main__':
    app.start()

Combining @route and routing tree

In this example, we combine the use of the @route decorator using the prefix /items with a routing tree. This can be a common pattern, especially when using multiple router classes (e.g., one class with /admin and another one to handle items).

# flake8: noqa: E999
import logging
from dataclasses import dataclass

from pytcher import Request, route, Integer
from pytcher.app import App

logger = logging.getLogger(__name__)


@dataclass
class InventoryItem(object):
    name: str
    unit_price: float
    quantity: int = 0


class MyRouter(object):

    def __init__(self):
        words = [
            'wine',
            'pizza',
            'cheese',
            'peanuts',
            'ice-cream'
        ]

        self._inventory = [
            InventoryItem(word, 10 + i, i + 1)
            for i in range(10)
            for word in words
        ]

    @route(prefix='/items')
    def handle_items(self, r: Request):
        with r / Integer as item_index:
            with r.get:
                return self._inventory[item_index]

            with r.put:
                item = r.entity(InventoryItem)
                self._inventory[item_index] = item
                return item

            with r.delete:
                item = self._inventory[item_index]
                del self._inventory[item_index]
                return item

        with r.end:
            with r.get:
                return self._inventory

            with r.post:
                item = r.entity(InventoryItem)
                self._inventory.append(item)
                return item


app = App(MyRouter(), debug=True)

if __name__ == '__main__':
    app.start()

Notes

To use multiple routers, one can simply pass them to App. For example:

app = App([AdminRouter(), ItemRouter()])
app.run()