CheeseAPI 2.0.8b4__tar.gz → 2.0.8b6__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.
@@ -361,7 +361,7 @@ class AppProxy:
361
361
  'request': request
362
362
  })
363
363
 
364
- if request._proxy.protocol is None or not self.app.keep_alive or (request._proxy.protocol == 'HTTP/1.0' and request.headers.get('Connection') != 'keep-alive') or (request._proxy.protocol == 'HTTP/1.1' and request.headers.get('Connection') == 'close'):
364
+ if request._proxy.protocol is None or not self.app.keep_alive or (request._proxy.protocol == 'HTTP/1.0' and request.headers.get('connection') != 'keep-alive') or (request._proxy.protocol == 'HTTP/1.1' and request.headers.get('connection') == 'close'):
365
365
  break
366
366
 
367
367
  client_socket, addr = await self.before_request(client_socket, addr)
@@ -19,31 +19,31 @@ class CORS:
19
19
  self.max_age: int | None = max_age
20
20
 
21
21
  def get_response(self, request: 'Request'):
22
- origin = request.headers.get('Origin', '')
22
+ origin = request.headers.get('origin', '')
23
23
  if '*' not in self.allow_origins and origin not in self.allow_origins:
24
24
  return Response(status = 403)
25
25
 
26
26
  headers = {}
27
27
 
28
28
  if origin in self.allow_origins:
29
- headers['Access-Control-Allow-Origin'] = origin
29
+ headers['access-control-allow-origin'] = origin
30
30
  elif '*' in self.allow_origins and not self.allow_credentials:
31
- headers['Access-Control-Allow-Origin'] = '*'
31
+ headers['access-control-allow-origin'] = '*'
32
32
 
33
- headers['Access-Control-Allow-Methods'] = ', '.join(self.allow_methods)
33
+ headers['access-control-allow-methods'] = ', '.join(self.allow_methods)
34
34
 
35
35
  if '*' in self.allow_headers:
36
- headers['Access-Control-Allow-Headers'] = request.headers.get('Access-Control-Request-Headers') or '*'
36
+ headers['access-control-allow-headers'] = request.headers.get('access-control-request-headers') or '*'
37
37
  else:
38
- headers['Access-Control-Allow-Headers'] = ', '.join(self.allow_headers)
38
+ headers['access-control-allow-headers'] = ', '.join(self.allow_headers)
39
39
 
40
40
  if self.allow_credentials:
41
- headers['Access-Control-Allow-Credentials'] = 'true'
41
+ headers['access-control-allow-credentials'] = 'true'
42
42
 
43
43
  if self.expose_headers:
44
- headers['Access-Control-Expose-Headers'] = ', '.join(self.expose_headers)
44
+ headers['access-control-expose-headers'] = ', '.join(self.expose_headers)
45
45
 
46
46
  if self.max_age is not None:
47
- headers['Access-Control-Max-Age'] = str(self.max_age)
47
+ headers['access-control-max-age'] = str(self.max_age)
48
48
 
49
49
  return Response(status = 204, headers = headers)
@@ -150,34 +150,34 @@ class RequestProxy:
150
150
  self.request._headers = {}
151
151
  for line in lines[1:]:
152
152
  key, value = line.split(': ', 1)
153
- self.request.headers[key] = value
153
+ self.request.headers[key.lower()] = value
154
154
 
155
- if 'Cookie' in self.request.headers:
155
+ if 'cookie' in self.request.headers:
156
156
  self.request._cookies = {}
157
- for cookie in self.request.headers['Cookie'].split(';'):
157
+ for cookie in self.request.headers['cookie'].split(';'):
158
158
  key, value = cookie.strip().split('=', 1)
159
159
  self.request.cookies[key] = value
160
160
 
161
- if 'X-Real-IP' in self.request.headers:
162
- self.request._ip = self.request.headers['X-Real-IP']
163
- elif 'X-Forwarded-For' in self.request.headers:
164
- self.request._ip = self.request.headers['X-Forwarded-For'].split(',')[0].strip()
161
+ if 'x-real-ip' in self.request.headers:
162
+ self.request._ip = self.request.headers['x-real-ip']
163
+ elif 'x-forwarded-for' in self.request.headers:
164
+ self.request._ip = self.request.headers['x-forwarded-for'].split(',')[0].strip()
165
165
 
166
- if 'Range' in self.request.headers:
166
+ if 'range' in self.request.headers:
167
167
  self.request._ranges = []
168
- for range_part in self.request.headers['Range'][6:].split(','):
168
+ for range_part in self.request.headers['range'][6:].split(','):
169
169
  range_part = range_part.strip()
170
170
  if '-' in range_part:
171
171
  start, end = range_part.split('-', 1)
172
172
  self.request.ranges.append((int(start) if start else 0, int(end) if end else None))
173
173
 
174
- if 'Upgrade' in self.request.headers and self.request.headers['Upgrade'] == 'websocket':
174
+ if 'upgrade' in self.request.headers and self.request.headers['upgrade'] == 'websocket':
175
175
  self.request._method = 'WEBSOCKET'
176
176
 
177
177
  async def recv_body(self, get_all: bool = False) -> bool | Response | None:
178
178
  loop = asyncio.get_event_loop()
179
179
 
180
- content_length = self.request.headers.get('Content-Length')
180
+ content_length = self.request.headers.get('content-length')
181
181
  if content_length:
182
182
  self.request._body = self.buffer
183
183
  self.buffer = b''
@@ -193,7 +193,7 @@ class RequestProxy:
193
193
  raise ConnectionAbortedError()
194
194
  return True
195
195
 
196
- is_chunked = self.request.headers.get('Transfer-Encoding') == 'chunked'
196
+ is_chunked = self.request.headers.get('transfer-encoding') == 'chunked'
197
197
  if is_chunked:
198
198
  if self.request._body is None:
199
199
  self.request._body = b''
@@ -213,7 +213,7 @@ class RequestProxy:
213
213
  return Response(status = 400)
214
214
 
215
215
  if chunk_size == 0:
216
- if self.request.headers.get('Trailer'):
216
+ if self.request.headers.get('trailer'):
217
217
  while b'\r\n\r\n' not in self.buffer:
218
218
  try:
219
219
  data = await asyncio.wait_for(loop.sock_recv(self.client_socket, self.app.socket_receive_buffer_size), self.app.request_timeout)
@@ -224,7 +224,7 @@ class RequestProxy:
224
224
  self.buffer += data
225
225
 
226
226
  trailer, self.buffer = self.buffer.split(b'\r\n\r\n', 1)
227
- if trailer and trailer.startswith('Content-MD5: ') and trailer[13:] not in (base64.b64encode(hashlib.md5(self.request._body).digest()).decode(), hashlib.md5(self.request._body).hexdigest()):
227
+ if trailer and trailer.startswith('content-md5: ') and trailer[13:] not in (base64.b64encode(hashlib.md5(self.request._body).digest()).decode(), hashlib.md5(self.request._body).hexdigest()):
228
228
  return Response(status = 400)
229
229
 
230
230
  self.buffer = b''
@@ -246,7 +246,7 @@ class RequestProxy:
246
246
  if self.request.body is None:
247
247
  return
248
248
 
249
- content_type = self.request.headers.get('Content-Type')
249
+ content_type = self.request.headers.get('content-type')
250
250
  if content_type == 'text/plain' or content_type is None:
251
251
  self.request._body = self.request.body.decode()
252
252
  elif content_type == 'application/json':
@@ -287,7 +287,7 @@ class RequestProxy:
287
287
  self.request._form = {}
288
288
  self.request._form[name] = data.decode()
289
289
 
290
- if self.request.headers.get('Content-Disposition'):
291
- match = re.search(r'filename="([^"]*)"', self.request.headers['Content-Disposition'])
290
+ if self.request.headers.get('content-disposition'):
291
+ match = re.search(r'filename="([^"]*)"', self.request.headers['content-disposition'])
292
292
  if match:
293
293
  self.request._file = File(match.group(1), self.request.body)
@@ -125,7 +125,7 @@ class RedirectResponse(Response):
125
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
126
  if headers is None:
127
127
  headers = {}
128
- headers['Location'] = location
128
+ headers['location'] = location
129
129
 
130
130
  super().__init__(status, body, headers)
131
131
 
@@ -173,14 +173,14 @@ class ResponseProxy:
173
173
  bytes.extend(['', ''])
174
174
  bytes = '\r\n'.join(bytes).encode()
175
175
  if not no_body:
176
- if self.response.headers.get('Transfer-Encoding') == 'chunked':
176
+ if self.response.headers.get('transfer-encoding') == 'chunked':
177
177
  bytes += hex(len(data)).encode() + b'\r\n' + data + b'\r\n'
178
178
  else:
179
179
  bytes += data
180
180
  await loop.sock_sendall(client_socket, bytes)
181
181
 
182
182
  if not no_body:
183
- if self.response.headers.get('Transfer-Encoding') == 'chunked':
183
+ if self.response.headers.get('transfer-encoding') == 'chunked':
184
184
  async for _, _, data in gen:
185
185
  await loop.sock_sendall(client_socket, hex(len(data)).encode() + b'\r\n' + data + b'\r\n')
186
186
  await loop.sock_sendall(client_socket, b'0\r\n\r\n')
@@ -193,7 +193,7 @@ class ResponseProxy:
193
193
 
194
194
  async def get_status(self, status: int, headers: dict[str, str], body: dict | list | str | bytes | None) -> tuple[int, dict[str, str], dict | list | str | bytes | None]:
195
195
  if isinstance(self.response, FileResponse):
196
- if self.request.headers.get('Range') is not None:
196
+ if self.request.headers.get('range') is not None:
197
197
  max_range = -1
198
198
  for range in self.request.ranges:
199
199
  if range[1] is not None:
@@ -229,36 +229,36 @@ class ResponseProxy:
229
229
  elif self.response.transmission_type == 'CHUNKED':
230
230
  body = self.file_response_chunked_body()
231
231
 
232
- if 'Content-Type' not in headers and 'Content-Disposition' not in headers:
232
+ if 'content-type' not in headers and 'content-disposition' not in headers:
233
233
  mime_type = mimetypes.guess_type(self.response.file.name)[0] or 'application/octet-stream'
234
234
  if mime_type in MERGE_TYPES:
235
235
  mime_type = MERGE_TYPES[mime_type]
236
- headers['Content-Type'] = f'{mime_type}; charset=utf-8'
237
- headers['Content-Disposition'] = f'{"inline" if self.response.preview and mime_type in PREVIEWABLE_TYPES else "attachment"}; filename="{self.response.file.name}"'
236
+ headers['content-type'] = f'{mime_type}; charset=utf-8'
237
+ headers['content-disposition'] = f'{"inline" if self.response.preview and mime_type in PREVIEWABLE_TYPES else "attachment"}; filename="{self.response.file.name}"'
238
238
 
239
239
  if isinstance(self.response.body, AsyncIterable):
240
- if 'Transfer-Encoding' not in headers:
241
- headers['Transfer-Encoding'] = 'chunked'
240
+ if 'transfer-encoding' not in headers:
241
+ headers['transfer-encoding'] = 'chunked'
242
242
 
243
- if 'Date' not in headers:
243
+ if 'date' not in headers:
244
244
  now = datetime.datetime.now(datetime.timezone.utc)
245
- headers['Date'] = (now.strftime('%a, %d %b %Y %H:%M:%S.') + f'{now.microsecond:06d} GMT') if self.response.high_precision_date else now.strftime('%a, %d %b %Y %H:%M:%S GMT')
245
+ headers['date'] = (now.strftime('%a, %d %b %Y %H:%M:%S.') + f'{now.microsecond:06d} GMT') if self.response.high_precision_date else now.strftime('%a, %d %b %Y %H:%M:%S GMT')
246
246
 
247
- if 'Connection' not in headers:
248
- if self.app.keep_alive and self.request and ((self.request._proxy.protocol == 'HTTP/1.1' and self.request.headers.get('Connection', '') != 'close') or (self.request._proxy.protocol == 'HTTP/1.0' and self.request.headers.get('Connection', '') == 'keep-alive')):
249
- headers['Connection'] = 'keep-alive'
250
- headers['Keep-Alive'] = f'timeout={self.app.keep_alive_timeout}, max={self.app.keep_alive_max_requests}'
247
+ if 'connection' not in headers:
248
+ if self.app.keep_alive and self.request and ((self.request._proxy.protocol == 'HTTP/1.1' and self.request.headers.get('connection', '') != 'close') or (self.request._proxy.protocol == 'HTTP/1.0' and self.request.headers.get('connection', '') == 'keep-alive')):
249
+ headers['connection'] = 'keep-alive'
250
+ headers['keep-alive'] = f'timeout={self.app.keep_alive_timeout}, max={self.app.keep_alive_max_requests}'
251
251
  else:
252
- headers['Connection'] = 'close'
252
+ headers['connection'] = 'close'
253
253
 
254
254
  if self.request.ranges:
255
255
  if status == 206:
256
- headers.setdefault('Accept-Ranges', 'bytes')
256
+ headers.setdefault('accept-ranges', 'bytes')
257
257
  else:
258
258
  encodings = []
259
259
  encoding_quality = False
260
- if self.app.compress and self.request and self.request.headers and self.request.headers.get('Accept-Encoding'):
261
- for encoding in self.request.headers.get('Accept-Encoding').split(','):
260
+ if self.app.compress and self.request and self.request.headers and self.request.headers.get('accept-encoding'):
261
+ for encoding in self.request.headers.get('accept-encoding').split(','):
262
262
  encoding_split = encoding.strip().split(';')
263
263
  if len(encoding_split) == 1:
264
264
  encoding_split.append(1)
@@ -270,26 +270,26 @@ class ResponseProxy:
270
270
  encodings = [encoding[0] for encoding in encodings]
271
271
 
272
272
  if self.response.compress is not None:
273
- headers.setdefault('Content-Encoding', self.response.compress)
273
+ headers.setdefault('content-encoding', self.response.compress)
274
274
  elif encodings:
275
275
  if encoding_quality is True:
276
276
  for encoding in encodings:
277
277
  if encoding in self.app.compress:
278
- headers.setdefault('Content-Encoding', encoding)
278
+ headers.setdefault('content-encoding', encoding)
279
279
  break
280
280
  if encoding == '*':
281
- headers.setdefault('Content-Encoding', self.app.compress[0])
281
+ headers.setdefault('content-encoding', self.app.compress[0])
282
282
  break
283
283
  else:
284
284
  if encodings[0] == '*':
285
- headers.setdefault('Content-Encoding', self.app.compress[0])
285
+ headers.setdefault('content-encoding', self.app.compress[0])
286
286
  else:
287
287
  for encoding in self.app.compress:
288
288
  if encoding in encodings:
289
- headers.setdefault('Content-Encoding', encoding)
289
+ headers.setdefault('content-encoding', encoding)
290
290
  break
291
291
 
292
- if self.response.cookies and 'Set-Cookie' not in headers:
292
+ if self.response.cookies and 'set-cookie' not in headers:
293
293
  cookies = []
294
294
  for key, cookie in self.response.cookies.items():
295
295
  _cookie = f'{key}={cookie["value"]}'
@@ -304,7 +304,7 @@ class ResponseProxy:
304
304
  if cookie['http_only']:
305
305
  _cookie += '; HttpOnly'
306
306
  cookies.append(_cookie)
307
- headers['Set-Cookie'] = ', '.join(cookies)
307
+ headers['set-cookie'] = ', '.join(cookies)
308
308
 
309
309
  return status, headers, body
310
310
 
@@ -321,13 +321,13 @@ class ResponseProxy:
321
321
  handler.seek(self.request.ranges[0][0] or 0)
322
322
  data = handler.read((self.request.ranges[0][1] or size) - (self.request.ranges[0][0]))
323
323
  handler.close()
324
- headers['Content-Length'] = str(len(data))
325
- headers['Content-Range'] = f'bytes {self.request.ranges[0][0]}-{(self.request.ranges[0][1] or size) - 1}/{size}'
324
+ headers['content-length'] = str(len(data))
325
+ headers['content-range'] = f'bytes {self.request.ranges[0][0]}-{(self.request.ranges[0][1] or size) - 1}/{size}'
326
326
  yield status, headers, data
327
327
  else:
328
328
  boundary = uuid.uuid4().hex
329
- content_type = headers['Content-Type']
330
- headers['Content-Type'] = f'multipart/byteranges; boundary={boundary}'
329
+ content_type = headers['content-type']
330
+ headers['content-type'] = f'multipart/byteranges; boundary={boundary}'
331
331
  if self.response.file._data is not None:
332
332
  size = len(self.response.file.data)
333
333
  else:
@@ -336,10 +336,10 @@ class ResponseProxy:
336
336
  content_length = 0
337
337
  for range in self.request.ranges:
338
338
  content_length += 2 + 32 + 2 + 14 + len(content_type) + 2 + 21 + (1 if range[0] == 0 else int(math.log10(range[0]))) + 1 + 1 + int(math.log10(range[1] or size)) + 1 + 1 + int(math.log10(size)) + 1 + 4 + (range[1] or size) - range[0] + 1
339
- headers['Content-Length'] = str(content_length)
339
+ headers['content-length'] = str(content_length)
340
340
 
341
341
  for range in self.request.ranges:
342
- data = [b'--', boundary.encode(), b'\r\n', b'Content-Type: ', content_type.encode(), b'\r\n', b'Content-Range: bytes ', str(range[0]).encode(), b'-', str(range[1] or size).encode(), b'/', str(size).encode() + b'\r\n\r\n']
342
+ data = [b'--', boundary.encode(), b'\r\n', b'content-type: ', content_type.encode(), b'\r\n', b'content-range: bytes ', str(range[0]).encode(), b'-', str(range[1] or size).encode(), b'/', str(size).encode() + b'\r\n\r\n']
343
343
  if self.response.file._data is not None:
344
344
  data.append(self.response.file.data[range[0]:range[1] or size + 1])
345
345
  else:
@@ -357,22 +357,22 @@ class ResponseProxy:
357
357
 
358
358
  if isinstance(data, (dict, list)):
359
359
  data = json.dumps(data).encode()
360
- headers.setdefault('Content-Type', 'application/json; charset=utf-8')
360
+ headers.setdefault('content-type', 'application/json; charset=utf-8')
361
361
  elif isinstance(data, str):
362
362
  data = data.encode()
363
- headers.setdefault('Content-Type', 'text/plain; charset=utf-8')
363
+ headers.setdefault('content-type', 'text/plain; charset=utf-8')
364
364
  elif data is None:
365
365
  data = HTTP_STATUS[status].encode()
366
- headers.setdefault('Content-Type', 'text/plain; charset=utf-8')
366
+ headers.setdefault('content-type', 'text/plain; charset=utf-8')
367
367
  elif isinstance(data, bytes):
368
- headers.setdefault('Content-Type', 'application/octet-stream; charset=utf-8')
368
+ headers.setdefault('content-type', 'application/octet-stream; charset=utf-8')
369
369
 
370
370
  if isinstance(body, AsyncIterable) is False:
371
- headers['Content-Length'] = str(len(data))
371
+ headers['content-length'] = str(len(data))
372
372
 
373
373
  status, headers, data = await self.get_encode_body(status, headers, data)
374
374
  if isinstance(body, AsyncIterable) is False:
375
- headers['Content-Length'] = str(len(data))
375
+ headers['content-length'] = str(len(data))
376
376
  yield status, headers, data
377
377
 
378
378
  if isinstance(body, AsyncIterable):
@@ -385,25 +385,25 @@ class ResponseProxy:
385
385
  yield status, headers, data
386
386
 
387
387
  async def get_encode_body(self, status: int, headers: dict[str, str], body: bytes) -> tuple[int, dict[str, str], bytes]:
388
- content_length = headers.get('Content-Length')
388
+ content_length = headers.get('content-length')
389
389
  if content_length and int(content_length) < self.app.compress_min_length:
390
- if 'Content-Encoding' in headers:
391
- del headers['Content-Encoding']
390
+ if 'content-encoding' in headers:
391
+ del headers['content-encoding']
392
392
 
393
- if 'Content-Encoding' in headers and 'Content-Length' in headers and (int(headers['Content-Length']) > self.app.compress_min_length or self.response.compress is not None):
393
+ if 'content-encoding' in headers and 'content-length' in headers and (int(headers['content-length']) > self.app.compress_min_length or self.response.compress is not None):
394
394
  compress_level = self.response.compress_level if self.response.compress_level is not None else self.app.compress_level
395
- if headers['Content-Encoding'] == 'gzip':
395
+ if headers['content-encoding'] == 'gzip':
396
396
  body = gzip.compress(body, compress_level)
397
- elif headers['Content-Encoding'] == 'deflate':
397
+ elif headers['content-encoding'] == 'deflate':
398
398
  body = zlib.compress(body, level = compress_level)
399
- elif headers['Content-Encoding'] == 'br':
399
+ elif headers['content-encoding'] == 'br':
400
400
  body = brotli.compress(body, quality = compress_level)
401
- elif headers['Content-Encoding'] == 'zstd':
401
+ elif headers['content-encoding'] == 'zstd':
402
402
  body = zstandard.ZstdCompressor(level = compress_level).compress(body)
403
403
 
404
- headers['Content-Length'] = str(len(body))
404
+ headers['content-length'] = str(len(body))
405
405
  else:
406
- if 'Content-Encoding' in headers:
407
- del headers['Content-Encoding']
406
+ if 'content-encoding' in headers:
407
+ del headers['content-encoding']
408
408
 
409
409
  return status, headers, body
@@ -109,6 +109,8 @@ class Websocket:
109
109
  close = DualMethod(_static_close, _instance_close)
110
110
 
111
111
  class WebsocketProxy:
112
+ _sync_tasks: dict[str, asyncio.Task] = {}
113
+
112
114
  @staticmethod
113
115
  def _static_send(path: str, data: bytes | list | str | dict, *, websocket_key_or_keys: str | list[str] | None = None):
114
116
  if static.websocket_sync_servers is not None:
@@ -244,7 +246,7 @@ class WebsocketProxy:
244
246
  Websocket.connectors.setdefault(self.websocket.request.path, []).append(self.websocket)
245
247
  if self.app.sync_server_url and self.websocket.request.path not in static.websocket_sync_server:
246
248
  static.websocket_sync_server[self.websocket.request.path] = redis.asyncio.Redis.from_url(self.app.sync_server_url, socket_timeout = self.app.sync_server_timeout, socket_connect_timeout = self.app.sync_server_timeout)
247
- asyncio.create_task(self.sync_server_running())
249
+ WebsocketProxy._sync_tasks[self.websocket.request.path] = asyncio.create_task(self.sync_server_running())
248
250
 
249
251
  loop = asyncio.get_running_loop()
250
252
  self.reader = asyncio.StreamReader()
@@ -259,10 +261,18 @@ class WebsocketProxy:
259
261
  await self.websocket.on_connect()
260
262
 
261
263
  async def sync_server_running(self):
264
+ pubsub = None
262
265
  try:
263
266
  if self.app.sync_server_url.startswith('redis'):
264
267
  while True:
265
268
  try:
269
+ if pubsub is not None:
270
+ try:
271
+ await pubsub.aclose()
272
+ except Exception:
273
+ pass
274
+ pubsub = None
275
+
266
276
  pubsub = redis.asyncio.from_url(self.app.sync_server_url, socket_timeout = None, socket_connect_timeout = None).pubsub()
267
277
  await pubsub.subscribe(self.websocket.request.path)
268
278
  async for message in pubsub.listen():
@@ -302,6 +312,13 @@ class WebsocketProxy:
302
312
  await asyncio.sleep(self.app.sync_server_timeout)
303
313
  except (KeyboardInterrupt, SystemExit, asyncio.CancelledError):
304
314
  ...
315
+ finally:
316
+ WebsocketProxy._sync_tasks.pop(self.websocket.request.path, None)
317
+ if pubsub is not None:
318
+ try:
319
+ await pubsub.aclose()
320
+ except Exception:
321
+ pass
305
322
 
306
323
  async def message(self):
307
324
  while True:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CheeseAPI
3
- Version: 2.0.8b4
3
+ Version: 2.0.8b6
4
4
  Summary: 一款web协程框架
5
5
  Project-URL: Source, https://github.com/CheeseUnknown/CheeseAPI
6
6
  Author-email: Cheese Unknown <cheese@cheese.ren>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "CheeseAPI"
7
- version = "2.0.8-beta.4"
7
+ version = "2.0.8-beta.6"
8
8
  description = "一款web协程框架"
9
9
  readme = "README.md"
10
10
  license-files = { paths = [ "LICENSE" ] }
File without changes
File without changes
File without changes