@malamute/ai-rules 1.0.0 → 1.3.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 +272 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/_shared/rules/conventions/documentation.md +324 -0
- package/configs/_shared/rules/conventions/git.md +265 -0
- package/configs/_shared/rules/conventions/npm.md +80 -0
- package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
- package/configs/_shared/rules/conventions/principles.md +334 -0
- package/configs/_shared/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/rules/devops/docker.md +275 -0
- package/configs/_shared/rules/devops/nx.md +194 -0
- package/configs/_shared/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/rules/lang/python/async.md +337 -0
- package/configs/_shared/rules/lang/python/celery.md +476 -0
- package/configs/_shared/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/rules/lang/python/python.md +172 -0
- package/configs/_shared/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/rules/quality/error-handling.md +48 -0
- package/configs/_shared/rules/quality/logging.md +45 -0
- package/configs/_shared/rules/quality/observability.md +240 -0
- package/configs/_shared/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/rules/security/secrets-management.md +222 -0
- package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/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/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
- package/configs/angular/rules/core/resource.md +285 -0
- package/configs/angular/rules/core/signals.md +323 -0
- package/configs/angular/rules/http.md +338 -0
- package/configs/angular/rules/routing.md +291 -0
- package/configs/angular/rules/ssr.md +312 -0
- package/configs/angular/rules/state/signal-store.md +408 -0
- package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
- package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
- package/configs/angular/rules/ui/aria.md +422 -0
- package/configs/angular/rules/ui/forms.md +424 -0
- package/configs/angular/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/{.claude/settings.json → settings.json} +3 -0
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/dotnet/rules/background-services.md +552 -0
- package/configs/dotnet/rules/configuration.md +426 -0
- package/configs/dotnet/rules/ddd.md +447 -0
- package/configs/dotnet/rules/dependency-injection.md +343 -0
- package/configs/dotnet/rules/mediatr.md +320 -0
- package/configs/dotnet/rules/middleware.md +489 -0
- package/configs/dotnet/rules/result-pattern.md +363 -0
- package/configs/dotnet/rules/validation.md +388 -0
- package/configs/dotnet/settings.json +29 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/fastapi/rules/background-tasks.md +254 -0
- package/configs/fastapi/rules/dependencies.md +170 -0
- package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
- package/configs/fastapi/rules/lifespan.md +274 -0
- package/configs/fastapi/rules/middleware.md +229 -0
- package/configs/fastapi/rules/pydantic.md +433 -0
- package/configs/fastapi/rules/responses.md +251 -0
- package/configs/fastapi/rules/routers.md +202 -0
- package/configs/fastapi/rules/security.md +222 -0
- package/configs/fastapi/rules/testing.md +251 -0
- package/configs/fastapi/rules/websockets.md +298 -0
- package/configs/fastapi/settings.json +35 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/flask/rules/blueprints.md +208 -0
- package/configs/flask/rules/cli.md +285 -0
- package/configs/flask/rules/configuration.md +281 -0
- package/configs/flask/rules/context.md +238 -0
- package/configs/flask/rules/error-handlers.md +278 -0
- package/configs/flask/rules/extensions.md +278 -0
- package/configs/flask/rules/flask.md +171 -0
- package/configs/flask/rules/marshmallow.md +206 -0
- package/configs/flask/rules/security.md +267 -0
- package/configs/flask/rules/testing.md +284 -0
- package/configs/flask/settings.json +35 -0
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nestjs/rules/common-patterns.md +300 -0
- package/configs/nestjs/rules/filters.md +376 -0
- package/configs/nestjs/rules/interceptors.md +317 -0
- package/configs/nestjs/rules/middleware.md +321 -0
- package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
- package/configs/nestjs/rules/pipes.md +351 -0
- package/configs/nestjs/rules/websockets.md +451 -0
- package/configs/nestjs/settings.json +31 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/configs/nextjs/rules/api-routes.md +358 -0
- package/configs/nextjs/rules/authentication.md +355 -0
- package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
- package/configs/nextjs/rules/data-fetching.md +249 -0
- package/configs/nextjs/rules/database.md +400 -0
- package/configs/nextjs/rules/middleware.md +303 -0
- package/configs/nextjs/rules/routing.md +324 -0
- package/configs/nextjs/rules/seo.md +350 -0
- package/configs/nextjs/rules/server-actions.md +353 -0
- package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
- package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
- package/package.json +24 -9
- package/src/cli.js +218 -0
- package/src/config.js +63 -0
- package/src/index.js +4 -0
- package/src/installer.js +414 -0
- package/src/merge.js +109 -0
- package/src/tech-config.json +45 -0
- package/src/utils.js +88 -0
- package/configs/dotnet/.claude/settings.json +0 -9
- package/configs/nestjs/.claude/settings.json +0 -15
- 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 → rules/domain/frontend}/accessibility.md +0 -0
- /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Flask CLI Commands
|
|
7
|
+
|
|
8
|
+
## Basic Commands
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
import click
|
|
12
|
+
from flask import Flask
|
|
13
|
+
from flask.cli import with_appcontext
|
|
14
|
+
|
|
15
|
+
app = Flask(__name__)
|
|
16
|
+
|
|
17
|
+
@app.cli.command("init-db")
|
|
18
|
+
@with_appcontext
|
|
19
|
+
def init_db_command():
|
|
20
|
+
"""Initialize the database."""
|
|
21
|
+
db.create_all()
|
|
22
|
+
click.echo("Database initialized.")
|
|
23
|
+
|
|
24
|
+
@app.cli.command("seed")
|
|
25
|
+
@with_appcontext
|
|
26
|
+
def seed_command():
|
|
27
|
+
"""Seed the database with sample data."""
|
|
28
|
+
from app.seeds import seed_all
|
|
29
|
+
seed_all()
|
|
30
|
+
click.echo("Database seeded.")
|
|
31
|
+
|
|
32
|
+
# Usage:
|
|
33
|
+
# flask init-db
|
|
34
|
+
# flask seed
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Commands with Arguments
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
@app.cli.command("create-user")
|
|
41
|
+
@click.argument("email")
|
|
42
|
+
@click.argument("name")
|
|
43
|
+
@click.option("--admin", is_flag=True, help="Make user an admin")
|
|
44
|
+
@with_appcontext
|
|
45
|
+
def create_user_command(email: str, name: str, admin: bool):
|
|
46
|
+
"""Create a new user."""
|
|
47
|
+
user = User(email=email, name=name, is_admin=admin)
|
|
48
|
+
db.session.add(user)
|
|
49
|
+
db.session.commit()
|
|
50
|
+
click.echo(f"Created user: {user.email} (admin={admin})")
|
|
51
|
+
|
|
52
|
+
# Usage:
|
|
53
|
+
# flask create-user john@example.com "John Doe"
|
|
54
|
+
# flask create-user admin@example.com "Admin" --admin
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Commands with Options
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
@app.cli.command("export-users")
|
|
61
|
+
@click.option("--format", type=click.Choice(["json", "csv"]), default="json")
|
|
62
|
+
@click.option("--output", "-o", type=click.Path(), default="users.json")
|
|
63
|
+
@click.option("--active-only", is_flag=True, help="Export only active users")
|
|
64
|
+
@with_appcontext
|
|
65
|
+
def export_users_command(format: str, output: str, active_only: bool):
|
|
66
|
+
"""Export users to a file."""
|
|
67
|
+
query = User.query
|
|
68
|
+
if active_only:
|
|
69
|
+
query = query.filter(User.is_active == True)
|
|
70
|
+
|
|
71
|
+
users = query.all()
|
|
72
|
+
|
|
73
|
+
if format == "json":
|
|
74
|
+
data = UserSchema(many=True).dump(users)
|
|
75
|
+
with open(output, "w") as f:
|
|
76
|
+
json.dump(data, f, indent=2)
|
|
77
|
+
elif format == "csv":
|
|
78
|
+
# CSV export logic
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
click.echo(f"Exported {len(users)} users to {output}")
|
|
82
|
+
|
|
83
|
+
# Usage:
|
|
84
|
+
# flask export-users --format csv -o users.csv --active-only
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Command Groups
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
@app.cli.group()
|
|
91
|
+
def user():
|
|
92
|
+
"""User management commands."""
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
@user.command("create")
|
|
96
|
+
@click.argument("email")
|
|
97
|
+
@click.argument("name")
|
|
98
|
+
@with_appcontext
|
|
99
|
+
def user_create(email: str, name: str):
|
|
100
|
+
"""Create a new user."""
|
|
101
|
+
user = User(email=email, name=name)
|
|
102
|
+
db.session.add(user)
|
|
103
|
+
db.session.commit()
|
|
104
|
+
click.echo(f"Created: {user.email}")
|
|
105
|
+
|
|
106
|
+
@user.command("delete")
|
|
107
|
+
@click.argument("email")
|
|
108
|
+
@click.confirmation_option(prompt="Are you sure?")
|
|
109
|
+
@with_appcontext
|
|
110
|
+
def user_delete(email: str):
|
|
111
|
+
"""Delete a user."""
|
|
112
|
+
user = User.query.filter_by(email=email).first()
|
|
113
|
+
if not user:
|
|
114
|
+
click.echo(f"User not found: {email}", err=True)
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
db.session.delete(user)
|
|
118
|
+
db.session.commit()
|
|
119
|
+
click.echo(f"Deleted: {email}")
|
|
120
|
+
|
|
121
|
+
@user.command("list")
|
|
122
|
+
@click.option("--limit", default=10)
|
|
123
|
+
@with_appcontext
|
|
124
|
+
def user_list(limit: int):
|
|
125
|
+
"""List users."""
|
|
126
|
+
users = User.query.limit(limit).all()
|
|
127
|
+
for user in users:
|
|
128
|
+
click.echo(f"{user.id}: {user.email} ({user.name})")
|
|
129
|
+
|
|
130
|
+
# Usage:
|
|
131
|
+
# flask user create john@example.com "John"
|
|
132
|
+
# flask user delete john@example.com
|
|
133
|
+
# flask user list --limit 20
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Blueprint Commands
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# In blueprint
|
|
140
|
+
from flask import Blueprint
|
|
141
|
+
|
|
142
|
+
users_bp = Blueprint("users", __name__, cli_group="users")
|
|
143
|
+
|
|
144
|
+
@users_bp.cli.command("sync")
|
|
145
|
+
@with_appcontext
|
|
146
|
+
def sync_users():
|
|
147
|
+
"""Sync users from external source."""
|
|
148
|
+
# Sync logic
|
|
149
|
+
click.echo("Users synced")
|
|
150
|
+
|
|
151
|
+
# Usage:
|
|
152
|
+
# flask users sync
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Progress Bars
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
@app.cli.command("migrate-data")
|
|
159
|
+
@with_appcontext
|
|
160
|
+
def migrate_data_command():
|
|
161
|
+
"""Migrate data with progress bar."""
|
|
162
|
+
records = OldModel.query.all()
|
|
163
|
+
|
|
164
|
+
with click.progressbar(records, label="Migrating") as bar:
|
|
165
|
+
for record in bar:
|
|
166
|
+
new_record = migrate_record(record)
|
|
167
|
+
db.session.add(new_record)
|
|
168
|
+
|
|
169
|
+
db.session.commit()
|
|
170
|
+
click.echo("Migration complete!")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Colored Output
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
@app.cli.command("check")
|
|
177
|
+
@with_appcontext
|
|
178
|
+
def check_command():
|
|
179
|
+
"""Check application health."""
|
|
180
|
+
# Database
|
|
181
|
+
try:
|
|
182
|
+
db.session.execute("SELECT 1")
|
|
183
|
+
click.secho("✓ Database: OK", fg="green")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
click.secho(f"✗ Database: {e}", fg="red")
|
|
186
|
+
|
|
187
|
+
# Redis
|
|
188
|
+
try:
|
|
189
|
+
redis_client.ping()
|
|
190
|
+
click.secho("✓ Redis: OK", fg="green")
|
|
191
|
+
except Exception as e:
|
|
192
|
+
click.secho(f"✗ Redis: {e}", fg="red")
|
|
193
|
+
|
|
194
|
+
# External API
|
|
195
|
+
try:
|
|
196
|
+
response = requests.get(f"{API_URL}/health", timeout=5)
|
|
197
|
+
response.raise_for_status()
|
|
198
|
+
click.secho("✓ External API: OK", fg="green")
|
|
199
|
+
except Exception as e:
|
|
200
|
+
click.secho(f"✗ External API: {e}", fg="yellow")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Interactive Prompts
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
@app.cli.command("setup")
|
|
207
|
+
def setup_command():
|
|
208
|
+
"""Interactive setup wizard."""
|
|
209
|
+
click.echo("Welcome to the setup wizard!")
|
|
210
|
+
|
|
211
|
+
# Text input
|
|
212
|
+
app_name = click.prompt("Application name", default="My App")
|
|
213
|
+
|
|
214
|
+
# Password input
|
|
215
|
+
secret = click.prompt("Secret key", hide_input=True)
|
|
216
|
+
|
|
217
|
+
# Choice
|
|
218
|
+
env = click.prompt(
|
|
219
|
+
"Environment",
|
|
220
|
+
type=click.Choice(["development", "staging", "production"]),
|
|
221
|
+
default="development",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Confirmation
|
|
225
|
+
if click.confirm("Save configuration?"):
|
|
226
|
+
save_config(app_name=app_name, secret=secret, env=env)
|
|
227
|
+
click.echo("Configuration saved!")
|
|
228
|
+
else:
|
|
229
|
+
click.echo("Aborted.")
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Async Commands (Flask 2.0+)
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
import asyncio
|
|
236
|
+
|
|
237
|
+
@app.cli.command("async-task")
|
|
238
|
+
@with_appcontext
|
|
239
|
+
def async_task_command():
|
|
240
|
+
"""Run async task."""
|
|
241
|
+
asyncio.run(run_async_task())
|
|
242
|
+
|
|
243
|
+
async def run_async_task():
|
|
244
|
+
async with aiohttp.ClientSession() as session:
|
|
245
|
+
async with session.get("https://api.example.com/data") as response:
|
|
246
|
+
data = await response.json()
|
|
247
|
+
click.echo(f"Fetched {len(data)} records")
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Custom CLI Script
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
# manage.py
|
|
254
|
+
import click
|
|
255
|
+
from app import create_app, db
|
|
256
|
+
|
|
257
|
+
app = create_app()
|
|
258
|
+
|
|
259
|
+
@click.group()
|
|
260
|
+
def cli():
|
|
261
|
+
"""Management script."""
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
@cli.command()
|
|
265
|
+
def runserver():
|
|
266
|
+
"""Run development server."""
|
|
267
|
+
app.run(debug=True)
|
|
268
|
+
|
|
269
|
+
@cli.command()
|
|
270
|
+
@click.option("--drop", is_flag=True, help="Drop tables first")
|
|
271
|
+
def initdb(drop: bool):
|
|
272
|
+
"""Initialize database."""
|
|
273
|
+
with app.app_context():
|
|
274
|
+
if drop:
|
|
275
|
+
db.drop_all()
|
|
276
|
+
db.create_all()
|
|
277
|
+
click.echo("Database initialized.")
|
|
278
|
+
|
|
279
|
+
if __name__ == "__main__":
|
|
280
|
+
cli()
|
|
281
|
+
|
|
282
|
+
# Usage:
|
|
283
|
+
# python manage.py runserver
|
|
284
|
+
# python manage.py initdb --drop
|
|
285
|
+
```
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Flask Configuration Patterns
|
|
7
|
+
|
|
8
|
+
## Class-Based Configuration
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
# config.py
|
|
12
|
+
import os
|
|
13
|
+
from datetime import timedelta
|
|
14
|
+
|
|
15
|
+
class Config:
|
|
16
|
+
"""Base configuration."""
|
|
17
|
+
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-key")
|
|
18
|
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
19
|
+
|
|
20
|
+
# JWT
|
|
21
|
+
JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY", SECRET_KEY)
|
|
22
|
+
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)
|
|
23
|
+
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30)
|
|
24
|
+
|
|
25
|
+
# Mail
|
|
26
|
+
MAIL_SERVER = os.environ.get("MAIL_SERVER", "localhost")
|
|
27
|
+
MAIL_PORT = int(os.environ.get("MAIL_PORT", 587))
|
|
28
|
+
MAIL_USE_TLS = True
|
|
29
|
+
|
|
30
|
+
class DevelopmentConfig(Config):
|
|
31
|
+
"""Development configuration."""
|
|
32
|
+
DEBUG = True
|
|
33
|
+
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
|
34
|
+
"DATABASE_URL",
|
|
35
|
+
"postgresql://localhost/app_dev"
|
|
36
|
+
)
|
|
37
|
+
SQLALCHEMY_ECHO = True # Log SQL queries
|
|
38
|
+
|
|
39
|
+
class TestingConfig(Config):
|
|
40
|
+
"""Testing configuration."""
|
|
41
|
+
TESTING = True
|
|
42
|
+
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
|
|
43
|
+
WTF_CSRF_ENABLED = False
|
|
44
|
+
|
|
45
|
+
class ProductionConfig(Config):
|
|
46
|
+
"""Production configuration."""
|
|
47
|
+
DEBUG = False
|
|
48
|
+
SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
|
|
49
|
+
|
|
50
|
+
# Security
|
|
51
|
+
SESSION_COOKIE_SECURE = True
|
|
52
|
+
SESSION_COOKIE_HTTPONLY = True
|
|
53
|
+
SESSION_COOKIE_SAMESITE = "Lax"
|
|
54
|
+
|
|
55
|
+
config = {
|
|
56
|
+
"development": DevelopmentConfig,
|
|
57
|
+
"testing": TestingConfig,
|
|
58
|
+
"production": ProductionConfig,
|
|
59
|
+
"default": DevelopmentConfig,
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Loading Configuration
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
# app/__init__.py
|
|
67
|
+
from flask import Flask
|
|
68
|
+
from config import config
|
|
69
|
+
|
|
70
|
+
def create_app(config_name: str = None) -> Flask:
|
|
71
|
+
if config_name is None:
|
|
72
|
+
config_name = os.environ.get("FLASK_CONFIG", "development")
|
|
73
|
+
|
|
74
|
+
app = Flask(__name__)
|
|
75
|
+
app.config.from_object(config[config_name])
|
|
76
|
+
|
|
77
|
+
# Load additional config from file
|
|
78
|
+
app.config.from_pyfile("config.py", silent=True)
|
|
79
|
+
|
|
80
|
+
# Load from environment variable pointing to file
|
|
81
|
+
app.config.from_envvar("APP_CONFIG_FILE", silent=True)
|
|
82
|
+
|
|
83
|
+
return app
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Environment Variables
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# .env (development)
|
|
90
|
+
FLASK_APP=app
|
|
91
|
+
FLASK_CONFIG=development
|
|
92
|
+
SECRET_KEY=your-secret-key
|
|
93
|
+
DATABASE_URL=postgresql://user:pass@localhost/app_dev
|
|
94
|
+
REDIS_URL=redis://localhost:6379/0
|
|
95
|
+
|
|
96
|
+
# Load with python-dotenv
|
|
97
|
+
from dotenv import load_dotenv
|
|
98
|
+
load_dotenv()
|
|
99
|
+
|
|
100
|
+
# Or in create_app
|
|
101
|
+
def create_app(config_name: str = None) -> Flask:
|
|
102
|
+
load_dotenv()
|
|
103
|
+
...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Pydantic Settings (Recommended)
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from pydantic_settings import BaseSettings
|
|
110
|
+
from functools import lru_cache
|
|
111
|
+
|
|
112
|
+
class Settings(BaseSettings):
|
|
113
|
+
# App
|
|
114
|
+
app_name: str = "My App"
|
|
115
|
+
debug: bool = False
|
|
116
|
+
secret_key: str
|
|
117
|
+
|
|
118
|
+
# Database
|
|
119
|
+
database_url: str
|
|
120
|
+
|
|
121
|
+
# Redis
|
|
122
|
+
redis_url: str = "redis://localhost:6379/0"
|
|
123
|
+
|
|
124
|
+
# JWT
|
|
125
|
+
jwt_secret_key: str | None = None
|
|
126
|
+
jwt_access_token_expires: int = 3600 # seconds
|
|
127
|
+
|
|
128
|
+
# Mail
|
|
129
|
+
mail_server: str = "localhost"
|
|
130
|
+
mail_port: int = 587
|
|
131
|
+
mail_username: str | None = None
|
|
132
|
+
mail_password: str | None = None
|
|
133
|
+
|
|
134
|
+
class Config:
|
|
135
|
+
env_file = ".env"
|
|
136
|
+
env_file_encoding = "utf-8"
|
|
137
|
+
|
|
138
|
+
@lru_cache
|
|
139
|
+
def get_settings() -> Settings:
|
|
140
|
+
return Settings()
|
|
141
|
+
|
|
142
|
+
settings = get_settings()
|
|
143
|
+
|
|
144
|
+
# Usage in app
|
|
145
|
+
def create_app() -> Flask:
|
|
146
|
+
app = Flask(__name__)
|
|
147
|
+
|
|
148
|
+
app.config["SECRET_KEY"] = settings.secret_key
|
|
149
|
+
app.config["SQLALCHEMY_DATABASE_URI"] = settings.database_url
|
|
150
|
+
app.config["DEBUG"] = settings.debug
|
|
151
|
+
|
|
152
|
+
return app
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Configuration Validation
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
def validate_config(app: Flask):
|
|
159
|
+
"""Validate required configuration."""
|
|
160
|
+
required = [
|
|
161
|
+
"SECRET_KEY",
|
|
162
|
+
"SQLALCHEMY_DATABASE_URI",
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
missing = [key for key in required if not app.config.get(key)]
|
|
166
|
+
|
|
167
|
+
if missing:
|
|
168
|
+
raise RuntimeError(f"Missing required config: {', '.join(missing)}")
|
|
169
|
+
|
|
170
|
+
# Validate SECRET_KEY strength in production
|
|
171
|
+
if not app.debug:
|
|
172
|
+
if len(app.config["SECRET_KEY"]) < 32:
|
|
173
|
+
raise RuntimeError("SECRET_KEY must be at least 32 characters in production")
|
|
174
|
+
|
|
175
|
+
def create_app(config_name: str = None) -> Flask:
|
|
176
|
+
app = Flask(__name__)
|
|
177
|
+
app.config.from_object(config[config_name])
|
|
178
|
+
validate_config(app)
|
|
179
|
+
return app
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Instance Configuration
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# instance/config.py (not in version control)
|
|
186
|
+
SECRET_KEY = "your-production-secret"
|
|
187
|
+
SQLALCHEMY_DATABASE_URI = "postgresql://prod-db/app"
|
|
188
|
+
|
|
189
|
+
# Load instance config
|
|
190
|
+
app = Flask(__name__, instance_relative_config=True)
|
|
191
|
+
app.config.from_pyfile("config.py", silent=True)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Configuration by Feature
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
class Config:
|
|
198
|
+
# Core
|
|
199
|
+
SECRET_KEY = os.environ["SECRET_KEY"]
|
|
200
|
+
|
|
201
|
+
# Database
|
|
202
|
+
SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
|
|
203
|
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
204
|
+
SQLALCHEMY_ENGINE_OPTIONS = {
|
|
205
|
+
"pool_size": 10,
|
|
206
|
+
"pool_recycle": 3600,
|
|
207
|
+
"pool_pre_ping": True,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Cache
|
|
211
|
+
CACHE_TYPE = "redis"
|
|
212
|
+
CACHE_REDIS_URL = os.environ.get("REDIS_URL")
|
|
213
|
+
CACHE_DEFAULT_TIMEOUT = 300
|
|
214
|
+
|
|
215
|
+
# Session
|
|
216
|
+
SESSION_TYPE = "redis"
|
|
217
|
+
SESSION_REDIS = redis.from_url(os.environ.get("REDIS_URL"))
|
|
218
|
+
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
|
|
219
|
+
|
|
220
|
+
# Security
|
|
221
|
+
SESSION_COOKIE_SECURE = True
|
|
222
|
+
SESSION_COOKIE_HTTPONLY = True
|
|
223
|
+
SESSION_COOKIE_SAMESITE = "Lax"
|
|
224
|
+
|
|
225
|
+
# CORS
|
|
226
|
+
CORS_ORIGINS = os.environ.get("CORS_ORIGINS", "").split(",")
|
|
227
|
+
|
|
228
|
+
# Uploads
|
|
229
|
+
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
|
|
230
|
+
UPLOAD_FOLDER = os.environ.get("UPLOAD_FOLDER", "/tmp/uploads")
|
|
231
|
+
|
|
232
|
+
# Logging
|
|
233
|
+
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
|
|
234
|
+
LOG_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Runtime Configuration Access
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
from flask import current_app
|
|
241
|
+
|
|
242
|
+
@users_bp.route("/config-example")
|
|
243
|
+
def config_example():
|
|
244
|
+
# Access config in routes
|
|
245
|
+
debug = current_app.config["DEBUG"]
|
|
246
|
+
app_name = current_app.config.get("APP_NAME", "Default")
|
|
247
|
+
|
|
248
|
+
return jsonify({
|
|
249
|
+
"debug": debug,
|
|
250
|
+
"app_name": app_name,
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
# In services
|
|
254
|
+
class EmailService:
|
|
255
|
+
def __init__(self):
|
|
256
|
+
self.server = current_app.config["MAIL_SERVER"]
|
|
257
|
+
self.port = current_app.config["MAIL_PORT"]
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Configuration for Extensions
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
def configure_extensions(app: Flask):
|
|
264
|
+
"""Configure Flask extensions."""
|
|
265
|
+
# SQLAlchemy
|
|
266
|
+
app.config.setdefault("SQLALCHEMY_TRACK_MODIFICATIONS", False)
|
|
267
|
+
|
|
268
|
+
# JWT
|
|
269
|
+
app.config.setdefault("JWT_TOKEN_LOCATION", ["headers"])
|
|
270
|
+
app.config.setdefault("JWT_HEADER_NAME", "Authorization")
|
|
271
|
+
app.config.setdefault("JWT_HEADER_TYPE", "Bearer")
|
|
272
|
+
|
|
273
|
+
# CORS
|
|
274
|
+
app.config.setdefault("CORS_SUPPORTS_CREDENTIALS", True)
|
|
275
|
+
|
|
276
|
+
def create_app(config_name: str = None) -> Flask:
|
|
277
|
+
app = Flask(__name__)
|
|
278
|
+
app.config.from_object(config[config_name])
|
|
279
|
+
configure_extensions(app)
|
|
280
|
+
return app
|
|
281
|
+
```
|