Skip to content

Commit 2af3678

Browse files
authored
Add base code files (#1)
* Add the base code * Add test code * Update README.md
1 parent 3934b0f commit 2af3678

File tree

14 files changed

+977
-0
lines changed

14 files changed

+977
-0
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
__pycache__/
2+
.pdm-python
3+
.pdm-build/
4+
.venv
5+
venv/
6+
.idea/
7+
dist/

.pre-commit-config.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.4.0
4+
hooks:
5+
- id: check-added-large-files
6+
- id: end-of-file-fixer
7+
- id: check-toml
8+
9+
- repo: https://github.com/charliermarsh/ruff-pre-commit
10+
rev: v0.4.0
11+
hooks:
12+
- id: ruff
13+
args:
14+
- '--fix'
15+
- id: ruff-format
16+
17+
- repo: https://github.com/pdm-project/pdm
18+
rev: 2.12.2
19+
hooks:
20+
- id: pdm-export
21+
args:
22+
- -o
23+
- 'requirements.txt'
24+
- '--without-hashes'
25+
files: ^pdm.lock$
26+
- id: pdm-lock-check

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# sqlalchemy-crud-plus
2+
3+
基于 SQLAlChemy2 模型的异步 CRUD 操作
4+
5+
## 下载
6+
7+
```shell
8+
pip install sqlalchemy-crud-plus
9+
```
10+
11+
## TODO
12+
13+
- [ ] ...
14+
15+
## Use
16+
17+
以下仅为简易示例
18+
19+
```python
20+
from sqlalchemy.orm import declarative_base
21+
from sqlalchemy_crud_plus import CRUDPlus
22+
23+
Base = declarative_base()
24+
25+
26+
class ModelIns(Base):
27+
# your sqlalchemy model
28+
pass
29+
30+
31+
class CRUDIns(CRUDPlus[ModelIns]):
32+
# your controller service
33+
pass
34+
35+
36+
# singleton
37+
ins_dao = CRUDIns(ModelIns)
38+
```
39+
40+
## 互动
41+
42+
[WeChat / QQ](https://github.com/wu-clan)
43+
44+
## 赞助
45+
46+
如果此项目能够帮助到你,你可以赞助作者一些咖啡豆表示鼓励:[:coffee: Sponsor :coffee:](https://wu-clan.github.io/sponsor/)

pdm.lock

Lines changed: 553 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pre-commit.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
3+
pre-commit run --all-files --verbose --show-diff-on-failure

pyproject.toml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
[project]
2+
name = "sqlalchemy-crud-plus"
3+
description = "Asynchronous CRUD Operation based on SQLAlChemy2 Model"
4+
dynamic = [
5+
"version",
6+
]
7+
authors = [
8+
{ name = "Wu Clan", email = "jianhengwu0407@gmail.com" },
9+
]
10+
dependencies = [
11+
"sqlalchemy>=2.0.0",
12+
"pydantic>=2.0",
13+
]
14+
requires-python = ">=3.10"
15+
readme = "README.md"
16+
license = { text = "MIT" }
17+
18+
[tool.pdm.dev-dependencies]
19+
lint = [
20+
"ruff>=0.4.0",
21+
"pre-commit>=3.5.0",
22+
]
23+
test = [
24+
"pytest>=8.1.1",
25+
"aiosqlite>=0.20.0",
26+
"pytest-asyncio>=0.23.6",
27+
]
28+
29+
[tool.ruff]
30+
line-length = 120
31+
unsafe-fixes = true
32+
target-version = "py310"
33+
34+
[tool.ruff.lint]
35+
select = [
36+
"E",
37+
"F",
38+
"I"
39+
]
40+
preview = true
41+
ignore-init-module-imports = true
42+
43+
[tool.ruff.lint.isort]
44+
lines-between-types = 1
45+
order-by-type = true
46+
47+
[tool.ruff.format]
48+
quote-style = "single"
49+
docstring-code-format = true
50+
51+
[tool.pdm]
52+
distribution = true
53+
version = { source = "file", path = "sqlalchemy_crud_plus/__init__.py" }
54+
55+
[tool.pdm.scripts]
56+
lint = "pre-commit run --all-files"
57+
58+
[build-system]
59+
requires = ["pdm-backend"]
60+
build-backend = "pdm.backend"

requirements.txt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# This file is @generated by PDM.
2+
# Please do not edit it manually.
3+
4+
aiosqlite==0.20.0
5+
annotated-types==0.6.0
6+
cfgv==3.4.0
7+
colorama==0.4.6; sys_platform == "win32"
8+
distlib==0.3.8
9+
exceptiongroup==1.2.1; python_version < "3.11"
10+
filelock==3.13.4
11+
greenlet==3.0.3; platform_machine == "win32" or platform_machine == "WIN32" or platform_machine == "AMD64" or platform_machine == "amd64" or platform_machine == "x86_64" or platform_machine == "ppc64le" or platform_machine == "aarch64"
12+
identify==2.5.35
13+
iniconfig==2.0.0
14+
nodeenv==1.8.0
15+
packaging==24.0
16+
platformdirs==4.2.0
17+
pluggy==1.5.0
18+
pre-commit==3.5.0
19+
pydantic==2.7.0
20+
pydantic-core==2.18.1
21+
pytest==8.1.1
22+
pytest-asyncio==0.23.6
23+
pyyaml==6.0.1
24+
ruff==0.4.0
25+
setuptools==69.5.1
26+
sqlalchemy==2.0.29
27+
tomli==2.0.1; python_version < "3.11"
28+
typing-extensions==4.10.0
29+
virtualenv==20.25.3

sqlalchemy_crud_plus/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
from .crud import CRUDPlus as CRUDPlus
4+
from .errors import ModelColumnError as ModelColumnError
5+
6+
__version__ = 'v0.0.1'

sqlalchemy_crud_plus/crud.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
from typing import Any, Generic, Sequence, Type, TypeVar
4+
5+
from pydantic import BaseModel
6+
from sqlalchemy import Row, RowMapping, select
7+
from sqlalchemy import delete as sa_delete
8+
from sqlalchemy import update as sa_update
9+
from sqlalchemy.ext.asyncio import AsyncSession
10+
11+
from sqlalchemy_crud_plus.errors import ModelColumnError
12+
13+
_Model = TypeVar('_Model')
14+
_CreateSchema = TypeVar('_CreateSchema', bound=BaseModel)
15+
_UpdateSchema = TypeVar('_UpdateSchema', bound=BaseModel)
16+
17+
18+
class CRUDPlus(Generic[_Model]):
19+
def __init__(self, model: Type[_Model]):
20+
self.model = model
21+
22+
async def create_model(self, session: AsyncSession, obj: _CreateSchema, **kwargs) -> None:
23+
"""
24+
Create a new instance of a model
25+
26+
:param session:
27+
:param obj:
28+
:param kwargs:
29+
:return:
30+
"""
31+
if kwargs:
32+
instance = self.model(**obj.model_dump(), **kwargs)
33+
else:
34+
instance = self.model(**obj.model_dump())
35+
session.add(instance)
36+
37+
async def select_model_by_id(self, session: AsyncSession, pk: int) -> _Model | None:
38+
"""
39+
Query by ID
40+
41+
:param session:
42+
:param pk:
43+
:return:
44+
"""
45+
query = await session.execute(select(self.model).where(self.model.id == pk))
46+
return query.scalars().first()
47+
48+
async def select_model_by_column(self, session: AsyncSession, column: str, column_value: Any) -> _Model | None:
49+
"""
50+
Query by column
51+
52+
:param session:
53+
:param column:
54+
:param column_value:
55+
:return:
56+
"""
57+
if hasattr(self.model, column):
58+
model_column = getattr(self.model, column)
59+
query = await session.execute(select(self.model).where(model_column == column_value)) # type: ignore
60+
return query.scalars().first()
61+
else:
62+
raise ModelColumnError('Model column not found')
63+
64+
async def select_models(self, session: AsyncSession) -> Sequence[Row | RowMapping | Any] | None:
65+
"""
66+
Query all rows
67+
68+
:param session:
69+
:return:
70+
"""
71+
query = await session.execute(select(self.model))
72+
return query.scalars().all()
73+
74+
async def update_model(self, session: AsyncSession, pk: int, obj: _UpdateSchema | dict[str, Any], **kwargs) -> int:
75+
"""
76+
Update an instance of a model
77+
78+
:param session:
79+
:param pk:
80+
:param obj:
81+
:param kwargs:
82+
:return:
83+
"""
84+
if isinstance(obj, dict):
85+
instance_data = obj
86+
else:
87+
instance_data = obj.model_dump(exclude_unset=True)
88+
if kwargs:
89+
instance_data.update(kwargs)
90+
result = await session.execute(sa_update(self.model).where(self.model.id == pk).values(**instance_data))
91+
return result.rowcount # type: ignore
92+
93+
async def delete_model(self, session: AsyncSession, pk: int, **kwargs) -> int:
94+
"""
95+
Delete an instance of a model
96+
97+
:param session:
98+
:param pk:
99+
:param kwargs:
100+
:return:
101+
"""
102+
if not kwargs:
103+
result = await session.execute(sa_delete(self.model).where(self.model.id == pk))
104+
else:
105+
result = await session.execute(sa_update(self.model).where(self.model.id == pk).values(**kwargs))
106+
return result.rowcount # type: ignore

sqlalchemy_crud_plus/errors.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
5+
class SQLAlchemyCRUDPlusException(Exception):
6+
def __init__(self, msg: str) -> None:
7+
self.msg = msg
8+
9+
def __str__(self) -> str:
10+
return self.msg
11+
12+
13+
class ModelColumnError(SQLAlchemyCRUDPlusException):
14+
"""Error raised when an SCP column is invalid."""
15+
16+
def __init__(self, msg: str) -> None:
17+
super().__init__(msg)

0 commit comments

Comments
 (0)