agentic-loop 1.0.0
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/.claude/commands/explain.md +114 -0
- package/.claude/commands/idea.md +398 -0
- package/.claude/commands/my-dna.md +122 -0
- package/.claude/commands/prd.md +286 -0
- package/.claude/commands/review.md +167 -0
- package/.claude/commands/sign.md +32 -0
- package/.claude/commands/styleguide.md +450 -0
- package/.claude/commands/tour.md +301 -0
- package/.claude/commands/vibe-check.md +116 -0
- package/.claude/commands/vibe-help.md +47 -0
- package/.claude/commands/vibe-list.md +203 -0
- package/.pre-commit-hooks.yaml +102 -0
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/bin/agentic-loop.sh +24 -0
- package/bin/postinstall.sh +29 -0
- package/bin/ralph.sh +171 -0
- package/bin/vibe-check.js +19 -0
- package/dist/checks/check-any-types.d.ts +6 -0
- package/dist/checks/check-any-types.d.ts.map +1 -0
- package/dist/checks/check-any-types.js +73 -0
- package/dist/checks/check-any-types.js.map +1 -0
- package/dist/checks/check-commented-code.d.ts +6 -0
- package/dist/checks/check-commented-code.d.ts.map +1 -0
- package/dist/checks/check-commented-code.js +81 -0
- package/dist/checks/check-commented-code.js.map +1 -0
- package/dist/checks/check-console-error.d.ts +6 -0
- package/dist/checks/check-console-error.d.ts.map +1 -0
- package/dist/checks/check-console-error.js +41 -0
- package/dist/checks/check-console-error.js.map +1 -0
- package/dist/checks/check-debug-statements.d.ts +6 -0
- package/dist/checks/check-debug-statements.d.ts.map +1 -0
- package/dist/checks/check-debug-statements.js +120 -0
- package/dist/checks/check-debug-statements.js.map +1 -0
- package/dist/checks/check-deep-nesting.d.ts +6 -0
- package/dist/checks/check-deep-nesting.d.ts.map +1 -0
- package/dist/checks/check-deep-nesting.js +116 -0
- package/dist/checks/check-deep-nesting.js.map +1 -0
- package/dist/checks/check-docker-platform.d.ts +6 -0
- package/dist/checks/check-docker-platform.d.ts.map +1 -0
- package/dist/checks/check-docker-platform.js +42 -0
- package/dist/checks/check-docker-platform.js.map +1 -0
- package/dist/checks/check-dry-violations.d.ts +6 -0
- package/dist/checks/check-dry-violations.d.ts.map +1 -0
- package/dist/checks/check-dry-violations.js +124 -0
- package/dist/checks/check-dry-violations.js.map +1 -0
- package/dist/checks/check-empty-catch.d.ts +6 -0
- package/dist/checks/check-empty-catch.d.ts.map +1 -0
- package/dist/checks/check-empty-catch.js +111 -0
- package/dist/checks/check-empty-catch.js.map +1 -0
- package/dist/checks/check-function-length.d.ts +6 -0
- package/dist/checks/check-function-length.d.ts.map +1 -0
- package/dist/checks/check-function-length.js +152 -0
- package/dist/checks/check-function-length.js.map +1 -0
- package/dist/checks/check-hardcoded-ai-models.d.ts +10 -0
- package/dist/checks/check-hardcoded-ai-models.d.ts.map +1 -0
- package/dist/checks/check-hardcoded-ai-models.js +102 -0
- package/dist/checks/check-hardcoded-ai-models.js.map +1 -0
- package/dist/checks/check-hardcoded-urls.d.ts +6 -0
- package/dist/checks/check-hardcoded-urls.d.ts.map +1 -0
- package/dist/checks/check-hardcoded-urls.js +124 -0
- package/dist/checks/check-hardcoded-urls.js.map +1 -0
- package/dist/checks/check-magic-numbers.d.ts +6 -0
- package/dist/checks/check-magic-numbers.d.ts.map +1 -0
- package/dist/checks/check-magic-numbers.js +116 -0
- package/dist/checks/check-magic-numbers.js.map +1 -0
- package/dist/checks/check-secrets.d.ts +6 -0
- package/dist/checks/check-secrets.d.ts.map +1 -0
- package/dist/checks/check-secrets.js +138 -0
- package/dist/checks/check-secrets.js.map +1 -0
- package/dist/checks/check-snake-case-ts.d.ts +6 -0
- package/dist/checks/check-snake-case-ts.d.ts.map +1 -0
- package/dist/checks/check-snake-case-ts.js +78 -0
- package/dist/checks/check-snake-case-ts.js.map +1 -0
- package/dist/checks/check-todo-fixme.d.ts +6 -0
- package/dist/checks/check-todo-fixme.d.ts.map +1 -0
- package/dist/checks/check-todo-fixme.js +41 -0
- package/dist/checks/check-todo-fixme.js.map +1 -0
- package/dist/checks/check-unsafe-html.d.ts +6 -0
- package/dist/checks/check-unsafe-html.d.ts.map +1 -0
- package/dist/checks/check-unsafe-html.js +101 -0
- package/dist/checks/check-unsafe-html.js.map +1 -0
- package/dist/checks/index.d.ts +30 -0
- package/dist/checks/index.d.ts.map +1 -0
- package/dist/checks/index.js +57 -0
- package/dist/checks/index.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +208 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/file-reader.d.ts +24 -0
- package/dist/utils/file-reader.d.ts.map +1 -0
- package/dist/utils/file-reader.js +146 -0
- package/dist/utils/file-reader.js.map +1 -0
- package/dist/utils/patterns.d.ts +27 -0
- package/dist/utils/patterns.d.ts.map +1 -0
- package/dist/utils/patterns.js +84 -0
- package/dist/utils/patterns.js.map +1 -0
- package/dist/utils/reporters.d.ts +21 -0
- package/dist/utils/reporters.d.ts.map +1 -0
- package/dist/utils/reporters.js +115 -0
- package/dist/utils/reporters.js.map +1 -0
- package/dist/utils/types.d.ts +71 -0
- package/dist/utils/types.d.ts.map +1 -0
- package/dist/utils/types.js +5 -0
- package/dist/utils/types.js.map +1 -0
- package/package.json +83 -0
- package/ralph/api.sh +216 -0
- package/ralph/backup.sh +838 -0
- package/ralph/browser-verify/README.md +135 -0
- package/ralph/browser-verify/verify.ts +450 -0
- package/ralph/checks/check-fastapi-responses.py +155 -0
- package/ralph/hooks/hooks-config.json +72 -0
- package/ralph/hooks/inject-context.sh +44 -0
- package/ralph/hooks/install.sh +207 -0
- package/ralph/hooks/log-tools.sh +45 -0
- package/ralph/hooks/protect-prd.sh +27 -0
- package/ralph/hooks/save-learnings.sh +36 -0
- package/ralph/hooks/warn-debug.sh +54 -0
- package/ralph/hooks/warn-empty-catch.sh +63 -0
- package/ralph/hooks/warn-secrets.sh +89 -0
- package/ralph/hooks/warn-urls.sh +77 -0
- package/ralph/init.sh +515 -0
- package/ralph/loop.sh +730 -0
- package/ralph/playwright.sh +238 -0
- package/ralph/prd.sh +295 -0
- package/ralph/setup/feature-tour.sh +155 -0
- package/ralph/setup/quick-setup.sh +239 -0
- package/ralph/setup/tutorial.sh +159 -0
- package/ralph/setup/ui.sh +136 -0
- package/ralph/setup.sh +401 -0
- package/ralph/signs.sh +150 -0
- package/ralph/utils.sh +682 -0
- package/ralph/verify/browser.sh +324 -0
- package/ralph/verify/lint.sh +363 -0
- package/ralph/verify/review.sh +152 -0
- package/ralph/verify/tests.sh +81 -0
- package/ralph/verify.sh +268 -0
- package/templates/PROMPT.md +235 -0
- package/templates/config/fullstack.json +86 -0
- package/templates/config/go.json +81 -0
- package/templates/config/minimal.json +76 -0
- package/templates/config/node.json +81 -0
- package/templates/config/python.json +81 -0
- package/templates/config/rust.json +81 -0
- package/templates/examples/CLAUDE-django.md +174 -0
- package/templates/examples/CLAUDE-fastapi.md +270 -0
- package/templates/examples/CLAUDE-fastmcp.md +352 -0
- package/templates/examples/CLAUDE-fullstack.md +256 -0
- package/templates/examples/CLAUDE-node.md +246 -0
- package/templates/examples/CLAUDE-react.md +138 -0
- package/templates/optional/cursorrules.template +147 -0
- package/templates/optional/eslint.config.js +34 -0
- package/templates/optional/lint-staged.config.js +34 -0
- package/templates/optional/ruff.toml +125 -0
- package/templates/optional/vibe-check.yml +116 -0
- package/templates/optional/vscode-settings.json +127 -0
- package/templates/signs.json +46 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"auth": {
|
|
3
|
+
"testUser": "",
|
|
4
|
+
"testPassword": "",
|
|
5
|
+
"loginEndpoint": "/api/auth/login",
|
|
6
|
+
"loginMethod": "POST",
|
|
7
|
+
"tokenType": "jwt",
|
|
8
|
+
"tokenHeader": "Authorization",
|
|
9
|
+
"tokenPrefix": "Bearer"
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
"docker": {
|
|
13
|
+
"enabled": false,
|
|
14
|
+
"composeFile": "docker-compose.yml",
|
|
15
|
+
"serviceName": "app",
|
|
16
|
+
"execPrefix": "docker compose exec -T"
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
"paths": {
|
|
20
|
+
"src": "src",
|
|
21
|
+
"tests": "tests",
|
|
22
|
+
"e2e": "tests/e2e"
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
"commands": {
|
|
26
|
+
"dev": "cargo run",
|
|
27
|
+
"install": "cargo build",
|
|
28
|
+
"seed": "",
|
|
29
|
+
"resetDb": ""
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
"migrations": {
|
|
33
|
+
"command": "sqlx migrate run",
|
|
34
|
+
"pattern": "migrations/.*\\.sql$"
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
"checks": {
|
|
38
|
+
"build": "cargo build",
|
|
39
|
+
"lint": "cargo clippy -- -D warnings",
|
|
40
|
+
"test": "cargo test"
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
"api": {
|
|
44
|
+
"baseUrl": "http://localhost:8080",
|
|
45
|
+
"healthEndpoint": "/health",
|
|
46
|
+
"timeout": 30
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
"playwright": {
|
|
50
|
+
"enabled": false,
|
|
51
|
+
"testDir": "tests/e2e",
|
|
52
|
+
"projects": ["chromium"],
|
|
53
|
+
"baseUrl": "http://localhost:8080"
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
"verification": {
|
|
57
|
+
"codeReviewEnabled": true,
|
|
58
|
+
"browserEnabled": true,
|
|
59
|
+
"a11yEnabled": false,
|
|
60
|
+
"mobileViewport": 375,
|
|
61
|
+
"screenshotOnFailure": true
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
"urls": {
|
|
65
|
+
"app": "http://localhost:8080",
|
|
66
|
+
"docs": "http://localhost:8080/swagger"
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
"env": {
|
|
70
|
+
"required": ["DATABASE_URL"],
|
|
71
|
+
"optional": ["REDIS_URL"]
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
"maxIterations": 20,
|
|
75
|
+
"maxSessionSeconds": 600,
|
|
76
|
+
|
|
77
|
+
"contextRotThreshold": {
|
|
78
|
+
"maxStories": 10,
|
|
79
|
+
"maxFilesChanged": 20
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Project Instructions for AI Coding Agents
|
|
2
|
+
|
|
3
|
+
## Naming Conventions
|
|
4
|
+
- **Files**: `snake_case.py` — e.g., `user_views.py`, `auth_serializers.py`
|
|
5
|
+
- **Functions/Variables**: `snake_case` — e.g., `get_user_by_id`, `is_authenticated`
|
|
6
|
+
- **Classes**: `PascalCase` — e.g., `UserViewSet`, `AuthSerializer`
|
|
7
|
+
- **Models**: `PascalCase` singular — e.g., `User`, `BlogPost` (Django pluralizes table names)
|
|
8
|
+
- **Constants**: `SCREAMING_SNAKE` — e.g., `MAX_UPLOAD_SIZE`, `DEFAULT_PAGE_SIZE`
|
|
9
|
+
- **URL patterns**: `kebab-case` — e.g., `/api/user-profile/`, `/api/blog-posts/`
|
|
10
|
+
- **Template files**: `snake_case.html` — e.g., `user_detail.html`
|
|
11
|
+
|
|
12
|
+
## Tech Stack
|
|
13
|
+
- **Backend**: Django 5, Django REST Framework
|
|
14
|
+
- **Database**: PostgreSQL
|
|
15
|
+
- **Cache/Queue**: Redis, Celery
|
|
16
|
+
- **Testing**: pytest, pytest-django
|
|
17
|
+
|
|
18
|
+
## Code Quality Standards
|
|
19
|
+
|
|
20
|
+
### Python Style
|
|
21
|
+
- Follow PEP 8
|
|
22
|
+
- Use type hints for function signatures
|
|
23
|
+
- Maximum line length: 88 (Black default)
|
|
24
|
+
- Use f-strings for string formatting
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# Good
|
|
28
|
+
def get_user_by_email(email: str) -> User | None:
|
|
29
|
+
"""Fetch a user by their email address."""
|
|
30
|
+
return User.objects.filter(email=email).first()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Django Models
|
|
34
|
+
- Use explicit `related_name` on ForeignKey/M2M
|
|
35
|
+
- Add `__str__` method to all models
|
|
36
|
+
- Use model managers for complex queries
|
|
37
|
+
- Add indexes for frequently queried fields
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
class Order(models.Model):
|
|
41
|
+
user = models.ForeignKey(
|
|
42
|
+
User,
|
|
43
|
+
on_delete=models.CASCADE,
|
|
44
|
+
related_name='orders'
|
|
45
|
+
)
|
|
46
|
+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
|
47
|
+
|
|
48
|
+
class Meta:
|
|
49
|
+
ordering = ['-created_at']
|
|
50
|
+
|
|
51
|
+
def __str__(self):
|
|
52
|
+
return f"Order {self.id} by {self.user.email}"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Django REST Framework
|
|
56
|
+
- Use serializers for all input/output
|
|
57
|
+
- Use ViewSets for CRUD operations
|
|
58
|
+
- Use proper permission classes
|
|
59
|
+
- Return appropriate HTTP status codes
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
class OrderViewSet(viewsets.ModelViewSet):
|
|
63
|
+
serializer_class = OrderSerializer
|
|
64
|
+
permission_classes = [IsAuthenticated]
|
|
65
|
+
|
|
66
|
+
def get_queryset(self):
|
|
67
|
+
# Only return user's own orders
|
|
68
|
+
return Order.objects.filter(user=self.request.user)
|
|
69
|
+
|
|
70
|
+
def perform_create(self, serializer):
|
|
71
|
+
serializer.save(user=self.request.user)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Error Handling
|
|
75
|
+
- Use DRF exceptions for API errors
|
|
76
|
+
- Log errors with context
|
|
77
|
+
- Never expose internal errors to users
|
|
78
|
+
- Handle specific exceptions, not bare except
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# Bad
|
|
82
|
+
try:
|
|
83
|
+
process_order(order)
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
# Good
|
|
88
|
+
try:
|
|
89
|
+
process_order(order)
|
|
90
|
+
except PaymentError as e:
|
|
91
|
+
logger.error(f"Payment failed for order {order.id}: {e}")
|
|
92
|
+
raise ValidationError({"payment": "Payment processing failed"})
|
|
93
|
+
except InventoryError as e:
|
|
94
|
+
logger.warning(f"Inventory issue for order {order.id}: {e}")
|
|
95
|
+
raise ValidationError({"inventory": str(e)})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Database Queries
|
|
99
|
+
- Avoid N+1 queries (use select_related/prefetch_related)
|
|
100
|
+
- Use .only() or .defer() for large models
|
|
101
|
+
- Use database transactions for multi-step operations
|
|
102
|
+
- Add indexes for filter/order fields
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# Bad - N+1 query
|
|
106
|
+
orders = Order.objects.all()
|
|
107
|
+
for order in orders:
|
|
108
|
+
print(order.user.email) # Hits DB for each order
|
|
109
|
+
|
|
110
|
+
# Good
|
|
111
|
+
orders = Order.objects.select_related('user').all()
|
|
112
|
+
for order in orders:
|
|
113
|
+
print(order.user.email) # No additional queries
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Security
|
|
117
|
+
- Use Django's CSRF protection
|
|
118
|
+
- Validate all user input with serializers
|
|
119
|
+
- Use parameterized queries (Django ORM does this)
|
|
120
|
+
- Never trust user input for file paths or shell commands
|
|
121
|
+
|
|
122
|
+
### Testing
|
|
123
|
+
- Use pytest and pytest-django
|
|
124
|
+
- Use fixtures for test data
|
|
125
|
+
- Test API endpoints with APIClient
|
|
126
|
+
- Mock external services
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
@pytest.mark.django_db
|
|
130
|
+
class TestOrderAPI:
|
|
131
|
+
def test_create_order(self, authenticated_client, product):
|
|
132
|
+
response = authenticated_client.post('/api/orders/', {
|
|
133
|
+
'product_id': product.id,
|
|
134
|
+
'quantity': 2,
|
|
135
|
+
})
|
|
136
|
+
assert response.status_code == 201
|
|
137
|
+
assert Order.objects.count() == 1
|
|
138
|
+
|
|
139
|
+
def test_cannot_create_order_unauthenticated(self, client):
|
|
140
|
+
response = client.post('/api/orders/', {})
|
|
141
|
+
assert response.status_code == 401
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## File Structure
|
|
145
|
+
```
|
|
146
|
+
project/
|
|
147
|
+
├── config/ # Django settings, URLs, WSGI
|
|
148
|
+
├── apps/
|
|
149
|
+
│ ├── users/ # User-related models, views
|
|
150
|
+
│ ├── orders/ # Order-related models, views
|
|
151
|
+
│ └── common/ # Shared utilities
|
|
152
|
+
├── tests/ # Test files
|
|
153
|
+
└── manage.py
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Environment Variables
|
|
157
|
+
```python
|
|
158
|
+
# settings.py
|
|
159
|
+
DATABASE_URL = os.getenv('DATABASE_URL')
|
|
160
|
+
SECRET_KEY = os.getenv('SECRET_KEY')
|
|
161
|
+
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Pre-commit Hooks
|
|
165
|
+
This project uses agentic-loop hooks. Run `/vibe-check` before committing.
|
|
166
|
+
|
|
167
|
+
## Common Commands
|
|
168
|
+
```bash
|
|
169
|
+
make up # Start Docker services
|
|
170
|
+
make migrate # Run migrations
|
|
171
|
+
make test # Run tests
|
|
172
|
+
make shell # Django shell
|
|
173
|
+
make logs # View logs
|
|
174
|
+
```
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# CLAUDE.md - FastAPI Project
|
|
2
|
+
|
|
3
|
+
## Naming Conventions
|
|
4
|
+
- **Files**: `snake_case.py` — e.g., `user_service.py`, `auth_router.py`
|
|
5
|
+
- **Functions/Variables**: `snake_case` — e.g., `get_user_by_id`, `is_valid`
|
|
6
|
+
- **Classes**: `PascalCase` — e.g., `UserService`, `AuthRouter`
|
|
7
|
+
- **Pydantic Models**: `PascalCase` — e.g., `UserCreate`, `UserResponse`
|
|
8
|
+
- **Constants**: `SCREAMING_SNAKE` — e.g., `MAX_RETRIES`, `DEFAULT_LIMIT`
|
|
9
|
+
- **Database tables**: `snake_case` — e.g., `user_sessions`, `api_keys`
|
|
10
|
+
- **API endpoints**: `kebab-case` — e.g., `/api/user-profile`, `/api/v1/auth`
|
|
11
|
+
|
|
12
|
+
## Project Overview
|
|
13
|
+
|
|
14
|
+
This is a FastAPI backend application with async SQLAlchemy and Pydantic.
|
|
15
|
+
|
|
16
|
+
## Tech Stack
|
|
17
|
+
|
|
18
|
+
- **Framework**: FastAPI
|
|
19
|
+
- **ORM**: SQLAlchemy 2.0 (async)
|
|
20
|
+
- **Validation**: Pydantic v2
|
|
21
|
+
- **Database**: PostgreSQL
|
|
22
|
+
- **Migrations**: Alembic
|
|
23
|
+
- **Testing**: pytest + httpx
|
|
24
|
+
- **Task Queue**: Celery + Redis (if applicable)
|
|
25
|
+
|
|
26
|
+
## Project Structure
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
app/
|
|
30
|
+
├── main.py # FastAPI app entry point
|
|
31
|
+
├── config.py # Settings via pydantic-settings
|
|
32
|
+
├── database.py # Async SQLAlchemy setup
|
|
33
|
+
├── models/ # SQLAlchemy models
|
|
34
|
+
├── schemas/ # Pydantic schemas
|
|
35
|
+
├── routers/ # API route handlers
|
|
36
|
+
├── services/ # Business logic
|
|
37
|
+
├── dependencies.py # FastAPI dependencies
|
|
38
|
+
└── exceptions.py # Custom exceptions
|
|
39
|
+
tests/
|
|
40
|
+
├── conftest.py # Fixtures
|
|
41
|
+
├── test_api/ # API tests
|
|
42
|
+
└── test_services/ # Unit tests
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Development
|
|
49
|
+
uvicorn app.main:app --reload --port 8000
|
|
50
|
+
|
|
51
|
+
# Database
|
|
52
|
+
alembic upgrade head # Run migrations
|
|
53
|
+
alembic revision --autogenerate -m "" # Create migration
|
|
54
|
+
|
|
55
|
+
# Testing
|
|
56
|
+
pytest # Run all tests
|
|
57
|
+
pytest -x # Stop on first failure
|
|
58
|
+
pytest --cov=app # With coverage
|
|
59
|
+
|
|
60
|
+
# Linting
|
|
61
|
+
ruff check app/
|
|
62
|
+
ruff format app/
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Code Standards
|
|
66
|
+
|
|
67
|
+
### API Endpoints
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# Good - explicit status codes, response model, dependencies
|
|
71
|
+
@router.post("/users", status_code=status.HTTP_201_CREATED, response_model=UserResponse)
|
|
72
|
+
async def create_user(
|
|
73
|
+
user_in: UserCreate,
|
|
74
|
+
db: AsyncSession = Depends(get_db),
|
|
75
|
+
current_user: User = Depends(get_current_user),
|
|
76
|
+
) -> UserResponse:
|
|
77
|
+
"""Create a new user."""
|
|
78
|
+
return await user_service.create(db, user_in)
|
|
79
|
+
|
|
80
|
+
# Bad - implicit everything
|
|
81
|
+
@router.post("/users")
|
|
82
|
+
async def create_user(user_in: UserCreate, db = Depends(get_db)):
|
|
83
|
+
return await create(db, user_in)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Pydantic Schemas
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# Good - explicit validation, examples, field descriptions
|
|
90
|
+
class UserCreate(BaseModel):
|
|
91
|
+
email: EmailStr = Field(..., description="User's email address")
|
|
92
|
+
password: str = Field(..., min_length=8, description="Password (min 8 chars)")
|
|
93
|
+
name: str = Field(..., min_length=1, max_length=100)
|
|
94
|
+
|
|
95
|
+
model_config = ConfigDict(
|
|
96
|
+
json_schema_extra={
|
|
97
|
+
"example": {
|
|
98
|
+
"email": "user@example.com",
|
|
99
|
+
"password": "securepass123",
|
|
100
|
+
"name": "John Doe"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Bad - no validation
|
|
106
|
+
class UserCreate(BaseModel):
|
|
107
|
+
email: str
|
|
108
|
+
password: str
|
|
109
|
+
name: str
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### SQLAlchemy Models
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# Good - explicit types, relationships, indexes
|
|
116
|
+
class User(Base):
|
|
117
|
+
__tablename__ = "users"
|
|
118
|
+
|
|
119
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
120
|
+
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
|
|
121
|
+
hashed_password: Mapped[str] = mapped_column(String(255))
|
|
122
|
+
is_active: Mapped[bool] = mapped_column(default=True)
|
|
123
|
+
created_at: Mapped[datetime] = mapped_column(default=func.now())
|
|
124
|
+
|
|
125
|
+
# Relationships
|
|
126
|
+
posts: Mapped[list["Post"]] = relationship(back_populates="author")
|
|
127
|
+
|
|
128
|
+
# Bad - old style, no types
|
|
129
|
+
class User(Base):
|
|
130
|
+
__tablename__ = "users"
|
|
131
|
+
id = Column(Integer, primary_key=True)
|
|
132
|
+
email = Column(String)
|
|
133
|
+
password = Column(String)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Async Database Sessions
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# Good - async context manager
|
|
140
|
+
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|
141
|
+
async with async_session() as session:
|
|
142
|
+
try:
|
|
143
|
+
yield session
|
|
144
|
+
await session.commit()
|
|
145
|
+
except Exception:
|
|
146
|
+
await session.rollback()
|
|
147
|
+
raise
|
|
148
|
+
|
|
149
|
+
# Bad - no cleanup
|
|
150
|
+
async def get_db():
|
|
151
|
+
return async_session()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Error Handling
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# Good - custom exceptions with proper HTTP codes
|
|
158
|
+
class NotFoundError(Exception):
|
|
159
|
+
def __init__(self, resource: str, id: int):
|
|
160
|
+
self.resource = resource
|
|
161
|
+
self.id = id
|
|
162
|
+
|
|
163
|
+
@app.exception_handler(NotFoundError)
|
|
164
|
+
async def not_found_handler(request: Request, exc: NotFoundError):
|
|
165
|
+
return JSONResponse(
|
|
166
|
+
status_code=404,
|
|
167
|
+
content={"detail": f"{exc.resource} with id {exc.id} not found"}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Bad - generic exceptions
|
|
171
|
+
raise Exception("User not found")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Service Layer
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# Good - service handles business logic
|
|
178
|
+
class UserService:
|
|
179
|
+
async def create(self, db: AsyncSession, user_in: UserCreate) -> User:
|
|
180
|
+
# Check if exists
|
|
181
|
+
existing = await self.get_by_email(db, user_in.email)
|
|
182
|
+
if existing:
|
|
183
|
+
raise ConflictError("User", "email", user_in.email)
|
|
184
|
+
|
|
185
|
+
# Hash password
|
|
186
|
+
hashed = hash_password(user_in.password)
|
|
187
|
+
|
|
188
|
+
# Create user
|
|
189
|
+
user = User(email=user_in.email, hashed_password=hashed, name=user_in.name)
|
|
190
|
+
db.add(user)
|
|
191
|
+
await db.flush()
|
|
192
|
+
return user
|
|
193
|
+
|
|
194
|
+
# Bad - logic in router
|
|
195
|
+
@router.post("/users")
|
|
196
|
+
async def create_user(user_in: UserCreate, db: AsyncSession = Depends(get_db)):
|
|
197
|
+
existing = await db.execute(select(User).where(User.email == user_in.email))
|
|
198
|
+
if existing.scalar():
|
|
199
|
+
raise HTTPException(409, "Email exists")
|
|
200
|
+
hashed = bcrypt.hash(user_in.password)
|
|
201
|
+
user = User(email=user_in.email, hashed_password=hashed)
|
|
202
|
+
db.add(user)
|
|
203
|
+
# ... more logic
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Do NOT
|
|
207
|
+
|
|
208
|
+
- Use `Any` type - create proper Pydantic models
|
|
209
|
+
- Put business logic in routers - use service layer
|
|
210
|
+
- Use synchronous database calls in async endpoints
|
|
211
|
+
- Hardcode secrets - use `pydantic-settings` with env vars
|
|
212
|
+
- Skip input validation - use Pydantic Field validators
|
|
213
|
+
- Return SQLAlchemy models directly - use response schemas
|
|
214
|
+
- Use `*` imports - explicit imports only
|
|
215
|
+
- Catch generic `Exception` - catch specific exceptions
|
|
216
|
+
|
|
217
|
+
## Do
|
|
218
|
+
|
|
219
|
+
- Use async/await consistently
|
|
220
|
+
- Add OpenAPI descriptions to all endpoints
|
|
221
|
+
- Use dependency injection for services
|
|
222
|
+
- Write tests for all endpoints
|
|
223
|
+
- Use Alembic for all schema changes
|
|
224
|
+
- Add proper logging with structlog
|
|
225
|
+
- Use HTTPException for API errors
|
|
226
|
+
- Validate all inputs with Pydantic
|
|
227
|
+
|
|
228
|
+
## Environment Variables
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/dbname
|
|
232
|
+
SECRET_KEY=your-secret-key
|
|
233
|
+
REDIS_URL=redis://localhost:6379/0
|
|
234
|
+
DEBUG=false
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Always use `pydantic-settings`:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
class Settings(BaseSettings):
|
|
241
|
+
database_url: PostgresDsn
|
|
242
|
+
secret_key: str
|
|
243
|
+
debug: bool = False
|
|
244
|
+
|
|
245
|
+
model_config = SettingsConfigDict(env_file=".env")
|
|
246
|
+
|
|
247
|
+
settings = Settings()
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Testing
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
# Good - async test with fixtures
|
|
254
|
+
@pytest.mark.asyncio
|
|
255
|
+
async def test_create_user(client: AsyncClient, db: AsyncSession):
|
|
256
|
+
response = await client.post(
|
|
257
|
+
"/api/users",
|
|
258
|
+
json={"email": "test@example.com", "password": "testpass123", "name": "Test"}
|
|
259
|
+
)
|
|
260
|
+
assert response.status_code == 201
|
|
261
|
+
data = response.json()
|
|
262
|
+
assert data["email"] == "test@example.com"
|
|
263
|
+
assert "password" not in data # Never return password
|
|
264
|
+
|
|
265
|
+
# conftest.py
|
|
266
|
+
@pytest.fixture
|
|
267
|
+
async def client(app: FastAPI) -> AsyncGenerator[AsyncClient, None]:
|
|
268
|
+
async with AsyncClient(app=app, base_url="http://test") as client:
|
|
269
|
+
yield client
|
|
270
|
+
```
|