start-vibing-stacks 1.7.4 โ 1.8.1
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.
- package/dist/detector.d.ts +1 -0
- package/dist/detector.js +18 -1
- package/dist/index.js +5 -5
- package/dist/ui.js +1 -1
- package/package.json +1 -1
- package/stacks/python/skills/async-patterns/SKILL.md +98 -0
- package/stacks/python/skills/django-patterns/SKILL.md +101 -0
- package/stacks/python/skills/fastapi-patterns/SKILL.md +129 -0
- package/stacks/python/skills/pydantic-validation/SKILL.md +103 -0
- package/stacks/python/skills/pytest-testing/SKILL.md +108 -0
- package/stacks/python/skills/python-patterns/SKILL.md +110 -0
- package/stacks/python/skills/python-performance/SKILL.md +109 -0
- package/stacks/python/stack.json +45 -0
package/dist/detector.d.ts
CHANGED
|
@@ -14,3 +14,4 @@ export declare function detectPhpFramework(projectDir: string): string | null;
|
|
|
14
14
|
* Detect framework within a Node.js project
|
|
15
15
|
*/
|
|
16
16
|
export declare function detectNodeFramework(projectDir: string): string | null;
|
|
17
|
+
export declare function detectPythonFramework(projectDir: string): string | null;
|
package/dist/detector.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Auto-detects project stack by scanning for known files.
|
|
5
5
|
* Also detects .cursorrules, CLAUDE.md, and git presence.
|
|
6
6
|
*/
|
|
7
|
-
import { existsSync, readdirSync } from 'fs';
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
const STACK_SIGNATURES = [
|
|
10
10
|
// PHP
|
|
@@ -101,3 +101,20 @@ export function detectNodeFramework(projectDir) {
|
|
|
101
101
|
return 'astro';
|
|
102
102
|
return null;
|
|
103
103
|
}
|
|
104
|
+
export function detectPythonFramework(projectDir) {
|
|
105
|
+
// Check manage.py (Django)
|
|
106
|
+
if (existsSync(join(projectDir, 'manage.py')))
|
|
107
|
+
return 'django';
|
|
108
|
+
// Check for FastAPI in requirements or pyproject
|
|
109
|
+
for (const reqFile of ['requirements.txt', 'pyproject.toml', 'Pipfile']) {
|
|
110
|
+
const filePath = join(projectDir, reqFile);
|
|
111
|
+
if (existsSync(filePath)) {
|
|
112
|
+
const content = readFileSync(filePath, 'utf8').toLowerCase();
|
|
113
|
+
if (content.includes('fastapi'))
|
|
114
|
+
return 'fastapi';
|
|
115
|
+
if (content.includes('flask'))
|
|
116
|
+
return 'flask';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { join, basename } from 'path';
|
|
|
10
10
|
import inquirer from 'inquirer';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import * as ui from './ui.js';
|
|
13
|
-
import { detectProject, detectPhpFramework, detectNodeFramework } from './detector.js';
|
|
13
|
+
import { detectProject, detectPhpFramework, detectNodeFramework, detectPythonFramework } from './detector.js';
|
|
14
14
|
import { autoInstall, installComposer, installClaudeCode } from './installer.js';
|
|
15
15
|
import { loadStackConfig, setupProject } from './setup.js';
|
|
16
16
|
import { selectMcpServers, installMcpServers } from './mcp.js';
|
|
@@ -56,9 +56,7 @@ if (FLAGS.help) {
|
|
|
56
56
|
const AVAILABLE_STACKS = [
|
|
57
57
|
{ id: 'php', name: 'PHP 8.3+', icon: '๐', available: true },
|
|
58
58
|
{ id: 'nodejs', name: 'Node.js / TypeScript', icon: '๐ฆ', available: true },
|
|
59
|
-
{ id: 'python', name: 'Python', icon: '๐', available:
|
|
60
|
-
{ id: 'rust', name: 'Rust', icon: '๐ฆ', available: false },
|
|
61
|
-
{ id: 'go', name: 'Go', icon: '๐น', available: false },
|
|
59
|
+
{ id: 'python', name: 'Python 3.12+', icon: '๐', available: true },
|
|
62
60
|
];
|
|
63
61
|
// =============================================================================
|
|
64
62
|
// Main Flow
|
|
@@ -158,7 +156,9 @@ async function main() {
|
|
|
158
156
|
? detectPhpFramework(projectDir)
|
|
159
157
|
: stackId === 'nodejs'
|
|
160
158
|
? detectNodeFramework(projectDir)
|
|
161
|
-
:
|
|
159
|
+
: stackId === 'python'
|
|
160
|
+
? detectPythonFramework(projectDir)
|
|
161
|
+
: null;
|
|
162
162
|
const { framework } = await inquirer.prompt([
|
|
163
163
|
{
|
|
164
164
|
type: 'list',
|
package/dist/ui.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Start Vibing Stacks โ Terminal UI
|
|
3
3
|
*/
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
const VERSION = '1.
|
|
5
|
+
const VERSION = '1.8.1';
|
|
6
6
|
const gradient = (text) => {
|
|
7
7
|
const colors = [chalk.hex('#FF6B6B'), chalk.hex('#FF8E53'), chalk.hex('#FFBD2E'), chalk.hex('#48BB78'), chalk.hex('#4299E1'), chalk.hex('#9F7AEA')];
|
|
8
8
|
return text.split('').map((c, i) => colors[i % colors.length](c)).join('');
|
package/package.json
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Async Python Patterns โ Concurrency & Performance
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when writing async/await code or concurrent operations.**
|
|
4
|
+
|
|
5
|
+
## Core: asyncio.gather vs TaskGroup
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import asyncio
|
|
9
|
+
|
|
10
|
+
# Gather โ run multiple coroutines concurrently
|
|
11
|
+
async def fetch_all():
|
|
12
|
+
results = await asyncio.gather(
|
|
13
|
+
fetch_users(),
|
|
14
|
+
fetch_products(),
|
|
15
|
+
fetch_orders(),
|
|
16
|
+
)
|
|
17
|
+
users, products, orders = results
|
|
18
|
+
|
|
19
|
+
# TaskGroup (Python 3.11+) โ structured concurrency with proper cancellation
|
|
20
|
+
async def fetch_all_safe():
|
|
21
|
+
async with asyncio.TaskGroup() as tg:
|
|
22
|
+
t1 = tg.create_task(fetch_users())
|
|
23
|
+
t2 = tg.create_task(fetch_products())
|
|
24
|
+
return t1.result(), t2.result()
|
|
25
|
+
# If any task fails, ALL are cancelled
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## HTTP Client (httpx)
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import httpx
|
|
32
|
+
|
|
33
|
+
# REUSE client (connection pooling)
|
|
34
|
+
async def fetch_data():
|
|
35
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
36
|
+
response = await client.get("https://api.example.com/data")
|
|
37
|
+
response.raise_for_status()
|
|
38
|
+
return response.json()
|
|
39
|
+
|
|
40
|
+
# Concurrent requests
|
|
41
|
+
async def fetch_many(urls: list[str]) -> list[dict]:
|
|
42
|
+
async with httpx.AsyncClient() as client:
|
|
43
|
+
tasks = [client.get(url) for url in urls]
|
|
44
|
+
responses = await asyncio.gather(*tasks)
|
|
45
|
+
return [r.json() for r in responses]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Semaphore (rate limiting)
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# Limit concurrent operations
|
|
52
|
+
sem = asyncio.Semaphore(10) # Max 10 concurrent
|
|
53
|
+
|
|
54
|
+
async def fetch_with_limit(url: str):
|
|
55
|
+
async with sem:
|
|
56
|
+
async with httpx.AsyncClient() as client:
|
|
57
|
+
return await client.get(url)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Queue Pattern (producer/consumer)
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
async def producer(queue: asyncio.Queue):
|
|
64
|
+
for item in data:
|
|
65
|
+
await queue.put(item)
|
|
66
|
+
await queue.put(None) # Sentinel
|
|
67
|
+
|
|
68
|
+
async def consumer(queue: asyncio.Queue):
|
|
69
|
+
while True:
|
|
70
|
+
item = await queue.get()
|
|
71
|
+
if item is None:
|
|
72
|
+
break
|
|
73
|
+
await process(item)
|
|
74
|
+
queue.task_done()
|
|
75
|
+
|
|
76
|
+
async def main():
|
|
77
|
+
queue = asyncio.Queue(maxsize=100) # Backpressure
|
|
78
|
+
await asyncio.gather(producer(queue), consumer(queue))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Timeouts
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# Always add timeouts to external calls
|
|
85
|
+
try:
|
|
86
|
+
result = await asyncio.wait_for(fetch_data(), timeout=5.0)
|
|
87
|
+
except asyncio.TimeoutError:
|
|
88
|
+
logger.warning("External API timed out")
|
|
89
|
+
result = default_value
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## FORBIDDEN
|
|
93
|
+
|
|
94
|
+
1. **`time.sleep()` in async code** โ use `await asyncio.sleep()`
|
|
95
|
+
2. **Sync HTTP in async context** โ use `httpx.AsyncClient`, not `requests`
|
|
96
|
+
3. **No timeout on external calls** โ always `asyncio.wait_for()` or `httpx.Timeout`
|
|
97
|
+
4. **Creating new client per request** โ reuse with `async with`
|
|
98
|
+
5. **Bare `asyncio.gather` without error handling** โ use `return_exceptions=True` or TaskGroup
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Django Patterns โ Full-Stack Python (Django 5+)
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when writing Django models, views, or serializers.**
|
|
4
|
+
|
|
5
|
+
## Model Design (Fat Models, Thin Views)
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from django.db import models
|
|
9
|
+
from django.utils import timezone
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
class TimeStampedModel(models.Model):
|
|
13
|
+
"""Abstract base โ reuse in all models."""
|
|
14
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
15
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
16
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
17
|
+
|
|
18
|
+
class Meta:
|
|
19
|
+
abstract = True
|
|
20
|
+
|
|
21
|
+
class User(TimeStampedModel):
|
|
22
|
+
email = models.EmailField(unique=True, db_index=True)
|
|
23
|
+
name = models.CharField(max_length=255)
|
|
24
|
+
is_active = models.BooleanField(default=True)
|
|
25
|
+
|
|
26
|
+
# Fat model: business logic HERE
|
|
27
|
+
def deactivate(self):
|
|
28
|
+
self.is_active = False
|
|
29
|
+
self.save(update_fields=['is_active', 'updated_at'])
|
|
30
|
+
|
|
31
|
+
# Custom manager
|
|
32
|
+
objects = UserManager()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## QuerySet Optimization
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
# ALWAYS use select_related for ForeignKey
|
|
39
|
+
users = User.objects.select_related('profile').filter(is_active=True)
|
|
40
|
+
|
|
41
|
+
# ALWAYS use prefetch_related for ManyToMany
|
|
42
|
+
posts = Post.objects.prefetch_related('tags', 'comments').all()
|
|
43
|
+
|
|
44
|
+
# Use .only() for specific fields
|
|
45
|
+
User.objects.only('id', 'email').filter(is_active=True)
|
|
46
|
+
|
|
47
|
+
# Avoid N+1 โ NEVER loop queries
|
|
48
|
+
# WRONG:
|
|
49
|
+
for post in Post.objects.all():
|
|
50
|
+
print(post.author.name) # N+1!
|
|
51
|
+
|
|
52
|
+
# CORRECT:
|
|
53
|
+
for post in Post.objects.select_related('author'):
|
|
54
|
+
print(post.author.name) # 1 query
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Django REST Framework
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# serializers.py
|
|
61
|
+
class UserSerializer(serializers.ModelSerializer):
|
|
62
|
+
class Meta:
|
|
63
|
+
model = User
|
|
64
|
+
fields = ['id', 'email', 'name', 'created_at']
|
|
65
|
+
read_only_fields = ['id', 'created_at']
|
|
66
|
+
|
|
67
|
+
# views.py
|
|
68
|
+
class UserViewSet(viewsets.ModelViewSet):
|
|
69
|
+
queryset = User.objects.filter(is_active=True)
|
|
70
|
+
serializer_class = UserSerializer
|
|
71
|
+
permission_classes = [IsAuthenticated]
|
|
72
|
+
pagination_class = PageNumberPagination
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Async Views (Django 5.0+)
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# Use async when calling external APIs or heavy I/O
|
|
79
|
+
async def fetch_external_data(request):
|
|
80
|
+
async with httpx.AsyncClient() as client:
|
|
81
|
+
response = await client.get('https://api.example.com/data')
|
|
82
|
+
return JsonResponse(response.json())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Migrations Best Practices
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python manage.py makemigrations --name descriptive_name
|
|
89
|
+
python manage.py migrate
|
|
90
|
+
|
|
91
|
+
# Check for missing migrations in CI
|
|
92
|
+
python manage.py makemigrations --check --dry-run
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## FORBIDDEN
|
|
96
|
+
|
|
97
|
+
1. **Logic in views** โ fat models, thin views
|
|
98
|
+
2. **N+1 queries** โ always `select_related`/`prefetch_related`
|
|
99
|
+
3. **`objects.all()` without limit** โ paginate or `.only()`
|
|
100
|
+
4. **Raw SQL without parameterization** โ use ORM or `params=[]`
|
|
101
|
+
5. **Skipping migrations** โ always `makemigrations` after model changes
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# FastAPI Patterns โ High-Performance Async APIs
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when writing FastAPI routes, dependencies, or middleware.**
|
|
4
|
+
|
|
5
|
+
## Route Pattern
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
9
|
+
from app.schemas.user import UserCreate, UserResponse
|
|
10
|
+
from app.services.user import UserService
|
|
11
|
+
from app.api.deps import get_current_user, get_db
|
|
12
|
+
|
|
13
|
+
router = APIRouter(prefix="/users", tags=["users"])
|
|
14
|
+
|
|
15
|
+
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
|
16
|
+
async def create_user(data: UserCreate, db=Depends(get_db)):
|
|
17
|
+
service = UserService(db)
|
|
18
|
+
return await service.create(data)
|
|
19
|
+
|
|
20
|
+
@router.get("/me", response_model=UserResponse)
|
|
21
|
+
async def get_me(user=Depends(get_current_user)):
|
|
22
|
+
return user
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Dependency Injection
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# app/api/deps.py
|
|
29
|
+
from fastapi import Depends, HTTPException, status
|
|
30
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
31
|
+
|
|
32
|
+
security = HTTPBearer()
|
|
33
|
+
|
|
34
|
+
async def get_db():
|
|
35
|
+
async with async_session() as session:
|
|
36
|
+
yield session # Auto-cleanup
|
|
37
|
+
|
|
38
|
+
async def get_current_user(
|
|
39
|
+
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
40
|
+
db=Depends(get_db),
|
|
41
|
+
) -> User:
|
|
42
|
+
user = await verify_token(credentials.credentials, db)
|
|
43
|
+
if not user:
|
|
44
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
|
45
|
+
return user
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Pydantic Settings (env config)
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from pydantic_settings import BaseSettings
|
|
52
|
+
|
|
53
|
+
class Settings(BaseSettings):
|
|
54
|
+
DATABASE_URL: str
|
|
55
|
+
SECRET_KEY: str
|
|
56
|
+
DEBUG: bool = False
|
|
57
|
+
ALLOWED_ORIGINS: list[str] = ["http://localhost:3000"]
|
|
58
|
+
|
|
59
|
+
class Config:
|
|
60
|
+
env_file = ".env"
|
|
61
|
+
|
|
62
|
+
settings = Settings()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Middleware
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
69
|
+
|
|
70
|
+
app.add_middleware(
|
|
71
|
+
CORSMiddleware,
|
|
72
|
+
allow_origins=settings.ALLOWED_ORIGINS,
|
|
73
|
+
allow_credentials=True,
|
|
74
|
+
allow_methods=["*"],
|
|
75
|
+
allow_headers=["*"],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Custom middleware
|
|
79
|
+
@app.middleware("http")
|
|
80
|
+
async def add_request_id(request, call_next):
|
|
81
|
+
request_id = str(uuid4())
|
|
82
|
+
request.state.request_id = request_id
|
|
83
|
+
response = await call_next(request)
|
|
84
|
+
response.headers["X-Request-ID"] = request_id
|
|
85
|
+
return response
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## SQLAlchemy 2.0 Async
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
|
92
|
+
|
|
93
|
+
engine = create_async_engine(settings.DATABASE_URL, pool_size=20, max_overflow=10)
|
|
94
|
+
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
|
95
|
+
|
|
96
|
+
# Repository pattern
|
|
97
|
+
class UserRepository:
|
|
98
|
+
def __init__(self, session):
|
|
99
|
+
self.session = session
|
|
100
|
+
|
|
101
|
+
async def get_by_id(self, id: UUID) -> User | None:
|
|
102
|
+
result = await self.session.execute(
|
|
103
|
+
select(User).where(User.id == id)
|
|
104
|
+
)
|
|
105
|
+
return result.scalar_one_or_none()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Production Deployment
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# uvicorn with gunicorn (production)
|
|
112
|
+
gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
|
|
113
|
+
|
|
114
|
+
# Docker
|
|
115
|
+
FROM python:3.12-slim
|
|
116
|
+
WORKDIR /app
|
|
117
|
+
COPY requirements.txt .
|
|
118
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
119
|
+
COPY . .
|
|
120
|
+
CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## FORBIDDEN
|
|
124
|
+
|
|
125
|
+
1. **`def` routes with async DB calls** โ use `async def`
|
|
126
|
+
2. **Business logic in routes** โ use services layer
|
|
127
|
+
3. **Hardcoded secrets** โ use Pydantic Settings + `.env`
|
|
128
|
+
4. **No response_model** โ always specify for auto-docs
|
|
129
|
+
5. **Catching bare `Exception`** โ catch specific exceptions
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Pydantic Validation โ Runtime Type Safety for Python
|
|
2
|
+
|
|
3
|
+
**ALWAYS use Pydantic for API schemas, config, and data boundaries.**
|
|
4
|
+
|
|
5
|
+
## Multi-Model Pattern
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from pydantic import BaseModel, Field, EmailStr
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
# Base โ shared fields
|
|
13
|
+
class UserBase(BaseModel):
|
|
14
|
+
name: str = Field(..., min_length=2, max_length=100)
|
|
15
|
+
email: EmailStr
|
|
16
|
+
|
|
17
|
+
# Create โ request body (required fields)
|
|
18
|
+
class UserCreate(UserBase):
|
|
19
|
+
password: str = Field(..., min_length=8)
|
|
20
|
+
|
|
21
|
+
# Update โ PATCH (all optional)
|
|
22
|
+
class UserUpdate(BaseModel):
|
|
23
|
+
name: str | None = Field(None, min_length=2)
|
|
24
|
+
email: EmailStr | None = None
|
|
25
|
+
|
|
26
|
+
# Response โ API output
|
|
27
|
+
class UserResponse(UserBase):
|
|
28
|
+
id: UUID
|
|
29
|
+
created_at: datetime
|
|
30
|
+
is_active: bool
|
|
31
|
+
|
|
32
|
+
model_config = {"from_attributes": True} # Works with ORM objects
|
|
33
|
+
|
|
34
|
+
# InDB โ database document (Cosmos, Mongo)
|
|
35
|
+
class UserInDB(UserResponse):
|
|
36
|
+
doc_type: str = "user"
|
|
37
|
+
hashed_password: str
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Validators
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from pydantic import field_validator, model_validator
|
|
44
|
+
|
|
45
|
+
class OrderCreate(BaseModel):
|
|
46
|
+
quantity: int
|
|
47
|
+
price: float
|
|
48
|
+
discount: float = 0.0
|
|
49
|
+
|
|
50
|
+
@field_validator('quantity')
|
|
51
|
+
@classmethod
|
|
52
|
+
def quantity_positive(cls, v):
|
|
53
|
+
if v <= 0:
|
|
54
|
+
raise ValueError('Quantity must be positive')
|
|
55
|
+
return v
|
|
56
|
+
|
|
57
|
+
@model_validator(mode='after')
|
|
58
|
+
def discount_not_exceed_price(self):
|
|
59
|
+
if self.discount > self.price * self.quantity:
|
|
60
|
+
raise ValueError('Discount exceeds total')
|
|
61
|
+
return self
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Settings (env config)
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from pydantic_settings import BaseSettings
|
|
68
|
+
|
|
69
|
+
class Settings(BaseSettings):
|
|
70
|
+
DATABASE_URL: str
|
|
71
|
+
SECRET_KEY: str
|
|
72
|
+
DEBUG: bool = False
|
|
73
|
+
REDIS_URL: str = "redis://localhost:6379"
|
|
74
|
+
|
|
75
|
+
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
|
|
76
|
+
|
|
77
|
+
settings = Settings()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## camelCase API (Python snake_case โ JSON camelCase)
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from pydantic import ConfigDict
|
|
84
|
+
|
|
85
|
+
class ApiModel(BaseModel):
|
|
86
|
+
model_config = ConfigDict(
|
|
87
|
+
populate_by_name=True,
|
|
88
|
+
alias_generator=lambda s: ''.join(
|
|
89
|
+
w.capitalize() if i else w for i, w in enumerate(s.split('_'))
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
class UserResponse(ApiModel):
|
|
94
|
+
first_name: str # JSON: "firstName"
|
|
95
|
+
created_at: datetime # JSON: "createdAt"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## FORBIDDEN
|
|
99
|
+
|
|
100
|
+
1. **Raw dicts for API I/O** โ always Pydantic models
|
|
101
|
+
2. **Skipping validation** โ `model_validate()` not `dict()`
|
|
102
|
+
3. **Single model for everything** โ use Base/Create/Update/Response pattern
|
|
103
|
+
4. **No `from_attributes`** โ needed for ORM integration
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Pytest Testing โ Python Testing Patterns
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke AFTER implementing any feature.**
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
tests/
|
|
9
|
+
โโโ conftest.py # Shared fixtures
|
|
10
|
+
โโโ unit/
|
|
11
|
+
โ โโโ test_services.py
|
|
12
|
+
โ โโโ test_models.py
|
|
13
|
+
โโโ integration/
|
|
14
|
+
โ โโโ test_api.py
|
|
15
|
+
โ โโโ test_db.py
|
|
16
|
+
โโโ e2e/
|
|
17
|
+
โโโ test_flows.py
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Fixtures
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import pytest
|
|
24
|
+
from httpx import AsyncClient, ASGITransport
|
|
25
|
+
from app.main import app
|
|
26
|
+
from app.core.config import settings
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
async def client():
|
|
30
|
+
transport = ASGITransport(app=app)
|
|
31
|
+
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
|
32
|
+
yield ac
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
async def db_session():
|
|
36
|
+
async with async_session() as session:
|
|
37
|
+
yield session
|
|
38
|
+
await session.rollback() # Cleanup
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def sample_user():
|
|
42
|
+
return {"name": "Test User", "email": f"test_{uuid4().hex[:8]}@test.com", "password": "Pass1234!"}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Async Tests
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import pytest
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_create_user(client, sample_user):
|
|
52
|
+
response = await client.post("/api/v1/users", json=sample_user)
|
|
53
|
+
assert response.status_code == 201
|
|
54
|
+
data = response.json()
|
|
55
|
+
assert data["email"] == sample_user["email"]
|
|
56
|
+
assert "id" in data
|
|
57
|
+
assert "password" not in data # Never leak passwords
|
|
58
|
+
|
|
59
|
+
@pytest.mark.asyncio
|
|
60
|
+
async def test_get_me_unauthorized(client):
|
|
61
|
+
response = await client.get("/api/v1/users/me")
|
|
62
|
+
assert response.status_code == 401
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Parameterized Tests
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
@pytest.mark.parametrize("email,expected", [
|
|
69
|
+
("valid@test.com", 201),
|
|
70
|
+
("invalid-email", 422),
|
|
71
|
+
("", 422),
|
|
72
|
+
])
|
|
73
|
+
async def test_email_validation(client, email, expected):
|
|
74
|
+
response = await client.post("/api/v1/users", json={"name": "Test", "email": email, "password": "Pass1234!"})
|
|
75
|
+
assert response.status_code == expected
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Mocking
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from unittest.mock import AsyncMock, patch
|
|
82
|
+
|
|
83
|
+
@pytest.mark.asyncio
|
|
84
|
+
async def test_external_api_failure(client):
|
|
85
|
+
with patch("app.services.external.fetch_data", new_callable=AsyncMock) as mock:
|
|
86
|
+
mock.side_effect = ConnectionError("API down")
|
|
87
|
+
response = await client.get("/api/v1/data")
|
|
88
|
+
assert response.status_code == 503
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Commands
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pytest # Run all
|
|
95
|
+
pytest -x # Stop on first failure
|
|
96
|
+
pytest --tb=short # Short traceback
|
|
97
|
+
pytest -k "test_create" # Filter by name
|
|
98
|
+
pytest --cov=app --cov-report=html # Coverage
|
|
99
|
+
pytest -n auto # Parallel (pytest-xdist)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## FORBIDDEN
|
|
103
|
+
|
|
104
|
+
1. **No fixtures for cleanup** โ always rollback/cleanup
|
|
105
|
+
2. **Hardcoded test data** โ use factories/uuid
|
|
106
|
+
3. **Testing implementation** โ test behavior, not internals
|
|
107
|
+
4. **Skipping async tests** โ use `pytest-asyncio`
|
|
108
|
+
5. **No coverage in CI** โ `--cov --cov-fail-under=70`
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Python Patterns โ Architecture & Decision-Making
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when making Python architecture decisions.**
|
|
4
|
+
|
|
5
|
+
## Framework Selection
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
What are you building?
|
|
9
|
+
โโโ API / Microservices โ FastAPI (async, Pydantic, fast)
|
|
10
|
+
โโโ Full-stack / CMS / Admin โ Django (batteries-included)
|
|
11
|
+
โโโ Simple / Script โ Flask (minimal)
|
|
12
|
+
โโโ AI/ML API serving โ FastAPI (Pydantic, uvicorn)
|
|
13
|
+
โโโ Background workers โ Celery + any framework
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Async vs Sync
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
I/O-bound (waiting for DB, HTTP, files) โ async def
|
|
20
|
+
CPU-bound (computing, parsing) โ def + multiprocessing
|
|
21
|
+
|
|
22
|
+
Don't:
|
|
23
|
+
โโโ Mix sync and async carelessly
|
|
24
|
+
โโโ Use sync libraries in async code (blocks event loop!)
|
|
25
|
+
โโโ Force async for CPU work
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Async Library Selection
|
|
29
|
+
|
|
30
|
+
| Need | Library |
|
|
31
|
+
|------|---------|
|
|
32
|
+
| HTTP client | `httpx` |
|
|
33
|
+
| PostgreSQL | `asyncpg` |
|
|
34
|
+
| Redis | `redis[async]` |
|
|
35
|
+
| File I/O | `aiofiles` |
|
|
36
|
+
| ORM | SQLAlchemy 2.0+ async, Tortoise |
|
|
37
|
+
|
|
38
|
+
## Type Hints (MANDATORY for public APIs)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from typing import Optional
|
|
42
|
+
|
|
43
|
+
def find_user(id: int) -> Optional[User]: ...
|
|
44
|
+
def process(data: str | dict) -> None: ...
|
|
45
|
+
def get_items() -> list[Item]: ...
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Project Structure
|
|
49
|
+
|
|
50
|
+
### FastAPI
|
|
51
|
+
```
|
|
52
|
+
app/
|
|
53
|
+
โโโ main.py # FastAPI app + startup
|
|
54
|
+
โโโ api/v1/
|
|
55
|
+
โ โโโ routes/ # Endpoints
|
|
56
|
+
โ โโโ deps.py # Dependencies (auth, db)
|
|
57
|
+
โโโ models/ # SQLAlchemy / Beanie models
|
|
58
|
+
โโโ schemas/ # Pydantic schemas
|
|
59
|
+
โโโ services/ # Business logic
|
|
60
|
+
โโโ core/
|
|
61
|
+
โ โโโ config.py # Pydantic Settings
|
|
62
|
+
โ โโโ security.py # Auth helpers
|
|
63
|
+
โโโ tests/
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Django
|
|
67
|
+
```
|
|
68
|
+
myproject/
|
|
69
|
+
โโโ manage.py
|
|
70
|
+
โโโ config/ # Settings, URLs, ASGI/WSGI
|
|
71
|
+
โโโ apps/
|
|
72
|
+
โ โโโ users/ # Per-app: models, views, serializers, tests
|
|
73
|
+
โ โโโ products/
|
|
74
|
+
โโโ tests/
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Error Handling
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# Custom exceptions in services
|
|
81
|
+
class NotFoundError(Exception):
|
|
82
|
+
def __init__(self, resource: str, id: str):
|
|
83
|
+
self.resource = resource
|
|
84
|
+
self.id = id
|
|
85
|
+
|
|
86
|
+
# FastAPI exception handler
|
|
87
|
+
@app.exception_handler(NotFoundError)
|
|
88
|
+
async def not_found_handler(request, exc):
|
|
89
|
+
return JSONResponse(status_code=404, content={
|
|
90
|
+
"error": "not_found",
|
|
91
|
+
"message": f"{exc.resource} {exc.id} not found"
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Background Tasks
|
|
96
|
+
|
|
97
|
+
| Solution | Best For |
|
|
98
|
+
|----------|----------|
|
|
99
|
+
| `BackgroundTasks` | Simple, in-process, fire-and-forget |
|
|
100
|
+
| `Celery` | Distributed, retries, complex workflows |
|
|
101
|
+
| `ARQ` | Async, Redis-based, lightweight |
|
|
102
|
+
| `Dramatiq` | Actor-based, simpler than Celery |
|
|
103
|
+
|
|
104
|
+
## FORBIDDEN
|
|
105
|
+
|
|
106
|
+
1. **Business logic in routes/views** โ use services layer
|
|
107
|
+
2. **Sync libraries in async code** โ blocks event loop
|
|
108
|
+
3. **No type hints on public APIs** โ always type
|
|
109
|
+
4. **Raw SQL without parameterization** โ injection risk
|
|
110
|
+
5. **`import *`** โ explicit imports only
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Python Performance โ Profiling & Optimization
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when optimizing slow Python code.**
|
|
4
|
+
|
|
5
|
+
## Profiling Tools
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# CPU profiling
|
|
9
|
+
python -m cProfile -s cumulative app.py
|
|
10
|
+
|
|
11
|
+
# Line profiling (pip install line-profiler)
|
|
12
|
+
kernprof -l -v script.py
|
|
13
|
+
|
|
14
|
+
# Memory profiling (pip install memory-profiler)
|
|
15
|
+
python -m memory_profiler script.py
|
|
16
|
+
|
|
17
|
+
# py-spy (production-safe, no code changes)
|
|
18
|
+
py-spy top --pid 12345
|
|
19
|
+
py-spy record -o profile.svg --pid 12345
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Common Optimizations
|
|
23
|
+
|
|
24
|
+
### Data Structures
|
|
25
|
+
```python
|
|
26
|
+
# Use set for membership testing (O(1) vs O(n))
|
|
27
|
+
# SLOW:
|
|
28
|
+
if item in large_list: ...
|
|
29
|
+
# FAST:
|
|
30
|
+
large_set = set(large_list)
|
|
31
|
+
if item in large_set: ...
|
|
32
|
+
|
|
33
|
+
# Use dict.get() instead of try/except
|
|
34
|
+
value = data.get('key', default)
|
|
35
|
+
|
|
36
|
+
# Use collections for specialized needs
|
|
37
|
+
from collections import defaultdict, Counter, deque
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Generators (memory)
|
|
41
|
+
```python
|
|
42
|
+
# WRONG โ loads everything in memory
|
|
43
|
+
def get_all():
|
|
44
|
+
return [process(x) for x in huge_dataset]
|
|
45
|
+
|
|
46
|
+
# CORRECT โ lazy evaluation
|
|
47
|
+
def get_all():
|
|
48
|
+
for x in huge_dataset:
|
|
49
|
+
yield process(x)
|
|
50
|
+
|
|
51
|
+
# Or generator expression
|
|
52
|
+
total = sum(x.price for x in orders) # Not [x.price for x in orders]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### String Operations
|
|
56
|
+
```python
|
|
57
|
+
# SLOW โ string concatenation in loop
|
|
58
|
+
result = ""
|
|
59
|
+
for s in strings:
|
|
60
|
+
result += s # O(nยฒ)
|
|
61
|
+
|
|
62
|
+
# FAST โ join
|
|
63
|
+
result = "".join(strings) # O(n)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Database (SQLAlchemy / Django ORM)
|
|
67
|
+
```python
|
|
68
|
+
# Bulk operations (not one-by-one)
|
|
69
|
+
# SLOW:
|
|
70
|
+
for item in items:
|
|
71
|
+
db.add(Item(**item))
|
|
72
|
+
|
|
73
|
+
# FAST:
|
|
74
|
+
db.add_all([Item(**item) for item in items])
|
|
75
|
+
await db.commit()
|
|
76
|
+
|
|
77
|
+
# Django bulk
|
|
78
|
+
Item.objects.bulk_create([Item(**item) for item in items], batch_size=1000)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Caching
|
|
82
|
+
```python
|
|
83
|
+
from functools import lru_cache
|
|
84
|
+
|
|
85
|
+
@lru_cache(maxsize=256)
|
|
86
|
+
def expensive_computation(n: int) -> int:
|
|
87
|
+
return sum(range(n))
|
|
88
|
+
|
|
89
|
+
# Async caching with Redis
|
|
90
|
+
import redis.asyncio as redis
|
|
91
|
+
|
|
92
|
+
cache = redis.from_url("redis://localhost")
|
|
93
|
+
|
|
94
|
+
async def get_user(id: str) -> User:
|
|
95
|
+
cached = await cache.get(f"user:{id}")
|
|
96
|
+
if cached:
|
|
97
|
+
return User.model_validate_json(cached)
|
|
98
|
+
user = await db.get(User, id)
|
|
99
|
+
await cache.setex(f"user:{id}", 300, user.model_dump_json())
|
|
100
|
+
return user
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## FORBIDDEN
|
|
104
|
+
|
|
105
|
+
1. **Premature optimization** โ profile FIRST, optimize SECOND
|
|
106
|
+
2. **`+` for string concatenation in loops** โ use `"".join()`
|
|
107
|
+
3. **`list` for membership testing** โ use `set`
|
|
108
|
+
4. **Loading entire dataset in memory** โ use generators/pagination
|
|
109
|
+
5. **Individual DB inserts in loop** โ use bulk operations
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "python",
|
|
3
|
+
"name": "Python 3.12+",
|
|
4
|
+
"icon": "๐",
|
|
5
|
+
"runtime": "python",
|
|
6
|
+
"requirements": [
|
|
7
|
+
{ "command": "python3", "minVersion": "3.12.0", "install": "brew install python@3.12" },
|
|
8
|
+
{ "command": "pip3", "minVersion": "23.0", "install": "python3 -m ensurepip --upgrade" },
|
|
9
|
+
{ "command": "uv", "minVersion": "0.1.0", "install": "curl -LsSf https://astral.sh/uv/install.sh | sh", "optional": true }
|
|
10
|
+
],
|
|
11
|
+
"frameworks": [
|
|
12
|
+
{ "id": "fastapi", "name": "FastAPI + Uvicorn", "icon": "โก" },
|
|
13
|
+
{ "id": "django", "name": "Django 5+", "icon": "๐ธ" },
|
|
14
|
+
{ "id": "flask", "name": "Flask", "icon": "๐งช" },
|
|
15
|
+
{ "id": "vanilla", "name": "Vanilla Python", "icon": "๐" }
|
|
16
|
+
],
|
|
17
|
+
"databases": [
|
|
18
|
+
{ "id": "postgresql", "name": "PostgreSQL", "icon": "๐" },
|
|
19
|
+
{ "id": "mysql", "name": "MySQL / MariaDB", "icon": "๐ฌ" },
|
|
20
|
+
{ "id": "mongodb", "name": "MongoDB", "icon": "๐" },
|
|
21
|
+
{ "id": "sqlite", "name": "SQLite", "icon": "๐ฆ" }
|
|
22
|
+
],
|
|
23
|
+
"frontendOptions": [
|
|
24
|
+
{ "id": "react", "name": "React (SPA)", "icon": "โ๏ธ" },
|
|
25
|
+
{ "id": "htmx", "name": "HTMX + Jinja2", "icon": "๐" },
|
|
26
|
+
{ "id": "none", "name": "API only (no frontend)", "icon": "๐" }
|
|
27
|
+
],
|
|
28
|
+
"deployTargets": [
|
|
29
|
+
{ "id": "github", "name": "GitHub (git push)", "icon": "๐" }
|
|
30
|
+
],
|
|
31
|
+
"skills": [
|
|
32
|
+
"python-patterns",
|
|
33
|
+
"fastapi-patterns",
|
|
34
|
+
"django-patterns",
|
|
35
|
+
"pydantic-validation",
|
|
36
|
+
"pytest-testing",
|
|
37
|
+
"async-patterns",
|
|
38
|
+
"python-performance"
|
|
39
|
+
],
|
|
40
|
+
"qualityGates": [
|
|
41
|
+
{ "name": "mypy", "command": "mypy .", "description": "Type checking" },
|
|
42
|
+
{ "name": "ruff", "command": "ruff check .", "description": "Linting" },
|
|
43
|
+
{ "name": "pytest", "command": "pytest --tb=short", "description": "Tests" }
|
|
44
|
+
]
|
|
45
|
+
}
|