Slide 1

Slide 1 text

Writing Async Microservices in Python Iacopo Spalletti - Nephila PyConIT 2022 3 June 2022

Slide 2

Slide 2 text

Hello, I am Iacopo Founder and CTO @NephilaIT Djangonaut and open source developer 2 / 9 Iacopo Spalletti - @yakkys

Slide 3

Slide 3 text

intro async programming 3 / 9 Iacopo Spalletti - @yakkys

Slide 4

Slide 4 text

async programming blocking IO cooperative multitasking 4 / 9 Iacopo Spalletti - @yakkys

Slide 5

Slide 5 text

async programming concurrency Q: When do we want? A: More concurrency! A: Now! Q: What do we want? 5 / 9 Iacopo Spalletti - @yakkys

Slide 6

Slide 6 text

concurrency more performance? 6 / 9 Iacopo Spalletti - @yakkys

Slide 7

Slide 7 text

concurrency more performance? better resource usage! 7 / 9 Iacopo Spalletti - @yakkys

Slide 8

Slide 8 text

concurrency example 8 / 9 Iacopo Spalletti - @yakkys

Slide 9

Slide 9 text

concurrency example async def serve_client(): take_order() await prepare_dish() serve_plate() 9 / 9 Iacopo Spalletti - @yakkys

Slide 10

Slide 10 text

Writing Async Microservices in Python Iacopo Spalletti - Nephila PyConIT 2022 3 June 2022

Slide 11

Slide 11 text

Hello, I am Iacopo Founder and CTO @NephilaIT Djangonaut and open source developer 2 / 66 Iacopo Spalletti - @yakkys

Slide 12

Slide 12 text

intro async programming 3 / 66 Iacopo Spalletti - @yakkys

Slide 13

Slide 13 text

async programming blocking IO cooperative multitasking 4 / 66 Iacopo Spalletti - @yakkys

Slide 14

Slide 14 text

async programming concurrency Q: When do we want? A: More concurrency! A: Now! Q: What do we want? 5 / 66 Iacopo Spalletti - @yakkys

Slide 15

Slide 15 text

concurrency more performance? 6 / 66 Iacopo Spalletti - @yakkys

Slide 16

Slide 16 text

concurrency more performance? better resource usage! 7 / 66 Iacopo Spalletti - @yakkys

Slide 17

Slide 17 text

concurrency example 8 / 66 Iacopo Spalletti - @yakkys

Slide 18

Slide 18 text

concurrency example async def serve_client(): take_order() await prepare_dish() serve_plate() 9 / 66 Iacopo Spalletti - @yakkys

Slide 19

Slide 19 text

concurrency microservice architecture 10 / 66 Iacopo Spalletti - @yakkys

Slide 20

Slide 20 text

async programming event-driven programming no request / response pub/sub message queues ... 11 / 66 Iacopo Spalletti - @yakkys

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

FastAPI: A web async framework type hints based opinionated native OpenAPI / JSON Schema some batteries included 18 / 66 Iacopo Spalletti - @yakkys

Slide 28

Slide 28 text

FastAPI: A web async framework built on top of proven components Starlette Pydantic 19 / 66 Iacopo Spalletti - @yakkys

Slide 29

Slide 29 text

why not django? 20 / 66 Iacopo Spalletti - @yakkys

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

why not django? cons higher footprint unused code no Django REST framework async (yet) less new things to learn 22 / 66 Iacopo Spalletti - @yakkys

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

concepts ASGI ? 25 / 66 Iacopo Spalletti - @yakkys

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

fastapi documentation 33 / 66 Iacopo Spalletti - @yakkys

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Search function Routing from .. import app @app.get("/search/", response_model=models.BookListResponse, name="search") async def search( ... 46 / 66 Iacopo Spalletti - @yakkys

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Pydantic Caveat 55 / 66 Iacopo Spalletti - @yakkys

Slide 65

Slide 65 text

Pydantic Caveat Data validation not serialization Manipulating data is a bit tricky Return data ready for serialization 56 / 66 Iacopo Spalletti - @yakkys

Slide 66

Slide 66 text

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