diff --git a/.gitignore b/.gitignore
index ad78031..08201a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,5 @@ wheels/
# Virtual environments
.venv
*.svg
+
+/fastapi-example/images/
diff --git a/fastapi-example/main.py b/fastapi-example/main.py
new file mode 100644
index 0000000..20dab85
--- /dev/null
+++ b/fastapi-example/main.py
@@ -0,0 +1,64 @@
+from contextlib import asynccontextmanager
+from pathlib import Path
+from uuid import uuid4
+
+import logfire
+from fastapi import FastAPI
+from fastapi.staticfiles import StaticFiles
+from httpx import AsyncClient
+from openai import AsyncOpenAI
+from pydantic import BaseModel, Field
+from starlette.responses import FileResponse
+
+logfire.configure(service_name='fastapi-example')
+http_client: AsyncClient
+openai_client = AsyncOpenAI()
+logfire.instrument_openai(openai_client)
+
+
+@asynccontextmanager
+async def lifespan(_app: FastAPI):
+ global http_client, openai_client
+ async with AsyncClient() as _http_client:
+ http_client = _http_client
+ logfire.instrument_httpx(http_client, capture_headers=True)
+ yield
+
+
+app = FastAPI(lifespan=lifespan)
+logfire.instrument_fastapi(app, capture_headers=True)
+this_dir = Path(__file__).parent
+image_dir = Path(__file__).parent / 'images'
+image_dir.mkdir(exist_ok=True)
+app.mount('/static', StaticFiles(directory=image_dir), name='static')
+
+
+@app.get('/')
+@app.get('/display/{image:path}')
+async def main() -> FileResponse:
+ return FileResponse(this_dir / 'page.html')
+
+
+class GenerateResponse(BaseModel):
+ next_url: str = Field(serialization_alias='nextUrl')
+
+
+@app.post('/generate')
+async def generate_image(prompt: str) -> GenerateResponse:
+ response = await openai_client.images.generate(prompt=prompt, model='dall-e-3')
+
+ assert response.data, 'No image in response'
+
+ image_url = response.data[0].url
+ assert image_url, 'No image URL in response'
+ r = await http_client.get(image_url)
+ r.raise_for_status()
+ path = f'{uuid4().hex}.jpg'
+ (image_dir / path).write_bytes(r.content)
+ return GenerateResponse(next_url=f'/display/{path}')
+
+
+if __name__ == '__main__':
+ import uvicorn
+
+ uvicorn.run(app)
diff --git a/fastapi-example/page.html b/fastapi-example/page.html
new file mode 100644
index 0000000..484b411
--- /dev/null
+++ b/fastapi-example/page.html
@@ -0,0 +1,228 @@
+
+
+
+
+
+ Image Generator
+
+
+
+
+
+
Image Generator
+
+
+
+
+
+
+
+
+
Generating your image...
+
+
+
+
+
+
+
+
+
+
diff --git a/pai-weather/main.py b/pai-weather/main.py
index 072021c..cd9bbc0 100644
--- a/pai-weather/main.py
+++ b/pai-weather/main.py
@@ -42,6 +42,7 @@ async def get_lat_lng(ctx: RunContext[Deps], location_description: str) -> LatLn
ctx: The context.
location_description: A description of a location.
"""
+ # NOTE: the response here will be random, and is not related to the location description.
r = await ctx.deps.client.get(
'https://demo-endpoints.pydantic.workers.dev/latlng',
params={'location': location_description, 'sleep': randint(200, 1200)},
@@ -59,6 +60,7 @@ async def get_weather(ctx: RunContext[Deps], lat: float, lng: float) -> dict[str
lat: Latitude of the location.
lng: Longitude of the location.
"""
+ # NOTE: the responses here will be random, and are not related to the lat and lng.
temp_response, descr_response = await asyncio.gather(
ctx.deps.client.get(
'https://demo-endpoints.pydantic.workers.dev/number',
diff --git a/pyproject.toml b/pyproject.toml
index e033c11..8cb7eba 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,8 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"devtools>=0.12.2",
- "logfire[httpx]>=3.21.1",
+ "fastapi>=0.115.14",
+ "logfire[fastapi,httpx]>=3.21.1",
"pydantic-ai>=0.3.4",
]
diff --git a/uv.lock b/uv.lock
index e4c9abe..149a5dd 100644
--- a/uv.lock
+++ b/uv.lock
@@ -52,6 +52,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708, upload-time = "2025-04-03T04:57:01.591Z" },
]
+[[package]]
+name = "asgiref"
+version = "3.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/68/fb4fb78c9eac59d5e819108a57664737f855c5a8e9b76aec1738bb137f9e/asgiref-3.9.0.tar.gz", hash = "sha256:3dd2556d0f08c4fab8a010d9ab05ef8c34565f6bf32381d17505f7ca5b273767", size = 36772, upload-time = "2025-07-03T13:25:01.491Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/f9/76c9f4d4985b5a642926162e2d41fe6019b1fa929cfa58abb7d2dc9041e5/asgiref-3.9.0-py3-none-any.whl", hash = "sha256:06a41250a0114d2b6f6a2cb3ab962147d355b53d1de15eebc34a9d04a7b79981", size = 23788, upload-time = "2025-07-03T13:24:59.115Z" },
+]
+
[[package]]
name = "asttokens"
version = "2.4.1"
@@ -250,6 +259,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/20/133fbd3c62829f8b9d0589694201c36175a6a359393d3c1bfd9f8c14994e/fasta2a-0.3.4-py3-none-any.whl", hash = "sha256:8c23ec0fa1e29f58031d32c3ff530e69da298af893f883c94ba973e2ba3ccaa9", size = 15329, upload-time = "2025-06-26T06:25:07.259Z" },
]
+[[package]]
+name = "fastapi"
+version = "0.115.14"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" },
+]
+
[[package]]
name = "fastavro"
version = "1.11.1"
@@ -571,6 +594,9 @@ wheels = [
]
[package.optional-dependencies]
+fastapi = [
+ { name = "opentelemetry-instrumentation-fastapi" },
+]
httpx = [
{ name = "opentelemetry-instrumentation-httpx" },
]
@@ -719,6 +745,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/60/7d/8ddfda1506c2fcca137924d5688ccabffa1aed9ec0955b7d0772de02cec3/opentelemetry_instrumentation-0.55b1-py3-none-any.whl", hash = "sha256:cbb1496b42bc394e01bc63701b10e69094e8564e281de063e4328d122cc7a97e", size = 31108, upload-time = "2025-06-10T08:57:14.355Z" },
]
+[[package]]
+name = "opentelemetry-instrumentation-asgi"
+version = "0.55b1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asgiref" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "opentelemetry-util-http" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/51/4a/900ea42d36757e3b7219f873d3d16358107da43fcb8d7f11a2b1d0bb56a0/opentelemetry_instrumentation_asgi-0.55b1.tar.gz", hash = "sha256:615cde388dd3af4d0e52629a6c75828253618aebcc6e65d93068463811528606", size = 24356, upload-time = "2025-06-10T08:58:19.347Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/45/b5f78f0456f8e2e2ec152d7b6496197f5661c7ca49f610fe19c63b350aa4/opentelemetry_instrumentation_asgi-0.55b1-py3-none-any.whl", hash = "sha256:186620f7d0a71c8c817c5cbe91c80faa8f9c50967d458b8131c5694e21eb8583", size = 16402, upload-time = "2025-06-10T08:57:22.034Z" },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-fastapi"
+version = "0.55b1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-instrumentation-asgi" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "opentelemetry-util-http" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2b/76/0df9cdff4cce18b1967e97152d419e2325c307ff96eb6ba8e69294690c18/opentelemetry_instrumentation_fastapi-0.55b1.tar.gz", hash = "sha256:bb9f8c13a053e7ff7da221248067529cc320e9308d57f3908de0afa36f6c5744", size = 20275, upload-time = "2025-06-10T08:58:29.281Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/6e/d608a9336ede3d15869c70ebdd4ec670f774641104b0873bb973bce9d822/opentelemetry_instrumentation_fastapi-0.55b1-py3-none-any.whl", hash = "sha256:af4c09aebb0bd6b4a0881483b175e76547d2bc96329c94abfb794bf44f29f6bb", size = 12713, upload-time = "2025-06-10T08:57:39.712Z" },
+]
+
[[package]]
name = "opentelemetry-instrumentation-httpx"
version = "0.55b1"
@@ -973,14 +1031,16 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "devtools" },
- { name = "logfire", extra = ["httpx"] },
+ { name = "fastapi" },
+ { name = "logfire", extra = ["fastapi", "httpx"] },
{ name = "pydantic-ai" },
]
[package.metadata]
requires-dist = [
{ name = "devtools", specifier = ">=0.12.2" },
- { name = "logfire", extras = ["httpx"], specifier = ">=3.21.1" },
+ { name = "fastapi", specifier = ">=0.115.14" },
+ { name = "logfire", extras = ["fastapi", "httpx"], specifier = ">=3.21.1" },
{ name = "pydantic-ai", specifier = ">=0.3.4" },
]
@@ -1242,15 +1302,14 @@ wheels = [
[[package]]
name = "starlette"
-version = "0.47.1"
+version = "0.46.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/0a/69/662169fdb92fb96ec3eaee218cf540a629d629c86d7993d9651226a6789b/starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b", size = 2583072, upload-time = "2025-06-21T04:03:17.337Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/82/95/38ef0cd7fa11eaba6a99b3c4f5ac948d8bc6ff199aabd327a29cc000840c/starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527", size = 72747, upload-time = "2025-06-21T04:03:15.705Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" },
]
[[package]]