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 itemsPOST
: 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()