Chris Withers
Jump Trading
Who am I?
Representational State Transfer
GET /entry/?contains=foo GET /entry/123 POST /entry/ PUT /entry/123 DELETE /entry/123
Co-operative multitasking
async def get_item(name): client = httpx.AsyncClient() response = await client.get('https://.../'+name) return response.json()
Why should you hate it?
Say what you want, not how to get it.
def render(session: Session, name: str): item = Session.query(Item).filter_by(name=name) return make_json(item)
Why should you love it?
class UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = User fields = ['url', 'username', 'email', 'groups'] class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all().order_by('-date_joined') serializer_class = UserSerializer
from flask import Flask, session, redirect, \ url_for, escape, request app = Flask(__name__) @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': session['username'] = request.form['username'] return redirect(url_for('index')) return ''' <form method="post"> <p><input type=text name=username> <p><input type=submit value=Login> </form> '''
def hello_world(request): return Response('Hello World!') if __name__ == '__main__': with Configurator() as config: config.add_route('hello', '/') config.add_view(hello_world, route_name='hello') app = config.make_wsgi_app() server = make_server('0.0.0.0', 6543, app) server.serve_forever()
@hug.get(examples='name=Timothy&age=26') @hug.local() def happy_birthday( name: hug.types.text, age: hug.types.number, hug_timer=3 ): """Says happy birthday to a user""" return { 'message': f'Happy {age} Birthday {name}!', 'took': float(hug_timer) }
future proof: (websocket, graphql, etc)
app = FastAPI() @app.get("/") def root(...): return {"greeting": "Hello World"} app.include_router(events.router, prefix="/events"...)
router = APIRouter() @router.post("/", ..., status_code=201) def create_object(...): ...
GET /events/1234?detail=full Authorization: Token ABCD1234
@router.get("/event/{id}") def get_object(id: int): ...
from fastapi import Path from pydantic import Required @router.get("events/{id}") def get_object(id: int = Path(Required)): ...
GET /search/?text=something Authorization: Token ABCD1234
@router.get("/search") def search(text:str = None, offset:int = 0, limit:int = 100): ...
from fastapi import Query @router.get("/search") def search( text: str = Query(default=None, max_length=20), offset: int = Query(0), limit: int = Query(100), ):
GET /search/?text=something Authorization: Token ABCD1234
from fastapi import Header @router.get("/search") def search(authorization=Header(default=None)): ...
GET /search/?text=something Authorization: Token ABCD1234 🍪_suggest_author: Chris
from fastapi import Cookie @router.get("/search/") def search(_suggest_author=Cookie(default=None)): ...
<form action="/files/" enctype="multipart/form-data" method="post"> <input name="token" type="text"> <input name="file" type="file"> <input type="submit"> </form>
from fastapi import File, Form, UploadFile from pydantic import Required @router.post("/files/") async def create_file( token: str = Form(Required), file: UploadFile = File(Required), ): ...
POST /events/ Authorization: Token ABCD1234
{ 'date': '2019-06-02', 'type': 'DONE', 'text': 'some stuff got done' }
from datetime import date as DateType from enum import Enum from pydantic import BaseModel class Types(Enum): done = 'DONE' cancelled = 'CANCELLED' class Event(BaseModel): date: DateType type: Types text: str
class Event(BaseModel): date: DateType type: Types text: str
POST /events/ Authorization: Token ABCD1234
{ 'date': '2019-06-02', 'type': 'DONE', 'text': 'some stuff got done' }
@router.post("/events/") def create_object(event: Event = Required): ...
@app.get("/") def root(...): return {"greeting": "Hello World"}
@app.post("/events/", response_model=Event, status_code=201) def create_object(event: Event = Required): ... return { 'date': '2019-06-02', 'type': 'DONE', 'text': 'some stuff got done' }
from starlette.responses import Response @app.get("/headers/") def get_headers(response: Response): response.headers["X-Cat-Dog"] = "alone in the world" return {"message": "Hello World"}
from starlette.responses import JSONResponse @app.get("/headers/") def get_headers(): content = {"message": "Hello World"} headers = {"X-Cat-Dog": "alone in the world"} return JSONResponse(content=content, headers=headers)
from starlette.responses import Response @app.post("/cookie-and-object/") def create_cookie(response: Response): response.set_cookie(key="fakesession", value="fake-cookie-session-value") return {"message": "Come to the dark side"}
from starlette.responses import JSONResponse @app.post("/cookie/") def create_cookie(): response = JSONResponse(content=...) response.set_cookie(key="fakesession", value="fake-cookie-session-value") return response
app = FastAPI() @app.get("/sync") def root(): return {"greeting": "Hello World"} @app.get("/async") async def root(): return {"message": "Hello World"}
from fastapi import Depends, Security @router.get("/{id}", response_model=EventFull) def get_object( id: int, session: Session = Depends(db_session), current_user: User = Security(get_current_user) ): ...
DID travel to pycon UK DID speak to Evil Util Co: -- Have promised it will be fixed tomorrow -- CANCELLED go to gym
from enum import Enum from sqlalchemy import Column, Integer, Text, Date from sqlalchemy.dialects.postgresql import ENUM from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class Types(Enum): done = 'DONE' cancelled = 'CANCELLED' class Event(Base): __tablename__ = 'entry' id = Column(Integer(), primary_key=True) date = Column(Date, nullable=False) type = Column(ENUM(Types, name='types_enum')) text = Column(Text, nullable=False)
from configurator import Config from pydantic import BaseModel, DSN # schema class DatabaseConfig(BaseModel): url: DSN class AppConfig(BaseModel): testing: bool db: DatabaseConfig # defaults config = Config({ 'testing': False, })
def load_config(path=None): if config.testing: return # file if path is None: path = Path(__file__).resolve().parent / 'app.yml' config.merge( Config.from_path(path) ) # env config.merge(os.environ, { 'DB_URL': 'db.url', }) # validate AppConfig(**config.data) return config
Pathlib! :-)
from fastapi import FastAPI from sqlalchemy import create_engine app = FastAPI() @app.on_event("startup") def configure(): load_config() Session.configure(bind=create_engine(config.db.url))
@app.get("/") def root(session: Session = Depends(db_session)): return {"count": session.query(Event).count()}
from sqlalchemy.orm import sessionmaker Session = sessionmaker() def db_session(request: Request): return request.state.db
from starlette.requests import Request from starlette.concurrency import run_in_threadpool def finish_session(session): session.rollback() session.close() @app.middleware('http') async def make_db_session(request: Request, call_next): request.state.db = session = Session() response = await call_next(request) await run_in_threadpool(finish_session, session) return response
class EventNonPrimaryKey(BaseModel): date: DateType type: Types text: str class EventFull(EventNonPrimaryKey): id: int class EventList(BaseModel): items: List[EventFull] count: int prev: str = None next: str = None
@router.post("/", response_model=EventFull, status_code=201) def create_object( event: EventNonPrimaryKey = Required, session: Session = Depends(db_session), ): """ Create new Event. """ with session.transaction: obj = Event(**event.dict()) session.add(obj) session.flush() return simplify(obj)
explain simplify and why the code isn't there
@router.get("/{id}", response_model=EventFull) def get_object( id: int, session: Session = Depends(db_session), ): """ Get Event by ID. """ with session.transaction: try: obj = session.query(Event).filter_by(id=id).one() except NoResultFound: raise HTTPException(status_code=404) else: return simplify(obj)
@router.get("/", response_model=EventList, name='events_list') def list_object( text: str = None, offset: int = Query(0), limit: int = 100, session: Session = Depends(db_session), request: Request = Required, ): with session.transaction: items = session.query(Event).order_by(Event.date.desc(), 'id') if text: items = items.filter(Event.text.ilike('%'+text.strip()+'%')) count = items.count() items = [EventFull(**simplify(i)) for i in items.offset(offset).limit(limit)] if len(items) != count: ... prev = url_for(request, ...) next = url_for(request, ...) else: prev = next = None return EventList(count=count, items=items, prev=prev, next=next)
@router.put("/{id}", response_model=EventFull) def update_object( id: int, event: EventNonPrimaryKey = Required, session: Session = Depends(db_session), ): with session.transaction: try: obj = session.query(Event).filter_by(id=id).one() except NoResultFound: raise HTTPException(status_code=404) else: for key, value in event.dict().items(): setattr(obj, key, value) return simplify(obj)
@router.delete("/{id}", response_model=EventFull) def delete_object( id: int, session: Session = Depends(db_session), ): """ Delete an Event. """ with session.transaction: try: obj = session.query(Event).filter_by(id=id).one() except NoResultFound: raise HTTPException(status_code=404) else: session.delete(obj) return simplify(obj)
from fastapi import Depends, Security from fastapi.security import HTTPBasic, HTTPBasicCredentials security = HTTPBasic() def get_current_user( credentials: HTTPBasicCredentials = Depends(security) ): return credentials.username @router.get("/{id}", response_model=EventFull) def get_object( id: int, session: Session = Depends(db_session), current_user: User = Security(get_current_user) ): ...
Supports OAuth2, all the other goodness Obviously need to actually check passwords!
@pytest.fixture(scope='session') def client(): with config.push({ 'testing': True, 'db': {'url': os.environ['TEST_DB_URL']} }): with TestClient(app) as client: yield client
@pytest.fixture(scope='session') def db(client): engine = Session.kw['bind'] conn = engine.connect() transaction = conn.begin() try: Base.metadata.create_all(bind=conn, checkfirst=False) yield conn finally: transaction.rollback() Session.configure(bind=engine)
@pytest.fixture() def session(db): transaction = db.begin_nested() try: Session.configure(bind=db) yield Session() finally: transaction.rollback()
def test_create_full_data(session, client): response = client.post('/events/', json={ 'date': '2019-06-02', 'type': 'DONE', 'text': 'some stuff got done' }) actual = session.query(Event).one() compare(actual.date, expected=date(2019, 6, 2)) compare(response.json(), expected={ 'id': actual.id, 'date': '2019-06-02', 'type': 'DONE', 'text': 'some stuff got done' }) compare(response.status_code, expected=201)
remember to explain testfixtures!
Getting the talk materials:
Fast API Documentation:
Getting hold of me:
Space | Forward |
---|---|
Right, Down, Page Down | Next slide |
Left, Up, Page Up | Previous slide |
G | Go to slide number |
P | Open presenter console |
H | Toggle this help |