sanic-api 0.3.0a6__tar.gz → 0.4.1__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.
Files changed (33) hide show
  1. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/PKG-INFO +2 -4
  2. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/example/app.py +8 -6
  3. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/pyproject.toml +2 -6
  4. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/requirements-dev.lock +3 -5
  5. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/requirements.lock +3 -5
  6. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/src/sanic_api/__init__.py +1 -1
  7. sanic_api-0.4.1/src/sanic_api/api/__init__.py +1 -0
  8. sanic_api-0.4.1/src/sanic_api/api/error.py +19 -0
  9. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/src/sanic_api/api/request.py +38 -3
  10. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/src/sanic_api/app.py +24 -6
  11. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/src/sanic_api/config/setting.py +38 -1
  12. sanic_api-0.4.1/src/sanic_api/openapi/extension.py +41 -0
  13. sanic_api-0.3.0a6/src/sanic_api/api/__init__.py +0 -17
  14. sanic_api-0.3.0a6/src/sanic_api/api/response.py +0 -34
  15. sanic_api-0.3.0a6/src/sanic_api/openapi/extension.py +0 -0
  16. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/.gitignore +0 -0
  17. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/.pre-commit-config.yaml +0 -0
  18. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/CHANGELOG.md +0 -0
  19. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/COMMIT_GUIDE.MD +0 -0
  20. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/LICENSE.txt +0 -0
  21. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/README.md +0 -0
  22. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/example/__init__.py +0 -0
  23. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/example/log.txt +0 -0
  24. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/example/mini_app.py +0 -0
  25. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/images/logo.png +0 -0
  26. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/src/sanic_api/api/model.py +0 -0
  27. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/src/sanic_api/config/__init__.py +0 -0
  28. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/src/sanic_api/logger/__init__.py +0 -0
  29. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/src/sanic_api/logger/config.py +0 -0
  30. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/src/sanic_api/logger/extension.py +0 -0
  31. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/src/sanic_api/openapi/__init__.py +0 -0
  32. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/src/sanic_api/utils/__init__.py +0 -0
  33. {sanic_api-0.3.0a6 → sanic_api-0.4.1}/src/sanic_api/utils/enum.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sanic-api
3
- Version: 0.3.0a6
3
+ Version: 0.4.1
4
4
  Summary: Sanic 框架实用API工具集,拥有自动生成文档、参数校验、配置的导入、日志功能的优化等功能,更好的助力接口的开发
5
5
  Project-URL: homepage, https://github.com/x-haose/sanic-api
6
6
  Project-URL: repository, https://github.com/x-haose/sanic-api
@@ -23,12 +23,10 @@ Classifier: Topic :: Internet :: WWW/HTTP
23
23
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
24
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
25
  Requires-Python: >=3.10
26
- Requires-Dist: hs-config==0.1.3a5
26
+ Requires-Dist: hs-config==0.1.3
27
27
  Requires-Dist: loguru>=0.7.2
28
28
  Requires-Dist: loki-logger-handler>=0.1.4
29
29
  Requires-Dist: orjson>=3.8.6
30
- Requires-Dist: pydantic-settings[toml,yaml]>=2.5.2
31
- Requires-Dist: pydantic[dotenv]>=2.9.2
32
30
  Requires-Dist: requests>=2.32.0
33
31
  Requires-Dist: sanic-ext>=23.12.0
34
32
  Requires-Dist: sanic>=24.12.0
@@ -2,8 +2,9 @@ from pydantic import BaseModel, Field
2
2
  from sanic import Blueprint, Sanic, json
3
3
  from sanic.log import logger
4
4
 
5
- from sanic_api.api import BaseRespTml, Request
5
+ from sanic_api.api import Request
6
6
  from sanic_api.app import BaseApp
7
+ from sanic_api.config import DefaultSettings
7
8
 
8
9
  user_blueprint = Blueprint("user", "/user")
9
10
 
@@ -12,7 +13,7 @@ class UserInfoModel(BaseModel):
12
13
  user_id: int = Field(title="用户ID")
13
14
 
14
15
 
15
- class UserInfoResponse(BaseRespTml):
16
+ class UserInfoResponse(BaseModel):
16
17
  user_name: str = Field(title="用户名")
17
18
 
18
19
 
@@ -33,9 +34,7 @@ async def user_info(request: Request, json_data: UserInfoModel):
33
34
  """
34
35
  logger.info(f"data: {json_data}")
35
36
  info = UserInfoResponse(user_name="张三")
36
- info.temp_data.code = "0000"
37
- info.temp_data.msg = "查询成功"
38
- return info.resp()
37
+ return request.json_resp(info, server_code="0000", server_msg="查询成功")
39
38
 
40
39
 
41
40
  @user_blueprint.post("login")
@@ -59,4 +58,7 @@ class App(BaseApp):
59
58
 
60
59
 
61
60
  if __name__ == "__main__":
62
- App.run()
61
+ settings = DefaultSettings()
62
+ settings.port = 9999
63
+ settings.logger.loki_url = "http://127.0.0.1:23100/loki/api/v1/push"
64
+ App.run(settings)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sanic-api"
3
- version = "0.3.0a6"
3
+ version = "0.4.1"
4
4
  keywords = ["Sanic", "异步", "Sanic扩展"]
5
5
  description = "Sanic 框架实用API工具集,拥有自动生成文档、参数校验、配置的导入、日志功能的优化等功能,更好的助力接口的开发"
6
6
  authors = [
@@ -29,17 +29,13 @@ dependencies = [
29
29
  # sanic
30
30
  "sanic>=24.12.0",
31
31
  "sanic-ext>=23.12.0",
32
- # pydantic 模型定义
33
- "pydantic[dotenv]>=2.9.2",
34
- # pydantic的设置插件
35
- "pydantic-settings[yaml,toml]>=2.5.2",
36
32
  # 日志打印
37
33
  "loguru>=0.7.2",
38
34
  # json
39
35
  "ujson>=5.7.0",
40
36
  "orjson>=3.8.6",
41
37
  # 配置
42
- "hs-config==0.1.3a5",
38
+ "hs-config==0.1.3",
43
39
  # 哨兵
44
40
  "sentry-sdk>=2.17.0",
45
41
  # loki日志推送
@@ -29,7 +29,7 @@ docutils==0.21.2
29
29
  # via readme-renderer
30
30
  filelock==3.16.1
31
31
  # via virtualenv
32
- hs-config==0.1.3a5
32
+ hs-config==0.1.3
33
33
  # via sanic-api
34
34
  html5tagger==1.3.0
35
35
  # via sanic
@@ -76,15 +76,13 @@ pkginfo==1.12.0
76
76
  platformdirs==4.3.6
77
77
  # via virtualenv
78
78
  pre-commit==4.0.1
79
- pydantic==2.9.2
79
+ pydantic==2.10.6
80
80
  # via hs-config
81
81
  # via pydantic-settings
82
- # via sanic-api
83
- pydantic-core==2.23.4
82
+ pydantic-core==2.27.2
84
83
  # via pydantic
85
84
  pydantic-settings==2.6.1
86
85
  # via hs-config
87
- # via sanic-api
88
86
  pygments==2.18.0
89
87
  # via readme-renderer
90
88
  # via rich
@@ -19,7 +19,7 @@ certifi==2024.8.30
19
19
  # via sentry-sdk
20
20
  charset-normalizer==3.4.0
21
21
  # via requests
22
- hs-config==0.1.3a5
22
+ hs-config==0.1.3
23
23
  # via sanic-api
24
24
  html5tagger==1.3.0
25
25
  # via sanic
@@ -36,15 +36,13 @@ multidict==6.1.0
36
36
  # via sanic
37
37
  orjson==3.10.11
38
38
  # via sanic-api
39
- pydantic==2.9.2
39
+ pydantic==2.10.6
40
40
  # via hs-config
41
41
  # via pydantic-settings
42
- # via sanic-api
43
- pydantic-core==2.23.4
42
+ pydantic-core==2.27.2
44
43
  # via pydantic
45
44
  pydantic-settings==2.6.1
46
45
  # via hs-config
47
- # via sanic-api
48
46
  python-dotenv==1.0.1
49
47
  # via pydantic-settings
50
48
  pyyaml==6.0.2
@@ -1,3 +1,3 @@
1
1
  from sanic_api.logger.extension import LoggerExtend
2
2
 
3
- __version__ = "0.3.0a6"
3
+ __version__ = "0.4.1"
@@ -0,0 +1 @@
1
+ from sanic_api.api.request import Request
@@ -0,0 +1,19 @@
1
+ from sanic.errorpages import JSONRenderer
2
+
3
+ from sanic_api.config.setting import JsonRespSettings
4
+
5
+
6
+ class ErrorJSONRenderer(JSONRenderer):
7
+ json_resp_setting: JsonRespSettings | None = None
8
+
9
+ def _generate_output(self, *, full: bool) -> dict:
10
+ data = super()._generate_output(full=full)
11
+ if not self.json_resp_setting or not self.json_resp_setting.use_tml:
12
+ return data
13
+
14
+ data = {
15
+ self.json_resp_setting.code_field_name: self.json_resp_setting.error_code,
16
+ self.json_resp_setting.msg_field_name: data["message"],
17
+ self.json_resp_setting.data_field_name: data,
18
+ }
19
+ return data
@@ -4,8 +4,12 @@ from typing import get_origin
4
4
 
5
5
  from pydantic import BaseModel
6
6
  from sanic import Request as SanicRequest
7
+ from sanic.compat import Header
8
+ from sanic.response import JSONResponse
7
9
  from sanic_ext.extensions.openapi.builders import OperationStore
8
10
 
11
+ from sanic_api.config.setting import JsonRespSettings
12
+
9
13
 
10
14
  class Request(SanicRequest):
11
15
  """
@@ -13,6 +17,8 @@ class Request(SanicRequest):
13
17
  加入便于接口参数获取校验的方法
14
18
  """
15
19
 
20
+ json_resp_setting: JsonRespSettings
21
+
16
22
  json_data: BaseModel
17
23
  form_data: BaseModel
18
24
  query_data: BaseModel
@@ -22,8 +28,38 @@ class Request(SanicRequest):
22
28
  _form_data_type: type[BaseModel] | None
23
29
  _query_data_type: type[BaseModel] | None
24
30
 
25
- def __init__(self, *args, **kwargs):
26
- super().__init__(*args, **kwargs)
31
+ def json_resp(
32
+ self,
33
+ data: BaseModel | dict | str | int | float | list,
34
+ server_code: str | int = "",
35
+ server_msg: str = "",
36
+ status: int = 200,
37
+ headers: Header | dict[str, str] | None = None,
38
+ ) -> JSONResponse:
39
+ """
40
+ 自定义返回 JSONResponse 的方法
41
+ 如果使用了模板模式则返回模板模式的数据
42
+ 不使用模版情况下:如果data是dict、list直接返回,否则返回{"data": data}
43
+ Args:
44
+ data: 内容
45
+ server_code: 接口服务码
46
+ server_msg: 接口服务消息
47
+ status: 状态码
48
+ headers: 请求头
49
+
50
+ Returns:
51
+
52
+ """
53
+ data = data.model_dump(mode="json") if isinstance(data, BaseModel) else data
54
+ if self.json_resp_setting.use_tml:
55
+ data = {
56
+ self.json_resp_setting.code_field_name: server_code,
57
+ self.json_resp_setting.msg_field_name: server_msg,
58
+ self.json_resp_setting.data_field_name: data,
59
+ }
60
+ else:
61
+ data = {"data": data} if type(data) not in (dict, list) else data
62
+ return JSONResponse(data, status=status, headers=headers)
27
63
 
28
64
  async def receive_body(self):
29
65
  """
@@ -35,7 +71,6 @@ class Request(SanicRequest):
35
71
  await super().receive_body()
36
72
  self._get_data_type()
37
73
  self._load_data()
38
- self._set_openapi()
39
74
 
40
75
  def _get_type(self, name: str, base_type: type):
41
76
  """
@@ -1,5 +1,6 @@
1
1
  import sentry_sdk
2
2
  from sanic import Sanic, text
3
+ from sanic.errorpages import RENDERERS_BY_CONTENT_TYPE
3
4
  from sanic.log import logger
4
5
  from sanic.worker.loader import AppLoader
5
6
  from sanic_ext import Extend
@@ -7,6 +8,7 @@ from sentry_sdk.integrations.asyncio import AsyncioIntegration
7
8
 
8
9
  from sanic_api import LoggerExtend
9
10
  from sanic_api.api import Request
11
+ from sanic_api.api.error import ErrorJSONRenderer
10
12
  from sanic_api.config import DefaultSettings, RunModeEnum
11
13
 
12
14
 
@@ -55,11 +57,13 @@ class BaseApp:
55
57
  Returns:
56
58
 
57
59
  """
60
+ Request.json_resp_setting = self.settings.json_resp
58
61
  app = Sanic(self.name, configure_logging=False, request_class=Request)
59
62
 
60
63
  self._setup_logger(app)
61
64
  self._setup_config(app)
62
65
  self._setup_openai(app)
66
+ self._setup_error_handler(app)
63
67
 
64
68
  app.main_process_stop(self._main_process_stop)
65
69
  app.main_process_start(self._main_process_start)
@@ -153,11 +157,24 @@ class BaseApp:
153
157
  Returns:
154
158
 
155
159
  """
156
- app.config.LOGGING = True
160
+ # app.config.LOGGING = True
161
+ app.config.FALLBACK_ERROR_FORMAT = "json"
157
162
  self._setup_cors(app)
158
163
 
159
164
  def _setup_openai(self, app: Sanic): ...
160
165
 
166
+ def _setup_error_handler(self, _app: Sanic):
167
+ """
168
+ 设置错误处理器
169
+ Args:
170
+ _app: Sanic App
171
+
172
+ Returns:
173
+
174
+ """
175
+ ErrorJSONRenderer.json_resp_setting = self.settings.json_resp
176
+ RENDERERS_BY_CONTENT_TYPE["application/json"] = ErrorJSONRenderer
177
+
161
178
  def _setup_cors(self, app: Sanic):
162
179
  """
163
180
  设置跨域: 开发模式下允许所有跨域,生产模式下使用配置中的跨域列表
@@ -167,12 +184,13 @@ class BaseApp:
167
184
  Returns:
168
185
 
169
186
  """
170
- cors = ",".join(self.settings.cors_origins)
187
+ cors = ",".join(self.settings.cors.origins)
171
188
  if self.settings.mode == RunModeEnum.DEBNUG:
172
- app.config.CORS_ORIGINS = cors or "*"
173
189
  app.config.CORS_SEND_WILDCARD = True
190
+ app.config.CORS_SUPPORTS_CREDENTIALS = self.settings.cors.supports_credentials
191
+ app.config.CORS_ORIGINS = cors or "*"
174
192
  else:
175
- app.config.CORS_ORIGINS = ",".join(self.settings.cors_origins)
193
+ app.config.CORS_ORIGINS = cors
176
194
 
177
195
  async def _setup_route(self, app: Sanic):
178
196
  """
@@ -203,7 +221,7 @@ class BaseApp:
203
221
  retention=log_config.retention,
204
222
  compression=log_config.compression,
205
223
  loki_url=log_config.loki_url,
206
- loki_labels={"Application": self.name, "Envornment": self.settings.envornment},
224
+ loki_labels={"application": self.name, "envornment": self.settings.envornment},
207
225
  )
208
226
  Extend.register(log_ext)
209
227
 
@@ -220,7 +238,7 @@ class BaseApp:
220
238
  return
221
239
 
222
240
  sentry_sdk.init(
223
- dsn=self.settings.sentry_dsn,
241
+ dsn=str(self.settings.sentry_dsn),
224
242
  # Set traces_sample_rate to 1.0 to capture 100%
225
243
  # of transactions for tracing.
226
244
  traces_sample_rate=1.0,
@@ -37,6 +37,40 @@ class LoggerSettings(BaseModel):
37
37
  loki_url: HttpUrl | None = Field(default=None)
38
38
 
39
39
 
40
+ class JsonRespSettings(BaseModel):
41
+ """
42
+ json 响应配置类
43
+ """
44
+
45
+ # 是否使用 {"code": "0", "msg": "success", "data": {}} 的格式
46
+ use_tml: bool = Field(default=True)
47
+
48
+ error_code: str | int = Field(default="ERROR-1")
49
+
50
+ # code 字段的名字
51
+ code_field_name: str = Field(default="code")
52
+
53
+ # msg 字段的名字
54
+ msg_field_name: str = Field(default="msg")
55
+
56
+ # data 字段的名字
57
+ data_field_name: str = Field(default="data")
58
+
59
+
60
+ class CrosSettings(BaseModel):
61
+ """
62
+ 跨域设置
63
+ """
64
+
65
+ # 地址、地址列表、*
66
+ # 当 credentials等于 'include' 时,origins必须是具体是地址不能是 “*”
67
+ origins: list[str] | None = Field(default_factory=list)
68
+
69
+ # 支持凭证。
70
+ # 当前端启用了withCredentials 后端需要设置这个值为True
71
+ supports_credentials: bool = Field(default=False)
72
+
73
+
40
74
  class DefaultSettings(SettingsBase):
41
75
  """
42
76
  配置类
@@ -62,10 +96,13 @@ class DefaultSettings(SettingsBase):
62
96
  access_log: bool = Field(default=True)
63
97
 
64
98
  # 跨域设置
65
- cors_origins: list[str] | None = Field(default_factory=list)
99
+ cors: CrosSettings = Field(default_factory=CrosSettings)
66
100
 
67
101
  # 哨兵连接dsn,如果存在则会把错误信息推送给哨兵
68
102
  sentry_dsn: HttpUrl | None = Field(default=None)
69
103
 
70
104
  # 日志配置
71
105
  logger: LoggerSettings = Field(default_factory=LoggerSettings)
106
+
107
+ # json 响应配置
108
+ json_resp: JsonRespSettings = Field(default_factory=JsonRespSettings)
@@ -0,0 +1,41 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from sanic import Blueprint
4
+ from sanic_ext.extensions.openapi.blueprint import blueprint_factory
5
+ from sanic_ext.extensions.openapi.builders import SpecificationBuilder
6
+ from sanic_ext.extensions.openapi.extension import OpenAPIExtension as BaseExtension
7
+
8
+ if TYPE_CHECKING:
9
+ from sanic_ext import Extend
10
+
11
+
12
+ class OpenAPIExtension(BaseExtension):
13
+ name = "openapi"
14
+ bp: Blueprint
15
+
16
+ def startup(self, bootstrap: Extend) -> None:
17
+ if self.app.config.OAS:
18
+ self.bp = blueprint_factory(self.app.config)
19
+ self.app.blueprint(self.bp)
20
+ bootstrap._openapi = SpecificationBuilder()
21
+
22
+
23
+ """
24
+ import inspect
25
+ import ast
26
+
27
+ def build_spec(app, loop):
28
+ ...
29
+
30
+ source_code = inspect.getsource(blueprint_factory)
31
+ ast_tree = ast.parse(source_code)
32
+
33
+ my_source_code = inspect.getsource(build_spec)
34
+ my_func_ast = ast.parse(my_source_code)
35
+
36
+ ast_tree.body[0].body[6] = my_func_ast
37
+ modified_code = compile(ast_tree, filename="<ast>", mode="exec")
38
+
39
+
40
+
41
+ """
@@ -1,17 +0,0 @@
1
- from sanic_api.api.request import Request
2
- from sanic_api.api.response import BaseResp, BaseRespTml, TempModel
3
-
4
- """
5
- class BaseResponseModel(BaseModel):
6
- "直接返回str、int、float、bool等类型"
7
- ...
8
-
9
- class JsonResponseModel(BaseModel):
10
- "返回JSON的Pydantic结构"
11
- ...
12
-
13
- class ListResponseModel(BaseModel):
14
- "返回JSON的Pydantic结构的列表"
15
- ...
16
-
17
- """
@@ -1,34 +0,0 @@
1
- from typing import Any, ClassVar
2
-
3
- from pydantic import BaseModel, Field, PrivateAttr
4
- from sanic.compat import Header
5
- from sanic.response import JSONResponse
6
-
7
-
8
- class TempModel(BaseModel):
9
- _data_field: str = PrivateAttr(default="data")
10
- data: Any = Field(default=None, title="数据")
11
- code: str = Field(default_factory=str, title="业务状态码")
12
- msg: str = Field(default_factory=str, title="消息")
13
-
14
- def get_data_field_name(self) -> str:
15
- return self._data_field
16
-
17
-
18
- class BaseResp(BaseModel):
19
- temp_data: ClassVar = Ellipsis
20
-
21
- def resp(self, status: int = 200, headers: Header | dict[str, str] | None = None) -> JSONResponse:
22
- data = self.model_dump(mode="json")
23
- return JSONResponse(data, status=status, headers=headers)
24
-
25
-
26
- class BaseRespTml(BaseModel):
27
- temp_data: TempModel | None = Field(default_factory=TempModel)
28
-
29
- def resp(self, status: int = 200, headers: Header | dict[str, str] | None = None) -> JSONResponse:
30
- tmp_data_field_name = self.temp_data.get_data_field_name()
31
- self_data = self.model_dump(mode="json", exclude={"temp_data"})
32
- setattr(self.temp_data, tmp_data_field_name, self_data)
33
- tml_data = self.temp_data.model_dump(mode="json")
34
- return JSONResponse(tml_data, status=status, headers=headers)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes