Skip to content

Add FastAPI example #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ wheels/
# Virtual environments
.venv
*.svg

/fastapi-example/images/
64 changes: 64 additions & 0 deletions fastapi-example/main.py
Original file line number Diff line number Diff line change
@@ -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)
228 changes: 228 additions & 0 deletions fastapi-example/page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Image Generator</title>
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<style>
body {
font-family: "IBM Plex Sans", sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 50px 20px 20px;
background-color: #f5f5f5;
}

.container {
text-align: center;
}

h1 {
color: #333;
margin-bottom: 30px;
}

.prompt-section {
margin-bottom: 20px;
}

input[type="text"] {
width: 100%;
padding: 10px;
font-size: 16px;
font-family: inherit;
border: 2px solid #ddd;
border-radius: 8px;
box-sizing: border-box;
margin-bottom: 15px;
}

input[type="text"]:focus {
outline: none;
border-color: #4caf50;
}

a {
text-decoration: none;
}

a,
button {
background-color: #4caf50;
color: white;
padding: 15px 30px;
font-size: 16px;
font-family: inherit;
border: none;
border-radius: 8px;
cursor: pointer;
margin: 5px;
}

button:hover {
background-color: #45a049;
}

button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}

.clear-btn {
background-color: #f44336;
}

.clear-btn:hover {
background-color: #da190b;
}

.loading {
margin: 20px 0;
color: #666;
}

.image-container {
margin-top: 20px;
}

.generated-image {
max-width: 100%;
max-height: 600px;
height: auto;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}

.error {
color: #f44336;
margin: 20px 0;
padding: 15px;
background-color: #ffebee;
border-radius: 5px;
}

.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<h1>Image Generator</h1>

<div id="promptSection" class="prompt-section">
<input
type="text"
id="promptInput"
placeholder="Painting of an iphone in the style of Titian..."
maxlength="500"
/>
<br />
<button id="generateBtn" onclick="generateImage()">
Generate Image
</button>
</div>

<div id="loadingSection" class="loading hidden">
<p>Generating your image...</p>
</div>

<div id="errorSection" class="error hidden">
<p id="errorMessage"></p>
</div>

<div id="imageSection" class="image-container hidden">
<img
id="generatedImage"
class="generated-image"
alt="Generated image"
/>
<br /><br />
<a href="/" class="clear-btn" onclick="clearImage()">Clear</a>
</div>
</div>

<script>
const promptInput = document.getElementById("promptInput");
const generateBtn = document.getElementById("generateBtn");
const promptSection = document.getElementById("promptSection");
const loadingSection = document.getElementById("loadingSection");
const errorSection = document.getElementById("errorSection");
const imageSection = document.getElementById("imageSection");
const generatedImage = document.getElementById("generatedImage");
const errorMessage = document.getElementById("errorMessage");

promptInput.addEventListener("keypress", function (e) {
if (e.key === "Enter") {
generateImage();
}
});

async function generateImage() {
const prompt = promptInput.value.trim();

if (!prompt) {
showError("Please enter a description for the image.");
return;
}

showLoading();

const params = new URLSearchParams();
params.append("prompt", prompt);

try {
const response = await fetch(`/generate?${params.toString()}`, {
method: "POST",
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const { nextUrl } = await response.json();

window.location.href = nextUrl;
} catch (error) {
console.error("Error generating image:", error);
showError("Failed to generate image. Please try again.");
}
}

function showLoading() {
promptSection.classList.add("hidden");
errorSection.classList.add("hidden");
imageSection.classList.add("hidden");
loadingSection.classList.remove("hidden");
}

function showImage(imageUrl) {
loadingSection.classList.add("hidden");
errorSection.classList.add("hidden");
generatedImage.src = imageUrl;
imageSection.classList.remove("hidden");
}

function showError(message) {
loadingSection.classList.add("hidden");
imageSection.classList.add("hidden");
errorMessage.textContent = message;
errorSection.classList.remove("hidden");
promptSection.classList.remove("hidden");
}

const path = window.location.pathname;
const imageMatch = path.match(/^\/display\/(.+\.jpg)$/);

if (imageMatch) {
const imageUrl = `/static/${imageMatch[1]}`;
showLoading();
showImage(imageUrl);
}
</script>
</body>
</html>
2 changes: 2 additions & 0 deletions pai-weather/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)},
Expand All @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]

Expand Down
Loading