Skip to content

Commit

Permalink
Add a lesson on dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathansick committed May 6, 2024
1 parent 4e72ba0 commit ee2204e
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 1 deletion.
1 change: 1 addition & 0 deletions changelog.d/20240506_125840_jsick_DM_44230.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
### New features

- Demonstrate `SlackException` in the `POST /fastapi-bootcamp/error-demo` endpoint.
- Demonstrate custom FastAPI dependencies in the `GET /fastapi-bootcamp/dependency-demo` endpoint.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ select = ["ALL"]
"src/fastapibootcamp/handlers/external.py" = [
"ERA001", # Allow some commented code for documentation
]
"src/fastapibootcamp/dependencies/demostatefuldependency.py" = [
"S311", # Allow use of random in this module
]
"tests/**" = [
"C901", # tests are allowed to be complex, sometimes that's convenient
"D101", # tests don't need docstrings
Expand Down
68 changes: 68 additions & 0 deletions src/fastapibootcamp/dependencies/demostatefuldependency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""A class-based FastAPI dependency for demonstration purposes."""

from __future__ import annotations

import random

from ..exceptions import DemoInternalError

__all__ = ["example_stateful_dependency", "ExampleStatefulDependency"]


class ExampleStatefulDependency:
"""A stateful FastAPI dependency for demonstration purposes."""

def __init__(self) -> None:
# For this demo we're just using a semi-random string as the state. In
# real applications, this could be a database connection, a client
# to a remote service, etc. This "state" is reused over the life of
# this application instance. It's not shared between instances, though,
self._state: str | None = None

async def init(self) -> None:
"""Initialize the dependency."""
# This initialization is called in main.py in the lifespan context
self._state = f"{random.choice(ADJECTIVES)} {random.choice(ANIMALS)}"

async def __call__(self) -> str:
"""Provide the dependency.
This gets called by the fastapi Depends() function when your
path operation function is called.
"""
if self._state is None:
raise DemoInternalError(
"ExamplePersistentDependency not initialized"
)

return self._state

async def aclose(self) -> None:
"""Close the dependency."""
self.state = None


# This is the instance of the dependency that's referenced in path operation
# functions with the fastapi.Depends() function. Note that it needs to be
# initialized before it can be used. This is done in the lifespan context
# manager in main.py. Another option is to initialize it on the first use.
example_stateful_dependency = ExampleStatefulDependency()


ADJECTIVES = [
"speedy",
"ponderous",
"furious",
"careful",
"mammoth",
"crafty",
]

ANIMALS = [
"cat",
"dog",
"sloth",
"snail",
"rabbit",
"turtle",
]
86 changes: 85 additions & 1 deletion src/fastapibootcamp/handlers/external.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from enum import Enum
from typing import Annotated

from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends, Query, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from safir.dependencies.logger import logger_dependency
Expand All @@ -13,6 +13,7 @@
from structlog.stdlib import BoundLogger

from ..config import config
from ..dependencies.demostatefuldependency import example_stateful_dependency
from ..exceptions import DemoInternalError

# The APIRouter is what the individual endpoints are attached to. In main.py,
Expand Down Expand Up @@ -378,3 +379,86 @@ async def post_error_demo(data: ErrorRequestModel) -> JSONResponse:
)

raise RuntimeError("A generic error occurred.")


# =============================================================================
# Lesson 6: Custom dependencies
#
# FastAPI dependencies are a way to add reusable code to your path operation
# that's aware of the current request. Safir provides several dependencies,
# see https://safir.lsst.io/api.html#module-safir.dependencies.arq etc.
#
# - arq_dependency provides a client to the Arq distributed job queue
# - db_session_dependency provides a SQLAlchemy session
# - auth_delegated_token_dependency provides a delegated token
# - auth_dependency provides info about the current user
# - auth_logger_dependency provides a logger with user info bound
# - http_client_dependency provides an HTTPX async client
# - logger_dependency provides a structlog logger (see Lesson 4)
#
# Besides these, you can create your own dependencies. In the astroplan
# application we'll explore the request context dependency pattern.
#
# There are two types of dependencies you'll develop: functional dependencies
# and class-based dependencies. These class-based dependencies can hold
# persistent state that's reused across multiple requests.
#
# Try it out:
# http get :8000/fastapi-bootcamp/dependency-demo X-Custom-Header:foo

# See src/fastapibootcamp/dependencies/demopersistentdependency.py for the
# dependency that holds a persistent value. Below is a functional dependency:


async def custom_header_dependency(request: Request) -> str | None:
"""Provide the value of the X-Custom-Header from the request.
This shows how you can access the request object, query parameters, and
even other depdenencies in a dependency function's arguments.
"""
return request.headers["X-Custom-Header"]


class DependencyDemoResponseModel(BaseModel):
"""Response model for the dependency demo endpoint."""

header: str | None = Field(
..., title="The X-Custom-Header.", examples=["foo"]
)

persistent_value: str = Field(
...,
title="A persistent value provided by the dependency.",
examples=["crafty sloth"],
)


@external_router.get(
"/dependency-demo",
summary="Demonstrate custom dependencies.",
response_model=DependencyDemoResponseModel,
)
async def get_dependency_demo(
# This is the functional dependency defined above
custom_header: Annotated[str | None, Depends(custom_header_dependency)],
# This is the class-based dependency defined in
# src/fastapibootcamp/dependencies/demopersistentdependency.py
persistent_value: Annotated[str, Depends(example_stateful_dependency)],
# This is a dependency from Safir
logger: Annotated[BoundLogger, Depends(logger_dependency)],
) -> DependencyDemoResponseModel:
logger.info(
"Dependency demo",
custom_header=custom_header,
persistent_value=persistent_value,
)

return DependencyDemoResponseModel(
header=custom_header, persistent_value=persistent_value
)


# =============================================================================
# This covers the basics of writing endpoints in FastAPI. Next, we'll explore
# how to structure a larger application with a service/storage/domain
# architecture. We'll see you there at src/fastapibootcamp/handlers/astroplan.
7 changes: 7 additions & 0 deletions src/fastapibootcamp/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
# Notice how the the config instance is imported early so it's both
# instantiated on app start-up and available to set up the app.
from .config import config
from .dependencies.demostatefuldependency import example_stateful_dependency
from .handlers.astroplan import astroplan_router
from .handlers.external import external_router
from .handlers.internal import internal_router
Expand All @@ -40,6 +41,10 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Set up and tear down the application."""
# Any code here will be run when the application starts up.
logger = get_logger(__name__)

# Set up the example persistent dependency.
await example_stateful_dependency.init()

iers_cache_manager = IersCacheManager(logger)
iers_cache_manager.config_iers_cache()
if config.clear_iers_on_startup:
Expand All @@ -51,6 +56,8 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:

# Any code here will be run when the application shuts down.
await http_client_dependency.aclose()
await example_stateful_dependency.aclose()

logger.info("fastapi-bootcamp application shut down complete.")


Expand Down

0 comments on commit ee2204e

Please sign in to comment.