aury-boot 0.0.2__py3-none-any.whl → 0.0.4__py3-none-any.whl

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 (138) hide show
  1. aury/boot/__init__.py +66 -0
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +120 -0
  4. aury/boot/application/app/__init__.py +39 -0
  5. aury/boot/application/app/base.py +511 -0
  6. aury/boot/application/app/components.py +434 -0
  7. aury/boot/application/app/middlewares.py +101 -0
  8. aury/boot/application/config/__init__.py +44 -0
  9. aury/boot/application/config/settings.py +663 -0
  10. aury/boot/application/constants/__init__.py +19 -0
  11. aury/boot/application/constants/components.py +50 -0
  12. aury/boot/application/constants/scheduler.py +28 -0
  13. aury/boot/application/constants/service.py +29 -0
  14. aury/boot/application/errors/__init__.py +55 -0
  15. aury/boot/application/errors/chain.py +80 -0
  16. aury/boot/application/errors/codes.py +67 -0
  17. aury/boot/application/errors/exceptions.py +238 -0
  18. aury/boot/application/errors/handlers.py +320 -0
  19. aury/boot/application/errors/response.py +120 -0
  20. aury/boot/application/interfaces/__init__.py +76 -0
  21. aury/boot/application/interfaces/egress.py +224 -0
  22. aury/boot/application/interfaces/ingress.py +98 -0
  23. aury/boot/application/middleware/__init__.py +22 -0
  24. aury/boot/application/middleware/logging.py +451 -0
  25. aury/boot/application/migrations/__init__.py +13 -0
  26. aury/boot/application/migrations/manager.py +685 -0
  27. aury/boot/application/migrations/setup.py +237 -0
  28. aury/boot/application/rpc/__init__.py +63 -0
  29. aury/boot/application/rpc/base.py +108 -0
  30. aury/boot/application/rpc/client.py +294 -0
  31. aury/boot/application/rpc/discovery.py +218 -0
  32. aury/boot/application/scheduler/__init__.py +13 -0
  33. aury/boot/application/scheduler/runner.py +123 -0
  34. aury/boot/application/server/__init__.py +296 -0
  35. aury/boot/commands/__init__.py +30 -0
  36. aury/boot/commands/add.py +76 -0
  37. aury/boot/commands/app.py +105 -0
  38. aury/boot/commands/config.py +177 -0
  39. aury/boot/commands/docker.py +367 -0
  40. aury/boot/commands/docs.py +284 -0
  41. aury/boot/commands/generate.py +1277 -0
  42. aury/boot/commands/init.py +892 -0
  43. aury/boot/commands/migrate/__init__.py +37 -0
  44. aury/boot/commands/migrate/app.py +54 -0
  45. aury/boot/commands/migrate/commands.py +303 -0
  46. aury/boot/commands/scheduler.py +124 -0
  47. aury/boot/commands/server/__init__.py +21 -0
  48. aury/boot/commands/server/app.py +541 -0
  49. aury/boot/commands/templates/generate/api.py.tpl +105 -0
  50. aury/boot/commands/templates/generate/model.py.tpl +17 -0
  51. aury/boot/commands/templates/generate/repository.py.tpl +19 -0
  52. aury/boot/commands/templates/generate/schema.py.tpl +29 -0
  53. aury/boot/commands/templates/generate/service.py.tpl +48 -0
  54. aury/boot/commands/templates/project/CLI.md.tpl +92 -0
  55. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
  56. aury/boot/commands/templates/project/README.md.tpl +111 -0
  57. aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
  58. aury/boot/commands/templates/project/config.py.tpl +30 -0
  59. aury/boot/commands/templates/project/conftest.py.tpl +26 -0
  60. aury/boot/commands/templates/project/env.example.tpl +213 -0
  61. aury/boot/commands/templates/project/gitignore.tpl +128 -0
  62. aury/boot/commands/templates/project/main.py.tpl +41 -0
  63. aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
  64. aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
  65. aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
  66. aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
  67. aury/boot/commands/worker.py +143 -0
  68. aury/boot/common/__init__.py +35 -0
  69. aury/boot/common/exceptions/__init__.py +114 -0
  70. aury/boot/common/i18n/__init__.py +16 -0
  71. aury/boot/common/i18n/translator.py +272 -0
  72. aury/boot/common/logging/__init__.py +716 -0
  73. aury/boot/contrib/__init__.py +10 -0
  74. aury/boot/contrib/admin_console/__init__.py +18 -0
  75. aury/boot/contrib/admin_console/auth.py +137 -0
  76. aury/boot/contrib/admin_console/discovery.py +69 -0
  77. aury/boot/contrib/admin_console/install.py +172 -0
  78. aury/boot/contrib/admin_console/utils.py +44 -0
  79. aury/boot/domain/__init__.py +79 -0
  80. aury/boot/domain/exceptions/__init__.py +132 -0
  81. aury/boot/domain/models/__init__.py +51 -0
  82. aury/boot/domain/models/base.py +69 -0
  83. aury/boot/domain/models/mixins.py +135 -0
  84. aury/boot/domain/models/models.py +96 -0
  85. aury/boot/domain/pagination/__init__.py +279 -0
  86. aury/boot/domain/repository/__init__.py +23 -0
  87. aury/boot/domain/repository/impl.py +423 -0
  88. aury/boot/domain/repository/interceptors.py +47 -0
  89. aury/boot/domain/repository/interface.py +106 -0
  90. aury/boot/domain/repository/query_builder.py +348 -0
  91. aury/boot/domain/service/__init__.py +11 -0
  92. aury/boot/domain/service/base.py +73 -0
  93. aury/boot/domain/transaction/__init__.py +404 -0
  94. aury/boot/infrastructure/__init__.py +104 -0
  95. aury/boot/infrastructure/cache/__init__.py +31 -0
  96. aury/boot/infrastructure/cache/backends.py +348 -0
  97. aury/boot/infrastructure/cache/base.py +68 -0
  98. aury/boot/infrastructure/cache/exceptions.py +37 -0
  99. aury/boot/infrastructure/cache/factory.py +94 -0
  100. aury/boot/infrastructure/cache/manager.py +274 -0
  101. aury/boot/infrastructure/database/__init__.py +39 -0
  102. aury/boot/infrastructure/database/config.py +71 -0
  103. aury/boot/infrastructure/database/exceptions.py +44 -0
  104. aury/boot/infrastructure/database/manager.py +317 -0
  105. aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
  106. aury/boot/infrastructure/database/strategies/__init__.py +198 -0
  107. aury/boot/infrastructure/di/__init__.py +15 -0
  108. aury/boot/infrastructure/di/container.py +393 -0
  109. aury/boot/infrastructure/events/__init__.py +33 -0
  110. aury/boot/infrastructure/events/bus.py +362 -0
  111. aury/boot/infrastructure/events/config.py +52 -0
  112. aury/boot/infrastructure/events/consumer.py +134 -0
  113. aury/boot/infrastructure/events/middleware.py +51 -0
  114. aury/boot/infrastructure/events/models.py +63 -0
  115. aury/boot/infrastructure/monitoring/__init__.py +529 -0
  116. aury/boot/infrastructure/scheduler/__init__.py +19 -0
  117. aury/boot/infrastructure/scheduler/exceptions.py +37 -0
  118. aury/boot/infrastructure/scheduler/manager.py +478 -0
  119. aury/boot/infrastructure/storage/__init__.py +38 -0
  120. aury/boot/infrastructure/storage/base.py +164 -0
  121. aury/boot/infrastructure/storage/exceptions.py +37 -0
  122. aury/boot/infrastructure/storage/factory.py +88 -0
  123. aury/boot/infrastructure/tasks/__init__.py +24 -0
  124. aury/boot/infrastructure/tasks/config.py +45 -0
  125. aury/boot/infrastructure/tasks/constants.py +37 -0
  126. aury/boot/infrastructure/tasks/exceptions.py +37 -0
  127. aury/boot/infrastructure/tasks/manager.py +490 -0
  128. aury/boot/testing/__init__.py +24 -0
  129. aury/boot/testing/base.py +122 -0
  130. aury/boot/testing/client.py +163 -0
  131. aury/boot/testing/factory.py +154 -0
  132. aury/boot/toolkit/__init__.py +21 -0
  133. aury/boot/toolkit/http/__init__.py +367 -0
  134. {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/METADATA +3 -2
  135. aury_boot-0.0.4.dist-info/RECORD +137 -0
  136. aury_boot-0.0.2.dist-info/RECORD +0 -5
  137. {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/WHEEL +0 -0
  138. {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,423 @@
1
+ """Domain 层仓储实现 - BaseRepository 的具体实现。
2
+
3
+ 提供通用的 CRUD 操作实现,是 IRepository 接口的具体实现。
4
+
5
+ **重要架构决策**:本模块在 domain 层实现了 IRepository 接口。
6
+ Infrastructure 层现在仅负责数据库连接管理,完全不依赖 domain 层。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+ from typing import TYPE_CHECKING, Any, Union, cast
13
+
14
+ from sqlalchemy import Select, delete, func, select
15
+ from sqlalchemy.ext.asyncio import AsyncSession
16
+
17
+ from aury.boot.common.logging import logger
18
+ from aury.boot.domain.exceptions import VersionConflictError
19
+ from aury.boot.domain.models import GUID, Base
20
+ from aury.boot.domain.transaction import _transaction_depth
21
+ from aury.boot.domain.pagination import (
22
+ PaginationParams,
23
+ PaginationResult,
24
+ SortParams,
25
+ )
26
+ from aury.boot.domain.repository.interface import IRepository
27
+ from aury.boot.domain.repository.query_builder import QueryBuilder
28
+
29
+ if TYPE_CHECKING:
30
+ from typing import Self
31
+
32
+
33
+ def _get_model_attr(model_class: type, attr_name: str) -> Any:
34
+ """安全地获取模型类的属性,避免类型检查器警告。
35
+
36
+ Args:
37
+ model_class: SQLAlchemy 模型类
38
+ attr_name: 属性名称
39
+
40
+ Returns:
41
+ 属性值(如果存在),否则返回 None
42
+ """
43
+ return cast(Any, getattr(model_class, attr_name, None))
44
+
45
+
46
+ class BaseRepository[ModelType: Base](IRepository[ModelType]):
47
+ """仓储基类实现。提供通用 CRUD 操作。
48
+
49
+ **重要**:使用此类的模型必须至少继承以下 Mixin 之一:
50
+ - IDMixin: 标准自增整数主键
51
+ - UUIDMixin: UUID 主键
52
+
53
+ 可选 Mixin:
54
+ - AuditableStateMixin: 软删除支持
55
+ - VersionMixin: 乐观锁支持
56
+ - TimestampMixin: 时间戳字段
57
+
58
+ 自动提交行为:
59
+ - auto_commit=True(默认):非事务中自动 commit
60
+ - auto_commit=False:只 flush,需要手动管理事务或使用 .with_commit()
61
+ - 在事务中(transactional_context 等):永远不自动 commit,由事务统一管理
62
+
63
+ 示例:
64
+ class User(IDMixin, TimestampMixin, AuditableStateMixin, Base):
65
+ __tablename__ = "users"
66
+ name: Mapped[str]
67
+
68
+ # 默认 auto_commit=True,非事务中自动提交
69
+ repo = UserRepository(session, User)
70
+ await repo.create({"name": "test"}) # 自动 commit
71
+
72
+ # auto_commit=False,需要手动管理
73
+ repo = UserRepository(session, User, auto_commit=False)
74
+ await repo.create({"name": "test"}) # 只 flush
75
+ await repo.with_commit().create({"name": "test2"}) # 强制 commit
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ session: AsyncSession,
81
+ model_class: type[ModelType],
82
+ auto_commit: bool = True,
83
+ ) -> None:
84
+ self._session = session
85
+ self._model_class = model_class
86
+ self._auto_commit = auto_commit
87
+ self._force_commit = False # 单次强制提交标记(用于 with_commit)
88
+ logger.debug(f"初始化 {self.__class__.__name__}")
89
+
90
+ @property
91
+ def session(self) -> AsyncSession:
92
+ return self._session
93
+
94
+ @property
95
+ def model_class(self) -> type[ModelType]:
96
+ return self._model_class
97
+
98
+ @property
99
+ def auto_commit(self) -> bool:
100
+ """是否启用自动提交。"""
101
+ return self._auto_commit
102
+
103
+ def with_commit(self) -> Self:
104
+ """返回强制提交的 Repository 视图。
105
+
106
+ 在 auto_commit=False 时,使用此方法可以强制单次操作提交。
107
+
108
+ 示例:
109
+ repo = UserRepository(session, User, auto_commit=False)
110
+ await repo.create({"name": "test"}) # 只 flush
111
+ await repo.with_commit().create({"name": "test2"}) # 强制 commit
112
+
113
+ Returns:
114
+ Self: 带强制提交标记的 Repository 副本
115
+ """
116
+ clone = self.__class__(
117
+ self._session,
118
+ self._model_class,
119
+ self._auto_commit,
120
+ )
121
+ clone._force_commit = True
122
+ return clone
123
+
124
+ async def _maybe_commit(self) -> None:
125
+ """根据配置决定是否提交。
126
+
127
+ 提交条件:
128
+ 1. 不在显式事务中(transactional_context / @transactional)
129
+ 2. 满足以下任一条件:
130
+ - _force_commit=True(来自 with_commit())
131
+ - _auto_commit=True(默认配置)
132
+
133
+ 注意:使用 _transaction_depth 而非 session.in_transaction() 判断,
134
+ 因为 SQLAlchemy 的 autobegin 会让 in_transaction() 在任何 SQL 执行后返回 True,
135
+ 但这不代表用户在显式事务中。
136
+ """
137
+ # 在显式事务中(transactional_context / @transactional),由事务统一管理
138
+ if _transaction_depth.get() > 0:
139
+ return
140
+
141
+ # 强制提交或自动提交
142
+ if self._force_commit or self._auto_commit:
143
+ await self._session.commit()
144
+ logger.debug("Repository 自动提交")
145
+
146
+ def query(self) -> QueryBuilder[ModelType]:
147
+ builder = QueryBuilder(self._model_class)
148
+ if hasattr(self._model_class, "deleted_at"):
149
+ deleted_at = _get_model_attr(self._model_class, "deleted_at")
150
+ builder.filter_by(deleted_at == 0)
151
+ return builder
152
+
153
+ def _build_base_query(self) -> Select:
154
+ query = select(self._model_class)
155
+ if hasattr(self._model_class, "deleted_at"):
156
+ deleted_at = _get_model_attr(self._model_class, "deleted_at")
157
+ query = query.where(deleted_at == 0)
158
+ return query
159
+
160
+ def _apply_filters(self, query: Select, **filters) -> Select:
161
+ """将简单和带操作符的 filters 应用于查询。
162
+
163
+ 支持键名后缀操作符(与 QueryBuilder.filter 一致):
164
+ - __gt, __lt, __gte, __lte, __in, __like, __ilike, __isnull, __ne
165
+ 例如:
166
+ - name__ilike="%foo%"
167
+ - age__gte=18
168
+ - id__in=[1,2,3]
169
+ - deleted_at__isnull=True
170
+
171
+ 注意:此处所有条件均以 AND 组合。更复杂的 AND/OR/NOT 组合请使用 repo.query()。
172
+ """
173
+ for key, value in filters.items():
174
+ if value is None:
175
+ continue
176
+
177
+ if "__" in key:
178
+ field_name, operator = key.rsplit("__", 1)
179
+ if not hasattr(self._model_class, field_name):
180
+ continue
181
+ field = _get_model_attr(self._model_class, field_name)
182
+
183
+ if operator == "isnull":
184
+ condition = field.is_(None) if bool(value) else field.isnot(None)
185
+ elif operator == "in":
186
+ condition = field.in_(value)
187
+ elif operator == "gt":
188
+ condition = field > value
189
+ elif operator == "lt":
190
+ condition = field < value
191
+ elif operator == "gte":
192
+ condition = field >= value
193
+ elif operator == "lte":
194
+ condition = field <= value
195
+ elif operator == "like":
196
+ condition = field.like(value)
197
+ elif operator == "ilike":
198
+ condition = field.ilike(value)
199
+ elif operator == "ne":
200
+ condition = field != value
201
+ else:
202
+ # 未知操作符,忽略
203
+ continue
204
+ query = query.where(condition)
205
+ else:
206
+ if hasattr(self._model_class, key):
207
+ query = query.where(getattr(self._model_class, key) == value)
208
+ return query
209
+
210
+ async def get(self, id: Union[int, GUID]) -> ModelType | None:
211
+ """按 ID 获取实体,支持 int 和 GUID。"""
212
+ if not hasattr(self._model_class, "id"):
213
+ raise AttributeError(f"模型 {self._model_class.__name__} 没有 'id' 字段,请继承 IDMixin 或 UUIDMixin")
214
+ id_attr = _get_model_attr(self._model_class, "id")
215
+ query = self._build_base_query().where(id_attr == id)
216
+ result = await self._session.execute(query)
217
+ return result.scalar_one_or_none()
218
+
219
+ async def get_by(self, **filters) -> ModelType | None:
220
+ query = self._build_base_query()
221
+ query = self._apply_filters(query, **filters)
222
+ result = await self._session.execute(query)
223
+ return result.scalar_one_or_none()
224
+
225
+ async def list(self, skip: int = 0, limit: int | None = 100, **filters) -> list[ModelType]:
226
+ query = self._build_base_query()
227
+ query = self._apply_filters(query, **filters)
228
+ query = query.offset(skip)
229
+ if limit is not None:
230
+ query = query.limit(limit)
231
+ result = await self._session.execute(query)
232
+ return list(result.scalars().all())
233
+
234
+ async def paginate(
235
+ self,
236
+ pagination_params: PaginationParams,
237
+ sort_params: SortParams | None = None,
238
+ **filters
239
+ ) -> PaginationResult[ModelType]:
240
+ query = self._build_base_query()
241
+ query = self._apply_filters(query, **filters)
242
+
243
+ if sort_params:
244
+ order_by_list = []
245
+ for field, direction in sort_params.sorts:
246
+ field_attr = getattr(self._model_class, field, None)
247
+ if field_attr is not None:
248
+ if direction == "desc":
249
+ order_by_list.append(field_attr.desc())
250
+ else:
251
+ order_by_list.append(field_attr)
252
+ if order_by_list:
253
+ query = query.order_by(*order_by_list)
254
+
255
+ query = query.offset(pagination_params.offset).limit(pagination_params.limit)
256
+ result = await self._session.execute(query)
257
+ items = list(result.scalars().all())
258
+ total_count = await self.count(**filters)
259
+
260
+ return PaginationResult.create(
261
+ items=items,
262
+ total=total_count,
263
+ pagination_params=pagination_params,
264
+ )
265
+
266
+ async def count(self, **filters) -> int:
267
+ query = select(func.count()).select_from(self._model_class)
268
+ if hasattr(self._model_class, "deleted_at"):
269
+ deleted_at = _get_model_attr(self._model_class, "deleted_at")
270
+ query = query.where(deleted_at == 0)
271
+ query = self._apply_filters(query, **filters)
272
+ result = await self._session.execute(query)
273
+ return result.scalar_one()
274
+
275
+ async def exists(self, **filters) -> bool:
276
+ return await self.count(**filters) > 0
277
+
278
+ async def add(self, entity: ModelType) -> ModelType:
279
+ self._session.add(entity)
280
+ await self._session.flush()
281
+ await self._session.refresh(entity)
282
+ await self._maybe_commit()
283
+ logger.debug(f"添加实体: {entity}")
284
+ return entity
285
+
286
+ async def create(self, data: dict[str, Any]) -> ModelType:
287
+ entity = self._model_class(**data)
288
+ # add() 已调用 _maybe_commit,此处无需重复
289
+ return await self.add(entity)
290
+
291
+ async def update(self, entity: ModelType, data: dict[str, Any] | None = None) -> ModelType:
292
+ if hasattr(entity, "version"):
293
+ entity_any = cast(Any, entity)
294
+ current_version = entity_any.version
295
+ await self._session.refresh(entity, ["version"])
296
+ if entity_any.version != current_version:
297
+ raise VersionConflictError(
298
+ message="数据已被其他操作修改,请刷新后重试",
299
+ current_version=entity_any.version,
300
+ expected_version=current_version,
301
+ )
302
+ entity_any.version = entity_any.version + 1
303
+
304
+ if data:
305
+ for key, value in data.items():
306
+ if hasattr(entity, key) and key != "version":
307
+ setattr(entity, key, value)
308
+
309
+ await self._session.flush()
310
+ await self._session.refresh(entity)
311
+ await self._maybe_commit()
312
+ logger.debug(f"更新实体: {entity}")
313
+ return entity
314
+
315
+ async def delete(self, entity: ModelType, soft: bool = True) -> None:
316
+ if soft:
317
+ if hasattr(entity, "deleted_at"):
318
+ # 使用 setattr 因为类型检查器不知道动态属性
319
+ setattr(entity, "deleted_at", int(time.time())) # noqa: B010
320
+ await self._session.flush()
321
+ await self._maybe_commit()
322
+ logger.debug(f"软删除实体: {entity}")
323
+ elif hasattr(entity, "mark_deleted"):
324
+ entity.mark_deleted()
325
+ await self._session.flush()
326
+ await self._maybe_commit()
327
+ logger.debug(f"软删除实体(使用方法): {entity}")
328
+ else:
329
+ await self._session.delete(entity)
330
+ await self._session.flush()
331
+ await self._maybe_commit()
332
+ logger.debug(f"硬删除实体(无软删除字段): {entity}")
333
+ else:
334
+ await self._session.delete(entity)
335
+ await self._session.flush()
336
+ await self._maybe_commit()
337
+ logger.debug(f"硬删除实体: {entity}")
338
+
339
+ async def mark_deleted(self, entity: ModelType) -> None:
340
+ await self.delete(entity, soft=True)
341
+
342
+ async def hard_delete(self, entity: ModelType) -> None:
343
+ await self.delete(entity, soft=False)
344
+
345
+ async def delete_by_id(self, id: int, soft: bool = True) -> bool:
346
+ entity = await self.get(id)
347
+ if entity:
348
+ await self.delete(entity, soft=soft)
349
+ return True
350
+ return False
351
+
352
+ async def batch_create(self, data_list: list[dict[str, Any]]) -> list[ModelType]:
353
+ entities = [self._model_class(**data) for data in data_list]
354
+ self._session.add_all(entities)
355
+ await self._session.flush()
356
+ for entity in entities:
357
+ await self._session.refresh(entity)
358
+ await self._maybe_commit()
359
+ logger.debug(f"批量创建 {len(entities)} 个实体")
360
+ return entities
361
+
362
+ async def bulk_insert(self, data_list: list[dict[str, Any]]) -> None:
363
+ if not data_list:
364
+ return
365
+ await self._session.execute(
366
+ self._model_class.__table__.insert(),
367
+ data_list
368
+ )
369
+ await self._session.flush()
370
+ await self._maybe_commit()
371
+ logger.debug(f"批量插入 {len(data_list)} 条记录")
372
+
373
+ async def bulk_update(self, data_list: list[dict[str, Any]],
374
+ index_elements: list[str] | None = None) -> None:
375
+ if not data_list:
376
+ return
377
+ if index_elements is None:
378
+ index_elements = ["id"]
379
+ for data in data_list:
380
+ for field in index_elements:
381
+ if field not in data:
382
+ raise ValueError(f"批量更新数据缺少索引字段: {field}")
383
+ await self._session.bulk_update_mappings(self._model_class, data_list)
384
+ await self._session.flush()
385
+ await self._maybe_commit()
386
+ logger.debug(f"批量更新 {len(data_list)} 条记录")
387
+
388
+ async def bulk_delete(self, filters: dict[str, Any] | None = None) -> int:
389
+ query = delete(self._model_class)
390
+ if hasattr(self._model_class, "deleted_at"):
391
+ deleted_at = _get_model_attr(self._model_class, "deleted_at")
392
+ query = query.where(deleted_at == 0)
393
+ if filters:
394
+ for key, value in filters.items():
395
+ if hasattr(self._model_class, key) and value is not None:
396
+ attr = _get_model_attr(self._model_class, key)
397
+ query = query.where(attr == value)
398
+ result = await self._session.execute(query)
399
+ await self._session.flush()
400
+ await self._maybe_commit()
401
+ deleted_count = cast(Any, result).rowcount
402
+ logger.debug(f"批量删除 {deleted_count} 条记录")
403
+ return deleted_count
404
+
405
+ def __repr__(self) -> str:
406
+ return f"<{self.__class__.__name__} model={self._model_class.__name__}>"
407
+
408
+
409
+ class SimpleRepository:
410
+ """简单Repository基类。"""
411
+
412
+ def __init__(self, session: AsyncSession) -> None:
413
+ self._session = session
414
+
415
+ @property
416
+ def session(self) -> AsyncSession:
417
+ return self._session
418
+
419
+ def __repr__(self) -> str:
420
+ return f"<{self.__class__.__name__}>"
421
+
422
+
423
+ __all__ = ["BaseRepository", "SimpleRepository"]
@@ -0,0 +1,47 @@
1
+ """查询拦截器接口。
2
+
3
+ 用于在查询执行前后执行自定义逻辑,如审计、日志记录等。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import Any
10
+
11
+ from sqlalchemy import Select
12
+
13
+
14
+ class QueryInterceptor(ABC):
15
+ """查询拦截器接口。
16
+
17
+ 用于在查询执行前后执行自定义逻辑,如审计、日志记录等。
18
+
19
+ 用法:
20
+ class AuditInterceptor(QueryInterceptor):
21
+ async def before_query(self, query, **kwargs):
22
+ logger.info(f"执行查询: {query}")
23
+
24
+ async def after_query(self, result, **kwargs):
25
+ logger.info(f"查询结果: {len(result)} 条记录")
26
+ """
27
+
28
+ @abstractmethod
29
+ async def before_query(self, query: Select, **kwargs: Any) -> None:
30
+ """查询执行前的钩子。
31
+
32
+ Args:
33
+ query: SQLAlchemy 查询对象
34
+ **kwargs: 其他参数
35
+ """
36
+ pass
37
+
38
+ @abstractmethod
39
+ async def after_query(self, result: Any, **kwargs: Any) -> None:
40
+ """查询执行后的钩子。
41
+
42
+ Args:
43
+ result: 查询结果
44
+ **kwargs: 其他参数
45
+ """
46
+ pass
47
+
@@ -0,0 +1,106 @@
1
+ """仓储接口定义。
2
+
3
+ 定义所有Repository必须实现的接口。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from aury.boot.domain.models import Base, GUID
12
+ from aury.boot.domain.pagination import PaginationParams, PaginationResult, SortParams
13
+
14
+ if TYPE_CHECKING:
15
+ from typing import Self
16
+
17
+ from aury.boot.domain.repository.query_builder import QueryBuilder
18
+
19
+
20
+ class IRepository[ModelType: Base](ABC):
21
+ """仓储接口定义。
22
+
23
+ 定义所有Repository必须实现的方法。
24
+ 遵循接口隔离原则,只定义必要的方法。
25
+ """
26
+
27
+ @abstractmethod
28
+ async def get(self, id: int | GUID) -> ModelType | None:
29
+ """根据ID获取实体。支持 int 和 GUID。"""
30
+ pass
31
+
32
+ @abstractmethod
33
+ async def get_by(self, **filters) -> ModelType | None:
34
+ """根据条件获取单个实体。"""
35
+ pass
36
+
37
+ @abstractmethod
38
+ async def list(self, skip: int = 0, limit: int | None = 100, **filters) -> list[ModelType]:
39
+ """获取实体列表。limit=None 时返回全部记录(谨慎使用)。"""
40
+ pass
41
+
42
+ @abstractmethod
43
+ async def paginate(
44
+ self,
45
+ pagination_params: PaginationParams,
46
+ sort_params: SortParams | None = None,
47
+ **filters
48
+ ) -> PaginationResult[ModelType]:
49
+ """分页获取实体列表。
50
+
51
+ Args:
52
+ pagination_params: 分页参数
53
+ sort_params: 排序参数
54
+ **filters: 过滤条件
55
+
56
+ Returns:
57
+ PaginationResult[ModelType]: 分页结果
58
+ """
59
+ pass
60
+
61
+ @abstractmethod
62
+ async def count(self, **filters) -> int:
63
+ """统计实体数量。"""
64
+ pass
65
+
66
+ @abstractmethod
67
+ async def exists(self, **filters) -> bool:
68
+ """检查实体是否存在。"""
69
+ pass
70
+
71
+ @abstractmethod
72
+ async def add(self, entity: ModelType) -> ModelType:
73
+ """添加实体。"""
74
+ pass
75
+
76
+ @abstractmethod
77
+ async def create(self, data: dict[str, Any]) -> ModelType:
78
+ """创建实体。"""
79
+ pass
80
+
81
+ @abstractmethod
82
+ async def update(self, entity: ModelType, data: dict[str, Any]) -> ModelType:
83
+ """更新实体。"""
84
+ pass
85
+
86
+ @abstractmethod
87
+ async def delete(self, entity: ModelType, soft: bool = True) -> None:
88
+ """删除实体。"""
89
+ pass
90
+
91
+ @abstractmethod
92
+ def query(self) -> QueryBuilder[ModelType]:
93
+ """创建查询构建器。"""
94
+ pass
95
+
96
+ @abstractmethod
97
+ def with_commit(self) -> Self:
98
+ """返回强制提交的 Repository 视图。
99
+
100
+ 在 auto_commit=False 时,使用此方法可以强制单次操作提交。
101
+
102
+ Returns:
103
+ Self: 带强制提交标记的 Repository 副本
104
+ """
105
+ pass
106
+