@malamute/ai-rules 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +174 -0
  2. package/bin/cli.js +5 -0
  3. package/configs/_shared/.claude/commands/fix-issue.md +38 -0
  4. package/configs/_shared/.claude/commands/generate-tests.md +49 -0
  5. package/configs/_shared/.claude/commands/review-pr.md +77 -0
  6. package/configs/_shared/.claude/rules/accessibility.md +270 -0
  7. package/configs/_shared/.claude/rules/performance.md +226 -0
  8. package/configs/_shared/.claude/rules/security.md +188 -0
  9. package/configs/_shared/.claude/skills/debug/SKILL.md +118 -0
  10. package/configs/_shared/.claude/skills/learning/SKILL.md +224 -0
  11. package/configs/_shared/.claude/skills/review/SKILL.md +86 -0
  12. package/configs/_shared/.claude/skills/spec/SKILL.md +112 -0
  13. package/configs/_shared/CLAUDE.md +174 -0
  14. package/configs/angular/.claude/rules/components.md +257 -0
  15. package/configs/angular/.claude/rules/state.md +250 -0
  16. package/configs/angular/.claude/rules/testing.md +422 -0
  17. package/configs/angular/.claude/settings.json +31 -0
  18. package/configs/angular/CLAUDE.md +251 -0
  19. package/configs/dotnet/.claude/rules/api.md +370 -0
  20. package/configs/dotnet/.claude/rules/architecture.md +199 -0
  21. package/configs/dotnet/.claude/rules/database/efcore.md +408 -0
  22. package/configs/dotnet/.claude/rules/testing.md +389 -0
  23. package/configs/dotnet/.claude/settings.json +9 -0
  24. package/configs/dotnet/CLAUDE.md +319 -0
  25. package/configs/nestjs/.claude/rules/auth.md +321 -0
  26. package/configs/nestjs/.claude/rules/database/prisma.md +305 -0
  27. package/configs/nestjs/.claude/rules/database/typeorm.md +379 -0
  28. package/configs/nestjs/.claude/rules/modules.md +215 -0
  29. package/configs/nestjs/.claude/rules/testing.md +315 -0
  30. package/configs/nestjs/.claude/rules/validation.md +279 -0
  31. package/configs/nestjs/.claude/settings.json +15 -0
  32. package/configs/nestjs/CLAUDE.md +263 -0
  33. package/configs/nextjs/.claude/rules/components.md +211 -0
  34. package/configs/nextjs/.claude/rules/state/redux-toolkit.md +429 -0
  35. package/configs/nextjs/.claude/rules/state/zustand.md +299 -0
  36. package/configs/nextjs/.claude/rules/testing.md +315 -0
  37. package/configs/nextjs/.claude/settings.json +29 -0
  38. package/configs/nextjs/CLAUDE.md +376 -0
  39. package/configs/python/.claude/rules/database/sqlalchemy.md +355 -0
  40. package/configs/python/.claude/rules/fastapi.md +272 -0
  41. package/configs/python/.claude/rules/flask.md +332 -0
  42. package/configs/python/.claude/rules/testing.md +374 -0
  43. package/configs/python/.claude/settings.json +18 -0
  44. package/configs/python/CLAUDE.md +273 -0
  45. package/package.json +41 -0
  46. package/src/install.js +315 -0
@@ -0,0 +1,332 @@
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
+ ```
@@ -0,0 +1,374 @@
1
+ ---
2
+ paths:
3
+ - "tests/**/*.py"
4
+ - "**/test_*.py"
5
+ - "**/*_test.py"
6
+ ---
7
+
8
+ # Python Testing Rules
9
+
10
+ ## pytest Structure
11
+
12
+ ```
13
+ tests/
14
+ ├── conftest.py # Shared fixtures
15
+ ├── unit/
16
+ │ ├── test_services.py
17
+ │ └── test_utils.py
18
+ ├── integration/
19
+ │ ├── test_repositories.py
20
+ │ └── test_database.py
21
+ └── e2e/
22
+ ├── test_users_api.py
23
+ └── test_auth_api.py
24
+ ```
25
+
26
+ ## Fixtures (conftest.py)
27
+
28
+ ### FastAPI Fixtures
29
+
30
+ ```python
31
+ import pytest
32
+ from httpx import AsyncClient, ASGITransport
33
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
34
+ from sqlalchemy.orm import sessionmaker
35
+
36
+ from app.main import app
37
+ from app.database import Base, get_db
38
+ from app.config import settings
39
+
40
+ # Test database
41
+ TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db"
42
+
43
+ @pytest.fixture(scope="session")
44
+ def event_loop():
45
+ """Create event loop for async tests."""
46
+ import asyncio
47
+ loop = asyncio.new_event_loop()
48
+ yield loop
49
+ loop.close()
50
+
51
+ @pytest.fixture(scope="session")
52
+ async def engine():
53
+ """Create test database engine."""
54
+ engine = create_async_engine(TEST_DATABASE_URL, echo=False)
55
+ async with engine.begin() as conn:
56
+ await conn.run_sync(Base.metadata.create_all)
57
+ yield engine
58
+ async with engine.begin() as conn:
59
+ await conn.run_sync(Base.metadata.drop_all)
60
+ await engine.dispose()
61
+
62
+ @pytest.fixture
63
+ async def db_session(engine):
64
+ """Create a new database session for each test."""
65
+ async_session = sessionmaker(
66
+ engine, class_=AsyncSession, expire_on_commit=False
67
+ )
68
+ async with async_session() as session:
69
+ yield session
70
+ await session.rollback()
71
+
72
+ @pytest.fixture
73
+ async def client(db_session):
74
+ """Create test client with overridden dependencies."""
75
+ async def override_get_db():
76
+ yield db_session
77
+
78
+ app.dependency_overrides[get_db] = override_get_db
79
+
80
+ async with AsyncClient(
81
+ transport=ASGITransport(app=app),
82
+ base_url="http://test",
83
+ ) as client:
84
+ yield client
85
+
86
+ app.dependency_overrides.clear()
87
+
88
+ @pytest.fixture
89
+ async def authenticated_client(client, db_session):
90
+ """Client with authentication token."""
91
+ # Create test user
92
+ user = User(email="test@example.com", name="Test")
93
+ user.set_password("password123")
94
+ db_session.add(user)
95
+ await db_session.commit()
96
+
97
+ # Get token
98
+ response = await client.post("/api/v1/auth/login", json={
99
+ "email": "test@example.com",
100
+ "password": "password123",
101
+ })
102
+ token = response.json()["access_token"]
103
+
104
+ client.headers["Authorization"] = f"Bearer {token}"
105
+ return client
106
+ ```
107
+
108
+ ### Flask Fixtures
109
+
110
+ ```python
111
+ import pytest
112
+ from app import create_app, db as _db
113
+
114
+ @pytest.fixture(scope="session")
115
+ def app():
116
+ """Create application for testing."""
117
+ app = create_app("testing")
118
+ return app
119
+
120
+ @pytest.fixture
121
+ def client(app):
122
+ """Create test client."""
123
+ return app.test_client()
124
+
125
+ @pytest.fixture
126
+ def db(app):
127
+ """Create database for testing."""
128
+ with app.app_context():
129
+ _db.create_all()
130
+ yield _db
131
+ _db.drop_all()
132
+
133
+ @pytest.fixture
134
+ def session(db):
135
+ """Create database session."""
136
+ connection = db.engine.connect()
137
+ transaction = connection.begin()
138
+
139
+ session = db.session
140
+ yield session
141
+
142
+ session.close()
143
+ transaction.rollback()
144
+ connection.close()
145
+ ```
146
+
147
+ ## Unit Tests
148
+
149
+ ```python
150
+ import pytest
151
+ from unittest.mock import AsyncMock, MagicMock
152
+
153
+ from app.users.service import UserService
154
+ from app.users.schemas import UserCreate
155
+
156
+ class TestUserService:
157
+ @pytest.fixture
158
+ def mock_repository(self):
159
+ return AsyncMock()
160
+
161
+ @pytest.fixture
162
+ def service(self, mock_repository):
163
+ return UserService(repository=mock_repository)
164
+
165
+ async def test_create_user_success(self, service, mock_repository):
166
+ # Arrange
167
+ user_data = UserCreate(
168
+ email="test@example.com",
169
+ password="password123",
170
+ name="Test User",
171
+ )
172
+ mock_repository.get_by_email.return_value = None
173
+ mock_repository.create.return_value = User(
174
+ id=1,
175
+ email=user_data.email,
176
+ name=user_data.name,
177
+ )
178
+
179
+ # Act
180
+ result = await service.create(user_data)
181
+
182
+ # Assert
183
+ assert result.email == user_data.email
184
+ mock_repository.create.assert_called_once()
185
+
186
+ async def test_create_user_email_exists_raises(self, service, mock_repository):
187
+ # Arrange
188
+ user_data = UserCreate(
189
+ email="existing@example.com",
190
+ password="password123",
191
+ name="Test",
192
+ )
193
+ mock_repository.get_by_email.return_value = User(id=1, email=user_data.email)
194
+
195
+ # Act & Assert
196
+ with pytest.raises(EmailAlreadyExistsError):
197
+ await service.create(user_data)
198
+ ```
199
+
200
+ ## Integration Tests (API)
201
+
202
+ ### FastAPI Tests
203
+
204
+ ```python
205
+ import pytest
206
+ from httpx import AsyncClient
207
+
208
+ class TestUsersAPI:
209
+ async def test_create_user(self, client: AsyncClient):
210
+ # Arrange
211
+ user_data = {
212
+ "email": "new@example.com",
213
+ "password": "password123",
214
+ "name": "New User",
215
+ }
216
+
217
+ # Act
218
+ response = await client.post("/api/v1/users", json=user_data)
219
+
220
+ # Assert
221
+ assert response.status_code == 201
222
+ data = response.json()
223
+ assert data["email"] == user_data["email"]
224
+ assert "id" in data
225
+ assert "password" not in data
226
+
227
+ async def test_create_user_invalid_email(self, client: AsyncClient):
228
+ # Arrange
229
+ user_data = {
230
+ "email": "invalid",
231
+ "password": "password123",
232
+ "name": "Test",
233
+ }
234
+
235
+ # Act
236
+ response = await client.post("/api/v1/users", json=user_data)
237
+
238
+ # Assert
239
+ assert response.status_code == 422
240
+ assert "email" in response.json()["detail"][0]["loc"]
241
+
242
+ async def test_get_user_not_found(self, client: AsyncClient):
243
+ response = await client.get("/api/v1/users/99999")
244
+ assert response.status_code == 404
245
+
246
+ async def test_list_users_with_pagination(self, client: AsyncClient, db_session):
247
+ # Arrange - create 25 users
248
+ for i in range(25):
249
+ user = User(email=f"user{i}@example.com", name=f"User {i}")
250
+ db_session.add(user)
251
+ await db_session.commit()
252
+
253
+ # Act
254
+ response = await client.get("/api/v1/users?page=2&size=10")
255
+
256
+ # Assert
257
+ assert response.status_code == 200
258
+ data = response.json()
259
+ assert len(data["items"]) == 10
260
+ assert data["total"] == 25
261
+ assert data["page"] == 2
262
+
263
+ async def test_protected_route_unauthorized(self, client: AsyncClient):
264
+ response = await client.get("/api/v1/users/me")
265
+ assert response.status_code == 401
266
+
267
+ async def test_protected_route_authorized(self, authenticated_client: AsyncClient):
268
+ response = await authenticated_client.get("/api/v1/users/me")
269
+ assert response.status_code == 200
270
+ assert "email" in response.json()
271
+ ```
272
+
273
+ ### Flask Tests
274
+
275
+ ```python
276
+ import pytest
277
+
278
+ class TestUsersAPI:
279
+ def test_create_user(self, client):
280
+ response = client.post("/api/v1/users", json={
281
+ "email": "new@example.com",
282
+ "password": "password123",
283
+ "name": "New User",
284
+ })
285
+
286
+ assert response.status_code == 201
287
+ assert response.json["email"] == "new@example.com"
288
+
289
+ def test_get_user(self, client, session):
290
+ # Create user
291
+ user = User(email="test@example.com", name="Test")
292
+ session.add(user)
293
+ session.commit()
294
+
295
+ response = client.get(f"/api/v1/users/{user.id}")
296
+
297
+ assert response.status_code == 200
298
+ assert response.json["id"] == user.id
299
+ ```
300
+
301
+ ## Parametrized Tests
302
+
303
+ ```python
304
+ import pytest
305
+
306
+ @pytest.mark.parametrize("email,expected_valid", [
307
+ ("valid@example.com", True),
308
+ ("also.valid@example.co.uk", True),
309
+ ("invalid", False),
310
+ ("missing@", False),
311
+ ("@nodomain.com", False),
312
+ ])
313
+ def test_email_validation(email: str, expected_valid: bool):
314
+ schema = UserCreateSchema()
315
+ if expected_valid:
316
+ result = schema.load({"email": email, "password": "12345678", "name": "Test"})
317
+ assert result["email"] == email
318
+ else:
319
+ with pytest.raises(ValidationError):
320
+ schema.load({"email": email, "password": "12345678", "name": "Test"})
321
+
322
+ @pytest.mark.parametrize("password,should_pass", [
323
+ ("short", False),
324
+ ("longenough", True),
325
+ ("12345678", True),
326
+ ])
327
+ def test_password_validation(password: str, should_pass: bool):
328
+ ...
329
+ ```
330
+
331
+ ## Markers
332
+
333
+ ```python
334
+ # pytest.ini or pyproject.toml
335
+ [tool.pytest.ini_options]
336
+ markers = [
337
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')",
338
+ "integration: marks tests that require database",
339
+ "e2e: marks end-to-end tests",
340
+ ]
341
+
342
+ # Usage
343
+ @pytest.mark.slow
344
+ async def test_heavy_computation():
345
+ ...
346
+
347
+ @pytest.mark.integration
348
+ async def test_database_query():
349
+ ...
350
+
351
+ # Run specific markers
352
+ # pytest -m "not slow"
353
+ # pytest -m integration
354
+ ```
355
+
356
+ ## Coverage
357
+
358
+ ```bash
359
+ # Run with coverage
360
+ pytest --cov=app --cov-report=html --cov-report=term-missing
361
+
362
+ # Coverage config in pyproject.toml
363
+ [tool.coverage.run]
364
+ source = ["app"]
365
+ omit = ["*/tests/*", "*/__init__.py"]
366
+
367
+ [tool.coverage.report]
368
+ exclude_lines = [
369
+ "pragma: no cover",
370
+ "if TYPE_CHECKING:",
371
+ "raise NotImplementedError",
372
+ ]
373
+ fail_under = 80
374
+ ```
@@ -0,0 +1,18 @@
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
+ }