@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,421 @@
1
+ ---
2
+ paths:
3
+ - "**/alembic/**/*.py"
4
+ - "**/migrations/**/*.py"
5
+ - "**/alembic.ini"
6
+ ---
7
+
8
+ # Python Database Migrations
9
+
10
+ ## Alembic Setup (SQLAlchemy)
11
+
12
+ ### Configuration
13
+
14
+ ```python
15
+ # alembic/env.py
16
+ from logging.config import fileConfig
17
+ from sqlalchemy import engine_from_config, pool
18
+ from alembic import context
19
+ from app.core.config import settings
20
+ from app.db.base import Base
21
+
22
+ config = context.config
23
+
24
+ if config.config_file_name is not None:
25
+ fileConfig(config.config_file_name)
26
+
27
+ target_metadata = Base.metadata
28
+
29
+
30
+ def get_url() -> str:
31
+ return settings.DATABASE_URL
32
+
33
+
34
+ def run_migrations_offline() -> None:
35
+ """Run migrations in 'offline' mode."""
36
+ url = get_url()
37
+ context.configure(
38
+ url=url,
39
+ target_metadata=target_metadata,
40
+ literal_binds=True,
41
+ dialect_opts={"paramstyle": "named"},
42
+ compare_type=True,
43
+ compare_server_default=True,
44
+ )
45
+
46
+ with context.begin_transaction():
47
+ context.run_migrations()
48
+
49
+
50
+ def run_migrations_online() -> None:
51
+ """Run migrations in 'online' mode."""
52
+ configuration = config.get_section(config.config_ini_section) or {}
53
+ configuration["sqlalchemy.url"] = get_url()
54
+
55
+ connectable = engine_from_config(
56
+ configuration,
57
+ prefix="sqlalchemy.",
58
+ poolclass=pool.NullPool,
59
+ )
60
+
61
+ with connectable.connect() as connection:
62
+ context.configure(
63
+ connection=connection,
64
+ target_metadata=target_metadata,
65
+ compare_type=True,
66
+ compare_server_default=True,
67
+ )
68
+
69
+ with context.begin_transaction():
70
+ context.run_migrations()
71
+
72
+
73
+ if context.is_offline_mode():
74
+ run_migrations_offline()
75
+ else:
76
+ run_migrations_online()
77
+ ```
78
+
79
+ ### alembic.ini
80
+
81
+ ```ini
82
+ [alembic]
83
+ script_location = alembic
84
+ prepend_sys_path = .
85
+ version_path_separator = os
86
+ sqlalchemy.url = driver://user:pass@localhost/dbname
87
+
88
+ [post_write_hooks]
89
+ hooks = black
90
+ black.type = console_scripts
91
+ black.entrypoint = black
92
+ black.options = -l 88
93
+
94
+ [loggers]
95
+ keys = root,sqlalchemy,alembic
96
+
97
+ [handlers]
98
+ keys = console
99
+
100
+ [formatters]
101
+ keys = generic
102
+
103
+ [logger_root]
104
+ level = WARN
105
+ handlers = console
106
+ qualname =
107
+
108
+ [logger_sqlalchemy]
109
+ level = WARN
110
+ handlers =
111
+ qualname = sqlalchemy.engine
112
+
113
+ [logger_alembic]
114
+ level = INFO
115
+ handlers =
116
+ qualname = alembic
117
+
118
+ [handler_console]
119
+ class = StreamHandler
120
+ args = (sys.stderr,)
121
+ level = NOTSET
122
+ formatter = generic
123
+
124
+ [formatter_generic]
125
+ format = %(levelname)-5.5s [%(name)s] %(message)s
126
+ datefmt = %H:%M:%S
127
+ ```
128
+
129
+ ## Migration Commands
130
+
131
+ ```bash
132
+ # Create migration
133
+ alembic revision --autogenerate -m "add users table"
134
+
135
+ # Apply migrations
136
+ alembic upgrade head
137
+
138
+ # Rollback last migration
139
+ alembic downgrade -1
140
+
141
+ # Rollback to specific revision
142
+ alembic downgrade abc123
143
+
144
+ # Show current revision
145
+ alembic current
146
+
147
+ # Show migration history
148
+ alembic history --verbose
149
+
150
+ # Generate SQL without applying
151
+ alembic upgrade head --sql > migration.sql
152
+ ```
153
+
154
+ ## Migration File Structure
155
+
156
+ ```python
157
+ # alembic/versions/2024_01_15_add_users_table.py
158
+ """Add users table
159
+
160
+ Revision ID: abc123def456
161
+ Revises:
162
+ Create Date: 2024-01-15 10:00:00.000000
163
+ """
164
+ from typing import Sequence, Union
165
+ from alembic import op
166
+ import sqlalchemy as sa
167
+
168
+ revision: str = "abc123def456"
169
+ down_revision: Union[str, None] = None
170
+ branch_labels: Union[str, Sequence[str], None] = None
171
+ depends_on: Union[str, Sequence[str], None] = None
172
+
173
+
174
+ def upgrade() -> None:
175
+ op.create_table(
176
+ "users",
177
+ sa.Column("id", sa.UUID(), primary_key=True),
178
+ sa.Column("email", sa.String(255), nullable=False, unique=True),
179
+ sa.Column("name", sa.String(100), nullable=True),
180
+ sa.Column("hashed_password", sa.String(255), nullable=False),
181
+ sa.Column("is_active", sa.Boolean(), default=True),
182
+ sa.Column("created_at", sa.DateTime(), server_default=sa.func.now()),
183
+ sa.Column("updated_at", sa.DateTime(), onupdate=sa.func.now()),
184
+ )
185
+ op.create_index("ix_users_email", "users", ["email"])
186
+
187
+
188
+ def downgrade() -> None:
189
+ op.drop_index("ix_users_email", table_name="users")
190
+ op.drop_table("users")
191
+ ```
192
+
193
+ ## Common Migration Operations
194
+
195
+ ### Column Operations
196
+
197
+ ```python
198
+ def upgrade() -> None:
199
+ # Add column
200
+ op.add_column("users", sa.Column("phone", sa.String(20), nullable=True))
201
+
202
+ # Alter column
203
+ op.alter_column(
204
+ "users",
205
+ "name",
206
+ existing_type=sa.String(100),
207
+ type_=sa.String(200),
208
+ existing_nullable=True,
209
+ )
210
+
211
+ # Rename column
212
+ op.alter_column("users", "name", new_column_name="full_name")
213
+
214
+ # Drop column
215
+ op.drop_column("users", "deprecated_field")
216
+
217
+ # Add non-nullable column with default
218
+ op.add_column("users", sa.Column("role", sa.String(20), nullable=True))
219
+ op.execute("UPDATE users SET role = 'user' WHERE role IS NULL")
220
+ op.alter_column("users", "role", nullable=False)
221
+ ```
222
+
223
+ ### Index Operations
224
+
225
+ ```python
226
+ def upgrade() -> None:
227
+ # Create index
228
+ op.create_index("ix_users_email", "users", ["email"], unique=True)
229
+
230
+ # Composite index
231
+ op.create_index("ix_posts_author_date", "posts", ["author_id", "created_at"])
232
+
233
+ # Partial index
234
+ op.create_index(
235
+ "ix_users_active_email",
236
+ "users",
237
+ ["email"],
238
+ postgresql_where=sa.text("is_active = true"),
239
+ )
240
+
241
+ # Drop index
242
+ op.drop_index("ix_old_index", table_name="users")
243
+ ```
244
+
245
+ ### Foreign Key Operations
246
+
247
+ ```python
248
+ def upgrade() -> None:
249
+ # Add foreign key
250
+ op.create_foreign_key(
251
+ "fk_posts_author",
252
+ "posts",
253
+ "users",
254
+ ["author_id"],
255
+ ["id"],
256
+ ondelete="CASCADE",
257
+ )
258
+
259
+ # Drop foreign key
260
+ op.drop_constraint("fk_posts_author", "posts", type_="foreignkey")
261
+ ```
262
+
263
+ ### Enum Operations
264
+
265
+ ```python
266
+ def upgrade() -> None:
267
+ # Create enum type
268
+ status_enum = sa.Enum("pending", "active", "suspended", name="user_status")
269
+ status_enum.create(op.get_bind())
270
+
271
+ # Add column with enum
272
+ op.add_column("users", sa.Column("status", status_enum, default="pending"))
273
+
274
+
275
+ def downgrade() -> None:
276
+ op.drop_column("users", "status")
277
+
278
+ # Drop enum type
279
+ sa.Enum(name="user_status").drop(op.get_bind())
280
+ ```
281
+
282
+ ## Data Migrations
283
+
284
+ ```python
285
+ from sqlalchemy.orm import Session
286
+ from alembic import op
287
+ import sqlalchemy as sa
288
+
289
+ def upgrade() -> None:
290
+ # Get connection
291
+ connection = op.get_bind()
292
+
293
+ # Execute raw SQL
294
+ connection.execute(sa.text("""
295
+ UPDATE users
296
+ SET role = 'admin'
297
+ WHERE email LIKE '%@admin.com'
298
+ """))
299
+
300
+ # Using ORM (with Session)
301
+ session = Session(bind=connection)
302
+ try:
303
+ users = session.execute(sa.text("SELECT id, name FROM users")).fetchall()
304
+ for user in users:
305
+ session.execute(
306
+ sa.text("UPDATE users SET slug = :slug WHERE id = :id"),
307
+ {"slug": slugify(user.name), "id": user.id}
308
+ )
309
+ session.commit()
310
+ except Exception:
311
+ session.rollback()
312
+ raise
313
+ ```
314
+
315
+ ## Multi-Database Migrations
316
+
317
+ ```python
318
+ # alembic/env.py
319
+ from app.db.base import Base
320
+ from app.db.tenant import TenantBase
321
+
322
+ def run_migrations_online() -> None:
323
+ connectable = engine_from_config(...)
324
+
325
+ with connectable.connect() as connection:
326
+ # Run core migrations
327
+ context.configure(
328
+ connection=connection,
329
+ target_metadata=Base.metadata,
330
+ version_table="alembic_version_core",
331
+ )
332
+ with context.begin_transaction():
333
+ context.run_migrations()
334
+
335
+ # Run tenant migrations
336
+ context.configure(
337
+ connection=connection,
338
+ target_metadata=TenantBase.metadata,
339
+ version_table="alembic_version_tenant",
340
+ )
341
+ with context.begin_transaction():
342
+ context.run_migrations()
343
+ ```
344
+
345
+ ## CI/CD Integration
346
+
347
+ ```yaml
348
+ # .github/workflows/migrations.yml
349
+ - name: Check for pending migrations
350
+ run: |
351
+ alembic upgrade head --sql > /dev/null
352
+ if [ $? -ne 0 ]; then
353
+ echo "Migration check failed"
354
+ exit 1
355
+ fi
356
+
357
+ - name: Run migrations
358
+ run: alembic upgrade head
359
+ env:
360
+ DATABASE_URL: ${{ secrets.DATABASE_URL }}
361
+ ```
362
+
363
+ ## Anti-patterns
364
+
365
+ ```python
366
+ # BAD: Hardcoded values
367
+ op.execute("UPDATE users SET role = 'admin' WHERE id = 1")
368
+
369
+ # GOOD: Use parameterized queries
370
+ connection.execute(
371
+ sa.text("UPDATE users SET role = :role WHERE id = :id"),
372
+ {"role": "admin", "id": 1}
373
+ )
374
+
375
+ # BAD: Destructive migration without data preservation
376
+ def upgrade() -> None:
377
+ op.drop_column("users", "legacy_data") # Data lost!
378
+
379
+ # GOOD: Preserve data first
380
+ def upgrade() -> None:
381
+ # Step 1: Copy data
382
+ op.execute("INSERT INTO users_archive SELECT * FROM users")
383
+ # Step 2: Then drop
384
+ op.drop_column("users", "legacy_data")
385
+
386
+ # BAD: Long-running migration blocking table
387
+ def upgrade() -> None:
388
+ op.add_column("users", sa.Column("computed", sa.String(255)))
389
+ op.execute("UPDATE users SET computed = complex_function(data)") # Locks table!
390
+
391
+ # GOOD: Use batching with parameterized queries
392
+ def upgrade() -> None:
393
+ connection = op.get_bind()
394
+ batch_size = 1000
395
+ offset = 0
396
+
397
+ while True:
398
+ result = connection.execute(
399
+ sa.text("""
400
+ UPDATE users SET computed = complex_function(data)
401
+ WHERE id IN (
402
+ SELECT id FROM users
403
+ ORDER BY id
404
+ LIMIT :batch_size OFFSET :offset
405
+ )
406
+ """),
407
+ {"batch_size": batch_size, "offset": offset}
408
+ )
409
+ if result.rowcount == 0:
410
+ break
411
+ offset += batch_size
412
+ connection.commit()
413
+
414
+ # BAD: No downgrade path
415
+ def downgrade() -> None:
416
+ pass # Cannot rollback!
417
+
418
+ # GOOD: Always implement downgrade
419
+ def downgrade() -> None:
420
+ op.drop_column("users", "new_column")
421
+ ```
@@ -0,0 +1,172 @@
1
+ # Python Conventions
2
+
3
+ ## Activation
4
+
5
+ ```yaml
6
+ paths:
7
+ - "**/*.py"
8
+ - "**/pyproject.toml"
9
+ - "**/requirements*.txt"
10
+ ```
11
+
12
+ ## Type Hints (Required)
13
+
14
+ Use modern syntax (Python 3.10+):
15
+
16
+ ```python
17
+ # GOOD
18
+ def get_user(user_id: int) -> User | None: ...
19
+ def process(items: list[str]) -> dict[str, int]: ...
20
+
21
+ # BAD - old syntax
22
+ def get_user(user_id: int) -> Optional[User]: ...
23
+ def process(items: List[str]) -> Dict[str, int]: ...
24
+ ```
25
+
26
+ Use `Annotated` for metadata:
27
+
28
+ ```python
29
+ from typing import Annotated
30
+ from fastapi import Depends
31
+
32
+ CurrentUser = Annotated[User, Depends(get_current_user)]
33
+ ```
34
+
35
+ ## Naming Conventions
36
+
37
+ | Element | Convention | Example |
38
+ |------------|---------------|----------------------|
39
+ | Modules | snake_case | `user_service.py` |
40
+ | Classes | PascalCase | `UserRepository` |
41
+ | Functions | snake_case | `get_user_by_id` |
42
+ | Constants | UPPER_SNAKE | `MAX_RETRY_COUNT` |
43
+ | Private | _prefix | `_validate_input` |
44
+ | Protected | _prefix | `_internal_method` |
45
+
46
+ ## Async/Await
47
+
48
+ ```python
49
+ # GOOD - async for I/O
50
+ async def fetch_user(user_id: int) -> User:
51
+ async with httpx.AsyncClient() as client:
52
+ response = await client.get(f"/users/{user_id}")
53
+ return User(**response.json())
54
+
55
+ # BAD - sync I/O in async context
56
+ async def fetch_user(user_id: int) -> User:
57
+ response = requests.get(f"/users/{user_id}") # Blocks event loop!
58
+ return User(**response.json())
59
+ ```
60
+
61
+ Async library choices:
62
+ - HTTP: `httpx` (not `requests`)
63
+ - PostgreSQL: `asyncpg` (not `psycopg2`)
64
+ - Redis: `redis.asyncio` (not sync `redis`)
65
+
66
+ ## Code Style
67
+
68
+ ```bash
69
+ # Tools
70
+ ruff check . # Linting
71
+ ruff format . # Formatting (Black-compatible)
72
+ mypy --strict . # Type checking
73
+ ```
74
+
75
+ Configuration in `pyproject.toml`:
76
+
77
+ ```toml
78
+ [tool.ruff]
79
+ line-length = 88
80
+ target-version = "py312"
81
+
82
+ [tool.ruff.lint]
83
+ select = ["E", "F", "I", "N", "UP", "B", "A", "C4", "PT", "RUF"]
84
+
85
+ [tool.mypy]
86
+ python_version = "3.12"
87
+ strict = true
88
+ ```
89
+
90
+ ## Avoid
91
+
92
+ ```python
93
+ # BAD - type: ignore without reason
94
+ result = some_call() # type: ignore
95
+
96
+ # GOOD - explain why
97
+ result = some_call() # type: ignore[arg-type] # Library typing issue, see #123
98
+
99
+ # BAD - mutable default argument
100
+ def append_to(item, target=[]): # Shared across calls!
101
+ target.append(item)
102
+ return target
103
+
104
+ # GOOD - use None
105
+ def append_to(item, target: list | None = None):
106
+ if target is None:
107
+ target = []
108
+ target.append(item)
109
+ return target
110
+
111
+ # BAD - global state
112
+ _cache = {}
113
+ def get_cached(key): ...
114
+
115
+ # GOOD - class or function parameter
116
+ class Cache:
117
+ def __init__(self):
118
+ self._data = {}
119
+ ```
120
+
121
+ ## Docstrings (Google Style)
122
+
123
+ ```python
124
+ def calculate_total(
125
+ items: list[Item],
126
+ discount: float = 0.0,
127
+ ) -> Decimal:
128
+ """Calculate the total price of items with optional discount.
129
+
130
+ Args:
131
+ items: List of items to calculate.
132
+ discount: Discount percentage (0.0 to 1.0).
133
+
134
+ Returns:
135
+ Total price after discount.
136
+
137
+ Raises:
138
+ ValueError: If discount is not between 0 and 1.
139
+ """
140
+ ```
141
+
142
+ ## Testing
143
+
144
+ ```python
145
+ import pytest
146
+ from httpx import AsyncClient
147
+
148
+ @pytest.mark.asyncio
149
+ async def test_get_user_returns_user_when_exists(
150
+ client: AsyncClient,
151
+ user_factory: UserFactory,
152
+ ):
153
+ # Arrange
154
+ user = await user_factory.create()
155
+
156
+ # Act
157
+ response = await client.get(f"/users/{user.id}")
158
+
159
+ # Assert
160
+ assert response.status_code == 200
161
+ assert response.json()["id"] == user.id
162
+ ```
163
+
164
+ Use fixtures for setup:
165
+
166
+ ```python
167
+ @pytest.fixture
168
+ async def db_session():
169
+ async with async_session() as session:
170
+ yield session
171
+ await session.rollback()
172
+ ```