@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,383 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/repositories/**/*.py"
|
|
4
|
+
- "**/repository/**/*.py"
|
|
5
|
+
- "**/*_repository.py"
|
|
6
|
+
- "**/dal/**/*.py"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Python Repository Pattern
|
|
10
|
+
|
|
11
|
+
## Base Repository (SQLAlchemy)
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
# repositories/base.py
|
|
15
|
+
from typing import Generic, TypeVar, Sequence
|
|
16
|
+
from sqlalchemy import select, func
|
|
17
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
18
|
+
|
|
19
|
+
from models.base import Base
|
|
20
|
+
|
|
21
|
+
ModelT = TypeVar("ModelT", bound=Base)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BaseRepository(Generic[ModelT]):
|
|
25
|
+
"""Base repository with common CRUD operations."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, session: AsyncSession, model: type[ModelT]):
|
|
28
|
+
self.session = session
|
|
29
|
+
self.model = model
|
|
30
|
+
|
|
31
|
+
async def get(self, id: int) -> ModelT | None:
|
|
32
|
+
return await self.session.get(self.model, id)
|
|
33
|
+
|
|
34
|
+
async def get_or_raise(self, id: int) -> ModelT:
|
|
35
|
+
entity = await self.get(id)
|
|
36
|
+
if not entity:
|
|
37
|
+
raise NotFoundError(self.model.__name__, id)
|
|
38
|
+
return entity
|
|
39
|
+
|
|
40
|
+
async def get_all(
|
|
41
|
+
self,
|
|
42
|
+
*,
|
|
43
|
+
skip: int = 0,
|
|
44
|
+
limit: int = 100,
|
|
45
|
+
) -> Sequence[ModelT]:
|
|
46
|
+
result = await self.session.scalars(
|
|
47
|
+
select(self.model).offset(skip).limit(limit)
|
|
48
|
+
)
|
|
49
|
+
return result.all()
|
|
50
|
+
|
|
51
|
+
async def count(self) -> int:
|
|
52
|
+
result = await self.session.scalar(
|
|
53
|
+
select(func.count()).select_from(self.model)
|
|
54
|
+
)
|
|
55
|
+
return result or 0
|
|
56
|
+
|
|
57
|
+
async def create(self, **kwargs) -> ModelT:
|
|
58
|
+
entity = self.model(**kwargs)
|
|
59
|
+
self.session.add(entity)
|
|
60
|
+
await self.session.flush()
|
|
61
|
+
await self.session.refresh(entity)
|
|
62
|
+
return entity
|
|
63
|
+
|
|
64
|
+
async def update(self, entity: ModelT, **kwargs) -> ModelT:
|
|
65
|
+
for key, value in kwargs.items():
|
|
66
|
+
if hasattr(entity, key):
|
|
67
|
+
setattr(entity, key, value)
|
|
68
|
+
await self.session.flush()
|
|
69
|
+
await self.session.refresh(entity)
|
|
70
|
+
return entity
|
|
71
|
+
|
|
72
|
+
async def delete(self, entity: ModelT) -> None:
|
|
73
|
+
await self.session.delete(entity)
|
|
74
|
+
await self.session.flush()
|
|
75
|
+
|
|
76
|
+
async def exists(self, id: int) -> bool:
|
|
77
|
+
result = await self.session.scalar(
|
|
78
|
+
select(func.count())
|
|
79
|
+
.select_from(self.model)
|
|
80
|
+
.where(self.model.id == id)
|
|
81
|
+
)
|
|
82
|
+
return (result or 0) > 0
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Typed Repository
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# repositories/user.py
|
|
89
|
+
from sqlalchemy import select
|
|
90
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
91
|
+
from sqlalchemy.orm import selectinload
|
|
92
|
+
|
|
93
|
+
from models import User
|
|
94
|
+
from schemas import UserCreate, UserUpdate
|
|
95
|
+
from .base import BaseRepository
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class UserRepository(BaseRepository[User]):
|
|
99
|
+
def __init__(self, session: AsyncSession):
|
|
100
|
+
super().__init__(session, User)
|
|
101
|
+
|
|
102
|
+
async def get_by_email(self, email: str) -> User | None:
|
|
103
|
+
result = await self.session.scalar(
|
|
104
|
+
select(User).where(User.email == email)
|
|
105
|
+
)
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
async def get_with_posts(self, user_id: int) -> User | None:
|
|
109
|
+
result = await self.session.scalar(
|
|
110
|
+
select(User)
|
|
111
|
+
.options(selectinload(User.posts))
|
|
112
|
+
.where(User.id == user_id)
|
|
113
|
+
)
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
async def get_active_users(
|
|
117
|
+
self,
|
|
118
|
+
*,
|
|
119
|
+
skip: int = 0,
|
|
120
|
+
limit: int = 100,
|
|
121
|
+
) -> list[User]:
|
|
122
|
+
result = await self.session.scalars(
|
|
123
|
+
select(User)
|
|
124
|
+
.where(User.is_active == True)
|
|
125
|
+
.order_by(User.created_at.desc())
|
|
126
|
+
.offset(skip)
|
|
127
|
+
.limit(limit)
|
|
128
|
+
)
|
|
129
|
+
return list(result.all())
|
|
130
|
+
|
|
131
|
+
async def search(
|
|
132
|
+
self,
|
|
133
|
+
query: str,
|
|
134
|
+
*,
|
|
135
|
+
skip: int = 0,
|
|
136
|
+
limit: int = 100,
|
|
137
|
+
) -> list[User]:
|
|
138
|
+
search_term = f"%{query}%"
|
|
139
|
+
result = await self.session.scalars(
|
|
140
|
+
select(User)
|
|
141
|
+
.where(
|
|
142
|
+
(User.name.ilike(search_term)) |
|
|
143
|
+
(User.email.ilike(search_term))
|
|
144
|
+
)
|
|
145
|
+
.offset(skip)
|
|
146
|
+
.limit(limit)
|
|
147
|
+
)
|
|
148
|
+
return list(result.all())
|
|
149
|
+
|
|
150
|
+
async def create_user(self, data: UserCreate) -> User:
|
|
151
|
+
return await self.create(
|
|
152
|
+
email=data.email,
|
|
153
|
+
name=data.name,
|
|
154
|
+
hashed_password=hash_password(data.password),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
async def update_user(self, user: User, data: UserUpdate) -> User:
|
|
158
|
+
update_data = data.model_dump(exclude_unset=True)
|
|
159
|
+
return await self.update(user, **update_data)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Unit of Work Pattern
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# repositories/uow.py
|
|
166
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
167
|
+
|
|
168
|
+
from .user import UserRepository
|
|
169
|
+
from .post import PostRepository
|
|
170
|
+
from .order import OrderRepository
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class UnitOfWork:
|
|
174
|
+
"""Unit of Work pattern for managing transactions."""
|
|
175
|
+
|
|
176
|
+
def __init__(self, session: AsyncSession):
|
|
177
|
+
self.session = session
|
|
178
|
+
self._users: UserRepository | None = None
|
|
179
|
+
self._posts: PostRepository | None = None
|
|
180
|
+
self._orders: OrderRepository | None = None
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def users(self) -> UserRepository:
|
|
184
|
+
if self._users is None:
|
|
185
|
+
self._users = UserRepository(self.session)
|
|
186
|
+
return self._users
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def posts(self) -> PostRepository:
|
|
190
|
+
if self._posts is None:
|
|
191
|
+
self._posts = PostRepository(self.session)
|
|
192
|
+
return self._posts
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def orders(self) -> OrderRepository:
|
|
196
|
+
if self._orders is None:
|
|
197
|
+
self._orders = OrderRepository(self.session)
|
|
198
|
+
return self._orders
|
|
199
|
+
|
|
200
|
+
async def commit(self) -> None:
|
|
201
|
+
await self.session.commit()
|
|
202
|
+
|
|
203
|
+
async def rollback(self) -> None:
|
|
204
|
+
await self.session.rollback()
|
|
205
|
+
|
|
206
|
+
async def __aenter__(self) -> "UnitOfWork":
|
|
207
|
+
return self
|
|
208
|
+
|
|
209
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
210
|
+
if exc_type:
|
|
211
|
+
await self.rollback()
|
|
212
|
+
else:
|
|
213
|
+
await self.commit()
|
|
214
|
+
await self.session.close()
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## FastAPI Integration
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
# dependencies.py
|
|
221
|
+
from typing import Annotated, AsyncIterator
|
|
222
|
+
from fastapi import Depends
|
|
223
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
224
|
+
|
|
225
|
+
from database import async_session
|
|
226
|
+
from repositories import UnitOfWork, UserRepository
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def get_session() -> AsyncIterator[AsyncSession]:
|
|
230
|
+
async with async_session() as session:
|
|
231
|
+
yield session
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
async def get_uow(
|
|
235
|
+
session: Annotated[AsyncSession, Depends(get_session)]
|
|
236
|
+
) -> AsyncIterator[UnitOfWork]:
|
|
237
|
+
async with UnitOfWork(session) as uow:
|
|
238
|
+
yield uow
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
async def get_user_repository(
|
|
242
|
+
session: Annotated[AsyncSession, Depends(get_session)]
|
|
243
|
+
) -> UserRepository:
|
|
244
|
+
return UserRepository(session)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# Type aliases for dependency injection
|
|
248
|
+
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
|
249
|
+
UowDep = Annotated[UnitOfWork, Depends(get_uow)]
|
|
250
|
+
UserRepoDep = Annotated[UserRepository, Depends(get_user_repository)]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# Usage in routes
|
|
254
|
+
@router.get("/{user_id}")
|
|
255
|
+
async def get_user(user_id: int, repo: UserRepoDep) -> UserResponse:
|
|
256
|
+
user = await repo.get_or_raise(user_id)
|
|
257
|
+
return UserResponse.model_validate(user)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# Usage with UoW for transactions
|
|
261
|
+
@router.post("/transfer")
|
|
262
|
+
async def transfer_funds(data: TransferRequest, uow: UowDep) -> None:
|
|
263
|
+
sender = await uow.users.get_or_raise(data.sender_id)
|
|
264
|
+
receiver = await uow.users.get_or_raise(data.receiver_id)
|
|
265
|
+
|
|
266
|
+
sender.balance -= data.amount
|
|
267
|
+
receiver.balance += data.amount
|
|
268
|
+
|
|
269
|
+
await uow.commit() # Single transaction
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Abstract Repository (Interface)
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
# repositories/interfaces.py
|
|
276
|
+
from abc import ABC, abstractmethod
|
|
277
|
+
from typing import Generic, TypeVar, Sequence
|
|
278
|
+
|
|
279
|
+
T = TypeVar("T")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class IRepository(ABC, Generic[T]):
|
|
283
|
+
"""Repository interface for dependency inversion."""
|
|
284
|
+
|
|
285
|
+
@abstractmethod
|
|
286
|
+
async def get(self, id: int) -> T | None:
|
|
287
|
+
...
|
|
288
|
+
|
|
289
|
+
@abstractmethod
|
|
290
|
+
async def get_all(self, *, skip: int = 0, limit: int = 100) -> Sequence[T]:
|
|
291
|
+
...
|
|
292
|
+
|
|
293
|
+
@abstractmethod
|
|
294
|
+
async def create(self, **kwargs) -> T:
|
|
295
|
+
...
|
|
296
|
+
|
|
297
|
+
@abstractmethod
|
|
298
|
+
async def update(self, entity: T, **kwargs) -> T:
|
|
299
|
+
...
|
|
300
|
+
|
|
301
|
+
@abstractmethod
|
|
302
|
+
async def delete(self, entity: T) -> None:
|
|
303
|
+
...
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class IUserRepository(IRepository["User"], ABC):
|
|
307
|
+
"""User-specific repository interface."""
|
|
308
|
+
|
|
309
|
+
@abstractmethod
|
|
310
|
+
async def get_by_email(self, email: str) -> "User | None":
|
|
311
|
+
...
|
|
312
|
+
|
|
313
|
+
@abstractmethod
|
|
314
|
+
async def get_active_users(self) -> list["User"]:
|
|
315
|
+
...
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# SQLAlchemy implementation
|
|
319
|
+
class SqlAlchemyUserRepository(IUserRepository):
|
|
320
|
+
def __init__(self, session: AsyncSession):
|
|
321
|
+
self.session = session
|
|
322
|
+
|
|
323
|
+
async def get(self, id: int) -> User | None:
|
|
324
|
+
return await self.session.get(User, id)
|
|
325
|
+
|
|
326
|
+
# ... implement all methods
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# In-memory implementation for testing
|
|
330
|
+
class InMemoryUserRepository(IUserRepository):
|
|
331
|
+
def __init__(self):
|
|
332
|
+
self._users: dict[int, User] = {}
|
|
333
|
+
self._counter = 0
|
|
334
|
+
|
|
335
|
+
async def get(self, id: int) -> User | None:
|
|
336
|
+
return self._users.get(id)
|
|
337
|
+
|
|
338
|
+
async def create(self, **kwargs) -> User:
|
|
339
|
+
self._counter += 1
|
|
340
|
+
user = User(id=self._counter, **kwargs)
|
|
341
|
+
self._users[self._counter] = user
|
|
342
|
+
return user
|
|
343
|
+
|
|
344
|
+
# ... implement all methods
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Anti-Patterns
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
# BAD: Business logic in repository
|
|
351
|
+
class UserRepository:
|
|
352
|
+
async def create_user(self, data: UserCreate) -> User:
|
|
353
|
+
if await self.get_by_email(data.email):
|
|
354
|
+
raise ConflictError("User", "email", data.email) # Business logic!
|
|
355
|
+
# ...
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# GOOD: Repository only handles data access
|
|
359
|
+
class UserRepository:
|
|
360
|
+
async def get_by_email(self, email: str) -> User | None:
|
|
361
|
+
# Just data access
|
|
362
|
+
...
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class UserService:
|
|
366
|
+
async def create_user(self, data: UserCreate) -> User:
|
|
367
|
+
if await self.repo.get_by_email(data.email):
|
|
368
|
+
raise ConflictError("User", "email", data.email) # In service
|
|
369
|
+
return await self.repo.create(...)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# BAD: Using session directly in routes
|
|
373
|
+
@router.get("/{user_id}")
|
|
374
|
+
async def get_user(user_id: int, session: SessionDep):
|
|
375
|
+
user = await session.get(User, user_id) # Couples route to ORM
|
|
376
|
+
return user
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# GOOD: Use repository abstraction
|
|
380
|
+
@router.get("/{user_id}")
|
|
381
|
+
async def get_user(user_id: int, repo: UserRepoDep):
|
|
382
|
+
return await repo.get_or_raise(user_id) # Decoupled
|
|
383
|
+
```
|
|
@@ -25,7 +25,7 @@ tests/
|
|
|
25
25
|
|
|
26
26
|
## Fixtures (conftest.py)
|
|
27
27
|
|
|
28
|
-
###
|
|
28
|
+
### Fixtures
|
|
29
29
|
|
|
30
30
|
```python
|
|
31
31
|
import pytest
|
|
@@ -105,45 +105,6 @@ async def authenticated_client(client, db_session):
|
|
|
105
105
|
return client
|
|
106
106
|
```
|
|
107
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
108
|
## Unit Tests
|
|
148
109
|
|
|
149
110
|
```python
|
|
@@ -199,7 +160,7 @@ class TestUserService:
|
|
|
199
160
|
|
|
200
161
|
## Integration Tests (API)
|
|
201
162
|
|
|
202
|
-
###
|
|
163
|
+
### API Tests
|
|
203
164
|
|
|
204
165
|
```python
|
|
205
166
|
import pytest
|
|
@@ -270,34 +231,6 @@ class TestUsersAPI:
|
|
|
270
231
|
assert "email" in response.json()
|
|
271
232
|
```
|
|
272
233
|
|
|
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
234
|
## Parametrized Tests
|
|
302
235
|
|
|
303
236
|
```python
|