aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/asgiref
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/asgiref
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/asgiref')
-rw-r--r--.venv/lib/python3.12/site-packages/asgiref/__init__.py1
-rw-r--r--.venv/lib/python3.12/site-packages/asgiref/compatibility.py48
-rw-r--r--.venv/lib/python3.12/site-packages/asgiref/current_thread_executor.py115
-rw-r--r--.venv/lib/python3.12/site-packages/asgiref/local.py128
-rw-r--r--.venv/lib/python3.12/site-packages/asgiref/py.typed0
-rw-r--r--.venv/lib/python3.12/site-packages/asgiref/server.py157
-rw-r--r--.venv/lib/python3.12/site-packages/asgiref/sync.py613
-rw-r--r--.venv/lib/python3.12/site-packages/asgiref/testing.py103
-rw-r--r--.venv/lib/python3.12/site-packages/asgiref/timeout.py118
-rw-r--r--.venv/lib/python3.12/site-packages/asgiref/typing.py278
-rw-r--r--.venv/lib/python3.12/site-packages/asgiref/wsgi.py166
11 files changed, 1727 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/asgiref/__init__.py b/.venv/lib/python3.12/site-packages/asgiref/__init__.py
new file mode 100644
index 00000000..e4e78c0b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/asgiref/__init__.py
@@ -0,0 +1 @@
+__version__ = "3.8.1"
diff --git a/.venv/lib/python3.12/site-packages/asgiref/compatibility.py b/.venv/lib/python3.12/site-packages/asgiref/compatibility.py
new file mode 100644
index 00000000..3a2a63e6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/asgiref/compatibility.py
@@ -0,0 +1,48 @@
+import inspect
+
+from .sync import iscoroutinefunction
+
+
+def is_double_callable(application):
+ """
+ Tests to see if an application is a legacy-style (double-callable) application.
+ """
+ # Look for a hint on the object first
+ if getattr(application, "_asgi_single_callable", False):
+ return False
+ if getattr(application, "_asgi_double_callable", False):
+ return True
+ # Uninstanted classes are double-callable
+ if inspect.isclass(application):
+ return True
+ # Instanted classes depend on their __call__
+ if hasattr(application, "__call__"):
+ # We only check to see if its __call__ is a coroutine function -
+ # if it's not, it still might be a coroutine function itself.
+ if iscoroutinefunction(application.__call__):
+ return False
+ # Non-classes we just check directly
+ return not iscoroutinefunction(application)
+
+
+def double_to_single_callable(application):
+ """
+ Transforms a double-callable ASGI application into a single-callable one.
+ """
+
+ async def new_application(scope, receive, send):
+ instance = application(scope)
+ return await instance(receive, send)
+
+ return new_application
+
+
+def guarantee_single_callable(application):
+ """
+ Takes either a single- or double-callable application and always returns it
+ in single-callable style. Use this to add backwards compatibility for ASGI
+ 2.0 applications to your server/test harness/etc.
+ """
+ if is_double_callable(application):
+ application = double_to_single_callable(application)
+ return application
diff --git a/.venv/lib/python3.12/site-packages/asgiref/current_thread_executor.py b/.venv/lib/python3.12/site-packages/asgiref/current_thread_executor.py
new file mode 100644
index 00000000..67a7926f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/asgiref/current_thread_executor.py
@@ -0,0 +1,115 @@
+import queue
+import sys
+import threading
+from concurrent.futures import Executor, Future
+from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
+
+if sys.version_info >= (3, 10):
+ from typing import ParamSpec
+else:
+ from typing_extensions import ParamSpec
+
+_T = TypeVar("_T")
+_P = ParamSpec("_P")
+_R = TypeVar("_R")
+
+
+class _WorkItem:
+ """
+ Represents an item needing to be run in the executor.
+ Copied from ThreadPoolExecutor (but it's private, so we're not going to rely on importing it)
+ """
+
+ def __init__(
+ self,
+ future: "Future[_R]",
+ fn: Callable[_P, _R],
+ *args: _P.args,
+ **kwargs: _P.kwargs,
+ ):
+ self.future = future
+ self.fn = fn
+ self.args = args
+ self.kwargs = kwargs
+
+ def run(self) -> None:
+ __traceback_hide__ = True # noqa: F841
+ if not self.future.set_running_or_notify_cancel():
+ return
+ try:
+ result = self.fn(*self.args, **self.kwargs)
+ except BaseException as exc:
+ self.future.set_exception(exc)
+ # Break a reference cycle with the exception 'exc'
+ self = None # type: ignore[assignment]
+ else:
+ self.future.set_result(result)
+
+
+class CurrentThreadExecutor(Executor):
+ """
+ An Executor that actually runs code in the thread it is instantiated in.
+ Passed to other threads running async code, so they can run sync code in
+ the thread they came from.
+ """
+
+ def __init__(self) -> None:
+ self._work_thread = threading.current_thread()
+ self._work_queue: queue.Queue[Union[_WorkItem, "Future[Any]"]] = queue.Queue()
+ self._broken = False
+
+ def run_until_future(self, future: "Future[Any]") -> None:
+ """
+ Runs the code in the work queue until a result is available from the future.
+ Should be run from the thread the executor is initialised in.
+ """
+ # Check we're in the right thread
+ if threading.current_thread() != self._work_thread:
+ raise RuntimeError(
+ "You cannot run CurrentThreadExecutor from a different thread"
+ )
+ future.add_done_callback(self._work_queue.put)
+ # Keep getting and running work items until we get the future we're waiting for
+ # back via the future's done callback.
+ try:
+ while True:
+ # Get a work item and run it
+ work_item = self._work_queue.get()
+ if work_item is future:
+ return
+ assert isinstance(work_item, _WorkItem)
+ work_item.run()
+ del work_item
+ finally:
+ self._broken = True
+
+ def _submit(
+ self,
+ fn: Callable[_P, _R],
+ *args: _P.args,
+ **kwargs: _P.kwargs,
+ ) -> "Future[_R]":
+ # Check they're not submitting from the same thread
+ if threading.current_thread() == self._work_thread:
+ raise RuntimeError(
+ "You cannot submit onto CurrentThreadExecutor from its own thread"
+ )
+ # Check they're not too late or the executor errored
+ if self._broken:
+ raise RuntimeError("CurrentThreadExecutor already quit or is broken")
+ # Add to work queue
+ f: "Future[_R]" = Future()
+ work_item = _WorkItem(f, fn, *args, **kwargs)
+ self._work_queue.put(work_item)
+ # Return the future
+ return f
+
+ # Python 3.9+ has a new signature for submit with a "/" after `fn`, to enforce
+ # it to be a positional argument. If we ignore[override] mypy on 3.9+ will be
+ # happy but 3.8 will say that the ignore comment is unused, even when
+ # defining them differently based on sys.version_info.
+ # We should be able to remove this when we drop support for 3.8.
+ if not TYPE_CHECKING:
+
+ def submit(self, fn, *args, **kwargs):
+ return self._submit(fn, *args, **kwargs)
diff --git a/.venv/lib/python3.12/site-packages/asgiref/local.py b/.venv/lib/python3.12/site-packages/asgiref/local.py
new file mode 100644
index 00000000..a8b9459b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/asgiref/local.py
@@ -0,0 +1,128 @@
+import asyncio
+import contextlib
+import contextvars
+import threading
+from typing import Any, Dict, Union
+
+
+class _CVar:
+ """Storage utility for Local."""
+
+ def __init__(self) -> None:
+ self._data: "contextvars.ContextVar[Dict[str, Any]]" = contextvars.ContextVar(
+ "asgiref.local"
+ )
+
+ def __getattr__(self, key):
+ storage_object = self._data.get({})
+ try:
+ return storage_object[key]
+ except KeyError:
+ raise AttributeError(f"{self!r} object has no attribute {key!r}")
+
+ def __setattr__(self, key: str, value: Any) -> None:
+ if key == "_data":
+ return super().__setattr__(key, value)
+
+ storage_object = self._data.get({})
+ storage_object[key] = value
+ self._data.set(storage_object)
+
+ def __delattr__(self, key: str) -> None:
+ storage_object = self._data.get({})
+ if key in storage_object:
+ del storage_object[key]
+ self._data.set(storage_object)
+ else:
+ raise AttributeError(f"{self!r} object has no attribute {key!r}")
+
+
+class Local:
+ """Local storage for async tasks.
+
+ This is a namespace object (similar to `threading.local`) where data is
+ also local to the current async task (if there is one).
+
+ In async threads, local means in the same sense as the `contextvars`
+ module - i.e. a value set in an async frame will be visible:
+
+ - to other async code `await`-ed from this frame.
+ - to tasks spawned using `asyncio` utilities (`create_task`, `wait_for`,
+ `gather` and probably others).
+ - to code scheduled in a sync thread using `sync_to_async`
+
+ In "sync" threads (a thread with no async event loop running), the
+ data is thread-local, but additionally shared with async code executed
+ via the `async_to_sync` utility, which schedules async code in a new thread
+ and copies context across to that thread.
+
+ If `thread_critical` is True, then the local will only be visible per-thread,
+ behaving exactly like `threading.local` if the thread is sync, and as
+ `contextvars` if the thread is async. This allows genuinely thread-sensitive
+ code (such as DB handles) to be kept stricly to their initial thread and
+ disable the sharing across `sync_to_async` and `async_to_sync` wrapped calls.
+
+ Unlike plain `contextvars` objects, this utility is threadsafe.
+ """
+
+ def __init__(self, thread_critical: bool = False) -> None:
+ self._thread_critical = thread_critical
+ self._thread_lock = threading.RLock()
+
+ self._storage: "Union[threading.local, _CVar]"
+
+ if thread_critical:
+ # Thread-local storage
+ self._storage = threading.local()
+ else:
+ # Contextvar storage
+ self._storage = _CVar()
+
+ @contextlib.contextmanager
+ def _lock_storage(self):
+ # Thread safe access to storage
+ if self._thread_critical:
+ try:
+ # this is a test for are we in a async or sync
+ # thread - will raise RuntimeError if there is
+ # no current loop
+ asyncio.get_running_loop()
+ except RuntimeError:
+ # We are in a sync thread, the storage is
+ # just the plain thread local (i.e, "global within
+ # this thread" - it doesn't matter where you are
+ # in a call stack you see the same storage)
+ yield self._storage
+ else:
+ # We are in an async thread - storage is still
+ # local to this thread, but additionally should
+ # behave like a context var (is only visible with
+ # the same async call stack)
+
+ # Ensure context exists in the current thread
+ if not hasattr(self._storage, "cvar"):
+ self._storage.cvar = _CVar()
+
+ # self._storage is a thread local, so the members
+ # can't be accessed in another thread (we don't
+ # need any locks)
+ yield self._storage.cvar
+ else:
+ # Lock for thread_critical=False as other threads
+ # can access the exact same storage object
+ with self._thread_lock:
+ yield self._storage
+
+ def __getattr__(self, key):
+ with self._lock_storage() as storage:
+ return getattr(storage, key)
+
+ def __setattr__(self, key, value):
+ if key in ("_local", "_storage", "_thread_critical", "_thread_lock"):
+ return super().__setattr__(key, value)
+ with self._lock_storage() as storage:
+ setattr(storage, key, value)
+
+ def __delattr__(self, key):
+ with self._lock_storage() as storage:
+ delattr(storage, key)
diff --git a/.venv/lib/python3.12/site-packages/asgiref/py.typed b/.venv/lib/python3.12/site-packages/asgiref/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/asgiref/py.typed
diff --git a/.venv/lib/python3.12/site-packages/asgiref/server.py b/.venv/lib/python3.12/site-packages/asgiref/server.py
new file mode 100644
index 00000000..43c28c6c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/asgiref/server.py
@@ -0,0 +1,157 @@
+import asyncio
+import logging
+import time
+import traceback
+
+from .compatibility import guarantee_single_callable
+
+logger = logging.getLogger(__name__)
+
+
+class StatelessServer:
+ """
+ Base server class that handles basic concepts like application instance
+ creation/pooling, exception handling, and similar, for stateless protocols
+ (i.e. ones without actual incoming connections to the process)
+
+ Your code should override the handle() method, doing whatever it needs to,
+ and calling get_or_create_application_instance with a unique `scope_id`
+ and `scope` for the scope it wants to get.
+
+ If an application instance is found with the same `scope_id`, you are
+ given its input queue, otherwise one is made for you with the scope provided
+ and you are given that fresh new input queue. Either way, you should do
+ something like:
+
+ input_queue = self.get_or_create_application_instance(
+ "user-123456",
+ {"type": "testprotocol", "user_id": "123456", "username": "andrew"},
+ )
+ input_queue.put_nowait(message)
+
+ If you try and create an application instance and there are already
+ `max_application` instances, the oldest/least recently used one will be
+ reclaimed and shut down to make space.
+
+ Application coroutines that error will be found periodically (every 100ms
+ by default) and have their exceptions printed to the console. Override
+ application_exception() if you want to do more when this happens.
+
+ If you override run(), make sure you handle things like launching the
+ application checker.
+ """
+
+ application_checker_interval = 0.1
+
+ def __init__(self, application, max_applications=1000):
+ # Parameters
+ self.application = application
+ self.max_applications = max_applications
+ # Initialisation
+ self.application_instances = {}
+
+ ### Mainloop and handling
+
+ def run(self):
+ """
+ Runs the asyncio event loop with our handler loop.
+ """
+ event_loop = asyncio.get_event_loop()
+ asyncio.ensure_future(self.application_checker())
+ try:
+ event_loop.run_until_complete(self.handle())
+ except KeyboardInterrupt:
+ logger.info("Exiting due to Ctrl-C/interrupt")
+
+ async def handle(self):
+ raise NotImplementedError("You must implement handle()")
+
+ async def application_send(self, scope, message):
+ """
+ Receives outbound sends from applications and handles them.
+ """
+ raise NotImplementedError("You must implement application_send()")
+
+ ### Application instance management
+
+ def get_or_create_application_instance(self, scope_id, scope):
+ """
+ Creates an application instance and returns its queue.
+ """
+ if scope_id in self.application_instances:
+ self.application_instances[scope_id]["last_used"] = time.time()
+ return self.application_instances[scope_id]["input_queue"]
+ # See if we need to delete an old one
+ while len(self.application_instances) > self.max_applications:
+ self.delete_oldest_application_instance()
+ # Make an instance of the application
+ input_queue = asyncio.Queue()
+ application_instance = guarantee_single_callable(self.application)
+ # Run it, and stash the future for later checking
+ future = asyncio.ensure_future(
+ application_instance(
+ scope=scope,
+ receive=input_queue.get,
+ send=lambda message: self.application_send(scope, message),
+ ),
+ )
+ self.application_instances[scope_id] = {
+ "input_queue": input_queue,
+ "future": future,
+ "scope": scope,
+ "last_used": time.time(),
+ }
+ return input_queue
+
+ def delete_oldest_application_instance(self):
+ """
+ Finds and deletes the oldest application instance
+ """
+ oldest_time = min(
+ details["last_used"] for details in self.application_instances.values()
+ )
+ for scope_id, details in self.application_instances.items():
+ if details["last_used"] == oldest_time:
+ self.delete_application_instance(scope_id)
+ # Return to make sure we only delete one in case two have
+ # the same oldest time
+ return
+
+ def delete_application_instance(self, scope_id):
+ """
+ Removes an application instance (makes sure its task is stopped,
+ then removes it from the current set)
+ """
+ details = self.application_instances[scope_id]
+ del self.application_instances[scope_id]
+ if not details["future"].done():
+ details["future"].cancel()
+
+ async def application_checker(self):
+ """
+ Goes through the set of current application instance Futures and cleans up
+ any that are done/prints exceptions for any that errored.
+ """
+ while True:
+ await asyncio.sleep(self.application_checker_interval)
+ for scope_id, details in list(self.application_instances.items()):
+ if details["future"].done():
+ exception = details["future"].exception()
+ if exception:
+ await self.application_exception(exception, details)
+ try:
+ del self.application_instances[scope_id]
+ except KeyError:
+ # Exception handling might have already got here before us. That's fine.
+ pass
+
+ async def application_exception(self, exception, application_details):
+ """
+ Called whenever an application coroutine has an exception.
+ """
+ logging.error(
+ "Exception inside application: %s\n%s%s",
+ exception,
+ "".join(traceback.format_tb(exception.__traceback__)),
+ f" {exception}",
+ )
diff --git a/.venv/lib/python3.12/site-packages/asgiref/sync.py b/.venv/lib/python3.12/site-packages/asgiref/sync.py
new file mode 100644
index 00000000..4427fc2a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/asgiref/sync.py
@@ -0,0 +1,613 @@
+import asyncio
+import asyncio.coroutines
+import contextvars
+import functools
+import inspect
+import os
+import sys
+import threading
+import warnings
+import weakref
+from concurrent.futures import Future, ThreadPoolExecutor
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Awaitable,
+ Callable,
+ Coroutine,
+ Dict,
+ Generic,
+ List,
+ Optional,
+ TypeVar,
+ Union,
+ overload,
+)
+
+from .current_thread_executor import CurrentThreadExecutor
+from .local import Local
+
+if sys.version_info >= (3, 10):
+ from typing import ParamSpec
+else:
+ from typing_extensions import ParamSpec
+
+if TYPE_CHECKING:
+ # This is not available to import at runtime
+ from _typeshed import OptExcInfo
+
+_F = TypeVar("_F", bound=Callable[..., Any])
+_P = ParamSpec("_P")
+_R = TypeVar("_R")
+
+
+def _restore_context(context: contextvars.Context) -> None:
+ # Check for changes in contextvars, and set them to the current
+ # context for downstream consumers
+ for cvar in context:
+ cvalue = context.get(cvar)
+ try:
+ if cvar.get() != cvalue:
+ cvar.set(cvalue)
+ except LookupError:
+ cvar.set(cvalue)
+
+
+# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for
+# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker.
+# The latter is replaced with the inspect.markcoroutinefunction decorator.
+# Until 3.12 is the minimum supported Python version, provide a shim.
+
+if hasattr(inspect, "markcoroutinefunction"):
+ iscoroutinefunction = inspect.iscoroutinefunction
+ markcoroutinefunction: Callable[[_F], _F] = inspect.markcoroutinefunction
+else:
+ iscoroutinefunction = asyncio.iscoroutinefunction # type: ignore[assignment]
+
+ def markcoroutinefunction(func: _F) -> _F:
+ func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore
+ return func
+
+
+class ThreadSensitiveContext:
+ """Async context manager to manage context for thread sensitive mode
+
+ This context manager controls which thread pool executor is used when in
+ thread sensitive mode. By default, a single thread pool executor is shared
+ within a process.
+
+ The ThreadSensitiveContext() context manager may be used to specify a
+ thread pool per context.
+
+ This context manager is re-entrant, so only the outer-most call to
+ ThreadSensitiveContext will set the context.
+
+ Usage:
+
+ >>> import time
+ >>> async with ThreadSensitiveContext():
+ ... await sync_to_async(time.sleep, 1)()
+ """
+
+ def __init__(self):
+ self.token = None
+
+ async def __aenter__(self):
+ try:
+ SyncToAsync.thread_sensitive_context.get()
+ except LookupError:
+ self.token = SyncToAsync.thread_sensitive_context.set(self)
+
+ return self
+
+ async def __aexit__(self, exc, value, tb):
+ if not self.token:
+ return
+
+ executor = SyncToAsync.context_to_thread_executor.pop(self, None)
+ if executor:
+ executor.shutdown()
+ SyncToAsync.thread_sensitive_context.reset(self.token)
+
+
+class AsyncToSync(Generic[_P, _R]):
+ """
+ Utility class which turns an awaitable that only works on the thread with
+ the event loop into a synchronous callable that works in a subthread.
+
+ If the call stack contains an async loop, the code runs there.
+ Otherwise, the code runs in a new loop in a new thread.
+
+ Either way, this thread then pauses and waits to run any thread_sensitive
+ code called from further down the call stack using SyncToAsync, before
+ finally exiting once the async task returns.
+ """
+
+ # Keeps a reference to the CurrentThreadExecutor in local context, so that
+ # any sync_to_async inside the wrapped code can find it.
+ executors: "Local" = Local()
+
+ # When we can't find a CurrentThreadExecutor from the context, such as
+ # inside create_task, we'll look it up here from the running event loop.
+ loop_thread_executors: "Dict[asyncio.AbstractEventLoop, CurrentThreadExecutor]" = {}
+
+ def __init__(
+ self,
+ awaitable: Union[
+ Callable[_P, Coroutine[Any, Any, _R]],
+ Callable[_P, Awaitable[_R]],
+ ],
+ force_new_loop: bool = False,
+ ):
+ if not callable(awaitable) or (
+ not iscoroutinefunction(awaitable)
+ and not iscoroutinefunction(getattr(awaitable, "__call__", awaitable))
+ ):
+ # Python does not have very reliable detection of async functions
+ # (lots of false negatives) so this is just a warning.
+ warnings.warn(
+ "async_to_sync was passed a non-async-marked callable", stacklevel=2
+ )
+ self.awaitable = awaitable
+ try:
+ self.__self__ = self.awaitable.__self__ # type: ignore[union-attr]
+ except AttributeError:
+ pass
+ self.force_new_loop = force_new_loop
+ self.main_event_loop = None
+ try:
+ self.main_event_loop = asyncio.get_running_loop()
+ except RuntimeError:
+ # There's no event loop in this thread.
+ pass
+
+ def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
+ __traceback_hide__ = True # noqa: F841
+
+ if not self.force_new_loop and not self.main_event_loop:
+ # There's no event loop in this thread. Look for the threadlocal if
+ # we're inside SyncToAsync
+ main_event_loop_pid = getattr(
+ SyncToAsync.threadlocal, "main_event_loop_pid", None
+ )
+ # We make sure the parent loop is from the same process - if
+ # they've forked, this is not going to be valid any more (#194)
+ if main_event_loop_pid and main_event_loop_pid == os.getpid():
+ self.main_event_loop = getattr(
+ SyncToAsync.threadlocal, "main_event_loop", None
+ )
+
+ # You can't call AsyncToSync from a thread with a running event loop
+ try:
+ event_loop = asyncio.get_running_loop()
+ except RuntimeError:
+ pass
+ else:
+ if event_loop.is_running():
+ raise RuntimeError(
+ "You cannot use AsyncToSync in the same thread as an async event loop - "
+ "just await the async function directly."
+ )
+
+ # Make a future for the return information
+ call_result: "Future[_R]" = Future()
+
+ # Make a CurrentThreadExecutor we'll use to idle in this thread - we
+ # need one for every sync frame, even if there's one above us in the
+ # same thread.
+ old_executor = getattr(self.executors, "current", None)
+ current_executor = CurrentThreadExecutor()
+ self.executors.current = current_executor
+
+ # Wrapping context in list so it can be reassigned from within
+ # `main_wrap`.
+ context = [contextvars.copy_context()]
+
+ # Get task context so that parent task knows which task to propagate
+ # an asyncio.CancelledError to.
+ task_context = getattr(SyncToAsync.threadlocal, "task_context", None)
+
+ loop = None
+ # Use call_soon_threadsafe to schedule a synchronous callback on the
+ # main event loop's thread if it's there, otherwise make a new loop
+ # in this thread.
+ try:
+ awaitable = self.main_wrap(
+ call_result,
+ sys.exc_info(),
+ task_context,
+ context,
+ *args,
+ **kwargs,
+ )
+
+ if not (self.main_event_loop and self.main_event_loop.is_running()):
+ # Make our own event loop - in a new thread - and run inside that.
+ loop = asyncio.new_event_loop()
+ self.loop_thread_executors[loop] = current_executor
+ loop_executor = ThreadPoolExecutor(max_workers=1)
+ loop_future = loop_executor.submit(
+ self._run_event_loop, loop, awaitable
+ )
+ if current_executor:
+ # Run the CurrentThreadExecutor until the future is done
+ current_executor.run_until_future(loop_future)
+ # Wait for future and/or allow for exception propagation
+ loop_future.result()
+ else:
+ # Call it inside the existing loop
+ self.main_event_loop.call_soon_threadsafe(
+ self.main_event_loop.create_task, awaitable
+ )
+ if current_executor:
+ # Run the CurrentThreadExecutor until the future is done
+ current_executor.run_until_future(call_result)
+ finally:
+ # Clean up any executor we were running
+ if loop is not None:
+ del self.loop_thread_executors[loop]
+ _restore_context(context[0])
+ # Restore old current thread executor state
+ self.executors.current = old_executor
+
+ # Wait for results from the future.
+ return call_result.result()
+
+ def _run_event_loop(self, loop, coro):
+ """
+ Runs the given event loop (designed to be called in a thread).
+ """
+ asyncio.set_event_loop(loop)
+ try:
+ loop.run_until_complete(coro)
+ finally:
+ try:
+ # mimic asyncio.run() behavior
+ # cancel unexhausted async generators
+ tasks = asyncio.all_tasks(loop)
+ for task in tasks:
+ task.cancel()
+
+ async def gather():
+ await asyncio.gather(*tasks, return_exceptions=True)
+
+ loop.run_until_complete(gather())
+ for task in tasks:
+ if task.cancelled():
+ continue
+ if task.exception() is not None:
+ loop.call_exception_handler(
+ {
+ "message": "unhandled exception during loop shutdown",
+ "exception": task.exception(),
+ "task": task,
+ }
+ )
+ if hasattr(loop, "shutdown_asyncgens"):
+ loop.run_until_complete(loop.shutdown_asyncgens())
+ finally:
+ loop.close()
+ asyncio.set_event_loop(self.main_event_loop)
+
+ def __get__(self, parent: Any, objtype: Any) -> Callable[_P, _R]:
+ """
+ Include self for methods
+ """
+ func = functools.partial(self.__call__, parent)
+ return functools.update_wrapper(func, self.awaitable)
+
+ async def main_wrap(
+ self,
+ call_result: "Future[_R]",
+ exc_info: "OptExcInfo",
+ task_context: "Optional[List[asyncio.Task[Any]]]",
+ context: List[contextvars.Context],
+ *args: _P.args,
+ **kwargs: _P.kwargs,
+ ) -> None:
+ """
+ Wraps the awaitable with something that puts the result into the
+ result/exception future.
+ """
+
+ __traceback_hide__ = True # noqa: F841
+
+ if context is not None:
+ _restore_context(context[0])
+
+ current_task = asyncio.current_task()
+ if current_task is not None and task_context is not None:
+ task_context.append(current_task)
+
+ try:
+ # If we have an exception, run the function inside the except block
+ # after raising it so exc_info is correctly populated.
+ if exc_info[1]:
+ try:
+ raise exc_info[1]
+ except BaseException:
+ result = await self.awaitable(*args, **kwargs)
+ else:
+ result = await self.awaitable(*args, **kwargs)
+ except BaseException as e:
+ call_result.set_exception(e)
+ else:
+ call_result.set_result(result)
+ finally:
+ if current_task is not None and task_context is not None:
+ task_context.remove(current_task)
+ context[0] = contextvars.copy_context()
+
+
+class SyncToAsync(Generic[_P, _R]):
+ """
+ Utility class which turns a synchronous callable into an awaitable that
+ runs in a threadpool. It also sets a threadlocal inside the thread so
+ calls to AsyncToSync can escape it.
+
+ If thread_sensitive is passed, the code will run in the same thread as any
+ outer code. This is needed for underlying Python code that is not
+ threadsafe (for example, code which handles SQLite database connections).
+
+ If the outermost program is async (i.e. SyncToAsync is outermost), then
+ this will be a dedicated single sub-thread that all sync code runs in,
+ one after the other. If the outermost program is sync (i.e. AsyncToSync is
+ outermost), this will just be the main thread. This is achieved by idling
+ with a CurrentThreadExecutor while AsyncToSync is blocking its sync parent,
+ rather than just blocking.
+
+ If executor is passed in, that will be used instead of the loop's default executor.
+ In order to pass in an executor, thread_sensitive must be set to False, otherwise
+ a TypeError will be raised.
+ """
+
+ # Storage for main event loop references
+ threadlocal = threading.local()
+
+ # Single-thread executor for thread-sensitive code
+ single_thread_executor = ThreadPoolExecutor(max_workers=1)
+
+ # Maintain a contextvar for the current execution context. Optionally used
+ # for thread sensitive mode.
+ thread_sensitive_context: "contextvars.ContextVar[ThreadSensitiveContext]" = (
+ contextvars.ContextVar("thread_sensitive_context")
+ )
+
+ # Contextvar that is used to detect if the single thread executor
+ # would be awaited on while already being used in the same context
+ deadlock_context: "contextvars.ContextVar[bool]" = contextvars.ContextVar(
+ "deadlock_context"
+ )
+
+ # Maintaining a weak reference to the context ensures that thread pools are
+ # erased once the context goes out of scope. This terminates the thread pool.
+ context_to_thread_executor: "weakref.WeakKeyDictionary[ThreadSensitiveContext, ThreadPoolExecutor]" = (
+ weakref.WeakKeyDictionary()
+ )
+
+ def __init__(
+ self,
+ func: Callable[_P, _R],
+ thread_sensitive: bool = True,
+ executor: Optional["ThreadPoolExecutor"] = None,
+ ) -> None:
+ if (
+ not callable(func)
+ or iscoroutinefunction(func)
+ or iscoroutinefunction(getattr(func, "__call__", func))
+ ):
+ raise TypeError("sync_to_async can only be applied to sync functions.")
+ self.func = func
+ functools.update_wrapper(self, func)
+ self._thread_sensitive = thread_sensitive
+ markcoroutinefunction(self)
+ if thread_sensitive and executor is not None:
+ raise TypeError("executor must not be set when thread_sensitive is True")
+ self._executor = executor
+ try:
+ self.__self__ = func.__self__ # type: ignore
+ except AttributeError:
+ pass
+
+ async def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
+ __traceback_hide__ = True # noqa: F841
+ loop = asyncio.get_running_loop()
+
+ # Work out what thread to run the code in
+ if self._thread_sensitive:
+ current_thread_executor = getattr(AsyncToSync.executors, "current", None)
+ if current_thread_executor:
+ # If we have a parent sync thread above somewhere, use that
+ executor = current_thread_executor
+ elif self.thread_sensitive_context.get(None):
+ # If we have a way of retrieving the current context, attempt
+ # to use a per-context thread pool executor
+ thread_sensitive_context = self.thread_sensitive_context.get()
+
+ if thread_sensitive_context in self.context_to_thread_executor:
+ # Re-use thread executor in current context
+ executor = self.context_to_thread_executor[thread_sensitive_context]
+ else:
+ # Create new thread executor in current context
+ executor = ThreadPoolExecutor(max_workers=1)
+ self.context_to_thread_executor[thread_sensitive_context] = executor
+ elif loop in AsyncToSync.loop_thread_executors:
+ # Re-use thread executor for running loop
+ executor = AsyncToSync.loop_thread_executors[loop]
+ elif self.deadlock_context.get(False):
+ raise RuntimeError(
+ "Single thread executor already being used, would deadlock"
+ )
+ else:
+ # Otherwise, we run it in a fixed single thread
+ executor = self.single_thread_executor
+ self.deadlock_context.set(True)
+ else:
+ # Use the passed in executor, or the loop's default if it is None
+ executor = self._executor
+
+ context = contextvars.copy_context()
+ child = functools.partial(self.func, *args, **kwargs)
+ func = context.run
+ task_context: List[asyncio.Task[Any]] = []
+
+ # Run the code in the right thread
+ exec_coro = loop.run_in_executor(
+ executor,
+ functools.partial(
+ self.thread_handler,
+ loop,
+ sys.exc_info(),
+ task_context,
+ func,
+ child,
+ ),
+ )
+ ret: _R
+ try:
+ ret = await asyncio.shield(exec_coro)
+ except asyncio.CancelledError:
+ cancel_parent = True
+ try:
+ task = task_context[0]
+ task.cancel()
+ try:
+ await task
+ cancel_parent = False
+ except asyncio.CancelledError:
+ pass
+ except IndexError:
+ pass
+ if exec_coro.done():
+ raise
+ if cancel_parent:
+ exec_coro.cancel()
+ ret = await exec_coro
+ finally:
+ _restore_context(context)
+ self.deadlock_context.set(False)
+
+ return ret
+
+ def __get__(
+ self, parent: Any, objtype: Any
+ ) -> Callable[_P, Coroutine[Any, Any, _R]]:
+ """
+ Include self for methods
+ """
+ func = functools.partial(self.__call__, parent)
+ return functools.update_wrapper(func, self.func)
+
+ def thread_handler(self, loop, exc_info, task_context, func, *args, **kwargs):
+ """
+ Wraps the sync application with exception handling.
+ """
+
+ __traceback_hide__ = True # noqa: F841
+
+ # Set the threadlocal for AsyncToSync
+ self.threadlocal.main_event_loop = loop
+ self.threadlocal.main_event_loop_pid = os.getpid()
+ self.threadlocal.task_context = task_context
+
+ # Run the function
+ # If we have an exception, run the function inside the except block
+ # after raising it so exc_info is correctly populated.
+ if exc_info[1]:
+ try:
+ raise exc_info[1]
+ except BaseException:
+ return func(*args, **kwargs)
+ else:
+ return func(*args, **kwargs)
+
+
+@overload
+def async_to_sync(
+ *,
+ force_new_loop: bool = False,
+) -> Callable[
+ [Union[Callable[_P, Coroutine[Any, Any, _R]], Callable[_P, Awaitable[_R]]]],
+ Callable[_P, _R],
+]:
+ ...
+
+
+@overload
+def async_to_sync(
+ awaitable: Union[
+ Callable[_P, Coroutine[Any, Any, _R]],
+ Callable[_P, Awaitable[_R]],
+ ],
+ *,
+ force_new_loop: bool = False,
+) -> Callable[_P, _R]:
+ ...
+
+
+def async_to_sync(
+ awaitable: Optional[
+ Union[
+ Callable[_P, Coroutine[Any, Any, _R]],
+ Callable[_P, Awaitable[_R]],
+ ]
+ ] = None,
+ *,
+ force_new_loop: bool = False,
+) -> Union[
+ Callable[
+ [Union[Callable[_P, Coroutine[Any, Any, _R]], Callable[_P, Awaitable[_R]]]],
+ Callable[_P, _R],
+ ],
+ Callable[_P, _R],
+]:
+ if awaitable is None:
+ return lambda f: AsyncToSync(
+ f,
+ force_new_loop=force_new_loop,
+ )
+ return AsyncToSync(
+ awaitable,
+ force_new_loop=force_new_loop,
+ )
+
+
+@overload
+def sync_to_async(
+ *,
+ thread_sensitive: bool = True,
+ executor: Optional["ThreadPoolExecutor"] = None,
+) -> Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]]:
+ ...
+
+
+@overload
+def sync_to_async(
+ func: Callable[_P, _R],
+ *,
+ thread_sensitive: bool = True,
+ executor: Optional["ThreadPoolExecutor"] = None,
+) -> Callable[_P, Coroutine[Any, Any, _R]]:
+ ...
+
+
+def sync_to_async(
+ func: Optional[Callable[_P, _R]] = None,
+ *,
+ thread_sensitive: bool = True,
+ executor: Optional["ThreadPoolExecutor"] = None,
+) -> Union[
+ Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]],
+ Callable[_P, Coroutine[Any, Any, _R]],
+]:
+ if func is None:
+ return lambda f: SyncToAsync(
+ f,
+ thread_sensitive=thread_sensitive,
+ executor=executor,
+ )
+ return SyncToAsync(
+ func,
+ thread_sensitive=thread_sensitive,
+ executor=executor,
+ )
diff --git a/.venv/lib/python3.12/site-packages/asgiref/testing.py b/.venv/lib/python3.12/site-packages/asgiref/testing.py
new file mode 100644
index 00000000..aa7cff1c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/asgiref/testing.py
@@ -0,0 +1,103 @@
+import asyncio
+import contextvars
+import time
+
+from .compatibility import guarantee_single_callable
+from .timeout import timeout as async_timeout
+
+
+class ApplicationCommunicator:
+ """
+ Runs an ASGI application in a test mode, allowing sending of
+ messages to it and retrieval of messages it sends.
+ """
+
+ def __init__(self, application, scope):
+ self.application = guarantee_single_callable(application)
+ self.scope = scope
+ self.input_queue = asyncio.Queue()
+ self.output_queue = asyncio.Queue()
+ # Clear context - this ensures that context vars set in the testing scope
+ # are not "leaked" into the application which would normally begin with
+ # an empty context. In Python >= 3.11 this could also be written as:
+ # asyncio.create_task(..., context=contextvars.Context())
+ self.future = contextvars.Context().run(
+ asyncio.create_task,
+ self.application(scope, self.input_queue.get, self.output_queue.put),
+ )
+
+ async def wait(self, timeout=1):
+ """
+ Waits for the application to stop itself and returns any exceptions.
+ """
+ try:
+ async with async_timeout(timeout):
+ try:
+ await self.future
+ self.future.result()
+ except asyncio.CancelledError:
+ pass
+ finally:
+ if not self.future.done():
+ self.future.cancel()
+ try:
+ await self.future
+ except asyncio.CancelledError:
+ pass
+
+ def stop(self, exceptions=True):
+ if not self.future.done():
+ self.future.cancel()
+ elif exceptions:
+ # Give a chance to raise any exceptions
+ self.future.result()
+
+ def __del__(self):
+ # Clean up on deletion
+ try:
+ self.stop(exceptions=False)
+ except RuntimeError:
+ # Event loop already stopped
+ pass
+
+ async def send_input(self, message):
+ """
+ Sends a single message to the application
+ """
+ # Give it the message
+ await self.input_queue.put(message)
+
+ async def receive_output(self, timeout=1):
+ """
+ Receives a single message from the application, with optional timeout.
+ """
+ # Make sure there's not an exception to raise from the task
+ if self.future.done():
+ self.future.result()
+ # Wait and receive the message
+ try:
+ async with async_timeout(timeout):
+ return await self.output_queue.get()
+ except asyncio.TimeoutError as e:
+ # See if we have another error to raise inside
+ if self.future.done():
+ self.future.result()
+ else:
+ self.future.cancel()
+ try:
+ await self.future
+ except asyncio.CancelledError:
+ pass
+ raise e
+
+ async def receive_nothing(self, timeout=0.1, interval=0.01):
+ """
+ Checks that there is no message to receive in the given time.
+ """
+ # `interval` has precedence over `timeout`
+ start = time.monotonic()
+ while time.monotonic() - start < timeout:
+ if not self.output_queue.empty():
+ return False
+ await asyncio.sleep(interval)
+ return self.output_queue.empty()
diff --git a/.venv/lib/python3.12/site-packages/asgiref/timeout.py b/.venv/lib/python3.12/site-packages/asgiref/timeout.py
new file mode 100644
index 00000000..fd5381d0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/asgiref/timeout.py
@@ -0,0 +1,118 @@
+# This code is originally sourced from the aio-libs project "async_timeout",
+# under the Apache 2.0 license. You may see the original project at
+# https://github.com/aio-libs/async-timeout
+
+# It is vendored here to reduce chain-dependencies on this library, and
+# modified slightly to remove some features we don't use.
+
+
+import asyncio
+import warnings
+from types import TracebackType
+from typing import Any # noqa
+from typing import Optional, Type
+
+
+class timeout:
+ """timeout context manager.
+
+ Useful in cases when you want to apply timeout logic around block
+ of code or in cases when asyncio.wait_for is not suitable. For example:
+
+ >>> with timeout(0.001):
+ ... async with aiohttp.get('https://github.com') as r:
+ ... await r.text()
+
+
+ timeout - value in seconds or None to disable timeout logic
+ loop - asyncio compatible event loop
+ """
+
+ def __init__(
+ self,
+ timeout: Optional[float],
+ *,
+ loop: Optional[asyncio.AbstractEventLoop] = None,
+ ) -> None:
+ self._timeout = timeout
+ if loop is None:
+ loop = asyncio.get_running_loop()
+ else:
+ warnings.warn(
+ """The loop argument to timeout() is deprecated.""", DeprecationWarning
+ )
+ self._loop = loop
+ self._task = None # type: Optional[asyncio.Task[Any]]
+ self._cancelled = False
+ self._cancel_handler = None # type: Optional[asyncio.Handle]
+ self._cancel_at = None # type: Optional[float]
+
+ def __enter__(self) -> "timeout":
+ return self._do_enter()
+
+ def __exit__(
+ self,
+ exc_type: Type[BaseException],
+ exc_val: BaseException,
+ exc_tb: TracebackType,
+ ) -> Optional[bool]:
+ self._do_exit(exc_type)
+ return None
+
+ async def __aenter__(self) -> "timeout":
+ return self._do_enter()
+
+ async def __aexit__(
+ self,
+ exc_type: Type[BaseException],
+ exc_val: BaseException,
+ exc_tb: TracebackType,
+ ) -> None:
+ self._do_exit(exc_type)
+
+ @property
+ def expired(self) -> bool:
+ return self._cancelled
+
+ @property
+ def remaining(self) -> Optional[float]:
+ if self._cancel_at is not None:
+ return max(self._cancel_at - self._loop.time(), 0.0)
+ else:
+ return None
+
+ def _do_enter(self) -> "timeout":
+ # Support Tornado 5- without timeout
+ # Details: https://github.com/python/asyncio/issues/392
+ if self._timeout is None:
+ return self
+
+ self._task = asyncio.current_task(self._loop)
+ if self._task is None:
+ raise RuntimeError(
+ "Timeout context manager should be used " "inside a task"
+ )
+
+ if self._timeout <= 0:
+ self._loop.call_soon(self._cancel_task)
+ return self
+
+ self._cancel_at = self._loop.time() + self._timeout
+ self._cancel_handler = self._loop.call_at(self._cancel_at, self._cancel_task)
+ return self
+
+ def _do_exit(self, exc_type: Type[BaseException]) -> None:
+ if exc_type is asyncio.CancelledError and self._cancelled:
+ self._cancel_handler = None
+ self._task = None
+ raise asyncio.TimeoutError
+ if self._timeout is not None and self._cancel_handler is not None:
+ self._cancel_handler.cancel()
+ self._cancel_handler = None
+ self._task = None
+ return None
+
+ def _cancel_task(self) -> None:
+ if self._task is not None:
+ self._task.cancel()
+ self._cancelled = True
diff --git a/.venv/lib/python3.12/site-packages/asgiref/typing.py b/.venv/lib/python3.12/site-packages/asgiref/typing.py
new file mode 100644
index 00000000..71c25ed8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/asgiref/typing.py
@@ -0,0 +1,278 @@
+import sys
+from typing import (
+ Any,
+ Awaitable,
+ Callable,
+ Dict,
+ Iterable,
+ Literal,
+ Optional,
+ Protocol,
+ Tuple,
+ Type,
+ TypedDict,
+ Union,
+)
+
+if sys.version_info >= (3, 11):
+ from typing import NotRequired
+else:
+ from typing_extensions import NotRequired
+
+__all__ = (
+ "ASGIVersions",
+ "HTTPScope",
+ "WebSocketScope",
+ "LifespanScope",
+ "WWWScope",
+ "Scope",
+ "HTTPRequestEvent",
+ "HTTPResponseStartEvent",
+ "HTTPResponseBodyEvent",
+ "HTTPResponseTrailersEvent",
+ "HTTPResponsePathsendEvent",
+ "HTTPServerPushEvent",
+ "HTTPDisconnectEvent",
+ "WebSocketConnectEvent",
+ "WebSocketAcceptEvent",
+ "WebSocketReceiveEvent",
+ "WebSocketSendEvent",
+ "WebSocketResponseStartEvent",
+ "WebSocketResponseBodyEvent",
+ "WebSocketDisconnectEvent",
+ "WebSocketCloseEvent",
+ "LifespanStartupEvent",
+ "LifespanShutdownEvent",
+ "LifespanStartupCompleteEvent",
+ "LifespanStartupFailedEvent",
+ "LifespanShutdownCompleteEvent",
+ "LifespanShutdownFailedEvent",
+ "ASGIReceiveEvent",
+ "ASGISendEvent",
+ "ASGIReceiveCallable",
+ "ASGISendCallable",
+ "ASGI2Protocol",
+ "ASGI2Application",
+ "ASGI3Application",
+ "ASGIApplication",
+)
+
+
+class ASGIVersions(TypedDict):
+ spec_version: str
+ version: Union[Literal["2.0"], Literal["3.0"]]
+
+
+class HTTPScope(TypedDict):
+ type: Literal["http"]
+ asgi: ASGIVersions
+ http_version: str
+ method: str
+ scheme: str
+ path: str
+ raw_path: bytes
+ query_string: bytes
+ root_path: str
+ headers: Iterable[Tuple[bytes, bytes]]
+ client: Optional[Tuple[str, int]]
+ server: Optional[Tuple[str, Optional[int]]]
+ state: NotRequired[Dict[str, Any]]
+ extensions: Optional[Dict[str, Dict[object, object]]]
+
+
+class WebSocketScope(TypedDict):
+ type: Literal["websocket"]
+ asgi: ASGIVersions
+ http_version: str
+ scheme: str
+ path: str
+ raw_path: bytes
+ query_string: bytes
+ root_path: str
+ headers: Iterable[Tuple[bytes, bytes]]
+ client: Optional[Tuple[str, int]]
+ server: Optional[Tuple[str, Optional[int]]]
+ subprotocols: Iterable[str]
+ state: NotRequired[Dict[str, Any]]
+ extensions: Optional[Dict[str, Dict[object, object]]]
+
+
+class LifespanScope(TypedDict):
+ type: Literal["lifespan"]
+ asgi: ASGIVersions
+ state: NotRequired[Dict[str, Any]]
+
+
+WWWScope = Union[HTTPScope, WebSocketScope]
+Scope = Union[HTTPScope, WebSocketScope, LifespanScope]
+
+
+class HTTPRequestEvent(TypedDict):
+ type: Literal["http.request"]
+ body: bytes
+ more_body: bool
+
+
+class HTTPResponseDebugEvent(TypedDict):
+ type: Literal["http.response.debug"]
+ info: Dict[str, object]
+
+
+class HTTPResponseStartEvent(TypedDict):
+ type: Literal["http.response.start"]
+ status: int
+ headers: Iterable[Tuple[bytes, bytes]]
+ trailers: bool
+
+
+class HTTPResponseBodyEvent(TypedDict):
+ type: Literal["http.response.body"]
+ body: bytes
+ more_body: bool
+
+
+class HTTPResponseTrailersEvent(TypedDict):
+ type: Literal["http.response.trailers"]
+ headers: Iterable[Tuple[bytes, bytes]]
+ more_trailers: bool
+
+
+class HTTPResponsePathsendEvent(TypedDict):
+ type: Literal["http.response.pathsend"]
+ path: str
+
+
+class HTTPServerPushEvent(TypedDict):
+ type: Literal["http.response.push"]
+ path: str
+ headers: Iterable[Tuple[bytes, bytes]]
+
+
+class HTTPDisconnectEvent(TypedDict):
+ type: Literal["http.disconnect"]
+
+
+class WebSocketConnectEvent(TypedDict):
+ type: Literal["websocket.connect"]
+
+
+class WebSocketAcceptEvent(TypedDict):
+ type: Literal["websocket.accept"]
+ subprotocol: Optional[str]
+ headers: Iterable[Tuple[bytes, bytes]]
+
+
+class WebSocketReceiveEvent(TypedDict):
+ type: Literal["websocket.receive"]
+ bytes: Optional[bytes]
+ text: Optional[str]
+
+
+class WebSocketSendEvent(TypedDict):
+ type: Literal["websocket.send"]
+ bytes: Optional[bytes]
+ text: Optional[str]
+
+
+class WebSocketResponseStartEvent(TypedDict):
+ type: Literal["websocket.http.response.start"]
+ status: int
+ headers: Iterable[Tuple[bytes, bytes]]
+
+
+class WebSocketResponseBodyEvent(TypedDict):
+ type: Literal["websocket.http.response.body"]
+ body: bytes
+ more_body: bool
+
+
+class WebSocketDisconnectEvent(TypedDict):
+ type: Literal["websocket.disconnect"]
+ code: int
+
+
+class WebSocketCloseEvent(TypedDict):
+ type: Literal["websocket.close"]
+ code: int
+ reason: Optional[str]
+
+
+class LifespanStartupEvent(TypedDict):
+ type: Literal["lifespan.startup"]
+
+
+class LifespanShutdownEvent(TypedDict):
+ type: Literal["lifespan.shutdown"]
+
+
+class LifespanStartupCompleteEvent(TypedDict):
+ type: Literal["lifespan.startup.complete"]
+
+
+class LifespanStartupFailedEvent(TypedDict):
+ type: Literal["lifespan.startup.failed"]
+ message: str
+
+
+class LifespanShutdownCompleteEvent(TypedDict):
+ type: Literal["lifespan.shutdown.complete"]
+
+
+class LifespanShutdownFailedEvent(TypedDict):
+ type: Literal["lifespan.shutdown.failed"]
+ message: str
+
+
+ASGIReceiveEvent = Union[
+ HTTPRequestEvent,
+ HTTPDisconnectEvent,
+ WebSocketConnectEvent,
+ WebSocketReceiveEvent,
+ WebSocketDisconnectEvent,
+ LifespanStartupEvent,
+ LifespanShutdownEvent,
+]
+
+
+ASGISendEvent = Union[
+ HTTPResponseStartEvent,
+ HTTPResponseBodyEvent,
+ HTTPResponseTrailersEvent,
+ HTTPServerPushEvent,
+ HTTPDisconnectEvent,
+ WebSocketAcceptEvent,
+ WebSocketSendEvent,
+ WebSocketResponseStartEvent,
+ WebSocketResponseBodyEvent,
+ WebSocketCloseEvent,
+ LifespanStartupCompleteEvent,
+ LifespanStartupFailedEvent,
+ LifespanShutdownCompleteEvent,
+ LifespanShutdownFailedEvent,
+]
+
+
+ASGIReceiveCallable = Callable[[], Awaitable[ASGIReceiveEvent]]
+ASGISendCallable = Callable[[ASGISendEvent], Awaitable[None]]
+
+
+class ASGI2Protocol(Protocol):
+ def __init__(self, scope: Scope) -> None:
+ ...
+
+ async def __call__(
+ self, receive: ASGIReceiveCallable, send: ASGISendCallable
+ ) -> None:
+ ...
+
+
+ASGI2Application = Type[ASGI2Protocol]
+ASGI3Application = Callable[
+ [
+ Scope,
+ ASGIReceiveCallable,
+ ASGISendCallable,
+ ],
+ Awaitable[None],
+]
+ASGIApplication = Union[ASGI2Application, ASGI3Application]
diff --git a/.venv/lib/python3.12/site-packages/asgiref/wsgi.py b/.venv/lib/python3.12/site-packages/asgiref/wsgi.py
new file mode 100644
index 00000000..65af4279
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/asgiref/wsgi.py
@@ -0,0 +1,166 @@
+from io import BytesIO
+from tempfile import SpooledTemporaryFile
+
+from asgiref.sync import AsyncToSync, sync_to_async
+
+
+class WsgiToAsgi:
+ """
+ Wraps a WSGI application to make it into an ASGI application.
+ """
+
+ def __init__(self, wsgi_application):
+ self.wsgi_application = wsgi_application
+
+ async def __call__(self, scope, receive, send):
+ """
+ ASGI application instantiation point.
+ We return a new WsgiToAsgiInstance here with the WSGI app
+ and the scope, ready to respond when it is __call__ed.
+ """
+ await WsgiToAsgiInstance(self.wsgi_application)(scope, receive, send)
+
+
+class WsgiToAsgiInstance:
+ """
+ Per-socket instance of a wrapped WSGI application
+ """
+
+ def __init__(self, wsgi_application):
+ self.wsgi_application = wsgi_application
+ self.response_started = False
+ self.response_content_length = None
+
+ async def __call__(self, scope, receive, send):
+ if scope["type"] != "http":
+ raise ValueError("WSGI wrapper received a non-HTTP scope")
+ self.scope = scope
+ with SpooledTemporaryFile(max_size=65536) as body:
+ # Alright, wait for the http.request messages
+ while True:
+ message = await receive()
+ if message["type"] != "http.request":
+ raise ValueError("WSGI wrapper received a non-HTTP-request message")
+ body.write(message.get("body", b""))
+ if not message.get("more_body"):
+ break
+ body.seek(0)
+ # Wrap send so it can be called from the subthread
+ self.sync_send = AsyncToSync(send)
+ # Call the WSGI app
+ await self.run_wsgi_app(body)
+
+ def build_environ(self, scope, body):
+ """
+ Builds a scope and request body into a WSGI environ object.
+ """
+ script_name = scope.get("root_path", "").encode("utf8").decode("latin1")
+ path_info = scope["path"].encode("utf8").decode("latin1")
+ if path_info.startswith(script_name):
+ path_info = path_info[len(script_name) :]
+ environ = {
+ "REQUEST_METHOD": scope["method"],
+ "SCRIPT_NAME": script_name,
+ "PATH_INFO": path_info,
+ "QUERY_STRING": scope["query_string"].decode("ascii"),
+ "SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"],
+ "wsgi.version": (1, 0),
+ "wsgi.url_scheme": scope.get("scheme", "http"),
+ "wsgi.input": body,
+ "wsgi.errors": BytesIO(),
+ "wsgi.multithread": True,
+ "wsgi.multiprocess": True,
+ "wsgi.run_once": False,
+ }
+ # Get server name and port - required in WSGI, not in ASGI
+ if "server" in scope:
+ environ["SERVER_NAME"] = scope["server"][0]
+ environ["SERVER_PORT"] = str(scope["server"][1])
+ else:
+ environ["SERVER_NAME"] = "localhost"
+ environ["SERVER_PORT"] = "80"
+
+ if scope.get("client") is not None:
+ environ["REMOTE_ADDR"] = scope["client"][0]
+
+ # Go through headers and make them into environ entries
+ for name, value in self.scope.get("headers", []):
+ name = name.decode("latin1")
+ if name == "content-length":
+ corrected_name = "CONTENT_LENGTH"
+ elif name == "content-type":
+ corrected_name = "CONTENT_TYPE"
+ else:
+ corrected_name = "HTTP_%s" % name.upper().replace("-", "_")
+ # HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in case
+ value = value.decode("latin1")
+ if corrected_name in environ:
+ value = environ[corrected_name] + "," + value
+ environ[corrected_name] = value
+ return environ
+
+ def start_response(self, status, response_headers, exc_info=None):
+ """
+ WSGI start_response callable.
+ """
+ # Don't allow re-calling once response has begun
+ if self.response_started:
+ raise exc_info[1].with_traceback(exc_info[2])
+ # Don't allow re-calling without exc_info
+ if hasattr(self, "response_start") and exc_info is None:
+ raise ValueError(
+ "You cannot call start_response a second time without exc_info"
+ )
+ # Extract status code
+ status_code, _ = status.split(" ", 1)
+ status_code = int(status_code)
+ # Extract headers
+ headers = [
+ (name.lower().encode("ascii"), value.encode("ascii"))
+ for name, value in response_headers
+ ]
+ # Extract content-length
+ self.response_content_length = None
+ for name, value in response_headers:
+ if name.lower() == "content-length":
+ self.response_content_length = int(value)
+ # Build and send response start message.
+ self.response_start = {
+ "type": "http.response.start",
+ "status": status_code,
+ "headers": headers,
+ }
+
+ @sync_to_async
+ def run_wsgi_app(self, body):
+ """
+ Called in a subthread to run the WSGI app. We encapsulate like
+ this so that the start_response callable is called in the same thread.
+ """
+ # Translate the scope and incoming request body into a WSGI environ
+ environ = self.build_environ(self.scope, body)
+ # Run the WSGI app
+ bytes_sent = 0
+ for output in self.wsgi_application(environ, self.start_response):
+ # If this is the first response, include the response headers
+ if not self.response_started:
+ self.response_started = True
+ self.sync_send(self.response_start)
+ # If the application supplies a Content-Length header
+ if self.response_content_length is not None:
+ # The server should not transmit more bytes to the client than the header allows
+ bytes_allowed = self.response_content_length - bytes_sent
+ if len(output) > bytes_allowed:
+ output = output[:bytes_allowed]
+ self.sync_send(
+ {"type": "http.response.body", "body": output, "more_body": True}
+ )
+ bytes_sent += len(output)
+ # The server should stop iterating over the response when enough data has been sent
+ if bytes_sent == self.response_content_length:
+ break
+ # Close connection
+ if not self.response_started:
+ self.response_started = True
+ self.sync_send(self.response_start)
+ self.sync_send({"type": "http.response.body"})