CheeseAPI 2.0.4b1__tar.gz → 2.0.5b3__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.
@@ -1,9 +1,10 @@
1
- import os, pathlib, multiprocessing, ssl, socket, asyncio, concurrent.futures, inspect
1
+ import os, pathlib, multiprocessing, ssl, socket, asyncio, concurrent.futures, inspect, importlib.util
2
2
  from typing import Type, Literal, Callable, AsyncIterable, TYPE_CHECKING
3
3
 
4
- import redis, signal
4
+ import signal, redis
5
5
  from CheeseLog import CheeseLogger, Message
6
6
 
7
+ from CheeseAPI import static
7
8
  from CheeseAPI.printer import Printer
8
9
  from CheeseAPI.signal import Signal
9
10
  from CheeseAPI.request import RequestProxy
@@ -31,6 +32,8 @@ class AppProxy:
31
32
  def start(self):
32
33
  signal.signal(signal.SIGTERM, lambda signum, frame: self.app.stop())
33
34
 
35
+ self._process_init()
36
+
34
37
  waiting_list = []
35
38
 
36
39
  try:
@@ -98,7 +101,31 @@ class AppProxy:
98
101
  'modules': module
99
102
  })
100
103
 
101
- for path in pathlib.Path(module.replace('.', '/')).glob('*.py'):
104
+ try:
105
+ spec = importlib.util.find_spec(module)
106
+ except (ImportError, ValueError, AttributeError):
107
+ continue
108
+
109
+ if spec is None:
110
+ continue
111
+
112
+ p = None
113
+ if spec.origin is None and spec.submodule_search_locations:
114
+ for location in spec.submodule_search_locations:
115
+ p = pathlib.Path(location)
116
+ if p.exists():
117
+ break
118
+ if not p:
119
+ continue
120
+
121
+ if spec.origin is None:
122
+ continue
123
+
124
+ origin = pathlib.Path(spec.origin)
125
+ if origin.name == '__init__.py':
126
+ origin = origin.parent
127
+
128
+ for path in origin.glob('*.py'):
102
129
  __import__(f'{module}.{path.stem}')
103
130
 
104
131
  self.after_load_module(i, module)
@@ -240,14 +267,6 @@ class AppProxy:
240
267
  loop = asyncio.get_event_loop()
241
268
  loop.set_default_executor(concurrent.futures.ThreadPoolExecutor())
242
269
 
243
- if self.app.sync_server_url:
244
- if WebsocketProxy.sync_servers is None:
245
- WebsocketProxy.sync_servers = (redis.Redis.from_url(self.app.sync_server_url), redis.asyncio.Redis.from_url(self.app.sync_server_url))
246
- if WebsocketProxy.data_encode is None:
247
- WebsocketProxy.data_encode = self.app.sync_server_data_encode
248
- if WebsocketProxy.data_decode is None:
249
- WebsocketProxy.data_decode = self.app.sync_server_data_decode
250
-
251
270
  await self.after_worker_start(is_first)
252
271
  await self.app.signal.after_worker_start.async_send(kwargs = {
253
272
  'is_first': is_first
@@ -443,10 +462,21 @@ class AppProxy:
443
462
  async def after_response(self, response: Response):
444
463
  ...
445
464
 
465
+ def _process_init(self, app: 'CheeseAPI' = None):
466
+ if not app:
467
+ app = self.app
468
+
469
+ static.websocket_data_decode = app.sync_server_data_decode
470
+ static.websocket_data_encode = app.sync_server_data_encode
471
+ if app.sync_server_url:
472
+ static.websocket_sync_servers = (redis.ConnectionPool.from_url(app.sync_server_url), redis.asyncio.ConnectionPool.from_url(app.sync_server_url))
473
+ static.websocket_sync_server = {}
474
+ app.scheduler._proxy.init(app)
475
+
446
476
  class CheeseAPI:
447
- __slots__ = ('_host', '_port', '_ipv6', '_logger_path', '_dual_stack', '_socket_backlog', '_socket_send_buffer_size', '_socket_receive_buffer_size', '_workers', '_ssl_cert', '_ssl_key', '_sync_server_url', '_static_path', '_printer', '_compress', '_compress_min_length', '_compress_level', '_manual_modules', '_exclude_modules', '_priority_modules', '_sync_server_data_encode', '_sync_server_data_decode', '_logger_messages', '_logger', '_is_running', '_request_timeout', '_keep_alive', '_keep_alive_timeout', '_keep_alive_max_requests', '_AppProxy_Class', '_RequestProxy_Class', '_proxy', '_signal', '_ResponseProxy_Class', '_RouteProxy_Class', '_route', '_WebsocketProxy_Class', '_cors', '_SchedulerProxy_Class', '_scheduler')
477
+ __slots__ = ('_host', '_port', '_ipv6', '_logger_path', '_dual_stack', '_socket_backlog', '_socket_send_buffer_size', '_socket_receive_buffer_size', '_workers', '_ssl_cert', '_ssl_key', '_sync_server_url', '_static_path', '_printer', '_compress', '_compress_min_length', '_compress_level', '_manual_modules', '_exclude_modules', '_priority_modules', '_sync_server_data_encode', '_sync_server_data_decode', '_logger_messages', '_logger', '_is_running', '_request_timeout', '_keep_alive', '_keep_alive_timeout', '_keep_alive_max_requests', '_AppProxy_Class', '_RequestProxy_Class', '_proxy', '_signal', '_ResponseProxy_Class', '_RouteProxy_Class', '_route', '_WebsocketProxy_Class', '_cors', '_SchedulerProxy_Class', '_scheduler', '_sync_server_timeout')
448
478
 
449
- def __init__(self, host: str | None = None, port: int = 5214, *, ipv6: bool = False, logger_path: str | None = None, dual_stack: bool = False, socket_backlog: int | None = None, socket_send_buffer_size: int | None = None, socket_receive_buffer_size: int | None = None, workers: int = 1, ssl_cert: str | None = None, ssl_key: str | None = None, sync_server_url: str | None = None, static_path: dict[str, str] = {}, printer: Type[Printer] = Printer, compress: list[Literal['gzip', 'br', 'zstd', 'deflate']] = ['gzip', 'br', 'zstd', 'deflate'], compress_min_length: int = 1024, compress_level: int = 6, manual_modules: list[str] = [], exclude_modules: list[str] = [], priority_modules: list[str] = [], sync_server_data_encode: Callable[[bytes], bytes] | None = None, sync_server_data_decode: Callable[[bytes], bytes] | None = None, logger_messages: dict[str, 'Message'] = {}, request_timeout: float | None = None, keep_alive: bool = True, keep_alive_timeout: float = 5, keep_alive_max_requests: int = 100, AppProxy_Class: Type[AppProxy] = AppProxy, RequestProxy_Class: Type[RequestProxy] = RequestProxy, ResponseProxy_Class: Type[ResponseProxy] = ResponseProxy, RouteProxy_Class: Type[RouteProxy] = RouteProxy, cors_allow_origins: list[str] = ['*'], cors_allow_methods: Literal['GET', 'PUT', 'POST', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT'] = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH', 'CONNECT'], cors_allow_headers: list[str] = ['*'], cors_allow_credentials: bool = True, cors_expose_headers: list[str] = [], cors_max_age: int | None = None, WebsocketProxy_Class: Type[WebsocketProxy] = WebsocketProxy, route_patterns: list['Pattern'] = [], SchedulerProxy_Class: Type[SchedulerProxy] = SchedulerProxy):
479
+ def __init__(self, host: str | None = None, port: int = 5214, *, ipv6: bool = False, logger_path: str | None = None, dual_stack: bool = False, socket_backlog: int | None = None, socket_send_buffer_size: int | None = None, socket_receive_buffer_size: int | None = None, workers: int = 1, ssl_cert: str | None = None, ssl_key: str | None = None, sync_server_url: str | None = None, static_path: dict[str, str] = {}, printer: Type[Printer] = Printer, compress: list[Literal['gzip', 'br', 'zstd', 'deflate']] = ['gzip', 'br', 'zstd', 'deflate'], compress_min_length: int = 1024, compress_level: int = 6, manual_modules: list[str] = [], exclude_modules: list[str] = [], priority_modules: list[str] = [], sync_server_data_encode: Callable[[bytes], bytes] | None = None, sync_server_data_decode: Callable[[bytes], bytes] | None = None, logger_messages: dict[str, 'Message'] = {}, request_timeout: float | None = None, keep_alive: bool = True, keep_alive_timeout: float = 5, keep_alive_max_requests: int = 100, AppProxy_Class: Type[AppProxy] = AppProxy, RequestProxy_Class: Type[RequestProxy] = RequestProxy, ResponseProxy_Class: Type[ResponseProxy] = ResponseProxy, RouteProxy_Class: Type[RouteProxy] = RouteProxy, cors_allow_origins: list[str] = ['*'], cors_allow_methods: Literal['GET', 'PUT', 'POST', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT'] = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH', 'CONNECT'], cors_allow_headers: list[str] = ['*'], cors_allow_credentials: bool = True, cors_expose_headers: list[str] = [], cors_max_age: int | None = None, WebsocketProxy_Class: Type[WebsocketProxy] = WebsocketProxy, route_patterns: list['Pattern'] = [], SchedulerProxy_Class: Type[SchedulerProxy] = SchedulerProxy, sync_server_timeout: float = 5):
450
480
  '''
451
481
  - Args
452
482
  - logger_path: 日志文件路径,支持日期格式化
@@ -478,6 +508,7 @@ class CheeseAPI:
478
508
  - WebsocketProxy_Class: 若想要对 Websocket 处理逻辑进行处理,可传入自定义的 WebsocketProxy 类
479
509
  - route_patterns: 自定义路由校验规则
480
510
  - SchedulerProxy_Class: 自定义任务调度器代理类
511
+ - sync_server_timeout: 同步服务器操作超时时间
481
512
  '''
482
513
 
483
514
  self._host: str = host if host is not None else ('::' if ipv6 else '0.0.0.0')
@@ -513,6 +544,7 @@ class CheeseAPI:
513
544
  self._RouteProxy_Class: Type[RouteProxy] = RouteProxy_Class
514
545
  self._WebsocketProxy_Class: Type[WebsocketProxy] = WebsocketProxy_Class
515
546
  self._SchedulerProxy_Class: Type[SchedulerProxy] = SchedulerProxy_Class
547
+ self._sync_server_timeout: float = sync_server_timeout
516
548
 
517
549
  self._logger: CheeseLogger = CheeseLogger(self.logger_path, messages = {
518
550
  'START': Message('START', 20, message_template_styled = '(<green>%k</green>) <black>%t</black> > %c'),
@@ -533,16 +565,10 @@ class CheeseAPI:
533
565
  self.route.patterns.extend(route_patterns)
534
566
  self.route.patterns.sort(key = lambda x: x['weight'], reverse = True)
535
567
 
536
- def __setstate__(self, state: tuple[None, dict[str, any]]):
537
- for key, value in state[1].items():
568
+ def __setstate__(self, data: tuple[None, dict[str, any]]):
569
+ for key, value in data[1].items():
538
570
  setattr(self, key, value)
539
-
540
- if self.sync_server_url and WebsocketProxy.sync_servers is None:
541
- WebsocketProxy.sync_servers = (redis.Redis.from_url(self.sync_server_url), redis.asyncio.Redis.from_url(self.sync_server_url))
542
- if WebsocketProxy.data_encode is None:
543
- WebsocketProxy.data_encode = self.sync_server_data_encode
544
- if WebsocketProxy.data_decode is None:
545
- WebsocketProxy.data_decode = self.sync_server_data_decode
571
+ self._proxy._process_init(self)
546
572
 
547
573
  def start(self):
548
574
  self._proxy.start()
@@ -829,3 +855,11 @@ class CheeseAPI:
829
855
  '''
830
856
 
831
857
  return self._SchedulerProxy_Class
858
+
859
+ @property
860
+ def sync_server_timeout(self) -> float:
861
+ '''
862
+ 同步服务器操作超时时间
863
+ '''
864
+
865
+ return self._sync_server_timeout
@@ -5,7 +5,7 @@ from CheeseAPI.response import Response
5
5
  if TYPE_CHECKING:
6
6
  from CheeseAPI.request import Request
7
7
 
8
- HTTP_METHOD_TYPE = Literal['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH']
8
+ HTTP_METHOD_TYPE = Literal['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH', 'CONNECT']
9
9
 
10
10
  class CORS:
11
11
  __slots__ = ('allow_origins', 'allow_methods', 'allow_headers', 'allow_credentials', 'expose_headers', 'max_age')
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
11
11
  HTTP_METHOD_TYPE = Literal['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'WEBSOCKET']
12
12
 
13
13
  class Request:
14
- __slots__ = ('_proxy', '_ip', '_method', '_path', '_params', '_headers', '_query', '_body', '_json', '_form', '_files', '_cookies', '_full_path', '_ranges', '_fn')
14
+ __slots__ = ('_proxy', '_ip', '_method', '_path', '_params', '_headers', '_query', '_body', '_json', '_form', '_files', '_cookies', '_full_path', '_ranges', '_fn', '_file')
15
15
 
16
16
  def __init__(self, app: 'CheeseAPI', client_socket: socket.socket, addr: tuple[str, int]):
17
17
  self._proxy: RequestProxy = app.RequestProxy_Class(app, self, client_socket)
@@ -26,6 +26,7 @@ class Request:
26
26
  self._json: dict | list | None = None
27
27
  self._form: dict[str, str] | None = None
28
28
  self._files: dict[str, File] | None = None
29
+ self._file: File | None = None
29
30
  self._cookies: dict[str, str] | None = None
30
31
  self._full_path: str | None = None
31
32
  self._ranges: list[tuple[int, int | None]] | None = None
@@ -93,6 +94,10 @@ class Request:
93
94
  def files(self) -> dict[str, bytes] | None:
94
95
  return self._files
95
96
 
97
+ @property
98
+ def file(self) -> File | None:
99
+ return self._file
100
+
96
101
  @property
97
102
  def cookies(self) -> dict[str, str] | None:
98
103
  return self._cookies
@@ -183,6 +188,7 @@ class RequestProxy:
183
188
  data = None
184
189
  try:
185
190
  data = await asyncio.wait_for(loop.sock_recv(self.client_socket, self.app.socket_receive_buffer_size), self.app.request_timeout)
191
+ self.request._body += data
186
192
  except asyncio.TimeoutError:
187
193
  raise
188
194
  if not data:
@@ -75,6 +75,10 @@ HTTP_STATUS = {
75
75
  }
76
76
  NO_BODY_STATUS = (100, 101, 102, 204, 304)
77
77
  PREVIEWABLE_TYPES = ('text/plain', 'text/html', 'text/css', 'text/javascript', 'application/json', 'application/xml', 'text/xml', 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp', 'image/bmp', 'video/mp4', 'video/webm', 'video/ogg', 'audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/webm', 'application/pdf')
78
+ MERGE_TYPES = {
79
+ 'audio/x-wav': 'audio/wav',
80
+ 'audio/vnd.wave': 'audio/wav'
81
+ }
78
82
 
79
83
  class Cookie(TypedDict):
80
84
  value: str
@@ -87,7 +91,7 @@ class Cookie(TypedDict):
87
91
  class Response:
88
92
  __slots__ = ('status', '_proxy', 'body', 'headers', 'cookies', 'high_precision_date', 'compress', 'compress_level')
89
93
 
90
- def __init__(self, body: dict | list | str | bytes | AsyncIterable | None = None, status: int = 200, headers: dict[str, str] = {}, *, high_precision_date: bool = False, compress: Literal['gzip', 'deflate', 'br', 'zstd'] | None = None, compress_level: int | None = None):
94
+ def __init__(self, body: dict | list | str | bytes | AsyncIterable | None = None, status: int = 200, headers: dict[str, str] | None = None, *, high_precision_date: bool = False, compress: Literal['gzip', 'deflate', 'br', 'zstd'] | None = None, compress_level: int | None = None):
91
95
  '''
92
96
  - Args
93
97
  - body: 当为 `AsyncIterable` 时,自动使用 chunked 传输编码
@@ -99,7 +103,7 @@ class Response:
99
103
 
100
104
  self.status: int = status
101
105
  self.body: dict | list | str | bytes | AsyncIterable | None = body
102
- self.headers: dict[str, str] = headers
106
+ self.headers: dict[str, str] = headers if headers is not None else {}
103
107
  self.cookies: dict[str, Cookie] = {}
104
108
  self.high_precision_date: bool = high_precision_date
105
109
  self.compress: Literal['gzip', 'deflate', 'br', 'zstd'] | None = compress
@@ -118,7 +122,9 @@ class Response:
118
122
  }
119
123
 
120
124
  class RedirectResponse(Response):
121
- def __init__(self, location: str, status: Literal[301, 302, 303, 307, 308] = 302, headers: dict[str, str] = {}, body: bytes | str | list | dict | None = None):
125
+ def __init__(self, location: str, status: Literal[301, 302, 303, 307, 308] = 302, headers: dict[str, str] | None = None, body: bytes | str | list | dict | None = None):
126
+ if headers is None:
127
+ headers = {}
122
128
  headers['Location'] = location
123
129
 
124
130
  super().__init__(status, body, headers)
@@ -126,7 +132,7 @@ class RedirectResponse(Response):
126
132
  class FileResponse(Response):
127
133
  __slots__ = ('file', 'preview', 'transmission_type', 'chunked_size')
128
134
 
129
- def __init__(self, file_path_or_file: str | File, *, status: int = 200, headers: dict[str, str] = {}, preview: bool = True, transmission_type: Literal['CONTENT_LENGTH', 'CHUNKED'] = 'CONTENT_LENGTH', chunked_size: int | None = None, compress: Literal['gzip', 'deflate', 'br', 'zstd'] | None = None, compress_level: int | None = None):
135
+ def __init__(self, file_path_or_file: str | File, *, status: int = 200, headers: dict[str, str] | None = None , preview: bool = True, transmission_type: Literal['CONTENT_LENGTH', 'CHUNKED'] = 'CONTENT_LENGTH', chunked_size: int | None = None, compress: Literal['gzip', 'deflate', 'br', 'zstd'] | None = None, compress_level: int | None = None):
130
136
  '''
131
137
  - Args
132
138
  - preview: 优先预览文件
@@ -225,6 +231,8 @@ class ResponseProxy:
225
231
 
226
232
  if 'Content-Type' not in headers and 'Content-Disposition' not in headers:
227
233
  mime_type = mimetypes.guess_type(self.response.file.name)[0] or 'application/octet-stream'
234
+ if mime_type in MERGE_TYPES:
235
+ mime_type = MERGE_TYPES[mime_type]
228
236
  headers['Content-Type'] = f'{mime_type}; charset=utf-8'
229
237
  headers['Content-Disposition'] = f'{"inline" if self.response.preview and mime_type in PREVIEWABLE_TYPES else "attachment"}; filename="{self.response.file.name}"'
230
238
 
@@ -398,4 +406,4 @@ class ResponseProxy:
398
406
  if 'Content-Encoding' in headers:
399
407
  del headers['Content-Encoding']
400
408
 
401
- return status, headers, body
409
+ return status, headers, body
@@ -1,6 +1,8 @@
1
1
  import re, uuid
2
2
  from typing import Literal, TypedDict, Callable, TYPE_CHECKING, AsyncIterable, Union
3
3
 
4
+ from urllib.parse import unquote
5
+
4
6
  from CheeseAPI.cors import CORS
5
7
 
6
8
  if TYPE_CHECKING:
@@ -252,7 +254,7 @@ class RouteProxy:
252
254
  i = 1
253
255
  for key, type in self.app.route._proxy.dynamic_routes[_path][method]['params'].items():
254
256
  value = match.group(i)
255
- _params[key] = type(value)
257
+ _params[key] = type(unquote(value))
256
258
  i += 1
257
259
  return self.app.route._proxy.dynamic_routes[_path][method], _params
258
260
  else: