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

+ +
+ +
+ +
+ + + + + + +
+ + + + 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]]