Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Writing Async Microservices in Python

Writing Async Microservices in Python

While not exactly new, async programming has arrived quite recently in the Python core. This enabled to create a wide ecosystem of async-first or async-enabled libraries and frameworks that makes async programming more available to the everyday developer. Writing an async microservice is a good way to get your hands dirty with async programming: we are going to see how to build it using FastAPI and its ecosystem to show techniques and pitfalls.

Iacopo Spalletti

June 04, 2022
Tweet

More Decks by Iacopo Spalletti

Other Decks in Programming

Transcript

  1. Writing Async Microservices in Python Iacopo Spalletti - Nephila PyConIT

    2022 3 June 2022
  2. Hello, I am Iacopo Founder and CTO @NephilaIT Djangonaut and

    open source developer 2 / 9 Iacopo Spalletti - @yakkys
  3. intro async programming 3 / 9 Iacopo Spalletti - @yakkys

  4. async programming blocking IO cooperative multitasking 4 / 9 Iacopo

    Spalletti - @yakkys
  5. async programming concurrency Q: When do we want? A: More

    concurrency! A: Now! Q: What do we want? 5 / 9 Iacopo Spalletti - @yakkys
  6. concurrency more performance? 6 / 9 Iacopo Spalletti - @yakkys

  7. concurrency more performance? better resource usage! 7 / 9 Iacopo

    Spalletti - @yakkys
  8. concurrency example 8 / 9 Iacopo Spalletti - @yakkys

  9. concurrency example async def serve_client(): take_order() await prepare_dish() serve_plate() 9

    / 9 Iacopo Spalletti - @yakkys
  10. Writing Async Microservices in Python Iacopo Spalletti - Nephila PyConIT

    2022 3 June 2022
  11. Hello, I am Iacopo Founder and CTO @NephilaIT Djangonaut and

    open source developer 2 / 66 Iacopo Spalletti - @yakkys
  12. intro async programming 3 / 66 Iacopo Spalletti - @yakkys

  13. async programming blocking IO cooperative multitasking 4 / 66 Iacopo

    Spalletti - @yakkys
  14. async programming concurrency Q: When do we want? A: More

    concurrency! A: Now! Q: What do we want? 5 / 66 Iacopo Spalletti - @yakkys
  15. concurrency more performance? 6 / 66 Iacopo Spalletti - @yakkys

  16. concurrency more performance? better resource usage! 7 / 66 Iacopo

    Spalletti - @yakkys
  17. concurrency example 8 / 66 Iacopo Spalletti - @yakkys

  18. concurrency example async def serve_client(): take_order() await prepare_dish() serve_plate() 9

    / 66 Iacopo Spalletti - @yakkys
  19. concurrency microservice architecture 10 / 66 Iacopo Spalletti - @yakkys

  20. async programming event-driven programming no request / response pub/sub message

    queues ... 11 / 66 Iacopo Spalletti - @yakkys
  21. async in python twisted tornado asyncio trio ... 12 /

    66 Iacopo Spalletti - @yakkys
  22. asyncio async def prepare_dish(): ... async def serve_client(): take_order() prepare_dish()

    serve_plate() if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(serve_client()) 13 / 66 Iacopo Spalletti - @yakkys
  23. asyncio async def prepare_dish(): ... async def serve_client(): take_order() prepare_dish()

    serve_plate() if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(serve_client()) 14 / 66 Iacopo Spalletti - @yakkys
  24. asyncio async def prepare_dish(): ... async def serve_client(): take_order() await

    prepare_dish() serve_plate() if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(serve_client()) 15 / 66 Iacopo Spalletti - @yakkys
  25. asyncio async def prepare_dish(): ... async def serve_client(): take_order() await

    prepare_dish() serve_plate() if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(serve_client()) 16 / 66 Iacopo Spalletti - @yakkys
  26. asyncio async def prepare_dish(): ... async def serve_client(): take_order() await

    prepare_dish() serve_plate() if __name__ == "__main__": asyncio.run(serve_client()) 17 / 66 Iacopo Spalletti - @yakkys
  27. FastAPI: A web async framework type hints based opinionated native

    OpenAPI / JSON Schema some batteries included 18 / 66 Iacopo Spalletti - @yakkys
  28. FastAPI: A web async framework built on top of proven

    components Starlette Pydantic 19 / 66 Iacopo Spalletti - @yakkys
  29. why not django? 20 / 66 Iacopo Spalletti - @yakkys

  30. why not django? pros async views in 3.1+ async ORM

    and class-based views coming in 4.1 good ol' boring Django code 21 / 66 Iacopo Spalletti - @yakkys
  31. why not django? cons higher footprint unused code no Django

    REST framework async (yet) less new things to learn 22 / 66 Iacopo Spalletti - @yakkys
  32. fastapi the basics app = FastAPI() @app.get("/items/{item_id}", response_model=Item) async def

    get_item(item_id: int, q: Optional[str]=None): item = await get_item_obj(item_id) return { "name": item.name, "id": item.id, "quantity": item.quantity } 23 / 66 Iacopo Spalletti - @yakkys
  33. fastapi the basics app = FastAPI() @app.get("/items/{item_id}", response_model=Item) async def

    get_item(item_id: int, q: Optional[str]=None): item = await get_item_obj(item_id) return { "name": item.name, "id": item.id, "quantity": item.quantity } 24 / 66 Iacopo Spalletti - @yakkys
  34. concepts ASGI ? 25 / 66 Iacopo Spalletti - @yakkys

  35. concepts ASGI ? Standard python protocol for async applications 26

    / 66 Iacopo Spalletti - @yakkys
  36. concepts ASGI ? Standard python protocol for async applications HTTP

    → ASGI Server → Async application 27 / 66 Iacopo Spalletti - @yakkys
  37. concepts ASGI ? Standard python protocol for async applications HTTP

    → ASGI Server → Async application Everything is an app entrypoints routers middlewares 28 / 66 Iacopo Spalletti - @yakkys
  38. fastapi routing / path operations app = FastAPI() @app.get("/items/{item_id}") async

    def get_item(item_id: int, q: Optional[str]=None): item = await get_item_obj(item_id) return { "name": item.name, "id": item.id, "quantity": item.quantity } 29 / 66 Iacopo Spalletti - @yakkys
  39. fastapi routing / path operations app = FastAPI() @app.get("/items/{item_id}") async

    def get_item(item_id: int, q: Optional[str]=None): item = await get_item_obj(item_id) return { "name": item.name, "id": item.id, "quantity": item.quantity } 30 / 66 Iacopo Spalletti - @yakkys
  40. fastapi business logic app = FastAPI() @app.get("/items/{item_id}") async def get_item(item_id:

    int, q: Optional[str]=None): item = await get_item_obj(item_id) return { "name": item.name, "id": item.id, "quantity": item.quantity } 31 / 66 Iacopo Spalletti - @yakkys
  41. fastapi business logic app = FastAPI() @app.get("/items/{item_id}") async def get_item(item_id:

    int, q: Optional[str]=None): item = await get_item_obj(item_id) return { "name": item.name, "id": item.id, "quantity": item.quantity } 32 / 66 Iacopo Spalletti - @yakkys
  42. fastapi documentation 33 / 66 Iacopo Spalletti - @yakkys

  43. sample application https:/ /github.com/yakky/microservice-talk/ 34 / 66 Iacopo Spalletti -

    @yakkys
  44. sample application ASGI server uvicorn book_search.main:app 35 / 66 Iacopo

    Spalletti - @yakkys
  45. sample application ASGI server book_search/__main__.py def main(): from book_search.app_settings import

    get_settings settings = get_settings() uvicorn.run( "book_search.main:app", host=settings.API_IP, port=settings.API_PORT, ... ) main() python -mbook_search 36 / 66 Iacopo Spalletti - @yakkys
  46. sample application Entrypoint settings = Settings() app = FastAPI(title=settings.PROJECT_NAME) app.add_middleware(

    CORSMiddleware, allow_origin_regex=settings.BACKEND_CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(search_router, prefix=settings.API_V1_STR) 37 / 66 Iacopo Spalletti - @yakkys
  47. sample application Settings settings = Settings() app = FastAPI(title=settings.PROJECT_NAME) app.add_middleware(

    CORSMiddleware, allow_origin_regex=settings.BACKEND_CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(search_router, prefix=settings.API_V1_STR) 38 / 66 Iacopo Spalletti - @yakkys
  48. sample application Settings class Settings(BaseSettings): PROJECT_NAME: str = "Book Search"

    API_V1_STR: str = "/api/v1" ES_HOST: str = "127.0.0.1:9200" BACKEND_CORS_ORIGINS: str = ".*" API_IP = "0.0.0.0" API_PORT = 5000 ES_HOST=es:9200 PROJECT_NAME=MyAPI python -mbook_search 39 / 66 Iacopo Spalletti - @yakkys
  49. sample application Settings class Settings(BaseSettings): PROJECT_NAME: str = "Book Search"

    API_V1_STR: str = "/api/v1" ES_HOST: str = "127.0.0.1:9200" BACKEND_CORS_ORIGINS: str = ".*" class Config: env_file = ".env" 40 / 66 Iacopo Spalletti - @yakkys
  50. sample application Entrypoint settings = Settings() app = FastAPI(title=settings.PROJECT_NAME) app.add_middleware(

    CORSMiddleware, allow_origin_regex=settings.BACKEND_CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(search_router, prefix=settings.API_V1_STR) 41 / 66 Iacopo Spalletti - @yakkys
  51. sample application Middleware settings = Settings() app = FastAPI(title=settings.PROJECT_NAME) app.add_middleware(

    CORSMiddleware, allow_origin_regex=settings.BACKEND_CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(search_router, prefix=settings.API_V1_STR) 42 / 66 Iacopo Spalletti - @yakkys
  52. sample application Middleware @app.middleware("http") async def add_process_time_header(request: Request, call_next): start_time

    = time.time() response = await call_next(request) process_time = time.time() - start_time response.headers["X-Process-Time"] = str(process_time) return response 43 / 66 Iacopo Spalletti - @yakkys
  53. sample application Routing settings = Settings() app = FastAPI(title=settings.PROJECT_NAME) app.add_middleware(

    CORSMiddleware, allow_origin_regex=settings.BACKEND_CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(search_router, prefix=settings.API_V1_STR) 44 / 66 Iacopo Spalletti - @yakkys
  54. Search function Routing search_router = APIRouter() @search_router.get("/search/", response_model=models.BookListResponse, name="search") async

    def search( request: Request, settings: Settings = Depends(get_settings), q: str = Query(None, description="Free text query string."), tags: List[str] = Query([], description="List of tags slug."), year: int = Query(None, description="Filter by year."), size: int = Query(20, description="Results per page."), ): """ Search ES data according to the provided filters. """ results, total = await search_es(settings, q, year, tags, size) return {"results": results, "count": total} 45 / 66 Iacopo Spalletti - @yakkys
  55. Search function Routing from .. import app @app.get("/search/", response_model=models.BookListResponse, name="search")

    async def search( ... 46 / 66 Iacopo Spalletti - @yakkys
  56. Search function Dependency injection search_router = APIRouter() @search_router.get("/search/", response_model=models.BookListResponse, name="search")

    async def search( request: Request, settings: Settings = Depends(get_settings), q: str = Query(None, description="Free text query string."), tags: List[str] = Query([], description="List of tags slug."), year: int = Query(None, description="Filter by year."), size: int = Query(20, description="Results per page."), ): """ Search ES data according to the provided filters. """ results, total = await search_es(settings, q, year, tags, size) return {"results": results, "count": total} 47 / 66 Iacopo Spalletti - @yakkys
  57. Search function Parameters search_router = APIRouter() @search_router.get("/search/", response_model=models.BookListResponse, name="search") async

    def search( request: Request, settings: Settings = Depends(get_settings), q: str = Query(None, description="Free text query string."), tags: List[str] = Query([], description="List of tags slug."), year: int = Query(None, description="Filter by year."), size: int = Query(20, description="Results per page."), ): """ Search ES data according to the provided filters. """ results, total = await search_es(settings, q, year, tags, size) return {"results": results, "count": total} 48 / 66 Iacopo Spalletti - @yakkys
  58. Search function Dependency injection search_router = APIRouter() @search_router.get("/search/", response_model=models.BookListResponse, name="search")

    async def search( request: Request, settings: Settings = Depends(get_settings), q: str = Query(None, description="Free text query string."), tags: List[str] = Query([], description="List of tags slug."), year: int = Query(None, description="Filter by year."), size: int = Query(20, description="Results per page."), ): """ Search ES data according to the provided filters. """ results, total = await search_es(settings, q, year, tags, size) return {"results": results, "count": total} 49 / 66 Iacopo Spalletti - @yakkys
  59. Search function Business logic search_router = APIRouter() @search_router.get("/search/", response_model=models.BookListResponse, name="search")

    async def search( request: Request, settings: Settings = Depends(get_settings), q: str = Query(None, description="Free text query string."), tags: List[str] = Query([], description="List of tags slug."), year: int = Query(None, description="Filter by year."), size: int = Query(20, description="Results per page."), ): """ Search ES data according to the provided filters. """ results, total = await search_es(settings, q, year, tags, size) return {"results": results, "count": total} 50 / 66 Iacopo Spalletti - @yakkys
  60. Search function Running the search async def search_es( settings: Settings,

    q: str, year: int, tags: List[str], size: int ) -> Tuple[List[Book], int]: """Build the ES query and run it, returning results and total count.""" client = init_es(settings, use_async=True) query_dict = {"bool": {"must": []}} ... response = await client.search(index="book", query=query_dict, size=size) results = [row["_source"] for row in response["hits"]["hits"]] total = response["hits"]["total"]["value"] await client.close() return results, total 51 / 66 Iacopo Spalletti - @yakkys
  61. Search function Response search_router = APIRouter() @search_router.get("/search/", response_model=models.BookListResponse, name="search") async

    def search( request: Request, settings: Settings = Depends(get_settings), q: str = Query(None, description="Free text query string."), tags: List[str] = Query([], description="List of tags slug."), year: int = Query(None, description="Filter by year."), size: int = Query(20, description="Results per page."), ): """ Search ES data according to the provided filters. """ results, total = await search_es(settings, q, year, tags, size) return {"results": results, "count": total} 52 / 66 Iacopo Spalletti - @yakkys
  62. Search function Response ... class Book(BaseModel): book_id: int title: str

    isbn13: str authors_list: List[Author] = Field(..., alias='authors') tags: List[Tag] original_publication_year: int class BookList(BaseModel): results: List[Book] count: int 53 / 66 Iacopo Spalletti - @yakkys
  63. Search function Response ... class Book(BaseModel): book_id: int title: str

    isbn13: str authors_list: List[Author] = Field(..., alias='authors') tags: List[Tag] original_publication_year: int class BookList(BaseModel): results: List[Book] count: int 54 / 66 Iacopo Spalletti - @yakkys
  64. Pydantic Caveat 55 / 66 Iacopo Spalletti - @yakkys

  65. Pydantic Caveat Data validation not serialization Manipulating data is a

    bit tricky Return data ready for serialization 56 / 66 Iacopo Spalletti - @yakkys
  66. sample application Testing @pytest.mark.asyncio async def test_search_basic(load_books): async with httpx.AsyncClient(app=app,

    base_url="http://test") as client: url = app.url_path_for("search") params = urlencode({"q": "Susan Collins"}) response = await client.get(f"{url}?{params}") assert response.status_code == 200 57 / 66 Iacopo Spalletti - @yakkys