asgi-tools 1.1.0__cp311-cp311-macosx_10_9_universal2.whl → 1.3.1__cp311-cp311-macosx_10_9_universal2.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.
Binary file
asgi_tools/forms.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from io import BytesIO
6
6
  from tempfile import SpooledTemporaryFile
7
- from typing import TYPE_CHECKING, Callable, Optional
7
+ from typing import TYPE_CHECKING, Callable
8
8
  from urllib.parse import unquote_to_bytes
9
9
 
10
10
  from multidict import MultiDict
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
19
19
  async def read_formdata(
20
20
  request: "Request",
21
21
  max_size: int,
22
- upload_to: Optional[Callable],
22
+ upload_to: Callable | None,
23
23
  file_memory_limit: int = 1024 * 1024,
24
24
  ) -> MultiDict:
25
25
  """Read formdata from the given request."""
@@ -43,7 +43,7 @@ async def read_formdata(
43
43
  class FormReader:
44
44
  """Process querystring form data."""
45
45
 
46
- __slots__ = "form", "curname", "curvalue", "charset"
46
+ __slots__ = "charset", "curname", "curvalue", "form"
47
47
 
48
48
  def __init__(self, charset: str):
49
49
  self.charset = charset
@@ -80,18 +80,18 @@ class MultipartReader(FormReader):
80
80
  """Process multipart formdata."""
81
81
 
82
82
  __slots__ = (
83
- "form",
83
+ "charset",
84
84
  "curname",
85
85
  "curvalue",
86
- "charset",
86
+ "file_memory_limit",
87
+ "form",
88
+ "headers",
87
89
  "name",
88
90
  "partdata",
89
- "headers",
90
91
  "upload_to",
91
- "file_memory_limit",
92
92
  )
93
93
 
94
- def __init__(self, charset: str, upload_to: Optional[Callable], file_memory_limit: int):
94
+ def __init__(self, charset: str, upload_to: Callable | None, file_memory_limit: int):
95
95
  super().__init__(charset)
96
96
  self.name = ""
97
97
  self.headers: dict[bytes, bytes] = {}
@@ -102,7 +102,7 @@ class MultipartReader(FormReader):
102
102
  def init_parser(self, request: "Request", max_size: int) -> BaseParser:
103
103
  boundary = request.media.get("boundary", "")
104
104
  if not boundary:
105
- raise ValueError("Invalid content type boundary") # noqa: TRY003
105
+ raise ValueError("Invalid content type boundary")
106
106
 
107
107
  return MultipartParser(
108
108
  boundary,
asgi_tools/middleware.py CHANGED
@@ -8,7 +8,7 @@ from contextvars import ContextVar
8
8
  from functools import partial
9
9
  from inspect import isawaitable
10
10
  from pathlib import Path
11
- from typing import TYPE_CHECKING, Awaitable, Callable, Final, Mapping, Optional, Union
11
+ from typing import TYPE_CHECKING, Awaitable, Callable, Final, Mapping
12
12
 
13
13
  from http_router import Router
14
14
 
@@ -22,12 +22,12 @@ if TYPE_CHECKING:
22
22
  from .types import TASGIApp, TASGIMessage, TASGIReceive, TASGIScope, TASGISend
23
23
 
24
24
 
25
- class BaseMiddeware(metaclass=abc.ABCMeta):
25
+ class BaseMiddleware(metaclass=abc.ABCMeta):
26
26
  """Base class for ASGI-Tools middlewares."""
27
27
 
28
28
  scopes: tuple[str, ...] = ("http", "websocket")
29
29
 
30
- def __init__(self, app: Optional[TASGIApp] = None) -> None:
30
+ def __init__(self, app: TASGIApp | None = None) -> None:
31
31
  """Save ASGI App."""
32
32
  self.bind(app)
33
33
 
@@ -48,13 +48,17 @@ class BaseMiddeware(metaclass=abc.ABCMeta):
48
48
  """Setup the middleware without an initialization."""
49
49
  return partial(cls, **params) # type: ignore[abstract]
50
50
 
51
- def bind(self, app: Optional[TASGIApp] = None):
51
+ def bind(self, app: TASGIApp | None = None):
52
52
  """Rebind the middleware to an ASGI application if it has been inited already."""
53
53
  self.app = app or ResponseError.NOT_FOUND()
54
54
  return self
55
55
 
56
56
 
57
- class ResponseMiddleware(BaseMiddeware):
57
+ # Backward compatibility
58
+ BaseMiddeware = BaseMiddleware
59
+
60
+
61
+ class ResponseMiddleware(BaseMiddleware):
58
62
  """Automatically convert ASGI_ apps results into responses :class:`~asgi_tools.Response` and
59
63
  send them to server as ASGI_ messages.
60
64
 
@@ -121,22 +125,22 @@ class ResponseMiddleware(BaseMiddeware):
121
125
  await exc(scope, receive, send)
122
126
 
123
127
  def send(self, _: TASGIMessage):
124
- raise RuntimeError("You can't use send() method in ResponseMiddleware") # noqa: TRY003
128
+ raise RuntimeError("You can't use send() method in ResponseMiddleware")
125
129
 
126
- def bind(self, app: Optional[TASGIApp] = None):
130
+ def bind(self, app: TASGIApp | None = None):
127
131
  """Rebind the middleware to an ASGI application if it has been inited already."""
128
132
  self.app = app or to_awaitable(lambda *_: ResponseError.NOT_FOUND())
129
133
  return self
130
134
 
131
135
 
132
- class RequestMiddleware(BaseMiddeware):
136
+ class RequestMiddleware(BaseMiddleware):
133
137
  """Automatically create :class:`asgi_tools.Request` from the scope and pass it to ASGI_ apps.
134
138
 
135
139
  .. code-block:: python
136
140
 
137
141
  from asgi_tools import RequestMiddleware, Response
138
142
 
139
- async def app(request, receive, send):
143
+ async def app(scope, receive, send):
140
144
  content = f"{ request.method } { request.url.path }"
141
145
  response = Response(content)
142
146
  await response(scope, receive, send)
@@ -150,7 +154,7 @@ class RequestMiddleware(BaseMiddeware):
150
154
  return await self.app(Request(scope, receive, send), receive, send)
151
155
 
152
156
 
153
- class LifespanMiddleware(BaseMiddeware):
157
+ class LifespanMiddleware(BaseMiddleware):
154
158
  """Manage ASGI_ Lifespan events.
155
159
 
156
160
  :param ignore_errors: Ignore errors from startup/shutdown handlers
@@ -193,12 +197,12 @@ class LifespanMiddleware(BaseMiddeware):
193
197
 
194
198
  def __init__(
195
199
  self,
196
- app: Optional[TASGIApp] = None,
200
+ app: TASGIApp | None = None,
197
201
  *,
198
202
  logger=logger,
199
203
  ignore_errors: bool = False,
200
- on_startup: Union[Callable, list[Callable], None] = None,
201
- on_shutdown: Union[Callable, list[Callable], None] = None,
204
+ on_startup: Callable | list[Callable] | None = None,
205
+ on_shutdown: Callable | list[Callable] | None = None,
202
206
  ) -> None:
203
207
  """Prepare the middleware."""
204
208
  super(LifespanMiddleware, self).__init__(app)
@@ -223,7 +227,7 @@ class LifespanMiddleware(BaseMiddeware):
223
227
  break
224
228
 
225
229
  def __register__(
226
- self, handlers: Union[Callable, list[Callable], None], container: list[Callable]
230
+ self, handlers: Callable | list[Callable] | None, container: list[Callable]
227
231
  ) -> None:
228
232
  """Register lifespan handlers."""
229
233
  if not handlers:
@@ -243,7 +247,7 @@ class LifespanMiddleware(BaseMiddeware):
243
247
  """Use the lifespan middleware as a context manager."""
244
248
  await self.run("shutdown")
245
249
 
246
- async def run(self, event: str, _: Optional[TASGISend] = None):
250
+ async def run(self, event: str, _: TASGISend | None = None):
247
251
  """Run startup/shutdown handlers."""
248
252
  assert event in {"startup", "shutdown"}
249
253
  handlers = getattr(self, f"__{event}__")
@@ -273,7 +277,7 @@ class LifespanMiddleware(BaseMiddeware):
273
277
  self.__register__(fn, self.__shutdown__)
274
278
 
275
279
 
276
- class RouterMiddleware(BaseMiddeware):
280
+ class RouterMiddleware(BaseMiddleware):
277
281
  r"""Manage routing.
278
282
 
279
283
  .. code-block:: python
@@ -328,7 +332,7 @@ class RouterMiddleware(BaseMiddeware):
328
332
 
329
333
  """
330
334
 
331
- def __init__(self, app: Optional[TASGIApp] = None, router: Optional[Router] = None) -> None:
335
+ def __init__(self, app: TASGIApp | None = None, router: Router | None = None) -> None:
332
336
  """Initialize HTTP router."""
333
337
  super().__init__(app)
334
338
  self.router = router or Router(validator=callable)
@@ -338,7 +342,7 @@ class RouterMiddleware(BaseMiddeware):
338
342
  app, scope["path_params"] = self.__dispatch__(scope)
339
343
  return await app(scope, *args)
340
344
 
341
- def __dispatch__(self, scope: TASGIScope) -> tuple[Callable, Optional[Mapping]]:
345
+ def __dispatch__(self, scope: TASGIScope) -> tuple[Callable, Mapping | None]:
342
346
  """Lookup for a callback."""
343
347
  path = f"{scope.get('root_path', '')}{scope['path']}"
344
348
  try:
@@ -355,7 +359,7 @@ class RouterMiddleware(BaseMiddeware):
355
359
  return self.router.route(*args, **kwargs)
356
360
 
357
361
 
358
- class StaticFilesMiddleware(BaseMiddeware):
362
+ class StaticFilesMiddleware(BaseMiddleware):
359
363
  """Serve static files.
360
364
 
361
365
  :param url_prefix: an URL prefix for static files
@@ -368,7 +372,7 @@ class StaticFilesMiddleware(BaseMiddeware):
368
372
  from asgi_tools import StaticFilesMiddleware, ResponseHTML
369
373
 
370
374
  async def app(scope, receive, send):
371
- response = ResponseHTML('OK)
375
+ response = ResponseHTML('OK')
372
376
  await response(scope, receive, send)
373
377
 
374
378
  # Files from static folder will be served from /static
@@ -380,9 +384,9 @@ class StaticFilesMiddleware(BaseMiddeware):
380
384
 
381
385
  def __init__(
382
386
  self,
383
- app: Optional[TASGIApp] = None,
387
+ app: TASGIApp | None = None,
384
388
  url_prefix: str = "/static",
385
- folders: Optional[list[Union[str, Path]]] = None,
389
+ folders: list[str | Path] | None = None,
386
390
  ) -> None:
387
391
  """Initialize the middleware."""
388
392
  super().__init__(app)
@@ -395,10 +399,12 @@ class StaticFilesMiddleware(BaseMiddeware):
395
399
  path = scope["path"]
396
400
  url_prefix = self.url_prefix
397
401
  if path.startswith(url_prefix):
398
- response: Optional[Response] = None
402
+ response: Response | None = None
399
403
  filename = path[len(url_prefix) :].strip("/")
400
404
  for folder in self.folders:
401
405
  filepath = folder.joinpath(filename).resolve()
406
+ if folder != filepath.parent:
407
+ continue
402
408
  with suppress(ASGIError):
403
409
  response = ResponseFile(filepath, headers_only=scope["method"] == "HEAD")
404
410
  break
@@ -410,10 +416,10 @@ class StaticFilesMiddleware(BaseMiddeware):
410
416
  await self.app(scope, receive, send)
411
417
 
412
418
 
413
- BACKGROUND_TASK: Final = ContextVar[Optional[Awaitable]]("background_task", default=None)
419
+ BACKGROUND_TASK: Final = ContextVar[list[Awaitable] | None]("background_task", default=None)
414
420
 
415
421
 
416
- class BackgroundMiddleware(BaseMiddeware):
422
+ class BackgroundMiddleware(BaseMiddleware):
417
423
  """Run background tasks.
418
424
 
419
425
 
@@ -439,11 +445,14 @@ class BackgroundMiddleware(BaseMiddeware):
439
445
  async def __process__(self, scope: TASGIScope, receive: TASGIReceive, send: TASGISend):
440
446
  """Run background tasks."""
441
447
  await self.app(scope, receive, send)
442
- bgtask = BACKGROUND_TASK.get()
443
- if bgtask is not None and isawaitable(bgtask):
444
- await bgtask
448
+ for task in BACKGROUND_TASK.get() or []:
449
+ await task
450
+
451
+ BACKGROUND_TASK.set(None) # Clear the context variable
445
452
 
446
453
  @staticmethod
447
454
  def set_task(task: Awaitable):
448
455
  """Set a task for background execution."""
449
- BACKGROUND_TASK.set(task)
456
+ tasks = BACKGROUND_TASK.get() or []
457
+ tasks.append(task)
458
+ BACKGROUND_TASK.set(tasks)