@malamute/ai-rules 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,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
|
+
```
|