sanic-api 0.2.8__py3-none-any.whl → 0.3.0a1__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.
Files changed (44) hide show
  1. sanic_api/__init__.py +3 -21
  2. sanic_api/api/__init__.py +17 -2
  3. sanic_api/api/model.py +0 -45
  4. sanic_api/api/request.py +138 -0
  5. sanic_api/api/response.py +34 -0
  6. sanic_api/app.py +275 -0
  7. sanic_api/config/__init__.py +1 -2
  8. sanic_api/config/setting.py +71 -30
  9. sanic_api/logger/__init__.py +0 -1
  10. sanic_api/logger/config.py +99 -77
  11. sanic_api/logger/extension.py +129 -0
  12. sanic_api/openapi/__init__.py +0 -7
  13. sanic_api/openapi/extension.py +41 -0
  14. {example → sanic_api/utils}/__init__.py +0 -0
  15. sanic_api/{enum.py → utils/enum.py} +58 -88
  16. {sanic_api-0.2.8.dist-info → sanic_api-0.3.0a1.dist-info}/METADATA +30 -10
  17. sanic_api-0.3.0a1.dist-info/RECORD +19 -0
  18. {sanic_api-0.2.8.dist-info → sanic_api-0.3.0a1.dist-info}/WHEEL +1 -1
  19. example/__main__.py +0 -19
  20. example/api/__init__.py +0 -1
  21. example/api/t1/__init__.py +0 -2
  22. example/api/t1/t1_1.py +0 -22
  23. example/api/t1/t1_2.py +0 -9
  24. example/api/t2/__init__.py +0 -1
  25. example/api/t2/t2.py +0 -9
  26. example/api/t2/t3/__init__.py +0 -1
  27. example/api/t2/t3/t3.py +0 -9
  28. example/api/user.py +0 -89
  29. example/api/validator.py +0 -97
  30. example/app.py +0 -62
  31. example/settings.py +0 -10
  32. sanic_api/api/api.py +0 -177
  33. sanic_api/api/exception.py +0 -46
  34. sanic_api/api/extend.py +0 -25
  35. sanic_api/api/handle_exception.py +0 -61
  36. sanic_api/api/validators.py +0 -76
  37. sanic_api/config/sanic.py +0 -19
  38. sanic_api/config/sanic_api.py +0 -11
  39. sanic_api/logger/extend.py +0 -71
  40. sanic_api/logger/sanic_http.py +0 -68
  41. sanic_api/openapi/openapi.py +0 -102
  42. sanic_api/utils.py +0 -112
  43. sanic_api-0.2.8.dist-info/RECORD +0 -38
  44. {sanic_api-0.2.8.dist-info → sanic_api-0.3.0a1.dist-info}/licenses/LICENSE.txt +0 -0
sanic_api/__init__.py CHANGED
@@ -1,21 +1,3 @@
1
- from sanic import Sanic
2
- from sanic_ext import Extend
3
-
4
- from sanic_api import api, openapi
5
-
6
- from .api import ApiExtend
7
- from .logger import LoggerExtend
8
-
9
-
10
- def init_api(app: Sanic):
11
- """
12
- 初始化API
13
- Args:
14
- app: Sanic的APP
15
-
16
- Returns:
17
-
18
- """
19
- Extend.register(LoggerExtend)
20
- Extend.register(ApiExtend)
21
- openapi.init(app)
1
+ from sanic_api.logger.extension import LoggerExtend
2
+
3
+ __version__ = "0.3.0a1"
sanic_api/api/__init__.py CHANGED
@@ -1,2 +1,17 @@
1
- from .api import API
2
- from .extend import ApiExtend
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
+ """
sanic_api/api/model.py CHANGED
@@ -1,45 +0,0 @@
1
- from typing import Optional, Union
2
-
3
- from pydantic.fields import ModelField, ModelPrivateAttr
4
- from pydantic.main import BaseModel
5
-
6
-
7
- class ResponseModel(BaseModel):
8
- """
9
- 响应基础模型
10
- """
11
-
12
- def __new__(cls, *args, **kwargs):
13
- for _field, value in cls.__fields__.items():
14
- if not isinstance(value, ModelField):
15
- continue
16
- value.required = False
17
-
18
- return super().__new__(cls, *args, **kwargs)
19
-
20
-
21
- class ListRespModel(ResponseModel):
22
- """
23
- 列表格式的响应基础模型
24
- """
25
-
26
- _data_list: Optional[Union[ModelPrivateAttr, list]] = ModelPrivateAttr(default_factory=list)
27
-
28
- def add_data(self):
29
- """
30
- 当前模型下数据添加到列表中
31
- Returns:
32
-
33
- """
34
- data = self.dict()
35
- self._data_list.append(data)
36
- for attr in data.keys():
37
- self.__setattr__(attr, None)
38
-
39
- def to_list(self):
40
- """
41
- 返回列表响应数据
42
- Returns:
43
-
44
- """
45
- return self._data_list
@@ -0,0 +1,138 @@
1
+ import inspect
2
+ from collections.abc import Collection
3
+ from typing import get_origin
4
+
5
+ from pydantic import BaseModel
6
+ from sanic import Request as SanicRequest
7
+
8
+
9
+ class Request(SanicRequest):
10
+ """
11
+ 自定义的请求类
12
+ 加入便于接口参数获取校验的方法
13
+ """
14
+
15
+ request_type: type["Request"] | None
16
+ json_data_type: type[BaseModel] | None
17
+ form_data_type: type[BaseModel] | None
18
+ query_data_type: type[BaseModel] | None
19
+
20
+ json_data: BaseModel
21
+ form_data: BaseModel
22
+ query_data: BaseModel
23
+
24
+ def __init__(self, *args, **kwargs):
25
+ super().__init__(*args, **kwargs)
26
+
27
+ async def receive_body(self):
28
+ """
29
+ 接收请求体
30
+ 在这里进行参数校验注入的所有操作
31
+ Returns:
32
+
33
+ """
34
+ await super().receive_body()
35
+ self._get_data_type()
36
+ self._load_data()
37
+
38
+ def _get_type(self, name: str, base_type: type):
39
+ """
40
+ 获取指定属性的类型
41
+ Args:
42
+ name: 属性名字
43
+ base_type: 所属的基本类型
44
+
45
+ Returns:
46
+ 对于参数返回BaseModel或None
47
+ 对于情求类型返回Request或None
48
+ """
49
+
50
+ route_handler_arg_spec = inspect.getfullargspec(self.route.handler)
51
+ arg_type = route_handler_arg_spec.annotations.get(name)
52
+ is_name = name in route_handler_arg_spec.args
53
+ is_type = arg_type and issubclass(arg_type, base_type) and arg_type not in (Request, SanicRequest)
54
+ if is_name and is_type:
55
+ return arg_type
56
+ return None
57
+
58
+ def _get_data_type(self):
59
+ """
60
+ 获取json_data、form_data、query_data、request的类型
61
+ 如果定义了request就从request上面获取上述类型
62
+ Returns:
63
+
64
+ """
65
+
66
+ # 从函数参数注解上面获取类型
67
+ self.json_data_type = self._get_type("json_data", BaseModel)
68
+ self.form_data_type = self._get_type("form_data", BaseModel)
69
+ self.query_data_type = self._get_type("query_data", BaseModel)
70
+ self.request_type = self._get_type("request", Request)
71
+
72
+ # 没有json_data参数 但是有自定义的request类就从request类上面获取json_data类型
73
+ if self.request_type and not self.json_data_type:
74
+ self.json_data_type = self.request_type.__annotations__.get("json_data")
75
+
76
+ # 没有form_data参数 但是有自定义的request类就从request类上面获取form_data类型
77
+ if self.request_type and not self.form_data_type:
78
+ self.form_data_type = self.request_type.__annotations__.get("form_data")
79
+
80
+ # 没有query_data参数 但是有自定义的request类就从request类上面获取query_data类型
81
+ if self.request_type and not self.query_data_type:
82
+ self.query_data_type = self.request_type.__annotations__.get("query_data")
83
+
84
+ # noinspection PyBroadException
85
+ def _load_data(self):
86
+ """
87
+ 加载json_data、form_data、query_data数据
88
+ 同时做参数校验及参数的注入
89
+ Returns:
90
+
91
+ """
92
+ route_handler_arg_spec = inspect.getfullargspec(self.route.handler)
93
+
94
+ def _set_arg(name, value):
95
+ arg_type = route_handler_arg_spec.annotations.get(name)
96
+ if name in route_handler_arg_spec.args and issubclass(arg_type, BaseModel):
97
+ self.match_info.update({name: value})
98
+
99
+ def _proc_param_data(data: dict, data_type: BaseModel | None):
100
+ for k, v in data.items():
101
+ if isinstance(v, list) and len(v) == 1:
102
+ field = data_type.model_fields.get(k)
103
+ if not field:
104
+ continue
105
+ arg_type = field.annotation
106
+ arg_type = get_origin(arg_type) or arg_type
107
+ is_list = issubclass(arg_type, Collection) and arg_type is not str
108
+ if not is_list:
109
+ data[k] = v[0]
110
+ return data
111
+
112
+ try:
113
+ json_data = self.json
114
+ except Exception:
115
+ json_data = None
116
+ if json_data and self.json_data_type:
117
+ self.json_data = self.json_data_type(**json_data)
118
+ _set_arg("json_data", self.json_data)
119
+
120
+ # 由于form和query的参数的key是可以重复的,所以默认类型是类似dict[str, list]的
121
+ # 这里做了个处理,如果key的数量是1个则直接转为dict[str, dict]
122
+ try:
123
+ form_data = self.form
124
+ except Exception:
125
+ form_data = None
126
+ if form_data and self.form_data_type:
127
+ form_data = _proc_param_data(form_data, self.form_data_type)
128
+ self.form_data = self.form_data_type(**form_data)
129
+ _set_arg("form_data", self.form_data)
130
+
131
+ try:
132
+ query_data = self.args
133
+ except Exception:
134
+ query_data = None
135
+ if query_data and self.query_data_type:
136
+ query_data = _proc_param_data(query_data, self.query_data_type)
137
+ self.query_data = self.query_data_type(**query_data)
138
+ _set_arg("query_data", self.query_data)
@@ -0,0 +1,34 @@
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)
sanic_api/app.py ADDED
@@ -0,0 +1,275 @@
1
+ import sentry_sdk
2
+ from sanic import Sanic, text
3
+ from sanic.log import logger
4
+ from sanic.worker.loader import AppLoader
5
+ from sanic_ext import Extend
6
+ from sentry_sdk.integrations.asyncio import AsyncioIntegration
7
+
8
+ from sanic_api import LoggerExtend
9
+ from sanic_api.api import Request
10
+ from sanic_api.config import DefaultSettings, RunModeEnum
11
+
12
+
13
+ class BaseApp:
14
+ name: str = "sanic-server"
15
+ settings: DefaultSettings
16
+
17
+ def __init__(self, settings: DefaultSettings):
18
+ self.settings = settings
19
+
20
+ @classmethod
21
+ def run(cls, settings: DefaultSettings = None):
22
+ """
23
+ 运行服务
24
+ Args:
25
+ settings: 设置,为空就使用默认设置
26
+
27
+ Returns:
28
+
29
+ """
30
+ if not cls.name:
31
+ raise ValueError("请设置服务名称!")
32
+
33
+ settings = settings or DefaultSettings()
34
+ self = cls(settings)
35
+ loader = AppLoader(factory=self._create_app)
36
+ app = loader.load()
37
+
38
+ # 服务配置
39
+ # 开发模式下workers指定为1,自动重载根据配置决定,默认关闭,跨域设置为允许所有跨域
40
+ # 生产模式下使用fast模型,自动指定最多的workers,自定重载强制关闭,跨域使用配置中的跨域列表
41
+ # 默认启用sanic_ext里面的后台日志记录器
42
+ motd_display = {"envornment": settings.envornment}
43
+ config = {"access_log": settings.access_log, "motd_display": motd_display}
44
+ if settings.mode == RunModeEnum.DEBNUG:
45
+ cors_origins = "*"
46
+ config.update({"auto_reload": settings.auto_reload, "workers": 1, "debug": True})
47
+ else:
48
+ cors_origins = ",".join(settings.cors_origins)
49
+ config.update({"fast": True, "auto_reload": False})
50
+
51
+ app.config.LOGGING = True
52
+ app.config.CORS_ORIGINS = cors_origins
53
+ app.prepare(settings.host, settings.port, **config)
54
+ Sanic.serve(primary=app, app_loader=loader)
55
+
56
+ def _create_app(self):
57
+ """
58
+ 创建app的内部工厂方法
59
+ Returns:
60
+
61
+ """
62
+ app = Sanic(self.name, configure_logging=False, request_class=Request)
63
+ self._setup_logger(app)
64
+ app.main_process_stop(self._main_process_stop)
65
+ app.main_process_start(self._main_process_start)
66
+ app.before_server_start(self._before_server_start)
67
+ app.before_server_stop(self._before_server_stop)
68
+ app.after_server_start(self._after_server_start)
69
+ app.after_server_stop(self._after_server_stop)
70
+ return app
71
+
72
+ async def _main_process_start(self, app: Sanic):
73
+ """
74
+ 主进程启动的内部方法
75
+ Args:
76
+ app:
77
+
78
+ Returns:
79
+
80
+ """
81
+ logger.info("主进程启动")
82
+ await self.main_process_start(app)
83
+
84
+ async def _main_process_stop(self, app: Sanic):
85
+ """
86
+ 主进程停止的内部方法
87
+ Args:
88
+ app:
89
+
90
+ Returns:
91
+
92
+ """
93
+ logger.info("主进程停止")
94
+ await self.main_process_stop(app)
95
+
96
+ async def _before_server_start(self, app: Sanic):
97
+ """
98
+ 工作进程启动之前的内部方法。
99
+ 设置路由写在这里是因为sanic_ext是会在before_server_start阶段对路由设置_cors属性
100
+ Args:
101
+ app:
102
+
103
+ Returns:
104
+
105
+ """
106
+ logger.info(f"工作进程 {app.m.pid} 即将启动")
107
+
108
+ await self._setup_route(app)
109
+ await self.before_server_start(app)
110
+
111
+ async def _before_server_stop(self, app: Sanic):
112
+ """
113
+ 服务停止之前的内部方法
114
+ Args:
115
+ app:
116
+
117
+ Returns:
118
+
119
+ """
120
+ logger.info(f"工作进程 {app.m.pid} 即将停止")
121
+ await self.before_server_stop(app)
122
+
123
+ async def _after_server_start(self, app: Sanic):
124
+ """
125
+ 服务启动之后的内部方法
126
+ Args:
127
+ app:
128
+
129
+ Returns:
130
+
131
+ """
132
+ logger.info(f"工作进程 {app.m.pid} 启动完毕")
133
+ await self.after_server_start(app)
134
+
135
+ async def _after_server_stop(self, app: Sanic):
136
+ """
137
+ 服务停止之后的内部方法
138
+ Args:
139
+ app:
140
+
141
+ Returns:
142
+
143
+ """
144
+ logger.info(f"工作进程 {app.m.pid} 停止")
145
+ await self.after_server_stop(app)
146
+
147
+ async def _setup_route(self, app: Sanic):
148
+ """
149
+ 设置路由和蓝图的内部方法,自动设置一个ping的路由
150
+ Args:
151
+ app:
152
+
153
+ Returns:
154
+
155
+ """
156
+ app.add_route(self._ping, "ping", methods=["GET", "POST"])
157
+ await self.setup_route(app)
158
+
159
+ def _setup_logger(self, app: Sanic):
160
+ """
161
+ 设置日志
162
+ Args:
163
+ app:
164
+
165
+ Returns:
166
+
167
+ """
168
+ log_config = self.settings.logger
169
+ log_ext = LoggerExtend(
170
+ app,
171
+ log_file=log_config.file,
172
+ rotation=log_config.rotation,
173
+ retention=log_config.retention,
174
+ compression=log_config.compression,
175
+ loki_url=log_config.loki_url,
176
+ loki_labels={"Application": self.name, "Envornment": self.settings.envornment},
177
+ )
178
+ Extend.register(log_ext)
179
+
180
+ def _setup_sentry(self, _app: Sanic):
181
+ """
182
+ 如果存在sentryURL配置,则自动设置sentry哨兵
183
+ Args:
184
+ _app:
185
+
186
+ Returns:
187
+
188
+ """
189
+ if self.settings.sentry_dsn is None:
190
+ return
191
+
192
+ sentry_sdk.init(
193
+ dsn=self.settings.sentry_dsn,
194
+ # Set traces_sample_rate to 1.0 to capture 100%
195
+ # of transactions for tracing.
196
+ traces_sample_rate=1.0,
197
+ # Set profiles_sample_rate to 1.0 to profile 100%
198
+ # of sampled transactions.
199
+ # We recommend adjusting this value in production.
200
+ profiles_sample_rate=1.0,
201
+ integrations=[AsyncioIntegration()],
202
+ )
203
+
204
+ async def _ping(self, _request):
205
+ return text("ok")
206
+
207
+ async def main_process_start(self, app: Sanic):
208
+ """
209
+ 主进程启动的方法
210
+ Args:
211
+ app:
212
+
213
+ Returns:
214
+
215
+ """
216
+
217
+ async def main_process_stop(self, app: Sanic):
218
+ """
219
+ 主进程停止的方法
220
+ Args:
221
+ app:
222
+
223
+ Returns:
224
+
225
+ """
226
+
227
+ async def before_server_start(self, app: Sanic):
228
+ """
229
+ 工作进程启动之前的方法。
230
+ Args:
231
+ app:
232
+
233
+ Returns:
234
+
235
+ """
236
+
237
+ async def before_server_stop(self, app: Sanic):
238
+ """
239
+ 工作进程停止之前的方法。
240
+ Args:
241
+ app:
242
+
243
+ Returns:
244
+
245
+ """
246
+
247
+ async def after_server_start(self, app: Sanic):
248
+ """
249
+ 工作进程启动之后的方法。
250
+ Args:
251
+ app:
252
+
253
+ Returns:
254
+
255
+ """
256
+
257
+ async def after_server_stop(self, app: Sanic):
258
+ """
259
+ 工作进程停止之后的方法。
260
+ Args:
261
+ app:
262
+
263
+ Returns:
264
+
265
+ """
266
+
267
+ async def setup_route(self, app: Sanic):
268
+ """
269
+ 继承此方法去设置蓝图及路由
270
+ Args:
271
+ app:
272
+
273
+ Returns:
274
+
275
+ """
@@ -1,2 +1 @@
1
- from .base import SettingsBase
2
- from .setting import DefaultSettings
1
+ from sanic_api.config.setting import DefaultSettings, RunModeEnum, SettingsBase
@@ -1,30 +1,71 @@
1
- from hs_config import ConfigBase
2
- from pydantic import Field
3
-
4
- from sanic_api.config.sanic import SanicConfig
5
- from sanic_api.config.sanic_api import SanicApiConfig
6
- from sanic_api.enum import RunModeEnum
7
-
8
-
9
- class DefaultSettings(ConfigBase):
10
- """
11
- 配置类
12
- """
13
-
14
- # 主机
15
- host: str = Field(default="127.0.0.1")
16
-
17
- # 端口
18
- port: int = Field(default=5798)
19
-
20
- # 运行模式
21
- mode: RunModeEnum = Field(default=RunModeEnum.DEV)
22
-
23
- # 工作进程的数量
24
- workers: int = Field(default=1)
25
-
26
- # sanic 自身的配置
27
- sanic: SanicConfig = Field(default_factory=SanicConfig)
28
-
29
- # sanic_api 扩展自身需要的配置
30
- sanic_api: SanicApiConfig = Field(default_factory=SanicApiConfig)
1
+ from hs_config import SettingsBase
2
+ from pydantic import BaseModel, Field, FilePath, HttpUrl, NewPath
3
+
4
+ from sanic_api.utils.enum import EnumBase, EnumField
5
+
6
+
7
+ class RunModeEnum(EnumBase):
8
+ """
9
+ 运行模式
10
+ """
11
+
12
+ DEBNUG = EnumField("debug", desc="开发模式")
13
+ PRODUCTION = EnumField("prod", desc="生产模式")
14
+
15
+
16
+ class LoggerSettings(BaseModel):
17
+ """
18
+ 日志配置类
19
+ """
20
+
21
+ # 日志文件路径
22
+ file: FilePath | NewPath | None = Field(default=None)
23
+
24
+ # 自动轮转条件。就是保留几天的日志。
25
+ # 具体查看loguru文档:https://loguru.readthedocs.io/en/stable/api/logger.html#file
26
+ rotation: str | None = Field(default=None)
27
+
28
+ # 日志文件保留条件。就是保留多大的日志。
29
+ # 具体查看loguru文档:https://loguru.readthedocs.io/en/stable/api/logger.html#file
30
+ retention: str | None = Field(default=None)
31
+
32
+ # 日志文件的压缩格式。zip、gz、tar等。
33
+ # 体查看loguru文档:https://loguru.readthedocs.io/en/stable/api/logger.html#file
34
+ compression: str | None = Field(default=None)
35
+
36
+ # loji的地址。如果存在,则会把日志推送给logki
37
+ loki_url: HttpUrl | None = Field(default=None)
38
+
39
+
40
+ class DefaultSettings(SettingsBase):
41
+ """
42
+ 配置类
43
+ """
44
+
45
+ # 主机
46
+ host: str = Field(default="127.0.0.1")
47
+
48
+ # 端口
49
+ port: int = Field(default=6969)
50
+
51
+ # 运行模式
52
+ mode: RunModeEnum = Field(default=RunModeEnum.DEBNUG)
53
+
54
+ # 运行环境,仅作为环境标识。
55
+ # 尽量不要使用这个字段去做逻辑判断。请使用mode去进行判断,因为测试环境、预发布环境、生产环境都应属于生产模式模式
56
+ envornment: str = Field(default="dev")
57
+
58
+ # 自动重载。生产模式强制关闭
59
+ auto_reload: bool = Field(default=False)
60
+
61
+ # 访问日志开关
62
+ access_log: bool = Field(default=True)
63
+
64
+ # 跨域设置
65
+ cors_origins: list[str] | None = Field(default_factory=list)
66
+
67
+ # 哨兵连接dsn,如果存在则会把错误信息推送给哨兵
68
+ sentry_dsn: HttpUrl | None = Field(default=None)
69
+
70
+ # 日志配置
71
+ logger: LoggerSettings = Field(default_factory=LoggerSettings)
@@ -1 +0,0 @@
1
- from .extend import LoggerExtend