@malamute/ai-rules 1.0.0 → 1.2.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/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "tests/**/*.py"
|
|
4
|
+
- "**/test_*.py"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Flask Testing Patterns
|
|
8
|
+
|
|
9
|
+
## Test Configuration
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
# conftest.py
|
|
13
|
+
import pytest
|
|
14
|
+
from app import create_app, db
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(scope="session")
|
|
17
|
+
def app():
|
|
18
|
+
"""Create application for testing."""
|
|
19
|
+
app = create_app("testing")
|
|
20
|
+
return app
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def client(app):
|
|
24
|
+
"""Create test client."""
|
|
25
|
+
return app.test_client()
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def runner(app):
|
|
29
|
+
"""Create CLI test runner."""
|
|
30
|
+
return app.test_cli_runner()
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def db_session(app):
|
|
34
|
+
"""Create database session for testing."""
|
|
35
|
+
with app.app_context():
|
|
36
|
+
db.create_all()
|
|
37
|
+
yield db.session
|
|
38
|
+
db.session.rollback()
|
|
39
|
+
db.drop_all()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Testing Config
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# config.py
|
|
46
|
+
class TestingConfig:
|
|
47
|
+
TESTING = True
|
|
48
|
+
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
|
|
49
|
+
WTF_CSRF_ENABLED = False
|
|
50
|
+
SECRET_KEY = "test-secret-key"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## API Endpoint Tests
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
class TestUsersAPI:
|
|
57
|
+
def test_create_user(self, client, db_session):
|
|
58
|
+
response = client.post("/api/v1/users", json={
|
|
59
|
+
"email": "test@example.com",
|
|
60
|
+
"password": "password123",
|
|
61
|
+
"name": "Test User",
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
assert response.status_code == 201
|
|
65
|
+
data = response.get_json()
|
|
66
|
+
assert data["email"] == "test@example.com"
|
|
67
|
+
assert "id" in data
|
|
68
|
+
assert "password" not in data
|
|
69
|
+
|
|
70
|
+
def test_create_user_duplicate_email(self, client, db_session):
|
|
71
|
+
# Create existing user
|
|
72
|
+
user = User(email="existing@example.com", name="Existing")
|
|
73
|
+
db_session.add(user)
|
|
74
|
+
db_session.commit()
|
|
75
|
+
|
|
76
|
+
response = client.post("/api/v1/users", json={
|
|
77
|
+
"email": "existing@example.com",
|
|
78
|
+
"password": "password123",
|
|
79
|
+
"name": "New User",
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
assert response.status_code == 409
|
|
83
|
+
|
|
84
|
+
def test_get_user_not_found(self, client):
|
|
85
|
+
response = client.get("/api/v1/users/99999")
|
|
86
|
+
assert response.status_code == 404
|
|
87
|
+
|
|
88
|
+
def test_list_users_pagination(self, client, db_session):
|
|
89
|
+
# Create users
|
|
90
|
+
for i in range(25):
|
|
91
|
+
db_session.add(User(email=f"user{i}@example.com", name=f"User {i}"))
|
|
92
|
+
db_session.commit()
|
|
93
|
+
|
|
94
|
+
response = client.get("/api/v1/users?page=2&size=10")
|
|
95
|
+
|
|
96
|
+
assert response.status_code == 200
|
|
97
|
+
data = response.get_json()
|
|
98
|
+
assert len(data["items"]) == 10
|
|
99
|
+
assert data["total"] == 25
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Authentication Tests
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
@pytest.fixture
|
|
106
|
+
def auth_headers(client, db_session):
|
|
107
|
+
"""Create authenticated user and return headers."""
|
|
108
|
+
user = User(email="auth@example.com", name="Auth User")
|
|
109
|
+
user.set_password("password123")
|
|
110
|
+
db_session.add(user)
|
|
111
|
+
db_session.commit()
|
|
112
|
+
|
|
113
|
+
response = client.post("/api/v1/auth/login", json={
|
|
114
|
+
"email": "auth@example.com",
|
|
115
|
+
"password": "password123",
|
|
116
|
+
})
|
|
117
|
+
token = response.get_json()["access_token"]
|
|
118
|
+
|
|
119
|
+
return {"Authorization": f"Bearer {token}"}
|
|
120
|
+
|
|
121
|
+
class TestAuthenticatedEndpoints:
|
|
122
|
+
def test_get_me_unauthorized(self, client):
|
|
123
|
+
response = client.get("/api/v1/users/me")
|
|
124
|
+
assert response.status_code == 401
|
|
125
|
+
|
|
126
|
+
def test_get_me_authorized(self, client, auth_headers):
|
|
127
|
+
response = client.get("/api/v1/users/me", headers=auth_headers)
|
|
128
|
+
assert response.status_code == 200
|
|
129
|
+
assert response.get_json()["email"] == "auth@example.com"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Form Tests
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
def test_login_form(client, db_session):
|
|
136
|
+
# Create user
|
|
137
|
+
user = User(email="test@example.com", name="Test")
|
|
138
|
+
user.set_password("password")
|
|
139
|
+
db_session.add(user)
|
|
140
|
+
db_session.commit()
|
|
141
|
+
|
|
142
|
+
response = client.post("/login", data={
|
|
143
|
+
"email": "test@example.com",
|
|
144
|
+
"password": "password",
|
|
145
|
+
}, follow_redirects=True)
|
|
146
|
+
|
|
147
|
+
assert response.status_code == 200
|
|
148
|
+
assert b"Dashboard" in response.data
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## File Upload Tests
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from io import BytesIO
|
|
155
|
+
|
|
156
|
+
def test_file_upload(client, auth_headers):
|
|
157
|
+
data = {
|
|
158
|
+
"file": (BytesIO(b"file content"), "test.txt"),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
response = client.post(
|
|
162
|
+
"/api/v1/files/upload",
|
|
163
|
+
data=data,
|
|
164
|
+
content_type="multipart/form-data",
|
|
165
|
+
headers=auth_headers,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
assert response.status_code == 201
|
|
169
|
+
assert response.get_json()["filename"] == "test.txt"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## CLI Command Tests
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
def test_init_db_command(runner):
|
|
176
|
+
result = runner.invoke(args=["init-db"])
|
|
177
|
+
assert result.exit_code == 0
|
|
178
|
+
assert "Database initialized" in result.output
|
|
179
|
+
|
|
180
|
+
def test_create_user_command(runner, db_session):
|
|
181
|
+
result = runner.invoke(args=[
|
|
182
|
+
"create-user",
|
|
183
|
+
"test@example.com",
|
|
184
|
+
"Test User",
|
|
185
|
+
"--admin",
|
|
186
|
+
])
|
|
187
|
+
|
|
188
|
+
assert result.exit_code == 0
|
|
189
|
+
assert "Created user" in result.output
|
|
190
|
+
|
|
191
|
+
user = User.query.filter_by(email="test@example.com").first()
|
|
192
|
+
assert user is not None
|
|
193
|
+
assert user.is_admin is True
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Mocking External Services
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from unittest.mock import patch, MagicMock
|
|
200
|
+
|
|
201
|
+
def test_send_email(client, db_session):
|
|
202
|
+
with patch("app.services.email.mail") as mock_mail:
|
|
203
|
+
response = client.post("/api/v1/users", json={
|
|
204
|
+
"email": "test@example.com",
|
|
205
|
+
"password": "password123",
|
|
206
|
+
"name": "Test User",
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
assert response.status_code == 201
|
|
210
|
+
mock_mail.send.assert_called_once()
|
|
211
|
+
|
|
212
|
+
def test_external_api_call(client):
|
|
213
|
+
with patch("app.services.external.requests") as mock_requests:
|
|
214
|
+
mock_requests.get.return_value = MagicMock(
|
|
215
|
+
status_code=200,
|
|
216
|
+
json=lambda: {"data": "mocked"},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
response = client.get("/api/v1/external-data")
|
|
220
|
+
|
|
221
|
+
assert response.status_code == 200
|
|
222
|
+
assert response.get_json()["data"] == "mocked"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Parametrized Tests
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
@pytest.mark.parametrize("email,expected_status", [
|
|
229
|
+
("valid@example.com", 201),
|
|
230
|
+
("also.valid@test.co.uk", 201),
|
|
231
|
+
("invalid", 400),
|
|
232
|
+
("missing@", 400),
|
|
233
|
+
("@nodomain.com", 400),
|
|
234
|
+
])
|
|
235
|
+
def test_email_validation(client, email: str, expected_status: int):
|
|
236
|
+
response = client.post("/api/v1/users", json={
|
|
237
|
+
"email": email,
|
|
238
|
+
"password": "password123",
|
|
239
|
+
"name": "Test",
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
assert response.status_code == expected_status
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Test Markers
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
# pytest.ini or pyproject.toml
|
|
249
|
+
[tool.pytest.ini_options]
|
|
250
|
+
markers = [
|
|
251
|
+
"slow: marks tests as slow",
|
|
252
|
+
"integration: requires database",
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
# Usage
|
|
256
|
+
@pytest.mark.slow
|
|
257
|
+
def test_heavy_computation():
|
|
258
|
+
...
|
|
259
|
+
|
|
260
|
+
@pytest.mark.integration
|
|
261
|
+
def test_database_query(db_session):
|
|
262
|
+
...
|
|
263
|
+
|
|
264
|
+
# Run specific markers
|
|
265
|
+
# pytest -m "not slow"
|
|
266
|
+
# pytest -m integration
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Coverage Configuration
|
|
270
|
+
|
|
271
|
+
```toml
|
|
272
|
+
# pyproject.toml
|
|
273
|
+
[tool.coverage.run]
|
|
274
|
+
source = ["app"]
|
|
275
|
+
omit = ["*/tests/*", "*/__init__.py"]
|
|
276
|
+
|
|
277
|
+
[tool.coverage.report]
|
|
278
|
+
exclude_lines = [
|
|
279
|
+
"pragma: no cover",
|
|
280
|
+
"if TYPE_CHECKING:",
|
|
281
|
+
"raise NotImplementedError",
|
|
282
|
+
]
|
|
283
|
+
fail_under = 80
|
|
284
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(python *)",
|
|
5
|
+
"Bash(python3 *)",
|
|
6
|
+
"Bash(flask *)",
|
|
7
|
+
"Bash(gunicorn *)",
|
|
8
|
+
"Bash(pytest *)",
|
|
9
|
+
"Bash(ruff *)",
|
|
10
|
+
"Bash(mypy *)",
|
|
11
|
+
"Bash(alembic *)",
|
|
12
|
+
"Bash(uv *)",
|
|
13
|
+
"Bash(poetry *)",
|
|
14
|
+
"Bash(pip *)",
|
|
15
|
+
"Bash(pip3 *)",
|
|
16
|
+
"Read",
|
|
17
|
+
"Edit",
|
|
18
|
+
"Write"
|
|
19
|
+
],
|
|
20
|
+
"deny": [
|
|
21
|
+
"Bash(rm -rf *)",
|
|
22
|
+
"Read(.env)",
|
|
23
|
+
"Read(.env.*)",
|
|
24
|
+
"Read(**/secrets/**)",
|
|
25
|
+
"Read(**/*.pem)"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"env": {
|
|
29
|
+
"FLASK_DEBUG": "1",
|
|
30
|
+
"PYTHONDONTWRITEBYTECODE": "1",
|
|
31
|
+
"PYTHONUNBUFFERED": "1"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Flask Project Guidelines
|
|
2
|
+
|
|
3
|
+
@../_shared/CLAUDE.md
|
|
4
|
+
|
|
5
|
+
## Stack
|
|
6
|
+
|
|
7
|
+
- Python 3.12+
|
|
8
|
+
- Flask 3.0+
|
|
9
|
+
- SQLAlchemy 2.0+ (sync or async)
|
|
10
|
+
- Marshmallow for validation
|
|
11
|
+
- pytest for testing
|
|
12
|
+
- uv or poetry for dependencies
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
src/app/
|
|
18
|
+
├── __init__.py # App factory
|
|
19
|
+
├── config.py # Configuration classes
|
|
20
|
+
├── extensions.py # Flask extensions (db, migrate, etc.)
|
|
21
|
+
├── [domain]/ # Feature blueprints
|
|
22
|
+
│ ├── __init__.py # Blueprint registration
|
|
23
|
+
│ ├── routes.py # Route handlers
|
|
24
|
+
│ ├── schemas.py # Marshmallow schemas
|
|
25
|
+
│ ├── models.py # SQLAlchemy models
|
|
26
|
+
│ ├── services.py # Business logic
|
|
27
|
+
│ └── repository.py # Data access
|
|
28
|
+
├── core/ # Shared utilities
|
|
29
|
+
│ ├── exceptions.py
|
|
30
|
+
│ └── security.py
|
|
31
|
+
└── common/
|
|
32
|
+
├── models.py # Base models
|
|
33
|
+
└── schemas.py # Shared schemas
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Flask Patterns
|
|
37
|
+
|
|
38
|
+
### Application Factory
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from flask import Flask
|
|
42
|
+
from app.extensions import db, migrate
|
|
43
|
+
from app.config import config
|
|
44
|
+
|
|
45
|
+
def create_app(config_name: str = "development") -> Flask:
|
|
46
|
+
app = Flask(__name__)
|
|
47
|
+
app.config.from_object(config[config_name])
|
|
48
|
+
|
|
49
|
+
# Initialize extensions
|
|
50
|
+
db.init_app(app)
|
|
51
|
+
migrate.init_app(app, db)
|
|
52
|
+
|
|
53
|
+
# Register blueprints
|
|
54
|
+
from app.users import users_bp
|
|
55
|
+
app.register_blueprint(users_bp, url_prefix="/api/v1/users")
|
|
56
|
+
|
|
57
|
+
# Register error handlers
|
|
58
|
+
register_error_handlers(app)
|
|
59
|
+
|
|
60
|
+
return app
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Blueprints
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from flask import Blueprint, request, jsonify
|
|
67
|
+
from app.users.schemas import UserCreateSchema, UserResponseSchema
|
|
68
|
+
from app.users.services import UserService
|
|
69
|
+
|
|
70
|
+
users_bp = Blueprint("users", __name__)
|
|
71
|
+
|
|
72
|
+
@users_bp.route("/", methods=["POST"])
|
|
73
|
+
def create_user():
|
|
74
|
+
schema = UserCreateSchema()
|
|
75
|
+
data = schema.load(request.get_json())
|
|
76
|
+
|
|
77
|
+
user = UserService.create(data)
|
|
78
|
+
|
|
79
|
+
return jsonify(UserResponseSchema().dump(user)), 201
|
|
80
|
+
|
|
81
|
+
@users_bp.route("/<int:user_id>")
|
|
82
|
+
def get_user(user_id: int):
|
|
83
|
+
user = UserService.get_by_id(user_id)
|
|
84
|
+
if not user:
|
|
85
|
+
return jsonify({"error": "User not found"}), 404
|
|
86
|
+
return jsonify(UserResponseSchema().dump(user))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Extensions
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# extensions.py
|
|
93
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
94
|
+
from flask_migrate import Migrate
|
|
95
|
+
from flask_jwt_extended import JWTManager
|
|
96
|
+
|
|
97
|
+
db = SQLAlchemy()
|
|
98
|
+
migrate = Migrate()
|
|
99
|
+
jwt = JWTManager()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Marshmallow Schemas
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from marshmallow import Schema, fields, validate, post_load
|
|
106
|
+
|
|
107
|
+
class UserCreateSchema(Schema):
|
|
108
|
+
email = fields.Email(required=True)
|
|
109
|
+
password = fields.Str(required=True, validate=validate.Length(min=8))
|
|
110
|
+
name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
|
|
111
|
+
|
|
112
|
+
class UserResponseSchema(Schema):
|
|
113
|
+
id = fields.Int(dump_only=True)
|
|
114
|
+
email = fields.Email()
|
|
115
|
+
name = fields.Str()
|
|
116
|
+
created_at = fields.DateTime(dump_only=True)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## SQLAlchemy 2.0
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
123
|
+
from app.extensions import db
|
|
124
|
+
|
|
125
|
+
class User(db.Model):
|
|
126
|
+
__tablename__ = "users"
|
|
127
|
+
|
|
128
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
129
|
+
email: Mapped[str] = mapped_column(unique=True, index=True)
|
|
130
|
+
hashed_password: Mapped[str]
|
|
131
|
+
name: Mapped[str] = mapped_column(db.String(100))
|
|
132
|
+
is_active: Mapped[bool] = mapped_column(default=True)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Error Handling
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from flask import jsonify
|
|
139
|
+
from werkzeug.exceptions import HTTPException
|
|
140
|
+
|
|
141
|
+
def register_error_handlers(app):
|
|
142
|
+
@app.errorhandler(HTTPException)
|
|
143
|
+
def handle_http_error(error):
|
|
144
|
+
return jsonify({
|
|
145
|
+
"error": error.name,
|
|
146
|
+
"message": error.description,
|
|
147
|
+
}), error.code
|
|
148
|
+
|
|
149
|
+
@app.errorhandler(ValidationError)
|
|
150
|
+
def handle_validation_error(error):
|
|
151
|
+
return jsonify({
|
|
152
|
+
"error": "Validation Error",
|
|
153
|
+
"details": error.messages,
|
|
154
|
+
}), 400
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Commands
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
flask run # Dev server
|
|
161
|
+
flask db upgrade # Run migrations
|
|
162
|
+
flask db migrate -m "message" # Generate migration
|
|
163
|
+
pytest # Run tests
|
|
164
|
+
pytest --cov=app # Coverage
|
|
165
|
+
ruff check . && ruff format . # Lint + format
|
|
166
|
+
```
|