Skip to content

Commit 20bf143

Browse files
committed
fastapi example
1 parent c1f1af8 commit 20bf143

File tree

6 files changed

+355
-7
lines changed

6 files changed

+355
-7
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ wheels/
99
# Virtual environments
1010
.venv
1111
*.svg
12+
13+
/fastapi-example/images/

fastapi-example/index.html

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Image Generator</title>
7+
<link
8+
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap"
9+
rel="stylesheet"
10+
/>
11+
<style>
12+
body {
13+
font-family: "IBM Plex Sans", sans-serif;
14+
max-width: 800px;
15+
margin: 0 auto;
16+
padding: 80px 20px 20px;
17+
background-color: #f5f5f5;
18+
}
19+
20+
.container {
21+
text-align: center;
22+
}
23+
24+
h1 {
25+
color: #333;
26+
margin-bottom: 30px;
27+
}
28+
29+
.prompt-section {
30+
margin-bottom: 20px;
31+
}
32+
33+
input[type="text"] {
34+
width: 100%;
35+
padding: 10px;
36+
font-size: 16px;
37+
border: 2px solid #ddd;
38+
border-radius: 8px;
39+
box-sizing: border-box;
40+
margin-bottom: 15px;
41+
}
42+
43+
input[type="text"]:focus {
44+
outline: none;
45+
border-color: #4caf50;
46+
}
47+
48+
button {
49+
background-color: #4caf50;
50+
color: white;
51+
padding: 15px 30px;
52+
font-size: 16px;
53+
border: none;
54+
border-radius: 8px;
55+
cursor: pointer;
56+
margin: 5px;
57+
}
58+
59+
button:hover {
60+
background-color: #45a049;
61+
}
62+
63+
button:disabled {
64+
background-color: #cccccc;
65+
cursor: not-allowed;
66+
}
67+
68+
.clear-btn {
69+
background-color: #f44336;
70+
}
71+
72+
.clear-btn:hover {
73+
background-color: #da190b;
74+
}
75+
76+
.loading {
77+
margin: 20px 0;
78+
color: #666;
79+
}
80+
81+
.image-container {
82+
margin-top: 20px;
83+
}
84+
85+
.generated-image {
86+
max-width: 100%;
87+
height: auto;
88+
border-radius: 10px;
89+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
90+
}
91+
92+
.error {
93+
color: #f44336;
94+
margin: 20px 0;
95+
padding: 15px;
96+
background-color: #ffebee;
97+
border-radius: 5px;
98+
}
99+
100+
.hidden {
101+
display: none;
102+
}
103+
</style>
104+
</head>
105+
<body>
106+
<div class="container">
107+
<h1>Image Generator</h1>
108+
109+
<div id="promptSection" class="prompt-section">
110+
<input
111+
type="text"
112+
id="promptInput"
113+
placeholder="Painting of an iphone in the style of Titian..."
114+
maxlength="500"
115+
/>
116+
<br />
117+
<button id="generateBtn" onclick="generateImage()">
118+
Generate Image
119+
</button>
120+
</div>
121+
122+
<div id="loadingSection" class="loading hidden">
123+
<p>Generating your image...</p>
124+
</div>
125+
126+
<div id="errorSection" class="error hidden">
127+
<p id="errorMessage"></p>
128+
</div>
129+
130+
<div id="imageSection" class="image-container hidden">
131+
<img
132+
id="generatedImage"
133+
class="generated-image"
134+
alt="Generated image"
135+
/>
136+
<br /><br />
137+
<button class="clear-btn" onclick="clearImage()">Clear</button>
138+
</div>
139+
</div>
140+
141+
<script>
142+
const promptInput = document.getElementById("promptInput");
143+
const generateBtn = document.getElementById("generateBtn");
144+
const promptSection = document.getElementById("promptSection");
145+
const loadingSection = document.getElementById("loadingSection");
146+
const errorSection = document.getElementById("errorSection");
147+
const imageSection = document.getElementById("imageSection");
148+
const generatedImage = document.getElementById("generatedImage");
149+
const errorMessage = document.getElementById("errorMessage");
150+
151+
promptInput.addEventListener("keypress", function (e) {
152+
if (e.key === "Enter") {
153+
generateImage();
154+
}
155+
});
156+
157+
async function generateImage() {
158+
const prompt = promptInput.value.trim();
159+
160+
if (!prompt) {
161+
showError("Please enter a description for the image.");
162+
return;
163+
}
164+
165+
showLoading();
166+
167+
const params = new URLSearchParams();
168+
params.append("prompt", prompt);
169+
170+
try {
171+
const response = await fetch(`/generate?${params.toString()}`, {
172+
method: "POST",
173+
});
174+
175+
if (!response.ok) {
176+
throw new Error(`HTTP error! status: ${response.status}`);
177+
}
178+
179+
const { imageUrl } = await response.json();
180+
181+
showImage(imageUrl);
182+
} catch (error) {
183+
console.error("Error generating image:", error);
184+
showError("Failed to generate image. Please try again.");
185+
}
186+
}
187+
188+
function showLoading() {
189+
promptSection.classList.add("hidden");
190+
errorSection.classList.add("hidden");
191+
imageSection.classList.add("hidden");
192+
loadingSection.classList.remove("hidden");
193+
}
194+
195+
function showImage(imageUrl) {
196+
loadingSection.classList.add("hidden");
197+
errorSection.classList.add("hidden");
198+
generatedImage.src = imageUrl;
199+
imageSection.classList.remove("hidden");
200+
}
201+
202+
function showError(message) {
203+
loadingSection.classList.add("hidden");
204+
imageSection.classList.add("hidden");
205+
errorMessage.textContent = message;
206+
errorSection.classList.remove("hidden");
207+
promptSection.classList.remove("hidden");
208+
}
209+
210+
function clearImage() {
211+
imageSection.classList.add("hidden");
212+
errorSection.classList.add("hidden");
213+
promptSection.classList.remove("hidden");
214+
promptInput.value = "";
215+
promptInput.focus();
216+
217+
// Clean up the blob URL
218+
URL.revokeObjectURL(generatedImage.src);
219+
}
220+
</script>
221+
</body>
222+
</html>

fastapi-example/main.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from contextlib import asynccontextmanager
2+
from pathlib import Path
3+
from uuid import uuid4
4+
5+
import logfire
6+
from fastapi import FastAPI
7+
from fastapi.staticfiles import StaticFiles
8+
from httpx import AsyncClient
9+
from openai import AsyncOpenAI
10+
from pydantic import BaseModel, Field
11+
from starlette.responses import FileResponse
12+
13+
logfire.configure(service_name='fastapi-example')
14+
http_client: AsyncClient
15+
openai_client = AsyncOpenAI()
16+
logfire.instrument_openai(openai_client)
17+
18+
19+
@asynccontextmanager
20+
async def lifespan(_app: FastAPI):
21+
global http_client, openai_client
22+
async with AsyncClient() as _http_client:
23+
http_client = _http_client
24+
logfire.instrument_httpx(http_client, headers=True)
25+
yield
26+
27+
28+
app = FastAPI(lifespan=lifespan)
29+
this_dir = Path(__file__).parent
30+
image_dir = Path(__file__).parent / 'images'
31+
image_dir.mkdir(exist_ok=True)
32+
app.mount('/static', StaticFiles(directory=image_dir), name='static')
33+
34+
35+
@app.get('/')
36+
async def main() -> FileResponse:
37+
return FileResponse(this_dir / 'index.html')
38+
39+
40+
class GenerateResponse(BaseModel):
41+
image_url: str = Field(serialization_alias='imageUrl')
42+
43+
44+
@app.post('/generate')
45+
async def generate_image(prompt: str) -> GenerateResponse:
46+
response = await openai_client.images.generate(prompt=prompt, model='dall-e-3')
47+
48+
assert response.data, 'No image in response'
49+
50+
image_url = response.data[0].url
51+
assert image_url, 'No image URL in response'
52+
r = await http_client.get(image_url)
53+
r.raise_for_status()
54+
path = f'{uuid4().hex}.jpg'
55+
(image_dir / path).write_bytes(r.content)
56+
return GenerateResponse(image_url=f'/static/{path}')
57+
58+
59+
if __name__ == '__main__':
60+
import uvicorn
61+
62+
uvicorn.run(app, port=8000)

pai-weather/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ async def get_lat_lng(ctx: RunContext[Deps], location_description: str) -> LatLn
4242
ctx: The context.
4343
location_description: A description of a location.
4444
"""
45+
# NOTE: the response here will be random, and is not related to the location description.
4546
r = await ctx.deps.client.get(
4647
'https://demo-endpoints.pydantic.workers.dev/latlng',
4748
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
5960
lat: Latitude of the location.
6061
lng: Longitude of the location.
6162
"""
63+
# NOTE: the responses here will be random, and are not related to the lat and lng.
6264
temp_response, descr_response = await asyncio.gather(
6365
ctx.deps.client.get(
6466
'https://demo-endpoints.pydantic.workers.dev/number',

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
88
"devtools>=0.12.2",
9-
"logfire[httpx]>=3.21.1",
9+
"fastapi>=0.115.14",
10+
"logfire[fastapi,httpx]>=3.21.1",
1011
"pydantic-ai>=0.3.4",
1112
]
1213

0 commit comments

Comments
 (0)