aury-boot 0.0.2__py3-none-any.whl → 0.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aury/boot/__init__.py +66 -0
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +120 -0
- aury/boot/application/app/__init__.py +39 -0
- aury/boot/application/app/base.py +511 -0
- aury/boot/application/app/components.py +434 -0
- aury/boot/application/app/middlewares.py +101 -0
- aury/boot/application/config/__init__.py +44 -0
- aury/boot/application/config/settings.py +663 -0
- aury/boot/application/constants/__init__.py +19 -0
- aury/boot/application/constants/components.py +50 -0
- aury/boot/application/constants/scheduler.py +28 -0
- aury/boot/application/constants/service.py +29 -0
- aury/boot/application/errors/__init__.py +55 -0
- aury/boot/application/errors/chain.py +80 -0
- aury/boot/application/errors/codes.py +67 -0
- aury/boot/application/errors/exceptions.py +238 -0
- aury/boot/application/errors/handlers.py +320 -0
- aury/boot/application/errors/response.py +120 -0
- aury/boot/application/interfaces/__init__.py +76 -0
- aury/boot/application/interfaces/egress.py +224 -0
- aury/boot/application/interfaces/ingress.py +98 -0
- aury/boot/application/middleware/__init__.py +22 -0
- aury/boot/application/middleware/logging.py +451 -0
- aury/boot/application/migrations/__init__.py +13 -0
- aury/boot/application/migrations/manager.py +685 -0
- aury/boot/application/migrations/setup.py +237 -0
- aury/boot/application/rpc/__init__.py +63 -0
- aury/boot/application/rpc/base.py +108 -0
- aury/boot/application/rpc/client.py +294 -0
- aury/boot/application/rpc/discovery.py +218 -0
- aury/boot/application/scheduler/__init__.py +13 -0
- aury/boot/application/scheduler/runner.py +123 -0
- aury/boot/application/server/__init__.py +296 -0
- aury/boot/commands/__init__.py +30 -0
- aury/boot/commands/add.py +76 -0
- aury/boot/commands/app.py +105 -0
- aury/boot/commands/config.py +177 -0
- aury/boot/commands/docker.py +367 -0
- aury/boot/commands/docs.py +284 -0
- aury/boot/commands/generate.py +1277 -0
- aury/boot/commands/init.py +892 -0
- aury/boot/commands/migrate/__init__.py +37 -0
- aury/boot/commands/migrate/app.py +54 -0
- aury/boot/commands/migrate/commands.py +303 -0
- aury/boot/commands/scheduler.py +124 -0
- aury/boot/commands/server/__init__.py +21 -0
- aury/boot/commands/server/app.py +541 -0
- aury/boot/commands/templates/generate/api.py.tpl +105 -0
- aury/boot/commands/templates/generate/model.py.tpl +17 -0
- aury/boot/commands/templates/generate/repository.py.tpl +19 -0
- aury/boot/commands/templates/generate/schema.py.tpl +29 -0
- aury/boot/commands/templates/generate/service.py.tpl +48 -0
- aury/boot/commands/templates/project/CLI.md.tpl +92 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
- aury/boot/commands/templates/project/README.md.tpl +111 -0
- aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
- aury/boot/commands/templates/project/config.py.tpl +30 -0
- aury/boot/commands/templates/project/conftest.py.tpl +26 -0
- aury/boot/commands/templates/project/env.example.tpl +213 -0
- aury/boot/commands/templates/project/gitignore.tpl +128 -0
- aury/boot/commands/templates/project/main.py.tpl +41 -0
- aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
- aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
- aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
- aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
- aury/boot/commands/worker.py +143 -0
- aury/boot/common/__init__.py +35 -0
- aury/boot/common/exceptions/__init__.py +114 -0
- aury/boot/common/i18n/__init__.py +16 -0
- aury/boot/common/i18n/translator.py +272 -0
- aury/boot/common/logging/__init__.py +716 -0
- aury/boot/contrib/__init__.py +10 -0
- aury/boot/contrib/admin_console/__init__.py +18 -0
- aury/boot/contrib/admin_console/auth.py +137 -0
- aury/boot/contrib/admin_console/discovery.py +69 -0
- aury/boot/contrib/admin_console/install.py +172 -0
- aury/boot/contrib/admin_console/utils.py +44 -0
- aury/boot/domain/__init__.py +79 -0
- aury/boot/domain/exceptions/__init__.py +132 -0
- aury/boot/domain/models/__init__.py +51 -0
- aury/boot/domain/models/base.py +69 -0
- aury/boot/domain/models/mixins.py +135 -0
- aury/boot/domain/models/models.py +96 -0
- aury/boot/domain/pagination/__init__.py +279 -0
- aury/boot/domain/repository/__init__.py +23 -0
- aury/boot/domain/repository/impl.py +423 -0
- aury/boot/domain/repository/interceptors.py +47 -0
- aury/boot/domain/repository/interface.py +106 -0
- aury/boot/domain/repository/query_builder.py +348 -0
- aury/boot/domain/service/__init__.py +11 -0
- aury/boot/domain/service/base.py +73 -0
- aury/boot/domain/transaction/__init__.py +404 -0
- aury/boot/infrastructure/__init__.py +104 -0
- aury/boot/infrastructure/cache/__init__.py +31 -0
- aury/boot/infrastructure/cache/backends.py +348 -0
- aury/boot/infrastructure/cache/base.py +68 -0
- aury/boot/infrastructure/cache/exceptions.py +37 -0
- aury/boot/infrastructure/cache/factory.py +94 -0
- aury/boot/infrastructure/cache/manager.py +274 -0
- aury/boot/infrastructure/database/__init__.py +39 -0
- aury/boot/infrastructure/database/config.py +71 -0
- aury/boot/infrastructure/database/exceptions.py +44 -0
- aury/boot/infrastructure/database/manager.py +317 -0
- aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
- aury/boot/infrastructure/database/strategies/__init__.py +198 -0
- aury/boot/infrastructure/di/__init__.py +15 -0
- aury/boot/infrastructure/di/container.py +393 -0
- aury/boot/infrastructure/events/__init__.py +33 -0
- aury/boot/infrastructure/events/bus.py +362 -0
- aury/boot/infrastructure/events/config.py +52 -0
- aury/boot/infrastructure/events/consumer.py +134 -0
- aury/boot/infrastructure/events/middleware.py +51 -0
- aury/boot/infrastructure/events/models.py +63 -0
- aury/boot/infrastructure/monitoring/__init__.py +529 -0
- aury/boot/infrastructure/scheduler/__init__.py +19 -0
- aury/boot/infrastructure/scheduler/exceptions.py +37 -0
- aury/boot/infrastructure/scheduler/manager.py +478 -0
- aury/boot/infrastructure/storage/__init__.py +38 -0
- aury/boot/infrastructure/storage/base.py +164 -0
- aury/boot/infrastructure/storage/exceptions.py +37 -0
- aury/boot/infrastructure/storage/factory.py +88 -0
- aury/boot/infrastructure/tasks/__init__.py +24 -0
- aury/boot/infrastructure/tasks/config.py +45 -0
- aury/boot/infrastructure/tasks/constants.py +37 -0
- aury/boot/infrastructure/tasks/exceptions.py +37 -0
- aury/boot/infrastructure/tasks/manager.py +490 -0
- aury/boot/testing/__init__.py +24 -0
- aury/boot/testing/base.py +122 -0
- aury/boot/testing/client.py +163 -0
- aury/boot/testing/factory.py +154 -0
- aury/boot/toolkit/__init__.py +21 -0
- aury/boot/toolkit/http/__init__.py +367 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/METADATA +3 -2
- aury_boot-0.0.4.dist-info/RECORD +137 -0
- aury_boot-0.0.2.dist-info/RECORD +0 -5
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""错误处理器实现。
|
|
2
|
+
|
|
3
|
+
提供责任链模式的错误处理器。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from fastapi import HTTPException, Request, status
|
|
12
|
+
from fastapi.responses import JSONResponse
|
|
13
|
+
from pydantic import ValidationError
|
|
14
|
+
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
|
15
|
+
|
|
16
|
+
from aury.boot.common.exceptions import FoundationError
|
|
17
|
+
from aury.boot.common.logging import logger
|
|
18
|
+
from aury.boot.domain.exceptions import (
|
|
19
|
+
ModelError,
|
|
20
|
+
ServiceException,
|
|
21
|
+
)
|
|
22
|
+
from aury.boot.domain.exceptions import (
|
|
23
|
+
VersionConflictError as DomainVersionConflictError,
|
|
24
|
+
)
|
|
25
|
+
from aury.boot.infrastructure.database.exceptions import (
|
|
26
|
+
DatabaseError as InfraDatabaseError,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from ..interfaces.egress import ResponseBuilder
|
|
30
|
+
from .exceptions import BaseError, BusinessError, VersionConflictError
|
|
31
|
+
from .response import ErrorDetail
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ErrorHandler(ABC):
|
|
35
|
+
"""错误处理器抽象基类 - 责任链模式。"""
|
|
36
|
+
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
"""初始化处理器。"""
|
|
39
|
+
self._next_handler: ErrorHandler | None = None
|
|
40
|
+
|
|
41
|
+
def set_next(self, handler: ErrorHandler) -> ErrorHandler:
|
|
42
|
+
"""设置下一个处理器。
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
handler: 下一个处理器
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
ErrorHandler: 下一个处理器(支持链式调用)
|
|
49
|
+
"""
|
|
50
|
+
self._next_handler = handler
|
|
51
|
+
return handler
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def can_handle(self, exception: Exception) -> bool:
|
|
55
|
+
"""判断是否可以处理该异常。
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
exception: 异常对象
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
bool: 是否可以处理
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
async def handle(self, exception: Exception, request: Request) -> JSONResponse:
|
|
67
|
+
"""处理异常。
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
exception: 异常对象
|
|
71
|
+
request: 请求对象
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
JSONResponse: 响应对象
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
async def process(self, exception: Exception, request: Request) -> JSONResponse:
|
|
79
|
+
"""处理异常(责任链入口)。
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
exception: 异常对象
|
|
83
|
+
request: 请求对象
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
JSONResponse: 响应对象
|
|
87
|
+
"""
|
|
88
|
+
if self.can_handle(exception):
|
|
89
|
+
return await self.handle(exception, request)
|
|
90
|
+
|
|
91
|
+
if self._next_handler:
|
|
92
|
+
return await self._next_handler.process(exception, request)
|
|
93
|
+
|
|
94
|
+
# 默认处理
|
|
95
|
+
return await self._default_handle(exception, request)
|
|
96
|
+
|
|
97
|
+
async def _default_handle(self, exception: Exception, request: Request) -> JSONResponse:
|
|
98
|
+
"""默认异常处理。"""
|
|
99
|
+
logger.exception(f"未处理的异常: {request.method} {request.url.path}")
|
|
100
|
+
|
|
101
|
+
response = ResponseBuilder.fail(
|
|
102
|
+
message="服务器内部错误",
|
|
103
|
+
code=-1,
|
|
104
|
+
)
|
|
105
|
+
return JSONResponse(
|
|
106
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
107
|
+
content=response.model_dump(mode="json"),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class BaseErrorHandler(ErrorHandler):
|
|
112
|
+
"""自定义基础异常处理器。"""
|
|
113
|
+
|
|
114
|
+
def can_handle(self, exception: Exception) -> bool:
|
|
115
|
+
"""判断是否为自定义异常。"""
|
|
116
|
+
return isinstance(exception, BaseError)
|
|
117
|
+
|
|
118
|
+
async def handle(self, exception: BaseError, request: Request) -> JSONResponse:
|
|
119
|
+
"""处理自定义异常。"""
|
|
120
|
+
logger.warning(f"业务异常: {exception}")
|
|
121
|
+
|
|
122
|
+
errors = [detail.model_dump() for detail in exception.details] if exception.details else None
|
|
123
|
+
|
|
124
|
+
response = ResponseBuilder.fail(
|
|
125
|
+
message=exception.message,
|
|
126
|
+
code=int(exception.code.value),
|
|
127
|
+
errors=errors,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# metadata 放入 details 字段
|
|
131
|
+
if exception.metadata:
|
|
132
|
+
response.details = exception.metadata
|
|
133
|
+
|
|
134
|
+
return JSONResponse(
|
|
135
|
+
status_code=exception.status_code,
|
|
136
|
+
content=response.model_dump(mode="json"),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class HTTPExceptionHandler(ErrorHandler):
|
|
141
|
+
"""FastAPI HTTP异常处理器。"""
|
|
142
|
+
|
|
143
|
+
def can_handle(self, exception: Exception) -> bool:
|
|
144
|
+
"""判断是否为HTTP异常。"""
|
|
145
|
+
return isinstance(exception, HTTPException)
|
|
146
|
+
|
|
147
|
+
async def handle(self, exception: HTTPException, request: Request) -> JSONResponse:
|
|
148
|
+
"""处理HTTP异常。"""
|
|
149
|
+
logger.warning(f"HTTP异常: {exception.status_code} - {exception.detail}")
|
|
150
|
+
|
|
151
|
+
response = ResponseBuilder.fail(
|
|
152
|
+
message=exception.detail,
|
|
153
|
+
code=exception.status_code,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return JSONResponse(
|
|
157
|
+
status_code=exception.status_code,
|
|
158
|
+
content=response.model_dump(mode="json"),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ValidationErrorHandler(ErrorHandler):
|
|
163
|
+
"""Pydantic验证异常处理器。"""
|
|
164
|
+
|
|
165
|
+
def can_handle(self, exception: Exception) -> bool:
|
|
166
|
+
"""判断是否为验证异常。"""
|
|
167
|
+
return isinstance(exception, ValidationError)
|
|
168
|
+
|
|
169
|
+
async def handle(self, exception: Exception, request: Request) -> JSONResponse:
|
|
170
|
+
"""处理验证异常。"""
|
|
171
|
+
logger.warning(f"数据验证失败: {exception}")
|
|
172
|
+
|
|
173
|
+
errors = []
|
|
174
|
+
for error in exception.errors():
|
|
175
|
+
errors.append({
|
|
176
|
+
"field": ".".join(str(loc) for loc in error["loc"]),
|
|
177
|
+
"message": error["msg"],
|
|
178
|
+
"type": error["type"],
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
response = ResponseBuilder.fail(
|
|
182
|
+
message="数据验证失败",
|
|
183
|
+
code=400,
|
|
184
|
+
errors=errors,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return JSONResponse(
|
|
188
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
189
|
+
content=response.model_dump(mode="json"),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ServiceErrorHandler(ErrorHandler):
|
|
194
|
+
"""服务层异常处理器。
|
|
195
|
+
|
|
196
|
+
处理 Domain 层的 ServiceException。
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def can_handle(self, exception: Exception) -> bool:
|
|
200
|
+
"""判断是否为服务层异常。"""
|
|
201
|
+
return isinstance(exception, ServiceException)
|
|
202
|
+
|
|
203
|
+
async def handle(self, exception: Exception, request: Request) -> JSONResponse:
|
|
204
|
+
"""处理服务层异常。"""
|
|
205
|
+
if not isinstance(exception, ServiceException):
|
|
206
|
+
return await self._default_handle(exception, request)
|
|
207
|
+
|
|
208
|
+
logger.warning(f"服务层异常: {exception}")
|
|
209
|
+
|
|
210
|
+
# 直接使用 ServiceException 的信息
|
|
211
|
+
response = ResponseBuilder.fail(
|
|
212
|
+
message=exception.message,
|
|
213
|
+
code=400, # 服务层异常默认 400
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# 构建 details
|
|
217
|
+
details: dict = {}
|
|
218
|
+
if exception.code:
|
|
219
|
+
details["code"] = exception.code
|
|
220
|
+
if exception.metadata:
|
|
221
|
+
details.update(exception.metadata)
|
|
222
|
+
if details:
|
|
223
|
+
response.details = details
|
|
224
|
+
|
|
225
|
+
return JSONResponse(
|
|
226
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
227
|
+
content=response.model_dump(mode="json"),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class DatabaseErrorHandler(ErrorHandler):
|
|
232
|
+
"""数据库异常处理器。
|
|
233
|
+
|
|
234
|
+
处理数据库相关错误,包括 Domain 层的异常。
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
def can_handle(self, exception: Exception) -> bool:
|
|
238
|
+
"""判断是否为数据库异常。"""
|
|
239
|
+
return isinstance(exception, SQLAlchemyError | ModelError | InfraDatabaseError | FoundationError)
|
|
240
|
+
|
|
241
|
+
async def handle(self, exception: Exception, request: Request) -> JSONResponse:
|
|
242
|
+
"""处理数据库异常。"""
|
|
243
|
+
# 处理 Domain 层的 VersionConflictError
|
|
244
|
+
if isinstance(exception, DomainVersionConflictError):
|
|
245
|
+
# 转换为应用层异常
|
|
246
|
+
app_error = VersionConflictError.from_domain_exception(exception)
|
|
247
|
+
response = ResponseBuilder.fail(
|
|
248
|
+
message=app_error.message,
|
|
249
|
+
code=app_error.status_code,
|
|
250
|
+
details=[ErrorDetail(
|
|
251
|
+
message=app_error.message,
|
|
252
|
+
code=app_error.code.value,
|
|
253
|
+
)],
|
|
254
|
+
)
|
|
255
|
+
return JSONResponse(
|
|
256
|
+
status_code=app_error.status_code,
|
|
257
|
+
content=response.model_dump(mode="json"),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# 处理唯一约束冲突(如重复的 email、username 等)
|
|
261
|
+
if isinstance(exception, IntegrityError):
|
|
262
|
+
logger.warning(f"数据库完整性约束冲突: {exception}")
|
|
263
|
+
|
|
264
|
+
# 解析错误信息,提取字段名
|
|
265
|
+
error_msg = str(exception.orig) if exception.orig else str(exception)
|
|
266
|
+
field_name = None
|
|
267
|
+
|
|
268
|
+
# 尝试从错误信息中提取字段名
|
|
269
|
+
if "unique constraint" in error_msg.lower() or "duplicate key" in error_msg.lower():
|
|
270
|
+
# PostgreSQL: Key (email)=(xxx) already exists
|
|
271
|
+
# MySQL: Duplicate entry 'xxx' for key 'users.email'
|
|
272
|
+
import re
|
|
273
|
+
# PostgreSQL 格式
|
|
274
|
+
match = re.search(r"Key \((\w+)\)", error_msg)
|
|
275
|
+
if match:
|
|
276
|
+
field_name = match.group(1)
|
|
277
|
+
else:
|
|
278
|
+
# MySQL 格式
|
|
279
|
+
match = re.search(r"for key ['\"]?\w+\.(\w+)['\"]?", error_msg, re.IGNORECASE)
|
|
280
|
+
if match:
|
|
281
|
+
field_name = match.group(1)
|
|
282
|
+
|
|
283
|
+
if field_name:
|
|
284
|
+
message = f"{field_name} 已存在"
|
|
285
|
+
else:
|
|
286
|
+
message = "数据已存在,请检查唯一字段"
|
|
287
|
+
|
|
288
|
+
response = ResponseBuilder.fail(
|
|
289
|
+
message=message,
|
|
290
|
+
code=409,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return JSONResponse(
|
|
294
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
295
|
+
content=response.model_dump(mode="json"),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# 处理其他数据库错误
|
|
299
|
+
logger.exception(f"数据库错误: {request.method} {request.url.path}")
|
|
300
|
+
|
|
301
|
+
response = ResponseBuilder.fail(
|
|
302
|
+
message="数据库操作失败",
|
|
303
|
+
code=500,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return JSONResponse(
|
|
307
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
308
|
+
content=response.model_dump(mode="json"),
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
__all__ = [
|
|
313
|
+
"BaseErrorHandler",
|
|
314
|
+
"DatabaseErrorHandler",
|
|
315
|
+
"ErrorHandler",
|
|
316
|
+
"HTTPExceptionHandler",
|
|
317
|
+
"ServiceErrorHandler",
|
|
318
|
+
"ValidationErrorHandler",
|
|
319
|
+
]
|
|
320
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""错误响应模型(接口层)。
|
|
2
|
+
|
|
3
|
+
提供用于 HTTP API 响应的错误详情模型。
|
|
4
|
+
这是接口层的数据模型,用于序列化和传输。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, ClassVar
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ErrorDetail(BaseModel):
|
|
15
|
+
"""错误详情模型(Pydantic)。
|
|
16
|
+
|
|
17
|
+
支持两种错误类型:
|
|
18
|
+
1. 字段错误:field不为空,表示特定字段的验证错误
|
|
19
|
+
2. 通用错误:field为空,表示系统级或业务级错误
|
|
20
|
+
|
|
21
|
+
使用示例:
|
|
22
|
+
# 字段验证错误
|
|
23
|
+
ErrorDetail(
|
|
24
|
+
field="username",
|
|
25
|
+
message="用户名格式不正确",
|
|
26
|
+
code="INVALID_FORMAT",
|
|
27
|
+
location="body"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# 通用业务错误
|
|
31
|
+
ErrorDetail(
|
|
32
|
+
message="库存不足",
|
|
33
|
+
code="INSUFFICIENT_STOCK"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# 系统错误
|
|
37
|
+
ErrorDetail(
|
|
38
|
+
message="数据库连接失败",
|
|
39
|
+
code="DB_CONNECTION_ERROR"
|
|
40
|
+
)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
message: str = Field(..., description="错误消息")
|
|
44
|
+
code: str | None = Field(None, description="错误代码")
|
|
45
|
+
field: str | None = Field(None, description="错误字段(仅字段验证错误)")
|
|
46
|
+
location: str | None = Field(None, description="错误位置(如:body, query, path)")
|
|
47
|
+
value: Any | None = Field(None, description="导致错误的值")
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def field_error(
|
|
51
|
+
cls,
|
|
52
|
+
field: str,
|
|
53
|
+
message: str,
|
|
54
|
+
code: str | None = None,
|
|
55
|
+
location: str = "body",
|
|
56
|
+
value: Any | None = None,
|
|
57
|
+
) -> "ErrorDetail":
|
|
58
|
+
"""创建字段验证错误。
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
field: 字段名
|
|
62
|
+
message: 错误消息
|
|
63
|
+
code: 错误代码
|
|
64
|
+
location: 错误位置
|
|
65
|
+
value: 错误值
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
ErrorDetail: 错误详情对象
|
|
69
|
+
"""
|
|
70
|
+
return cls(
|
|
71
|
+
message=message,
|
|
72
|
+
code=code,
|
|
73
|
+
field=field,
|
|
74
|
+
location=location,
|
|
75
|
+
value=value,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def generic_error(
|
|
80
|
+
cls,
|
|
81
|
+
message: str,
|
|
82
|
+
code: str | None = None,
|
|
83
|
+
) -> "ErrorDetail":
|
|
84
|
+
"""创建通用错误。
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
message: 错误消息
|
|
88
|
+
code: 错误代码
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
ErrorDetail: 错误详情对象
|
|
92
|
+
"""
|
|
93
|
+
return cls(
|
|
94
|
+
message=message,
|
|
95
|
+
code=code,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
model_config = ConfigDict(
|
|
99
|
+
json_schema_extra={
|
|
100
|
+
"examples": [
|
|
101
|
+
{
|
|
102
|
+
"message": "用户名格式不正确",
|
|
103
|
+
"code": "INVALID_FORMAT",
|
|
104
|
+
"field": "username",
|
|
105
|
+
"location": "body",
|
|
106
|
+
"value": "user@123"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"message": "库存不足",
|
|
110
|
+
"code": "INSUFFICIENT_STOCK"
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = [
|
|
118
|
+
"ErrorDetail",
|
|
119
|
+
]
|
|
120
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""接口层模块。
|
|
2
|
+
|
|
3
|
+
提供API定义,包括:
|
|
4
|
+
- 错误处理
|
|
5
|
+
- 请求模型(Ingress)
|
|
6
|
+
- 响应模型(Egress)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# 错误处理已移至 application.errors
|
|
10
|
+
from ..errors import (
|
|
11
|
+
AlreadyExistsError,
|
|
12
|
+
BaseError,
|
|
13
|
+
BusinessError,
|
|
14
|
+
DatabaseError,
|
|
15
|
+
ErrorCode,
|
|
16
|
+
ErrorDetail,
|
|
17
|
+
ErrorHandler,
|
|
18
|
+
ErrorHandlerChain,
|
|
19
|
+
ForbiddenError,
|
|
20
|
+
NotFoundError,
|
|
21
|
+
UnauthorizedError,
|
|
22
|
+
ValidationError,
|
|
23
|
+
VersionConflictError,
|
|
24
|
+
global_exception_handler,
|
|
25
|
+
)
|
|
26
|
+
from .egress import (
|
|
27
|
+
BaseResponse,
|
|
28
|
+
CountResponse,
|
|
29
|
+
ErrorResponse,
|
|
30
|
+
IDResponse,
|
|
31
|
+
Pagination,
|
|
32
|
+
PaginationResponse,
|
|
33
|
+
ResponseBuilder,
|
|
34
|
+
SuccessResponse,
|
|
35
|
+
)
|
|
36
|
+
from .ingress import (
|
|
37
|
+
BaseRequest,
|
|
38
|
+
FilterRequest,
|
|
39
|
+
ListRequest,
|
|
40
|
+
PaginationRequest,
|
|
41
|
+
SortOrder,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"AlreadyExistsError",
|
|
46
|
+
"BaseError",
|
|
47
|
+
# 请求模型
|
|
48
|
+
"BaseRequest",
|
|
49
|
+
# 响应模型
|
|
50
|
+
"BaseResponse",
|
|
51
|
+
"BusinessError",
|
|
52
|
+
"CountResponse",
|
|
53
|
+
"DatabaseError",
|
|
54
|
+
# 错误处理
|
|
55
|
+
"ErrorCode",
|
|
56
|
+
"ErrorDetail",
|
|
57
|
+
"ErrorHandler",
|
|
58
|
+
"ErrorHandlerChain",
|
|
59
|
+
"ErrorResponse",
|
|
60
|
+
"FilterRequest",
|
|
61
|
+
"ForbiddenError",
|
|
62
|
+
"IDResponse",
|
|
63
|
+
"ListRequest",
|
|
64
|
+
"NotFoundError",
|
|
65
|
+
"Pagination",
|
|
66
|
+
"PaginationRequest",
|
|
67
|
+
"PaginationResponse",
|
|
68
|
+
"ResponseBuilder",
|
|
69
|
+
"SortOrder",
|
|
70
|
+
"SuccessResponse",
|
|
71
|
+
"UnauthorizedError",
|
|
72
|
+
"ValidationError",
|
|
73
|
+
"VersionConflictError",
|
|
74
|
+
"global_exception_handler",
|
|
75
|
+
]
|
|
76
|
+
|