@su-record/vibe 0.4.6 โ†’ 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,386 @@
1
+ # ๐Ÿ Python + FastAPI ํ’ˆ์งˆ ๊ทœ์น™
2
+
3
+ ## ํ•ต์‹ฌ ์›์น™ (core์—์„œ ์ƒ์†)
4
+
5
+ ```markdown
6
+ โœ… ๋‹จ์ผ ์ฑ…์ž„ (SRP)
7
+ โœ… ์ค‘๋ณต ์ œ๊ฑฐ (DRY)
8
+ โœ… ์žฌ์‚ฌ์šฉ์„ฑ
9
+ โœ… ๋‚ฎ์€ ๋ณต์žก๋„
10
+ โœ… ํ•จ์ˆ˜ โ‰ค 30์ค„ (๊ถŒ์žฅ), โ‰ค 50์ค„ (ํ—ˆ์šฉ)
11
+ โœ… ์ค‘์ฒฉ โ‰ค 3๋‹จ๊ณ„
12
+ โœ… Cyclomatic complexity โ‰ค 10
13
+ ```
14
+
15
+ ## Python ํŠนํ™” ๊ทœ์น™
16
+
17
+ ### 1. ํƒ€์ž… ํžŒํŠธ 100% ํ•„์ˆ˜
18
+
19
+ ```python
20
+ # โŒ ํƒ€์ž… ํžŒํŠธ ์—†์Œ
21
+ def get_user(user_id):
22
+ return db.get(user_id)
23
+
24
+ # โœ… ์™„์ „ํ•œ ํƒ€์ž… ํžŒํŠธ
25
+ async def get_user(user_id: str, db: AsyncSession) -> User | None:
26
+ result = await db.execute(select(User).where(User.id == user_id))
27
+ return result.scalar_one_or_none()
28
+ ```
29
+
30
+ ### 2. Pydantic์œผ๋กœ Contract ์ •์˜
31
+
32
+ ```python
33
+ from pydantic import BaseModel, Field, EmailStr, field_validator
34
+
35
+ class CreateUserRequest(BaseModel):
36
+ """์‚ฌ์šฉ์ž ์ƒ์„ฑ ์š”์ฒญ ์Šคํ‚ค๋งˆ"""
37
+ email: EmailStr
38
+ username: str = Field(min_length=3, max_length=50)
39
+ password: str = Field(min_length=8)
40
+ age: int = Field(ge=0, le=150)
41
+
42
+ @field_validator("username")
43
+ def validate_username(cls, v: str) -> str:
44
+ if not v.isalnum():
45
+ raise ValueError("Username must be alphanumeric")
46
+ return v.lower()
47
+
48
+ class Config:
49
+ json_schema_extra = {
50
+ "example": {
51
+ "email": "user@example.com",
52
+ "username": "johndoe",
53
+ "password": "securepass123",
54
+ "age": 25,
55
+ }
56
+ }
57
+
58
+ class UserResponse(BaseModel):
59
+ """์‚ฌ์šฉ์ž ์‘๋‹ต ์Šคํ‚ค๋งˆ"""
60
+ id: str
61
+ email: str
62
+ username: str
63
+ created_at: datetime
64
+
65
+ class Config:
66
+ from_attributes = True # SQLAlchemy ํ˜ธํ™˜
67
+ ```
68
+
69
+ ### 3. async/await ํŒจํ„ด
70
+
71
+ ```python
72
+ # โœ… ๋น„๋™๊ธฐ I/O (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, API ํ˜ธ์ถœ)
73
+ async def get_user_with_posts(
74
+ user_id: str,
75
+ db: AsyncSession
76
+ ) -> tuple[User, list[Post]]:
77
+ # ๋ณ‘๋ ฌ ์‹คํ–‰
78
+ user_task = db.execute(select(User).where(User.id == user_id))
79
+ posts_task = db.execute(select(Post).where(Post.user_id == user_id))
80
+
81
+ user_result, posts_result = await asyncio.gather(user_task, posts_task)
82
+
83
+ user = user_result.scalar_one_or_none()
84
+ posts = list(posts_result.scalars().all())
85
+
86
+ return user, posts
87
+
88
+ # โŒ ๋™๊ธฐ ํ•จ์ˆ˜ (๋ธ”๋กœํ‚น)
89
+ def get_user(user_id: str):
90
+ return requests.get(f"/users/{user_id}") # ๋ธ”๋กœํ‚น!
91
+ ```
92
+
93
+ ### 4. Early Return ์„ ํ˜ธ
94
+
95
+ ```python
96
+ # โŒ ์ค‘์ฒฉ๋œ if๋ฌธ
97
+ async def process_order(order_id: str, db: AsyncSession):
98
+ order = await get_order(order_id, db)
99
+ if order:
100
+ if order.is_valid:
101
+ if order.items:
102
+ if order.user.is_active:
103
+ return await process_items(order.items)
104
+ return None
105
+
106
+ # โœ… Early return
107
+ async def process_order(order_id: str, db: AsyncSession) -> ProcessResult | None:
108
+ order = await get_order(order_id, db)
109
+ if not order:
110
+ return None
111
+ if not order.is_valid:
112
+ return None
113
+ if not order.items:
114
+ return None
115
+ if not order.user.is_active:
116
+ return None
117
+
118
+ return await process_items(order.items)
119
+ ```
120
+
121
+ ### 5. Repository ํŒจํ„ด (๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค ๋ถ„๋ฆฌ)
122
+
123
+ ```python
124
+ # โœ… Repository ๋ ˆ์ด์–ด
125
+ class UserRepository:
126
+ """๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค๋งŒ ๋‹ด๋‹น"""
127
+
128
+ def __init__(self, db: AsyncSession):
129
+ self.db = db
130
+
131
+ async def get_by_id(self, user_id: str) -> User | None:
132
+ result = await self.db.execute(
133
+ select(User).where(User.id == user_id)
134
+ )
135
+ return result.scalar_one_or_none()
136
+
137
+ async def create(self, user: User) -> User:
138
+ self.db.add(user)
139
+ await self.db.commit()
140
+ await self.db.refresh(user)
141
+ return user
142
+
143
+ async def get_by_email(self, email: str) -> User | None:
144
+ result = await self.db.execute(
145
+ select(User).where(User.email == email)
146
+ )
147
+ return result.scalar_one_or_none()
148
+
149
+ # โœ… Service ๋ ˆ์ด์–ด (๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง)
150
+ class UserService:
151
+ """๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋งŒ ๋‹ด๋‹น"""
152
+
153
+ def __init__(self, repository: UserRepository):
154
+ self.repository = repository
155
+
156
+ async def create_user(
157
+ self, request: CreateUserRequest
158
+ ) -> UserResponse:
159
+ # ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™: ์ด๋ฉ”์ผ ์ค‘๋ณต ์ฒดํฌ
160
+ existing = await self.repository.get_by_email(request.email)
161
+ if existing:
162
+ raise HTTPException(409, detail="Email already exists")
163
+
164
+ # ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™: ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ
165
+ hashed_password = hash_password(request.password)
166
+
167
+ # ์ƒ์„ฑ
168
+ user = User(
169
+ email=request.email,
170
+ username=request.username,
171
+ password_hash=hashed_password,
172
+ )
173
+ user = await self.repository.create(user)
174
+
175
+ return UserResponse.model_validate(user)
176
+ ```
177
+
178
+ ### 6. ์˜์กด์„ฑ ์ฃผ์ž… (FastAPI Depends)
179
+
180
+ ```python
181
+ # app/core/deps.py
182
+ from sqlalchemy.ext.asyncio import AsyncSession
183
+
184
+ async def get_db() -> AsyncSession:
185
+ """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ธ์…˜ ์˜์กด์„ฑ"""
186
+ async with async_session_maker() as session:
187
+ yield session
188
+
189
+ async def get_current_user(
190
+ token: str = Depends(oauth2_scheme),
191
+ db: AsyncSession = Depends(get_db)
192
+ ) -> User:
193
+ """ํ˜„์žฌ ์‚ฌ์šฉ์ž ์˜์กด์„ฑ"""
194
+ payload = decode_jwt(token)
195
+ user = await get_user_by_id(payload["sub"], db)
196
+ if not user:
197
+ raise HTTPException(401, detail="Invalid credentials")
198
+ return user
199
+
200
+ # app/api/v1/users.py
201
+ @router.get("/me", response_model=UserResponse)
202
+ async def get_current_user_profile(
203
+ current_user: User = Depends(get_current_user)
204
+ ):
205
+ """ํ˜„์žฌ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์กฐํšŒ"""
206
+ return UserResponse.model_validate(current_user)
207
+ ```
208
+
209
+ ### 7. ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ‘œ์ค€
210
+
211
+ ```python
212
+ from fastapi import HTTPException
213
+
214
+ # โœ… ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
215
+ async def get_user(user_id: str, db: AsyncSession) -> User:
216
+ user = await db.get(User, user_id)
217
+ if not user:
218
+ raise HTTPException(
219
+ status_code=404,
220
+ detail=f"User {user_id} not found"
221
+ )
222
+ return user
223
+
224
+ # โœ… ์ปค์Šคํ…€ ์˜ˆ์™ธ
225
+ class UserNotFoundError(Exception):
226
+ def __init__(self, user_id: str):
227
+ self.user_id = user_id
228
+ super().__init__(f"User {user_id} not found")
229
+
230
+ # ์ „์—ญ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ
231
+ @app.exception_handler(UserNotFoundError)
232
+ async def user_not_found_handler(request: Request, exc: UserNotFoundError):
233
+ return JSONResponse(
234
+ status_code=404,
235
+ content={"detail": str(exc)}
236
+ )
237
+ ```
238
+
239
+ ### 8. SQLAlchemy 2.0 ์Šคํƒ€์ผ
240
+
241
+ ```python
242
+ from sqlalchemy import select, func
243
+ from sqlalchemy.orm import selectinload
244
+
245
+ # โœ… 2.0 ์Šคํƒ€์ผ (async + select)
246
+ async def get_users_with_posts(db: AsyncSession) -> list[User]:
247
+ result = await db.execute(
248
+ select(User)
249
+ .options(selectinload(User.posts)) # Eager loading
250
+ .where(User.is_active == True)
251
+ .order_by(User.created_at.desc())
252
+ .limit(20)
253
+ )
254
+ return list(result.scalars().all())
255
+
256
+ # โŒ 1.x ์Šคํƒ€์ผ (๋ ˆ๊ฑฐ์‹œ)
257
+ def get_users():
258
+ return session.query(User).filter_by(is_active=True).all()
259
+ ```
260
+
261
+ ### 9. Python ๊ด€์šฉ๊ตฌ ํ™œ์šฉ
262
+
263
+ ```python
264
+ # โœ… List comprehension
265
+ active_users = [u for u in users if u.is_active]
266
+
267
+ # โœ… Dictionary comprehension
268
+ user_dict = {u.id: u.name for u in users}
269
+
270
+ # โœ… Generator expression (๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ)
271
+ total = sum(u.age for u in users)
272
+
273
+ # โœ… Context manager
274
+ async with db.begin():
275
+ user = User(...)
276
+ db.add(user)
277
+ # ์ž๋™ commit/rollback
278
+
279
+ # โœ… Dataclass (๊ฐ„๋‹จํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ)
280
+ from dataclasses import dataclass
281
+
282
+ @dataclass(frozen=True) # Immutable
283
+ class Point:
284
+ x: float
285
+ y: float
286
+ ```
287
+
288
+ ### 10. ๋กœ๊น… ํ‘œ์ค€
289
+
290
+ ```python
291
+ import structlog
292
+
293
+ logger = structlog.get_logger()
294
+
295
+ # โœ… ๊ตฌ์กฐํ™”๋œ ๋กœ๊น…
296
+ async def create_user(request: CreateUserRequest):
297
+ logger.info(
298
+ "user_creation_started",
299
+ email=request.email,
300
+ username=request.username
301
+ )
302
+
303
+ try:
304
+ user = await user_service.create(request)
305
+ logger.info(
306
+ "user_creation_succeeded",
307
+ user_id=user.id,
308
+ email=user.email
309
+ )
310
+ return user
311
+ except Exception as e:
312
+ logger.error(
313
+ "user_creation_failed",
314
+ email=request.email,
315
+ error=str(e),
316
+ exc_info=True
317
+ )
318
+ raise
319
+ ```
320
+
321
+ ## ์•ˆํ‹ฐํŒจํ„ด
322
+
323
+ ```python
324
+ # โŒ any ํƒ€์ž…
325
+ def process_data(data: any): # ํƒ€์ž… ์•ˆ์ „์„ฑ ์ƒ์‹ค
326
+ return data
327
+
328
+ # โŒ ๋ธ”๋กœํ‚น I/O in async ํ•จ์ˆ˜
329
+ async def bad_example():
330
+ data = requests.get("https://api.example.com") # ๋ธ”๋กœํ‚น!
331
+ return data
332
+
333
+ # โŒ ์˜ˆ์™ธ ๋ฌด์‹œ
334
+ try:
335
+ risky_operation()
336
+ except:
337
+ pass # ์œ„ํ—˜!
338
+
339
+ # โŒ Mutable default argument
340
+ def append_to_list(item, my_list=[]): # ๋ฒ„๊ทธ!
341
+ my_list.append(item)
342
+ return my_list
343
+
344
+ # โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•
345
+ def append_to_list(item, my_list: list | None = None):
346
+ if my_list is None:
347
+ my_list = []
348
+ my_list.append(item)
349
+ return my_list
350
+ ```
351
+
352
+ ## ์ฝ”๋“œ ํ’ˆ์งˆ ๋„๊ตฌ
353
+
354
+ ```bash
355
+ # ํฌ๋งทํŒ…
356
+ black .
357
+ isort .
358
+
359
+ # ๋ฆฐํŒ…
360
+ flake8 .
361
+ ruff check .
362
+
363
+ # ํƒ€์ž… ์ฒดํฌ
364
+ mypy app/ --strict
365
+
366
+ # ํ…Œ์ŠคํŠธ
367
+ pytest tests/ -v --cov=app
368
+
369
+ # ๋ณด์•ˆ ์ฒดํฌ
370
+ bandit -r app/
371
+ ```
372
+
373
+ ## ์ฒดํฌ๋ฆฌ์ŠคํŠธ
374
+
375
+ Python/FastAPI ์ฝ”๋“œ ์ž‘์„ฑ ์‹œ:
376
+
377
+ - [ ] ํƒ€์ž… ํžŒํŠธ 100% (ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜, ๋ณ€์ˆ˜)
378
+ - [ ] Pydantic ์Šคํ‚ค๋งˆ๋กœ Contract ์ •์˜
379
+ - [ ] async/await ์‚ฌ์šฉ (I/O ์ž‘์—…)
380
+ - [ ] Early return ํŒจํ„ด
381
+ - [ ] Repository + Service ๋ ˆ์ด์–ด ๋ถ„๋ฆฌ
382
+ - [ ] ์˜์กด์„ฑ ์ฃผ์ž… (Depends)
383
+ - [ ] ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
384
+ - [ ] ๊ตฌ์กฐํ™”๋œ ๋กœ๊น…
385
+ - [ ] ํ•จ์ˆ˜ โ‰ค 30์ค„ (SRP ์ค€์ˆ˜)
386
+ - [ ] ๋ณต์žก๋„ โ‰ค 10