aury-boot 0.0.27__py3-none-any.whl → 0.0.28__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.
- aury/boot/_version.py +2 -2
- aury/boot/commands/templates/project/AGENTS.md.tpl +2 -0
- aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +31 -0
- aury/boot/domain/repository/impl.py +164 -0
- {aury_boot-0.0.27.dist-info → aury_boot-0.0.28.dist-info}/METADATA +1 -1
- {aury_boot-0.0.27.dist-info → aury_boot-0.0.28.dist-info}/RECORD +8 -8
- {aury_boot-0.0.27.dist-info → aury_boot-0.0.28.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.27.dist-info → aury_boot-0.0.28.dist-info}/entry_points.txt +0 -0
aury/boot/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
31
|
+
__version__ = version = '0.0.28'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 28)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -75,6 +75,8 @@ mypy {package_name}/
|
|
|
75
75
|
2. **[aury_docs/02-repository.md](./aury_docs/02-repository.md)** - Repository 使用
|
|
76
76
|
- BaseRepository API
|
|
77
77
|
- Filters 语法(__gt, __like 等)
|
|
78
|
+
- Cursor 分页(推荐,性能更优)
|
|
79
|
+
- 流式查询(大数据处理)
|
|
78
80
|
- 自动提交机制
|
|
79
81
|
|
|
80
82
|
3. **[aury_docs/03-service.md](./aury_docs/03-service.md)** - Service 编写与事务
|
|
@@ -72,6 +72,37 @@ result = await repo.paginate(
|
|
|
72
72
|
# - result.has_next: bool # 是否有下一页
|
|
73
73
|
# - result.has_prev: bool # 是否有上一页
|
|
74
74
|
|
|
75
|
+
# === Cursor 分页(推荐,性能更优) ===
|
|
76
|
+
from aury.boot.domain.pagination import CursorPaginationParams
|
|
77
|
+
|
|
78
|
+
# 第一页
|
|
79
|
+
result = await repo.cursor_paginate(
|
|
80
|
+
CursorPaginationParams(limit=20),
|
|
81
|
+
is_active=True,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# 下一页(带上 cursor)
|
|
85
|
+
result = await repo.cursor_paginate(
|
|
86
|
+
CursorPaginationParams(cursor=result.next_cursor, limit=20),
|
|
87
|
+
is_active=True,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# CursorPaginationResult 结构:
|
|
91
|
+
# - result.items: list[T] # 数据列表
|
|
92
|
+
# - result.next_cursor: str | None # 下一页游标
|
|
93
|
+
# - result.prev_cursor: str | None # 上一页游标
|
|
94
|
+
# - result.has_next: bool # 是否有下一页
|
|
95
|
+
# - result.has_prev: bool # 是否有上一页
|
|
96
|
+
|
|
97
|
+
# === 流式查询(大数据处理) ===
|
|
98
|
+
# 逐条流式处理,不会一次性加载到内存
|
|
99
|
+
async for user in repo.stream(batch_size=1000, is_active=True):
|
|
100
|
+
await process(user)
|
|
101
|
+
|
|
102
|
+
# 批量流式处理
|
|
103
|
+
async for batch in repo.stream_batches(batch_size=1000):
|
|
104
|
+
await bulk_sync_to_es(batch)
|
|
105
|
+
|
|
75
106
|
# === 创建 ===
|
|
76
107
|
user = await repo.create({{"name": "Alice", "email": "a@b.com"}})
|
|
77
108
|
users = await repo.batch_create([{{"name": "A"}}, {{"name": "B"}}]) # 返回实体
|
|
@@ -18,6 +18,8 @@ from aury.boot.common.logging import logger
|
|
|
18
18
|
from aury.boot.domain.exceptions import VersionConflictError
|
|
19
19
|
from aury.boot.domain.models import GUID, Base
|
|
20
20
|
from aury.boot.domain.pagination import (
|
|
21
|
+
CursorPaginationParams,
|
|
22
|
+
CursorPaginationResult,
|
|
21
23
|
PaginationParams,
|
|
22
24
|
PaginationResult,
|
|
23
25
|
SortParams,
|
|
@@ -263,6 +265,168 @@ class BaseRepository[ModelType: Base](IRepository[ModelType]):
|
|
|
263
265
|
pagination_params=pagination_params,
|
|
264
266
|
)
|
|
265
267
|
|
|
268
|
+
async def cursor_paginate(
|
|
269
|
+
self,
|
|
270
|
+
params: CursorPaginationParams,
|
|
271
|
+
cursor_field: str = "id",
|
|
272
|
+
**filters
|
|
273
|
+
) -> CursorPaginationResult[ModelType]:
|
|
274
|
+
"""基于游标的分页查询。
|
|
275
|
+
|
|
276
|
+
适用于无限滚动、大数据集等场景,性能优于 offset 分页。
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
params: 游标分页参数(cursor, limit, direction)
|
|
280
|
+
cursor_field: 游标字段名,默认 "id",必须是有序且唯一的字段
|
|
281
|
+
**filters: 过滤条件
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
CursorPaginationResult: 包含 items, next_cursor, prev_cursor, has_next, has_prev
|
|
285
|
+
|
|
286
|
+
示例:
|
|
287
|
+
# 第一页
|
|
288
|
+
result = await repo.cursor_paginate(
|
|
289
|
+
CursorPaginationParams(limit=20),
|
|
290
|
+
status="active"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# 下一页
|
|
294
|
+
result = await repo.cursor_paginate(
|
|
295
|
+
CursorPaginationParams(cursor=result.next_cursor, limit=20),
|
|
296
|
+
status="active"
|
|
297
|
+
)
|
|
298
|
+
"""
|
|
299
|
+
import base64
|
|
300
|
+
import json
|
|
301
|
+
|
|
302
|
+
query = self._build_base_query()
|
|
303
|
+
query = self._apply_filters(query, **filters)
|
|
304
|
+
|
|
305
|
+
cursor_attr = _get_model_attr(self._model_class, cursor_field)
|
|
306
|
+
if cursor_attr is None:
|
|
307
|
+
raise AttributeError(f"模型 {self._model_class.__name__} 没有 '{cursor_field}' 字段")
|
|
308
|
+
|
|
309
|
+
# 解码 cursor
|
|
310
|
+
cursor_value = None
|
|
311
|
+
if params.cursor:
|
|
312
|
+
try:
|
|
313
|
+
decoded = base64.urlsafe_b64decode(params.cursor.encode()).decode()
|
|
314
|
+
cursor_value = json.loads(decoded)
|
|
315
|
+
except Exception:
|
|
316
|
+
raise ValueError("无效的游标") from None
|
|
317
|
+
|
|
318
|
+
# 根据方向决定查询条件和排序
|
|
319
|
+
is_next = params.direction == "next"
|
|
320
|
+
if cursor_value is not None:
|
|
321
|
+
if is_next:
|
|
322
|
+
query = query.where(cursor_attr > cursor_value)
|
|
323
|
+
else:
|
|
324
|
+
query = query.where(cursor_attr < cursor_value)
|
|
325
|
+
|
|
326
|
+
# 排序
|
|
327
|
+
if is_next:
|
|
328
|
+
query = query.order_by(cursor_attr)
|
|
329
|
+
else:
|
|
330
|
+
query = query.order_by(cursor_attr.desc())
|
|
331
|
+
|
|
332
|
+
# 多取一条判断是否有更多
|
|
333
|
+
query = query.limit(params.limit + 1)
|
|
334
|
+
result = await self._session.execute(query)
|
|
335
|
+
items = list(result.scalars().all())
|
|
336
|
+
|
|
337
|
+
has_more = len(items) > params.limit
|
|
338
|
+
if has_more:
|
|
339
|
+
items = items[:params.limit]
|
|
340
|
+
|
|
341
|
+
# 反向查询时反转结果
|
|
342
|
+
if not is_next:
|
|
343
|
+
items = list(reversed(items))
|
|
344
|
+
|
|
345
|
+
# 生成游标
|
|
346
|
+
def encode_cursor(value) -> str:
|
|
347
|
+
return base64.urlsafe_b64encode(json.dumps(value).encode()).decode()
|
|
348
|
+
|
|
349
|
+
next_cursor = None
|
|
350
|
+
prev_cursor = None
|
|
351
|
+
|
|
352
|
+
if items:
|
|
353
|
+
last_value = getattr(items[-1], cursor_field)
|
|
354
|
+
first_value = getattr(items[0], cursor_field)
|
|
355
|
+
|
|
356
|
+
if is_next:
|
|
357
|
+
if has_more:
|
|
358
|
+
next_cursor = encode_cursor(last_value)
|
|
359
|
+
if cursor_value is not None:
|
|
360
|
+
prev_cursor = encode_cursor(first_value)
|
|
361
|
+
else:
|
|
362
|
+
if cursor_value is not None:
|
|
363
|
+
next_cursor = encode_cursor(last_value)
|
|
364
|
+
if has_more:
|
|
365
|
+
prev_cursor = encode_cursor(first_value)
|
|
366
|
+
|
|
367
|
+
return CursorPaginationResult.create(
|
|
368
|
+
items=items,
|
|
369
|
+
next_cursor=next_cursor,
|
|
370
|
+
prev_cursor=prev_cursor,
|
|
371
|
+
limit=params.limit,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
async def stream(
|
|
375
|
+
self,
|
|
376
|
+
batch_size: int = 1000,
|
|
377
|
+
**filters
|
|
378
|
+
):
|
|
379
|
+
"""流式查询,使用数据库原生 server-side cursor。
|
|
380
|
+
|
|
381
|
+
适用于大数据集处理,避免一次性加载所有数据到内存。
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
batch_size: 每批次获取的记录数,默认 1000
|
|
385
|
+
**filters: 过滤条件
|
|
386
|
+
|
|
387
|
+
Yields:
|
|
388
|
+
ModelType: 模型实例
|
|
389
|
+
|
|
390
|
+
示例:
|
|
391
|
+
async for user in repo.stream(batch_size=500, status="active"):
|
|
392
|
+
process(user)
|
|
393
|
+
"""
|
|
394
|
+
query = self._build_base_query()
|
|
395
|
+
query = self._apply_filters(query, **filters)
|
|
396
|
+
|
|
397
|
+
async with self._session.stream_scalars(
|
|
398
|
+
query.execution_options(yield_per=batch_size)
|
|
399
|
+
) as result:
|
|
400
|
+
async for item in result:
|
|
401
|
+
yield item
|
|
402
|
+
|
|
403
|
+
async def stream_batches(
|
|
404
|
+
self,
|
|
405
|
+
batch_size: int = 1000,
|
|
406
|
+
**filters
|
|
407
|
+
):
|
|
408
|
+
"""批量流式查询,每次返回一批数据。
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
batch_size: 每批次的记录数,默认 1000
|
|
412
|
+
**filters: 过滤条件
|
|
413
|
+
|
|
414
|
+
Yields:
|
|
415
|
+
list[ModelType]: 一批模型实例
|
|
416
|
+
|
|
417
|
+
示例:
|
|
418
|
+
async for batch in repo.stream_batches(batch_size=500):
|
|
419
|
+
bulk_process(batch)
|
|
420
|
+
"""
|
|
421
|
+
query = self._build_base_query()
|
|
422
|
+
query = self._apply_filters(query, **filters)
|
|
423
|
+
|
|
424
|
+
async with self._session.stream_scalars(
|
|
425
|
+
query.execution_options(yield_per=batch_size)
|
|
426
|
+
) as result:
|
|
427
|
+
async for partition in result.partitions():
|
|
428
|
+
yield list(partition)
|
|
429
|
+
|
|
266
430
|
async def count(self, **filters) -> int:
|
|
267
431
|
query = select(func.count()).select_from(self._model_class)
|
|
268
432
|
if hasattr(self._model_class, "deleted_at"):
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
aury/boot/__init__.py,sha256=pCno-EInnpIBa1OtxNYF-JWf9j95Cd2h6vmu0xqa_-4,1791
|
|
2
|
-
aury/boot/_version.py,sha256=
|
|
2
|
+
aury/boot/_version.py,sha256=rsZxmANBVfE-nN63y-ZZ_71Lkm6YP4u4TgVEBJV3mNM,706
|
|
3
3
|
aury/boot/application/__init__.py,sha256=0o_XmiwFCeAu06VHggS8I1e7_nSMoRq0Hcm0fYfCywU,3071
|
|
4
4
|
aury/boot/application/adapter/__init__.py,sha256=e1bcSb1bxUMfofTwiCuHBZJk5-STkMCWPF2EJXHQ7UU,3976
|
|
5
5
|
aury/boot/application/adapter/base.py,sha256=Ar_66fiHPDEmV-1DKnqXKwc53p3pozG31bgTJTEUriY,15763
|
|
@@ -61,7 +61,7 @@ aury/boot/commands/templates/generate/model.py.tpl,sha256=knFwMyGZ7wMpzH4_bQD_V1
|
|
|
61
61
|
aury/boot/commands/templates/generate/repository.py.tpl,sha256=xoEg6lPAaLIRDeFy4I0FBsPPVLSy91h6xosAlaCL_mM,590
|
|
62
62
|
aury/boot/commands/templates/generate/schema.py.tpl,sha256=HIaY5B0UG_S188nQLrZDEJ0q73WPdb7BmCdc0tseZA4,545
|
|
63
63
|
aury/boot/commands/templates/generate/service.py.tpl,sha256=2hwQ8e4a5d_bIMx_jGDobdmKPMFLBlfQrQVQH4Ym5k4,1842
|
|
64
|
-
aury/boot/commands/templates/project/AGENTS.md.tpl,sha256
|
|
64
|
+
aury/boot/commands/templates/project/AGENTS.md.tpl,sha256=-KrVhPqwVaf1IWRIl4L9Yb0LSRihq69ZaZ-ynnGM4W8,7882
|
|
65
65
|
aury/boot/commands/templates/project/README.md.tpl,sha256=oCeBiukk6Pa3hrCKybkfM2sIRHsPZ15nlwuFTUSFDwY,2459
|
|
66
66
|
aury/boot/commands/templates/project/admin_console_init.py.tpl,sha256=K81L14thyEhRA8lFCQJVZL_NU22-sBz0xS68MJPeoCo,1541
|
|
67
67
|
aury/boot/commands/templates/project/config.py.tpl,sha256=H_B05FypBJxTjb7qIL91zC1C9e37Pk7C9gO0-b3CqNs,1009
|
|
@@ -70,7 +70,7 @@ aury/boot/commands/templates/project/gitignore.tpl,sha256=OI0nt9u2E9EC-jAMoh3gpq
|
|
|
70
70
|
aury/boot/commands/templates/project/main.py.tpl,sha256=Q61ve3o1VkNPv8wcQK7lUosne18JWYeItxoXVNNoYJM,1070
|
|
71
71
|
aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl,sha256=8Aept3yEAe9cVdRvkddr_oEF-lr2riPXYRzBuw_6DBA,2138
|
|
72
72
|
aury/boot/commands/templates/project/aury_docs/01-model.md.tpl,sha256=1mQ3hGDxqEZjev4CD5-3dzYRFVonPNcAaStI1UBEUyM,6811
|
|
73
|
-
aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl,sha256=
|
|
73
|
+
aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl,sha256=JfUVdrIgW7_J6JGCcB-_uP_x-gCtjKiewwGv4Xr44QI,7803
|
|
74
74
|
aury/boot/commands/templates/project/aury_docs/03-service.md.tpl,sha256=Dg_8RGSeRmmyQrhhpppEoxl-6C5pNe9M2OzVOl1kjSk,13102
|
|
75
75
|
aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl,sha256=ZwwKhUbLI--PEEmwnuo2fIZrhCEZagBN6fRNDTFCnNk,2891
|
|
76
76
|
aury/boot/commands/templates/project/aury_docs/05-api.md.tpl,sha256=oPzda3V6ZPDDEW-5MwyzmsMRuu5mXrsRGEq3lj0M-58,2997
|
|
@@ -124,7 +124,7 @@ aury/boot/domain/models/mixins.py,sha256=7s4m4fzt0vWX71aTHgsoagjxSZZZ4_xSea_m0D8
|
|
|
124
124
|
aury/boot/domain/models/models.py,sha256=hNze58wPZkZ8QG2_pyszDsyKNjz2UgiRDzmneiCWLQs,2728
|
|
125
125
|
aury/boot/domain/pagination/__init__.py,sha256=HSU_NyLP-ij7ZDUi-ARSSvNkvhW1_wON2Zvu2QlF6HM,11890
|
|
126
126
|
aury/boot/domain/repository/__init__.py,sha256=dnmN8xFu1ASbLnzL6vx8gMoch7xBGxkJkxs9G1iGLGg,490
|
|
127
|
-
aury/boot/domain/repository/impl.py,sha256=
|
|
127
|
+
aury/boot/domain/repository/impl.py,sha256=BeWe6pMBP_OvxX91FmckxAZlM2qejol06If_UBy8FtU,22016
|
|
128
128
|
aury/boot/domain/repository/interceptors.py,sha256=SCTjRmBYwevAMlJ8U1uw-_McsDetNNQ7q0Da5lmfj_E,1238
|
|
129
129
|
aury/boot/domain/repository/interface.py,sha256=CmkiqVhhHPx_xcpuBCz11Vr26-govwYBxFsQ8myEVyw,2904
|
|
130
130
|
aury/boot/domain/repository/query_builder.py,sha256=pFErMzsBql-T6gBX0S4FxIheCkNaGjpSewzcJ2DxrUU,10890
|
|
@@ -192,7 +192,7 @@ aury/boot/testing/client.py,sha256=KOg1EemuIVsBG68G5y0DjSxZGcIQVdWQ4ASaHE3o1R0,4
|
|
|
192
192
|
aury/boot/testing/factory.py,sha256=8GvwX9qIDu0L65gzJMlrWB0xbmJ-7zPHuwk3eECULcg,5185
|
|
193
193
|
aury/boot/toolkit/__init__.py,sha256=AcyVb9fDf3CaEmJPNkWC4iGv32qCPyk4BuFKSuNiJRQ,334
|
|
194
194
|
aury/boot/toolkit/http/__init__.py,sha256=zIPmpIZ9Qbqe25VmEr7jixoY2fkRbLm7NkCB9vKpg6I,11039
|
|
195
|
-
aury_boot-0.0.
|
|
196
|
-
aury_boot-0.0.
|
|
197
|
-
aury_boot-0.0.
|
|
198
|
-
aury_boot-0.0.
|
|
195
|
+
aury_boot-0.0.28.dist-info/METADATA,sha256=7fabDsArEBQ_L5MoamQPJa9xmdi19-CLTSGG5YgPQPw,7695
|
|
196
|
+
aury_boot-0.0.28.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
197
|
+
aury_boot-0.0.28.dist-info/entry_points.txt,sha256=f9KXEkDIGc0BGkgBvsNx_HMz9VhDjNxu26q00jUpDwQ,49
|
|
198
|
+
aury_boot-0.0.28.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|