@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.
Files changed (133) hide show
  1. package/README.md +270 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
  4. package/configs/_shared/.claude/rules/conventions/git.md +265 -0
  5. package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
  6. package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
  7. package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
  8. package/configs/_shared/.claude/rules/devops/docker.md +275 -0
  9. package/configs/_shared/.claude/rules/devops/nx.md +194 -0
  10. package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
  11. package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
  12. package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
  13. package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
  14. package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
  15. package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
  16. package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
  17. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
  18. package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
  19. package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
  20. package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
  21. package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
  22. package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
  23. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
  24. package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
  25. package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
  26. package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
  27. package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
  28. package/configs/_shared/.claude/rules/quality/logging.md +45 -0
  29. package/configs/_shared/.claude/rules/quality/observability.md +240 -0
  30. package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
  31. package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
  32. package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
  33. package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
  34. package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
  35. package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  36. package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  37. package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  38. package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
  39. package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
  40. package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
  41. package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
  42. package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
  43. package/configs/_shared/CLAUDE.md +52 -149
  44. package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
  45. package/configs/angular/.claude/rules/core/resource.md +285 -0
  46. package/configs/angular/.claude/rules/core/signals.md +323 -0
  47. package/configs/angular/.claude/rules/http.md +338 -0
  48. package/configs/angular/.claude/rules/routing.md +291 -0
  49. package/configs/angular/.claude/rules/ssr.md +312 -0
  50. package/configs/angular/.claude/rules/state/signal-store.md +408 -0
  51. package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
  52. package/configs/angular/.claude/rules/testing.md +7 -7
  53. package/configs/angular/.claude/rules/ui/aria.md +422 -0
  54. package/configs/angular/.claude/rules/ui/forms.md +424 -0
  55. package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
  56. package/configs/angular/.claude/settings.json +1 -0
  57. package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
  58. package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
  59. package/configs/angular/CLAUDE.md +24 -216
  60. package/configs/dotnet/.claude/rules/background-services.md +552 -0
  61. package/configs/dotnet/.claude/rules/configuration.md +426 -0
  62. package/configs/dotnet/.claude/rules/ddd.md +447 -0
  63. package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/.claude/rules/mediatr.md +320 -0
  65. package/configs/dotnet/.claude/rules/middleware.md +489 -0
  66. package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/.claude/rules/validation.md +388 -0
  68. package/configs/dotnet/.claude/settings.json +21 -3
  69. package/configs/dotnet/CLAUDE.md +53 -286
  70. package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/.claude/rules/dependencies.md +170 -0
  72. package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
  73. package/configs/fastapi/.claude/rules/lifespan.md +274 -0
  74. package/configs/fastapi/.claude/rules/middleware.md +229 -0
  75. package/configs/fastapi/.claude/rules/pydantic.md +433 -0
  76. package/configs/fastapi/.claude/rules/responses.md +251 -0
  77. package/configs/fastapi/.claude/rules/routers.md +202 -0
  78. package/configs/fastapi/.claude/rules/security.md +222 -0
  79. package/configs/fastapi/.claude/rules/testing.md +251 -0
  80. package/configs/fastapi/.claude/rules/websockets.md +298 -0
  81. package/configs/fastapi/.claude/settings.json +33 -0
  82. package/configs/fastapi/CLAUDE.md +144 -0
  83. package/configs/flask/.claude/rules/blueprints.md +208 -0
  84. package/configs/flask/.claude/rules/cli.md +285 -0
  85. package/configs/flask/.claude/rules/configuration.md +281 -0
  86. package/configs/flask/.claude/rules/context.md +238 -0
  87. package/configs/flask/.claude/rules/error-handlers.md +278 -0
  88. package/configs/flask/.claude/rules/extensions.md +278 -0
  89. package/configs/flask/.claude/rules/flask.md +171 -0
  90. package/configs/flask/.claude/rules/marshmallow.md +206 -0
  91. package/configs/flask/.claude/rules/security.md +267 -0
  92. package/configs/flask/.claude/rules/testing.md +284 -0
  93. package/configs/flask/.claude/settings.json +33 -0
  94. package/configs/flask/CLAUDE.md +166 -0
  95. package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/.claude/rules/filters.md +376 -0
  97. package/configs/nestjs/.claude/rules/interceptors.md +317 -0
  98. package/configs/nestjs/.claude/rules/middleware.md +321 -0
  99. package/configs/nestjs/.claude/rules/modules.md +26 -0
  100. package/configs/nestjs/.claude/rules/pipes.md +351 -0
  101. package/configs/nestjs/.claude/rules/websockets.md +451 -0
  102. package/configs/nestjs/.claude/settings.json +16 -2
  103. package/configs/nestjs/CLAUDE.md +57 -215
  104. package/configs/nextjs/.claude/rules/api-routes.md +358 -0
  105. package/configs/nextjs/.claude/rules/authentication.md +355 -0
  106. package/configs/nextjs/.claude/rules/components.md +52 -0
  107. package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/.claude/rules/database.md +400 -0
  109. package/configs/nextjs/.claude/rules/middleware.md +303 -0
  110. package/configs/nextjs/.claude/rules/routing.md +324 -0
  111. package/configs/nextjs/.claude/rules/seo.md +350 -0
  112. package/configs/nextjs/.claude/rules/server-actions.md +353 -0
  113. package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
  114. package/configs/nextjs/.claude/settings.json +5 -0
  115. package/configs/nextjs/CLAUDE.md +69 -331
  116. package/package.json +23 -9
  117. package/src/cli.js +220 -0
  118. package/src/config.js +29 -0
  119. package/src/index.js +13 -0
  120. package/src/installer.js +361 -0
  121. package/src/merge.js +116 -0
  122. package/src/tech-config.json +29 -0
  123. package/src/utils.js +96 -0
  124. package/configs/python/.claude/rules/flask.md +0 -332
  125. package/configs/python/.claude/settings.json +0 -18
  126. package/configs/python/CLAUDE.md +0 -273
  127. package/src/install.js +0 -315
  128. /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
  129. /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
  130. /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
  131. /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
  132. /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
  133. /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
package/src/merge.js ADDED
@@ -0,0 +1,116 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { log, backupFile } = require('./utils');
4
+ const { VERSION } = require('./config');
5
+
6
+ function mergeClaudeMd(targetPath, sourcePath, isFirst, options = {}) {
7
+ const { dryRun = false, backup = false, targetDir } = options;
8
+ const content = fs.readFileSync(sourcePath, 'utf8');
9
+ const exists = fs.existsSync(targetPath);
10
+
11
+ if (dryRun) {
12
+ return { type: exists ? 'merge' : 'create', path: 'CLAUDE.md' };
13
+ }
14
+
15
+ if (exists && backup && isFirst) {
16
+ backupFile(targetPath, targetDir);
17
+ }
18
+
19
+ if (isFirst) {
20
+ fs.writeFileSync(targetPath, content);
21
+ } else {
22
+ const existing = fs.readFileSync(targetPath, 'utf8');
23
+ fs.writeFileSync(targetPath, `${existing}\n\n---\n\n${content}`);
24
+ }
25
+
26
+ return { type: exists ? 'merge' : 'create', path: 'CLAUDE.md' };
27
+ }
28
+
29
+ function mergeSettingsJson(targetPath, sourcePath, options = {}) {
30
+ const { dryRun = false, backup = false, targetDir } = options;
31
+ const exists = fs.existsSync(targetPath);
32
+
33
+ if (dryRun) {
34
+ return { type: exists ? 'merge' : 'create', path: '.claude/settings.json' };
35
+ }
36
+
37
+ if (!exists) {
38
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
39
+ fs.copyFileSync(sourcePath, targetPath);
40
+ return { type: 'create', path: '.claude/settings.json' };
41
+ }
42
+
43
+ if (backup) {
44
+ backupFile(targetPath, targetDir);
45
+ }
46
+
47
+ try {
48
+ const existing = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
49
+ const incoming = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
50
+
51
+ const merged = {
52
+ ...existing,
53
+ permissions: {
54
+ allow: [
55
+ ...new Set([
56
+ ...(existing.permissions?.allow || []),
57
+ ...(incoming.permissions?.allow || []),
58
+ ]),
59
+ ],
60
+ deny: [
61
+ ...new Set([
62
+ ...(existing.permissions?.deny || []),
63
+ ...(incoming.permissions?.deny || []),
64
+ ]),
65
+ ],
66
+ },
67
+ };
68
+
69
+ if (incoming.env) {
70
+ merged.env = { ...(existing.env || {}), ...incoming.env };
71
+ }
72
+
73
+ fs.writeFileSync(targetPath, JSON.stringify(merged, null, 2) + '\n');
74
+ return { type: 'merge', path: '.claude/settings.json' };
75
+ } catch (_e) {
76
+ log.warning('Could not merge settings.json, overwriting');
77
+ fs.copyFileSync(sourcePath, targetPath);
78
+ return { type: 'overwrite', path: '.claude/settings.json' };
79
+ }
80
+ }
81
+
82
+ function getManifestPath(targetDir) {
83
+ return path.join(targetDir, '.claude', '.ai-rules.json');
84
+ }
85
+
86
+ function readManifest(targetDir) {
87
+ const manifestPath = getManifestPath(targetDir);
88
+ if (!fs.existsSync(manifestPath)) return null;
89
+
90
+ try {
91
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ function writeManifest(targetDir, data, dryRun = false) {
98
+ if (dryRun) return;
99
+
100
+ const manifestPath = getManifestPath(targetDir);
101
+ const manifest = {
102
+ version: VERSION,
103
+ installedAt: new Date().toISOString(),
104
+ ...data,
105
+ };
106
+
107
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
108
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
109
+ }
110
+
111
+ module.exports = {
112
+ mergeClaudeMd,
113
+ mergeSettingsJson,
114
+ readManifest,
115
+ writeManifest,
116
+ };
@@ -0,0 +1,29 @@
1
+ {
2
+ "technologies": {
3
+ "angular": {
4
+ "includeRules": ["lang/typescript", "domain/frontend"]
5
+ },
6
+ "nextjs": {
7
+ "includeRules": ["lang/typescript", "domain/frontend"]
8
+ },
9
+ "nestjs": {
10
+ "includeRules": ["lang/typescript", "domain/backend"]
11
+ },
12
+ "dotnet": {
13
+ "includeRules": ["lang/csharp", "domain/backend"]
14
+ },
15
+ "fastapi": {
16
+ "includeRules": ["lang/python", "domain/backend"]
17
+ },
18
+ "flask": {
19
+ "includeRules": ["lang/python", "domain/backend"]
20
+ }
21
+ },
22
+ "ruleCategories": {
23
+ "lang/typescript": "TypeScript best practices",
24
+ "lang/python": "Python best practices",
25
+ "lang/csharp": "C# language best practices",
26
+ "domain/frontend": "Accessibility, UI patterns",
27
+ "domain/backend": "API design, server patterns"
28
+ }
29
+ }
package/src/utils.js ADDED
@@ -0,0 +1,96 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const colors = {
5
+ red: (text) => `\x1b[31m${text}\x1b[0m`,
6
+ green: (text) => `\x1b[32m${text}\x1b[0m`,
7
+ blue: (text) => `\x1b[34m${text}\x1b[0m`,
8
+ yellow: (text) => `\x1b[33m${text}\x1b[0m`,
9
+ cyan: (text) => `\x1b[36m${text}\x1b[0m`,
10
+ dim: (text) => `\x1b[2m${text}\x1b[0m`,
11
+ bold: (text) => `\x1b[1m${text}\x1b[0m`,
12
+ };
13
+
14
+ const log = {
15
+ info: (msg) => console.log(`${colors.blue('ℹ')} ${msg}`),
16
+ success: (msg) => console.log(`${colors.green('✓')} ${msg}`),
17
+ warning: (msg) => console.log(`${colors.yellow('⚠')} ${msg}`),
18
+ error: (msg) => console.log(`${colors.red('✗')} ${msg}`),
19
+ dry: (msg) => console.log(`${colors.cyan('○')} ${msg}`),
20
+ };
21
+
22
+ function getFilesRecursive(dir, baseDir = dir) {
23
+ const files = [];
24
+ if (!fs.existsSync(dir)) return files;
25
+
26
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ const fullPath = path.join(dir, entry.name);
29
+ if (entry.isDirectory()) {
30
+ files.push(...getFilesRecursive(fullPath, baseDir));
31
+ } else {
32
+ files.push(path.relative(baseDir, fullPath));
33
+ }
34
+ }
35
+ return files;
36
+ }
37
+
38
+ function copyDirRecursive(src, dest, options = {}) {
39
+ const { dryRun = false, backup = false, targetDir = dest } = options;
40
+ const operations = [];
41
+
42
+ if (!fs.existsSync(src)) return operations;
43
+
44
+ const entries = fs.readdirSync(src, { withFileTypes: true });
45
+
46
+ for (const entry of entries) {
47
+ const srcPath = path.join(src, entry.name);
48
+ const destPath = path.join(dest, entry.name);
49
+
50
+ if (entry.isDirectory()) {
51
+ operations.push(...copyDirRecursive(srcPath, destPath, options));
52
+ } else {
53
+ const exists = fs.existsSync(destPath);
54
+ const relativePath = path.relative(targetDir, destPath);
55
+
56
+ if (dryRun) {
57
+ operations.push({
58
+ type: exists ? 'overwrite' : 'create',
59
+ path: relativePath,
60
+ });
61
+ } else {
62
+ if (exists && backup) {
63
+ backupFile(destPath, targetDir);
64
+ }
65
+ fs.mkdirSync(dest, { recursive: true });
66
+ fs.copyFileSync(srcPath, destPath);
67
+ operations.push({
68
+ type: exists ? 'overwrite' : 'create',
69
+ path: relativePath,
70
+ });
71
+ }
72
+ }
73
+ }
74
+
75
+ return operations;
76
+ }
77
+
78
+ function backupFile(filePath, targetDir) {
79
+ const backupDir = path.join(targetDir, '.claude', 'backups');
80
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
81
+ const relativePath = path.relative(targetDir, filePath);
82
+ const backupPath = path.join(backupDir, `${relativePath}.${timestamp}`);
83
+
84
+ fs.mkdirSync(path.dirname(backupPath), { recursive: true });
85
+ fs.copyFileSync(filePath, backupPath);
86
+
87
+ return backupPath;
88
+ }
89
+
90
+ module.exports = {
91
+ colors,
92
+ log,
93
+ getFilesRecursive,
94
+ copyDirRecursive,
95
+ backupFile,
96
+ };
@@ -1,332 +0,0 @@
1
- ---
2
- paths:
3
- - "**/*.py"
4
- ---
5
-
6
- # Flask Rules
7
-
8
- ## Application Factory
9
-
10
- ```python
11
- from flask import Flask
12
- from flask_sqlalchemy import SQLAlchemy
13
- from flask_migrate import Migrate
14
-
15
- db = SQLAlchemy()
16
- migrate = Migrate()
17
-
18
- def create_app(config_name: str = "development") -> Flask:
19
- app = Flask(__name__)
20
- app.config.from_object(config[config_name])
21
-
22
- # Initialize extensions
23
- db.init_app(app)
24
- migrate.init_app(app, db)
25
-
26
- # Register blueprints
27
- from app.users import bp as users_bp
28
- from app.auth import bp as auth_bp
29
-
30
- app.register_blueprint(users_bp, url_prefix="/api/v1/users")
31
- app.register_blueprint(auth_bp, url_prefix="/api/v1/auth")
32
-
33
- # Register error handlers
34
- register_error_handlers(app)
35
-
36
- return app
37
- ```
38
-
39
- ## Blueprints
40
-
41
- ```python
42
- from flask import Blueprint, request, jsonify
43
- from http import HTTPStatus
44
-
45
- bp = Blueprint("users", __name__)
46
-
47
- @bp.get("/")
48
- def list_users():
49
- """List all users with pagination."""
50
- page = request.args.get("page", 1, type=int)
51
- per_page = request.args.get("per_page", 20, type=int)
52
-
53
- pagination = User.query.paginate(page=page, per_page=per_page)
54
-
55
- return jsonify({
56
- "items": [user.to_dict() for user in pagination.items],
57
- "total": pagination.total,
58
- "page": pagination.page,
59
- "pages": pagination.pages,
60
- })
61
-
62
-
63
- @bp.get("/<int:user_id>")
64
- def get_user(user_id: int):
65
- """Get a user by ID."""
66
- user = db.get_or_404(User, user_id)
67
- return jsonify(user.to_dict())
68
-
69
-
70
- @bp.post("/")
71
- def create_user():
72
- """Create a new user."""
73
- data = request.get_json()
74
-
75
- # Validation
76
- errors = validate_user_data(data)
77
- if errors:
78
- return jsonify({"errors": errors}), HTTPStatus.BAD_REQUEST
79
-
80
- user = User(
81
- email=data["email"],
82
- name=data["name"],
83
- )
84
- user.set_password(data["password"])
85
-
86
- db.session.add(user)
87
- db.session.commit()
88
-
89
- return jsonify(user.to_dict()), HTTPStatus.CREATED
90
-
91
-
92
- @bp.put("/<int:user_id>")
93
- def update_user(user_id: int):
94
- """Update a user."""
95
- user = db.get_or_404(User, user_id)
96
- data = request.get_json()
97
-
98
- if "email" in data:
99
- user.email = data["email"]
100
- if "name" in data:
101
- user.name = data["name"]
102
-
103
- db.session.commit()
104
- return jsonify(user.to_dict())
105
-
106
-
107
- @bp.delete("/<int:user_id>")
108
- def delete_user(user_id: int):
109
- """Delete a user."""
110
- user = db.get_or_404(User, user_id)
111
- db.session.delete(user)
112
- db.session.commit()
113
- return "", HTTPStatus.NO_CONTENT
114
- ```
115
-
116
- ## Models with Flask-SQLAlchemy
117
-
118
- ```python
119
- from datetime import datetime
120
- from werkzeug.security import generate_password_hash, check_password_hash
121
- from app import db
122
-
123
- class TimestampMixin:
124
- created_at = db.Column(db.DateTime, default=datetime.utcnow)
125
- updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
126
-
127
- class User(TimestampMixin, db.Model):
128
- __tablename__ = "users"
129
-
130
- id = db.Column(db.Integer, primary_key=True)
131
- email = db.Column(db.String(256), unique=True, nullable=False, index=True)
132
- password_hash = db.Column(db.String(256), nullable=False)
133
- name = db.Column(db.String(100))
134
- role = db.Column(db.String(50), default="user")
135
-
136
- # Relationships
137
- posts = db.relationship("Post", back_populates="author", lazy="dynamic")
138
-
139
- def set_password(self, password: str) -> None:
140
- self.password_hash = generate_password_hash(password)
141
-
142
- def check_password(self, password: str) -> bool:
143
- return check_password_hash(self.password_hash, password)
144
-
145
- def to_dict(self) -> dict:
146
- return {
147
- "id": self.id,
148
- "email": self.email,
149
- "name": self.name,
150
- "role": self.role,
151
- "created_at": self.created_at.isoformat(),
152
- }
153
- ```
154
-
155
- ## Error Handling
156
-
157
- ```python
158
- from flask import jsonify
159
- from werkzeug.exceptions import HTTPException
160
-
161
- def register_error_handlers(app: Flask) -> None:
162
-
163
- @app.errorhandler(HTTPException)
164
- def handle_http_exception(error: HTTPException):
165
- return jsonify({
166
- "error": error.name,
167
- "message": error.description,
168
- }), error.code
169
-
170
- @app.errorhandler(ValidationError)
171
- def handle_validation_error(error: ValidationError):
172
- return jsonify({
173
- "error": "Validation Error",
174
- "message": str(error),
175
- "details": error.errors,
176
- }), HTTPStatus.BAD_REQUEST
177
-
178
- @app.errorhandler(Exception)
179
- def handle_generic_exception(error: Exception):
180
- app.logger.exception("Unhandled exception")
181
- return jsonify({
182
- "error": "Internal Server Error",
183
- "message": "An unexpected error occurred",
184
- }), HTTPStatus.INTERNAL_SERVER_ERROR
185
- ```
186
-
187
- ## Authentication with Flask-JWT-Extended
188
-
189
- ```python
190
- from flask_jwt_extended import (
191
- JWTManager,
192
- create_access_token,
193
- create_refresh_token,
194
- jwt_required,
195
- get_jwt_identity,
196
- current_user,
197
- )
198
-
199
- jwt = JWTManager()
200
-
201
- @jwt.user_identity_loader
202
- def user_identity_lookup(user: User) -> int:
203
- return user.id
204
-
205
- @jwt.user_lookup_loader
206
- def user_lookup_callback(_jwt_header, jwt_data) -> User | None:
207
- identity = jwt_data["sub"]
208
- return User.query.get(identity)
209
-
210
- # Auth blueprint
211
- auth_bp = Blueprint("auth", __name__)
212
-
213
- @auth_bp.post("/login")
214
- def login():
215
- data = request.get_json()
216
- user = User.query.filter_by(email=data["email"]).first()
217
-
218
- if not user or not user.check_password(data["password"]):
219
- return jsonify({"error": "Invalid credentials"}), HTTPStatus.UNAUTHORIZED
220
-
221
- return jsonify({
222
- "access_token": create_access_token(identity=user),
223
- "refresh_token": create_refresh_token(identity=user),
224
- })
225
-
226
- @auth_bp.post("/refresh")
227
- @jwt_required(refresh=True)
228
- def refresh():
229
- user = current_user
230
- return jsonify({
231
- "access_token": create_access_token(identity=user),
232
- })
233
-
234
- # Protected route
235
- @bp.get("/me")
236
- @jwt_required()
237
- def get_current_user():
238
- return jsonify(current_user.to_dict())
239
- ```
240
-
241
- ## Request Validation with Marshmallow
242
-
243
- ```python
244
- from marshmallow import Schema, fields, validate, ValidationError, post_load
245
-
246
- class UserCreateSchema(Schema):
247
- email = fields.Email(required=True)
248
- password = fields.Str(required=True, validate=validate.Length(min=8))
249
- name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
250
-
251
- class UserUpdateSchema(Schema):
252
- email = fields.Email()
253
- name = fields.Str(validate=validate.Length(max=100))
254
-
255
- class UserResponseSchema(Schema):
256
- id = fields.Int(dump_only=True)
257
- email = fields.Email()
258
- name = fields.Str()
259
- created_at = fields.DateTime(dump_only=True)
260
-
261
- # Usage in route
262
- @bp.post("/")
263
- def create_user():
264
- schema = UserCreateSchema()
265
- try:
266
- data = schema.load(request.get_json())
267
- except ValidationError as err:
268
- return jsonify({"errors": err.messages}), HTTPStatus.BAD_REQUEST
269
-
270
- # Create user...
271
- ```
272
-
273
- ## Configuration
274
-
275
- ```python
276
- import os
277
-
278
- class Config:
279
- SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-key")
280
- SQLALCHEMY_TRACK_MODIFICATIONS = False
281
- JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "jwt-secret")
282
- JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)
283
-
284
- class DevelopmentConfig(Config):
285
- DEBUG = True
286
- SQLALCHEMY_DATABASE_URI = os.environ.get(
287
- "DATABASE_URL", "sqlite:///dev.db"
288
- )
289
-
290
- class ProductionConfig(Config):
291
- DEBUG = False
292
- SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
293
-
294
- class TestingConfig(Config):
295
- TESTING = True
296
- SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
297
-
298
- config = {
299
- "development": DevelopmentConfig,
300
- "production": ProductionConfig,
301
- "testing": TestingConfig,
302
- }
303
- ```
304
-
305
- ## CLI Commands
306
-
307
- ```python
308
- import click
309
- from flask.cli import with_appcontext
310
-
311
- @app.cli.command("seed-db")
312
- @with_appcontext
313
- def seed_db():
314
- """Seed the database with initial data."""
315
- admin = User(email="admin@example.com", name="Admin", role="admin")
316
- admin.set_password("admin123")
317
- db.session.add(admin)
318
- db.session.commit()
319
- click.echo("Database seeded!")
320
-
321
- @app.cli.command("create-admin")
322
- @click.argument("email")
323
- @click.password_option()
324
- @with_appcontext
325
- def create_admin(email: str, password: str):
326
- """Create an admin user."""
327
- user = User(email=email, role="admin")
328
- user.set_password(password)
329
- db.session.add(user)
330
- db.session.commit()
331
- click.echo(f"Admin {email} created!")
332
- ```
@@ -1,18 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(python *)",
5
- "Bash(uvicorn *)",
6
- "Bash(fastapi *)",
7
- "Bash(flask *)",
8
- "Bash(pytest *)",
9
- "Bash(ruff *)",
10
- "Bash(mypy *)",
11
- "Bash(alembic *)",
12
- "Bash(uv *)",
13
- "Bash(poetry *)",
14
- "Bash(pip *)"
15
- ],
16
- "deny": []
17
- }
18
- }