moss-mop 0.1.0__tar.gz
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.
- moss_mop-0.1.0/PKG-INFO +10 -0
- moss_mop-0.1.0/README.md +249 -0
- moss_mop-0.1.0/mop/__init__.py +3 -0
- moss_mop-0.1.0/mop/conf/__init__.py +1 -0
- moss_mop-0.1.0/mop/conf/settings.py +133 -0
- moss_mop-0.1.0/mop/crud/__init__.py +2 -0
- moss_mop-0.1.0/mop/crud/crud.py +164 -0
- moss_mop-0.1.0/mop/crud/page.py +11 -0
- moss_mop-0.1.0/mop/db/__init__.py +3 -0
- moss_mop-0.1.0/mop/db/async_session.py +33 -0
- moss_mop-0.1.0/mop/db/base.py +12 -0
- moss_mop-0.1.0/mop/db/session.py +29 -0
- moss_mop-0.1.0/mop/entity/__init__.py +1 -0
- moss_mop-0.1.0/mop/entity/entity.py +36 -0
- moss_mop-0.1.0/mop/error/__init__.py +2 -0
- moss_mop-0.1.0/mop/error/biz_error.py +13 -0
- moss_mop-0.1.0/mop/error/err_code.py +28 -0
- moss_mop-0.1.0/mop/logging/__init__.py +1 -0
- moss_mop-0.1.0/mop/logging/logging.py +152 -0
- moss_mop-0.1.0/mop/response/__init__.py +1 -0
- moss_mop-0.1.0/mop/response/response.py +38 -0
- moss_mop-0.1.0/mop/snowflake.py +192 -0
- moss_mop-0.1.0/mop/util/__init__.py +1 -0
- moss_mop-0.1.0/mop/util/dates.py +5 -0
- moss_mop-0.1.0/mop/util/encoder.py +178 -0
- moss_mop-0.1.0/mop/util/files.py +27 -0
- moss_mop-0.1.0/mop/util/strings.py +10 -0
- moss_mop-0.1.0/mop/util/util.py +43 -0
- moss_mop-0.1.0/moss_mop.egg-info/PKG-INFO +10 -0
- moss_mop-0.1.0/moss_mop.egg-info/SOURCES.txt +33 -0
- moss_mop-0.1.0/moss_mop.egg-info/dependency_links.txt +1 -0
- moss_mop-0.1.0/moss_mop.egg-info/requires.txt +5 -0
- moss_mop-0.1.0/moss_mop.egg-info/top_level.txt +1 -0
- moss_mop-0.1.0/pyproject.toml +12 -0
- moss_mop-0.1.0/setup.cfg +4 -0
moss_mop-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: moss-mop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight FastAPI + SQLAlchemy backend toolkit with logging, CRUD, and utilities
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: ascii-colors>=0.11.6
|
|
7
|
+
Requires-Dist: fastapi>=0.124.4
|
|
8
|
+
Requires-Dist: pydantic>=2.12.5
|
|
9
|
+
Requires-Dist: sqlalchemy>=2.0.45
|
|
10
|
+
Requires-Dist: structlog>=25.5.0
|
moss_mop-0.1.0/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# MoP
|
|
2
|
+
|
|
3
|
+
MoP is a lightweight Python backend framework built on FastAPI and SQLAlchemy, providing core functionality modules for building modern web applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Configuration Management**: Flexible configuration system with easy-to-use settings
|
|
8
|
+
- **Database Support**: Both synchronous and asynchronous database sessions with SQLAlchemy
|
|
9
|
+
- **Entity Framework**: Base entity class for consistent data modeling
|
|
10
|
+
- **Error Handling**: Centralized error handling with business error codes
|
|
11
|
+
- **Logging**: Structured logging with structlog
|
|
12
|
+
- **Response Formatting**: Consistent API response formatting
|
|
13
|
+
- **CRUD Operations**: Generic CRUD operations with pagination support
|
|
14
|
+
- **Utilities**: Various utility functions for dates, strings, files, and more
|
|
15
|
+
- **Snowflake ID Generation**: Distributed unique ID generation
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install moss-mop
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or using uv:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv add moss-mop
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Python 3.12+
|
|
32
|
+
- FastAPI 0.124.4+
|
|
33
|
+
- SQLAlchemy 2.0.45+
|
|
34
|
+
- Pydantic 2.12.5+
|
|
35
|
+
- structlog 25.5.0+
|
|
36
|
+
- ascii-colors 0.11.6+
|
|
37
|
+
|
|
38
|
+
## Project Structure
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
mop/
|
|
42
|
+
├── conf/ # Configuration management
|
|
43
|
+
├── db/ # Database connections
|
|
44
|
+
├── entity/ # Base entity class
|
|
45
|
+
├── error/ # Error handling
|
|
46
|
+
├── logging/ # Logging configuration
|
|
47
|
+
├── response/ # Response formatting
|
|
48
|
+
├── crud/ # CRUD operations
|
|
49
|
+
├── util/ # Utility functions
|
|
50
|
+
└── snowflake.py # Snowflake ID generation
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### 1. Configuration
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from mop.conf import settings
|
|
59
|
+
|
|
60
|
+
# Access configuration settings
|
|
61
|
+
print(settings.APP_NAME)
|
|
62
|
+
print(settings.DATABASE_URL)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 2. Database Setup
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from mop.db import session, async_session
|
|
69
|
+
from sqlalchemy import select
|
|
70
|
+
from your_model import User
|
|
71
|
+
|
|
72
|
+
# Synchronous session
|
|
73
|
+
with session() as db:
|
|
74
|
+
users = db.execute(select(User)).scalars().all()
|
|
75
|
+
print(users)
|
|
76
|
+
|
|
77
|
+
# Asynchronous session
|
|
78
|
+
async with async_session() as db:
|
|
79
|
+
users = await db.execute(select(User))
|
|
80
|
+
users = users.scalars().all()
|
|
81
|
+
print(users)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. Creating Models
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from mop.entity import Entity
|
|
88
|
+
from sqlalchemy import Column, String
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class User(Entity):
|
|
92
|
+
__tablename__ = "users"
|
|
93
|
+
|
|
94
|
+
username = Column(String(50), unique=True, index=True)
|
|
95
|
+
email = Column(String(100), unique=True, index=True)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 4. Error Handling
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from mop.error import BusinessError, ErrCode
|
|
102
|
+
|
|
103
|
+
# Raise business error
|
|
104
|
+
raise BusinessError(ErrCode.USER_NOT_FOUND, "User not found")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 5. Logging
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from mop.logging import logger
|
|
111
|
+
|
|
112
|
+
# Log messages
|
|
113
|
+
logger.info("User logged in", user_id=123)
|
|
114
|
+
logger.error("Failed to process request", error="Invalid input")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 6. Response Formatting
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from mop.response import Response, PageResponse
|
|
121
|
+
from fastapi import FastAPI
|
|
122
|
+
|
|
123
|
+
app = FastAPI()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.get("/users")
|
|
127
|
+
async def get_users():
|
|
128
|
+
users = [{"id": 1, "name": "John"}, {"id": 2, "name": "Jane"}]
|
|
129
|
+
return Response(data=users, message="Success")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@app.get("/users/page")
|
|
133
|
+
async def get_users_page():
|
|
134
|
+
users = [{"id": 1, "name": "John"}, {"id": 2, "name": "Jane"}]
|
|
135
|
+
return PageResponse(data=users, total=100, page=1, page_size=20)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 7. CRUD Operations
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from mop.crud import CRUDBase
|
|
142
|
+
from your_model import User, UserCreate, UserUpdate
|
|
143
|
+
|
|
144
|
+
# Create CRUD instance
|
|
145
|
+
user_crud = CRUDBase(User)
|
|
146
|
+
|
|
147
|
+
# Create user
|
|
148
|
+
user = user_crud.create(db, obj_in=UserCreate(username="test", email="test@example.com"))
|
|
149
|
+
|
|
150
|
+
# Get user by ID
|
|
151
|
+
user = user_crud.get(db, id=1)
|
|
152
|
+
|
|
153
|
+
# Update user
|
|
154
|
+
user = user_crud.update(db, db_obj=user, obj_in=UserUpdate(email="new@example.com"))
|
|
155
|
+
|
|
156
|
+
# Delete user
|
|
157
|
+
user_crud.remove(db, id=1)
|
|
158
|
+
|
|
159
|
+
# Get users with pagination
|
|
160
|
+
users, total = user_crud.get_multi(db, page=1, page_size=10)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 8. Snowflake ID Generation
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from mop.snowflake import SnowflakeGenerator
|
|
167
|
+
|
|
168
|
+
# Create generator instance
|
|
169
|
+
generator = SnowflakeGenerator(datacenter_id=1, worker_id=1)
|
|
170
|
+
|
|
171
|
+
# Generate unique ID
|
|
172
|
+
unique_id = generator.generate()
|
|
173
|
+
print(unique_id)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Utilities
|
|
177
|
+
|
|
178
|
+
### Date Utilities
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from mop.util.dates import format_datetime, parse_datetime
|
|
182
|
+
|
|
183
|
+
# Format datetime
|
|
184
|
+
formatted = format_datetime(datetime.now())
|
|
185
|
+
print(formatted)
|
|
186
|
+
|
|
187
|
+
# Parse datetime
|
|
188
|
+
parsed = parse_datetime("2023-01-01 12:00:00")
|
|
189
|
+
print(parsed)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### String Utilities
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from mop.util.strings import random_string, is_email_valid
|
|
196
|
+
|
|
197
|
+
# Generate random string
|
|
198
|
+
rand_str = random_string(10)
|
|
199
|
+
print(rand_str)
|
|
200
|
+
|
|
201
|
+
# Check if email is valid
|
|
202
|
+
is_valid = is_email_valid("test@example.com")
|
|
203
|
+
print(is_valid)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### File Utilities
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from mop.util.files import read_file, write_file
|
|
210
|
+
|
|
211
|
+
# Read file
|
|
212
|
+
content = read_file("test.txt")
|
|
213
|
+
print(content)
|
|
214
|
+
|
|
215
|
+
# Write file
|
|
216
|
+
write_file("test.txt", "Hello, World!")
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Changelog
|
|
220
|
+
|
|
221
|
+
### Version 0.1.0 (2025-12-15)
|
|
222
|
+
|
|
223
|
+
#### Added
|
|
224
|
+
- Initial release of MoP framework
|
|
225
|
+
- Configuration management module
|
|
226
|
+
- Database support with synchronous and asynchronous sessions
|
|
227
|
+
- Base entity class for data modeling
|
|
228
|
+
- Centralized error handling system
|
|
229
|
+
- Structured logging configuration
|
|
230
|
+
- Consistent API response formatting
|
|
231
|
+
- Generic CRUD operations with pagination
|
|
232
|
+
- Utility functions for dates, strings, files, and more
|
|
233
|
+
- Snowflake ID generation for distributed systems
|
|
234
|
+
|
|
235
|
+
## Contributing
|
|
236
|
+
|
|
237
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT License
|
|
242
|
+
|
|
243
|
+
## Author
|
|
244
|
+
|
|
245
|
+
- **puras** - [puras.he@gmail.com](mailto:puras.he@gmail.com)
|
|
246
|
+
|
|
247
|
+
## GitHub
|
|
248
|
+
|
|
249
|
+
[https://github.com/puras/mop](https://github.com/puras/mop)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .settings import *
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from pydantic.v1 import BaseSettings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Environment(str, Enum):
|
|
9
|
+
DEVELOPMENT = "dev"
|
|
10
|
+
PRODUCTION = "prod"
|
|
11
|
+
TEST = "test"
|
|
12
|
+
|
|
13
|
+
def get_environment() -> Environment:
|
|
14
|
+
match os.getenv("APP_ENV", "development").lower():
|
|
15
|
+
case "prod" | "production":
|
|
16
|
+
return Environment.PRODUCTION
|
|
17
|
+
case "test":
|
|
18
|
+
return Environment.TEST
|
|
19
|
+
case _:
|
|
20
|
+
return Environment.DEVELOPMENT
|
|
21
|
+
|
|
22
|
+
class Settings(BaseSettings):
|
|
23
|
+
# 基础配置
|
|
24
|
+
PROJECT_NAME: str = "MOSS"
|
|
25
|
+
PROJECT_DESCRIPTION: str = ""
|
|
26
|
+
VERSION: str = "0.1.0"
|
|
27
|
+
DEBUG: bool = True
|
|
28
|
+
ENVIRONMENT = get_environment()
|
|
29
|
+
|
|
30
|
+
# API配置
|
|
31
|
+
API_V1_STR: str = "/api/v1"
|
|
32
|
+
HOST: str = os.getenv("HOST", "0.0.0.0")
|
|
33
|
+
PORT: int = os.getenv("PORT", 8000)
|
|
34
|
+
WORKERS: int = 4
|
|
35
|
+
|
|
36
|
+
# 日志配置
|
|
37
|
+
LOG_DIR: str =os.getenv(
|
|
38
|
+
"LOG_DIR",
|
|
39
|
+
"logs"
|
|
40
|
+
)
|
|
41
|
+
LOG_LEVEL: str = os.getenv(
|
|
42
|
+
"LOG_LEVEL",
|
|
43
|
+
"INFO"
|
|
44
|
+
)
|
|
45
|
+
LOG_FORMAT: str = os.getenv(
|
|
46
|
+
"LOG_FORMAT",
|
|
47
|
+
"json"
|
|
48
|
+
) # json or console
|
|
49
|
+
|
|
50
|
+
# 安全配置
|
|
51
|
+
ALLOWED_ORIGINS: List[str] = [
|
|
52
|
+
"*",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# 数据库配置
|
|
56
|
+
SNOWFLAKE_DATACENTER_ID: int = os.getenv("SNOWFLAKE_DATACENTER_ID", 0)
|
|
57
|
+
|
|
58
|
+
DATABASE_URI: str = os.getenv(
|
|
59
|
+
"DATABASE_URI",
|
|
60
|
+
"sqlite:///./instance/moss.db" # 使用异步SQLite驱动
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
ASYNC_DATABASE_URI: str = os.getenv(
|
|
64
|
+
"ASYNC_DATABASE_URI",
|
|
65
|
+
"sqlite+aiosqlite:///./instance/moss.db" # 使用异步SQLite驱动
|
|
66
|
+
)
|
|
67
|
+
DB_TABLE_PREFIX = os.getenv(
|
|
68
|
+
"DB_TABLE_PREFIX",
|
|
69
|
+
"mo_"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
MINIO_ENDPOINT: str = os.getenv(
|
|
73
|
+
"MINIO_ENDPOINT",
|
|
74
|
+
"http://127.0.0.1:9000"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
MINIO_SHARE_ENDPOINT: str = os.getenv(
|
|
78
|
+
"MINIO_SHARE_ENDPOINT",
|
|
79
|
+
"http://127.0.0.1:9000/minio"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
MINIO_ACCESS_KEY: str = os.getenv(
|
|
83
|
+
"MINIO_ACCESS_KEY",
|
|
84
|
+
""
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
MINIO_SECRET_KEY: str = os.getenv(
|
|
88
|
+
"MINIO_SECRET_KEY",
|
|
89
|
+
""
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
MINIO_DEFAULT_BUCKET: str = os.getenv(
|
|
93
|
+
"MINIO_DEFAULT_BUCKET",
|
|
94
|
+
"mo"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
STORAGE_TYPE: str = os.getenv(
|
|
98
|
+
"STORAGE_TYPE",
|
|
99
|
+
"local"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
OPENDAL_SCHEME: str = os.getenv(
|
|
103
|
+
"OPENDAL_SCHEME",
|
|
104
|
+
"fs"
|
|
105
|
+
)
|
|
106
|
+
OPENDAL_FS_ROOT: str = os.getenv(
|
|
107
|
+
"OPENDAL_FS_ROOT",
|
|
108
|
+
"storage"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
TEMP_DIR: str = os.getenv(
|
|
112
|
+
"TEMP_DIR",
|
|
113
|
+
"temp"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
AUTH_SECRET_KEY: str = os.getenv(
|
|
117
|
+
"AUTH_SECRET_KEY",
|
|
118
|
+
"09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
|
|
119
|
+
)
|
|
120
|
+
AUTH_SECRET_ALGORITHM: str = os.getenv(
|
|
121
|
+
"AUTH_SECRET_ALGORITHM",
|
|
122
|
+
"HS256"
|
|
123
|
+
)
|
|
124
|
+
AUTH_ACCESS_TOKEN_EXPIRE: int = os.getenv(
|
|
125
|
+
"AUTH_ACCESS_TOKEN_EXPIRE",
|
|
126
|
+
30
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
class Config:
|
|
130
|
+
env_file = ".env"
|
|
131
|
+
case_sensitive = True
|
|
132
|
+
|
|
133
|
+
settings = Settings()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from typing import Generic, TypeVar
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import select, func
|
|
4
|
+
|
|
5
|
+
from .page import ListSlice
|
|
6
|
+
from mop.db import AsyncSessionLocal, SessionLocal
|
|
7
|
+
from mop.entity import BaseEntity
|
|
8
|
+
from mop.error import DATA_NOT_FOUND, BizError
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T", bound=BaseEntity)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CrudBiz(Generic[T]):
|
|
14
|
+
def __init__(self, model: type[T]):
|
|
15
|
+
self.model = model
|
|
16
|
+
|
|
17
|
+
def sync_find_all(self, **kwargs):
|
|
18
|
+
filters = [self.model.deleted == False]
|
|
19
|
+
|
|
20
|
+
for key, value in kwargs.items():
|
|
21
|
+
if hasattr(self.model, key):
|
|
22
|
+
filters.append(getattr(self.model, key) == value)
|
|
23
|
+
|
|
24
|
+
with SessionLocal() as db:
|
|
25
|
+
try:
|
|
26
|
+
ret = db.execute(select(self.model).where(*filters))
|
|
27
|
+
items = ret.scalars().all()
|
|
28
|
+
return items
|
|
29
|
+
finally:
|
|
30
|
+
db.close()
|
|
31
|
+
|
|
32
|
+
def sync_find_by_id(self, item_id: str):
|
|
33
|
+
filters = [self.model.id == item_id, self.model.deleted == False]
|
|
34
|
+
with SessionLocal() as db:
|
|
35
|
+
try:
|
|
36
|
+
ret = db.execute(select(self.model).where(*filters))
|
|
37
|
+
ret = ret.scalars().first()
|
|
38
|
+
if not ret:
|
|
39
|
+
raise BizError.init(DATA_NOT_FOUND)
|
|
40
|
+
return ret
|
|
41
|
+
finally:
|
|
42
|
+
db.close()
|
|
43
|
+
|
|
44
|
+
async def find_all(self, **kwargs):
|
|
45
|
+
filters = [self.model.deleted == False]
|
|
46
|
+
|
|
47
|
+
for key, value in kwargs.items():
|
|
48
|
+
if hasattr(self.model, key):
|
|
49
|
+
filters.append(getattr(self.model, key) == value)
|
|
50
|
+
|
|
51
|
+
async with AsyncSessionLocal() as db:
|
|
52
|
+
try:
|
|
53
|
+
ret = await db.execute(select(self.model).where(*filters))
|
|
54
|
+
items = ret.scalars().all()
|
|
55
|
+
return items
|
|
56
|
+
finally:
|
|
57
|
+
await db.close()
|
|
58
|
+
|
|
59
|
+
async def count_by(self, **kwargs):
|
|
60
|
+
filters = [self.model.deleted == False]
|
|
61
|
+
for key, value in kwargs.items():
|
|
62
|
+
if hasattr(self.model, key):
|
|
63
|
+
filters.append(getattr(self.model, key) == value)
|
|
64
|
+
primary_column = self.model.__mapper__.primary_key[0]
|
|
65
|
+
async with AsyncSessionLocal() as db:
|
|
66
|
+
try:
|
|
67
|
+
cnt = await db.execute(select(func.count(primary_column)).where(*filters))
|
|
68
|
+
return cnt.scalar()
|
|
69
|
+
finally:
|
|
70
|
+
await db.close()
|
|
71
|
+
|
|
72
|
+
async def _count_by(self, db: AsyncSessionLocal, *filters):
|
|
73
|
+
primary_column = self.model.__mapper__.primary_key[0]
|
|
74
|
+
cnt = await db.execute(select(func.count(primary_column)).where(*filters))
|
|
75
|
+
return cnt.scalar()
|
|
76
|
+
|
|
77
|
+
async def find_by(self, page_num: int = 1, page_size: int = 100, **kwargs):
|
|
78
|
+
filters = [self.model.deleted == False]
|
|
79
|
+
|
|
80
|
+
for key, value in kwargs.items():
|
|
81
|
+
if hasattr(self.model, key):
|
|
82
|
+
filters.append(getattr(self.model, key) == value)
|
|
83
|
+
|
|
84
|
+
skip = (page_num - 1) * page_size
|
|
85
|
+
async with AsyncSessionLocal() as db:
|
|
86
|
+
try:
|
|
87
|
+
print(filters)
|
|
88
|
+
total = await self._count_by(db, *filters)
|
|
89
|
+
ret = await db.execute(
|
|
90
|
+
select(self.model).where(*filters).offset(skip).limit(page_size)
|
|
91
|
+
)
|
|
92
|
+
items = ret.scalars().all()
|
|
93
|
+
return ListSlice(
|
|
94
|
+
items=items, total=total, page_num=page_num, page_size=page_size
|
|
95
|
+
)
|
|
96
|
+
finally:
|
|
97
|
+
await db.close()
|
|
98
|
+
|
|
99
|
+
async def find_by_id(self, item_id: str):
|
|
100
|
+
filters = [self.model.id == item_id, self.model.deleted == False]
|
|
101
|
+
async with AsyncSessionLocal() as db:
|
|
102
|
+
try:
|
|
103
|
+
ret = await db.execute(select(self.model).where(*filters))
|
|
104
|
+
ret = ret.scalars().first()
|
|
105
|
+
if not ret:
|
|
106
|
+
raise BizError.init(DATA_NOT_FOUND)
|
|
107
|
+
return ret
|
|
108
|
+
finally:
|
|
109
|
+
await db.close()
|
|
110
|
+
|
|
111
|
+
async def find_one(self, **kwargs):
|
|
112
|
+
filters = [self.model.deleted == False]
|
|
113
|
+
|
|
114
|
+
for key, value in kwargs.items():
|
|
115
|
+
if hasattr(self.model, key):
|
|
116
|
+
filters.append(getattr(self.model, key) == value)
|
|
117
|
+
|
|
118
|
+
async with AsyncSessionLocal() as db:
|
|
119
|
+
try:
|
|
120
|
+
ret = await db.execute(select(self.model).where(*filters))
|
|
121
|
+
item = ret.scalars().first()
|
|
122
|
+
return item
|
|
123
|
+
finally:
|
|
124
|
+
await db.close()
|
|
125
|
+
|
|
126
|
+
async def create(self, **kwargs):
|
|
127
|
+
item = self.model(**kwargs)
|
|
128
|
+
item.default_created()
|
|
129
|
+
async with AsyncSessionLocal() as db:
|
|
130
|
+
try:
|
|
131
|
+
db.add(item)
|
|
132
|
+
await db.commit()
|
|
133
|
+
await db.refresh(item)
|
|
134
|
+
return item
|
|
135
|
+
finally:
|
|
136
|
+
await db.close()
|
|
137
|
+
|
|
138
|
+
async def update(self, item_id: str, **kwargs):
|
|
139
|
+
item = await self.find_by_id(item_id)
|
|
140
|
+
|
|
141
|
+
other = self.model(**kwargs)
|
|
142
|
+
item.update_attrs(other)
|
|
143
|
+
|
|
144
|
+
async with AsyncSessionLocal() as db:
|
|
145
|
+
try:
|
|
146
|
+
db.add(item)
|
|
147
|
+
await db.commit()
|
|
148
|
+
await db.refresh(item)
|
|
149
|
+
return item
|
|
150
|
+
finally:
|
|
151
|
+
await db.close()
|
|
152
|
+
|
|
153
|
+
async def delete(self, item_id: str) -> bool:
|
|
154
|
+
item = await self.find_by_id(item_id)
|
|
155
|
+
item.default_deleted()
|
|
156
|
+
async with AsyncSessionLocal() as db:
|
|
157
|
+
try:
|
|
158
|
+
db.add(item)
|
|
159
|
+
await db.commit()
|
|
160
|
+
await db.refresh(item)
|
|
161
|
+
return True
|
|
162
|
+
finally:
|
|
163
|
+
await db.close()
|
|
164
|
+
return False
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import List, TypeVar, Generic
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T")
|
|
6
|
+
|
|
7
|
+
class ListSlice(BaseModel, Generic[T]):
|
|
8
|
+
items: List[T] = Field(description="当前页的数据集合")
|
|
9
|
+
total: int = Field(description="总记录数")
|
|
10
|
+
page_num: int = Field(1, description="当前页面, 默认为1")
|
|
11
|
+
page_size: int = Field(10, description="每页的数据量,默认为10")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import AsyncGenerator, Annotated
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends
|
|
4
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
5
|
+
from sqlalchemy.orm import sessionmaker
|
|
6
|
+
|
|
7
|
+
from mop.conf import settings
|
|
8
|
+
|
|
9
|
+
async_engine = create_async_engine(
|
|
10
|
+
settings.ASYNC_DATABASE_URI,
|
|
11
|
+
echo=settings.DEBUG
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
AsyncSessionLocal = sessionmaker(
|
|
15
|
+
async_engine,
|
|
16
|
+
class_=AsyncSession,
|
|
17
|
+
expire_on_commit=False,
|
|
18
|
+
autocommit=False,
|
|
19
|
+
autoflush=False,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
|
23
|
+
"""
|
|
24
|
+
获取数据库会话依赖
|
|
25
|
+
:return:
|
|
26
|
+
"""
|
|
27
|
+
async with AsyncSessionLocal() as session:
|
|
28
|
+
try:
|
|
29
|
+
yield session
|
|
30
|
+
finally:
|
|
31
|
+
await session.close()
|
|
32
|
+
|
|
33
|
+
AsyncSessionDep = Annotated[AsyncSession, Depends(get_async_session)]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from sqlalchemy.orm import declared_attr, declarative_base
|
|
2
|
+
|
|
3
|
+
from mop.conf import settings
|
|
4
|
+
from mop.util.strings import to_underline
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Base:
|
|
8
|
+
@declared_attr
|
|
9
|
+
def __tablename__(self):
|
|
10
|
+
return f"{settings.DB_TABLE_PREFIX}{to_underline(self.__name__)}"
|
|
11
|
+
|
|
12
|
+
Base = declarative_base(cls=Base)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends
|
|
4
|
+
from sqlalchemy import create_engine
|
|
5
|
+
from sqlalchemy.orm import sessionmaker, Session
|
|
6
|
+
|
|
7
|
+
from mop.conf import settings
|
|
8
|
+
|
|
9
|
+
engine = create_engine(
|
|
10
|
+
settings.DATABASE_URI,
|
|
11
|
+
echo=settings.DEBUG
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
SessionLocal = sessionmaker(
|
|
15
|
+
engine,
|
|
16
|
+
class_=Session,
|
|
17
|
+
expire_on_commit=False,
|
|
18
|
+
autocommit=False,
|
|
19
|
+
autoflush=False,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def get_session():
|
|
23
|
+
with SessionLocal() as session:
|
|
24
|
+
try:
|
|
25
|
+
yield session
|
|
26
|
+
finally:
|
|
27
|
+
session.close()
|
|
28
|
+
|
|
29
|
+
SessionDep = Annotated[Session, Depends(get_session)]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .entity import *
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import Column, String, Boolean, DateTime, text, Integer
|
|
4
|
+
|
|
5
|
+
from mop.db import Base
|
|
6
|
+
from mop.snowflake import IDWorker
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseEntity(Base):
|
|
10
|
+
__abstract__ = True
|
|
11
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
12
|
+
tenant_id = Column(String(32), nullable=False)
|
|
13
|
+
deleted = Column(Boolean, nullable=False, server_default=text("false"))
|
|
14
|
+
created_by = Column(String(32), nullable=True)
|
|
15
|
+
created_at = Column(DateTime, nullable=True, default=datetime.now)
|
|
16
|
+
updated_by = Column(String(32), nullable=True)
|
|
17
|
+
updated_at = Column(DateTime, nullable=True, default=datetime.now)
|
|
18
|
+
deleted_by = Column(String(32), nullable=True)
|
|
19
|
+
deleted_at = Column(DateTime, nullable=True)
|
|
20
|
+
|
|
21
|
+
def default_created(self):
|
|
22
|
+
self.id = IDWorker.gen_id()
|
|
23
|
+
self.tenant_id = '000000'
|
|
24
|
+
|
|
25
|
+
def update_attrs(self, other):
|
|
26
|
+
for attr in other.__dict__.keys():
|
|
27
|
+
if hasattr(self, attr) and attr != 'id' and attr != '_sa_instance_state':
|
|
28
|
+
setattr(self, attr, getattr(other, attr))
|
|
29
|
+
self.updated_at = datetime.now()
|
|
30
|
+
|
|
31
|
+
def default_deleted(self):
|
|
32
|
+
self.deleted = True
|
|
33
|
+
self.deleted_at = datetime.now()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|