-
-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathpopulate_db.py
More file actions
393 lines (360 loc) · 19.5 KB
/
populate_db.py
File metadata and controls
393 lines (360 loc) · 19.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
import asyncio
from datetime import UTC, datetime, timedelta
from pathlib import Path
import httpx
from sqlalchemy import delete, select, update
import models
from config import settings
from database import AsyncSessionLocal, engine
from image_utils import _get_s3_client
from main import app
POPULATE_IMAGES_DIR = Path("populate_images")
USERS = [
{
"username": "CoreyMSchafer",
"email": "CoreyMSchafer@gmail.com",
"password": "TestPassword1!",
"image": "corey.png",
},
{
"username": "DefaultDude",
"email": "TestEmail2@test.com",
"password": "TestPassword2!",
# No image - uses default
},
{
"username": "WillowTheCat",
"email": "TestEmail3@test.com",
"password": "TestPassword3!",
"image": "willow.png",
},
{
"username": "FarmDogs",
"email": "TestEmail4@test.com",
"password": "TestPassword4!",
"image": "farmdogs.png",
},
{
"username": "PoppyTheCoder",
"email": "TestEmail5@test.com",
"password": "TestPassword5!",
"image": "poppy.png",
},
{
"username": "GoodBoyBronx",
"email": "TestEmail6@test.com",
"password": "TestPassword6!",
"image": "bronx.png",
},
]
POSTS = [
{
"title": "Why I Love FastAPI",
"content": "FastAPI has completely changed how I build APIs. The automatic documentation, type hints, and async support make development so much faster. Plus, the performance is incredible!",
},
{
"title": "Corey Schafer Has the Best YouTube Tutorials!",
"content": "This was written by a viewer and definitely not by me... I mean him. Totally not written by him, but by me... a real viewer. Seriously, check out his channel for amazing Python content.",
},
{
"title": "Async/Await Finally Clicked",
"content": "I've been struggling with async programming for months, but FastAPI's approach finally made it click. Using 'async def' for endpoints and 'await' for database calls just makes sense.",
},
{
"title": "Schafer? I Barely Know Her!",
"content": "Is anyone actually reading these blog posts? Do they really need to say anything? I can keep going all day. At least AI can... Claude, keep going, please.",
},
{
"title": "Pydantic Validation is Magic",
"content": "The way Pydantic handles validation in FastAPI is incredible. Define your model with type hints, and boom - automatic validation, serialization, and documentation. No more writing validation code by hand!",
},
{
"title": "From Flask to FastAPI",
"content": "I made the switch from Flask to FastAPI last month. The learning curve was minimal, and the benefits are huge. Automatic OpenAPI docs, better performance, and native async support. No regrets!",
},
{
"title": "Some of My Favorite Horror Movies",
"content": "I love horror movies and practical effects. One of my favorites is 'The Thing'. Hereditary is a great modern one, but most people have seen it. One modern one I really liked that not as many people have seen is 'The Night House'. It's a slow burn but really effective. More psychological than jump-scare based.",
},
{
"title": "Type Hints Changed My Life",
"content": "I used to think type hints were just extra typing (pun intended). But after using FastAPI, I see how they enable incredible tooling - better autocomplete, automatic validation, and self-documenting code.",
},
{
"title": "The Power of Dependency Injection",
"content": "FastAPI's dependency injection system is so elegant. Need a database session? Just add it as a parameter. Need the current user? Same thing. It makes the code so clean and testable.",
},
{
"title": "SQLAlchemy 2.0 Is Worth the Upgrade",
"content": "If you're still using SQLAlchemy 1.x patterns, it's time to upgrade. The new 2.0 style with select() and mapped_column() is much more explicit and works beautifully with async.",
},
{
"title": "Hot Take: Python > JavaScript for APIs",
"content": "Yes, I said it. For backend APIs, Python with FastAPI beats Node.js. Fight me in the comments. (Just kidding, this blog doesn't have comments... yet.)",
},
{
"title": "Understanding HTTP Status Codes",
"content": "200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Internal Server Error. Learn these codes - they're how your API communicates with the world. FastAPI makes it easy to return the right ones.",
},
{
"title": "Some of My Favorite Video Games",
"content": "The one I probably play the most, but not my favorite, is League of Legends... It's a love/hate relationship. If you play, you get it. My favorites are all single-player RPGs. The Elder Scrolls series (Especially Morrowind and Skyrim) were awesome. The Baldur's Gate series took up a lot of my time as a kid, and more recently, the 3rd one was great. Speaking of Baldur's Gate, I love that old isometric style of RPG, so I looked for more modern equivalents and found Pillars of Eternity, which was fantastic. Also both Pathfinder: Kingmaker and Wrath of the Righteous were a lot of fun as well.",
},
{
"title": "JWT Authentication Demystified",
"content": "JSON Web Tokens seemed scary at first, but they're actually pretty simple. Encode some user data, sign it with a secret, and use it to verify requests. FastAPI + PyJWT makes it straightforward.",
},
{
"title": "Tips for API Design",
"content": "Use nouns for resources (/users, /posts), HTTP verbs for actions (GET, POST, PUT, DELETE), and return consistent responses. FastAPI's response_model helps enforce this consistency.",
},
{
"title": "Path Parameters vs Query Parameters",
"content": "Use path parameters for required resource identifiers (/users/123) and query parameters for optional filters (/posts?author=corey&limit=10). FastAPI handles both beautifully with automatic validation.",
},
{
"title": "Error Handling Done Right",
"content": "Don't just return 500 for everything! Use HTTPException to return meaningful status codes and messages. Your API consumers will thank you when debugging issues.",
},
{
"title": "Why I Switched to UV",
"content": "UV is blazingly fast for Python package management. Install packages in milliseconds instead of minutes. If you haven't tried it yet, you're missing out!",
},
{
"title": "What About Favorite Books?",
"content": "I don't read a lot of fiction. The last fiction book I read was 'The Martian' by Andy Weir, which I really enjoyed. But most of my reading is non-fiction. Some of my favorites are 'Meditations' by Marcus Aurelius, 'Conscious' by Annaka Harris, 'How to Die' by Seneca, and 'The Last Lecture' by Randy Pausch. The latest fiction book I'm reading through (and have been for a while) is 'House of Leaves' by Mark Z. Danielewski. It's... different, but awesome.",
},
{
"title": "Testing FastAPI Applications",
"content": "FastAPI's TestClient makes testing a breeze. Write tests for your endpoints, mock dependencies, and catch bugs before they hit production. Your future self will thank you.",
},
{
"title": "Environment Variables and Security",
"content": "Never hardcode secrets! Use environment variables and pydantic-settings to keep your API keys, database URLs, and JWT secrets safe. It's Security 101.",
},
{
"title": "CORS: The Bane of Frontend Devs",
"content": "Getting CORS errors? FastAPI's CORSMiddleware is your friend. Just remember: be specific about allowed origins in production. Don't use '*' unless you really mean it.",
},
{
"title": "Async Database Queries",
"content": "Blocking database calls in async code? That's a performance killer. Use async drivers like psycopg (for PostgreSQL) or aiosqlite to keep your event loop happy.",
},
{
"title": "The Beauty of Response Models",
"content": "Response models aren't just for documentation - they filter out sensitive fields automatically. Define what goes out, and Pydantic handles the rest.",
},
{
"title": "Let's Talk Board Games",
"content": "I love Settlers of Catan. It's a classic for a reason. I'm actually going to make a sword in my woodshop soon that will be my friend group's trophy for the annual Catan champion that we're going to call 'The Katana of Catan'. One thing I've always wanted to do, but never have, is play an in-person Dungeons & Dragons campaign. I've played so many D&D inspired video games, but never the real deal. Hopefully someday...",
},
{
"title": "API Versioning Strategies",
"content": "APIs evolve. Version them from day one! Whether you use URL prefixes (/v1/users) or headers, plan for change. Breaking changes without versioning breaks trust.",
},
{
"title": "Background Tasks in FastAPI",
"content": "Don't make users wait for emails to send or files to process. FastAPI's BackgroundTasks lets you return immediately while work continues in the background.",
},
{
"title": "Rate Limiting Your API",
"content": "Protect your API from abuse with rate limiting. Too many requests? Return 429 Too Many Requests. Your server (and your wallet) will thank you.",
},
{
"title": "Documentation That Writes Itself",
"content": "Add docstrings to your endpoints and they appear in Swagger UI. Add examples to your Pydantic models and they show up too. Documentation has never been this easy.",
},
{
"title": "WebSockets with FastAPI",
"content": "REST isn't the only game in town. FastAPI supports WebSockets for real-time communication. Chat apps, live updates, notifications - all possible!",
},
{
"title": "Favorite Hobbies, You Ask?",
"content": "Woodworking, hands down. I love making things with wood, but I wish I had more time for it. There's something special about making something with your own hands, with materials that are local. A lot of the stuff I've built came from trees that fell on my family's property. My stuff might not always be as good as something you buy in a store, but there's a story and a connection there that makes it better than anything I could buy elsewhere.",
},
{
"title": "Custom Validators in Pydantic",
"content": "Need validation beyond type checking? Pydantic's field_validator and model_validator decorators let you add custom logic. Validate emails, check password strength, whatever you need.",
},
{
"title": "The ORM vs Raw SQL Debate",
"content": "ORMs like SQLAlchemy add abstraction but can hide performance issues. Know when to use the ORM and when to drop to raw SQL. Both have their place.",
},
{
"title": "Debugging Async Code",
"content": "Async bugs can be tricky. Use logging liberally, understand the event loop, and don't mix sync and async without care. asyncio.run() is your entry point.",
},
{
"title": "Containerizing FastAPI Apps",
"content": "Docker + FastAPI = deployment bliss. Create a Dockerfile, build your image, and deploy anywhere. Consistency across environments is priceless.",
},
{
"title": "Health Check Endpoints",
"content": "Add a /health endpoint to your API. Load balancers and orchestrators need to know if your service is alive. Return 200 if healthy, details if not. I didn't do this in this tutorial, but there's only so much time in a video!",
},
{
"title": "Hmm... What Else?",
"content": "I'm running out of ideas for these blog posts. Maybe I should just write about how great FastAPI is... Oh wait, I've already done that multiple times. Well, if you're still reading, thanks for sticking with it! You're awesome.",
},
{
"title": "Pagination: Don't Return Everything",
"content": "Returning 10,000 records in one response? Please don't. Implement pagination with limit and offset (or better, cursor-based). Your database and clients will be happier.",
},
{
"title": "OpenAPI Schema Customization",
"content": "FastAPI's auto-generated OpenAPI schema is great, but sometimes you need to customize. Add examples, descriptions, and tags to make your docs shine.",
},
{
"title": "Security Headers Matter",
"content": "Add security headers to your responses: X-Content-Type-Options, X-Frame-Options, Content-Security-Policy. Small effort, big security improvement.",
},
{
"title": "Caching Strategies",
"content": "Not every request needs to hit the database. Use caching with Redis or even in-memory for frequently accessed data. Your response times will plummet (in a good way).",
},
{
"title": "GraphQL vs REST",
"content": "GraphQL is trendy, but REST is battle-tested. Choose based on your needs, not hype. FastAPI excels at REST, but Strawberry brings GraphQL if you need it.",
},
{
"title": "Movie Quotes!",
"content": "'You wanna know how I did it? This is how I did it, Anton. I never saved anything for the swim back.' - 'Gattaca'. One of my favorite movies of all time. As silly as it sounds, that movie is actually one of the main reasons I decided to pursue an internship at NASA back in college. After that internship, I found I had a craving to learn and do more. It pushed me to take programming more seriously, which eventually led me to where I am today... Which is writing a blog post about FastAPI that's just meant to fill space. TLDR: I watched Gattaca and now I'm writing sample blog posts at 3am on a Saturday for this FastAPI tutorial. And you can too!",
},
]
# The 44th post - always the oldest (easter egg for pagination tutorial)
POST_44 = {
"title": "Fun Fact: My High School Football Number Was #44",
"content": "If you've paginated all the way to this post, the 44th one... you get to learn this fun fact: that my high school football number was #44. Other notable absolute legends who wore number #44 include: Jerry West (NBA - Also fellow WV Native), Hank Aaron (MLB), and Floyd Little (NFL).",
}
async def clear_existing_data() -> None:
# Delete profile pictures from S3 (need DB records to know which files)
async with AsyncSessionLocal() as db:
result = await db.execute(
select(models.User.image_file).where(models.User.image_file.is_not(None)),
)
filenames = result.scalars().all()
if filenames:
s3 = _get_s3_client()
s3.delete_objects(
Bucket=settings.s3_bucket_name,
Delete={"Objects": [{"Key": f"profile_pics/{f}"} for f in filenames]},
)
print(f"Deleted {len(filenames)} images from S3")
# Clear database tables (order respects foreign keys)
async with AsyncSessionLocal() as db:
await db.execute(delete(models.PasswordResetToken))
await db.execute(delete(models.Post))
await db.execute(delete(models.User))
await db.commit()
print("Cleared existing data")
async def update_post_dates() -> None:
now = datetime.now(UTC)
async with AsyncSessionLocal() as db:
result = await db.execute(select(models.Post).order_by(models.Post.id))
posts = result.scalars().all()
if not posts:
return
# First post (POST_44) is the oldest - ~90 days ago
await db.execute(
update(models.Post)
.where(models.Post.id == posts[0].id)
.values(date_posted=now - timedelta(days=90)),
)
# Remaining posts: each ~1.5 days newer than previous
for i, post in enumerate(posts[1:], start=1):
days_ago = (len(posts) - i) * 1.5
hours_offset = (i * 7) % 24
post_date = now - timedelta(days=days_ago, hours=hours_offset)
await db.execute(
update(models.Post)
.where(models.Post.id == post.id)
.values(date_posted=post_date),
)
await db.commit()
print("Updated post dates")
async def populate() -> None:
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(
transport=transport,
base_url="http://localhost",
) as client:
# Clear existing data (S3 images first, then database)
await clear_existing_data()
users: list[dict] = []
print(f"\nCreating {len(USERS)} users...")
for user_data in USERS:
response = await client.post(
"/api/users",
json={
"username": user_data["username"],
"email": user_data["email"],
"password": user_data["password"],
},
)
response.raise_for_status()
user = response.json()
print(f" Created: {user['username']}")
response = await client.post(
"/api/users/token",
data={
"username": user_data["email"],
"password": user_data["password"],
},
)
response.raise_for_status()
token = response.json()["access_token"]
if image_name := user_data.get("image"):
image_path = POPULATE_IMAGES_DIR / image_name
if image_path.exists():
response = await client.patch(
f"/api/users/{user['id']}/picture",
files={
"file": (
image_name,
image_path.read_bytes(),
"image/png",
),
},
headers={"Authorization": f"Bearer {token}"},
)
response.raise_for_status()
print(f" Uploaded: {image_name}")
users.append(
{"id": user["id"], "username": user["username"], "token": token},
)
print(f"\nCreating {len(POSTS) + 1} posts...")
# First create POST_44 (will become oldest after date update)
response = await client.post(
"/api/posts",
json={"title": POST_44["title"], "content": POST_44["content"]},
headers={"Authorization": f"Bearer {users[0]['token']}"},
)
response.raise_for_status()
print(f" Created: '{POST_44['title']}'")
# Create remaining posts in reverse (last in list = oldest, first = newest)
for i, post_data in enumerate(reversed(POSTS)):
user = users[i % len(users)]
response = await client.post(
"/api/posts",
json={
"title": post_data["title"],
"content": post_data["content"],
},
headers={"Authorization": f"Bearer {user['token']}"},
)
response.raise_for_status()
title = post_data["title"]
print(
f" Created: '{title[:50]}...'"
if len(title) > 50
else f" Created: '{title}'",
)
print("\nUpdating post dates...")
await update_post_dates()
await engine.dispose()
print("\nDone!")
print(f" {len(USERS)} users")
print(f" {len(POSTS) + 1} posts")
print(" Profile pictures uploaded to S3")
if __name__ == "__main__":
asyncio.run(populate())