@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,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
|
+
```
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Flask Context Management
|
|
7
|
+
|
|
8
|
+
## Application Context
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from flask import current_app, g
|
|
12
|
+
|
|
13
|
+
# GOOD - use application context
|
|
14
|
+
with app.app_context():
|
|
15
|
+
db.create_all()
|
|
16
|
+
current_app.logger.info("Database initialized")
|
|
17
|
+
|
|
18
|
+
# In CLI commands
|
|
19
|
+
@app.cli.command("init-db")
|
|
20
|
+
def init_db():
|
|
21
|
+
# Already in app context
|
|
22
|
+
db.create_all()
|
|
23
|
+
click.echo("Database initialized")
|
|
24
|
+
|
|
25
|
+
# BAD - access outside context
|
|
26
|
+
db.create_all() # RuntimeError: Working outside of application context
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Request Context
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from flask import request, session, g
|
|
33
|
+
|
|
34
|
+
# Automatically pushed during requests
|
|
35
|
+
@app.route("/")
|
|
36
|
+
def index():
|
|
37
|
+
user_agent = request.headers.get("User-Agent")
|
|
38
|
+
user_id = session.get("user_id")
|
|
39
|
+
return f"UA: {user_agent}"
|
|
40
|
+
|
|
41
|
+
# Test request context
|
|
42
|
+
with app.test_request_context("/users?page=2"):
|
|
43
|
+
assert request.path == "/users"
|
|
44
|
+
assert request.args["page"] == "2"
|
|
45
|
+
url = url_for("users.list_users")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## The `g` Object
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from flask import g
|
|
52
|
+
|
|
53
|
+
# GOOD - store per-request data in g
|
|
54
|
+
@app.before_request
|
|
55
|
+
def load_user():
|
|
56
|
+
user_id = session.get("user_id")
|
|
57
|
+
if user_id:
|
|
58
|
+
g.user = User.query.get(user_id)
|
|
59
|
+
else:
|
|
60
|
+
g.user = None
|
|
61
|
+
|
|
62
|
+
@app.route("/profile")
|
|
63
|
+
def profile():
|
|
64
|
+
if g.user is None:
|
|
65
|
+
return redirect(url_for("auth.login"))
|
|
66
|
+
return render_template("profile.html", user=g.user)
|
|
67
|
+
|
|
68
|
+
# GOOD - lazy loading with g
|
|
69
|
+
def get_db():
|
|
70
|
+
if "db" not in g:
|
|
71
|
+
g.db = connect_to_database()
|
|
72
|
+
return g.db
|
|
73
|
+
|
|
74
|
+
@app.teardown_appcontext
|
|
75
|
+
def close_db(exception):
|
|
76
|
+
db = g.pop("db", None)
|
|
77
|
+
if db is not None:
|
|
78
|
+
db.close()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Request Hooks
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# Before first request (deprecated in Flask 2.3+)
|
|
85
|
+
# Use app.before_request or lifespan pattern instead
|
|
86
|
+
|
|
87
|
+
@app.before_request
|
|
88
|
+
def before_every_request():
|
|
89
|
+
"""Run before every request."""
|
|
90
|
+
g.start_time = time.time()
|
|
91
|
+
g.request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
|
|
92
|
+
|
|
93
|
+
@app.after_request
|
|
94
|
+
def after_every_request(response):
|
|
95
|
+
"""Run after every request (even on error)."""
|
|
96
|
+
duration = time.time() - g.start_time
|
|
97
|
+
response.headers["X-Request-ID"] = g.request_id
|
|
98
|
+
response.headers["X-Response-Time"] = f"{duration:.4f}s"
|
|
99
|
+
return response
|
|
100
|
+
|
|
101
|
+
@app.teardown_request
|
|
102
|
+
def teardown_request(exception):
|
|
103
|
+
"""Run at the end of request, for cleanup."""
|
|
104
|
+
if exception:
|
|
105
|
+
app.logger.error(f"Request failed: {exception}")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Blueprint-Specific Hooks
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
users_bp = Blueprint("users", __name__)
|
|
112
|
+
|
|
113
|
+
@users_bp.before_request
|
|
114
|
+
def before_user_request():
|
|
115
|
+
"""Only runs for requests to this blueprint."""
|
|
116
|
+
g.service = UserService(db.session)
|
|
117
|
+
|
|
118
|
+
@users_bp.after_request
|
|
119
|
+
def after_user_request(response):
|
|
120
|
+
"""Only runs for requests to this blueprint."""
|
|
121
|
+
return response
|
|
122
|
+
|
|
123
|
+
# App-wide hooks still run for blueprint requests
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Context Locals with werkzeug
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from werkzeug.local import LocalStack, LocalProxy
|
|
130
|
+
|
|
131
|
+
# Custom context local
|
|
132
|
+
_request_ctx_stack = LocalStack()
|
|
133
|
+
|
|
134
|
+
def get_current_request_id():
|
|
135
|
+
ctx = _request_ctx_stack.top
|
|
136
|
+
if ctx is not None:
|
|
137
|
+
return ctx.request_id
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
request_id = LocalProxy(get_current_request_id)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Async Context (Flask 2.0+)
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from flask import Flask
|
|
147
|
+
import asyncio
|
|
148
|
+
|
|
149
|
+
app = Flask(__name__)
|
|
150
|
+
|
|
151
|
+
@app.route("/async")
|
|
152
|
+
async def async_route():
|
|
153
|
+
# Can use async/await in routes
|
|
154
|
+
result = await some_async_operation()
|
|
155
|
+
return jsonify(result)
|
|
156
|
+
|
|
157
|
+
# Context is preserved in async functions
|
|
158
|
+
@app.route("/async-context")
|
|
159
|
+
async def async_with_context():
|
|
160
|
+
# current_app, g, request all work
|
|
161
|
+
app.logger.info("Async route called")
|
|
162
|
+
g.async_data = await fetch_data()
|
|
163
|
+
return jsonify(g.async_data)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Testing Contexts
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
import pytest
|
|
170
|
+
|
|
171
|
+
@pytest.fixture
|
|
172
|
+
def app():
|
|
173
|
+
app = create_app("testing")
|
|
174
|
+
return app
|
|
175
|
+
|
|
176
|
+
@pytest.fixture
|
|
177
|
+
def client(app):
|
|
178
|
+
return app.test_client()
|
|
179
|
+
|
|
180
|
+
@pytest.fixture
|
|
181
|
+
def app_context(app):
|
|
182
|
+
with app.app_context():
|
|
183
|
+
yield
|
|
184
|
+
|
|
185
|
+
@pytest.fixture
|
|
186
|
+
def request_context(app):
|
|
187
|
+
with app.test_request_context():
|
|
188
|
+
yield
|
|
189
|
+
|
|
190
|
+
def test_with_app_context(app_context):
|
|
191
|
+
# current_app is available
|
|
192
|
+
assert current_app.config["TESTING"]
|
|
193
|
+
|
|
194
|
+
def test_with_request_context(request_context):
|
|
195
|
+
# request, g are available
|
|
196
|
+
g.user = User(name="Test")
|
|
197
|
+
assert g.user.name == "Test"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Pushing Contexts Manually
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
# For background tasks, CLI commands, etc.
|
|
204
|
+
def background_task(app, user_id):
|
|
205
|
+
with app.app_context():
|
|
206
|
+
user = User.query.get(user_id)
|
|
207
|
+
send_email(user)
|
|
208
|
+
|
|
209
|
+
# Thread-safe context pushing
|
|
210
|
+
from threading import Thread
|
|
211
|
+
|
|
212
|
+
def run_in_thread(app):
|
|
213
|
+
def wrapper():
|
|
214
|
+
with app.app_context():
|
|
215
|
+
do_work()
|
|
216
|
+
|
|
217
|
+
thread = Thread(target=wrapper)
|
|
218
|
+
thread.start()
|
|
219
|
+
return thread
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Context Processor
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
@app.context_processor
|
|
226
|
+
def inject_globals():
|
|
227
|
+
"""Inject variables into all templates."""
|
|
228
|
+
return {
|
|
229
|
+
"current_year": datetime.now().year,
|
|
230
|
+
"app_name": current_app.config["APP_NAME"],
|
|
231
|
+
"user": g.get("user"),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# In templates
|
|
235
|
+
# {{ current_year }}
|
|
236
|
+
# {{ app_name }}
|
|
237
|
+
# {% if user %}Hello {{ user.name }}{% endif %}
|
|
238
|
+
```
|