diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0f4bb95..e8ec3d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: - id: check-added-large-files - id: end-of-file-fixer - id: check-toml - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.7.2 + rev: v0.11.5 hooks: - id: ruff args: @@ -16,7 +16,7 @@ repos: - id: ruff-format - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.4.29 + rev: 0.6.14 hooks: - id: uv-lock - id: uv-export diff --git a/README.md b/README.md index f9f0f17..071d693 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # sqlalchemy-crud-plus -Asynchronous CRUD operations based on SQLAlChemy 2.0 +基于 SQLAlChemy 2.0 的异步 CRUD 操作 ## Download @@ -14,7 +14,7 @@ pip install sqlalchemy-crud-plus ## 互动 -[TG / Discord](https://wu-clan.github.io/homepage/) +[Discord](https://wu-clan.github.io/homepage/) ## 赞助 diff --git a/docs/advanced/commit.md b/docs/advanced/commit.md index 8be5303..17a43f3 100644 --- a/docs/advanced/commit.md +++ b/docs/advanced/commit.md @@ -1,4 +1,4 @@ -SQLAlchemy CRUD Plus 内部很多方法都提供了 `commit` 参数,默认值为 `False`,它既不会执行提交操作,也不包含 `flush` +SQLAlchemy CRUD Plus 内部为很多方法都提供了 `commit` 参数,默认值为 `False`,它既不会执行提交操作,也不包含 `flush` 等行为,要想真正写入到数据库,你可以通过以下几种方案 ## `commit=True` @@ -9,6 +9,8 @@ SQLAlchemy CRUD Plus 将在内部自动执行提交 ```py hl_lines="31" from fastapi import FastAPI, Depends +from pydantic import BaseModel + --8<-- "docs/ext/get_db.py" app = FastAPI() @@ -32,7 +34,6 @@ async def create(self, obj: CreateIns, db: AsyncSession = Depends(get_db)) -> No ```py hl_lines="9" --8<-- "docs/ext/async_db_session.py" - async def create(self, obj: CreateIns) -> None: async with async_db_session.begin() as db: await self.create_model(db, obj) diff --git a/docs/advanced/filter.md b/docs/advanced/filter.md index 6e0c790..c35d505 100644 --- a/docs/advanced/filter.md +++ b/docs/advanced/filter.md @@ -1,15 +1,5 @@ SQLAlchemy CRUD Plus 支持高级过滤选项,允许使用运算符查询记录,如大于(`__gt`)、小于(`__lt`); -大多数过滤器操作符需要一个字符串或整数值 - -```python -# 获取年龄大于 30 岁以上的员工 -items = await item_crud.select_models( - session=db, - age__gt=30, -) -``` - ## 比较运算符 - `__gt`:大于 @@ -20,6 +10,14 @@ items = await item_crud.select_models( - `__ne`: 不等于 - `__between`: 在两者之间 +```python title="e.g." +# 获取年龄大于 30 岁以上的员工 +items = await item_crud.select_models( + session=db, + age__gt=30, +) +``` + ## IN 比较 - `__in`: 包含 @@ -30,7 +28,7 @@ items = await item_crud.select_models( - `__is`:用于测试 “真”、“假” 和 “无”。 - `__is_not`:“is” 的否定 - `__is_distinct_from`: 产生 SQL IS DISTINCT FROM -- `__is_not_distinct_from`: Produces SQL IS NOT DISTINCT FROM +- `__is_not_distinct_from`: 产生 SQL IS NOT DISTINCT FROM - `__like`:针对特定文本模式的 SQL “like” 搜索 - `__not_like`:“like” 的否定 - `__ilike`:大小写不敏感的 “like” @@ -52,7 +50,13 @@ items = await item_crud.select_models( ## 算术运算符 -此过滤器使用方法需查看:[算数](#_7) +!!! note + + 此过滤器必须传递字典,且字典结构必须为 `{'value': xxx, 'condition': {'已支持的过滤器': xxx}}` + + `value`:此值将与列值进行条件运算 + + `condition`:此值将作为运算条件和预期结果 - `__add`: Python `+` 运算符 - `__radd`: Python `+` 反向运算 @@ -67,6 +71,14 @@ items = await item_crud.select_models( - `__mod`: Python `%` 运算符 - `__rmod`: Python `%` 反向运算 +```python title="e.g." +# 获取薪资打八折以后仍高于 20k 的员工 +items = await item_crud.select_models( + session=db, + payroll__mul={'value': 0.8, 'condition': {'gt': 20000}}, +) +``` + ## BETWEEN、IN、NOT IN !!! note @@ -84,7 +96,7 @@ items = await item_crud.select_models( ## AND -可以通过将多个过滤器链接在一起来实现 AND 子句 +当多个过滤器同时存在时,将自动转为 AND 子句 ```python # 获取年龄在 30 以上,薪资大于 20k 的员工 @@ -142,21 +154,3 @@ items = await item_crud.select_models( ] ) ``` - -## 算数 - -!!! note - - 此过滤器必须传递字典,且字典结构必须为 `{'value': xxx, 'condition': {'已支持的过滤器': xxx}}` - - `value`:此值将与列值进行运算 - - `condition`:此值将作为运算后的比较值,比较条件取决于使用的过滤器 - -```python -# 获取薪资打八折以后仍高于 20k 的员工 -items = await item_crud.select_models( - session=db, - payroll__mul={'value': 0.8, 'condition': {'gt': 20000}}, -) -``` diff --git a/docs/advanced/primary_key.md b/docs/advanced/primary_key.md index efb2b3c..e5fc028 100644 --- a/docs/advanced/primary_key.md +++ b/docs/advanced/primary_key.md @@ -1,6 +1,6 @@ !!! note 主键参数命名 - 由于在 python 内部 id 的特殊性,我们设定 pk (参考 Django) 作为模型主键命名,所以在 crud 方法中,任何涉及到主键的地方,入参都为 `pk` + 由于在 python 内部 `id` 为关键字,因此,我们设定默认主键入参为 `pk`。这仅用于函数入参,并不要求模型主键必须定义为 `pk` ```py title="e.g." hl_lines="2" async def delete(self, db: AsyncSession, primary_key: int) -> int: @@ -12,11 +12,10 @@ async def delete(self, db: AsyncSession, primary_key: int) -> int: !!! tip 自动主键 我们在 SQLAlchemy CRUD Plus 内部通过 [inspect()](https://docs.sqlalchemy.org/en/20/core/inspection.html) 自动搜索表主键, - 而非强制绑定主键列必须命名为 id,感谢 [@DavidSche](https://github.com/DavidSche) 提供帮助 + 而非强制绑定主键列必须命名为 `id` ```py title="e.g." hl_lines="4" class ModelIns(Base): - # your sqlalchemy model - # define your primary_key - custom_id: Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True) + # define primary_key + primary_key: Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True) ``` diff --git a/docs/ext/get_db.py b/docs/ext/get_db.py index c335da6..e7a1ad4 100644 --- a/docs/ext/get_db.py +++ b/docs/ext/get_db.py @@ -1,18 +1,14 @@ -from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from typing import AsyncGenerator + +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine async_engine = create_async_engine('数据库连接', future=True) async_db_session = async_sessionmaker(async_engine, autoflush=False, expire_on_commit=False) -async def get_db() -> AsyncSession: +async def get_db() -> AsyncGenerator: """ session 生成器 """ - session = async_db_session() - try: + async with async_db_session() as session: yield session - except Exception as se: - await session.rollback() - raise se - finally: - await session.close() diff --git a/docs/ext/model.py b/docs/ext/model.py deleted file mode 100644 index 56fafa5..0000000 --- a/docs/ext/model.py +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- diff --git a/docs/usage/count.md b/docs/usage/count.md new file mode 100644 index 0000000..c95b53a --- /dev/null +++ b/docs/usage/count.md @@ -0,0 +1,46 @@ +查询符合条件过滤的记录数 + +```py title="count" hl_lines="14" +from sqlalchemy_crud_plus import CRUDPlus + +from sqlalchemy import DeclarativeBase as Base +from sqlalchemy.ext.asyncio import AsyncSession + + +class ModelIns(Base): + # your sqlalchemy model + pass + + +class CRUDIns(CRUDPlus[ModelIns]): + async def create(self, db: AsyncSession) -> int: + return await self.count(db, name="foo") +``` + +## API + +```python +async def count( + self, + session: AsyncSession, + filters: ColumnElement | list[ColumnElement] | None = None, + **kwargs, +) -> int: +``` + +**Parameters:** + +| Name | Type | Description | Default | +|---------|----------------------------------------------------|------------------|---------| +| session | AsyncSession | 数据库会话 | 必填 | +| filters | `ColumnElement `\|` list[ColumnElement] `\|` None` | 要应用于查询的 WHERE 子句 | `None` | + +!!! note "**kwargs" + + [条件过滤](../advanced/filter.md),将创建条件查询 SQL + +**Returns:** + +| Type | Description | +|------|-------------| +| int | 记录数 | diff --git a/docs/usage/create_model.md b/docs/usage/create_model.md index bf08407..1374788 100644 --- a/docs/usage/create_model.md +++ b/docs/usage/create_model.md @@ -1,4 +1,32 @@ -````py +添加单条新纪录 + +```py title="create_model" hl_lines="21" +from pydantic import BaseModel + +from sqlalchemy_crud_plus import CRUDPlus + +from sqlalchemy import DeclarativeBase as Base +from sqlalchemy.ext.asyncio import AsyncSession + + +class ModelIns(Base): + # your sqlalchemy model + pass + + +class CreateIns(BaseModel): + # your pydantic schema + pass + + +class CRUDIns(CRUDPlus[ModelIns]): + async def create(self, db: AsyncSession, obj: CreateIns) -> ModelIns: + return await self.create_model(db, obj) +``` + +## API + +```py async def create_model( self, session: AsyncSession, @@ -7,12 +35,18 @@ async def create_model( commit: bool = False, **kwargs, ) -> Model: -```` +``` -- 此方法提供 `flush` 参数,详见:[冲洗](../advanced/flush.md) -- 此方法提供 `commit` 参数,详见:[提交](../advanced/commit.md) +**Parameters:** -!!! note "关键字参数" +| Name | Type | Description | Default | +|---------|--------------|-----------------------------|---------| +| session | AsyncSession | 数据库会话 | 必填 | +| obj | TypeVar | 创建新数据参数 | 必填 | +| flush | bool | [冲洗](../advanced/flush.md) | `False` | +| commit | bool | [提交](../advanced/commit.md) | `False` | + +!!! note "**kwargs" 除了必须传入 obj schema 外,还可以通过关键字入参,传入非 schema 参数,比如,对于某些特定场景,其中一个字段并不是通用的,也不需要进行输入控制,只需在写入时赋予指定值,那么你可以使用关键字入参即可 @@ -28,30 +62,8 @@ async def create_model( 1. 在数据被最终写入前,所有入参(schema 和关键字参数)都会赋值给 SQLAlchemy 模型,即便你传入了非模型数据, 这也是安全的,因为它不会被模型所引用 +**Returns:** - -## 示例 - -```py title="create_model" hl_lines="21" -from pydantic import BaseModel - -from sqlalchemy_crud_plus import CRUDPlus - -from sqlalchemy import DeclarativeBase as Base -from sqlalchemy.ext.asyncio import AsyncSession - - -class ModelIns(Base): - # your sqlalchemy model - pass - - -class CreateIns(BaseModel): - # your pydantic schema - pass - - -class CRUDIns(CRUDPlus[ModelIns]): - async def create(self, db: AsyncSession, obj: CreateIns) -> ModelIns: - return await self.create_model(db, obj) -``` +| Type | Description | +|---------|-------------| +| TypeVar | 模型实例 | diff --git a/docs/usage/create_models.md b/docs/usage/create_models.md index 90fd7d2..fa4d34c 100644 --- a/docs/usage/create_models.md +++ b/docs/usage/create_models.md @@ -1,19 +1,4 @@ -```python -async def create_models( - self, - session: AsyncSession, - objs: Iterable[CreateSchema], - flush: bool = False, - commit: bool = False, - **kwargs, -) -> list[Model]: -``` - -- 此方法提供 `flush` 参数,详见:[冲洗](../advanced/flush.md) -- 此方法提供 `commit` 参数,详见:[提交](../advanced/commit.md) -- 此方法还提供与 `create_model()` 相同用法的关键字参数,需要额外注意的是,`create_models()` 会将关键字参数写入每个实例中 - -## 示例 +添加多条新纪录 ```py title="create_models" hl_lines="23" from typing import Iterable @@ -37,8 +22,38 @@ class CreateIns(BaseModel): class CRUDIns(CRUDPlus[ModelIns]): - async def creates(self, db: AsyncSession, objs: Iterable[CreateIns]) -> list[ModelIns]: # (1) - return await self.create_models(db, objs) + async def creates(self, db: AsyncSession, objs: Iterable[CreateIns]) -> list[ModelIns]: + return await self.create_models(db, objs) # objs 必须是一个 schema 列表 ``` -1. objs 必须是一个 schema 列表 +## API + +```python +async def create_models( + self, + session: AsyncSession, + objs: Iterable[CreateSchema], + flush: bool = False, + commit: bool = False, + **kwargs, +) -> list[Model]: +``` + +**Parameters:** + +| Name | Type | Description | Default | +|---------|--------------|-----------------------------|---------| +| session | AsyncSession | 数据库会话 | 必填 | +| obj | Iterable | 创建新数据参数 | 必填 | +| flush | bool | [冲洗](../advanced/flush.md) | `False` | +| commit | bool | [提交](../advanced/commit.md) | `False` | + +!!! note "**kwargs" + + 提供与 `create_model()` 相同用法的关键字参数,需要额外注意的是,`create_models()` 会将关键字参数写入每条新数据中 + +**Returns:** + +| Type | Description | +|------|-------------| +| list | 模型实例 | diff --git a/docs/usage/delete_model.md b/docs/usage/delete_model.md index e742dbf..0cbc0cf 100644 --- a/docs/usage/delete_model.md +++ b/docs/usage/delete_model.md @@ -1,22 +1,6 @@ -```py -async def delete_model( - self, - session: AsyncSession, - pk: int, - flush: bool = False, - commit: bool = False, -) -> int: -``` - -- 此方法提供 `flush` 参数,详见:[冲洗](../advanced/flush.md) -- 此方法使用主键 `pk` 参数,详见:[主键](../advanced/primary_key.md) -- 此方法提供 `commit` 参数,详见:[提交](../advanced/commit.md) - -## 示例 - -```py title="delete_model" hl_lines="18" -from pydantic import BaseModel +通过主键删除单条记录 +```py title="delete_model" hl_lines="15" from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy import Mapped, mapped_column @@ -25,12 +9,38 @@ from sqlalchemy.ext.asyncio import AsyncSession class ModelIns(Base): - # your sqlalchemy model # define your primary_key - custom_id: Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True) + primary_key: Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True) class CRUDIns(CRUDPlus[ModelIns]): async def delete(self, db: AsyncSession, pk: int) -> int: return await self.delete_model(db, pk) ``` + +## API + +```py +async def delete_model( + self, + session: AsyncSession, + pk: int, + flush: bool = False, + commit: bool = False, +) -> int: +``` + +**Parameters:** + +| Name | Type | Description | Default | +|---------|--------------|----------------------------------|---------| +| session | AsyncSession | 数据库会话 | 必填 | +| pk | int | [主键](../advanced/primary_key.md) | 必填 | +| flush | bool | [冲洗](../advanced/flush.md) | `False` | +| commit | bool | [提交](../advanced/commit.md) | `False` | + +**Returns:** + +| Type | Description | +|------|-------------| +| int | 删除数量 | diff --git a/docs/usage/delete_model_by_column.md b/docs/usage/delete_model_by_column.md index 32155a0..7d4c57f 100644 --- a/docs/usage/delete_model_by_column.md +++ b/docs/usage/delete_model_by_column.md @@ -1,3 +1,23 @@ +通过列删除单条/多条记录 + +```py title="delete_model_by_column" hl_lines="14" +from sqlalchemy_crud_plus import CRUDPlus + +from sqlalchemy import DeclarativeBase as Base +from sqlalchemy.ext.asyncio import AsyncSession + + +class ModelIns(Base): + # your sqlalchemy model + pass + + +class CRUDIns(CRUDPlus[ModelIns]): + async def delete(self, db: AsyncSession) -> int: + return await self.delete_model_by_column(db, name="foo") +``` + +## API ```py async def delete_model_by_column( self, @@ -11,40 +31,23 @@ async def delete_model_by_column( ) -> int: ``` -- 此方法提供 `flush` 参数,详见:[冲洗](../advanced/flush.md) -- 此方法提供 `commit` 参数,详见:[提交](../advanced/commit.md) -- 此方法可结合 [高级过滤器](../advanced/filter.md) 使用 - -## 删除多条 +**Parameters:** -将参数 `allow_multiple` 设置为 True,将允许删除多条记录,删除的数量取决于过滤器过滤后的数据 +| Name | Type | Description | Default | +|---------------------|--------------|------------------------------------------------------|------------| +| session | AsyncSession | 数据库会话 | 必填 | +| allow_multiple | bool | 设置为 `True`,将允许删除多条记录,删除的数量取决于过滤器过滤后的数据 | `False` | +| logical_deletion | bool | 设置为 `True`,将不会从数据库中直接删除数据,而是通过更新的方式,将数据库删除标志字段的值进行更新 | `False` | +| deleted_flag_column | str | 设置指定逻辑删除的字段 | `del_flag` | +| flush | bool | [冲洗](../advanced/flush.md) | `False` | +| commit | bool | [提交](../advanced/commit.md) | `False` | -## 软删除 +!!! note "**kwargs" -此方法同时提供逻辑删除,将参数 `logical_deletion` 设置为 True,将不会从数据库中直接删除数据,而是通过更新的方式, -将数据库删除标志字段的值进行更新,你可以通过 `deleted_flag_column` 参数设置指定逻辑删除字段,默认为 `del_flag` + [条件过滤](../advanced/filter.md),将删除符合条件的数据 -!!! tip +**Returns:** - 逻辑删除也允许同时删除多条,同样由参数 `allow_multiple` 和过滤器控制 - -## 示例 - -```py title="delete_model_by_column" hl_lines="16" -from pydantic import BaseModel - -from sqlalchemy_crud_plus import CRUDPlus - -from sqlalchemy import DeclarativeBase as Base -from sqlalchemy.ext.asyncio import AsyncSession - - -class ModelIns(Base): - # your sqlalchemy model - pass - - -class CRUDIns(CRUDPlus[ModelIns]): - async def delete(self, db: AsyncSession) -> int: - return await self.delete_model_by_column(db, name="foo") -``` +| Type | Description | +|------|-------------| +| int | 删除数量 | diff --git a/docs/usage/exists.md b/docs/usage/exists.md new file mode 100644 index 0000000..276aea8 --- /dev/null +++ b/docs/usage/exists.md @@ -0,0 +1,46 @@ +查询符合条件过滤的记录是否存在 + +```py title="exists" hl_lines="14" +from sqlalchemy_crud_plus import CRUDPlus + +from sqlalchemy import DeclarativeBase as Base +from sqlalchemy.ext.asyncio import AsyncSession + + +class ModelIns(Base): + # your sqlalchemy model + pass + + +class CRUDIns(CRUDPlus[ModelIns]): + async def create(self, db: AsyncSession) -> bool: + return await self.exists(db, name="foo") +``` + +## API + +```python +async def exists( + self, + session: AsyncSession, + filters: ColumnElement | list[ColumnElement] | None = None, + **kwargs, +) -> bool: +``` + +**Parameters:** + +| Name | Type | Description | Default | +|---------|----------------------------------------------------|------------------|---------| +| session | AsyncSession | 数据库会话 | 必填 | +| filters | `ColumnElement `\|` list[ColumnElement] `\|` None` | 要应用于查询的 WHERE 子句 | `None` | + +!!! note "**kwargs" + + [条件过滤](../advanced/filter.md),将创建条件查询 SQL + +**Returns:** + +| Type | Description | +|------|-------------| +| bool | 记录是否存在 | diff --git a/docs/usage/select.md b/docs/usage/select.md index 27d5e7f..cf3d9ff 100644 --- a/docs/usage/select.md +++ b/docs/usage/select.md @@ -1,12 +1,4 @@ -```py -async def select(self, **kwargs) -> Select: - ... -``` - -此方法用于构造 SQLAlchemy -Select,在一些特定场景将会很有用,比如,配合 [fastapi-pagination](https://github.com/uriyyo/fastapi-pagination) 使用 - -## 示例 +获取 SQLAlchemy Select 对象 ```py hl_lines="28" from typing import Any, Annotated @@ -16,7 +8,7 @@ from pydantic import BaseModel from sqlalchemy_crud_plus import CRUDPlus -from sqlalchemy import select, Select +from sqlalchemy import Select from sqlalchemy import DeclarativeBase as Base from sqlalchemy.ext.asyncio import AsyncSession @@ -35,23 +27,42 @@ class UserOut(BaseModel): class CRUDIns(CRUDPlus[ModelIns]): - async def get_list(self, name: str = None, method: str = None) -> Select: + async def get_list(self, name: str, method: str) -> Select: return await self.select(name__like=f'%{name}%', method=method) - + crud_ins = CRUDIns(ModelIns) - app = FastAPI() add_pagination(app) - + @app.get("/users", response_model=Page[UserOut]) async def get_users( db: AsyncSession = Depends(get_db), name: Annotated[str | None, Query()] = None, method: Annotated[str | None, Query()] = None, ) -> Any: - select = await crud_ins.get_list() + select = await crud_ins.get_list(name=name, method=method) return await paginate(db, select) ``` + +## API + +```py +async def select(self, **kwargs) -> Select: + ... +``` + +此方法用于构造 SQLAlchemy +Select,在一些特定场景将会很有用,例如,配合 [fastapi-pagination](https://github.com/uriyyo/fastapi-pagination) 使用 + +!!! note "**kwargs" + + [条件过滤](../advanced/filter.md),将创建条件查询 SQL + +**Returns:** + +| Type | Description | +|--------|----------------------| +| Select | SQLAlchemy Select 对象 | diff --git a/docs/usage/select_model.md b/docs/usage/select_model.md index e601325..45118a1 100644 --- a/docs/usage/select_model.md +++ b/docs/usage/select_model.md @@ -1,14 +1,6 @@ -```py -async def select_model(self, session: AsyncSession, pk: int) -> Model | None: -``` - -此方法使用主键 `pk` 参数,详见:[主键](../advanced/primary_key.md) - -## 示例 - -```py title="select_model" hl_lines="18" -from pydantic import BaseModel +通过主键查询单条记录 +```py title="select_model" hl_lines="15" from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy import Mapped, mapped_column @@ -17,12 +9,30 @@ from sqlalchemy.ext.asyncio import AsyncSession class ModelIns(Base): - # your sqlalchemy model # define your primary_key - custom_id: Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True) + primary_key: Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True) class CRUDIns(CRUDPlus[ModelIns]): async def select(self, db: AsyncSession, pk: int) -> ModelIns: return await self.select_model(db, pk) ``` + +## API + +```py +async def select_model(self, session: AsyncSession, pk: int) -> Model | None: +``` + +**Parameters:** + +| Name | Type | Description | Default | +|---------|--------------|----------------------------------|---------| +| session | AsyncSession | 数据库会话 | 必填 | +| pk | int | [主键](../advanced/primary_key.md) | 必填 | + +**Returns:** + +| Type | Description | +|---------------------|-------------| +| `TypeVar `\|` None` | 模型实例 | diff --git a/docs/usage/select_model_by_column.md b/docs/usage/select_model_by_column.md index 95885d2..bee0c4e 100644 --- a/docs/usage/select_model_by_column.md +++ b/docs/usage/select_model_by_column.md @@ -1,14 +1,6 @@ -```py -async def select_model_by_column(self, session: AsyncSession, **kwargs) -> Model | None: -``` - -此方法可结合 [高级过滤器](../advanced/filter.md) 使用 - -## 示例 - -```py title="select_model_by_cloumn" hl_lines="16" -from pydantic import BaseModel +通过列查询单条记录 +```py title="select_model_by_cloumn" hl_lines="14" from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy import DeclarativeBase as Base @@ -24,3 +16,25 @@ class CRUDIns(CRUDPlus[ModelIns]): async def create(self, db: AsyncSession) -> ModelIns: return await self.select_model_by_column(db, name="foo") ``` + +## API + +```py +async def select_model_by_column(self, session: AsyncSession, **kwargs) -> Model | None: +``` + +**Parameters:** + +| Name | Type | Description | Default | +|---------|--------------|----------------------------------|---------| +| session | AsyncSession | 数据库会话 | 必填 | + +!!! note "**kwargs" + + [条件过滤](../advanced/filter.md),将创建条件查询 SQL + +**Returns:** + +| Type | Description | +|---------------------|-------------| +| `TypeVar `\|` None` | 模型实例 | diff --git a/docs/usage/select_models.md b/docs/usage/select_models.md index 63dbb23..38b2b42 100644 --- a/docs/usage/select_models.md +++ b/docs/usage/select_models.md @@ -1,16 +1,8 @@ -```py -async def select_models(self, session: AsyncSession, **kwargs) -> Sequence[Row[Any] | RowMapping | Any]: -``` - -此方法可结合 [高级过滤器](../advanced/filter.md) 使用 - -## 示例 +通过列查询多条记录 -```py title="select_models" hl_lines="18" +```py title="select_models" hl_lines="16" from typing import Sequence -from pydantic import BaseModel - from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy import DeclarativeBase as Base @@ -26,3 +18,25 @@ class CRUDIns(CRUDPlus[ModelIns]): async def create(self, db: AsyncSession) -> Sequence[ModelIns]: return await self.select_models(db) ``` + +## API + +```py +async def select_models(self, session: AsyncSession, **kwargs) -> Sequence[Row[Any] | RowMapping | Any]: +``` + +**Parameters:** + +| Name | Type | Description | Default | +|---------|--------------|----------------------------------|---------| +| session | AsyncSession | 数据库会话 | 必填 | + +!!! note "**kwargs" + + [条件过滤](../advanced/filter.md),将创建条件查询 SQL + +**Returns:** + +| Type | Description | +|------------------------------------------------|-------------| +| `Sequence[Row[Any] `\|` RowMapping `\|` Any]` | 模型实例序列 | diff --git a/docs/usage/select_models_order.md b/docs/usage/select_models_order.md index ce7b9c6..ba3c08c 100644 --- a/docs/usage/select_models_order.md +++ b/docs/usage/select_models_order.md @@ -1,3 +1,26 @@ +此方法与 [select_models()](./select_models.md) 方法类似,但增加了排序功能 + +```py title="select_models_order" hl_lines="16" +from typing import Sequence + +from sqlalchemy_crud_plus import CRUDPlus + +from sqlalchemy import DeclarativeBase as Base +from sqlalchemy.ext.asyncio import AsyncSession + + +class ModelIns(Base): + # your sqlalchemy model + pass + + +class CRUDIns(CRUDPlus[ModelIns]): + async def create(self, db: AsyncSession) -> Sequence[ModelIns]: + return await self.select_models_order(db, sort_columns=['name', 'age'], sort_orders=['asc', 'desc']) +``` + +## API + ```py async def select_models_order( self, @@ -8,36 +31,20 @@ ) -> Sequence[Row | RowMapping | Any] | None: ``` -此方法可结合 [高级过滤器](../advanced/filter.md) 使用 +**Parameters:** -## 排序 +| Name | Type | Description | Default | +|--------------|--------------------------------|------------------------------------------------------------------------|---------| +| session | AsyncSession | 数据库会话 | 必填 | +| sort_columns | `str `\|` list[str]` | 应用排序的单个列名或列名列表 | 必填 | +| sort_orders | `str `\|` list[str] `\|` None` | 单个排序顺序(asc 或 desc)或与 sort_columns 中的列相对应的排序顺序列表。 如果未提供,则默认每列的排序顺序为 asc | `None` | -对结果进行排序涉及此方法的两个字段 +!!! note "**kwargs" -1. `sort_columns`:应用排序的单个列名或列名列表 + [条件过滤](../advanced/filter.md),将创建条件查询 SQL -2. `sort_orders`:单个排序顺序(`asc` 或 `desc`)或与 `sort_columns` 中的列相对应的排序顺序列表。 - 如果未提供,则默认每列的排序顺序为 `asc` +**Returns:** -## 示例 - -```py title="select_models_order" hl_lines="18" -from typing import Sequence - -from pydantic import BaseModel - -from sqlalchemy_crud_plus import CRUDPlus - -from sqlalchemy import DeclarativeBase as Base -from sqlalchemy.ext.asyncio import AsyncSession - - -class ModelIns(Base): - # your sqlalchemy model - pass - - -class CRUDIns(CRUDPlus[ModelIns]): - async def create(self, db: AsyncSession) -> Sequence[ModelIns]: - return await self.select_models_order(db, sort_columns=['name', 'age'], sort_orders=['asc', 'desc']) -``` +| Type | Description | +|------------------------------------------------|-------------| +| `Sequence[Row[Any] `\|` RowMapping `\|` Any]` | 模型实例序列 | diff --git a/docs/usage/select_order.md b/docs/usage/select_order.md index 87dd66d..bc91d6b 100644 --- a/docs/usage/select_order.md +++ b/docs/usage/select_order.md @@ -1,3 +1,5 @@ +此方法与 [select](./select.md) 方法类似,但增加了排序功能 + ```py async def select_order( self, @@ -7,4 +9,19 @@ async def select_order( ) -> Select: ``` -此方法与 [select](./select.md) 方法类似,但增加了 [排序](./select_models_order.md/#_1) 功能 +**Parameters:** + +| Name | Type | Description | Default | +|--------------|--------------------------------|------------------------------------------------------------------------|---------| +| sort_columns | `str `\|` list[str]` | 应用排序的单个列名或列名列表 | 必填 | +| sort_orders | `str `\|` list[str] `\|` None` | 单个排序顺序(asc 或 desc)或与 sort_columns 中的列相对应的排序顺序列表。 如果未提供,则默认每列的排序顺序为 asc | `None` | + +!!! note "**kwargs" + + [条件过滤](../advanced/filter.md),将创建条件查询 SQL + +**Returns:** + +| Type | Description | +|--------|----------------------| +| Select | SQLAlchemy Select 对象 | diff --git a/docs/usage/update_model.md b/docs/usage/update_model.md index ae5416b..c5a34e6 100644 --- a/docs/usage/update_model.md +++ b/docs/usage/update_model.md @@ -1,23 +1,6 @@ -```py -async def update_model( - self, - session: AsyncSession, - pk: int, - obj: UpdateSchema | dict[str, Any], - flush: bool = False, - commit: bool = False, - **kwargs, -) -> int: -``` - -- 此方法提供 `flush` 参数,详见:[冲洗](../advanced/flush.md) -- 此方法使用主键 `pk` 参数,详见:[主键](../advanced/primary_key.md) -- 此方法提供 `commit` 参数,详见:[提交](../advanced/commit.md) -- 此方法还提供与 `create_model()` 相同用法的关键字参数 +通过主键更新单条记录 -## 示例 - -```py title="update_model" hl_lines="23" +```py title="update_model" hl_lines="22" from pydantic import BaseModel from sqlalchemy_crud_plus import CRUDPlus @@ -28,9 +11,8 @@ from sqlalchemy.ext.asyncio import AsyncSession class ModelIns(Base): - # your sqlalchemy model # define your primary_key - custom_id: Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True) + primary_key: Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True) class UpdateIns(BaseModel): @@ -42,3 +24,49 @@ class CRUDIns(CRUDPlus[ModelIns]): async def create(self, db: AsyncSession, pk: int, obj: UpdateIns) -> int: return await self.update_model(db, pk, obj) ``` + +## API + +```py +async def update_model( + self, + session: AsyncSession, + pk: int, + obj: UpdateSchema | dict[str, Any], + flush: bool = False, + commit: bool = False, + **kwargs, +) -> int: +``` + +**Parameters:** + +| Name | Type | Description | Default | +|---------|-------------------------------|----------------------------------|---------| +| session | AsyncSession | 数据库会话 | 必填 | +| pk | int | [主键](../advanced/primary_key.md) | 必填 | +| obj | `TypeVar `\|` dict[str, Any]` | 更新数据参数 | 必填 | +| flush | bool | [冲洗](../advanced/flush.md) | `False` | +| commit | bool | [提交](../advanced/commit.md) | `False` | + +!!! note "**kwargs" + + 除了必须传入 obj schema 外,还可以通过关键字入参,传入非 schema + 参数,比如,对于某些特定场景,其中一个字段并不是通用的,也不需要进行输入控制,只需在写入时赋予指定值,那么你可以使用关键字入参即可 + + e.g. + + ```PY + async def update(self, obj: UpdateIns) -> None: + async with async_db_session.begin() as db: + await self.update_model(db, obj, status=1) # (1) + ``` + + 1. 在数据被最终写入前,所有入参(schema 和关键字参数)都会赋值给 SQLAlchemy 模型,即便你传入了非模型数据, + 这也是安全的,因为它不会被模型所引用 + +**Returns:** + +| Type | Description | +|------|-------------| +| int | 更新数量 | diff --git a/docs/usage/update_model_by_column.md b/docs/usage/update_model_by_column.md index d39c221..12a4bbf 100644 --- a/docs/usage/update_model_by_column.md +++ b/docs/usage/update_model_by_column.md @@ -1,24 +1,4 @@ -```py -async def update_model_by_column( - self, - session: AsyncSession, - obj: UpdateSchema | dict[str, Any], - allow_multiple: bool = False, - flush: bool = False, - commit: bool = False, - **kwargs, -) -> int: -``` - -- 此方法提供 `flush` 参数,详见:[冲洗](../advanced/flush.md) -- 此方法提供 `commit` 参数,详见:[提交](../advanced/commit.md) -- 此方法可结合 [高级过滤器](../advanced/filter.md) 使用 - -## 更新多条 - -将参数 `allow_multiple` 设置为 True,将允许更新多条记录,更新的数量取决于过滤器过滤后的数据 - -## 示例 +通过列更新单条/多条记录 ```py title="update_model_by_columnn" hl_lines="21" from pydantic import BaseModel @@ -43,3 +23,37 @@ class CRUDIns(CRUDPlus[ModelIns]): async def create(self, db: AsyncSession, obj: UpdateIns) -> int: return await self.update_model_by_column(db, obj, name="foo") ``` + +## API + +```py +async def update_model_by_column( + self, + session: AsyncSession, + obj: UpdateSchema | dict[str, Any], + allow_multiple: bool = False, + flush: bool = False, + commit: bool = False, + **kwargs, +) -> int: +``` + +**Parameters:** + +| Name | Type | Description | Default | +|----------------|-------------------------------|----------------------------------------|---------| +| session | AsyncSession | 数据库会话 | 必填 | +| obj | `TypeVar `\|` dict[str, Any]` | 更新数据参数 | 必填 | +| allow_multiple | bool | 设置为 `True`,将允许更新多条记录,更新的数量取决于过滤器过滤后的数据 | `False` | +| flush | bool | [冲洗](../advanced/flush.md) | `False` | +| commit | bool | [提交](../advanced/commit.md) | `False` | + +!!! note "**kwargs" + + [条件过滤](../advanced/filter.md),将更新符合条件的数据 + +**Returns:** + +| Type | Description | +|------|-------------| +| int | 更新数量 | diff --git a/mkdocs.yml b/mkdocs.yml index b732824..9a95e83 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,31 +6,33 @@ site_author: Wu Clan repo_name: sqlalchemy-crud-plus repo_url: https://github.com/wu-clan/sqlalchemy-crud-plus nav: - - Home: index.md - - Installing: installing.md - - Usage: + - 主页: index.md + - 安装: installing.md + - 用法: - 新增: - 单条: usage/create_model.md - 多条: usage/create_models.md - 查询: - - 主键 ID: usage/select_model.md - - 条件过滤: usage/select_model_by_column.md + - 主键: usage/select_model.md + - 列: usage/select_model_by_column.md - Select: usage/select.md - Select 排序: usage/select_order.md - 列表: usage/select_models.md - 列表排序: usage/select_models_order.md + - Count: usage/count.md + - Exists: usage/exists.md - 更新: - - 主键 ID: usage/update_model.md - - 高级用法: usage/update_model_by_column.md + - 主键: usage/update_model.md + - 列: usage/update_model_by_column.md - 删除: - - 主键 ID: usage/delete_model.md - - 高级用法: usage/delete_model_by_column.md - - Advanced: + - 主键: usage/delete_model.md + - 列: usage/delete_model_by_column.md + - 高级用法: - 主键: advanced/primary_key.md - 冲洗: advanced/flush.md - 提交: advanced/commit.md - 条件过滤: advanced/filter.md - - Changelog: changelog.md + - 变更日志: changelog.md theme: name: material font: @@ -59,7 +61,6 @@ theme: - navigation.instant.progress - navigation.path - navigation.tracking - - navigation.tabs - navigation.tabs.sticky - navigation.top - navigation.footer diff --git a/pyproject.toml b/pyproject.toml index 19f915f..50b6a02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,12 +31,12 @@ dev = [ "pytest-asyncio>=0.23.6", ] lint = [ - "ruff>=0.4.0", "pre-commit>=3.5.0", ] [tool.uv] python-downloads = "manual" +default-groups = ["dev", "lint"] [tool.ruff] line-length = 120 diff --git a/requirements.txt b/requirements.txt index 41c75e2..494ff05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,16 +3,50 @@ -e . aiosqlite==0.20.0 annotated-types==0.7.0 + # via pydantic +cfgv==3.4.0 + # via pre-commit colorama==0.4.6 ; sys_platform == 'win32' + # via pytest +distlib==0.3.9 + # via virtualenv exceptiongroup==1.2.2 ; python_full_version < '3.11' + # via pytest +filelock==3.16.1 + # via virtualenv greenlet==3.1.1 ; (python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'WIN32') or (python_full_version < '3.13' and platform_machine == 'aarch64') or (python_full_version < '3.13' and platform_machine == 'amd64') or (python_full_version < '3.13' and platform_machine == 'ppc64le') or (python_full_version < '3.13' and platform_machine == 'win32') or (python_full_version < '3.13' and platform_machine == 'x86_64') + # via sqlalchemy +identify==2.6.1 + # via pre-commit iniconfig==2.0.0 + # via pytest +nodeenv==1.9.1 + # via pre-commit packaging==24.1 + # via pytest +platformdirs==4.3.6 + # via virtualenv pluggy==1.5.0 + # via pytest +pre-commit==4.0.1 pydantic==2.9.2 + # via sqlalchemy-crud-plus pydantic-core==2.23.4 + # via pydantic pytest==8.3.3 + # via pytest-asyncio pytest-asyncio==0.24.0 +pyyaml==6.0.2 + # via pre-commit sqlalchemy==2.0.36 + # via sqlalchemy-crud-plus tomli==2.0.2 ; python_full_version < '3.11' + # via pytest typing-extensions==4.12.2 + # via + # aiosqlite + # pydantic + # pydantic-core + # sqlalchemy +virtualenv==20.27.1 + # via pre-commit diff --git a/sqlalchemy_crud_plus/crud.py b/sqlalchemy_crud_plus/crud.py index ee0f69c..f30314f 100644 --- a/sqlalchemy_crud_plus/crud.py +++ b/sqlalchemy_crud_plus/crud.py @@ -2,12 +2,12 @@ # -*- coding: utf-8 -*- from typing import Any, Generic, Iterable, Sequence, Type -from sqlalchemy import Row, RowMapping, Select, delete, inspect, select, update +from sqlalchemy import ColumnElement, Row, RowMapping, Select, delete, func, inspect, select, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy_crud_plus.errors import CompositePrimaryKeysError, MultipleResultsError from sqlalchemy_crud_plus.types import CreateSchema, Model, UpdateSchema -from sqlalchemy_crud_plus.utils import apply_sorting, count, parse_filters +from sqlalchemy_crud_plus.utils import apply_sorting, parse_filters class CRUDPlus(Generic[Model]): @@ -48,11 +48,14 @@ async def create_model( ins = self.model(**obj.model_dump()) else: ins = self.model(**obj.model_dump(), **kwargs) + session.add(ins) + if flush: await session.flush() if commit: await session.commit() + return ins async def create_models( @@ -80,13 +83,72 @@ async def create_models( else: ins = self.model(**obj.model_dump(), **kwargs) ins_list.append(ins) + session.add_all(ins_list) + if flush: await session.flush() if commit: await session.commit() + return ins_list + async def count( + self, + session: AsyncSession, + filters: ColumnElement | list[ColumnElement] | None = None, + **kwargs, + ) -> int: + """ + Counts records that match specified filters. + + :param session: The sqlalchemy session to use for the operation. + :param filters: The WHERE clauses to apply to the query. + :param kwargs: Query expressions. + :return: + """ + if filters is None: + filters = [] + + if not isinstance(filters, list): + filters = [filters] + + if kwargs: + filters.extend(parse_filters(self.model, **kwargs)) + + stmt = select(func.count()).select_from(self.model) + stmt = stmt.where(*filters) + query = await session.execute(stmt) + total_count = query.scalar() + return total_count if total_count is not None else 0 + + async def exists( + self, + session: AsyncSession, + filters: ColumnElement | list[ColumnElement] | None = None, + **kwargs, + ) -> bool: + """ + Whether the records that match the specified filter exist. + + :param session: The sqlalchemy session to use for the operation. + :param filters: The WHERE clauses to apply to the query. + :param kwargs: Query expressions. + :return: + """ + if filters is None: + filters = [] + + if not isinstance(filters, list): + filters = [filters] + + if kwargs: + filters.extend(parse_filters(self.model, **kwargs)) + + stmt = select(self.model).where(*filters).limit(1) + query = await session.execute(stmt) + return query.scalars().first() is not None + async def select_model(self, session: AsyncSession, pk: int) -> Model | None: """ Query by ID @@ -132,9 +194,9 @@ async def select_order( """ Constructing SQLAlchemy selection with sorting - :param kwargs: Query expressions. :param sort_columns: more details see apply_sorting :param sort_orders: more details see apply_sorting + :param kwargs: Query expressions. :return: """ stmt = await self.select(**kwargs) @@ -166,6 +228,7 @@ async def select_models_order( :param session: The SQLAlchemy async session. :param sort_columns: more details see apply_sorting :param sort_orders: more details see apply_sorting + :param kwargs: Query expressions. :return: """ stmt = await self.select_order(sort_columns, sort_orders, **kwargs) @@ -189,6 +252,7 @@ async def update_model( :param obj: A pydantic schema or dictionary containing the update data :param flush: If `True`, flush all object changes to the database. Default is `False`. :param commit: If `True`, commits the transaction immediately. Default is `False`. + :param kwargs: Additional model data not included in the pydantic schema. :return: """ if isinstance(obj, dict): @@ -197,12 +261,15 @@ async def update_model( instance_data = obj.model_dump(exclude_unset=True) if kwargs: instance_data.update(kwargs) + stmt = update(self.model).where(self.primary_key == pk).values(**instance_data) result = await session.execute(stmt) + if flush: await session.flush() if commit: await session.commit() + return result.rowcount # type: ignore async def update_model_by_column( @@ -226,19 +293,22 @@ async def update_model_by_column( :return: """ filters = parse_filters(self.model, **kwargs) - total_count = await count(session, self.model, filters) + total_count = await self.count(session, filters) if not allow_multiple and total_count > 1: raise MultipleResultsError(f'Only one record is expected to be update, found {total_count} records.') if isinstance(obj, dict): instance_data = obj else: instance_data = obj.model_dump(exclude_unset=True) + stmt = update(self.model).where(*filters).values(**instance_data) # type: ignore result = await session.execute(stmt) + if flush: await session.flush() if commit: await session.commit() + return result.rowcount # type: ignore async def delete_model( @@ -259,10 +329,12 @@ async def delete_model( """ stmt = delete(self.model).where(self.primary_key == pk) result = await session.execute(stmt) + if flush: await session.flush() if commit: await session.commit() + return result.rowcount # type: ignore async def delete_model_by_column( @@ -288,7 +360,7 @@ async def delete_model_by_column( :return: """ filters = parse_filters(self.model, **kwargs) - total_count = await count(session, self.model, filters) + total_count = await self.count(session, filters) if not allow_multiple and total_count > 1: raise MultipleResultsError(f'Only one record is expected to be delete, found {total_count} records.') if logical_deletion: @@ -296,9 +368,12 @@ async def delete_model_by_column( stmt = update(self.model).where(*filters).values(**deleted_flag) else: stmt = delete(self.model).where(*filters) + result = await session.execute(stmt) + if flush: await session.flush() if commit: await session.commit() + return result.rowcount # type: ignore diff --git a/sqlalchemy_crud_plus/utils.py b/sqlalchemy_crud_plus/utils.py index b1e5644..1d2e728 100644 --- a/sqlalchemy_crud_plus/utils.py +++ b/sqlalchemy_crud_plus/utils.py @@ -4,8 +4,7 @@ from typing import Any, Callable, Type -from sqlalchemy import ColumnElement, Select, and_, asc, desc, func, or_, select -from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import ColumnElement, Select, and_, asc, desc, or_ from sqlalchemy.orm import InstrumentedAttribute from sqlalchemy.orm.util import AliasedClass @@ -218,24 +217,3 @@ def apply_sorting( stmt = stmt.order_by(asc(column) if order == 'asc' else desc(column)) return stmt - - -async def count( - session: AsyncSession, - model: Type[Model] | AliasedClass, - filters: list[ColumnElement], -) -> int: - """ - Counts records that match specified filters. - - :param session: The sqlalchemy session to use for the operation. - :param model: The SQLAlchemy model. - :param filters: Filters to apply for the count. - :return: - """ - stmt = select(func.count()).select_from(model) - if filters: - stmt = stmt.where(*filters) - query = await session.execute(stmt) - total_count = query.scalar() - return total_count if total_count is not None else 0 diff --git a/tests/test_select.py b/tests/test_select.py index 566ea9e..f2d51cd 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -8,6 +8,42 @@ from tests.model import Ins +@pytest.mark.asyncio +async def test_count(create_test_model, async_db_session): + async with async_db_session() as session: + crud = CRUDPlus(Ins) + for i in range(1, 10): + result = await crud.count(session, id=i) + assert result == 1 + + +@pytest.mark.asyncio +async def test_count_filters(create_test_model, async_db_session): + async with async_db_session() as session: + crud = CRUDPlus(Ins) + for i in range(1, 10): + result = await crud.count(session, [Ins.id == i]) + assert result == 1 + + +@pytest.mark.asyncio +async def test_exists(create_test_model, async_db_session): + async with async_db_session() as session: + crud = CRUDPlus(Ins) + for i in range(1, 10): + result = await crud.exists(session, id=i) + assert result is True + + +@pytest.mark.asyncio +async def test_exists_filters(create_test_model, async_db_session): + async with async_db_session() as session: + crud = CRUDPlus(Ins) + for i in range(1, 10): + result = await crud.exists(session, [Ins.id == i]) + assert result is True + + @pytest.mark.asyncio async def test_select_model(create_test_model, async_db_session): async with async_db_session() as session: diff --git a/uv.lock b/uv.lock index 906ea7e..fd19857 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.10" resolution-markers = [ "python_full_version < '3.13'", @@ -346,31 +347,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, ] -[[package]] -name = "ruff" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/51/231bb3790e5b0b9fd4131f9a231d73d061b3667522e3f406fd9b63334d0e/ruff-0.7.2.tar.gz", hash = "sha256:2b14e77293380e475b4e3a7a368e14549288ed2931fce259a6f99978669e844f", size = 3210036 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/56/0caa2b5745d66a39aa239c01059f6918fc76ed8380033d2f44bf297d141d/ruff-0.7.2-py3-none-linux_armv6l.whl", hash = "sha256:b73f873b5f52092e63ed540adefc3c36f1f803790ecf2590e1df8bf0a9f72cb8", size = 10373973 }, - { url = "https://files.pythonhosted.org/packages/1a/33/cad6ff306731f335d481c50caa155b69a286d5b388e87ff234cd2a4b3557/ruff-0.7.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5b813ef26db1015953daf476202585512afd6a6862a02cde63f3bafb53d0b2d4", size = 10171140 }, - { url = "https://files.pythonhosted.org/packages/97/f5/6a2ca5c9ba416226eac9cf8121a1baa6f06655431937e85f38ffcb9d0d01/ruff-0.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:853277dbd9675810c6826dad7a428d52a11760744508340e66bf46f8be9701d9", size = 9809333 }, - { url = "https://files.pythonhosted.org/packages/16/83/e3e87f13d1a1dc205713632978cd7bc287a59b08bc95780dbe359b9aefcb/ruff-0.7.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21aae53ab1490a52bf4e3bf520c10ce120987b047c494cacf4edad0ba0888da2", size = 10622987 }, - { url = "https://files.pythonhosted.org/packages/22/16/97ccab194480e99a2e3c77ae132b3eebfa38c2112747570c403a4a13ba3a/ruff-0.7.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc7e0fc6e0cb3168443eeadb6445285abaae75142ee22b2b72c27d790ab60ba", size = 10184640 }, - { url = "https://files.pythonhosted.org/packages/97/1b/82ff05441b036f68817296c14f24da47c591cb27acfda473ee571a5651ac/ruff-0.7.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd77877a4e43b3a98e5ef4715ba3862105e299af0c48942cc6d51ba3d97dc859", size = 11210203 }, - { url = "https://files.pythonhosted.org/packages/a6/96/7ecb30a7ef7f942e2d8e0287ad4c1957dddc6c5097af4978c27cfc334f97/ruff-0.7.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e00163fb897d35523c70d71a46fbaa43bf7bf9af0f4534c53ea5b96b2e03397b", size = 11870894 }, - { url = "https://files.pythonhosted.org/packages/06/6a/c716bb126218227f8e604a9c484836257708a05ee3d2ebceb666ff3d3867/ruff-0.7.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3c54b538633482dc342e9b634d91168fe8cc56b30a4b4f99287f4e339103e88", size = 11449533 }, - { url = "https://files.pythonhosted.org/packages/e6/2f/3a5f9f9478904e5ae9506ea699109070ead1e79aac041e872cbaad8a7458/ruff-0.7.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b792468e9804a204be221b14257566669d1db5c00d6bb335996e5cd7004ba80", size = 12607919 }, - { url = "https://files.pythonhosted.org/packages/a0/57/4642e57484d80d274750dcc872ea66655bbd7e66e986fede31e1865b463d/ruff-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dba53ed84ac19ae4bfb4ea4bf0172550a2285fa27fbb13e3746f04c80f7fa088", size = 11016915 }, - { url = "https://files.pythonhosted.org/packages/4d/6d/59be6680abee34c22296ae3f46b2a3b91662b8b18ab0bf388b5eb1355c97/ruff-0.7.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b19fafe261bf741bca2764c14cbb4ee1819b67adb63ebc2db6401dcd652e3748", size = 10625424 }, - { url = "https://files.pythonhosted.org/packages/82/e7/f6a643683354c9bc7879d2f228ee0324fea66d253de49273a0814fba1927/ruff-0.7.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28bd8220f4d8f79d590db9e2f6a0674f75ddbc3847277dd44ac1f8d30684b828", size = 10233692 }, - { url = "https://files.pythonhosted.org/packages/d7/48/b4e02fc835cd7ed1ee7318d9c53e48bcf6b66301f55925a7dcb920e45532/ruff-0.7.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9fd67094e77efbea932e62b5d2483006154794040abb3a5072e659096415ae1e", size = 10751825 }, - { url = "https://files.pythonhosted.org/packages/1e/06/6c5ee6ab7bb4cbad9e8bb9b2dd0d818c759c90c1c9e057c6ed70334b97f4/ruff-0.7.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:576305393998b7bd6c46018f8104ea3a9cb3fa7908c21d8580e3274a3b04b691", size = 11074811 }, - { url = "https://files.pythonhosted.org/packages/a1/16/8969304f25bcd0e4af1778342e63b715e91db8a2dbb51807acd858cba915/ruff-0.7.2-py3-none-win32.whl", hash = "sha256:fa993cfc9f0ff11187e82de874dfc3611df80852540331bc85c75809c93253a8", size = 8650268 }, - { url = "https://files.pythonhosted.org/packages/d9/18/c4b00d161def43fe5968e959039c8f6ce60dca762cec4a34e4e83a4210a0/ruff-0.7.2-py3-none-win_amd64.whl", hash = "sha256:dd8800cbe0254e06b8fec585e97554047fb82c894973f7ff18558eee33d1cb88", size = 9433693 }, - { url = "https://files.pythonhosted.org/packages/7f/7b/c920673ac01c19814dd15fc617c02301c522f3d6812ca2024f4588ed4549/ruff-0.7.2-py3-none-win_arm64.whl", hash = "sha256:bb8368cd45bba3f57bb29cbb8d64b4a33f8415d0149d2655c5c8539452ce7760", size = 8735845 }, -] - [[package]] name = "sqlalchemy" version = "2.0.36" @@ -433,7 +409,6 @@ dev = [ ] lint = [ { name = "pre-commit" }, - { name = "ruff" }, ] [package.metadata] @@ -448,10 +423,7 @@ dev = [ { name = "pytest", specifier = ">=8.1.1" }, { name = "pytest-asyncio", specifier = ">=0.23.6" }, ] -lint = [ - { name = "pre-commit", specifier = ">=3.5.0" }, - { name = "ruff", specifier = ">=0.4.0" }, -] +lint = [{ name = "pre-commit", specifier = ">=3.5.0" }] [[package]] name = "tomli"