@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
@@ -0,0 +1,251 @@
1
+ ---
2
+ paths:
3
+ - "tests/**/*.py"
4
+ - "**/test_*.py"
5
+ ---
6
+
7
+ # FastAPI Testing Patterns
8
+
9
+ ## Test Client Setup
10
+
11
+ ```python
12
+ import pytest
13
+ from httpx import AsyncClient, ASGITransport
14
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
15
+ from sqlalchemy.orm import sessionmaker
16
+
17
+ from app.main import app
18
+ from app.database import Base, get_db
19
+
20
+ TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db"
21
+
22
+ @pytest.fixture(scope="session")
23
+ def event_loop():
24
+ import asyncio
25
+ loop = asyncio.new_event_loop()
26
+ yield loop
27
+ loop.close()
28
+
29
+ @pytest.fixture(scope="session")
30
+ async def engine():
31
+ engine = create_async_engine(TEST_DATABASE_URL)
32
+ async with engine.begin() as conn:
33
+ await conn.run_sync(Base.metadata.create_all)
34
+ yield engine
35
+ async with engine.begin() as conn:
36
+ await conn.run_sync(Base.metadata.drop_all)
37
+ await engine.dispose()
38
+
39
+ @pytest.fixture
40
+ async def db_session(engine):
41
+ async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
42
+ async with async_session() as session:
43
+ yield session
44
+ await session.rollback()
45
+
46
+ @pytest.fixture
47
+ async def client(db_session):
48
+ async def override_get_db():
49
+ yield db_session
50
+
51
+ app.dependency_overrides[get_db] = override_get_db
52
+
53
+ async with AsyncClient(
54
+ transport=ASGITransport(app=app),
55
+ base_url="http://test",
56
+ ) as client:
57
+ yield client
58
+
59
+ app.dependency_overrides.clear()
60
+ ```
61
+
62
+ ## API Tests
63
+
64
+ ```python
65
+ class TestUsersAPI:
66
+ async def test_create_user(self, client: AsyncClient):
67
+ response = await client.post("/api/v1/users", json={
68
+ "email": "test@example.com",
69
+ "password": "password123",
70
+ "name": "Test User",
71
+ })
72
+
73
+ assert response.status_code == 201
74
+ data = response.json()
75
+ assert data["email"] == "test@example.com"
76
+ assert "id" in data
77
+ assert "password" not in data
78
+
79
+ async def test_create_user_duplicate_email(self, client: AsyncClient, db_session):
80
+ # Create existing user
81
+ user = User(email="existing@example.com", name="Existing")
82
+ db_session.add(user)
83
+ await db_session.commit()
84
+
85
+ response = await client.post("/api/v1/users", json={
86
+ "email": "existing@example.com",
87
+ "password": "password123",
88
+ "name": "New User",
89
+ })
90
+
91
+ assert response.status_code == 409
92
+ assert "already exists" in response.json()["detail"]
93
+
94
+ async def test_get_user_not_found(self, client: AsyncClient):
95
+ response = await client.get("/api/v1/users/99999")
96
+
97
+ assert response.status_code == 404
98
+
99
+ async def test_list_users_pagination(self, client: AsyncClient, db_session):
100
+ # Create users
101
+ for i in range(25):
102
+ db_session.add(User(email=f"user{i}@example.com", name=f"User {i}"))
103
+ await db_session.commit()
104
+
105
+ response = await client.get("/api/v1/users?page=2&size=10")
106
+
107
+ assert response.status_code == 200
108
+ data = response.json()
109
+ assert len(data["items"]) == 10
110
+ assert data["total"] == 25
111
+ assert data["page"] == 2
112
+ ```
113
+
114
+ ## Authentication Tests
115
+
116
+ ```python
117
+ @pytest.fixture
118
+ async def auth_headers(client: AsyncClient, db_session):
119
+ # Create user
120
+ user = User(email="auth@example.com", name="Auth User")
121
+ user.set_password("password123")
122
+ db_session.add(user)
123
+ await db_session.commit()
124
+
125
+ # Get token
126
+ response = await client.post("/api/v1/auth/login", data={
127
+ "username": "auth@example.com",
128
+ "password": "password123",
129
+ })
130
+ token = response.json()["access_token"]
131
+
132
+ return {"Authorization": f"Bearer {token}"}
133
+
134
+ class TestAuthenticatedEndpoints:
135
+ async def test_get_me_unauthorized(self, client: AsyncClient):
136
+ response = await client.get("/api/v1/users/me")
137
+ assert response.status_code == 401
138
+
139
+ async def test_get_me_authorized(self, client: AsyncClient, auth_headers):
140
+ response = await client.get("/api/v1/users/me", headers=auth_headers)
141
+ assert response.status_code == 200
142
+ assert response.json()["email"] == "auth@example.com"
143
+ ```
144
+
145
+ ## Dependency Override
146
+
147
+ ```python
148
+ from unittest.mock import AsyncMock
149
+
150
+ @pytest.fixture
151
+ def mock_email_service():
152
+ return AsyncMock()
153
+
154
+ async def test_signup_sends_email(client: AsyncClient, mock_email_service):
155
+ app.dependency_overrides[get_email_service] = lambda: mock_email_service
156
+
157
+ response = await client.post("/api/v1/auth/signup", json={
158
+ "email": "new@example.com",
159
+ "password": "password123",
160
+ "name": "New User",
161
+ })
162
+
163
+ assert response.status_code == 201
164
+ mock_email_service.send_welcome.assert_called_once_with("new@example.com")
165
+
166
+ app.dependency_overrides.clear()
167
+ ```
168
+
169
+ ## WebSocket Tests
170
+
171
+ ```python
172
+ from httpx_ws import aconnect_ws
173
+
174
+ async def test_websocket_echo(client: AsyncClient):
175
+ async with aconnect_ws("http://test/ws", client) as ws:
176
+ await ws.send_text("Hello")
177
+ response = await ws.receive_text()
178
+ assert response == "Echo: Hello"
179
+
180
+ async def test_websocket_auth_required():
181
+ async with AsyncClient(
182
+ transport=ASGITransport(app=app),
183
+ base_url="http://test",
184
+ ) as client:
185
+ with pytest.raises(Exception):
186
+ async with aconnect_ws("http://test/ws", client) as ws:
187
+ pass # Should fail without token
188
+ ```
189
+
190
+ ## File Upload Tests
191
+
192
+ ```python
193
+ async def test_file_upload(client: AsyncClient, auth_headers):
194
+ files = {"file": ("test.txt", b"file content", "text/plain")}
195
+
196
+ response = await client.post(
197
+ "/api/v1/files/upload",
198
+ files=files,
199
+ headers=auth_headers,
200
+ )
201
+
202
+ assert response.status_code == 201
203
+ assert response.json()["filename"] == "test.txt"
204
+ ```
205
+
206
+ ## Parametrized Tests
207
+
208
+ ```python
209
+ @pytest.mark.parametrize("email,expected_valid", [
210
+ ("valid@example.com", True),
211
+ ("also.valid@example.co.uk", True),
212
+ ("invalid", False),
213
+ ("missing@", False),
214
+ ("@nodomain.com", False),
215
+ ])
216
+ async def test_email_validation(client: AsyncClient, email: str, expected_valid: bool):
217
+ response = await client.post("/api/v1/users", json={
218
+ "email": email,
219
+ "password": "password123",
220
+ "name": "Test",
221
+ })
222
+
223
+ if expected_valid:
224
+ assert response.status_code in (201, 409) # Created or duplicate
225
+ else:
226
+ assert response.status_code == 422
227
+ ```
228
+
229
+ ## Test Markers
230
+
231
+ ```python
232
+ # pyproject.toml
233
+ [tool.pytest.ini_options]
234
+ markers = [
235
+ "slow: marks tests as slow",
236
+ "integration: requires database",
237
+ ]
238
+
239
+ # Usage
240
+ @pytest.mark.slow
241
+ async def test_heavy_computation():
242
+ ...
243
+
244
+ @pytest.mark.integration
245
+ async def test_database_query():
246
+ ...
247
+
248
+ # Run specific markers
249
+ # pytest -m "not slow"
250
+ # pytest -m integration
251
+ ```
@@ -0,0 +1,298 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
6
+ # FastAPI WebSocket Patterns
7
+
8
+ ## Basic WebSocket
9
+
10
+ ```python
11
+ from fastapi import WebSocket, WebSocketDisconnect
12
+
13
+ @app.websocket("/ws")
14
+ async def websocket_endpoint(websocket: WebSocket):
15
+ await websocket.accept()
16
+
17
+ try:
18
+ while True:
19
+ data = await websocket.receive_text()
20
+ await websocket.send_text(f"Echo: {data}")
21
+ except WebSocketDisconnect:
22
+ print("Client disconnected")
23
+ ```
24
+
25
+ ## Connection Manager
26
+
27
+ ```python
28
+ from typing import List
29
+
30
+ class ConnectionManager:
31
+ def __init__(self):
32
+ self.active_connections: list[WebSocket] = []
33
+
34
+ async def connect(self, websocket: WebSocket):
35
+ await websocket.accept()
36
+ self.active_connections.append(websocket)
37
+
38
+ def disconnect(self, websocket: WebSocket):
39
+ self.active_connections.remove(websocket)
40
+
41
+ async def send_personal_message(self, message: str, websocket: WebSocket):
42
+ await websocket.send_text(message)
43
+
44
+ async def broadcast(self, message: str):
45
+ for connection in self.active_connections:
46
+ await connection.send_text(message)
47
+
48
+ manager = ConnectionManager()
49
+
50
+ @app.websocket("/ws/{client_id}")
51
+ async def websocket_endpoint(websocket: WebSocket, client_id: str):
52
+ await manager.connect(websocket)
53
+
54
+ try:
55
+ while True:
56
+ data = await websocket.receive_text()
57
+ await manager.broadcast(f"Client {client_id}: {data}")
58
+ except WebSocketDisconnect:
59
+ manager.disconnect(websocket)
60
+ await manager.broadcast(f"Client {client_id} left")
61
+ ```
62
+
63
+ ## Room-Based Connections
64
+
65
+ ```python
66
+ from collections import defaultdict
67
+
68
+ class RoomManager:
69
+ def __init__(self):
70
+ self.rooms: dict[str, list[WebSocket]] = defaultdict(list)
71
+
72
+ async def join_room(self, room: str, websocket: WebSocket):
73
+ await websocket.accept()
74
+ self.rooms[room].append(websocket)
75
+
76
+ def leave_room(self, room: str, websocket: WebSocket):
77
+ if websocket in self.rooms[room]:
78
+ self.rooms[room].remove(websocket)
79
+ if not self.rooms[room]:
80
+ del self.rooms[room]
81
+
82
+ async def broadcast_to_room(self, room: str, message: str, exclude: WebSocket = None):
83
+ for connection in self.rooms[room]:
84
+ if connection != exclude:
85
+ await connection.send_text(message)
86
+
87
+ room_manager = RoomManager()
88
+
89
+ @app.websocket("/ws/room/{room_id}")
90
+ async def room_websocket(websocket: WebSocket, room_id: str):
91
+ await room_manager.join_room(room_id, websocket)
92
+
93
+ try:
94
+ while True:
95
+ data = await websocket.receive_text()
96
+ await room_manager.broadcast_to_room(room_id, data, exclude=websocket)
97
+ except WebSocketDisconnect:
98
+ room_manager.leave_room(room_id, websocket)
99
+ ```
100
+
101
+ ## Authentication
102
+
103
+ ```python
104
+ from fastapi import Query, status
105
+
106
+ async def get_user_from_token(token: str) -> User | None:
107
+ # Verify JWT token
108
+ try:
109
+ payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
110
+ return await get_user(payload["sub"])
111
+ except JWTError:
112
+ return None
113
+
114
+ @app.websocket("/ws")
115
+ async def websocket_endpoint(
116
+ websocket: WebSocket,
117
+ token: str = Query(...),
118
+ ):
119
+ user = await get_user_from_token(token)
120
+
121
+ if not user:
122
+ await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
123
+ return
124
+
125
+ await websocket.accept()
126
+
127
+ try:
128
+ while True:
129
+ data = await websocket.receive_text()
130
+ await websocket.send_text(f"Hello {user.name}: {data}")
131
+ except WebSocketDisconnect:
132
+ pass
133
+ ```
134
+
135
+ ## JSON Messages
136
+
137
+ ```python
138
+ from pydantic import BaseModel
139
+
140
+ class WSMessage(BaseModel):
141
+ type: str
142
+ payload: dict
143
+
144
+ class WSResponse(BaseModel):
145
+ type: str
146
+ data: dict
147
+
148
+ @app.websocket("/ws")
149
+ async def websocket_endpoint(websocket: WebSocket):
150
+ await websocket.accept()
151
+
152
+ try:
153
+ while True:
154
+ raw_data = await websocket.receive_json()
155
+ message = WSMessage(**raw_data)
156
+
157
+ if message.type == "ping":
158
+ response = WSResponse(type="pong", data={})
159
+ elif message.type == "subscribe":
160
+ # Handle subscription
161
+ response = WSResponse(type="subscribed", data=message.payload)
162
+ else:
163
+ response = WSResponse(type="error", data={"message": "Unknown type"})
164
+
165
+ await websocket.send_json(response.model_dump())
166
+ except WebSocketDisconnect:
167
+ pass
168
+ ```
169
+
170
+ ## Pub/Sub with Redis
171
+
172
+ ```python
173
+ import aioredis
174
+ import asyncio
175
+
176
+ class PubSubManager:
177
+ def __init__(self, redis_url: str):
178
+ self.redis_url = redis_url
179
+ self.pubsub = None
180
+ self.redis = None
181
+
182
+ async def connect(self):
183
+ self.redis = await aioredis.from_url(self.redis_url)
184
+ self.pubsub = self.redis.pubsub()
185
+
186
+ async def subscribe(self, channel: str):
187
+ await self.pubsub.subscribe(channel)
188
+
189
+ async def publish(self, channel: str, message: str):
190
+ await self.redis.publish(channel, message)
191
+
192
+ async def listen(self):
193
+ async for message in self.pubsub.listen():
194
+ if message["type"] == "message":
195
+ yield message["data"].decode()
196
+
197
+ pubsub = PubSubManager("redis://localhost")
198
+
199
+ @app.websocket("/ws/subscribe/{channel}")
200
+ async def subscribe_websocket(websocket: WebSocket, channel: str):
201
+ await websocket.accept()
202
+ await pubsub.connect()
203
+ await pubsub.subscribe(channel)
204
+
205
+ try:
206
+ # Task to receive from WebSocket
207
+ async def receive():
208
+ while True:
209
+ data = await websocket.receive_text()
210
+ await pubsub.publish(channel, data)
211
+
212
+ # Task to send from Redis
213
+ async def send():
214
+ async for message in pubsub.listen():
215
+ await websocket.send_text(message)
216
+
217
+ await asyncio.gather(receive(), send())
218
+ except WebSocketDisconnect:
219
+ pass
220
+ ```
221
+
222
+ ## Heartbeat / Keep-Alive
223
+
224
+ ```python
225
+ import asyncio
226
+
227
+ @app.websocket("/ws")
228
+ async def websocket_endpoint(websocket: WebSocket):
229
+ await websocket.accept()
230
+
231
+ async def send_heartbeat():
232
+ while True:
233
+ try:
234
+ await asyncio.sleep(30)
235
+ await websocket.send_json({"type": "ping"})
236
+ except:
237
+ break
238
+
239
+ heartbeat_task = asyncio.create_task(send_heartbeat())
240
+
241
+ try:
242
+ while True:
243
+ data = await websocket.receive_json()
244
+
245
+ if data.get("type") == "pong":
246
+ continue # Heartbeat response
247
+
248
+ # Handle other messages
249
+ await process_message(data)
250
+ except WebSocketDisconnect:
251
+ heartbeat_task.cancel()
252
+ ```
253
+
254
+ ## Rate Limiting WebSocket
255
+
256
+ ```python
257
+ from collections import defaultdict
258
+ import time
259
+
260
+ class WebSocketRateLimiter:
261
+ def __init__(self, max_messages: int = 10, window: int = 1):
262
+ self.max_messages = max_messages
263
+ self.window = window
264
+ self.messages: dict[WebSocket, list[float]] = defaultdict(list)
265
+
266
+ def is_allowed(self, websocket: WebSocket) -> bool:
267
+ now = time.time()
268
+
269
+ # Clean old messages
270
+ self.messages[websocket] = [
271
+ t for t in self.messages[websocket]
272
+ if now - t < self.window
273
+ ]
274
+
275
+ if len(self.messages[websocket]) >= self.max_messages:
276
+ return False
277
+
278
+ self.messages[websocket].append(now)
279
+ return True
280
+
281
+ rate_limiter = WebSocketRateLimiter(max_messages=10, window=1)
282
+
283
+ @app.websocket("/ws")
284
+ async def websocket_endpoint(websocket: WebSocket):
285
+ await websocket.accept()
286
+
287
+ try:
288
+ while True:
289
+ data = await websocket.receive_text()
290
+
291
+ if not rate_limiter.is_allowed(websocket):
292
+ await websocket.send_json({"error": "Rate limit exceeded"})
293
+ continue
294
+
295
+ await process_and_respond(websocket, data)
296
+ except WebSocketDisconnect:
297
+ pass
298
+ ```
@@ -0,0 +1,33 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(python *)",
5
+ "Bash(python3 *)",
6
+ "Bash(uvicorn *)",
7
+ "Bash(gunicorn *)",
8
+ "Bash(fastapi *)",
9
+ "Bash(pytest *)",
10
+ "Bash(ruff *)",
11
+ "Bash(mypy *)",
12
+ "Bash(alembic *)",
13
+ "Bash(uv *)",
14
+ "Bash(poetry *)",
15
+ "Bash(pip *)",
16
+ "Bash(pip3 *)",
17
+ "Read",
18
+ "Edit",
19
+ "Write"
20
+ ],
21
+ "deny": [
22
+ "Bash(rm -rf *)",
23
+ "Read(.env)",
24
+ "Read(.env.*)",
25
+ "Read(**/secrets/**)",
26
+ "Read(**/*.pem)"
27
+ ]
28
+ },
29
+ "env": {
30
+ "PYTHONDONTWRITEBYTECODE": "1",
31
+ "PYTHONUNBUFFERED": "1"
32
+ }
33
+ }