asgiref 3.8.1__py3-none-any.whl → 3.9.0__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.
asgiref/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "3.8.1"
1
+ __version__ = "3.9.0"
@@ -1,8 +1,8 @@
1
- import queue
2
1
  import sys
3
2
  import threading
3
+ from collections import deque
4
4
  from concurrent.futures import Executor, Future
5
- from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
5
+ from typing import Any, Callable, TypeVar
6
6
 
7
7
  if sys.version_info >= (3, 10):
8
8
  from typing import ParamSpec
@@ -53,10 +53,12 @@ class CurrentThreadExecutor(Executor):
53
53
  the thread they came from.
54
54
  """
55
55
 
56
- def __init__(self) -> None:
56
+ def __init__(self, old_executor: "CurrentThreadExecutor | None") -> None:
57
57
  self._work_thread = threading.current_thread()
58
- self._work_queue: queue.Queue[Union[_WorkItem, "Future[Any]"]] = queue.Queue()
59
- self._broken = False
58
+ self._work_ready = threading.Condition(threading.Lock())
59
+ self._work_items = deque[_WorkItem]() # synchronized by _work_ready
60
+ self._broken = False # synchronized by _work_ready
61
+ self._old_executor = old_executor
60
62
 
61
63
  def run_until_future(self, future: "Future[Any]") -> None:
62
64
  """
@@ -68,24 +70,30 @@ class CurrentThreadExecutor(Executor):
68
70
  raise RuntimeError(
69
71
  "You cannot run CurrentThreadExecutor from a different thread"
70
72
  )
71
- future.add_done_callback(self._work_queue.put)
72
- # Keep getting and running work items until we get the future we're waiting for
73
- # back via the future's done callback.
74
- try:
75
- while True:
73
+
74
+ def done(future: "Future[Any]") -> None:
75
+ with self._work_ready:
76
+ self._broken = True
77
+ self._work_ready.notify()
78
+
79
+ future.add_done_callback(done)
80
+ # Keep getting and running work items until the future we're waiting for
81
+ # is done and the queue is empty.
82
+ while True:
83
+ with self._work_ready:
84
+ while not self._work_items and not self._broken:
85
+ self._work_ready.wait()
86
+ if not self._work_items:
87
+ break
76
88
  # Get a work item and run it
77
- work_item = self._work_queue.get()
78
- if work_item is future:
79
- return
80
- assert isinstance(work_item, _WorkItem)
81
- work_item.run()
82
- del work_item
83
- finally:
84
- self._broken = True
89
+ work_item = self._work_items.popleft()
90
+ work_item.run()
91
+ del work_item
85
92
 
86
- def _submit(
93
+ def submit(
87
94
  self,
88
95
  fn: Callable[_P, _R],
96
+ /,
89
97
  *args: _P.args,
90
98
  **kwargs: _P.kwargs,
91
99
  ) -> "Future[_R]":
@@ -94,22 +102,22 @@ class CurrentThreadExecutor(Executor):
94
102
  raise RuntimeError(
95
103
  "You cannot submit onto CurrentThreadExecutor from its own thread"
96
104
  )
97
- # Check they're not too late or the executor errored
98
- if self._broken:
99
- raise RuntimeError("CurrentThreadExecutor already quit or is broken")
100
- # Add to work queue
101
105
  f: "Future[_R]" = Future()
102
106
  work_item = _WorkItem(f, fn, *args, **kwargs)
103
- self._work_queue.put(work_item)
104
- # Return the future
105
- return f
106
107
 
107
- # Python 3.9+ has a new signature for submit with a "/" after `fn`, to enforce
108
- # it to be a positional argument. If we ignore[override] mypy on 3.9+ will be
109
- # happy but 3.8 will say that the ignore comment is unused, even when
110
- # defining them differently based on sys.version_info.
111
- # We should be able to remove this when we drop support for 3.8.
112
- if not TYPE_CHECKING:
108
+ # Walk up the CurrentThreadExecutor stack to find the closest one still
109
+ # running
110
+ executor = self
111
+ while True:
112
+ with executor._work_ready:
113
+ if not executor._broken:
114
+ # Add to work queue
115
+ executor._work_items.append(work_item)
116
+ executor._work_ready.notify()
117
+ break
118
+ if executor._old_executor is None:
119
+ raise RuntimeError("CurrentThreadExecutor already quit or is broken")
120
+ executor = executor._old_executor
113
121
 
114
- def submit(self, fn, *args, **kwargs):
115
- return self._submit(fn, *args, **kwargs)
122
+ # Return the future
123
+ return f
asgiref/local.py CHANGED
@@ -2,37 +2,38 @@ import asyncio
2
2
  import contextlib
3
3
  import contextvars
4
4
  import threading
5
- from typing import Any, Dict, Union
5
+ from typing import Any, Union
6
6
 
7
7
 
8
8
  class _CVar:
9
9
  """Storage utility for Local."""
10
10
 
11
11
  def __init__(self) -> None:
12
- self._data: "contextvars.ContextVar[Dict[str, Any]]" = contextvars.ContextVar(
13
- "asgiref.local"
14
- )
12
+ self._data: dict[str, contextvars.ContextVar[Any]] = {}
15
13
 
16
- def __getattr__(self, key):
17
- storage_object = self._data.get({})
14
+ def __getattr__(self, key: str) -> Any:
18
15
  try:
19
- return storage_object[key]
16
+ var = self._data[key]
20
17
  except KeyError:
21
18
  raise AttributeError(f"{self!r} object has no attribute {key!r}")
22
19
 
20
+ try:
21
+ return var.get()
22
+ except LookupError:
23
+ raise AttributeError(f"{self!r} object has no attribute {key!r}")
24
+
23
25
  def __setattr__(self, key: str, value: Any) -> None:
24
26
  if key == "_data":
25
27
  return super().__setattr__(key, value)
26
28
 
27
- storage_object = self._data.get({})
28
- storage_object[key] = value
29
- self._data.set(storage_object)
29
+ var = self._data.get(key)
30
+ if var is None:
31
+ self._data[key] = var = contextvars.ContextVar(key)
32
+ var.set(value)
30
33
 
31
34
  def __delattr__(self, key: str) -> None:
32
- storage_object = self._data.get({})
33
- if key in storage_object:
34
- del storage_object[key]
35
- self._data.set(storage_object)
35
+ if key in self._data:
36
+ del self._data[key]
36
37
  else:
37
38
  raise AttributeError(f"{self!r} object has no attribute {key!r}")
38
39
 
@@ -82,12 +83,15 @@ class Local:
82
83
  def _lock_storage(self):
83
84
  # Thread safe access to storage
84
85
  if self._thread_critical:
86
+ is_async = True
85
87
  try:
86
88
  # this is a test for are we in a async or sync
87
89
  # thread - will raise RuntimeError if there is
88
90
  # no current loop
89
91
  asyncio.get_running_loop()
90
92
  except RuntimeError:
93
+ is_async = False
94
+ if not is_async:
91
95
  # We are in a sync thread, the storage is
92
96
  # just the plain thread local (i.e, "global within
93
97
  # this thread" - it doesn't matter where you are
asgiref/server.py CHANGED
@@ -57,12 +57,28 @@ class StatelessServer:
57
57
  Runs the asyncio event loop with our handler loop.
58
58
  """
59
59
  event_loop = asyncio.get_event_loop()
60
- asyncio.ensure_future(self.application_checker())
61
60
  try:
62
- event_loop.run_until_complete(self.handle())
61
+ event_loop.run_until_complete(self.arun())
63
62
  except KeyboardInterrupt:
64
63
  logger.info("Exiting due to Ctrl-C/interrupt")
65
64
 
65
+ async def arun(self):
66
+ """
67
+ Runs the asyncio event loop with our handler loop.
68
+ """
69
+
70
+ class Done(Exception):
71
+ pass
72
+
73
+ async def handle():
74
+ await self.handle()
75
+ raise Done
76
+
77
+ try:
78
+ await asyncio.gather(self.application_checker(), handle())
79
+ except Done:
80
+ pass
81
+
66
82
  async def handle(self):
67
83
  raise NotImplementedError("You must implement handle()")
68
84
 
asgiref/sync.py CHANGED
@@ -179,15 +179,14 @@ class AsyncToSync(Generic[_P, _R]):
179
179
 
180
180
  # You can't call AsyncToSync from a thread with a running event loop
181
181
  try:
182
- event_loop = asyncio.get_running_loop()
182
+ asyncio.get_running_loop()
183
183
  except RuntimeError:
184
184
  pass
185
185
  else:
186
- if event_loop.is_running():
187
- raise RuntimeError(
188
- "You cannot use AsyncToSync in the same thread as an async event loop - "
189
- "just await the async function directly."
190
- )
186
+ raise RuntimeError(
187
+ "You cannot use AsyncToSync in the same thread as an async event loop - "
188
+ "just await the async function directly."
189
+ )
191
190
 
192
191
  # Make a future for the return information
193
192
  call_result: "Future[_R]" = Future()
@@ -196,7 +195,7 @@ class AsyncToSync(Generic[_P, _R]):
196
195
  # need one for every sync frame, even if there's one above us in the
197
196
  # same thread.
198
197
  old_executor = getattr(self.executors, "current", None)
199
- current_executor = CurrentThreadExecutor()
198
+ current_executor = CurrentThreadExecutor(old_executor)
200
199
  self.executors.current = current_executor
201
200
 
202
201
  # Wrapping context in list so it can be reassigned from within
@@ -207,7 +206,6 @@ class AsyncToSync(Generic[_P, _R]):
207
206
  # an asyncio.CancelledError to.
208
207
  task_context = getattr(SyncToAsync.threadlocal, "task_context", None)
209
208
 
210
- loop = None
211
209
  # Use call_soon_threadsafe to schedule a synchronous callback on the
212
210
  # main event loop's thread if it's there, otherwise make a new loop
213
211
  # in this thread.
@@ -217,35 +215,45 @@ class AsyncToSync(Generic[_P, _R]):
217
215
  sys.exc_info(),
218
216
  task_context,
219
217
  context,
220
- *args,
221
- **kwargs,
218
+ # prepare an awaitable which can be passed as is to self.main_wrap,
219
+ # so that `args` and `kwargs` don't need to be
220
+ # destructured when passed to self.main_wrap
221
+ # (which is required by `ParamSpec`)
222
+ # as that may cause overlapping arguments
223
+ self.awaitable(*args, **kwargs),
222
224
  )
223
225
 
224
- if not (self.main_event_loop and self.main_event_loop.is_running()):
225
- # Make our own event loop - in a new thread - and run inside that.
226
- loop = asyncio.new_event_loop()
226
+ async def new_loop_wrap() -> None:
227
+ loop = asyncio.get_running_loop()
227
228
  self.loop_thread_executors[loop] = current_executor
229
+ try:
230
+ await awaitable
231
+ finally:
232
+ del self.loop_thread_executors[loop]
233
+
234
+ if self.main_event_loop is not None:
235
+ try:
236
+ self.main_event_loop.call_soon_threadsafe(
237
+ self.main_event_loop.create_task, awaitable
238
+ )
239
+ except RuntimeError:
240
+ running_in_main_event_loop = False
241
+ else:
242
+ running_in_main_event_loop = True
243
+ # Run the CurrentThreadExecutor until the future is done.
244
+ current_executor.run_until_future(call_result)
245
+ else:
246
+ running_in_main_event_loop = False
247
+
248
+ if not running_in_main_event_loop:
249
+ # Make our own event loop - in a new thread - and run inside that.
228
250
  loop_executor = ThreadPoolExecutor(max_workers=1)
229
- loop_future = loop_executor.submit(
230
- self._run_event_loop, loop, awaitable
231
- )
232
- if current_executor:
233
- # Run the CurrentThreadExecutor until the future is done
234
- current_executor.run_until_future(loop_future)
251
+ loop_future = loop_executor.submit(asyncio.run, new_loop_wrap())
252
+ # Run the CurrentThreadExecutor until the future is done.
253
+ current_executor.run_until_future(loop_future)
235
254
  # Wait for future and/or allow for exception propagation
236
255
  loop_future.result()
237
- else:
238
- # Call it inside the existing loop
239
- self.main_event_loop.call_soon_threadsafe(
240
- self.main_event_loop.create_task, awaitable
241
- )
242
- if current_executor:
243
- # Run the CurrentThreadExecutor until the future is done
244
- current_executor.run_until_future(call_result)
245
256
  finally:
246
- # Clean up any executor we were running
247
- if loop is not None:
248
- del self.loop_thread_executors[loop]
249
257
  _restore_context(context[0])
250
258
  # Restore old current thread executor state
251
259
  self.executors.current = old_executor
@@ -253,42 +261,6 @@ class AsyncToSync(Generic[_P, _R]):
253
261
  # Wait for results from the future.
254
262
  return call_result.result()
255
263
 
256
- def _run_event_loop(self, loop, coro):
257
- """
258
- Runs the given event loop (designed to be called in a thread).
259
- """
260
- asyncio.set_event_loop(loop)
261
- try:
262
- loop.run_until_complete(coro)
263
- finally:
264
- try:
265
- # mimic asyncio.run() behavior
266
- # cancel unexhausted async generators
267
- tasks = asyncio.all_tasks(loop)
268
- for task in tasks:
269
- task.cancel()
270
-
271
- async def gather():
272
- await asyncio.gather(*tasks, return_exceptions=True)
273
-
274
- loop.run_until_complete(gather())
275
- for task in tasks:
276
- if task.cancelled():
277
- continue
278
- if task.exception() is not None:
279
- loop.call_exception_handler(
280
- {
281
- "message": "unhandled exception during loop shutdown",
282
- "exception": task.exception(),
283
- "task": task,
284
- }
285
- )
286
- if hasattr(loop, "shutdown_asyncgens"):
287
- loop.run_until_complete(loop.shutdown_asyncgens())
288
- finally:
289
- loop.close()
290
- asyncio.set_event_loop(self.main_event_loop)
291
-
292
264
  def __get__(self, parent: Any, objtype: Any) -> Callable[_P, _R]:
293
265
  """
294
266
  Include self for methods
@@ -302,8 +274,7 @@ class AsyncToSync(Generic[_P, _R]):
302
274
  exc_info: "OptExcInfo",
303
275
  task_context: "Optional[List[asyncio.Task[Any]]]",
304
276
  context: List[contextvars.Context],
305
- *args: _P.args,
306
- **kwargs: _P.kwargs,
277
+ awaitable: Union[Coroutine[Any, Any, _R], Awaitable[_R]],
307
278
  ) -> None:
308
279
  """
309
280
  Wraps the awaitable with something that puts the result into the
@@ -326,9 +297,9 @@ class AsyncToSync(Generic[_P, _R]):
326
297
  try:
327
298
  raise exc_info[1]
328
299
  except BaseException:
329
- result = await self.awaitable(*args, **kwargs)
300
+ result = await awaitable
330
301
  else:
331
- result = await self.awaitable(*args, **kwargs)
302
+ result = await awaitable
332
303
  except BaseException as e:
333
304
  call_result.set_exception(e)
334
305
  else:
asgiref/testing.py CHANGED
@@ -13,18 +13,40 @@ class ApplicationCommunicator:
13
13
  """
14
14
 
15
15
  def __init__(self, application, scope):
16
+ self._future = None
16
17
  self.application = guarantee_single_callable(application)
17
18
  self.scope = scope
18
- self.input_queue = asyncio.Queue()
19
- self.output_queue = asyncio.Queue()
20
- # Clear context - this ensures that context vars set in the testing scope
21
- # are not "leaked" into the application which would normally begin with
22
- # an empty context. In Python >= 3.11 this could also be written as:
23
- # asyncio.create_task(..., context=contextvars.Context())
24
- self.future = contextvars.Context().run(
25
- asyncio.create_task,
26
- self.application(scope, self.input_queue.get, self.output_queue.put),
27
- )
19
+ self._input_queue = None
20
+ self._output_queue = None
21
+
22
+ # For Python 3.9 we need to lazily bind the queues, on 3.10+ they bind the
23
+ # event loop lazily.
24
+ @property
25
+ def input_queue(self):
26
+ if self._input_queue is None:
27
+ self._input_queue = asyncio.Queue()
28
+ return self._input_queue
29
+
30
+ @property
31
+ def output_queue(self):
32
+ if self._output_queue is None:
33
+ self._output_queue = asyncio.Queue()
34
+ return self._output_queue
35
+
36
+ @property
37
+ def future(self):
38
+ if self._future is None:
39
+ # Clear context - this ensures that context vars set in the testing scope
40
+ # are not "leaked" into the application which would normally begin with
41
+ # an empty context. In Python >= 3.11 this could also be written as:
42
+ # asyncio.create_task(..., context=contextvars.Context())
43
+ self._future = contextvars.Context().run(
44
+ asyncio.create_task,
45
+ self.application(
46
+ self.scope, self.input_queue.get, self.output_queue.put
47
+ ),
48
+ )
49
+ return self._future
28
50
 
29
51
  async def wait(self, timeout=1):
30
52
  """
@@ -46,11 +68,15 @@ class ApplicationCommunicator:
46
68
  pass
47
69
 
48
70
  def stop(self, exceptions=True):
49
- if not self.future.done():
50
- self.future.cancel()
71
+ future = self._future
72
+ if future is None:
73
+ return
74
+
75
+ if not future.done():
76
+ future.cancel()
51
77
  elif exceptions:
52
78
  # Give a chance to raise any exceptions
53
- self.future.result()
79
+ future.result()
54
80
 
55
81
  def __del__(self):
56
82
  # Clean up on deletion
@@ -64,6 +90,10 @@ class ApplicationCommunicator:
64
90
  """
65
91
  Sends a single message to the application
66
92
  """
93
+ # Make sure there's not an exception to raise from the task
94
+ if self.future.done():
95
+ self.future.result()
96
+
67
97
  # Give it the message
68
98
  await self.input_queue.put(message)
69
99
 
@@ -94,6 +124,10 @@ class ApplicationCommunicator:
94
124
  """
95
125
  Checks that there is no message to receive in the given time.
96
126
  """
127
+ # Make sure there's not an exception to raise from the task
128
+ if self.future.done():
129
+ self.future.result()
130
+
97
131
  # `interval` has precedence over `timeout`
98
132
  start = time.monotonic()
99
133
  while time.monotonic() - start < timeout:
asgiref/typing.py CHANGED
@@ -189,6 +189,7 @@ class WebSocketResponseBodyEvent(TypedDict):
189
189
  class WebSocketDisconnectEvent(TypedDict):
190
190
  type: Literal["websocket.disconnect"]
191
191
  code: int
192
+ reason: Optional[str]
192
193
 
193
194
 
194
195
  class WebSocketCloseEvent(TypedDict):
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: asgiref
3
- Version: 3.8.1
3
+ Version: 3.9.0
4
4
  Summary: ASGI specs, helper code, and adapters
5
5
  Home-page: https://github.com/django/asgiref/
6
6
  Author: Django Software Foundation
@@ -17,19 +17,20 @@ Classifier: Operating System :: OS Independent
17
17
  Classifier: Programming Language :: Python
18
18
  Classifier: Programming Language :: Python :: 3
19
19
  Classifier: Programming Language :: Python :: 3 :: Only
20
- Classifier: Programming Language :: Python :: 3.8
21
20
  Classifier: Programming Language :: Python :: 3.9
22
21
  Classifier: Programming Language :: Python :: 3.10
23
22
  Classifier: Programming Language :: Python :: 3.11
24
23
  Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
25
  Classifier: Topic :: Internet :: WWW/HTTP
26
- Requires-Python: >=3.8
26
+ Requires-Python: >=3.9
27
27
  License-File: LICENSE
28
- Requires-Dist: typing-extensions >=4 ; python_version < "3.11"
28
+ Requires-Dist: typing_extensions>=4; python_version < "3.11"
29
29
  Provides-Extra: tests
30
- Requires-Dist: pytest ; extra == 'tests'
31
- Requires-Dist: pytest-asyncio ; extra == 'tests'
32
- Requires-Dist: mypy >=0.800 ; extra == 'tests'
30
+ Requires-Dist: pytest; extra == "tests"
31
+ Requires-Dist: pytest-asyncio; extra == "tests"
32
+ Requires-Dist: mypy>=1.14.0; extra == "tests"
33
+ Dynamic: license-file
33
34
 
34
35
  asgiref
35
36
  =======
@@ -129,7 +130,7 @@ file handles for incoming POST bodies).
129
130
  Dependencies
130
131
  ------------
131
132
 
132
- ``asgiref`` requires Python 3.8 or higher.
133
+ ``asgiref`` requires Python 3.9 or higher.
133
134
 
134
135
 
135
136
  Contributing
@@ -179,7 +180,7 @@ Then, build and push the packages::
179
180
 
180
181
  python -m build
181
182
  twine upload dist/*
182
- rm -r build/ dist/
183
+ rm -r asgiref.egg-info dist
183
184
 
184
185
 
185
186
  Implementation Details
@@ -0,0 +1,16 @@
1
+ asgiref/__init__.py,sha256=ya3Avm9qHzetIgZApSM6eHjyO1KYOqbRf5xjSmbIqdg,22
2
+ asgiref/compatibility.py,sha256=DhY1SOpOvOw0Y1lSEjCqg-znRUQKecG3LTaV48MZi68,1606
3
+ asgiref/current_thread_executor.py,sha256=42CU1VODLTk-_PYise-cP1XgyAvI5Djc8f97owFzdrs,4157
4
+ asgiref/local.py,sha256=XgqYyYlVcGgj8W95fIl5kKquvlajIq41_AqT0Isqu6Q,4874
5
+ asgiref/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ asgiref/server.py,sha256=3A68169Nuh2sTY_2O5JzRd_opKObWvvrEFcrXssq3kA,6311
7
+ asgiref/sync.py,sha256=5dlK0T61pMSNWf--49nUojn0mfduqq6ryuwyXxv2Y-A,20417
8
+ asgiref/testing.py,sha256=U5wcs_-ZYTO5SIGfl80EqRAGv_T8BHrAhvAKRuuztT4,4421
9
+ asgiref/timeout.py,sha256=LtGL-xQpG8JHprdsEUCMErJ0kNWj4qwWZhEHJ3iKu4s,3627
10
+ asgiref/typing.py,sha256=Zi72AZlOyF1C7N14LLZnpAdfUH4ljoBqFdQo_bBKMq0,6290
11
+ asgiref/wsgi.py,sha256=fxBLgUE_0PEVgcp13ticz6GHf3q-aKWcB5eFPhd6yxo,6753
12
+ asgiref-3.9.0.dist-info/licenses/LICENSE,sha256=uEZBXRtRTpwd_xSiLeuQbXlLxUbKYSn5UKGM0JHipmk,1552
13
+ asgiref-3.9.0.dist-info/METADATA,sha256=QF1mutxXPUG_-RWadNzX8hAgXrJVGGl4TD0Pklhku2Y,9286
14
+ asgiref-3.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ asgiref-3.9.0.dist-info/top_level.txt,sha256=bokQjCzwwERhdBiPdvYEZa4cHxT4NCeAffQNUqJ8ssg,8
16
+ asgiref-3.9.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,16 +0,0 @@
1
- asgiref/__init__.py,sha256=kZzGpxWKY4rWDQrrrlM7bN7YKRAjy17Wv4w__djvVYU,22
2
- asgiref/compatibility.py,sha256=DhY1SOpOvOw0Y1lSEjCqg-znRUQKecG3LTaV48MZi68,1606
3
- asgiref/current_thread_executor.py,sha256=EuowbT0oL_P4Fq8KTXNUyEgk3-k4Yh4E8F_anEVdeBI,3977
4
- asgiref/local.py,sha256=bNeER_QIfw2-PAPYanqAZq6yAAEJ-aio7e9o8Up-mgI,4808
5
- asgiref/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- asgiref/server.py,sha256=egTQhZo1k4G0F7SSBQNp_VOekpGcjBJZU2kkCoiGC_M,6005
7
- asgiref/sync.py,sha256=Why0YQV84vSp7IBBr-JDbxYCua-InLgBjuiCMlj9WgI,21444
8
- asgiref/testing.py,sha256=QgZgXKrwdq5xzhZqynr1msWOiTS3Kpastj7wHU2ePRY,3481
9
- asgiref/timeout.py,sha256=LtGL-xQpG8JHprdsEUCMErJ0kNWj4qwWZhEHJ3iKu4s,3627
10
- asgiref/typing.py,sha256=rLF3y_9OgvlQMaDm8yMw8QTgsO9Mv9YAc6Cj8xjvWo0,6264
11
- asgiref/wsgi.py,sha256=fxBLgUE_0PEVgcp13ticz6GHf3q-aKWcB5eFPhd6yxo,6753
12
- asgiref-3.8.1.dist-info/LICENSE,sha256=uEZBXRtRTpwd_xSiLeuQbXlLxUbKYSn5UKGM0JHipmk,1552
13
- asgiref-3.8.1.dist-info/METADATA,sha256=Cbu67XPstSkMxAdA4puvY-FAzN9OrT_AasH7IuK6DaM,9259
14
- asgiref-3.8.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
- asgiref-3.8.1.dist-info/top_level.txt,sha256=bokQjCzwwERhdBiPdvYEZa4cHxT4NCeAffQNUqJ8ssg,8
16
- asgiref-3.8.1.dist-info/RECORD,,