"
+
diff --git a/core/flaskui/templates/chats.html b/core/flaskui/templates/chats.html
new file mode 100644
index 0000000..0ad5ba1
--- /dev/null
+++ b/core/flaskui/templates/chats.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Страница чатов
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/flaskui/templates/index.html b/core/flaskui/templates/index.html
new file mode 100644
index 0000000..6d93ea5
--- /dev/null
+++ b/core/flaskui/templates/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+ Главная страница
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/main.py b/main.py
index 357d36c..7dd6e30 100644
--- a/main.py
+++ b/main.py
@@ -1,10 +1,5 @@
import sys
-from PySide6.QtWidgets import QApplication
-from core.ui.uimain import MainWindow
-
+from core.flaskui.flask import ui
if __name__ == "__main__":
- app = QApplication(sys.argv)
- window = MainWindow()
- window.show()
- sys.exit(app.exec())
\ No newline at end of file
+ ui.run(debug=True)
\ No newline at end of file
diff --git a/venv/Lib/site-packages/blinker-1.9.0.dist-info/INSTALLER b/venv/Lib/site-packages/blinker-1.9.0.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/Lib/site-packages/blinker-1.9.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/blinker-1.9.0.dist-info/LICENSE.txt b/venv/Lib/site-packages/blinker-1.9.0.dist-info/LICENSE.txt
new file mode 100644
index 0000000..79c9825
--- /dev/null
+++ b/venv/Lib/site-packages/blinker-1.9.0.dist-info/LICENSE.txt
@@ -0,0 +1,20 @@
+Copyright 2010 Jason Kirtland
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/venv/Lib/site-packages/blinker-1.9.0.dist-info/METADATA b/venv/Lib/site-packages/blinker-1.9.0.dist-info/METADATA
new file mode 100644
index 0000000..6d343f5
--- /dev/null
+++ b/venv/Lib/site-packages/blinker-1.9.0.dist-info/METADATA
@@ -0,0 +1,60 @@
+Metadata-Version: 2.3
+Name: blinker
+Version: 1.9.0
+Summary: Fast, simple object-to-object and broadcast signaling
+Author: Jason Kirtland
+Maintainer-email: Pallets Ecosystem
+Requires-Python: >=3.9
+Description-Content-Type: text/markdown
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python
+Classifier: Typing :: Typed
+Project-URL: Chat, https://discord.gg/pallets
+Project-URL: Documentation, https://blinker.readthedocs.io
+Project-URL: Source, https://github.com/pallets-eco/blinker/
+
+# Blinker
+
+Blinker provides a fast dispatching system that allows any number of
+interested parties to subscribe to events, or "signals".
+
+
+## Pallets Community Ecosystem
+
+> [!IMPORTANT]\
+> This project is part of the Pallets Community Ecosystem. Pallets is the open
+> source organization that maintains Flask; Pallets-Eco enables community
+> maintenance of related projects. If you are interested in helping maintain
+> this project, please reach out on [the Pallets Discord server][discord].
+>
+> [discord]: https://discord.gg/pallets
+
+
+## Example
+
+Signal receivers can subscribe to specific senders or receive signals
+sent by any sender.
+
+```pycon
+>>> from blinker import signal
+>>> started = signal('round-started')
+>>> def each(round):
+... print(f"Round {round}")
+...
+>>> started.connect(each)
+
+>>> def round_two(round):
+... print("This is round two.")
+...
+>>> started.connect(round_two, sender=2)
+
+>>> for round in range(1, 4):
+... started.send(round)
+...
+Round 1!
+Round 2!
+This is round two.
+Round 3!
+```
+
diff --git a/venv/Lib/site-packages/blinker-1.9.0.dist-info/RECORD b/venv/Lib/site-packages/blinker-1.9.0.dist-info/RECORD
new file mode 100644
index 0000000..e6ed9e3
--- /dev/null
+++ b/venv/Lib/site-packages/blinker-1.9.0.dist-info/RECORD
@@ -0,0 +1,12 @@
+blinker-1.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+blinker-1.9.0.dist-info/LICENSE.txt,sha256=nrc6HzhZekqhcCXSrhvjg5Ykx5XphdTw6Xac4p-spGc,1054
+blinker-1.9.0.dist-info/METADATA,sha256=uIRiM8wjjbHkCtbCyTvctU37IAZk0kEe5kxAld1dvzA,1633
+blinker-1.9.0.dist-info/RECORD,,
+blinker-1.9.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
+blinker/__init__.py,sha256=I2EdZqpy4LyjX17Hn1yzJGWCjeLaVaPzsMgHkLfj_cQ,317
+blinker/__pycache__/__init__.cpython-310.pyc,,
+blinker/__pycache__/_utilities.cpython-310.pyc,,
+blinker/__pycache__/base.cpython-310.pyc,,
+blinker/_utilities.py,sha256=0J7eeXXTUx0Ivf8asfpx0ycVkp0Eqfqnj117x2mYX9E,1675
+blinker/base.py,sha256=QpDuvXXcwJF49lUBcH5BiST46Rz9wSG7VW_p7N_027M,19132
+blinker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
diff --git a/venv/Lib/site-packages/blinker-1.9.0.dist-info/WHEEL b/venv/Lib/site-packages/blinker-1.9.0.dist-info/WHEEL
new file mode 100644
index 0000000..e3c6fee
--- /dev/null
+++ b/venv/Lib/site-packages/blinker-1.9.0.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 1.0
+Generator: flit 3.10.1
+Root-Is-Purelib: true
+Tag: py3-none-any
diff --git a/venv/Lib/site-packages/blinker/__init__.py b/venv/Lib/site-packages/blinker/__init__.py
new file mode 100644
index 0000000..1772fa4
--- /dev/null
+++ b/venv/Lib/site-packages/blinker/__init__.py
@@ -0,0 +1,17 @@
+from __future__ import annotations
+
+from .base import ANY
+from .base import default_namespace
+from .base import NamedSignal
+from .base import Namespace
+from .base import Signal
+from .base import signal
+
+__all__ = [
+ "ANY",
+ "default_namespace",
+ "NamedSignal",
+ "Namespace",
+ "Signal",
+ "signal",
+]
diff --git a/venv/Lib/site-packages/blinker/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/blinker/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..5a4fd5f
Binary files /dev/null and b/venv/Lib/site-packages/blinker/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/blinker/__pycache__/_utilities.cpython-310.pyc b/venv/Lib/site-packages/blinker/__pycache__/_utilities.cpython-310.pyc
new file mode 100644
index 0000000..b95828e
Binary files /dev/null and b/venv/Lib/site-packages/blinker/__pycache__/_utilities.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/blinker/__pycache__/base.cpython-310.pyc b/venv/Lib/site-packages/blinker/__pycache__/base.cpython-310.pyc
new file mode 100644
index 0000000..80b6577
Binary files /dev/null and b/venv/Lib/site-packages/blinker/__pycache__/base.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/blinker/_utilities.py b/venv/Lib/site-packages/blinker/_utilities.py
new file mode 100644
index 0000000..000c902
--- /dev/null
+++ b/venv/Lib/site-packages/blinker/_utilities.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+import collections.abc as c
+import inspect
+import typing as t
+from weakref import ref
+from weakref import WeakMethod
+
+T = t.TypeVar("T")
+
+
+class Symbol:
+ """A constant symbol, nicer than ``object()``. Repeated calls return the
+ same instance.
+
+ >>> Symbol('foo') is Symbol('foo')
+ True
+ >>> Symbol('foo')
+ foo
+ """
+
+ symbols: t.ClassVar[dict[str, Symbol]] = {}
+
+ def __new__(cls, name: str) -> Symbol:
+ if name in cls.symbols:
+ return cls.symbols[name]
+
+ obj = super().__new__(cls)
+ cls.symbols[name] = obj
+ return obj
+
+ def __init__(self, name: str) -> None:
+ self.name = name
+
+ def __repr__(self) -> str:
+ return self.name
+
+ def __getnewargs__(self) -> tuple[t.Any, ...]:
+ return (self.name,)
+
+
+def make_id(obj: object) -> c.Hashable:
+ """Get a stable identifier for a receiver or sender, to be used as a dict
+ key or in a set.
+ """
+ if inspect.ismethod(obj):
+ # The id of a bound method is not stable, but the id of the unbound
+ # function and instance are.
+ return id(obj.__func__), id(obj.__self__)
+
+ if isinstance(obj, (str, int)):
+ # Instances with the same value always compare equal and have the same
+ # hash, even if the id may change.
+ return obj
+
+ # Assume other types are not hashable but will always be the same instance.
+ return id(obj)
+
+
+def make_ref(obj: T, callback: c.Callable[[ref[T]], None] | None = None) -> ref[T]:
+ if inspect.ismethod(obj):
+ return WeakMethod(obj, callback) # type: ignore[arg-type, return-value]
+
+ return ref(obj, callback)
diff --git a/venv/Lib/site-packages/blinker/base.py b/venv/Lib/site-packages/blinker/base.py
new file mode 100644
index 0000000..d051b94
--- /dev/null
+++ b/venv/Lib/site-packages/blinker/base.py
@@ -0,0 +1,512 @@
+from __future__ import annotations
+
+import collections.abc as c
+import sys
+import typing as t
+import weakref
+from collections import defaultdict
+from contextlib import contextmanager
+from functools import cached_property
+from inspect import iscoroutinefunction
+
+from ._utilities import make_id
+from ._utilities import make_ref
+from ._utilities import Symbol
+
+F = t.TypeVar("F", bound=c.Callable[..., t.Any])
+
+ANY = Symbol("ANY")
+"""Symbol for "any sender"."""
+
+ANY_ID = 0
+
+
+class Signal:
+ """A notification emitter.
+
+ :param doc: The docstring for the signal.
+ """
+
+ ANY = ANY
+ """An alias for the :data:`~blinker.ANY` sender symbol."""
+
+ set_class: type[set[t.Any]] = set
+ """The set class to use for tracking connected receivers and senders.
+ Python's ``set`` is unordered. If receivers must be dispatched in the order
+ they were connected, an ordered set implementation can be used.
+
+ .. versionadded:: 1.7
+ """
+
+ @cached_property
+ def receiver_connected(self) -> Signal:
+ """Emitted at the end of each :meth:`connect` call.
+
+ The signal sender is the signal instance, and the :meth:`connect`
+ arguments are passed through: ``receiver``, ``sender``, and ``weak``.
+
+ .. versionadded:: 1.2
+ """
+ return Signal(doc="Emitted after a receiver connects.")
+
+ @cached_property
+ def receiver_disconnected(self) -> Signal:
+ """Emitted at the end of each :meth:`disconnect` call.
+
+ The sender is the signal instance, and the :meth:`disconnect` arguments
+ are passed through: ``receiver`` and ``sender``.
+
+ This signal is emitted **only** when :meth:`disconnect` is called
+ explicitly. This signal cannot be emitted by an automatic disconnect
+ when a weakly referenced receiver or sender goes out of scope, as the
+ instance is no longer be available to be used as the sender for this
+ signal.
+
+ An alternative approach is available by subscribing to
+ :attr:`receiver_connected` and setting up a custom weakref cleanup
+ callback on weak receivers and senders.
+
+ .. versionadded:: 1.2
+ """
+ return Signal(doc="Emitted after a receiver disconnects.")
+
+ def __init__(self, doc: str | None = None) -> None:
+ if doc:
+ self.__doc__ = doc
+
+ self.receivers: dict[
+ t.Any, weakref.ref[c.Callable[..., t.Any]] | c.Callable[..., t.Any]
+ ] = {}
+ """The map of connected receivers. Useful to quickly check if any
+ receivers are connected to the signal: ``if s.receivers:``. The
+ structure and data is not part of the public API, but checking its
+ boolean value is.
+ """
+
+ self.is_muted: bool = False
+ self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)
+ self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)
+ self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {}
+
+ def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F:
+ """Connect ``receiver`` to be called when the signal is sent by
+ ``sender``.
+
+ :param receiver: The callable to call when :meth:`send` is called with
+ the given ``sender``, passing ``sender`` as a positional argument
+ along with any extra keyword arguments.
+ :param sender: Any object or :data:`ANY`. ``receiver`` will only be
+ called when :meth:`send` is called with this sender. If ``ANY``, the
+ receiver will be called for any sender. A receiver may be connected
+ to multiple senders by calling :meth:`connect` multiple times.
+ :param weak: Track the receiver with a :mod:`weakref`. The receiver will
+ be automatically disconnected when it is garbage collected. When
+ connecting a receiver defined within a function, set to ``False``,
+ otherwise it will be disconnected when the function scope ends.
+ """
+ receiver_id = make_id(receiver)
+ sender_id = ANY_ID if sender is ANY else make_id(sender)
+
+ if weak:
+ self.receivers[receiver_id] = make_ref(
+ receiver, self._make_cleanup_receiver(receiver_id)
+ )
+ else:
+ self.receivers[receiver_id] = receiver
+
+ self._by_sender[sender_id].add(receiver_id)
+ self._by_receiver[receiver_id].add(sender_id)
+
+ if sender is not ANY and sender_id not in self._weak_senders:
+ # store a cleanup for weakref-able senders
+ try:
+ self._weak_senders[sender_id] = make_ref(
+ sender, self._make_cleanup_sender(sender_id)
+ )
+ except TypeError:
+ pass
+
+ if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers:
+ try:
+ self.receiver_connected.send(
+ self, receiver=receiver, sender=sender, weak=weak
+ )
+ except TypeError:
+ # TODO no explanation or test for this
+ self.disconnect(receiver, sender)
+ raise
+
+ return receiver
+
+ def connect_via(self, sender: t.Any, weak: bool = False) -> c.Callable[[F], F]:
+ """Connect the decorated function to be called when the signal is sent
+ by ``sender``.
+
+ The decorated function will be called when :meth:`send` is called with
+ the given ``sender``, passing ``sender`` as a positional argument along
+ with any extra keyword arguments.
+
+ :param sender: Any object or :data:`ANY`. ``receiver`` will only be
+ called when :meth:`send` is called with this sender. If ``ANY``, the
+ receiver will be called for any sender. A receiver may be connected
+ to multiple senders by calling :meth:`connect` multiple times.
+ :param weak: Track the receiver with a :mod:`weakref`. The receiver will
+ be automatically disconnected when it is garbage collected. When
+ connecting a receiver defined within a function, set to ``False``,
+ otherwise it will be disconnected when the function scope ends.=
+
+ .. versionadded:: 1.1
+ """
+
+ def decorator(fn: F) -> F:
+ self.connect(fn, sender, weak)
+ return fn
+
+ return decorator
+
+ @contextmanager
+ def connected_to(
+ self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY
+ ) -> c.Generator[None, None, None]:
+ """A context manager that temporarily connects ``receiver`` to the
+ signal while a ``with`` block executes. When the block exits, the
+ receiver is disconnected. Useful for tests.
+
+ :param receiver: The callable to call when :meth:`send` is called with
+ the given ``sender``, passing ``sender`` as a positional argument
+ along with any extra keyword arguments.
+ :param sender: Any object or :data:`ANY`. ``receiver`` will only be
+ called when :meth:`send` is called with this sender. If ``ANY``, the
+ receiver will be called for any sender.
+
+ .. versionadded:: 1.1
+ """
+ self.connect(receiver, sender=sender, weak=False)
+
+ try:
+ yield None
+ finally:
+ self.disconnect(receiver)
+
+ @contextmanager
+ def muted(self) -> c.Generator[None, None, None]:
+ """A context manager that temporarily disables the signal. No receivers
+ will be called if the signal is sent, until the ``with`` block exits.
+ Useful for tests.
+ """
+ self.is_muted = True
+
+ try:
+ yield None
+ finally:
+ self.is_muted = False
+
+ def send(
+ self,
+ sender: t.Any | None = None,
+ /,
+ *,
+ _async_wrapper: c.Callable[
+ [c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]], c.Callable[..., t.Any]
+ ]
+ | None = None,
+ **kwargs: t.Any,
+ ) -> list[tuple[c.Callable[..., t.Any], t.Any]]:
+ """Call all receivers that are connected to the given ``sender``
+ or :data:`ANY`. Each receiver is called with ``sender`` as a positional
+ argument along with any extra keyword arguments. Return a list of
+ ``(receiver, return value)`` tuples.
+
+ The order receivers are called is undefined, but can be influenced by
+ setting :attr:`set_class`.
+
+ If a receiver raises an exception, that exception will propagate up.
+ This makes debugging straightforward, with an assumption that correctly
+ implemented receivers will not raise.
+
+ :param sender: Call receivers connected to this sender, in addition to
+ those connected to :data:`ANY`.
+ :param _async_wrapper: Will be called on any receivers that are async
+ coroutines to turn them into sync callables. For example, could run
+ the receiver with an event loop.
+ :param kwargs: Extra keyword arguments to pass to each receiver.
+
+ .. versionchanged:: 1.7
+ Added the ``_async_wrapper`` argument.
+ """
+ if self.is_muted:
+ return []
+
+ results = []
+
+ for receiver in self.receivers_for(sender):
+ if iscoroutinefunction(receiver):
+ if _async_wrapper is None:
+ raise RuntimeError("Cannot send to a coroutine function.")
+
+ result = _async_wrapper(receiver)(sender, **kwargs)
+ else:
+ result = receiver(sender, **kwargs)
+
+ results.append((receiver, result))
+
+ return results
+
+ async def send_async(
+ self,
+ sender: t.Any | None = None,
+ /,
+ *,
+ _sync_wrapper: c.Callable[
+ [c.Callable[..., t.Any]], c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]
+ ]
+ | None = None,
+ **kwargs: t.Any,
+ ) -> list[tuple[c.Callable[..., t.Any], t.Any]]:
+ """Await all receivers that are connected to the given ``sender``
+ or :data:`ANY`. Each receiver is called with ``sender`` as a positional
+ argument along with any extra keyword arguments. Return a list of
+ ``(receiver, return value)`` tuples.
+
+ The order receivers are called is undefined, but can be influenced by
+ setting :attr:`set_class`.
+
+ If a receiver raises an exception, that exception will propagate up.
+ This makes debugging straightforward, with an assumption that correctly
+ implemented receivers will not raise.
+
+ :param sender: Call receivers connected to this sender, in addition to
+ those connected to :data:`ANY`.
+ :param _sync_wrapper: Will be called on any receivers that are sync
+ callables to turn them into async coroutines. For example,
+ could call the receiver in a thread.
+ :param kwargs: Extra keyword arguments to pass to each receiver.
+
+ .. versionadded:: 1.7
+ """
+ if self.is_muted:
+ return []
+
+ results = []
+
+ for receiver in self.receivers_for(sender):
+ if not iscoroutinefunction(receiver):
+ if _sync_wrapper is None:
+ raise RuntimeError("Cannot send to a non-coroutine function.")
+
+ result = await _sync_wrapper(receiver)(sender, **kwargs)
+ else:
+ result = await receiver(sender, **kwargs)
+
+ results.append((receiver, result))
+
+ return results
+
+ def has_receivers_for(self, sender: t.Any) -> bool:
+ """Check if there is at least one receiver that will be called with the
+ given ``sender``. A receiver connected to :data:`ANY` will always be
+ called, regardless of sender. Does not check if weakly referenced
+ receivers are still live. See :meth:`receivers_for` for a stronger
+ search.
+
+ :param sender: Check for receivers connected to this sender, in addition
+ to those connected to :data:`ANY`.
+ """
+ if not self.receivers:
+ return False
+
+ if self._by_sender[ANY_ID]:
+ return True
+
+ if sender is ANY:
+ return False
+
+ return make_id(sender) in self._by_sender
+
+ def receivers_for(
+ self, sender: t.Any
+ ) -> c.Generator[c.Callable[..., t.Any], None, None]:
+ """Yield each receiver to be called for ``sender``, in addition to those
+ to be called for :data:`ANY`. Weakly referenced receivers that are not
+ live will be disconnected and skipped.
+
+ :param sender: Yield receivers connected to this sender, in addition
+ to those connected to :data:`ANY`.
+ """
+ # TODO: test receivers_for(ANY)
+ if not self.receivers:
+ return
+
+ sender_id = make_id(sender)
+
+ if sender_id in self._by_sender:
+ ids = self._by_sender[ANY_ID] | self._by_sender[sender_id]
+ else:
+ ids = self._by_sender[ANY_ID].copy()
+
+ for receiver_id in ids:
+ receiver = self.receivers.get(receiver_id)
+
+ if receiver is None:
+ continue
+
+ if isinstance(receiver, weakref.ref):
+ strong = receiver()
+
+ if strong is None:
+ self._disconnect(receiver_id, ANY_ID)
+ continue
+
+ yield strong
+ else:
+ yield receiver
+
+ def disconnect(self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY) -> None:
+ """Disconnect ``receiver`` from being called when the signal is sent by
+ ``sender``.
+
+ :param receiver: A connected receiver callable.
+ :param sender: Disconnect from only this sender. By default, disconnect
+ from all senders.
+ """
+ sender_id: c.Hashable
+
+ if sender is ANY:
+ sender_id = ANY_ID
+ else:
+ sender_id = make_id(sender)
+
+ receiver_id = make_id(receiver)
+ self._disconnect(receiver_id, sender_id)
+
+ if (
+ "receiver_disconnected" in self.__dict__
+ and self.receiver_disconnected.receivers
+ ):
+ self.receiver_disconnected.send(self, receiver=receiver, sender=sender)
+
+ def _disconnect(self, receiver_id: c.Hashable, sender_id: c.Hashable) -> None:
+ if sender_id == ANY_ID:
+ if self._by_receiver.pop(receiver_id, None) is not None:
+ for bucket in self._by_sender.values():
+ bucket.discard(receiver_id)
+
+ self.receivers.pop(receiver_id, None)
+ else:
+ self._by_sender[sender_id].discard(receiver_id)
+ self._by_receiver[receiver_id].discard(sender_id)
+
+ def _make_cleanup_receiver(
+ self, receiver_id: c.Hashable
+ ) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]:
+ """Create a callback function to disconnect a weakly referenced
+ receiver when it is garbage collected.
+ """
+
+ def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None:
+ # If the interpreter is shutting down, disconnecting can result in a
+ # weird ignored exception. Don't call it in that case.
+ if not sys.is_finalizing():
+ self._disconnect(receiver_id, ANY_ID)
+
+ return cleanup
+
+ def _make_cleanup_sender(
+ self, sender_id: c.Hashable
+ ) -> c.Callable[[weakref.ref[t.Any]], None]:
+ """Create a callback function to disconnect all receivers for a weakly
+ referenced sender when it is garbage collected.
+ """
+ assert sender_id != ANY_ID
+
+ def cleanup(ref: weakref.ref[t.Any]) -> None:
+ self._weak_senders.pop(sender_id, None)
+
+ for receiver_id in self._by_sender.pop(sender_id, ()):
+ self._by_receiver[receiver_id].discard(sender_id)
+
+ return cleanup
+
+ def _cleanup_bookkeeping(self) -> None:
+ """Prune unused sender/receiver bookkeeping. Not threadsafe.
+
+ Connecting & disconnecting leaves behind a small amount of bookkeeping
+ data. Typical workloads using Blinker, for example in most web apps,
+ Flask, CLI scripts, etc., are not adversely affected by this
+ bookkeeping.
+
+ With a long-running process performing dynamic signal routing with high
+ volume, e.g. connecting to function closures, senders are all unique
+ object instances. Doing all of this over and over may cause memory usage
+ to grow due to extraneous bookkeeping. (An empty ``set`` for each stale
+ sender/receiver pair.)
+
+ This method will prune that bookkeeping away, with the caveat that such
+ pruning is not threadsafe. The risk is that cleanup of a fully
+ disconnected receiver/sender pair occurs while another thread is
+ connecting that same pair. If you are in the highly dynamic, unique
+ receiver/sender situation that has lead you to this method, that failure
+ mode is perhaps not a big deal for you.
+ """
+ for mapping in (self._by_sender, self._by_receiver):
+ for ident, bucket in list(mapping.items()):
+ if not bucket:
+ mapping.pop(ident, None)
+
+ def _clear_state(self) -> None:
+ """Disconnect all receivers and senders. Useful for tests."""
+ self._weak_senders.clear()
+ self.receivers.clear()
+ self._by_sender.clear()
+ self._by_receiver.clear()
+
+
+class NamedSignal(Signal):
+ """A named generic notification emitter. The name is not used by the signal
+ itself, but matches the key in the :class:`Namespace` that it belongs to.
+
+ :param name: The name of the signal within the namespace.
+ :param doc: The docstring for the signal.
+ """
+
+ def __init__(self, name: str, doc: str | None = None) -> None:
+ super().__init__(doc)
+
+ #: The name of this signal.
+ self.name: str = name
+
+ def __repr__(self) -> str:
+ base = super().__repr__()
+ return f"{base[:-1]}; {self.name!r}>" # noqa: E702
+
+
+class Namespace(dict[str, NamedSignal]):
+ """A dict mapping names to signals."""
+
+ def signal(self, name: str, doc: str | None = None) -> NamedSignal:
+ """Return the :class:`NamedSignal` for the given ``name``, creating it
+ if required. Repeated calls with the same name return the same signal.
+
+ :param name: The name of the signal.
+ :param doc: The docstring of the signal.
+ """
+ if name not in self:
+ self[name] = NamedSignal(name, doc)
+
+ return self[name]
+
+
+class _PNamespaceSignal(t.Protocol):
+ def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ...
+
+
+default_namespace: Namespace = Namespace()
+"""A default :class:`Namespace` for creating named signals. :func:`signal`
+creates a :class:`NamedSignal` in this namespace.
+"""
+
+signal: _PNamespaceSignal = default_namespace.signal
+"""Return a :class:`NamedSignal` in :data:`default_namespace` with the given
+``name``, creating it if required. Repeated calls with the same name return the
+same signal.
+"""
diff --git a/venv/Lib/site-packages/blinker/py.typed b/venv/Lib/site-packages/blinker/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/click-8.3.3.dist-info/INSTALLER b/venv/Lib/site-packages/click-8.3.3.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/Lib/site-packages/click-8.3.3.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/click-8.3.3.dist-info/METADATA b/venv/Lib/site-packages/click-8.3.3.dist-info/METADATA
new file mode 100644
index 0000000..30fbba6
--- /dev/null
+++ b/venv/Lib/site-packages/click-8.3.3.dist-info/METADATA
@@ -0,0 +1,84 @@
+Metadata-Version: 2.4
+Name: click
+Version: 8.3.3
+Summary: Composable command line interface toolkit
+Maintainer-email: Pallets
+Requires-Python: >=3.10
+Description-Content-Type: text/markdown
+License-Expression: BSD-3-Clause
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Typing :: Typed
+License-File: LICENSE.txt
+Requires-Dist: colorama; platform_system == 'Windows'
+Project-URL: Changes, https://click.palletsprojects.com/page/changes/
+Project-URL: Chat, https://discord.gg/pallets
+Project-URL: Documentation, https://click.palletsprojects.com/
+Project-URL: Donate, https://palletsprojects.com/donate
+Project-URL: Source, https://github.com/pallets/click/
+
+
+
+# Click
+
+Click is a Python package for creating beautiful command line interfaces
+in a composable way with as little code as necessary. It's the "Command
+Line Interface Creation Kit". It's highly configurable but comes with
+sensible defaults out of the box.
+
+It aims to make the process of writing command line tools quick and fun
+while also preventing any frustration caused by the inability to
+implement an intended CLI API.
+
+Click in three points:
+
+- Arbitrary nesting of commands
+- Automatic help page generation
+- Supports lazy loading of subcommands at runtime
+
+
+## A Simple Example
+
+```python
+import click
+
+@click.command()
+@click.option("--count", default=1, help="Number of greetings.")
+@click.option("--name", prompt="Your name", help="The person to greet.")
+def hello(count, name):
+ """Simple program that greets NAME for a total of COUNT times."""
+ for _ in range(count):
+ click.echo(f"Hello, {name}!")
+
+if __name__ == '__main__':
+ hello()
+```
+
+```
+$ python hello.py --count=3
+Your name: Click
+Hello, Click!
+Hello, Click!
+Hello, Click!
+```
+
+
+## Donate
+
+The Pallets organization develops and supports Click and other popular
+packages. In order to grow the community of contributors and users, and
+allow the maintainers to devote more time to the projects, [please
+donate today][].
+
+[please donate today]: https://palletsprojects.com/donate
+
+## Contributing
+
+See our [detailed contributing documentation][contrib] for many ways to
+contribute, including reporting issues, requesting features, asking or answering
+questions, and making PRs.
+
+[contrib]: https://palletsprojects.com/contributing/
+
diff --git a/venv/Lib/site-packages/click-8.3.3.dist-info/RECORD b/venv/Lib/site-packages/click-8.3.3.dist-info/RECORD
new file mode 100644
index 0000000..978c3e2
--- /dev/null
+++ b/venv/Lib/site-packages/click-8.3.3.dist-info/RECORD
@@ -0,0 +1,40 @@
+click-8.3.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+click-8.3.3.dist-info/METADATA,sha256=_sjhT7_ZKYkyseqbWJtQA8eAcV2CeDJdcNrX_92pSCY,2621
+click-8.3.3.dist-info/RECORD,,
+click-8.3.3.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
+click-8.3.3.dist-info/licenses/LICENSE.txt,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475
+click/__init__.py,sha256=tq5olp0lVuUNbwOj0p5nMVdqhOyYz4aZzF8S5T0aFMU,4526
+click/__pycache__/__init__.cpython-310.pyc,,
+click/__pycache__/_compat.cpython-310.pyc,,
+click/__pycache__/_termui_impl.cpython-310.pyc,,
+click/__pycache__/_textwrap.cpython-310.pyc,,
+click/__pycache__/_utils.cpython-310.pyc,,
+click/__pycache__/_winconsole.cpython-310.pyc,,
+click/__pycache__/core.cpython-310.pyc,,
+click/__pycache__/decorators.cpython-310.pyc,,
+click/__pycache__/exceptions.cpython-310.pyc,,
+click/__pycache__/formatting.cpython-310.pyc,,
+click/__pycache__/globals.cpython-310.pyc,,
+click/__pycache__/parser.cpython-310.pyc,,
+click/__pycache__/shell_completion.cpython-310.pyc,,
+click/__pycache__/termui.cpython-310.pyc,,
+click/__pycache__/testing.cpython-310.pyc,,
+click/__pycache__/types.cpython-310.pyc,,
+click/__pycache__/utils.cpython-310.pyc,,
+click/_compat.py,sha256=v3xBZkFbvA1BXPRkFfBJc6-pIwPI7345m-kQEnpVAs4,18693
+click/_termui_impl.py,sha256=R3aYjPD2x1xgNXiXEqHSR-jAVhbXKkLKeqgr-4eDOkA,28066
+click/_textwrap.py,sha256=BOae0RQ6vg3FkNgSJyOoGzG1meGMxJ_ukWVZKx_v-0o,1400
+click/_utils.py,sha256=kZwtTf5gMuCilJJceS2iTCvRvCY-0aN5rJq8gKw7p8g,943
+click/_winconsole.py,sha256=_vxUuUaxwBhoR0vUWCNuHY8VUefiMdCIyU2SXPqoF-A,8465
+click/core.py,sha256=wzDpH4Wh45hBfsdw3BHqn_095457WRrb92dyaeIGkHk,134478
+click/decorators.py,sha256=5P7abhJtAQYp_KHgjUvhMv464ERwOzrv2enNknlwHyQ,18461
+click/exceptions.py,sha256=8utf8w6V5hJXMnO_ic1FNrtbwuEn1NUu1aDwV8UqnG4,9954
+click/formatting.py,sha256=RVfwwr0rwWNpgGr8NaHodPzkIr7_tUyVh_nDdanLMNc,9730
+click/globals.py,sha256=gM-Nh6A4M0HB_SgkaF5M4ncGGMDHc_flHXu9_oh4GEU,1923
+click/parser.py,sha256=Q31pH0FlQZEq-UXE_ABRzlygEfvxPTuZbWNh4xfXmzw,19010
+click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+click/shell_completion.py,sha256=Cc4GQUFuWpfQBa9sF5qXeeYI7n3tI_1k6ZdSn4BZbT0,20994
+click/termui.py,sha256=0xjUaEZJz1_1y8eqon2kOe2OXrmHzZCJoVohFb43yTs,31434
+click/testing.py,sha256=PYLYW4ZjCAP6HRWtNhFpr3imJxQVEPFq3EW3JL0LTfA,22714
+click/types.py,sha256=ek54BNSFwPKsqtfT7jsqcc4WHui8AIFVMKM4oVZIXhc,39927
+click/utils.py,sha256=vep6qqMkv3WkzA9OCu7f6Ku-3beZHNpo8qJqL-uzUk8,20282
diff --git a/venv/Lib/site-packages/click-8.3.3.dist-info/WHEEL b/venv/Lib/site-packages/click-8.3.3.dist-info/WHEEL
new file mode 100644
index 0000000..d8b9936
--- /dev/null
+++ b/venv/Lib/site-packages/click-8.3.3.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 1.0
+Generator: flit 3.12.0
+Root-Is-Purelib: true
+Tag: py3-none-any
diff --git a/venv/Lib/site-packages/click-8.3.3.dist-info/licenses/LICENSE.txt b/venv/Lib/site-packages/click-8.3.3.dist-info/licenses/LICENSE.txt
new file mode 100644
index 0000000..d12a849
--- /dev/null
+++ b/venv/Lib/site-packages/click-8.3.3.dist-info/licenses/LICENSE.txt
@@ -0,0 +1,28 @@
+Copyright 2014 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/venv/Lib/site-packages/click/__init__.py b/venv/Lib/site-packages/click/__init__.py
new file mode 100644
index 0000000..fa485a0
--- /dev/null
+++ b/venv/Lib/site-packages/click/__init__.py
@@ -0,0 +1,124 @@
+"""
+Click is a simple Python module inspired by the stdlib optparse to make
+writing command line scripts fun. Unlike other modules, it's based
+around a simple API that does not come with too much magic and is
+composable.
+"""
+
+from __future__ import annotations
+
+from .core import Argument as Argument
+from .core import Command as Command
+from .core import CommandCollection as CommandCollection
+from .core import Context as Context
+from .core import Group as Group
+from .core import Option as Option
+from .core import Parameter as Parameter
+from .core import ParameterSource as ParameterSource
+from .decorators import argument as argument
+from .decorators import command as command
+from .decorators import confirmation_option as confirmation_option
+from .decorators import group as group
+from .decorators import help_option as help_option
+from .decorators import make_pass_decorator as make_pass_decorator
+from .decorators import option as option
+from .decorators import pass_context as pass_context
+from .decorators import pass_obj as pass_obj
+from .decorators import password_option as password_option
+from .decorators import version_option as version_option
+from .exceptions import Abort as Abort
+from .exceptions import BadArgumentUsage as BadArgumentUsage
+from .exceptions import BadOptionUsage as BadOptionUsage
+from .exceptions import BadParameter as BadParameter
+from .exceptions import ClickException as ClickException
+from .exceptions import FileError as FileError
+from .exceptions import MissingParameter as MissingParameter
+from .exceptions import NoSuchOption as NoSuchOption
+from .exceptions import UsageError as UsageError
+from .formatting import HelpFormatter as HelpFormatter
+from .formatting import wrap_text as wrap_text
+from .globals import get_current_context as get_current_context
+from .termui import clear as clear
+from .termui import confirm as confirm
+from .termui import echo_via_pager as echo_via_pager
+from .termui import edit as edit
+from .termui import getchar as getchar
+from .termui import launch as launch
+from .termui import pause as pause
+from .termui import progressbar as progressbar
+from .termui import prompt as prompt
+from .termui import secho as secho
+from .termui import style as style
+from .termui import unstyle as unstyle
+from .types import BOOL as BOOL
+from .types import Choice as Choice
+from .types import DateTime as DateTime
+from .types import File as File
+from .types import FLOAT as FLOAT
+from .types import FloatRange as FloatRange
+from .types import INT as INT
+from .types import IntRange as IntRange
+from .types import ParamType as ParamType
+from .types import Path as Path
+from .types import STRING as STRING
+from .types import Tuple as Tuple
+from .types import UNPROCESSED as UNPROCESSED
+from .types import UUID as UUID
+from .utils import echo as echo
+from .utils import format_filename as format_filename
+from .utils import get_app_dir as get_app_dir
+from .utils import get_binary_stream as get_binary_stream
+from .utils import get_text_stream as get_text_stream
+from .utils import open_file as open_file
+
+
+def __getattr__(name: str) -> object:
+ import warnings
+
+ if name == "BaseCommand":
+ from .core import _BaseCommand
+
+ warnings.warn(
+ "'BaseCommand' is deprecated and will be removed in Click 9.0. Use"
+ " 'Command' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return _BaseCommand
+
+ if name == "MultiCommand":
+ from .core import _MultiCommand
+
+ warnings.warn(
+ "'MultiCommand' is deprecated and will be removed in Click 9.0. Use"
+ " 'Group' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return _MultiCommand
+
+ if name == "OptionParser":
+ from .parser import _OptionParser
+
+ warnings.warn(
+ "'OptionParser' is deprecated and will be removed in Click 9.0. The"
+ " old parser is available in 'optparse'.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return _OptionParser
+
+ if name == "__version__":
+ import importlib.metadata
+ import warnings
+
+ warnings.warn(
+ "The '__version__' attribute is deprecated and will be removed in"
+ " Click 9.1. Use feature detection or"
+ " 'importlib.metadata.version(\"click\")' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return importlib.metadata.version("click")
+
+ raise AttributeError(name)
diff --git a/venv/Lib/site-packages/click/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..c3b7141
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/_compat.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/_compat.cpython-310.pyc
new file mode 100644
index 0000000..89b5d3f
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/_compat.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/_termui_impl.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/_termui_impl.cpython-310.pyc
new file mode 100644
index 0000000..ca2a1e4
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/_termui_impl.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/_textwrap.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/_textwrap.cpython-310.pyc
new file mode 100644
index 0000000..964cbf9
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/_textwrap.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/_utils.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/_utils.cpython-310.pyc
new file mode 100644
index 0000000..8b4808a
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/_utils.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/_winconsole.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/_winconsole.cpython-310.pyc
new file mode 100644
index 0000000..18a712e
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/_winconsole.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/core.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/core.cpython-310.pyc
new file mode 100644
index 0000000..5faa097
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/core.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/decorators.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/decorators.cpython-310.pyc
new file mode 100644
index 0000000..c4e11fd
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/decorators.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/exceptions.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/exceptions.cpython-310.pyc
new file mode 100644
index 0000000..d3d1266
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/exceptions.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/formatting.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/formatting.cpython-310.pyc
new file mode 100644
index 0000000..cc26e6c
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/formatting.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/globals.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/globals.cpython-310.pyc
new file mode 100644
index 0000000..acb7bd1
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/globals.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/parser.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/parser.cpython-310.pyc
new file mode 100644
index 0000000..59b64c8
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/parser.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/shell_completion.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/shell_completion.cpython-310.pyc
new file mode 100644
index 0000000..975700d
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/shell_completion.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/termui.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/termui.cpython-310.pyc
new file mode 100644
index 0000000..7d1afd4
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/termui.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/testing.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/testing.cpython-310.pyc
new file mode 100644
index 0000000..4ebccee
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/testing.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/types.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/types.cpython-310.pyc
new file mode 100644
index 0000000..449f4f5
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/types.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/__pycache__/utils.cpython-310.pyc b/venv/Lib/site-packages/click/__pycache__/utils.cpython-310.pyc
new file mode 100644
index 0000000..46f1685
Binary files /dev/null and b/venv/Lib/site-packages/click/__pycache__/utils.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/click/_compat.py b/venv/Lib/site-packages/click/_compat.py
new file mode 100644
index 0000000..f2726b9
--- /dev/null
+++ b/venv/Lib/site-packages/click/_compat.py
@@ -0,0 +1,622 @@
+from __future__ import annotations
+
+import codecs
+import collections.abc as cabc
+import io
+import os
+import re
+import sys
+import typing as t
+from types import TracebackType
+from weakref import WeakKeyDictionary
+
+CYGWIN = sys.platform.startswith("cygwin")
+WIN = sys.platform.startswith("win")
+auto_wrap_for_ansi: t.Callable[[t.TextIO], t.TextIO] | None = None
+_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
+
+
+def _make_text_stream(
+ stream: t.BinaryIO,
+ encoding: str | None,
+ errors: str | None,
+ force_readable: bool = False,
+ force_writable: bool = False,
+) -> t.TextIO:
+ if encoding is None:
+ encoding = get_best_encoding(stream)
+ if errors is None:
+ errors = "replace"
+ return _NonClosingTextIOWrapper(
+ stream,
+ encoding,
+ errors,
+ line_buffering=True,
+ force_readable=force_readable,
+ force_writable=force_writable,
+ )
+
+
+def is_ascii_encoding(encoding: str) -> bool:
+ """Checks if a given encoding is ascii."""
+ try:
+ return codecs.lookup(encoding).name == "ascii"
+ except LookupError:
+ return False
+
+
+def get_best_encoding(stream: t.IO[t.Any]) -> str:
+ """Returns the default stream encoding if not found."""
+ rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
+ if is_ascii_encoding(rv):
+ return "utf-8"
+ return rv
+
+
+class _NonClosingTextIOWrapper(io.TextIOWrapper):
+ def __init__(
+ self,
+ stream: t.BinaryIO,
+ encoding: str | None,
+ errors: str | None,
+ force_readable: bool = False,
+ force_writable: bool = False,
+ **extra: t.Any,
+ ) -> None:
+ self._stream = stream = t.cast(
+ t.BinaryIO, _FixupStream(stream, force_readable, force_writable)
+ )
+ super().__init__(stream, encoding, errors, **extra)
+
+ def __del__(self) -> None:
+ try:
+ self.detach()
+ except Exception:
+ pass
+
+ def isatty(self) -> bool:
+ # https://bitbucket.org/pypy/pypy/issue/1803
+ return self._stream.isatty()
+
+
+class _FixupStream:
+ """The new io interface needs more from streams than streams
+ traditionally implement. As such, this fix-up code is necessary in
+ some circumstances.
+
+ The forcing of readable and writable flags are there because some tools
+ put badly patched objects on sys (one such offender are certain version
+ of jupyter notebook).
+ """
+
+ def __init__(
+ self,
+ stream: t.BinaryIO,
+ force_readable: bool = False,
+ force_writable: bool = False,
+ ):
+ self._stream = stream
+ self._force_readable = force_readable
+ self._force_writable = force_writable
+
+ def __getattr__(self, name: str) -> t.Any:
+ return getattr(self._stream, name)
+
+ def read1(self, size: int) -> bytes:
+ f = getattr(self._stream, "read1", None)
+
+ if f is not None:
+ return t.cast(bytes, f(size))
+
+ return self._stream.read(size)
+
+ def readable(self) -> bool:
+ if self._force_readable:
+ return True
+ x = getattr(self._stream, "readable", None)
+ if x is not None:
+ return t.cast(bool, x())
+ try:
+ self._stream.read(0)
+ except Exception:
+ return False
+ return True
+
+ def writable(self) -> bool:
+ if self._force_writable:
+ return True
+ x = getattr(self._stream, "writable", None)
+ if x is not None:
+ return t.cast(bool, x())
+ try:
+ self._stream.write(b"")
+ except Exception:
+ try:
+ self._stream.write(b"")
+ except Exception:
+ return False
+ return True
+
+ def seekable(self) -> bool:
+ x = getattr(self._stream, "seekable", None)
+ if x is not None:
+ return t.cast(bool, x())
+ try:
+ self._stream.seek(self._stream.tell())
+ except Exception:
+ return False
+ return True
+
+
+def _is_binary_reader(stream: t.IO[t.Any], default: bool = False) -> bool:
+ try:
+ return isinstance(stream.read(0), bytes)
+ except Exception:
+ return default
+ # This happens in some cases where the stream was already
+ # closed. In this case, we assume the default.
+
+
+def _is_binary_writer(stream: t.IO[t.Any], default: bool = False) -> bool:
+ try:
+ stream.write(b"")
+ except Exception:
+ try:
+ stream.write("")
+ return False
+ except Exception:
+ pass
+ return default
+ return True
+
+
+def _find_binary_reader(stream: t.IO[t.Any]) -> t.BinaryIO | None:
+ # We need to figure out if the given stream is already binary.
+ # This can happen because the official docs recommend detaching
+ # the streams to get binary streams. Some code might do this, so
+ # we need to deal with this case explicitly.
+ if _is_binary_reader(stream, False):
+ return t.cast(t.BinaryIO, stream)
+
+ buf = getattr(stream, "buffer", None)
+
+ # Same situation here; this time we assume that the buffer is
+ # actually binary in case it's closed.
+ if buf is not None and _is_binary_reader(buf, True):
+ return t.cast(t.BinaryIO, buf)
+
+ return None
+
+
+def _find_binary_writer(stream: t.IO[t.Any]) -> t.BinaryIO | None:
+ # We need to figure out if the given stream is already binary.
+ # This can happen because the official docs recommend detaching
+ # the streams to get binary streams. Some code might do this, so
+ # we need to deal with this case explicitly.
+ if _is_binary_writer(stream, False):
+ return t.cast(t.BinaryIO, stream)
+
+ buf = getattr(stream, "buffer", None)
+
+ # Same situation here; this time we assume that the buffer is
+ # actually binary in case it's closed.
+ if buf is not None and _is_binary_writer(buf, True):
+ return t.cast(t.BinaryIO, buf)
+
+ return None
+
+
+def _stream_is_misconfigured(stream: t.TextIO) -> bool:
+ """A stream is misconfigured if its encoding is ASCII."""
+ # If the stream does not have an encoding set, we assume it's set
+ # to ASCII. This appears to happen in certain unittest
+ # environments. It's not quite clear what the correct behavior is
+ # but this at least will force Click to recover somehow.
+ return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii")
+
+
+def _is_compat_stream_attr(stream: t.TextIO, attr: str, value: str | None) -> bool:
+ """A stream attribute is compatible if it is equal to the
+ desired value or the desired value is unset and the attribute
+ has a value.
+ """
+ stream_value = getattr(stream, attr, None)
+ return stream_value == value or (value is None and stream_value is not None)
+
+
+def _is_compatible_text_stream(
+ stream: t.TextIO, encoding: str | None, errors: str | None
+) -> bool:
+ """Check if a stream's encoding and errors attributes are
+ compatible with the desired values.
+ """
+ return _is_compat_stream_attr(
+ stream, "encoding", encoding
+ ) and _is_compat_stream_attr(stream, "errors", errors)
+
+
+def _force_correct_text_stream(
+ text_stream: t.IO[t.Any],
+ encoding: str | None,
+ errors: str | None,
+ is_binary: t.Callable[[t.IO[t.Any], bool], bool],
+ find_binary: t.Callable[[t.IO[t.Any]], t.BinaryIO | None],
+ force_readable: bool = False,
+ force_writable: bool = False,
+) -> t.TextIO:
+ if is_binary(text_stream, False):
+ binary_reader = t.cast(t.BinaryIO, text_stream)
+ else:
+ text_stream = t.cast(t.TextIO, text_stream)
+ # If the stream looks compatible, and won't default to a
+ # misconfigured ascii encoding, return it as-is.
+ if _is_compatible_text_stream(text_stream, encoding, errors) and not (
+ encoding is None and _stream_is_misconfigured(text_stream)
+ ):
+ return text_stream
+
+ # Otherwise, get the underlying binary reader.
+ possible_binary_reader = find_binary(text_stream)
+
+ # If that's not possible, silently use the original reader
+ # and get mojibake instead of exceptions.
+ if possible_binary_reader is None:
+ return text_stream
+
+ binary_reader = possible_binary_reader
+
+ # Default errors to replace instead of strict in order to get
+ # something that works.
+ if errors is None:
+ errors = "replace"
+
+ # Wrap the binary stream in a text stream with the correct
+ # encoding parameters.
+ return _make_text_stream(
+ binary_reader,
+ encoding,
+ errors,
+ force_readable=force_readable,
+ force_writable=force_writable,
+ )
+
+
+def _force_correct_text_reader(
+ text_reader: t.IO[t.Any],
+ encoding: str | None,
+ errors: str | None,
+ force_readable: bool = False,
+) -> t.TextIO:
+ return _force_correct_text_stream(
+ text_reader,
+ encoding,
+ errors,
+ _is_binary_reader,
+ _find_binary_reader,
+ force_readable=force_readable,
+ )
+
+
+def _force_correct_text_writer(
+ text_writer: t.IO[t.Any],
+ encoding: str | None,
+ errors: str | None,
+ force_writable: bool = False,
+) -> t.TextIO:
+ return _force_correct_text_stream(
+ text_writer,
+ encoding,
+ errors,
+ _is_binary_writer,
+ _find_binary_writer,
+ force_writable=force_writable,
+ )
+
+
+def get_binary_stdin() -> t.BinaryIO:
+ reader = _find_binary_reader(sys.stdin)
+ if reader is None:
+ raise RuntimeError("Was not able to determine binary stream for sys.stdin.")
+ return reader
+
+
+def get_binary_stdout() -> t.BinaryIO:
+ writer = _find_binary_writer(sys.stdout)
+ if writer is None:
+ raise RuntimeError("Was not able to determine binary stream for sys.stdout.")
+ return writer
+
+
+def get_binary_stderr() -> t.BinaryIO:
+ writer = _find_binary_writer(sys.stderr)
+ if writer is None:
+ raise RuntimeError("Was not able to determine binary stream for sys.stderr.")
+ return writer
+
+
+def get_text_stdin(encoding: str | None = None, errors: str | None = None) -> t.TextIO:
+ rv = _get_windows_console_stream(sys.stdin, encoding, errors)
+ if rv is not None:
+ return rv
+ return _force_correct_text_reader(sys.stdin, encoding, errors, force_readable=True)
+
+
+def get_text_stdout(encoding: str | None = None, errors: str | None = None) -> t.TextIO:
+ rv = _get_windows_console_stream(sys.stdout, encoding, errors)
+ if rv is not None:
+ return rv
+ return _force_correct_text_writer(sys.stdout, encoding, errors, force_writable=True)
+
+
+def get_text_stderr(encoding: str | None = None, errors: str | None = None) -> t.TextIO:
+ rv = _get_windows_console_stream(sys.stderr, encoding, errors)
+ if rv is not None:
+ return rv
+ return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True)
+
+
+def _wrap_io_open(
+ file: str | os.PathLike[str] | int,
+ mode: str,
+ encoding: str | None,
+ errors: str | None,
+) -> t.IO[t.Any]:
+ """Handles not passing ``encoding`` and ``errors`` in binary mode."""
+ if "b" in mode:
+ return open(file, mode)
+
+ return open(file, mode, encoding=encoding, errors=errors)
+
+
+def open_stream(
+ filename: str | os.PathLike[str],
+ mode: str = "r",
+ encoding: str | None = None,
+ errors: str | None = "strict",
+ atomic: bool = False,
+) -> tuple[t.IO[t.Any], bool]:
+ binary = "b" in mode
+ filename = os.fspath(filename)
+
+ # Standard streams first. These are simple because they ignore the
+ # atomic flag. Use fsdecode to handle Path("-").
+ if os.fsdecode(filename) == "-":
+ if any(m in mode for m in ["w", "a", "x"]):
+ if binary:
+ return get_binary_stdout(), False
+ return get_text_stdout(encoding=encoding, errors=errors), False
+ if binary:
+ return get_binary_stdin(), False
+ return get_text_stdin(encoding=encoding, errors=errors), False
+
+ # Non-atomic writes directly go out through the regular open functions.
+ if not atomic:
+ return _wrap_io_open(filename, mode, encoding, errors), True
+
+ # Some usability stuff for atomic writes
+ if "a" in mode:
+ raise ValueError(
+ "Appending to an existing file is not supported, because that"
+ " would involve an expensive `copy`-operation to a temporary"
+ " file. Open the file in normal `w`-mode and copy explicitly"
+ " if that's what you're after."
+ )
+ if "x" in mode:
+ raise ValueError("Use the `overwrite`-parameter instead.")
+ if "w" not in mode:
+ raise ValueError("Atomic writes only make sense with `w`-mode.")
+
+ # Atomic writes are more complicated. They work by opening a file
+ # as a proxy in the same folder and then using the fdopen
+ # functionality to wrap it in a Python file. Then we wrap it in an
+ # atomic file that moves the file over on close.
+ import errno
+ import random
+
+ try:
+ perm: int | None = os.stat(filename).st_mode
+ except OSError:
+ perm = None
+
+ flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
+
+ if binary:
+ flags |= getattr(os, "O_BINARY", 0)
+
+ while True:
+ tmp_filename = os.path.join(
+ os.path.dirname(filename),
+ f".__atomic-write{random.randrange(1 << 32):08x}",
+ )
+ try:
+ fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)
+ break
+ except OSError as e:
+ if e.errno == errno.EEXIST or (
+ os.name == "nt"
+ and e.errno == errno.EACCES
+ and os.path.isdir(e.filename)
+ and os.access(e.filename, os.W_OK)
+ ):
+ continue
+ raise
+
+ if perm is not None:
+ os.chmod(tmp_filename, perm) # in case perm includes bits in umask
+
+ f = _wrap_io_open(fd, mode, encoding, errors)
+ af = _AtomicFile(f, tmp_filename, os.path.realpath(filename))
+ return t.cast(t.IO[t.Any], af), True
+
+
+class _AtomicFile:
+ def __init__(self, f: t.IO[t.Any], tmp_filename: str, real_filename: str) -> None:
+ self._f = f
+ self._tmp_filename = tmp_filename
+ self._real_filename = real_filename
+ self.closed = False
+
+ @property
+ def name(self) -> str:
+ return self._real_filename
+
+ def close(self, delete: bool = False) -> None:
+ if self.closed:
+ return
+ self._f.close()
+ os.replace(self._tmp_filename, self._real_filename)
+ self.closed = True
+
+ def __getattr__(self, name: str) -> t.Any:
+ return getattr(self._f, name)
+
+ def __enter__(self) -> _AtomicFile:
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> None:
+ self.close(delete=exc_type is not None)
+
+ def __repr__(self) -> str:
+ return repr(self._f)
+
+
+def strip_ansi(value: str) -> str:
+ return _ansi_re.sub("", value)
+
+
+def _is_jupyter_kernel_output(stream: t.IO[t.Any]) -> bool:
+ while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
+ stream = stream._stream
+
+ return stream.__class__.__module__.startswith("ipykernel.")
+
+
+def should_strip_ansi(
+ stream: t.IO[t.Any] | None = None, color: bool | None = None
+) -> bool:
+ if color is None:
+ if stream is None:
+ stream = sys.stdin
+ return not isatty(stream) and not _is_jupyter_kernel_output(stream)
+ return not color
+
+
+# On Windows, wrap the output streams with colorama to support ANSI
+# color codes.
+# NOTE: double check is needed so mypy does not analyze this on Linux
+if sys.platform.startswith("win") and WIN:
+ from ._winconsole import _get_windows_console_stream
+
+ def _get_argv_encoding() -> str:
+ import locale
+
+ return locale.getpreferredencoding()
+
+ _ansi_stream_wrappers: cabc.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
+
+ def auto_wrap_for_ansi(stream: t.TextIO, color: bool | None = None) -> t.TextIO:
+ """Support ANSI color and style codes on Windows by wrapping a
+ stream with colorama.
+ """
+ try:
+ cached = _ansi_stream_wrappers.get(stream)
+ except Exception:
+ cached = None
+
+ if cached is not None:
+ return cached
+
+ import colorama
+
+ strip = should_strip_ansi(stream, color)
+ ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
+ rv = t.cast(t.TextIO, ansi_wrapper.stream)
+ _write = rv.write
+
+ def _safe_write(s: str) -> int:
+ try:
+ return _write(s)
+ except BaseException:
+ ansi_wrapper.reset_all()
+ raise
+
+ rv.write = _safe_write # type: ignore[method-assign]
+
+ try:
+ _ansi_stream_wrappers[stream] = rv
+ except Exception:
+ pass
+
+ return rv
+
+else:
+
+ def _get_argv_encoding() -> str:
+ return getattr(sys.stdin, "encoding", None) or sys.getfilesystemencoding()
+
+ def _get_windows_console_stream(
+ f: t.TextIO, encoding: str | None, errors: str | None
+ ) -> t.TextIO | None:
+ return None
+
+
+def term_len(x: str) -> int:
+ return len(strip_ansi(x))
+
+
+def isatty(stream: t.IO[t.Any]) -> bool:
+ try:
+ return stream.isatty()
+ except Exception:
+ return False
+
+
+def _make_cached_stream_func(
+ src_func: t.Callable[[], t.TextIO | None],
+ wrapper_func: t.Callable[[], t.TextIO],
+) -> t.Callable[[], t.TextIO | None]:
+ cache: cabc.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
+
+ def func() -> t.TextIO | None:
+ stream = src_func()
+
+ if stream is None:
+ return None
+
+ try:
+ rv = cache.get(stream)
+ except Exception:
+ rv = None
+ if rv is not None:
+ return rv
+ rv = wrapper_func()
+ try:
+ cache[stream] = rv
+ except Exception:
+ pass
+ return rv
+
+ return func
+
+
+_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin)
+_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout)
+_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)
+
+
+binary_streams: cabc.Mapping[str, t.Callable[[], t.BinaryIO]] = {
+ "stdin": get_binary_stdin,
+ "stdout": get_binary_stdout,
+ "stderr": get_binary_stderr,
+}
+
+text_streams: cabc.Mapping[str, t.Callable[[str | None, str | None], t.TextIO]] = {
+ "stdin": get_text_stdin,
+ "stdout": get_text_stdout,
+ "stderr": get_text_stderr,
+}
diff --git a/venv/Lib/site-packages/click/_termui_impl.py b/venv/Lib/site-packages/click/_termui_impl.py
new file mode 100644
index 0000000..c9f1e1c
--- /dev/null
+++ b/venv/Lib/site-packages/click/_termui_impl.py
@@ -0,0 +1,870 @@
+"""
+This module contains implementations for the termui module. To keep the
+import time of Click down, some infrequently used functionality is
+placed in this module and only imported as needed.
+"""
+
+from __future__ import annotations
+
+import collections.abc as cabc
+import contextlib
+import math
+import os
+import shlex
+import sys
+import time
+import typing as t
+from gettext import gettext as _
+from io import StringIO
+from pathlib import Path
+from types import TracebackType
+
+from ._compat import _default_text_stdout
+from ._compat import CYGWIN
+from ._compat import get_best_encoding
+from ._compat import isatty
+from ._compat import open_stream
+from ._compat import strip_ansi
+from ._compat import term_len
+from ._compat import WIN
+from .exceptions import ClickException
+from .utils import echo
+
+V = t.TypeVar("V")
+
+if os.name == "nt":
+ BEFORE_BAR = "\r"
+ AFTER_BAR = "\n"
+else:
+ BEFORE_BAR = "\r\033[?25l"
+ AFTER_BAR = "\033[?25h\n"
+
+
+class ProgressBar(t.Generic[V]):
+ def __init__(
+ self,
+ iterable: cabc.Iterable[V] | None,
+ length: int | None = None,
+ fill_char: str = "#",
+ empty_char: str = " ",
+ bar_template: str = "%(bar)s",
+ info_sep: str = " ",
+ hidden: bool = False,
+ show_eta: bool = True,
+ show_percent: bool | None = None,
+ show_pos: bool = False,
+ item_show_func: t.Callable[[V | None], str | None] | None = None,
+ label: str | None = None,
+ file: t.TextIO | None = None,
+ color: bool | None = None,
+ update_min_steps: int = 1,
+ width: int = 30,
+ ) -> None:
+ self.fill_char = fill_char
+ self.empty_char = empty_char
+ self.bar_template = bar_template
+ self.info_sep = info_sep
+ self.hidden = hidden
+ self.show_eta = show_eta
+ self.show_percent = show_percent
+ self.show_pos = show_pos
+ self.item_show_func = item_show_func
+ self.label: str = label or ""
+
+ if file is None:
+ file = _default_text_stdout()
+
+ # There are no standard streams attached to write to. For example,
+ # pythonw on Windows.
+ if file is None:
+ file = StringIO()
+
+ self.file = file
+ self.color = color
+ self.update_min_steps = update_min_steps
+ self._completed_intervals = 0
+ self.width: int = width
+ self.autowidth: bool = width == 0
+
+ if length is None:
+ from operator import length_hint
+
+ length = length_hint(iterable, -1)
+
+ if length == -1:
+ length = None
+ if iterable is None:
+ if length is None:
+ raise TypeError("iterable or length is required")
+ iterable = t.cast("cabc.Iterable[V]", range(length))
+ self.iter: cabc.Iterable[V] = iter(iterable)
+ self.length = length
+ self.pos: int = 0
+ self.avg: list[float] = []
+ self.last_eta: float
+ self.start: float
+ self.start = self.last_eta = time.time()
+ self.eta_known: bool = False
+ self.finished: bool = False
+ self.max_width: int | None = None
+ self.entered: bool = False
+ self.current_item: V | None = None
+ self._is_atty = isatty(self.file)
+ self._last_line: str | None = None
+
+ def __enter__(self) -> ProgressBar[V]:
+ self.entered = True
+ self.render_progress()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> None:
+ self.render_finish()
+
+ def __iter__(self) -> cabc.Iterator[V]:
+ if not self.entered:
+ raise RuntimeError("You need to use progress bars in a with block.")
+ self.render_progress()
+ return self.generator()
+
+ def __next__(self) -> V:
+ # Iteration is defined in terms of a generator function,
+ # returned by iter(self); use that to define next(). This works
+ # because `self.iter` is an iterable consumed by that generator,
+ # so it is re-entry safe. Calling `next(self.generator())`
+ # twice works and does "what you want".
+ return next(iter(self))
+
+ def render_finish(self) -> None:
+ if self.hidden or not self._is_atty:
+ return
+ self.file.write(AFTER_BAR)
+ self.file.flush()
+
+ @property
+ def pct(self) -> float:
+ if self.finished:
+ return 1.0
+ return min(self.pos / (float(self.length or 1) or 1), 1.0)
+
+ @property
+ def time_per_iteration(self) -> float:
+ if not self.avg:
+ return 0.0
+ return sum(self.avg) / float(len(self.avg))
+
+ @property
+ def eta(self) -> float:
+ if self.length is not None and not self.finished:
+ return self.time_per_iteration * (self.length - self.pos)
+ return 0.0
+
+ def format_eta(self) -> str:
+ if self.eta_known:
+ t = int(self.eta)
+ seconds = t % 60
+ t //= 60
+ minutes = t % 60
+ t //= 60
+ hours = t % 24
+ t //= 24
+ if t > 0:
+ return f"{t}d {hours:02}:{minutes:02}:{seconds:02}"
+ else:
+ return f"{hours:02}:{minutes:02}:{seconds:02}"
+ return ""
+
+ def format_pos(self) -> str:
+ pos = str(self.pos)
+ if self.length is not None:
+ pos += f"/{self.length}"
+ return pos
+
+ def format_pct(self) -> str:
+ return f"{int(self.pct * 100): 4}%"[1:]
+
+ def format_bar(self) -> str:
+ if self.length is not None:
+ bar_length = int(self.pct * self.width)
+ bar = self.fill_char * bar_length
+ bar += self.empty_char * (self.width - bar_length)
+ elif self.finished:
+ bar = self.fill_char * self.width
+ else:
+ chars = list(self.empty_char * (self.width or 1))
+ if self.time_per_iteration != 0:
+ chars[
+ int(
+ (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
+ * self.width
+ )
+ ] = self.fill_char
+ bar = "".join(chars)
+ return bar
+
+ def format_progress_line(self) -> str:
+ show_percent = self.show_percent
+
+ info_bits = []
+ if self.length is not None and show_percent is None:
+ show_percent = not self.show_pos
+
+ if self.show_pos:
+ info_bits.append(self.format_pos())
+ if show_percent:
+ info_bits.append(self.format_pct())
+ if self.show_eta and self.eta_known and not self.finished:
+ info_bits.append(self.format_eta())
+ if self.item_show_func is not None:
+ item_info = self.item_show_func(self.current_item)
+ if item_info is not None:
+ info_bits.append(item_info)
+
+ return (
+ self.bar_template
+ % {
+ "label": self.label,
+ "bar": self.format_bar(),
+ "info": self.info_sep.join(info_bits),
+ }
+ ).rstrip()
+
+ def render_progress(self) -> None:
+ if self.hidden:
+ return
+
+ if not self._is_atty:
+ # Only output the label once if the output is not a TTY.
+ if self._last_line != self.label:
+ self._last_line = self.label
+ echo(self.label, file=self.file, color=self.color)
+ return
+
+ buf = []
+ # Update width in case the terminal has been resized
+ if self.autowidth:
+ import shutil
+
+ old_width = self.width
+ self.width = 0
+ clutter_length = term_len(self.format_progress_line())
+ new_width = max(0, shutil.get_terminal_size().columns - clutter_length)
+ if new_width < old_width and self.max_width is not None:
+ buf.append(BEFORE_BAR)
+ buf.append(" " * self.max_width)
+ self.max_width = new_width
+ self.width = new_width
+
+ clear_width = self.width
+ if self.max_width is not None:
+ clear_width = self.max_width
+
+ buf.append(BEFORE_BAR)
+ line = self.format_progress_line()
+ line_len = term_len(line)
+ if self.max_width is None or self.max_width < line_len:
+ self.max_width = line_len
+
+ buf.append(line)
+ buf.append(" " * (clear_width - line_len))
+ line = "".join(buf)
+ # Render the line only if it changed.
+
+ if line != self._last_line:
+ self._last_line = line
+ echo(line, file=self.file, color=self.color, nl=False)
+ self.file.flush()
+
+ def make_step(self, n_steps: int) -> None:
+ self.pos += n_steps
+ if self.length is not None and self.pos >= self.length:
+ self.finished = True
+
+ if (time.time() - self.last_eta) < 1.0:
+ return
+
+ self.last_eta = time.time()
+
+ # self.avg is a rolling list of length <= 7 of steps where steps are
+ # defined as time elapsed divided by the total progress through
+ # self.length.
+ if self.pos:
+ step = (time.time() - self.start) / self.pos
+ else:
+ step = time.time() - self.start
+
+ self.avg = self.avg[-6:] + [step]
+
+ self.eta_known = self.length is not None
+
+ def update(self, n_steps: int, current_item: V | None = None) -> None:
+ """Update the progress bar by advancing a specified number of
+ steps, and optionally set the ``current_item`` for this new
+ position.
+
+ :param n_steps: Number of steps to advance.
+ :param current_item: Optional item to set as ``current_item``
+ for the updated position.
+
+ .. versionchanged:: 8.0
+ Added the ``current_item`` optional parameter.
+
+ .. versionchanged:: 8.0
+ Only render when the number of steps meets the
+ ``update_min_steps`` threshold.
+ """
+ if current_item is not None:
+ self.current_item = current_item
+
+ self._completed_intervals += n_steps
+
+ if self._completed_intervals >= self.update_min_steps:
+ self.make_step(self._completed_intervals)
+ self.render_progress()
+ self._completed_intervals = 0
+
+ def finish(self) -> None:
+ self.eta_known = False
+ self.current_item = None
+ self.finished = True
+
+ def generator(self) -> cabc.Iterator[V]:
+ """Return a generator which yields the items added to the bar
+ during construction, and updates the progress bar *after* the
+ yielded block returns.
+ """
+ # WARNING: the iterator interface for `ProgressBar` relies on
+ # this and only works because this is a simple generator which
+ # doesn't create or manage additional state. If this function
+ # changes, the impact should be evaluated both against
+ # `iter(bar)` and `next(bar)`. `next()` in particular may call
+ # `self.generator()` repeatedly, and this must remain safe in
+ # order for that interface to work.
+ if not self.entered:
+ raise RuntimeError("You need to use progress bars in a with block.")
+
+ if not self._is_atty:
+ yield from self.iter
+ else:
+ for rv in self.iter:
+ self.current_item = rv
+
+ # This allows show_item_func to be updated before the
+ # item is processed. Only trigger at the beginning of
+ # the update interval.
+ if self._completed_intervals == 0:
+ self.render_progress()
+
+ yield rv
+ self.update(1)
+
+ self.finish()
+ self.render_progress()
+
+
+def pager(generator: cabc.Iterable[str], color: bool | None = None) -> None:
+ """Decide what method to use for paging through text."""
+ stdout = _default_text_stdout()
+
+ # There are no standard streams attached to write to. For example,
+ # pythonw on Windows.
+ if stdout is None:
+ stdout = StringIO()
+
+ if not isatty(sys.stdin) or not isatty(stdout):
+ return _nullpager(stdout, generator, color)
+
+ # Split using POSIX mode (the default) so that quote characters are
+ # stripped from tokens and quoted Windows paths are preserved.
+ # Non-POSIX mode retains quotes in tokens, and wrapping tokens
+ # with shlex.quote re-introduces quoting issues on Windows.
+ pager_cmd_parts = shlex.split(os.environ.get("PAGER", ""))
+ if pager_cmd_parts:
+ if WIN:
+ if _tempfilepager(generator, pager_cmd_parts, color):
+ return
+ elif _pipepager(generator, pager_cmd_parts, color):
+ return
+
+ if os.environ.get("TERM") in ("dumb", "emacs"):
+ return _nullpager(stdout, generator, color)
+ if (WIN or sys.platform.startswith("os2")) and _tempfilepager(
+ generator, ["more"], color
+ ):
+ return
+ if _pipepager(generator, ["less"], color):
+ return
+
+ import tempfile
+
+ fd, filename = tempfile.mkstemp()
+ os.close(fd)
+ try:
+ if _pipepager(generator, ["more"], color):
+ return
+ return _nullpager(stdout, generator, color)
+ finally:
+ os.unlink(filename)
+
+
+def _pipepager(
+ generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None
+) -> bool:
+ """Page through text by feeding it to another program.
+
+ Invokes the pager via :class:`subprocess.Popen` with an ``argv`` list
+ produced by :func:`shlex.split`. The command is resolved to an absolute
+ path with :func:`shutil.which` as recommended by the
+ :mod:`subprocess` docs for Windows compatibility.
+
+ Invoking a pager through this might support colors: if piping to
+ ``less`` and the user hasn't decided on colors, ``LESS=-R`` is set
+ automatically.
+
+ Returns ``True`` if the command was found and executed, ``False``
+ otherwise so another pager can be attempted.
+ """
+ # Split the command into the invoked CLI and its parameters.
+ if not cmd_parts:
+ return False
+
+ import shutil
+
+ cmd = cmd_parts[0]
+ cmd_params = cmd_parts[1:]
+
+ cmd_filepath = shutil.which(cmd)
+ if not cmd_filepath:
+ return False
+
+ # Produces a normalized absolute path string.
+ # multi-call binaries such as busybox derive their identity from the symlink
+ # less -> busybox. resolve() causes them to misbehave. (eg. less becomes busybox)
+ cmd_path = Path(cmd_filepath).absolute()
+ cmd_name = cmd_path.name
+
+ import subprocess
+
+ # Make a local copy of the environment to not affect the global one.
+ env = dict(os.environ)
+
+ # If we're piping to less and the user hasn't decided on colors, we enable
+ # them by default we find the -R flag in the command line arguments.
+ if color is None and cmd_name == "less":
+ less_flags = f"{os.environ.get('LESS', '')}{' '.join(cmd_params)}"
+ if not less_flags:
+ env["LESS"] = "-R"
+ color = True
+ elif "r" in less_flags or "R" in less_flags:
+ color = True
+
+ c = subprocess.Popen(
+ [str(cmd_path)] + cmd_params,
+ shell=False,
+ stdin=subprocess.PIPE,
+ env=env,
+ errors="replace",
+ text=True,
+ )
+ assert c.stdin is not None
+ try:
+ for text in generator:
+ if not color:
+ text = strip_ansi(text)
+
+ c.stdin.write(text)
+ except BrokenPipeError:
+ # In case the pager exited unexpectedly, ignore the broken pipe error.
+ pass
+ except Exception as e:
+ # In case there is an exception we want to close the pager immediately
+ # and let the caller handle it.
+ # Otherwise the pager will keep running, and the user may not notice
+ # the error message, or worse yet it may leave the terminal in a broken state.
+ c.terminate()
+ raise e
+ finally:
+ # We must close stdin and wait for the pager to exit before we continue
+ try:
+ c.stdin.close()
+ # Close implies flush, so it might throw a BrokenPipeError if the pager
+ # process exited already.
+ except BrokenPipeError:
+ pass
+
+ # Less doesn't respect ^C, but catches it for its own UI purposes (aborting
+ # search or other commands inside less).
+ #
+ # That means when the user hits ^C, the parent process (click) terminates,
+ # but less is still alive, paging the output and messing up the terminal.
+ #
+ # If the user wants to make the pager exit on ^C, they should set
+ # `LESS='-K'`. It's not our decision to make.
+ while True:
+ try:
+ c.wait()
+ except KeyboardInterrupt:
+ pass
+ else:
+ break
+
+ return True
+
+
+def _tempfilepager(
+ generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None
+) -> bool:
+ """Page through text by invoking a program on a temporary file.
+
+ Used as the primary pager strategy on Windows (where piping to
+ ``more`` adds spurious ``\\r\\n``), and as a fallback on other
+ platforms. The command is resolved to an absolute path with
+ :func:`shutil.which`.
+
+ Returns ``True`` if the command was found and executed, ``False``
+ otherwise so another pager can be attempted.
+ """
+ # Split the command into the invoked CLI and its parameters.
+ if not cmd_parts:
+ return False
+
+ import shutil
+
+ cmd = cmd_parts[0]
+
+ cmd_filepath = shutil.which(cmd)
+ if not cmd_filepath:
+ return False
+ # Produces a normalized absolute path string.
+ # multi-call binaries such as busybox derive their identity from the symlink
+ # less -> busybox. resolve() causes them to misbehave. (eg. less becomes busybox)
+ cmd_path = Path(cmd_filepath).absolute()
+
+ import subprocess
+ import tempfile
+
+ fd, filename = tempfile.mkstemp()
+ # TODO: This never terminates if the passed generator never terminates.
+ text = "".join(generator)
+ if not color:
+ text = strip_ansi(text)
+ encoding = get_best_encoding(sys.stdout)
+ with open_stream(filename, "wb")[0] as f:
+ f.write(text.encode(encoding))
+ try:
+ subprocess.call([str(cmd_path), filename])
+ except OSError:
+ # Command not found
+ pass
+ finally:
+ os.close(fd)
+ os.unlink(filename)
+
+ return True
+
+
+def _nullpager(
+ stream: t.TextIO, generator: cabc.Iterable[str], color: bool | None
+) -> None:
+ """Simply print unformatted text. This is the ultimate fallback."""
+ for text in generator:
+ if not color:
+ text = strip_ansi(text)
+ stream.write(text)
+
+
+class Editor:
+ def __init__(
+ self,
+ editor: str | None = None,
+ env: cabc.Mapping[str, str] | None = None,
+ require_save: bool = True,
+ extension: str = ".txt",
+ ) -> None:
+ self.editor = editor
+ self.env = env
+ self.require_save = require_save
+ self.extension = extension
+
+ def get_editor(self) -> str:
+ if self.editor is not None:
+ return self.editor
+ for key in "VISUAL", "EDITOR":
+ rv = os.environ.get(key)
+ if rv:
+ return rv
+ if WIN:
+ return "notepad"
+
+ from shutil import which
+
+ for editor in "sensible-editor", "vim", "nano":
+ if which(editor) is not None:
+ return editor
+ return "vi"
+
+ def edit_files(self, filenames: cabc.Iterable[str]) -> None:
+ """Open files in the user's editor."""
+ import shlex
+ import subprocess
+
+ editor = self.get_editor()
+ environ: dict[str, str] | None = None
+
+ if self.env:
+ environ = os.environ.copy()
+ environ.update(self.env)
+
+ try:
+ # Split in POSIX mode (the default) for the same reasons as
+ # in pager(): strips quotes from tokens and preserves quoted
+ # Windows paths.
+ c = subprocess.Popen(
+ args=shlex.split(editor) + list(filenames),
+ env=environ,
+ )
+ exit_code = c.wait()
+ if exit_code != 0:
+ raise ClickException(
+ _("{editor}: Editing failed").format(editor=editor)
+ )
+ except OSError as e:
+ raise ClickException(
+ _("{editor}: Editing failed: {e}").format(editor=editor, e=e)
+ ) from e
+
+ @t.overload
+ def edit(self, text: bytes | bytearray) -> bytes | None: ...
+
+ # We cannot know whether or not the type expected is str or bytes when None
+ # is passed, so str is returned as that was what was done before.
+ @t.overload
+ def edit(self, text: str | None) -> str | None: ...
+
+ def edit(self, text: str | bytes | bytearray | None) -> str | bytes | None:
+ import tempfile
+
+ if text is None:
+ data: bytes | bytearray = b""
+ elif isinstance(text, (bytes, bytearray)):
+ data = text
+ else:
+ if text and not text.endswith("\n"):
+ text += "\n"
+
+ if WIN:
+ data = text.replace("\n", "\r\n").encode("utf-8-sig")
+ else:
+ data = text.encode("utf-8")
+
+ fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension)
+ f: t.BinaryIO
+
+ try:
+ with os.fdopen(fd, "wb") as f:
+ f.write(data)
+
+ # If the filesystem resolution is 1 second, like Mac OS
+ # 10.12 Extended, or 2 seconds, like FAT32, and the editor
+ # closes very fast, require_save can fail. Set the modified
+ # time to be 2 seconds in the past to work around this.
+ os.utime(name, (os.path.getatime(name), os.path.getmtime(name) - 2))
+ # Depending on the resolution, the exact value might not be
+ # recorded, so get the new recorded value.
+ timestamp = os.path.getmtime(name)
+
+ self.edit_files((name,))
+
+ if self.require_save and os.path.getmtime(name) == timestamp:
+ return None
+
+ with open(name, "rb") as f:
+ rv = f.read()
+
+ if isinstance(text, (bytes, bytearray)):
+ return rv
+
+ return rv.decode("utf-8-sig").replace("\r\n", "\n")
+ finally:
+ os.unlink(name)
+
+
+def open_url(url: str, wait: bool = False, locate: bool = False) -> int:
+ import subprocess
+
+ def _unquote_file(url: str) -> str:
+ from urllib.parse import unquote
+
+ if url.startswith("file://"):
+ url = unquote(url[7:])
+
+ return url
+
+ if sys.platform == "darwin":
+ args = ["open"]
+ if wait:
+ args.append("-W")
+ if locate:
+ args.append("-R")
+ args.append(_unquote_file(url))
+ null = open("/dev/null", "w")
+ try:
+ return subprocess.Popen(args, stderr=null).wait()
+ finally:
+ null.close()
+ elif WIN:
+ if locate:
+ url = _unquote_file(url)
+ args = ["explorer", f"/select,{url}"]
+ else:
+ args = ["start"]
+ if wait:
+ args.append("/WAIT")
+ args.append("")
+ args.append(url)
+ try:
+ return subprocess.call(args)
+ except OSError:
+ # Command not found
+ return 127
+ elif CYGWIN:
+ if locate:
+ url = _unquote_file(url)
+ args = ["cygstart", os.path.dirname(url)]
+ else:
+ args = ["cygstart"]
+ if wait:
+ args.append("-w")
+ args.append(url)
+ try:
+ return subprocess.call(args)
+ except OSError:
+ # Command not found
+ return 127
+
+ try:
+ if locate:
+ url = os.path.dirname(_unquote_file(url)) or "."
+ else:
+ url = _unquote_file(url)
+ c = subprocess.Popen(["xdg-open", url])
+ if wait:
+ return c.wait()
+ return 0
+ except OSError:
+ if url.startswith(("http://", "https://")) and not locate and not wait:
+ import webbrowser
+
+ webbrowser.open(url)
+ return 0
+ return 1
+
+
+def _translate_ch_to_exc(ch: str) -> None:
+ if ch == "\x03":
+ raise KeyboardInterrupt()
+
+ if ch == "\x04" and not WIN: # Unix-like, Ctrl+D
+ raise EOFError()
+
+ if ch == "\x1a" and WIN: # Windows, Ctrl+Z
+ raise EOFError()
+
+
+if sys.platform == "win32":
+ import msvcrt
+
+ @contextlib.contextmanager
+ def raw_terminal() -> cabc.Iterator[int]:
+ yield -1
+
+ def getchar(echo: bool) -> str:
+ # The function `getch` will return a bytes object corresponding to
+ # the pressed character. Since Windows 10 build 1803, it will also
+ # return \x00 when called a second time after pressing a regular key.
+ #
+ # `getwch` does not share this probably-bugged behavior. Moreover, it
+ # returns a Unicode object by default, which is what we want.
+ #
+ # Either of these functions will return \x00 or \xe0 to indicate
+ # a special key, and you need to call the same function again to get
+ # the "rest" of the code. The fun part is that \u00e0 is
+ # "latin small letter a with grave", so if you type that on a French
+ # keyboard, you _also_ get a \xe0.
+ # E.g., consider the Up arrow. This returns \xe0 and then \x48. The
+ # resulting Unicode string reads as "a with grave" + "capital H".
+ # This is indistinguishable from when the user actually types
+ # "a with grave" and then "capital H".
+ #
+ # When \xe0 is returned, we assume it's part of a special-key sequence
+ # and call `getwch` again, but that means that when the user types
+ # the \u00e0 character, `getchar` doesn't return until a second
+ # character is typed.
+ # The alternative is returning immediately, but that would mess up
+ # cross-platform handling of arrow keys and others that start with
+ # \xe0. Another option is using `getch`, but then we can't reliably
+ # read non-ASCII characters, because return values of `getch` are
+ # limited to the current 8-bit codepage.
+ #
+ # Anyway, Click doesn't claim to do this Right(tm), and using `getwch`
+ # is doing the right thing in more situations than with `getch`.
+
+ if echo:
+ func = t.cast(t.Callable[[], str], msvcrt.getwche)
+ else:
+ func = t.cast(t.Callable[[], str], msvcrt.getwch)
+
+ rv = func()
+
+ if rv in ("\x00", "\xe0"):
+ # \x00 and \xe0 are control characters that indicate special key,
+ # see above.
+ rv += func()
+
+ _translate_ch_to_exc(rv)
+ return rv
+
+else:
+ import termios
+ import tty
+
+ @contextlib.contextmanager
+ def raw_terminal() -> cabc.Iterator[int]:
+ f: t.TextIO | None
+ fd: int
+
+ if not isatty(sys.stdin):
+ f = open("/dev/tty")
+ fd = f.fileno()
+ else:
+ fd = sys.stdin.fileno()
+ f = None
+
+ try:
+ old_settings = termios.tcgetattr(fd)
+
+ try:
+ tty.setraw(fd)
+ yield fd
+ finally:
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
+ sys.stdout.flush()
+
+ if f is not None:
+ f.close()
+ except termios.error:
+ pass
+
+ def getchar(echo: bool) -> str:
+ with raw_terminal() as fd:
+ ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace")
+
+ if echo and isatty(sys.stdout):
+ sys.stdout.write(ch)
+
+ _translate_ch_to_exc(ch)
+ return ch
diff --git a/venv/Lib/site-packages/click/_textwrap.py b/venv/Lib/site-packages/click/_textwrap.py
new file mode 100644
index 0000000..97fbee3
--- /dev/null
+++ b/venv/Lib/site-packages/click/_textwrap.py
@@ -0,0 +1,51 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import textwrap
+from contextlib import contextmanager
+
+
+class TextWrapper(textwrap.TextWrapper):
+ def _handle_long_word(
+ self,
+ reversed_chunks: list[str],
+ cur_line: list[str],
+ cur_len: int,
+ width: int,
+ ) -> None:
+ space_left = max(width - cur_len, 1)
+
+ if self.break_long_words:
+ last = reversed_chunks[-1]
+ cut = last[:space_left]
+ res = last[space_left:]
+ cur_line.append(cut)
+ reversed_chunks[-1] = res
+ elif not cur_line:
+ cur_line.append(reversed_chunks.pop())
+
+ @contextmanager
+ def extra_indent(self, indent: str) -> cabc.Iterator[None]:
+ old_initial_indent = self.initial_indent
+ old_subsequent_indent = self.subsequent_indent
+ self.initial_indent += indent
+ self.subsequent_indent += indent
+
+ try:
+ yield
+ finally:
+ self.initial_indent = old_initial_indent
+ self.subsequent_indent = old_subsequent_indent
+
+ def indent_only(self, text: str) -> str:
+ rv = []
+
+ for idx, line in enumerate(text.splitlines()):
+ indent = self.initial_indent
+
+ if idx > 0:
+ indent = self.subsequent_indent
+
+ rv.append(f"{indent}{line}")
+
+ return "\n".join(rv)
diff --git a/venv/Lib/site-packages/click/_utils.py b/venv/Lib/site-packages/click/_utils.py
new file mode 100644
index 0000000..09fb008
--- /dev/null
+++ b/venv/Lib/site-packages/click/_utils.py
@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+import enum
+import typing as t
+
+
+class Sentinel(enum.Enum):
+ """Enum used to define sentinel values.
+
+ .. seealso::
+
+ `PEP 661 - Sentinel Values `_.
+ """
+
+ UNSET = object()
+ FLAG_NEEDS_VALUE = object()
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}.{self.name}"
+
+
+UNSET = Sentinel.UNSET
+"""Sentinel used to indicate that a value is not set."""
+
+FLAG_NEEDS_VALUE = Sentinel.FLAG_NEEDS_VALUE
+"""Sentinel used to indicate an option was passed as a flag without a
+value but is not a flag option.
+
+``Option.consume_value`` uses this to prompt or use the ``flag_value``.
+"""
+
+T_UNSET = t.Literal[UNSET] # type: ignore[valid-type]
+"""Type hint for the :data:`UNSET` sentinel value."""
+
+T_FLAG_NEEDS_VALUE = t.Literal[FLAG_NEEDS_VALUE] # type: ignore[valid-type]
+"""Type hint for the :data:`FLAG_NEEDS_VALUE` sentinel value."""
diff --git a/venv/Lib/site-packages/click/_winconsole.py b/venv/Lib/site-packages/click/_winconsole.py
new file mode 100644
index 0000000..e56c7c6
--- /dev/null
+++ b/venv/Lib/site-packages/click/_winconsole.py
@@ -0,0 +1,296 @@
+# This module is based on the excellent work by Adam Bartoš who
+# provided a lot of what went into the implementation here in
+# the discussion to issue1602 in the Python bug tracker.
+#
+# There are some general differences in regards to how this works
+# compared to the original patches as we do not need to patch
+# the entire interpreter but just work in our little world of
+# echo and prompt.
+from __future__ import annotations
+
+import collections.abc as cabc
+import io
+import sys
+import time
+import typing as t
+from ctypes import Array
+from ctypes import byref
+from ctypes import c_char
+from ctypes import c_char_p
+from ctypes import c_int
+from ctypes import c_ssize_t
+from ctypes import c_ulong
+from ctypes import c_void_p
+from ctypes import POINTER
+from ctypes import py_object
+from ctypes import Structure
+from ctypes.wintypes import DWORD
+from ctypes.wintypes import HANDLE
+from ctypes.wintypes import LPCWSTR
+from ctypes.wintypes import LPWSTR
+
+from ._compat import _NonClosingTextIOWrapper
+
+assert sys.platform == "win32"
+import msvcrt # noqa: E402
+from ctypes import windll # noqa: E402
+from ctypes import WINFUNCTYPE # noqa: E402
+
+c_ssize_p = POINTER(c_ssize_t)
+
+kernel32 = windll.kernel32
+GetStdHandle = kernel32.GetStdHandle
+ReadConsoleW = kernel32.ReadConsoleW
+WriteConsoleW = kernel32.WriteConsoleW
+GetConsoleMode = kernel32.GetConsoleMode
+GetLastError = kernel32.GetLastError
+GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
+CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
+ ("CommandLineToArgvW", windll.shell32)
+)
+LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32))
+
+STDIN_HANDLE = GetStdHandle(-10)
+STDOUT_HANDLE = GetStdHandle(-11)
+STDERR_HANDLE = GetStdHandle(-12)
+
+PyBUF_SIMPLE = 0
+PyBUF_WRITABLE = 1
+
+ERROR_SUCCESS = 0
+ERROR_NOT_ENOUGH_MEMORY = 8
+ERROR_OPERATION_ABORTED = 995
+
+STDIN_FILENO = 0
+STDOUT_FILENO = 1
+STDERR_FILENO = 2
+
+EOF = b"\x1a"
+MAX_BYTES_WRITTEN = 32767
+
+if t.TYPE_CHECKING:
+ try:
+ # Using `typing_extensions.Buffer` instead of `collections.abc`
+ # on Windows for some reason does not have `Sized` implemented.
+ from collections.abc import Buffer # type: ignore
+ except ImportError:
+ from typing_extensions import Buffer
+
+try:
+ from ctypes import pythonapi
+except ImportError:
+ # On PyPy we cannot get buffers so our ability to operate here is
+ # severely limited.
+ get_buffer = None
+else:
+
+ class Py_buffer(Structure):
+ _fields_ = [ # noqa: RUF012
+ ("buf", c_void_p),
+ ("obj", py_object),
+ ("len", c_ssize_t),
+ ("itemsize", c_ssize_t),
+ ("readonly", c_int),
+ ("ndim", c_int),
+ ("format", c_char_p),
+ ("shape", c_ssize_p),
+ ("strides", c_ssize_p),
+ ("suboffsets", c_ssize_p),
+ ("internal", c_void_p),
+ ]
+
+ PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
+ PyBuffer_Release = pythonapi.PyBuffer_Release
+
+ def get_buffer(obj: Buffer, writable: bool = False) -> Array[c_char]:
+ buf = Py_buffer()
+ flags: int = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
+ PyObject_GetBuffer(py_object(obj), byref(buf), flags)
+
+ try:
+ buffer_type = c_char * buf.len
+ out: Array[c_char] = buffer_type.from_address(buf.buf)
+ return out
+ finally:
+ PyBuffer_Release(byref(buf))
+
+
+class _WindowsConsoleRawIOBase(io.RawIOBase):
+ def __init__(self, handle: int | None) -> None:
+ self.handle = handle
+
+ def isatty(self) -> t.Literal[True]:
+ super().isatty()
+ return True
+
+
+class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
+ def readable(self) -> t.Literal[True]:
+ return True
+
+ def readinto(self, b: Buffer) -> int:
+ bytes_to_be_read = len(b)
+ if not bytes_to_be_read:
+ return 0
+ elif bytes_to_be_read % 2:
+ raise ValueError(
+ "cannot read odd number of bytes from UTF-16-LE encoded console"
+ )
+
+ buffer = get_buffer(b, writable=True)
+ code_units_to_be_read = bytes_to_be_read // 2
+ code_units_read = c_ulong()
+
+ rv = ReadConsoleW(
+ HANDLE(self.handle),
+ buffer,
+ code_units_to_be_read,
+ byref(code_units_read),
+ None,
+ )
+ if GetLastError() == ERROR_OPERATION_ABORTED:
+ # wait for KeyboardInterrupt
+ time.sleep(0.1)
+ if not rv:
+ raise OSError(f"Windows error: {GetLastError()}")
+
+ if buffer[0] == EOF:
+ return 0
+ return 2 * code_units_read.value
+
+
+class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
+ def writable(self) -> t.Literal[True]:
+ return True
+
+ @staticmethod
+ def _get_error_message(errno: int) -> str:
+ if errno == ERROR_SUCCESS:
+ return "ERROR_SUCCESS"
+ elif errno == ERROR_NOT_ENOUGH_MEMORY:
+ return "ERROR_NOT_ENOUGH_MEMORY"
+ return f"Windows error {errno}"
+
+ def write(self, b: Buffer) -> int:
+ bytes_to_be_written = len(b)
+ buf = get_buffer(b)
+ code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2
+ code_units_written = c_ulong()
+
+ WriteConsoleW(
+ HANDLE(self.handle),
+ buf,
+ code_units_to_be_written,
+ byref(code_units_written),
+ None,
+ )
+ bytes_written = 2 * code_units_written.value
+
+ if bytes_written == 0 and bytes_to_be_written > 0:
+ raise OSError(self._get_error_message(GetLastError()))
+ return bytes_written
+
+
+class ConsoleStream:
+ def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None:
+ self._text_stream = text_stream
+ self.buffer = byte_stream
+
+ @property
+ def name(self) -> str:
+ return self.buffer.name
+
+ def write(self, x: t.AnyStr) -> int:
+ if isinstance(x, str):
+ return self._text_stream.write(x)
+ try:
+ self.flush()
+ except Exception:
+ pass
+ return self.buffer.write(x)
+
+ def writelines(self, lines: cabc.Iterable[t.AnyStr]) -> None:
+ for line in lines:
+ self.write(line)
+
+ def __getattr__(self, name: str) -> t.Any:
+ return getattr(self._text_stream, name)
+
+ def isatty(self) -> bool:
+ return self.buffer.isatty()
+
+ def __repr__(self) -> str:
+ return f""
+
+
+def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO:
+ text_stream = _NonClosingTextIOWrapper(
+ io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
+ "utf-16-le",
+ "strict",
+ line_buffering=True,
+ )
+ return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
+
+
+def _get_text_stdout(buffer_stream: t.BinaryIO) -> t.TextIO:
+ text_stream = _NonClosingTextIOWrapper(
+ io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
+ "utf-16-le",
+ "strict",
+ line_buffering=True,
+ )
+ return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
+
+
+def _get_text_stderr(buffer_stream: t.BinaryIO) -> t.TextIO:
+ text_stream = _NonClosingTextIOWrapper(
+ io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
+ "utf-16-le",
+ "strict",
+ line_buffering=True,
+ )
+ return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
+
+
+_stream_factories: cabc.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = {
+ 0: _get_text_stdin,
+ 1: _get_text_stdout,
+ 2: _get_text_stderr,
+}
+
+
+def _is_console(f: t.TextIO) -> bool:
+ if not hasattr(f, "fileno"):
+ return False
+
+ try:
+ fileno = f.fileno()
+ except (OSError, io.UnsupportedOperation):
+ return False
+
+ handle = msvcrt.get_osfhandle(fileno)
+ return bool(GetConsoleMode(handle, byref(DWORD())))
+
+
+def _get_windows_console_stream(
+ f: t.TextIO, encoding: str | None, errors: str | None
+) -> t.TextIO | None:
+ if (
+ get_buffer is None
+ or encoding not in {"utf-16-le", None}
+ or errors not in {"strict", None}
+ or not _is_console(f)
+ ):
+ return None
+
+ func = _stream_factories.get(f.fileno())
+ if func is None:
+ return None
+
+ b = getattr(f, "buffer", None)
+
+ if b is None:
+ return None
+
+ return func(b)
diff --git a/venv/Lib/site-packages/click/core.py b/venv/Lib/site-packages/click/core.py
new file mode 100644
index 0000000..d940dd8
--- /dev/null
+++ b/venv/Lib/site-packages/click/core.py
@@ -0,0 +1,3476 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import enum
+import errno
+import inspect
+import os
+import sys
+import typing as t
+from collections import abc
+from collections import Counter
+from contextlib import AbstractContextManager
+from contextlib import contextmanager
+from contextlib import ExitStack
+from functools import update_wrapper
+from gettext import gettext as _
+from gettext import ngettext
+from itertools import repeat
+from types import TracebackType
+
+from . import types
+from ._utils import FLAG_NEEDS_VALUE
+from ._utils import UNSET
+from .exceptions import Abort
+from .exceptions import BadParameter
+from .exceptions import ClickException
+from .exceptions import Exit
+from .exceptions import MissingParameter
+from .exceptions import NoArgsIsHelpError
+from .exceptions import UsageError
+from .formatting import HelpFormatter
+from .formatting import join_options
+from .globals import pop_context
+from .globals import push_context
+from .parser import _OptionParser
+from .parser import _split_opt
+from .termui import confirm
+from .termui import prompt
+from .termui import style
+from .utils import _detect_program_name
+from .utils import _expand_args
+from .utils import echo
+from .utils import make_default_short_help
+from .utils import make_str
+from .utils import PacifyFlushWrapper
+
+if t.TYPE_CHECKING:
+ from .shell_completion import CompletionItem
+
+F = t.TypeVar("F", bound="t.Callable[..., t.Any]")
+V = t.TypeVar("V")
+
+
+def _complete_visible_commands(
+ ctx: Context, incomplete: str
+) -> cabc.Iterator[tuple[str, Command]]:
+ """List all the subcommands of a group that start with the
+ incomplete value and aren't hidden.
+
+ :param ctx: Invocation context for the group.
+ :param incomplete: Value being completed. May be empty.
+ """
+ multi = t.cast(Group, ctx.command)
+
+ for name in multi.list_commands(ctx):
+ if name.startswith(incomplete):
+ command = multi.get_command(ctx, name)
+
+ if command is not None and not command.hidden:
+ yield name, command
+
+
+def _check_nested_chain(
+ base_command: Group, cmd_name: str, cmd: Command, register: bool = False
+) -> None:
+ if not base_command.chain or not isinstance(cmd, Group):
+ return
+
+ if register:
+ message = (
+ f"It is not possible to add the group {cmd_name!r} to another"
+ f" group {base_command.name!r} that is in chain mode."
+ )
+ else:
+ message = (
+ f"Found the group {cmd_name!r} as subcommand to another group "
+ f" {base_command.name!r} that is in chain mode. This is not supported."
+ )
+
+ raise RuntimeError(message)
+
+
+def batch(iterable: cabc.Iterable[V], batch_size: int) -> list[tuple[V, ...]]:
+ return list(zip(*repeat(iter(iterable), batch_size), strict=False))
+
+
+@contextmanager
+def augment_usage_errors(
+ ctx: Context, param: Parameter | None = None
+) -> cabc.Iterator[None]:
+ """Context manager that attaches extra information to exceptions."""
+ try:
+ yield
+ except BadParameter as e:
+ if e.ctx is None:
+ e.ctx = ctx
+ if param is not None and e.param is None:
+ e.param = param
+ raise
+ except UsageError as e:
+ if e.ctx is None:
+ e.ctx = ctx
+ raise
+
+
+def iter_params_for_processing(
+ invocation_order: cabc.Sequence[Parameter],
+ declaration_order: cabc.Sequence[Parameter],
+) -> list[Parameter]:
+ """Returns all declared parameters in the order they should be processed.
+
+ The declared parameters are re-shuffled depending on the order in which
+ they were invoked, as well as the eagerness of each parameters.
+
+ The invocation order takes precedence over the declaration order. I.e. the
+ order in which the user provided them to the CLI is respected.
+
+ This behavior and its effect on callback evaluation is detailed at:
+ https://click.palletsprojects.com/en/stable/advanced/#callback-evaluation-order
+ """
+
+ def sort_key(item: Parameter) -> tuple[bool, float]:
+ try:
+ idx: float = invocation_order.index(item)
+ except ValueError:
+ idx = float("inf")
+
+ return not item.is_eager, idx
+
+ return sorted(declaration_order, key=sort_key)
+
+
+class ParameterSource(enum.IntEnum):
+ """This is an :class:`~enum.IntEnum` that indicates the source of a
+ parameter's value.
+
+ Use :meth:`click.Context.get_parameter_source` to get the
+ source for a parameter by name.
+
+ Members are ordered from most explicit to least explicit source.
+ This allows comparison to check if a value was explicitly provided:
+
+ .. code-block:: python
+
+ source = ctx.get_parameter_source("port")
+ if source < click.ParameterSource.DEFAULT_MAP:
+ ... # value was explicitly set
+
+ .. versionchanged:: 8.3.3
+ Use :class:`~enum.IntEnum` and reorder members from most to
+ least explicit. Supports comparison operators.
+
+ .. versionchanged:: 8.0
+ Use :class:`~enum.Enum` and drop the ``validate`` method.
+
+ .. versionchanged:: 8.0
+ Added the ``PROMPT`` value.
+ """
+
+ PROMPT = enum.auto()
+ """Used a prompt to confirm a default or provide a value."""
+ COMMANDLINE = enum.auto()
+ """The value was provided by the command line args."""
+ ENVIRONMENT = enum.auto()
+ """The value was provided with an environment variable."""
+ DEFAULT_MAP = enum.auto()
+ """Used a default provided by :attr:`Context.default_map`."""
+ DEFAULT = enum.auto()
+ """Used the default specified by the parameter."""
+
+
+class Context:
+ """The context is a special internal object that holds state relevant
+ for the script execution at every single level. It's normally invisible
+ to commands unless they opt-in to getting access to it.
+
+ The context is useful as it can pass internal objects around and can
+ control special execution features such as reading data from
+ environment variables.
+
+ A context can be used as context manager in which case it will call
+ :meth:`close` on teardown.
+
+ :param command: the command class for this context.
+ :param parent: the parent context.
+ :param info_name: the info name for this invocation. Generally this
+ is the most descriptive name for the script or
+ command. For the toplevel script it is usually
+ the name of the script, for commands below it it's
+ the name of the script.
+ :param obj: an arbitrary object of user data.
+ :param auto_envvar_prefix: the prefix to use for automatic environment
+ variables. If this is `None` then reading
+ from environment variables is disabled. This
+ does not affect manually set environment
+ variables which are always read.
+ :param default_map: a dictionary (like object) with default values
+ for parameters.
+ :param terminal_width: the width of the terminal. The default is
+ inherit from parent context. If no context
+ defines the terminal width then auto
+ detection will be applied.
+ :param max_content_width: the maximum width for content rendered by
+ Click (this currently only affects help
+ pages). This defaults to 80 characters if
+ not overridden. In other words: even if the
+ terminal is larger than that, Click will not
+ format things wider than 80 characters by
+ default. In addition to that, formatters might
+ add some safety mapping on the right.
+ :param resilient_parsing: if this flag is enabled then Click will
+ parse without any interactivity or callback
+ invocation. Default values will also be
+ ignored. This is useful for implementing
+ things such as completion support.
+ :param allow_extra_args: if this is set to `True` then extra arguments
+ at the end will not raise an error and will be
+ kept on the context. The default is to inherit
+ from the command.
+ :param allow_interspersed_args: if this is set to `False` then options
+ and arguments cannot be mixed. The
+ default is to inherit from the command.
+ :param ignore_unknown_options: instructs click to ignore options it does
+ not know and keeps them for later
+ processing.
+ :param help_option_names: optionally a list of strings that define how
+ the default help parameter is named. The
+ default is ``['--help']``.
+ :param token_normalize_func: an optional function that is used to
+ normalize tokens (options, choices,
+ etc.). This for instance can be used to
+ implement case insensitive behavior.
+ :param color: controls if the terminal supports ANSI colors or not. The
+ default is autodetection. This is only needed if ANSI
+ codes are used in texts that Click prints which is by
+ default not the case. This for instance would affect
+ help output.
+ :param show_default: Show the default value for commands. If this
+ value is not set, it defaults to the value from the parent
+ context. ``Command.show_default`` overrides this default for the
+ specific command.
+
+ .. versionchanged:: 8.2
+ The ``protected_args`` attribute is deprecated and will be removed in
+ Click 9.0. ``args`` will contain remaining unparsed tokens.
+
+ .. versionchanged:: 8.1
+ The ``show_default`` parameter is overridden by
+ ``Command.show_default``, instead of the other way around.
+
+ .. versionchanged:: 8.0
+ The ``show_default`` parameter defaults to the value from the
+ parent context.
+
+ .. versionchanged:: 7.1
+ Added the ``show_default`` parameter.
+
+ .. versionchanged:: 4.0
+ Added the ``color``, ``ignore_unknown_options``, and
+ ``max_content_width`` parameters.
+
+ .. versionchanged:: 3.0
+ Added the ``allow_extra_args`` and ``allow_interspersed_args``
+ parameters.
+
+ .. versionchanged:: 2.0
+ Added the ``resilient_parsing``, ``help_option_names``, and
+ ``token_normalize_func`` parameters.
+ """
+
+ #: The formatter class to create with :meth:`make_formatter`.
+ #:
+ #: .. versionadded:: 8.0
+ formatter_class: type[HelpFormatter] = HelpFormatter
+
+ def __init__(
+ self,
+ command: Command,
+ parent: Context | None = None,
+ info_name: str | None = None,
+ obj: t.Any | None = None,
+ auto_envvar_prefix: str | None = None,
+ default_map: cabc.MutableMapping[str, t.Any] | None = None,
+ terminal_width: int | None = None,
+ max_content_width: int | None = None,
+ resilient_parsing: bool = False,
+ allow_extra_args: bool | None = None,
+ allow_interspersed_args: bool | None = None,
+ ignore_unknown_options: bool | None = None,
+ help_option_names: list[str] | None = None,
+ token_normalize_func: t.Callable[[str], str] | None = None,
+ color: bool | None = None,
+ show_default: bool | None = None,
+ ) -> None:
+ #: the parent context or `None` if none exists.
+ self.parent = parent
+ #: the :class:`Command` for this context.
+ self.command = command
+ #: the descriptive information name
+ self.info_name = info_name
+ #: Map of parameter names to their parsed values. Parameters
+ #: with ``expose_value=False`` are not stored.
+ self.params: dict[str, t.Any] = {}
+ #: the leftover arguments.
+ self.args: list[str] = []
+ #: protected arguments. These are arguments that are prepended
+ #: to `args` when certain parsing scenarios are encountered but
+ #: must be never propagated to another arguments. This is used
+ #: to implement nested parsing.
+ self._protected_args: list[str] = []
+ #: the collected prefixes of the command's options.
+ self._opt_prefixes: set[str] = set(parent._opt_prefixes) if parent else set()
+
+ if obj is None and parent is not None:
+ obj = parent.obj
+
+ #: the user object stored.
+ self.obj: t.Any = obj
+ self._meta: dict[str, t.Any] = getattr(parent, "meta", {})
+
+ #: A dictionary (-like object) with defaults for parameters.
+ if (
+ default_map is None
+ and info_name is not None
+ and parent is not None
+ and parent.default_map is not None
+ ):
+ default_map = parent.default_map.get(info_name)
+
+ self.default_map: cabc.MutableMapping[str, t.Any] | None = default_map
+
+ #: This flag indicates if a subcommand is going to be executed. A
+ #: group callback can use this information to figure out if it's
+ #: being executed directly or because the execution flow passes
+ #: onwards to a subcommand. By default it's None, but it can be
+ #: the name of the subcommand to execute.
+ #:
+ #: If chaining is enabled this will be set to ``'*'`` in case
+ #: any commands are executed. It is however not possible to
+ #: figure out which ones. If you require this knowledge you
+ #: should use a :func:`result_callback`.
+ self.invoked_subcommand: str | None = None
+
+ if terminal_width is None and parent is not None:
+ terminal_width = parent.terminal_width
+
+ #: The width of the terminal (None is autodetection).
+ self.terminal_width: int | None = terminal_width
+
+ if max_content_width is None and parent is not None:
+ max_content_width = parent.max_content_width
+
+ #: The maximum width of formatted content (None implies a sensible
+ #: default which is 80 for most things).
+ self.max_content_width: int | None = max_content_width
+
+ if allow_extra_args is None:
+ allow_extra_args = command.allow_extra_args
+
+ #: Indicates if the context allows extra args or if it should
+ #: fail on parsing.
+ #:
+ #: .. versionadded:: 3.0
+ self.allow_extra_args = allow_extra_args
+
+ if allow_interspersed_args is None:
+ allow_interspersed_args = command.allow_interspersed_args
+
+ #: Indicates if the context allows mixing of arguments and
+ #: options or not.
+ #:
+ #: .. versionadded:: 3.0
+ self.allow_interspersed_args: bool = allow_interspersed_args
+
+ if ignore_unknown_options is None:
+ ignore_unknown_options = command.ignore_unknown_options
+
+ #: Instructs click to ignore options that a command does not
+ #: understand and will store it on the context for later
+ #: processing. This is primarily useful for situations where you
+ #: want to call into external programs. Generally this pattern is
+ #: strongly discouraged because it's not possibly to losslessly
+ #: forward all arguments.
+ #:
+ #: .. versionadded:: 4.0
+ self.ignore_unknown_options: bool = ignore_unknown_options
+
+ if help_option_names is None:
+ if parent is not None:
+ help_option_names = parent.help_option_names
+ else:
+ help_option_names = ["--help"]
+
+ #: The names for the help options.
+ self.help_option_names: list[str] = help_option_names
+
+ if token_normalize_func is None and parent is not None:
+ token_normalize_func = parent.token_normalize_func
+
+ #: An optional normalization function for tokens. This is
+ #: options, choices, commands etc.
+ self.token_normalize_func: t.Callable[[str], str] | None = token_normalize_func
+
+ #: Indicates if resilient parsing is enabled. In that case Click
+ #: will do its best to not cause any failures and default values
+ #: will be ignored. Useful for completion.
+ self.resilient_parsing: bool = resilient_parsing
+
+ # If there is no envvar prefix yet, but the parent has one and
+ # the command on this level has a name, we can expand the envvar
+ # prefix automatically.
+ if auto_envvar_prefix is None:
+ if (
+ parent is not None
+ and parent.auto_envvar_prefix is not None
+ and self.info_name is not None
+ ):
+ auto_envvar_prefix = (
+ f"{parent.auto_envvar_prefix}_{self.info_name.upper()}"
+ )
+ else:
+ auto_envvar_prefix = auto_envvar_prefix.upper()
+
+ if auto_envvar_prefix is not None:
+ auto_envvar_prefix = auto_envvar_prefix.replace("-", "_")
+
+ self.auto_envvar_prefix: str | None = auto_envvar_prefix
+
+ if color is None and parent is not None:
+ color = parent.color
+
+ #: Controls if styling output is wanted or not.
+ self.color: bool | None = color
+
+ if show_default is None and parent is not None:
+ show_default = parent.show_default
+
+ #: Show option default values when formatting help text.
+ self.show_default: bool | None = show_default
+
+ self._close_callbacks: list[t.Callable[[], t.Any]] = []
+ self._depth = 0
+ self._parameter_source: dict[str, ParameterSource] = {}
+ self._exit_stack = ExitStack()
+
+ @property
+ def protected_args(self) -> list[str]:
+ import warnings
+
+ warnings.warn(
+ "'protected_args' is deprecated and will be removed in Click 9.0."
+ " 'args' will contain remaining unparsed tokens.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self._protected_args
+
+ def to_info_dict(self) -> dict[str, t.Any]:
+ """Gather information that could be useful for a tool generating
+ user-facing documentation. This traverses the entire CLI
+ structure.
+
+ .. code-block:: python
+
+ with Context(cli) as ctx:
+ info = ctx.to_info_dict()
+
+ .. versionadded:: 8.0
+ """
+ return {
+ "command": self.command.to_info_dict(self),
+ "info_name": self.info_name,
+ "allow_extra_args": self.allow_extra_args,
+ "allow_interspersed_args": self.allow_interspersed_args,
+ "ignore_unknown_options": self.ignore_unknown_options,
+ "auto_envvar_prefix": self.auto_envvar_prefix,
+ }
+
+ def __enter__(self) -> Context:
+ self._depth += 1
+ push_context(self)
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> bool | None:
+ self._depth -= 1
+ exit_result: bool | None = None
+ if self._depth == 0:
+ exit_result = self._close_with_exception_info(exc_type, exc_value, tb)
+ pop_context()
+
+ return exit_result
+
+ @contextmanager
+ def scope(self, cleanup: bool = True) -> cabc.Iterator[Context]:
+ """This helper method can be used with the context object to promote
+ it to the current thread local (see :func:`get_current_context`).
+ The default behavior of this is to invoke the cleanup functions which
+ can be disabled by setting `cleanup` to `False`. The cleanup
+ functions are typically used for things such as closing file handles.
+
+ If the cleanup is intended the context object can also be directly
+ used as a context manager.
+
+ Example usage::
+
+ with ctx.scope():
+ assert get_current_context() is ctx
+
+ This is equivalent::
+
+ with ctx:
+ assert get_current_context() is ctx
+
+ .. versionadded:: 5.0
+
+ :param cleanup: controls if the cleanup functions should be run or
+ not. The default is to run these functions. In
+ some situations the context only wants to be
+ temporarily pushed in which case this can be disabled.
+ Nested pushes automatically defer the cleanup.
+ """
+ if not cleanup:
+ self._depth += 1
+ try:
+ with self as rv:
+ yield rv
+ finally:
+ if not cleanup:
+ self._depth -= 1
+
+ @property
+ def meta(self) -> dict[str, t.Any]:
+ """This is a dictionary which is shared with all the contexts
+ that are nested. It exists so that click utilities can store some
+ state here if they need to. It is however the responsibility of
+ that code to manage this dictionary well.
+
+ The keys are supposed to be unique dotted strings. For instance
+ module paths are a good choice for it. What is stored in there is
+ irrelevant for the operation of click. However what is important is
+ that code that places data here adheres to the general semantics of
+ the system.
+
+ Example usage::
+
+ LANG_KEY = f'{__name__}.lang'
+
+ def set_language(value):
+ ctx = get_current_context()
+ ctx.meta[LANG_KEY] = value
+
+ def get_language():
+ return get_current_context().meta.get(LANG_KEY, 'en_US')
+
+ .. versionadded:: 5.0
+ """
+ return self._meta
+
+ def make_formatter(self) -> HelpFormatter:
+ """Creates the :class:`~click.HelpFormatter` for the help and
+ usage output.
+
+ To quickly customize the formatter class used without overriding
+ this method, set the :attr:`formatter_class` attribute.
+
+ .. versionchanged:: 8.0
+ Added the :attr:`formatter_class` attribute.
+ """
+ return self.formatter_class(
+ width=self.terminal_width, max_width=self.max_content_width
+ )
+
+ def with_resource(self, context_manager: AbstractContextManager[V]) -> V:
+ """Register a resource as if it were used in a ``with``
+ statement. The resource will be cleaned up when the context is
+ popped.
+
+ Uses :meth:`contextlib.ExitStack.enter_context`. It calls the
+ resource's ``__enter__()`` method and returns the result. When
+ the context is popped, it closes the stack, which calls the
+ resource's ``__exit__()`` method.
+
+ To register a cleanup function for something that isn't a
+ context manager, use :meth:`call_on_close`. Or use something
+ from :mod:`contextlib` to turn it into a context manager first.
+
+ .. code-block:: python
+
+ @click.group()
+ @click.option("--name")
+ @click.pass_context
+ def cli(ctx):
+ ctx.obj = ctx.with_resource(connect_db(name))
+
+ :param context_manager: The context manager to enter.
+ :return: Whatever ``context_manager.__enter__()`` returns.
+
+ .. versionadded:: 8.0
+ """
+ return self._exit_stack.enter_context(context_manager)
+
+ def call_on_close(self, f: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
+ """Register a function to be called when the context tears down.
+
+ This can be used to close resources opened during the script
+ execution. Resources that support Python's context manager
+ protocol which would be used in a ``with`` statement should be
+ registered with :meth:`with_resource` instead.
+
+ :param f: The function to execute on teardown.
+ """
+ return self._exit_stack.callback(f)
+
+ def close(self) -> None:
+ """Invoke all close callbacks registered with
+ :meth:`call_on_close`, and exit all context managers entered
+ with :meth:`with_resource`.
+ """
+ self._close_with_exception_info(None, None, None)
+
+ def _close_with_exception_info(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> bool | None:
+ """Unwind the exit stack by calling its :meth:`__exit__` providing the exception
+ information to allow for exception handling by the various resources registered
+ using :meth;`with_resource`
+
+ :return: Whatever ``exit_stack.__exit__()`` returns.
+ """
+ exit_result = self._exit_stack.__exit__(exc_type, exc_value, tb)
+ # In case the context is reused, create a new exit stack.
+ self._exit_stack = ExitStack()
+
+ return exit_result
+
+ @property
+ def command_path(self) -> str:
+ """The computed command path. This is used for the ``usage``
+ information on the help page. It's automatically created by
+ combining the info names of the chain of contexts to the root.
+ """
+ rv = ""
+ if self.info_name is not None:
+ rv = self.info_name
+ if self.parent is not None:
+ parent_command_path = [self.parent.command_path]
+
+ if isinstance(self.parent.command, Command):
+ for param in self.parent.command.get_params(self):
+ parent_command_path.extend(param.get_usage_pieces(self))
+
+ rv = f"{' '.join(parent_command_path)} {rv}"
+ return rv.lstrip()
+
+ def find_root(self) -> Context:
+ """Finds the outermost context."""
+ node = self
+ while node.parent is not None:
+ node = node.parent
+ return node
+
+ def find_object(self, object_type: type[V]) -> V | None:
+ """Finds the closest object of a given type."""
+ node: Context | None = self
+
+ while node is not None:
+ if isinstance(node.obj, object_type):
+ return node.obj
+
+ node = node.parent
+
+ return None
+
+ def ensure_object(self, object_type: type[V]) -> V:
+ """Like :meth:`find_object` but sets the innermost object to a
+ new instance of `object_type` if it does not exist.
+ """
+ rv = self.find_object(object_type)
+ if rv is None:
+ self.obj = rv = object_type()
+ return rv
+
+ def _default_map_has(self, name: str | None) -> bool:
+ """Check if :attr:`default_map` contains a real value for ``name``.
+
+ Returns ``False`` when the key is absent, the map is ``None``,
+ ``name`` is ``None``, or the stored value is the internal
+ :data:`UNSET` sentinel.
+ """
+ return (
+ name is not None
+ and self.default_map is not None
+ and name in self.default_map
+ and self.default_map[name] is not UNSET
+ )
+
+ @t.overload
+ def lookup_default(
+ self, name: str, call: t.Literal[True] = True
+ ) -> t.Any | None: ...
+
+ @t.overload
+ def lookup_default(
+ self, name: str, call: t.Literal[False] = ...
+ ) -> t.Any | t.Callable[[], t.Any] | None: ...
+
+ def lookup_default(self, name: str, call: bool = True) -> t.Any | None:
+ """Get the default for a parameter from :attr:`default_map`.
+
+ :param name: Name of the parameter.
+ :param call: If the default is a callable, call it. Disable to
+ return the callable instead.
+
+ .. versionchanged:: 8.0
+ Added the ``call`` parameter.
+ """
+ if not self._default_map_has(name):
+ return None
+
+ # Assert to make the type checker happy.
+ assert self.default_map is not None
+ value = self.default_map[name]
+
+ if call and callable(value):
+ return value()
+
+ return value
+
+ def fail(self, message: str) -> t.NoReturn:
+ """Aborts the execution of the program with a specific error
+ message.
+
+ :param message: the error message to fail with.
+ """
+ raise UsageError(message, self)
+
+ def abort(self) -> t.NoReturn:
+ """Aborts the script."""
+ raise Abort()
+
+ def exit(self, code: int = 0) -> t.NoReturn:
+ """Exits the application with a given exit code.
+
+ .. versionchanged:: 8.2
+ Callbacks and context managers registered with :meth:`call_on_close`
+ and :meth:`with_resource` are closed before exiting.
+ """
+ self.close()
+ raise Exit(code)
+
+ def get_usage(self) -> str:
+ """Helper method to get formatted usage string for the current
+ context and command.
+ """
+ return self.command.get_usage(self)
+
+ def get_help(self) -> str:
+ """Helper method to get formatted help page for the current
+ context and command.
+ """
+ return self.command.get_help(self)
+
+ def _make_sub_context(self, command: Command) -> Context:
+ """Create a new context of the same type as this context, but
+ for a new command.
+
+ :meta private:
+ """
+ return type(self)(command, info_name=command.name, parent=self)
+
+ @t.overload
+ def invoke(
+ self, callback: t.Callable[..., V], /, *args: t.Any, **kwargs: t.Any
+ ) -> V: ...
+
+ @t.overload
+ def invoke(self, callback: Command, /, *args: t.Any, **kwargs: t.Any) -> t.Any: ...
+
+ def invoke(
+ self, callback: Command | t.Callable[..., V], /, *args: t.Any, **kwargs: t.Any
+ ) -> t.Any | V:
+ """Invokes a command callback in exactly the way it expects. There
+ are two ways to invoke this method:
+
+ 1. the first argument can be a callback and all other arguments and
+ keyword arguments are forwarded directly to the function.
+ 2. the first argument is a click command object. In that case all
+ arguments are forwarded as well but proper click parameters
+ (options and click arguments) must be keyword arguments and Click
+ will fill in defaults.
+
+ .. versionchanged:: 8.0
+ All ``kwargs`` are tracked in :attr:`params` so they will be
+ passed if :meth:`forward` is called at multiple levels.
+
+ .. versionchanged:: 3.2
+ A new context is created, and missing arguments use default values.
+ """
+ if isinstance(callback, Command):
+ other_cmd = callback
+
+ if other_cmd.callback is None:
+ raise TypeError(
+ "The given command does not have a callback that can be invoked."
+ )
+ else:
+ callback = t.cast("t.Callable[..., V]", other_cmd.callback)
+
+ ctx = self._make_sub_context(other_cmd)
+
+ for param in other_cmd.params:
+ if param.name not in kwargs and param.expose_value:
+ default_value = param.get_default(ctx)
+ # We explicitly hide the :attr:`UNSET` value to the user, as we
+ # choose to make it an implementation detail. And because ``invoke``
+ # has been designed as part of Click public API, we return ``None``
+ # instead. Refs:
+ # https://github.com/pallets/click/issues/3066
+ # https://github.com/pallets/click/issues/3065
+ # https://github.com/pallets/click/pull/3068
+ if default_value is UNSET:
+ default_value = None
+ kwargs[param.name] = param.type_cast_value( # type: ignore
+ ctx, default_value
+ )
+
+ # Track all kwargs as params, so that forward() will pass
+ # them on in subsequent calls.
+ ctx.params.update(kwargs)
+ else:
+ ctx = self
+
+ with augment_usage_errors(self):
+ with ctx:
+ return callback(*args, **kwargs)
+
+ def forward(self, cmd: Command, /, *args: t.Any, **kwargs: t.Any) -> t.Any:
+ """Similar to :meth:`invoke` but fills in default keyword
+ arguments from the current context if the other command expects
+ it. This cannot invoke callbacks directly, only other commands.
+
+ .. versionchanged:: 8.0
+ All ``kwargs`` are tracked in :attr:`params` so they will be
+ passed if ``forward`` is called at multiple levels.
+ """
+ # Can only forward to other commands, not direct callbacks.
+ if not isinstance(cmd, Command):
+ raise TypeError("Callback is not a command.")
+
+ for param in self.params:
+ if param not in kwargs:
+ kwargs[param] = self.params[param]
+
+ return self.invoke(cmd, *args, **kwargs)
+
+ def set_parameter_source(self, name: str, source: ParameterSource) -> None:
+ """Set the source of a parameter. This indicates the location
+ from which the value of the parameter was obtained.
+
+ :param name: The name of the parameter.
+ :param source: A member of :class:`~click.core.ParameterSource`.
+ """
+ self._parameter_source[name] = source
+
+ def get_parameter_source(self, name: str) -> ParameterSource | None:
+ """Get the source of a parameter. This indicates the location
+ from which the value of the parameter was obtained.
+
+ This can be useful for determining when a user specified a value
+ on the command line that is the same as the default value. It
+ will be :attr:`~click.core.ParameterSource.DEFAULT` only if the
+ value was actually taken from the default.
+
+ :param name: The name of the parameter.
+ :rtype: ParameterSource
+
+ .. versionchanged:: 8.0
+ Returns ``None`` if the parameter was not provided from any
+ source.
+ """
+ return self._parameter_source.get(name)
+
+
+class Command:
+ """Commands are the basic building block of command line interfaces in
+ Click. A basic command handles command line parsing and might dispatch
+ more parsing to commands nested below it.
+
+ :param name: the name of the command to use unless a group overrides it.
+ :param context_settings: an optional dictionary with defaults that are
+ passed to the context object.
+ :param callback: the callback to invoke. This is optional.
+ :param params: the parameters to register with this command. This can
+ be either :class:`Option` or :class:`Argument` objects.
+ :param help: the help string to use for this command.
+ :param epilog: like the help string but it's printed at the end of the
+ help page after everything else.
+ :param short_help: the short help to use for this command. This is
+ shown on the command listing of the parent command.
+ :param add_help_option: by default each command registers a ``--help``
+ option. This can be disabled by this parameter.
+ :param no_args_is_help: this controls what happens if no arguments are
+ provided. This option is disabled by default.
+ If enabled this will add ``--help`` as argument
+ if no arguments are passed
+ :param hidden: hide this command from help outputs.
+ :param deprecated: If ``True`` or non-empty string, issues a message
+ indicating that the command is deprecated and highlights
+ its deprecation in --help. The message can be customized
+ by using a string as the value.
+
+ .. versionchanged:: 8.2
+ This is the base class for all commands, not ``BaseCommand``.
+ ``deprecated`` can be set to a string as well to customize the
+ deprecation message.
+
+ .. versionchanged:: 8.1
+ ``help``, ``epilog``, and ``short_help`` are stored unprocessed,
+ all formatting is done when outputting help text, not at init,
+ and is done even if not using the ``@command`` decorator.
+
+ .. versionchanged:: 8.0
+ Added a ``repr`` showing the command name.
+
+ .. versionchanged:: 7.1
+ Added the ``no_args_is_help`` parameter.
+
+ .. versionchanged:: 2.0
+ Added the ``context_settings`` parameter.
+ """
+
+ #: The context class to create with :meth:`make_context`.
+ #:
+ #: .. versionadded:: 8.0
+ context_class: type[Context] = Context
+
+ #: the default for the :attr:`Context.allow_extra_args` flag.
+ allow_extra_args = False
+
+ #: the default for the :attr:`Context.allow_interspersed_args` flag.
+ allow_interspersed_args = True
+
+ #: the default for the :attr:`Context.ignore_unknown_options` flag.
+ ignore_unknown_options = False
+
+ def __init__(
+ self,
+ name: str | None,
+ context_settings: cabc.MutableMapping[str, t.Any] | None = None,
+ callback: t.Callable[..., t.Any] | None = None,
+ params: list[Parameter] | None = None,
+ help: str | None = None,
+ epilog: str | None = None,
+ short_help: str | None = None,
+ options_metavar: str | None = "[OPTIONS]",
+ add_help_option: bool = True,
+ no_args_is_help: bool = False,
+ hidden: bool = False,
+ deprecated: bool | str = False,
+ ) -> None:
+ #: the name the command thinks it has. Upon registering a command
+ #: on a :class:`Group` the group will default the command name
+ #: with this information. You should instead use the
+ #: :class:`Context`\'s :attr:`~Context.info_name` attribute.
+ self.name = name
+
+ if context_settings is None:
+ context_settings = {}
+
+ #: an optional dictionary with defaults passed to the context.
+ self.context_settings: cabc.MutableMapping[str, t.Any] = context_settings
+
+ #: the callback to execute when the command fires. This might be
+ #: `None` in which case nothing happens.
+ self.callback = callback
+ #: the list of parameters for this command in the order they
+ #: should show up in the help page and execute. Eager parameters
+ #: will automatically be handled before non eager ones.
+ self.params: list[Parameter] = params or []
+ self.help = help
+ self.epilog = epilog
+ self.options_metavar = options_metavar
+ self.short_help = short_help
+ self.add_help_option = add_help_option
+ self._help_option = None
+ self.no_args_is_help = no_args_is_help
+ self.hidden = hidden
+ self.deprecated = deprecated
+
+ def to_info_dict(self, ctx: Context) -> dict[str, t.Any]:
+ return {
+ "name": self.name,
+ "params": [param.to_info_dict() for param in self.get_params(ctx)],
+ "help": self.help,
+ "epilog": self.epilog,
+ "short_help": self.short_help,
+ "hidden": self.hidden,
+ "deprecated": self.deprecated,
+ }
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__.__name__} {self.name}>"
+
+ def get_usage(self, ctx: Context) -> str:
+ """Formats the usage line into a string and returns it.
+
+ Calls :meth:`format_usage` internally.
+ """
+ formatter = ctx.make_formatter()
+ self.format_usage(ctx, formatter)
+ return formatter.getvalue().rstrip("\n")
+
+ def get_params(self, ctx: Context) -> list[Parameter]:
+ params = self.params
+ help_option = self.get_help_option(ctx)
+
+ if help_option is not None:
+ params = [*params, help_option]
+
+ if __debug__:
+ import warnings
+
+ opts = [opt for param in params for opt in param.opts]
+ opts_counter = Counter(opts)
+ duplicate_opts = (opt for opt, count in opts_counter.items() if count > 1)
+
+ for duplicate_opt in duplicate_opts:
+ warnings.warn(
+ (
+ f"The parameter {duplicate_opt} is used more than once. "
+ "Remove its duplicate as parameters should be unique."
+ ),
+ stacklevel=3,
+ )
+
+ return params
+
+ def format_usage(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes the usage line into the formatter.
+
+ This is a low-level method called by :meth:`get_usage`.
+ """
+ pieces = self.collect_usage_pieces(ctx)
+ formatter.write_usage(ctx.command_path, " ".join(pieces))
+
+ def collect_usage_pieces(self, ctx: Context) -> list[str]:
+ """Returns all the pieces that go into the usage line and returns
+ it as a list of strings.
+ """
+ rv = [self.options_metavar] if self.options_metavar else []
+
+ for param in self.get_params(ctx):
+ rv.extend(param.get_usage_pieces(ctx))
+
+ return rv
+
+ def get_help_option_names(self, ctx: Context) -> list[str]:
+ """Returns the names for the help option."""
+ all_names = set(ctx.help_option_names)
+ for param in self.params:
+ all_names.difference_update(param.opts)
+ all_names.difference_update(param.secondary_opts)
+ return list(all_names)
+
+ def get_help_option(self, ctx: Context) -> Option | None:
+ """Returns the help option object.
+
+ Skipped if :attr:`add_help_option` is ``False``.
+
+ .. versionchanged:: 8.1.8
+ The help option is now cached to avoid creating it multiple times.
+ """
+ help_option_names = self.get_help_option_names(ctx)
+
+ if not help_option_names or not self.add_help_option:
+ return None
+
+ # Cache the help option object in private _help_option attribute to
+ # avoid creating it multiple times. Not doing this will break the
+ # callback odering by iter_params_for_processing(), which relies on
+ # object comparison.
+ if self._help_option is None:
+ # Avoid circular import.
+ from .decorators import help_option
+
+ # Apply help_option decorator and pop resulting option
+ help_option(*help_option_names)(self)
+ self._help_option = self.params.pop() # type: ignore[assignment]
+
+ return self._help_option
+
+ def make_parser(self, ctx: Context) -> _OptionParser:
+ """Creates the underlying option parser for this command."""
+ parser = _OptionParser(ctx)
+ for param in self.get_params(ctx):
+ param.add_to_parser(parser, ctx)
+ return parser
+
+ def get_help(self, ctx: Context) -> str:
+ """Formats the help into a string and returns it.
+
+ Calls :meth:`format_help` internally.
+ """
+ formatter = ctx.make_formatter()
+ self.format_help(ctx, formatter)
+ return formatter.getvalue().rstrip("\n")
+
+ def get_short_help_str(self, limit: int = 45) -> str:
+ """Gets short help for the command or makes it by shortening the
+ long help string.
+ """
+ if self.short_help:
+ text = inspect.cleandoc(self.short_help)
+ elif self.help:
+ text = make_default_short_help(self.help, limit)
+ else:
+ text = ""
+
+ if self.deprecated:
+ deprecated_message = (
+ f"(DEPRECATED: {self.deprecated})"
+ if isinstance(self.deprecated, str)
+ else "(DEPRECATED)"
+ )
+ text = _("{text} {deprecated_message}").format(
+ text=text, deprecated_message=deprecated_message
+ )
+
+ return text.strip()
+
+ def format_help(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes the help into the formatter if it exists.
+
+ This is a low-level method called by :meth:`get_help`.
+
+ This calls the following methods:
+
+ - :meth:`format_usage`
+ - :meth:`format_help_text`
+ - :meth:`format_options`
+ - :meth:`format_epilog`
+ """
+ self.format_usage(ctx, formatter)
+ self.format_help_text(ctx, formatter)
+ self.format_options(ctx, formatter)
+ self.format_epilog(ctx, formatter)
+
+ def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes the help text to the formatter if it exists."""
+ if self.help is not None:
+ # truncate the help text to the first form feed
+ text = inspect.cleandoc(self.help).partition("\f")[0]
+ else:
+ text = ""
+
+ if self.deprecated:
+ deprecated_message = (
+ f"(DEPRECATED: {self.deprecated})"
+ if isinstance(self.deprecated, str)
+ else "(DEPRECATED)"
+ )
+ text = _("{text} {deprecated_message}").format(
+ text=text, deprecated_message=deprecated_message
+ )
+
+ if text:
+ formatter.write_paragraph()
+
+ with formatter.indentation():
+ formatter.write_text(text)
+
+ def format_options(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes all the options into the formatter if they exist."""
+ opts = []
+ for param in self.get_params(ctx):
+ rv = param.get_help_record(ctx)
+ if rv is not None:
+ opts.append(rv)
+
+ if opts:
+ with formatter.section(_("Options")):
+ formatter.write_dl(opts)
+
+ def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes the epilog into the formatter if it exists."""
+ if self.epilog:
+ epilog = inspect.cleandoc(self.epilog)
+ formatter.write_paragraph()
+
+ with formatter.indentation():
+ formatter.write_text(epilog)
+
+ def make_context(
+ self,
+ info_name: str | None,
+ args: list[str],
+ parent: Context | None = None,
+ **extra: t.Any,
+ ) -> Context:
+ """This function when given an info name and arguments will kick
+ off the parsing and create a new :class:`Context`. It does not
+ invoke the actual command callback though.
+
+ To quickly customize the context class used without overriding
+ this method, set the :attr:`context_class` attribute.
+
+ :param info_name: the info name for this invocation. Generally this
+ is the most descriptive name for the script or
+ command. For the toplevel script it's usually
+ the name of the script, for commands below it's
+ the name of the command.
+ :param args: the arguments to parse as list of strings.
+ :param parent: the parent context if available.
+ :param extra: extra keyword arguments forwarded to the context
+ constructor.
+
+ .. versionchanged:: 8.0
+ Added the :attr:`context_class` attribute.
+ """
+ for key, value in self.context_settings.items():
+ if key not in extra:
+ extra[key] = value
+
+ ctx = self.context_class(self, info_name=info_name, parent=parent, **extra)
+
+ with ctx.scope(cleanup=False):
+ self.parse_args(ctx, args)
+ return ctx
+
+ def parse_args(self, ctx: Context, args: list[str]) -> list[str]:
+ if not args and self.no_args_is_help and not ctx.resilient_parsing:
+ raise NoArgsIsHelpError(ctx)
+
+ parser = self.make_parser(ctx)
+ opts, args, param_order = parser.parse_args(args=args)
+
+ for param in iter_params_for_processing(param_order, self.get_params(ctx)):
+ _, args = param.handle_parse_result(ctx, opts, args)
+
+ # We now have all parameters' values into `ctx.params`, but the data may contain
+ # the `UNSET` sentinel.
+ # Convert `UNSET` to `None` to ensure that the user doesn't see `UNSET`.
+ #
+ # Waiting until after the initial parse to convert allows us to treat `UNSET`
+ # more like a missing value when multiple params use the same name.
+ # Refs:
+ # https://github.com/pallets/click/issues/3071
+ # https://github.com/pallets/click/pull/3079
+ for name, value in ctx.params.items():
+ if value is UNSET:
+ ctx.params[name] = None
+
+ if args and not ctx.allow_extra_args and not ctx.resilient_parsing:
+ ctx.fail(
+ ngettext(
+ "Got unexpected extra argument ({args})",
+ "Got unexpected extra arguments ({args})",
+ len(args),
+ ).format(args=" ".join(map(str, args)))
+ )
+
+ ctx.args = args
+ ctx._opt_prefixes.update(parser._opt_prefixes)
+ return args
+
+ def invoke(self, ctx: Context) -> t.Any:
+ """Given a context, this invokes the attached callback (if it exists)
+ in the right way.
+ """
+ if self.deprecated:
+ extra_message = (
+ f" {self.deprecated}" if isinstance(self.deprecated, str) else ""
+ )
+ message = _(
+ "DeprecationWarning: The command {name!r} is deprecated.{extra_message}"
+ ).format(name=self.name, extra_message=extra_message)
+ echo(style(message, fg="red"), err=True)
+
+ if self.callback is not None:
+ return ctx.invoke(self.callback, **ctx.params)
+
+ def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]:
+ """Return a list of completions for the incomplete value. Looks
+ at the names of options and chained multi-commands.
+
+ Any command could be part of a chained multi-command, so sibling
+ commands are valid at any point during command completion.
+
+ :param ctx: Invocation context for this command.
+ :param incomplete: Value being completed. May be empty.
+
+ .. versionadded:: 8.0
+ """
+ from click.shell_completion import CompletionItem
+
+ results: list[CompletionItem] = []
+
+ if incomplete and not incomplete[0].isalnum():
+ for param in self.get_params(ctx):
+ if (
+ not isinstance(param, Option)
+ or param.hidden
+ or (
+ not param.multiple
+ and ctx.get_parameter_source(param.name) # type: ignore
+ is ParameterSource.COMMANDLINE
+ )
+ ):
+ continue
+
+ results.extend(
+ CompletionItem(name, help=param.help)
+ for name in [*param.opts, *param.secondary_opts]
+ if name.startswith(incomplete)
+ )
+
+ while ctx.parent is not None:
+ ctx = ctx.parent
+
+ if isinstance(ctx.command, Group) and ctx.command.chain:
+ results.extend(
+ CompletionItem(name, help=command.get_short_help_str())
+ for name, command in _complete_visible_commands(ctx, incomplete)
+ if name not in ctx._protected_args
+ )
+
+ return results
+
+ @t.overload
+ def main(
+ self,
+ args: cabc.Sequence[str] | None = None,
+ prog_name: str | None = None,
+ complete_var: str | None = None,
+ standalone_mode: t.Literal[True] = True,
+ **extra: t.Any,
+ ) -> t.NoReturn: ...
+
+ @t.overload
+ def main(
+ self,
+ args: cabc.Sequence[str] | None = None,
+ prog_name: str | None = None,
+ complete_var: str | None = None,
+ standalone_mode: bool = ...,
+ **extra: t.Any,
+ ) -> t.Any: ...
+
+ def main(
+ self,
+ args: cabc.Sequence[str] | None = None,
+ prog_name: str | None = None,
+ complete_var: str | None = None,
+ standalone_mode: bool = True,
+ windows_expand_args: bool = True,
+ **extra: t.Any,
+ ) -> t.Any:
+ """This is the way to invoke a script with all the bells and
+ whistles as a command line application. This will always terminate
+ the application after a call. If this is not wanted, ``SystemExit``
+ needs to be caught.
+
+ This method is also available by directly calling the instance of
+ a :class:`Command`.
+
+ :param args: the arguments that should be used for parsing. If not
+ provided, ``sys.argv[1:]`` is used.
+ :param prog_name: the program name that should be used. By default
+ the program name is constructed by taking the file
+ name from ``sys.argv[0]``.
+ :param complete_var: the environment variable that controls the
+ bash completion support. The default is
+ ``"__COMPLETE"`` with prog_name in
+ uppercase.
+ :param standalone_mode: the default behavior is to invoke the script
+ in standalone mode. Click will then
+ handle exceptions and convert them into
+ error messages and the function will never
+ return but shut down the interpreter. If
+ this is set to `False` they will be
+ propagated to the caller and the return
+ value of this function is the return value
+ of :meth:`invoke`.
+ :param windows_expand_args: Expand glob patterns, user dir, and
+ env vars in command line args on Windows.
+ :param extra: extra keyword arguments are forwarded to the context
+ constructor. See :class:`Context` for more information.
+
+ .. versionchanged:: 8.0.1
+ Added the ``windows_expand_args`` parameter to allow
+ disabling command line arg expansion on Windows.
+
+ .. versionchanged:: 8.0
+ When taking arguments from ``sys.argv`` on Windows, glob
+ patterns, user dir, and env vars are expanded.
+
+ .. versionchanged:: 3.0
+ Added the ``standalone_mode`` parameter.
+ """
+ if args is None:
+ args = sys.argv[1:]
+
+ if os.name == "nt" and windows_expand_args:
+ args = _expand_args(args)
+ else:
+ args = list(args)
+
+ if prog_name is None:
+ prog_name = _detect_program_name()
+
+ # Process shell completion requests and exit early.
+ self._main_shell_completion(extra, prog_name, complete_var)
+
+ try:
+ try:
+ with self.make_context(prog_name, args, **extra) as ctx:
+ rv = self.invoke(ctx)
+ if not standalone_mode:
+ return rv
+ # it's not safe to `ctx.exit(rv)` here!
+ # note that `rv` may actually contain data like "1" which
+ # has obvious effects
+ # more subtle case: `rv=[None, None]` can come out of
+ # chained commands which all returned `None` -- so it's not
+ # even always obvious that `rv` indicates success/failure
+ # by its truthiness/falsiness
+ ctx.exit()
+ except (EOFError, KeyboardInterrupt) as e:
+ echo(file=sys.stderr)
+ raise Abort() from e
+ except ClickException as e:
+ if not standalone_mode:
+ raise
+ e.show()
+ sys.exit(e.exit_code)
+ except OSError as e:
+ if e.errno == errno.EPIPE:
+ sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout))
+ sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr))
+ sys.exit(1)
+ else:
+ raise
+ except Exit as e:
+ if standalone_mode:
+ sys.exit(e.exit_code)
+ else:
+ # in non-standalone mode, return the exit code
+ # note that this is only reached if `self.invoke` above raises
+ # an Exit explicitly -- thus bypassing the check there which
+ # would return its result
+ # the results of non-standalone execution may therefore be
+ # somewhat ambiguous: if there are codepaths which lead to
+ # `ctx.exit(1)` and to `return 1`, the caller won't be able to
+ # tell the difference between the two
+ return e.exit_code
+ except Abort:
+ if not standalone_mode:
+ raise
+ echo(_("Aborted!"), file=sys.stderr)
+ sys.exit(1)
+
+ def _main_shell_completion(
+ self,
+ ctx_args: cabc.MutableMapping[str, t.Any],
+ prog_name: str,
+ complete_var: str | None = None,
+ ) -> None:
+ """Check if the shell is asking for tab completion, process
+ that, then exit early. Called from :meth:`main` before the
+ program is invoked.
+
+ :param prog_name: Name of the executable in the shell.
+ :param complete_var: Name of the environment variable that holds
+ the completion instruction. Defaults to
+ ``_{PROG_NAME}_COMPLETE``.
+
+ .. versionchanged:: 8.2.0
+ Dots (``.``) in ``prog_name`` are replaced with underscores (``_``).
+ """
+ if complete_var is None:
+ complete_name = prog_name.replace("-", "_").replace(".", "_")
+ complete_var = f"_{complete_name}_COMPLETE".upper()
+
+ instruction = os.environ.get(complete_var)
+
+ if not instruction:
+ return
+
+ from .shell_completion import shell_complete
+
+ rv = shell_complete(self, ctx_args, prog_name, complete_var, instruction)
+ sys.exit(rv)
+
+ def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
+ """Alias for :meth:`main`."""
+ return self.main(*args, **kwargs)
+
+
+class _FakeSubclassCheck(type):
+ def __subclasscheck__(cls, subclass: type) -> bool:
+ return issubclass(subclass, cls.__bases__[0])
+
+ def __instancecheck__(cls, instance: t.Any) -> bool:
+ return isinstance(instance, cls.__bases__[0])
+
+
+class _BaseCommand(Command, metaclass=_FakeSubclassCheck):
+ """
+ .. deprecated:: 8.2
+ Will be removed in Click 9.0. Use ``Command`` instead.
+ """
+
+
+class Group(Command):
+ """A group is a command that nests other commands (or more groups).
+
+ :param name: The name of the group command.
+ :param commands: Map names to :class:`Command` objects. Can be a list, which
+ will use :attr:`Command.name` as the keys.
+ :param invoke_without_command: Invoke the group's callback even if a
+ subcommand is not given.
+ :param no_args_is_help: If no arguments are given, show the group's help and
+ exit. Defaults to the opposite of ``invoke_without_command``.
+ :param subcommand_metavar: How to represent the subcommand argument in help.
+ The default will represent whether ``chain`` is set or not.
+ :param chain: Allow passing more than one subcommand argument. After parsing
+ a command's arguments, if any arguments remain another command will be
+ matched, and so on.
+ :param result_callback: A function to call after the group's and
+ subcommand's callbacks. The value returned by the subcommand is passed.
+ If ``chain`` is enabled, the value will be a list of values returned by
+ all the commands. If ``invoke_without_command`` is enabled, the value
+ will be the value returned by the group's callback, or an empty list if
+ ``chain`` is enabled.
+ :param kwargs: Other arguments passed to :class:`Command`.
+
+ .. versionchanged:: 8.0
+ The ``commands`` argument can be a list of command objects.
+
+ .. versionchanged:: 8.2
+ Merged with and replaces the ``MultiCommand`` base class.
+ """
+
+ allow_extra_args = True
+ allow_interspersed_args = False
+
+ #: If set, this is used by the group's :meth:`command` decorator
+ #: as the default :class:`Command` class. This is useful to make all
+ #: subcommands use a custom command class.
+ #:
+ #: .. versionadded:: 8.0
+ command_class: type[Command] | None = None
+
+ #: If set, this is used by the group's :meth:`group` decorator
+ #: as the default :class:`Group` class. This is useful to make all
+ #: subgroups use a custom group class.
+ #:
+ #: If set to the special value :class:`type` (literally
+ #: ``group_class = type``), this group's class will be used as the
+ #: default class. This makes a custom group class continue to make
+ #: custom groups.
+ #:
+ #: .. versionadded:: 8.0
+ group_class: type[Group] | type[type] | None = None
+ # Literal[type] isn't valid, so use Type[type]
+
+ def __init__(
+ self,
+ name: str | None = None,
+ commands: cabc.MutableMapping[str, Command]
+ | cabc.Sequence[Command]
+ | None = None,
+ invoke_without_command: bool = False,
+ no_args_is_help: bool | None = None,
+ subcommand_metavar: str | None = None,
+ chain: bool = False,
+ result_callback: t.Callable[..., t.Any] | None = None,
+ **kwargs: t.Any,
+ ) -> None:
+ super().__init__(name, **kwargs)
+
+ if commands is None:
+ commands = {}
+ elif isinstance(commands, abc.Sequence):
+ commands = {c.name: c for c in commands if c.name is not None}
+
+ #: The registered subcommands by their exported names.
+ self.commands: cabc.MutableMapping[str, Command] = commands
+
+ if no_args_is_help is None:
+ no_args_is_help = not invoke_without_command
+
+ self.no_args_is_help = no_args_is_help
+ self.invoke_without_command = invoke_without_command
+
+ if subcommand_metavar is None:
+ if chain:
+ subcommand_metavar = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
+ else:
+ subcommand_metavar = "COMMAND [ARGS]..."
+
+ self.subcommand_metavar = subcommand_metavar
+ self.chain = chain
+ # The result callback that is stored. This can be set or
+ # overridden with the :func:`result_callback` decorator.
+ self._result_callback = result_callback
+
+ if self.chain:
+ for param in self.params:
+ if isinstance(param, Argument) and not param.required:
+ raise RuntimeError(
+ "A group in chain mode cannot have optional arguments."
+ )
+
+ def to_info_dict(self, ctx: Context) -> dict[str, t.Any]:
+ info_dict = super().to_info_dict(ctx)
+ commands = {}
+
+ for name in self.list_commands(ctx):
+ command = self.get_command(ctx, name)
+
+ if command is None:
+ continue
+
+ sub_ctx = ctx._make_sub_context(command)
+
+ with sub_ctx.scope(cleanup=False):
+ commands[name] = command.to_info_dict(sub_ctx)
+
+ info_dict.update(commands=commands, chain=self.chain)
+ return info_dict
+
+ def add_command(self, cmd: Command, name: str | None = None) -> None:
+ """Registers another :class:`Command` with this group. If the name
+ is not provided, the name of the command is used.
+ """
+ name = name or cmd.name
+ if name is None:
+ raise TypeError("Command has no name.")
+ _check_nested_chain(self, name, cmd, register=True)
+ self.commands[name] = cmd
+
+ @t.overload
+ def command(self, __func: t.Callable[..., t.Any]) -> Command: ...
+
+ @t.overload
+ def command(
+ self, *args: t.Any, **kwargs: t.Any
+ ) -> t.Callable[[t.Callable[..., t.Any]], Command]: ...
+
+ def command(
+ self, *args: t.Any, **kwargs: t.Any
+ ) -> t.Callable[[t.Callable[..., t.Any]], Command] | Command:
+ """A shortcut decorator for declaring and attaching a command to
+ the group. This takes the same arguments as :func:`command` and
+ immediately registers the created command with this group by
+ calling :meth:`add_command`.
+
+ To customize the command class used, set the
+ :attr:`command_class` attribute.
+
+ .. versionchanged:: 8.1
+ This decorator can be applied without parentheses.
+
+ .. versionchanged:: 8.0
+ Added the :attr:`command_class` attribute.
+ """
+ from .decorators import command
+
+ func: t.Callable[..., t.Any] | None = None
+
+ if args and callable(args[0]):
+ assert len(args) == 1 and not kwargs, (
+ "Use 'command(**kwargs)(callable)' to provide arguments."
+ )
+ (func,) = args
+ args = ()
+
+ if self.command_class and kwargs.get("cls") is None:
+ kwargs["cls"] = self.command_class
+
+ def decorator(f: t.Callable[..., t.Any]) -> Command:
+ cmd: Command = command(*args, **kwargs)(f)
+ self.add_command(cmd)
+ return cmd
+
+ if func is not None:
+ return decorator(func)
+
+ return decorator
+
+ @t.overload
+ def group(self, __func: t.Callable[..., t.Any]) -> Group: ...
+
+ @t.overload
+ def group(
+ self, *args: t.Any, **kwargs: t.Any
+ ) -> t.Callable[[t.Callable[..., t.Any]], Group]: ...
+
+ def group(
+ self, *args: t.Any, **kwargs: t.Any
+ ) -> t.Callable[[t.Callable[..., t.Any]], Group] | Group:
+ """A shortcut decorator for declaring and attaching a group to
+ the group. This takes the same arguments as :func:`group` and
+ immediately registers the created group with this group by
+ calling :meth:`add_command`.
+
+ To customize the group class used, set the :attr:`group_class`
+ attribute.
+
+ .. versionchanged:: 8.1
+ This decorator can be applied without parentheses.
+
+ .. versionchanged:: 8.0
+ Added the :attr:`group_class` attribute.
+ """
+ from .decorators import group
+
+ func: t.Callable[..., t.Any] | None = None
+
+ if args and callable(args[0]):
+ assert len(args) == 1 and not kwargs, (
+ "Use 'group(**kwargs)(callable)' to provide arguments."
+ )
+ (func,) = args
+ args = ()
+
+ if self.group_class is not None and kwargs.get("cls") is None:
+ if self.group_class is type:
+ kwargs["cls"] = type(self)
+ else:
+ kwargs["cls"] = self.group_class
+
+ def decorator(f: t.Callable[..., t.Any]) -> Group:
+ cmd: Group = group(*args, **kwargs)(f)
+ self.add_command(cmd)
+ return cmd
+
+ if func is not None:
+ return decorator(func)
+
+ return decorator
+
+ def result_callback(self, replace: bool = False) -> t.Callable[[F], F]:
+ """Adds a result callback to the command. By default if a
+ result callback is already registered this will chain them but
+ this can be disabled with the `replace` parameter. The result
+ callback is invoked with the return value of the subcommand
+ (or the list of return values from all subcommands if chaining
+ is enabled) as well as the parameters as they would be passed
+ to the main callback.
+
+ Example::
+
+ @click.group()
+ @click.option('-i', '--input', default=23)
+ def cli(input):
+ return 42
+
+ @cli.result_callback()
+ def process_result(result, input):
+ return result + input
+
+ :param replace: if set to `True` an already existing result
+ callback will be removed.
+
+ .. versionchanged:: 8.0
+ Renamed from ``resultcallback``.
+
+ .. versionadded:: 3.0
+ """
+
+ def decorator(f: F) -> F:
+ old_callback = self._result_callback
+
+ if old_callback is None or replace:
+ self._result_callback = f
+ return f
+
+ def function(value: t.Any, /, *args: t.Any, **kwargs: t.Any) -> t.Any:
+ inner = old_callback(value, *args, **kwargs)
+ return f(inner, *args, **kwargs)
+
+ self._result_callback = rv = update_wrapper(t.cast(F, function), f)
+ return rv # type: ignore[return-value]
+
+ return decorator
+
+ def get_command(self, ctx: Context, cmd_name: str) -> Command | None:
+ """Given a context and a command name, this returns a :class:`Command`
+ object if it exists or returns ``None``.
+ """
+ return self.commands.get(cmd_name)
+
+ def list_commands(self, ctx: Context) -> list[str]:
+ """Returns a list of subcommand names in the order they should appear."""
+ return sorted(self.commands)
+
+ def collect_usage_pieces(self, ctx: Context) -> list[str]:
+ rv = super().collect_usage_pieces(ctx)
+ rv.append(self.subcommand_metavar)
+ return rv
+
+ def format_options(self, ctx: Context, formatter: HelpFormatter) -> None:
+ super().format_options(ctx, formatter)
+ self.format_commands(ctx, formatter)
+
+ def format_commands(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Extra format methods for multi methods that adds all the commands
+ after the options.
+ """
+ commands = []
+ for subcommand in self.list_commands(ctx):
+ cmd = self.get_command(ctx, subcommand)
+ # What is this, the tool lied about a command. Ignore it
+ if cmd is None:
+ continue
+ if cmd.hidden:
+ continue
+
+ commands.append((subcommand, cmd))
+
+ # allow for 3 times the default spacing
+ if len(commands):
+ limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands)
+
+ rows = []
+ for subcommand, cmd in commands:
+ help = cmd.get_short_help_str(limit)
+ rows.append((subcommand, help))
+
+ if rows:
+ with formatter.section(_("Commands")):
+ formatter.write_dl(rows)
+
+ def parse_args(self, ctx: Context, args: list[str]) -> list[str]:
+ if not args and self.no_args_is_help and not ctx.resilient_parsing:
+ raise NoArgsIsHelpError(ctx)
+
+ rest = super().parse_args(ctx, args)
+
+ if self.chain:
+ ctx._protected_args = rest
+ ctx.args = []
+ elif rest:
+ ctx._protected_args, ctx.args = rest[:1], rest[1:]
+
+ return ctx.args
+
+ def invoke(self, ctx: Context) -> t.Any:
+ def _process_result(value: t.Any) -> t.Any:
+ if self._result_callback is not None:
+ value = ctx.invoke(self._result_callback, value, **ctx.params)
+ return value
+
+ if not ctx._protected_args:
+ if self.invoke_without_command:
+ # No subcommand was invoked, so the result callback is
+ # invoked with the group return value for regular
+ # groups, or an empty list for chained groups.
+ with ctx:
+ rv = super().invoke(ctx)
+ return _process_result([] if self.chain else rv)
+ ctx.fail(_("Missing command."))
+
+ # Fetch args back out
+ args = [*ctx._protected_args, *ctx.args]
+ ctx.args = []
+ ctx._protected_args = []
+
+ # If we're not in chain mode, we only allow the invocation of a
+ # single command but we also inform the current context about the
+ # name of the command to invoke.
+ if not self.chain:
+ # Make sure the context is entered so we do not clean up
+ # resources until the result processor has worked.
+ with ctx:
+ cmd_name, cmd, args = self.resolve_command(ctx, args)
+ assert cmd is not None
+ ctx.invoked_subcommand = cmd_name
+ super().invoke(ctx)
+ sub_ctx = cmd.make_context(cmd_name, args, parent=ctx)
+ with sub_ctx:
+ return _process_result(sub_ctx.command.invoke(sub_ctx))
+
+ # In chain mode we create the contexts step by step, but after the
+ # base command has been invoked. Because at that point we do not
+ # know the subcommands yet, the invoked subcommand attribute is
+ # set to ``*`` to inform the command that subcommands are executed
+ # but nothing else.
+ with ctx:
+ ctx.invoked_subcommand = "*" if args else None
+ super().invoke(ctx)
+
+ # Otherwise we make every single context and invoke them in a
+ # chain. In that case the return value to the result processor
+ # is the list of all invoked subcommand's results.
+ contexts = []
+ while args:
+ cmd_name, cmd, args = self.resolve_command(ctx, args)
+ assert cmd is not None
+ sub_ctx = cmd.make_context(
+ cmd_name,
+ args,
+ parent=ctx,
+ allow_extra_args=True,
+ allow_interspersed_args=False,
+ )
+ contexts.append(sub_ctx)
+ args, sub_ctx.args = sub_ctx.args, []
+
+ rv = []
+ for sub_ctx in contexts:
+ with sub_ctx:
+ rv.append(sub_ctx.command.invoke(sub_ctx))
+ return _process_result(rv)
+
+ def resolve_command(
+ self, ctx: Context, args: list[str]
+ ) -> tuple[str | None, Command | None, list[str]]:
+ cmd_name = make_str(args[0])
+ original_cmd_name = cmd_name
+
+ # Get the command
+ cmd = self.get_command(ctx, cmd_name)
+
+ # If we can't find the command but there is a normalization
+ # function available, we try with that one.
+ if cmd is None and ctx.token_normalize_func is not None:
+ cmd_name = ctx.token_normalize_func(cmd_name)
+ cmd = self.get_command(ctx, cmd_name)
+
+ # If we don't find the command we want to show an error message
+ # to the user that it was not provided. However, there is
+ # something else we should do: if the first argument looks like
+ # an option we want to kick off parsing again for arguments to
+ # resolve things like --help which now should go to the main
+ # place.
+ if cmd is None and not ctx.resilient_parsing:
+ if _split_opt(cmd_name)[0]:
+ self.parse_args(ctx, args)
+ ctx.fail(_("No such command {name!r}.").format(name=original_cmd_name))
+ return cmd_name if cmd else None, cmd, args[1:]
+
+ def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]:
+ """Return a list of completions for the incomplete value. Looks
+ at the names of options, subcommands, and chained
+ multi-commands.
+
+ :param ctx: Invocation context for this command.
+ :param incomplete: Value being completed. May be empty.
+
+ .. versionadded:: 8.0
+ """
+ from click.shell_completion import CompletionItem
+
+ results = [
+ CompletionItem(name, help=command.get_short_help_str())
+ for name, command in _complete_visible_commands(ctx, incomplete)
+ ]
+ results.extend(super().shell_complete(ctx, incomplete))
+ return results
+
+
+class _MultiCommand(Group, metaclass=_FakeSubclassCheck):
+ """
+ .. deprecated:: 8.2
+ Will be removed in Click 9.0. Use ``Group`` instead.
+ """
+
+
+class CommandCollection(Group):
+ """A :class:`Group` that looks up subcommands on other groups. If a command
+ is not found on this group, each registered source is checked in order.
+ Parameters on a source are not added to this group, and a source's callback
+ is not invoked when invoking its commands. In other words, this "flattens"
+ commands in many groups into this one group.
+
+ :param name: The name of the group command.
+ :param sources: A list of :class:`Group` objects to look up commands from.
+ :param kwargs: Other arguments passed to :class:`Group`.
+
+ .. versionchanged:: 8.2
+ This is a subclass of ``Group``. Commands are looked up first on this
+ group, then each of its sources.
+ """
+
+ def __init__(
+ self,
+ name: str | None = None,
+ sources: list[Group] | None = None,
+ **kwargs: t.Any,
+ ) -> None:
+ super().__init__(name, **kwargs)
+ #: The list of registered groups.
+ self.sources: list[Group] = sources or []
+
+ def add_source(self, group: Group) -> None:
+ """Add a group as a source of commands."""
+ self.sources.append(group)
+
+ def get_command(self, ctx: Context, cmd_name: str) -> Command | None:
+ rv = super().get_command(ctx, cmd_name)
+
+ if rv is not None:
+ return rv
+
+ for source in self.sources:
+ rv = source.get_command(ctx, cmd_name)
+
+ if rv is not None:
+ if self.chain:
+ _check_nested_chain(self, cmd_name, rv)
+
+ return rv
+
+ return None
+
+ def list_commands(self, ctx: Context) -> list[str]:
+ rv: set[str] = set(super().list_commands(ctx))
+
+ for source in self.sources:
+ rv.update(source.list_commands(ctx))
+
+ return sorted(rv)
+
+
+def _check_iter(value: t.Any) -> cabc.Iterator[t.Any]:
+ """Check if the value is iterable but not a string. Raises a type
+ error, or return an iterator over the value.
+ """
+ if isinstance(value, str):
+ raise TypeError
+
+ return iter(value)
+
+
+class Parameter:
+ r"""A parameter to a command comes in two versions: they are either
+ :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently
+ not supported by design as some of the internals for parsing are
+ intentionally not finalized.
+
+ Some settings are supported by both options and arguments.
+
+ :param param_decls: the parameter declarations for this option or
+ argument. This is a list of flags or argument
+ names.
+ :param type: the type that should be used. Either a :class:`ParamType`
+ or a Python type. The latter is converted into the former
+ automatically if supported.
+ :param required: controls if this is optional or not.
+ :param default: the default value if omitted. This can also be a callable,
+ in which case it's invoked when the default is needed
+ without any arguments.
+ :param callback: A function to further process or validate the value
+ after type conversion. It is called as ``f(ctx, param, value)``
+ and must return the value. It is called for all sources,
+ including prompts.
+ :param nargs: the number of arguments to match. If not ``1`` the return
+ value is a tuple instead of single value. The default for
+ nargs is ``1`` (except if the type is a tuple, then it's
+ the arity of the tuple). If ``nargs=-1``, all remaining
+ parameters are collected.
+ :param metavar: how the value is represented in the help page.
+ :param expose_value: if this is `True` then the value is passed onwards
+ to the command callback and stored on the context,
+ otherwise it's skipped.
+ :param is_eager: eager values are processed before non eager ones. This
+ should not be set for arguments or it will inverse the
+ order of processing.
+ :param envvar: environment variable(s) that are used to provide a default value for
+ this parameter. This can be a string or a sequence of strings. If a sequence is
+ given, only the first non-empty environment variable is used for the parameter.
+ :param shell_complete: A function that returns custom shell
+ completions. Used instead of the param's type completion if
+ given. Takes ``ctx, param, incomplete`` and must return a list
+ of :class:`~click.shell_completion.CompletionItem` or a list of
+ strings.
+ :param deprecated: If ``True`` or non-empty string, issues a message
+ indicating that the argument is deprecated and highlights
+ its deprecation in --help. The message can be customized
+ by using a string as the value. A deprecated parameter
+ cannot be required, a ValueError will be raised otherwise.
+
+ .. versionchanged:: 8.2.0
+ Introduction of ``deprecated``.
+
+ .. versionchanged:: 8.2
+ Adding duplicate parameter names to a :class:`~click.core.Command` will
+ result in a ``UserWarning`` being shown.
+
+ .. versionchanged:: 8.2
+ Adding duplicate parameter names to a :class:`~click.core.Command` will
+ result in a ``UserWarning`` being shown.
+
+ .. versionchanged:: 8.0
+ ``process_value`` validates required parameters and bounded
+ ``nargs``, and invokes the parameter callback before returning
+ the value. This allows the callback to validate prompts.
+ ``full_process_value`` is removed.
+
+ .. versionchanged:: 8.0
+ ``autocompletion`` is renamed to ``shell_complete`` and has new
+ semantics described above. The old name is deprecated and will
+ be removed in 8.1, until then it will be wrapped to match the
+ new requirements.
+
+ .. versionchanged:: 8.0
+ For ``multiple=True, nargs>1``, the default must be a list of
+ tuples.
+
+ .. versionchanged:: 8.0
+ Setting a default is no longer required for ``nargs>1``, it will
+ default to ``None``. ``multiple=True`` or ``nargs=-1`` will
+ default to ``()``.
+
+ .. versionchanged:: 7.1
+ Empty environment variables are ignored rather than taking the
+ empty string value. This makes it possible for scripts to clear
+ variables if they can't unset them.
+
+ .. versionchanged:: 2.0
+ Changed signature for parameter callback to also be passed the
+ parameter. The old callback format will still work, but it will
+ raise a warning to give you a chance to migrate the code easier.
+ """
+
+ param_type_name = "parameter"
+
+ def __init__(
+ self,
+ param_decls: cabc.Sequence[str] | None = None,
+ type: types.ParamType | t.Any | None = None,
+ required: bool = False,
+ # XXX The default historically embed two concepts:
+ # - the declaration of a Parameter object carrying the default (handy to
+ # arbitrage the default value of coupled Parameters sharing the same
+ # self.name, like flag options),
+ # - and the actual value of the default.
+ # It is confusing and is the source of many issues discussed in:
+ # https://github.com/pallets/click/pull/3030
+ # In the future, we might think of splitting it in two, not unlike
+ # Option.is_flag and Option.flag_value: we could have something like
+ # Parameter.is_default and Parameter.default_value.
+ default: t.Any | t.Callable[[], t.Any] | None = UNSET,
+ callback: t.Callable[[Context, Parameter, t.Any], t.Any] | None = None,
+ nargs: int | None = None,
+ multiple: bool = False,
+ metavar: str | None = None,
+ expose_value: bool = True,
+ is_eager: bool = False,
+ envvar: str | cabc.Sequence[str] | None = None,
+ shell_complete: t.Callable[
+ [Context, Parameter, str], list[CompletionItem] | list[str]
+ ]
+ | None = None,
+ deprecated: bool | str = False,
+ ) -> None:
+ self.name: str | None
+ self.opts: list[str]
+ self.secondary_opts: list[str]
+ self.name, self.opts, self.secondary_opts = self._parse_decls(
+ param_decls or (), expose_value
+ )
+ self.type: types.ParamType = types.convert_type(type, default)
+
+ # Default nargs to what the type tells us if we have that
+ # information available.
+ if nargs is None:
+ if self.type.is_composite:
+ nargs = self.type.arity
+ else:
+ nargs = 1
+
+ self.required = required
+ self.callback = callback
+ self.nargs = nargs
+ self.multiple = multiple
+ self.expose_value = expose_value
+ self.default: t.Any | t.Callable[[], t.Any] | None = default
+ self.is_eager = is_eager
+ self.metavar = metavar
+ self.envvar = envvar
+ self._custom_shell_complete = shell_complete
+ self.deprecated = deprecated
+
+ if __debug__:
+ if self.type.is_composite and nargs != self.type.arity:
+ raise ValueError(
+ f"'nargs' must be {self.type.arity} (or None) for"
+ f" type {self.type!r}, but it was {nargs}."
+ )
+
+ if required and deprecated:
+ raise ValueError(
+ f"The {self.param_type_name} '{self.human_readable_name}' "
+ "is deprecated and still required. A deprecated "
+ f"{self.param_type_name} cannot be required."
+ )
+
+ def to_info_dict(self) -> dict[str, t.Any]:
+ """Gather information that could be useful for a tool generating
+ user-facing documentation.
+
+ Use :meth:`click.Context.to_info_dict` to traverse the entire
+ CLI structure.
+
+ .. versionchanged:: 8.3.0
+ Returns ``None`` for the :attr:`default` if it was not set.
+
+ .. versionadded:: 8.0
+ """
+ return {
+ "name": self.name,
+ "param_type_name": self.param_type_name,
+ "opts": self.opts,
+ "secondary_opts": self.secondary_opts,
+ "type": self.type.to_info_dict(),
+ "required": self.required,
+ "nargs": self.nargs,
+ "multiple": self.multiple,
+ # We explicitly hide the :attr:`UNSET` value to the user, as we choose to
+ # make it an implementation detail. And because ``to_info_dict`` has been
+ # designed for documentation purposes, we return ``None`` instead.
+ "default": self.default if self.default is not UNSET else None,
+ "envvar": self.envvar,
+ }
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__.__name__} {self.name}>"
+
+ def _parse_decls(
+ self, decls: cabc.Sequence[str], expose_value: bool
+ ) -> tuple[str | None, list[str], list[str]]:
+ raise NotImplementedError()
+
+ @property
+ def human_readable_name(self) -> str:
+ """Returns the human readable name of this parameter. This is the
+ same as the name for options, but the metavar for arguments.
+ """
+ return self.name # type: ignore
+
+ def make_metavar(self, ctx: Context) -> str:
+ if self.metavar is not None:
+ return self.metavar
+
+ metavar = self.type.get_metavar(param=self, ctx=ctx)
+
+ if metavar is None:
+ metavar = self.type.name.upper()
+
+ if self.nargs != 1:
+ metavar += "..."
+
+ return metavar
+
+ @t.overload
+ def get_default(
+ self, ctx: Context, call: t.Literal[True] = True
+ ) -> t.Any | None: ...
+
+ @t.overload
+ def get_default(
+ self, ctx: Context, call: bool = ...
+ ) -> t.Any | t.Callable[[], t.Any] | None: ...
+
+ def get_default(
+ self, ctx: Context, call: bool = True
+ ) -> t.Any | t.Callable[[], t.Any] | None:
+ """Get the default for the parameter. Tries
+ :meth:`Context.lookup_default` first, then the local default.
+
+ :param ctx: Current context.
+ :param call: If the default is a callable, call it. Disable to
+ return the callable instead.
+
+ .. versionchanged:: 8.0.2
+ Type casting is no longer performed when getting a default.
+
+ .. versionchanged:: 8.0.1
+ Type casting can fail in resilient parsing mode. Invalid
+ defaults will not prevent showing help text.
+
+ .. versionchanged:: 8.0
+ Looks at ``ctx.default_map`` first.
+
+ .. versionchanged:: 8.0
+ Added the ``call`` parameter.
+ """
+ name = self.name
+ value = ctx.lookup_default(name, call=False) if name is not None else None
+
+ if value is None and not ctx._default_map_has(name):
+ value = self.default
+
+ if call and callable(value):
+ value = value()
+
+ return value
+
+ def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None:
+ raise NotImplementedError()
+
+ def consume_value(
+ self, ctx: Context, opts: cabc.Mapping[str, t.Any]
+ ) -> tuple[t.Any, ParameterSource]:
+ """Returns the parameter value produced by the parser.
+
+ If the parser did not produce a value from user input, the value is either
+ sourced from the environment variable, the default map, or the parameter's
+ default value. In that order of precedence.
+
+ If no value is found, an internal sentinel value is returned.
+
+ :meta private:
+ """
+ # Collect from the parse the value passed by the user to the CLI.
+ value = opts.get(self.name, UNSET) # type: ignore
+ # If the value is set, it means it was sourced from the command line by the
+ # parser, otherwise it left unset by default.
+ source = (
+ ParameterSource.COMMANDLINE
+ if value is not UNSET
+ else ParameterSource.DEFAULT
+ )
+
+ if value is UNSET:
+ envvar_value = self.value_from_envvar(ctx)
+ if envvar_value is not None:
+ value = envvar_value
+ source = ParameterSource.ENVIRONMENT
+
+ if value is UNSET:
+ default_map_value = ctx.lookup_default(self.name) # type: ignore[arg-type]
+ if default_map_value is not None or ctx._default_map_has(self.name):
+ value = default_map_value
+ source = ParameterSource.DEFAULT_MAP
+
+ if value is UNSET:
+ default_value = self.get_default(ctx)
+ if default_value is not UNSET:
+ value = default_value
+ source = ParameterSource.DEFAULT
+
+ return value, source
+
+ def type_cast_value(self, ctx: Context, value: t.Any) -> t.Any:
+ """Convert and validate a value against the parameter's
+ :attr:`type`, :attr:`multiple`, and :attr:`nargs`.
+ """
+ if value is None:
+ if self.multiple or self.nargs == -1:
+ return ()
+ else:
+ return value
+
+ def check_iter(value: t.Any) -> cabc.Iterator[t.Any]:
+ try:
+ return _check_iter(value)
+ except TypeError:
+ # This should only happen when passing in args manually,
+ # the parser should construct an iterable when parsing
+ # the command line.
+ raise BadParameter(
+ _("Value must be an iterable."), ctx=ctx, param=self
+ ) from None
+
+ # Define the conversion function based on nargs and type.
+
+ if self.nargs == 1 or self.type.is_composite:
+
+ def convert(value: t.Any) -> t.Any:
+ return self.type(value, param=self, ctx=ctx)
+
+ elif self.nargs == -1:
+
+ def convert(value: t.Any) -> t.Any: # tuple[t.Any, ...]
+ return tuple(self.type(x, self, ctx) for x in check_iter(value))
+
+ else: # nargs > 1
+
+ def convert(value: t.Any) -> t.Any: # tuple[t.Any, ...]
+ value = tuple(check_iter(value))
+
+ if len(value) != self.nargs:
+ raise BadParameter(
+ ngettext(
+ "Takes {nargs} values but 1 was given.",
+ "Takes {nargs} values but {len} were given.",
+ len(value),
+ ).format(nargs=self.nargs, len=len(value)),
+ ctx=ctx,
+ param=self,
+ )
+
+ return tuple(self.type(x, self, ctx) for x in value)
+
+ if self.multiple:
+ return tuple(convert(x) for x in check_iter(value))
+
+ return convert(value)
+
+ def value_is_missing(self, value: t.Any) -> bool:
+ """A value is considered missing if:
+
+ - it is :attr:`UNSET`,
+ - or if it is an empty sequence while the parameter is suppose to have
+ non-single value (i.e. :attr:`nargs` is not ``1`` or :attr:`multiple` is
+ set).
+
+ :meta private:
+ """
+ if value is UNSET:
+ return True
+
+ if (self.nargs != 1 or self.multiple) and value == ():
+ return True
+
+ return False
+
+ def process_value(self, ctx: Context, value: t.Any) -> t.Any:
+ """Process the value of this parameter:
+
+ 1. Type cast the value using :meth:`type_cast_value`.
+ 2. Check if the value is missing (see: :meth:`value_is_missing`), and raise
+ :exc:`MissingParameter` if it is required.
+ 3. If a :attr:`callback` is set, call it to have the value replaced by the
+ result of the callback. If the value was not set, the callback receive
+ ``None``. This keep the legacy behavior as it was before the introduction of
+ the :attr:`UNSET` sentinel.
+
+ :meta private:
+ """
+ # shelter `type_cast_value` from ever seeing an `UNSET` value by handling the
+ # cases in which `UNSET` gets special treatment explicitly at this layer
+ #
+ # Refs:
+ # https://github.com/pallets/click/issues/3069
+ if value is UNSET:
+ if self.multiple or self.nargs == -1:
+ value = ()
+ else:
+ value = self.type_cast_value(ctx, value)
+
+ if self.required and self.value_is_missing(value):
+ raise MissingParameter(ctx=ctx, param=self)
+
+ if self.callback is not None:
+ # Legacy case: UNSET is not exposed directly to the callback, but converted
+ # to None.
+ if value is UNSET:
+ value = None
+
+ # Search for parameters with UNSET values in the context.
+ unset_keys = {k: None for k, v in ctx.params.items() if v is UNSET}
+ # No UNSET values, call the callback as usual.
+ if not unset_keys:
+ value = self.callback(ctx, self, value)
+
+ # Legacy case: provide a temporarily manipulated context to the callback
+ # to hide UNSET values as None.
+ #
+ # Refs:
+ # https://github.com/pallets/click/issues/3136
+ # https://github.com/pallets/click/pull/3137
+ else:
+ # Add another layer to the context stack to clearly hint that the
+ # context is temporarily modified.
+ with ctx:
+ # Update the context parameters to replace UNSET with None.
+ ctx.params.update(unset_keys)
+ # Feed these fake context parameters to the callback.
+ value = self.callback(ctx, self, value)
+ # Restore the UNSET values in the context parameters.
+ ctx.params.update(
+ {
+ k: UNSET
+ for k in unset_keys
+ # Only restore keys that are present and still None, in case
+ # the callback modified other parameters.
+ if k in ctx.params and ctx.params[k] is None
+ }
+ )
+
+ return value
+
+ def resolve_envvar_value(self, ctx: Context) -> str | None:
+ """Returns the value found in the environment variable(s) attached to this
+ parameter.
+
+ Environment variables values are `always returned as strings
+ `_.
+
+ This method returns ``None`` if:
+
+ - the :attr:`envvar` property is not set on the :class:`Parameter`,
+ - the environment variable is not found in the environment,
+ - the variable is found in the environment but its value is empty (i.e. the
+ environment variable is present but has an empty string).
+
+ If :attr:`envvar` is setup with multiple environment variables,
+ then only the first non-empty value is returned.
+
+ .. caution::
+
+ The raw value extracted from the environment is not normalized and is
+ returned as-is. Any normalization or reconciliation is performed later by
+ the :class:`Parameter`'s :attr:`type`.
+
+ :meta private:
+ """
+ if not self.envvar:
+ return None
+
+ if isinstance(self.envvar, str):
+ rv = os.environ.get(self.envvar)
+
+ if rv:
+ return rv
+ else:
+ for envvar in self.envvar:
+ rv = os.environ.get(envvar)
+
+ # Return the first non-empty value of the list of environment variables.
+ if rv:
+ return rv
+ # Else, absence of value is interpreted as an environment variable that
+ # is not set, so proceed to the next one.
+
+ return None
+
+ def value_from_envvar(self, ctx: Context) -> str | cabc.Sequence[str] | None:
+ """Process the raw environment variable string for this parameter.
+
+ Returns the string as-is or splits it into a sequence of strings if the
+ parameter is expecting multiple values (i.e. its :attr:`nargs` property is set
+ to a value other than ``1``).
+
+ :meta private:
+ """
+ rv = self.resolve_envvar_value(ctx)
+
+ if rv is not None and self.nargs != 1:
+ return self.type.split_envvar_value(rv)
+
+ return rv
+
+ def handle_parse_result(
+ self, ctx: Context, opts: cabc.Mapping[str, t.Any], args: list[str]
+ ) -> tuple[t.Any, list[str]]:
+ """Process the value produced by the parser from user input.
+
+ Always process the value through the Parameter's :attr:`type`, wherever it
+ comes from.
+
+ If the parameter is deprecated, this method warn the user about it. But only if
+ the value has been explicitly set by the user (and as such, is not coming from
+ a default).
+
+ :meta private:
+ """
+ with augment_usage_errors(ctx, param=self):
+ value, source = self.consume_value(ctx, opts)
+
+ ctx.set_parameter_source(self.name, source) # type: ignore
+
+ # Display a deprecation warning if necessary.
+ if (
+ self.deprecated
+ and value is not UNSET
+ and source < ParameterSource.DEFAULT_MAP
+ ):
+ extra_message = (
+ f" {self.deprecated}" if isinstance(self.deprecated, str) else ""
+ )
+ message = _(
+ "DeprecationWarning: The {param_type} {name!r} is deprecated."
+ "{extra_message}"
+ ).format(
+ param_type=self.param_type_name,
+ name=self.human_readable_name,
+ extra_message=extra_message,
+ )
+ echo(style(message, fg="red"), err=True)
+
+ # Process the value through the parameter's type.
+ try:
+ value = self.process_value(ctx, value)
+ except Exception:
+ if not ctx.resilient_parsing:
+ raise
+ # In resilient parsing mode, we do not want to fail the command if the
+ # value is incompatible with the parameter type, so we reset the value
+ # to UNSET, which will be interpreted as a missing value.
+ value = UNSET
+
+ # Add parameter's value to the context.
+ if (
+ self.expose_value
+ # We skip adding the value if it was previously set by another parameter
+ # targeting the same variable name. This prevents parameters competing for
+ # the same name to override each other.
+ and (self.name not in ctx.params or ctx.params[self.name] is UNSET)
+ ):
+ # Click is logically enforcing that the name is None if the parameter is
+ # not to be exposed. We still assert it here to please the type checker.
+ assert self.name is not None, (
+ f"{self!r} parameter's name should not be None when exposing value."
+ )
+ ctx.params[self.name] = value
+
+ return value, args
+
+ def get_help_record(self, ctx: Context) -> tuple[str, str] | None:
+ pass
+
+ def get_usage_pieces(self, ctx: Context) -> list[str]:
+ return []
+
+ def get_error_hint(self, ctx: Context) -> str:
+ """Get a stringified version of the param for use in error messages to
+ indicate which param caused the error.
+ """
+ hint_list = self.opts or [self.human_readable_name]
+ return " / ".join(f"'{x}'" for x in hint_list)
+
+ def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]:
+ """Return a list of completions for the incomplete value. If a
+ ``shell_complete`` function was given during init, it is used.
+ Otherwise, the :attr:`type`
+ :meth:`~click.types.ParamType.shell_complete` function is used.
+
+ :param ctx: Invocation context for this command.
+ :param incomplete: Value being completed. May be empty.
+
+ .. versionadded:: 8.0
+ """
+ if self._custom_shell_complete is not None:
+ results = self._custom_shell_complete(ctx, self, incomplete)
+
+ if results and isinstance(results[0], str):
+ from click.shell_completion import CompletionItem
+
+ results = [CompletionItem(c) for c in results]
+
+ return t.cast("list[CompletionItem]", results)
+
+ return self.type.shell_complete(ctx, self, incomplete)
+
+
+class Option(Parameter):
+ """Options are usually optional values on the command line and
+ have some extra features that arguments don't have.
+
+ All other parameters are passed onwards to the parameter constructor.
+
+ :param show_default: Show the default value for this option in its
+ help text. Values are not shown by default, unless
+ :attr:`Context.show_default` is ``True``. If this value is a
+ string, it shows that string in parentheses instead of the
+ actual value. This is particularly useful for dynamic options.
+ For single option boolean flags, the default remains hidden if
+ its value is ``False``.
+ :param show_envvar: Controls if an environment variable should be
+ shown on the help page and error messages.
+ Normally, environment variables are not shown.
+ :param prompt: If set to ``True`` or a non empty string then the
+ user will be prompted for input. If set to ``True`` the prompt
+ will be the option name capitalized. A deprecated option cannot be
+ prompted.
+ :param confirmation_prompt: Prompt a second time to confirm the
+ value if it was prompted for. Can be set to a string instead of
+ ``True`` to customize the message.
+ :param prompt_required: If set to ``False``, the user will be
+ prompted for input only when the option was specified as a flag
+ without a value.
+ :param hide_input: If this is ``True`` then the input on the prompt
+ will be hidden from the user. This is useful for password input.
+ :param is_flag: forces this option to act as a flag. The default is
+ auto detection.
+ :param flag_value: which value should be used for this flag if it's
+ enabled. This is set to a boolean automatically if
+ the option string contains a slash to mark two options.
+ :param multiple: if this is set to `True` then the argument is accepted
+ multiple times and recorded. This is similar to ``nargs``
+ in how it works but supports arbitrary number of
+ arguments.
+ :param count: this flag makes an option increment an integer.
+ :param allow_from_autoenv: if this is enabled then the value of this
+ parameter will be pulled from an environment
+ variable in case a prefix is defined on the
+ context.
+ :param help: the help string.
+ :param hidden: hide this option from help outputs.
+ :param attrs: Other command arguments described in :class:`Parameter`.
+
+ .. versionchanged:: 8.2
+ ``envvar`` used with ``flag_value`` will always use the ``flag_value``,
+ previously it would use the value of the environment variable.
+
+ .. versionchanged:: 8.1
+ Help text indentation is cleaned here instead of only in the
+ ``@option`` decorator.
+
+ .. versionchanged:: 8.1
+ The ``show_default`` parameter overrides
+ ``Context.show_default``.
+
+ .. versionchanged:: 8.1
+ The default of a single option boolean flag is not shown if the
+ default value is ``False``.
+
+ .. versionchanged:: 8.0.1
+ ``type`` is detected from ``flag_value`` if given.
+ """
+
+ param_type_name = "option"
+
+ def __init__(
+ self,
+ param_decls: cabc.Sequence[str] | None = None,
+ show_default: bool | str | None = None,
+ prompt: bool | str = False,
+ confirmation_prompt: bool | str = False,
+ prompt_required: bool = True,
+ hide_input: bool = False,
+ is_flag: bool | None = None,
+ flag_value: t.Any = UNSET,
+ multiple: bool = False,
+ count: bool = False,
+ allow_from_autoenv: bool = True,
+ type: types.ParamType | t.Any | None = None,
+ help: str | None = None,
+ hidden: bool = False,
+ show_choices: bool = True,
+ show_envvar: bool = False,
+ deprecated: bool | str = False,
+ **attrs: t.Any,
+ ) -> None:
+ if help:
+ help = inspect.cleandoc(help)
+
+ super().__init__(
+ param_decls, type=type, multiple=multiple, deprecated=deprecated, **attrs
+ )
+
+ if prompt is True:
+ if self.name is None:
+ raise TypeError("'name' is required with 'prompt=True'.")
+
+ prompt_text: str | None = self.name.replace("_", " ").capitalize()
+ elif prompt is False:
+ prompt_text = None
+ else:
+ prompt_text = prompt
+
+ if deprecated:
+ deprecated_message = (
+ f"(DEPRECATED: {deprecated})"
+ if isinstance(deprecated, str)
+ else "(DEPRECATED)"
+ )
+ help = help + deprecated_message if help is not None else deprecated_message
+
+ self.prompt = prompt_text
+ self.confirmation_prompt = confirmation_prompt
+ self.prompt_required = prompt_required
+ self.hide_input = hide_input
+ self.hidden = hidden
+
+ # The _flag_needs_value property tells the parser that this option is a flag
+ # that cannot be used standalone and needs a value. With this information, the
+ # parser can determine whether to consider the next user-provided argument in
+ # the CLI as a value for this flag or as a new option.
+ # If prompt is enabled but not required, then it opens the possibility for the
+ # option to gets its value from the user.
+ self._flag_needs_value = self.prompt is not None and not self.prompt_required
+
+ # Auto-detect if this is a flag or not.
+ if is_flag is None:
+ # Implicitly a flag because flag_value was set.
+ if flag_value is not UNSET:
+ is_flag = True
+ # Not a flag, but when used as a flag it shows a prompt.
+ elif self._flag_needs_value:
+ is_flag = False
+ # Implicitly a flag because secondary options names were given.
+ elif self.secondary_opts:
+ is_flag = True
+
+ # The option is explicitly not a flag, but to determine whether or not it needs
+ # value, we need to check if `flag_value` or `default` was set. Either one is
+ # sufficient.
+ # Ref: https://github.com/pallets/click/issues/3084
+ elif is_flag is False and not self._flag_needs_value:
+ self._flag_needs_value = flag_value is not UNSET or self.default is UNSET
+
+ if is_flag:
+ # Set missing default for flags if not explicitly required or prompted.
+ if self.default is UNSET and not self.required and not self.prompt:
+ if multiple:
+ self.default = ()
+
+ # Auto-detect the type of the flag based on the flag_value.
+ if type is None:
+ # A flag without a flag_value is a boolean flag.
+ if flag_value is UNSET:
+ self.type: types.ParamType = types.BoolParamType()
+ # If the flag value is a boolean, use BoolParamType.
+ elif isinstance(flag_value, bool):
+ self.type = types.BoolParamType()
+ # Otherwise, guess the type from the flag value.
+ else:
+ self.type = types.convert_type(None, flag_value)
+
+ self.is_flag: bool = bool(is_flag)
+ self.is_bool_flag: bool = bool(
+ is_flag and isinstance(self.type, types.BoolParamType)
+ )
+ self.flag_value: t.Any = flag_value
+
+ # Set boolean flag default to False if unset and not required.
+ if self.is_bool_flag:
+ if self.default is UNSET and not self.required:
+ self.default = False
+
+ # The alignement of default to the flag_value is resolved lazily in
+ # get_default() to prevent callable flag_values (like classes) from
+ # being instantiated. Refs:
+ # https://github.com/pallets/click/issues/3121
+ # https://github.com/pallets/click/issues/3024#issuecomment-3146199461
+ # https://github.com/pallets/click/pull/3030/commits/06847da
+
+ # Set the default flag_value if it is not set.
+ if self.flag_value is UNSET:
+ if self.is_flag:
+ self.flag_value = True
+ else:
+ self.flag_value = None
+
+ # Counting.
+ self.count = count
+ if count:
+ if type is None:
+ self.type = types.IntRange(min=0)
+ if self.default is UNSET:
+ self.default = 0
+
+ self.allow_from_autoenv = allow_from_autoenv
+ self.help = help
+ self.show_default = show_default
+ self.show_choices = show_choices
+ self.show_envvar = show_envvar
+
+ if __debug__:
+ if deprecated and prompt:
+ raise ValueError("`deprecated` options cannot use `prompt`.")
+
+ if self.nargs == -1:
+ raise TypeError("nargs=-1 is not supported for options.")
+
+ if not self.is_bool_flag and self.secondary_opts:
+ raise TypeError("Secondary flag is not valid for non-boolean flag.")
+
+ if self.is_bool_flag and self.hide_input and self.prompt is not None:
+ raise TypeError(
+ "'prompt' with 'hide_input' is not valid for boolean flag."
+ )
+
+ if self.count:
+ if self.multiple:
+ raise TypeError("'count' is not valid with 'multiple'.")
+
+ if self.is_flag:
+ raise TypeError("'count' is not valid with 'is_flag'.")
+
+ def to_info_dict(self) -> dict[str, t.Any]:
+ """
+ .. versionchanged:: 8.3.0
+ Returns ``None`` for the :attr:`flag_value` if it was not set.
+ """
+ info_dict = super().to_info_dict()
+ info_dict.update(
+ help=self.help,
+ prompt=self.prompt,
+ is_flag=self.is_flag,
+ # We explicitly hide the :attr:`UNSET` value to the user, as we choose to
+ # make it an implementation detail. And because ``to_info_dict`` has been
+ # designed for documentation purposes, we return ``None`` instead.
+ flag_value=self.flag_value if self.flag_value is not UNSET else None,
+ count=self.count,
+ hidden=self.hidden,
+ )
+ return info_dict
+
+ def get_default(
+ self, ctx: Context, call: bool = True
+ ) -> t.Any | t.Callable[[], t.Any] | None:
+ """Return the default value for this option.
+
+ For non-boolean flag options, ``default=True`` is treated as a sentinel
+ meaning "activate this flag by default" and is resolved to
+ :attr:`flag_value`. For example, with ``--upper/--lower`` feature
+ switches where ``flag_value="upper"`` and ``default=True``, the default
+ resolves to ``"upper"``.
+
+ .. caution::
+ This substitution only applies to non-boolean flags
+ (:attr:`is_bool_flag` is ``False``). For boolean flags, ``True`` is
+ a legitimate Python value and ``default=True`` is returned as-is.
+
+ .. versionchanged:: 8.3.3
+ ``default=True`` is no longer substituted with ``flag_value`` for
+ boolean flags, fixing negative boolean flags like
+ ``flag_value=False, default=True``.
+ """
+ value = super().get_default(ctx, call=False)
+
+ # Resolve default=True to flag_value lazily (here instead of
+ # __init__) to prevent callable flag_values (like classes) from
+ # being instantiated by the callable check below.
+ if value is True and self.is_flag and not self.is_bool_flag:
+ value = self.flag_value
+ elif call and callable(value):
+ value = value()
+
+ return value
+
+ def get_error_hint(self, ctx: Context) -> str:
+ result = super().get_error_hint(ctx)
+ if self.show_envvar and self.envvar is not None:
+ result += f" (env var: '{self.envvar}')"
+ return result
+
+ def _parse_decls(
+ self, decls: cabc.Sequence[str], expose_value: bool
+ ) -> tuple[str | None, list[str], list[str]]:
+ opts = []
+ secondary_opts = []
+ name = None
+ possible_names = []
+
+ for decl in decls:
+ if decl.isidentifier():
+ if name is not None:
+ raise TypeError(f"Name '{name}' defined twice")
+ name = decl
+ else:
+ split_char = ";" if decl[:1] == "/" else "/"
+ if split_char in decl:
+ first, second = decl.split(split_char, 1)
+ first = first.rstrip()
+ if first:
+ possible_names.append(_split_opt(first))
+ opts.append(first)
+ second = second.lstrip()
+ if second:
+ secondary_opts.append(second.lstrip())
+ if first == second:
+ raise ValueError(
+ f"Boolean option {decl!r} cannot use the"
+ " same flag for true/false."
+ )
+ else:
+ possible_names.append(_split_opt(decl))
+ opts.append(decl)
+
+ if name is None and possible_names:
+ possible_names.sort(key=lambda x: -len(x[0])) # group long options first
+ name = possible_names[0][1].replace("-", "_").lower()
+ if not name.isidentifier():
+ name = None
+
+ if name is None:
+ if not expose_value:
+ return None, opts, secondary_opts
+ raise TypeError(
+ f"Could not determine name for option with declarations {decls!r}"
+ )
+
+ if not opts and not secondary_opts:
+ raise TypeError(
+ f"No options defined but a name was passed ({name})."
+ " Did you mean to declare an argument instead? Did"
+ f" you mean to pass '--{name}'?"
+ )
+
+ return name, opts, secondary_opts
+
+ def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None:
+ if self.multiple:
+ action = "append"
+ elif self.count:
+ action = "count"
+ else:
+ action = "store"
+
+ if self.is_flag:
+ action = f"{action}_const"
+
+ if self.is_bool_flag and self.secondary_opts:
+ parser.add_option(
+ obj=self, opts=self.opts, dest=self.name, action=action, const=True
+ )
+ parser.add_option(
+ obj=self,
+ opts=self.secondary_opts,
+ dest=self.name,
+ action=action,
+ const=False,
+ )
+ else:
+ parser.add_option(
+ obj=self,
+ opts=self.opts,
+ dest=self.name,
+ action=action,
+ const=self.flag_value,
+ )
+ else:
+ parser.add_option(
+ obj=self,
+ opts=self.opts,
+ dest=self.name,
+ action=action,
+ nargs=self.nargs,
+ )
+
+ def get_help_record(self, ctx: Context) -> tuple[str, str] | None:
+ if self.hidden:
+ return None
+
+ any_prefix_is_slash = False
+
+ def _write_opts(opts: cabc.Sequence[str]) -> str:
+ nonlocal any_prefix_is_slash
+
+ rv, any_slashes = join_options(opts)
+
+ if any_slashes:
+ any_prefix_is_slash = True
+
+ if not self.is_flag and not self.count:
+ rv += f" {self.make_metavar(ctx=ctx)}"
+
+ return rv
+
+ rv = [_write_opts(self.opts)]
+
+ if self.secondary_opts:
+ rv.append(_write_opts(self.secondary_opts))
+
+ help = self.help or ""
+
+ extra = self.get_help_extra(ctx)
+ extra_items = []
+ if "envvars" in extra:
+ extra_items.append(
+ _("env var: {var}").format(var=", ".join(extra["envvars"]))
+ )
+ if "default" in extra:
+ extra_items.append(_("default: {default}").format(default=extra["default"]))
+ if "range" in extra:
+ extra_items.append(extra["range"])
+ if "required" in extra:
+ extra_items.append(_(extra["required"]))
+
+ if extra_items:
+ extra_str = "; ".join(extra_items)
+ help = f"{help} [{extra_str}]" if help else f"[{extra_str}]"
+
+ return ("; " if any_prefix_is_slash else " / ").join(rv), help
+
+ def get_help_extra(self, ctx: Context) -> types.OptionHelpExtra:
+ extra: types.OptionHelpExtra = {}
+
+ if self.show_envvar:
+ envvar = self.envvar
+
+ if envvar is None:
+ if (
+ self.allow_from_autoenv
+ and ctx.auto_envvar_prefix is not None
+ and self.name is not None
+ ):
+ envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}"
+
+ if envvar is not None:
+ if isinstance(envvar, str):
+ extra["envvars"] = (envvar,)
+ else:
+ extra["envvars"] = tuple(str(d) for d in envvar)
+
+ # Temporarily enable resilient parsing to avoid type casting
+ # failing for the default. Might be possible to extend this to
+ # help formatting in general.
+ resilient = ctx.resilient_parsing
+ ctx.resilient_parsing = True
+
+ try:
+ default_value = self.get_default(ctx, call=False)
+ finally:
+ ctx.resilient_parsing = resilient
+
+ show_default = False
+ show_default_is_str = False
+
+ if self.show_default is not None:
+ if isinstance(self.show_default, str):
+ show_default_is_str = show_default = True
+ else:
+ show_default = self.show_default
+ elif ctx.show_default is not None:
+ show_default = ctx.show_default
+
+ if show_default_is_str or (
+ show_default and (default_value not in (None, UNSET))
+ ):
+ if show_default_is_str:
+ default_string = f"({self.show_default})"
+ elif isinstance(default_value, (list, tuple)):
+ default_string = ", ".join(str(d) for d in default_value)
+ elif isinstance(default_value, enum.Enum):
+ default_string = default_value.name
+ elif inspect.isfunction(default_value):
+ default_string = _("(dynamic)")
+ elif self.is_bool_flag and self.secondary_opts:
+ # For boolean flags that have distinct True/False opts,
+ # use the opt without prefix instead of the value.
+ default_string = _split_opt(
+ (self.opts if default_value else self.secondary_opts)[0]
+ )[1]
+ elif self.is_bool_flag and not self.secondary_opts and not default_value:
+ default_string = ""
+ elif isinstance(default_value, str) and default_value == "":
+ default_string = '""'
+ else:
+ default_string = str(default_value)
+
+ if default_string:
+ extra["default"] = default_string
+
+ if (
+ isinstance(self.type, types._NumberRangeBase)
+ # skip count with default range type
+ and not (self.count and self.type.min == 0 and self.type.max is None)
+ ):
+ range_str = self.type._describe_range()
+
+ if range_str:
+ extra["range"] = range_str
+
+ if self.required:
+ extra["required"] = "required"
+
+ return extra
+
+ def prompt_for_value(self, ctx: Context) -> t.Any:
+ """This is an alternative flow that can be activated in the full
+ value processing if a value does not exist. It will prompt the
+ user until a valid value exists and then returns the processed
+ value as result.
+ """
+ assert self.prompt is not None
+
+ # Calculate the default before prompting anything to lock in the value before
+ # attempting any user interaction.
+ default = self.get_default(ctx)
+
+ # A boolean flag can use a simplified [y/n] confirmation prompt.
+ if self.is_bool_flag:
+ # If we have no boolean default, we force the user to explicitly provide
+ # one.
+ if default in (UNSET, None):
+ default = None
+ # Nothing prevent you to declare an option that is simultaneously:
+ # 1) auto-detected as a boolean flag,
+ # 2) allowed to prompt, and
+ # 3) still declare a non-boolean default.
+ # This forced casting into a boolean is necessary to align any non-boolean
+ # default to the prompt, which is going to be a [y/n]-style confirmation
+ # because the option is still a boolean flag. That way, instead of [y/n],
+ # we get [Y/n] or [y/N] depending on the truthy value of the default.
+ # Refs: https://github.com/pallets/click/pull/3030#discussion_r2289180249
+ else:
+ default = bool(default)
+ return confirm(self.prompt, default)
+
+ # If show_default is given, provide this to `prompt` as well,
+ # otherwise we use `prompt`'s default behavior
+ prompt_kwargs: t.Any = {}
+ if self.show_default is not None:
+ prompt_kwargs["show_default"] = self.show_default
+
+ return prompt(
+ self.prompt,
+ # Use ``None`` to inform the prompt() function to reiterate until a valid
+ # value is provided by the user if we have no default.
+ default=None if default is UNSET else default,
+ type=self.type,
+ hide_input=self.hide_input,
+ show_choices=self.show_choices,
+ confirmation_prompt=self.confirmation_prompt,
+ value_proc=lambda x: self.process_value(ctx, x),
+ **prompt_kwargs,
+ )
+
+ def resolve_envvar_value(self, ctx: Context) -> str | None:
+ """:class:`Option` resolves its environment variable the same way as
+ :func:`Parameter.resolve_envvar_value`, but it also supports
+ :attr:`Context.auto_envvar_prefix`. If we could not find an environment from
+ the :attr:`envvar` property, we fallback on :attr:`Context.auto_envvar_prefix`
+ to build dynamiccaly the environment variable name using the
+ :python:`{ctx.auto_envvar_prefix}_{self.name.upper()}` template.
+
+ :meta private:
+ """
+ rv = super().resolve_envvar_value(ctx)
+
+ if rv is not None:
+ return rv
+
+ if (
+ self.allow_from_autoenv
+ and ctx.auto_envvar_prefix is not None
+ and self.name is not None
+ ):
+ envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}"
+ rv = os.environ.get(envvar)
+
+ if rv:
+ return rv
+
+ return None
+
+ def value_from_envvar(self, ctx: Context) -> t.Any:
+ """For :class:`Option`, this method processes the raw environment variable
+ string the same way as :func:`Parameter.value_from_envvar` does.
+
+ But in the case of non-boolean flags, the value is analyzed to determine if the
+ flag is activated or not, and returns a boolean of its activation, or the
+ :attr:`flag_value` if the latter is set.
+
+ This method also takes care of repeated options (i.e. options with
+ :attr:`multiple` set to ``True``).
+
+ :meta private:
+ """
+ rv = self.resolve_envvar_value(ctx)
+
+ # Absent environment variable or an empty string is interpreted as unset.
+ if rv is None:
+ return None
+
+ # Non-boolean flags are more liberal in what they accept. But a flag being a
+ # flag, its envvar value still needs to be analyzed to determine if the flag is
+ # activated or not.
+ if self.is_flag and not self.is_bool_flag:
+ # If the flag_value is set and match the envvar value, return it
+ # directly.
+ if self.flag_value is not UNSET and rv == self.flag_value:
+ return self.flag_value
+ # Analyze the envvar value as a boolean to know if the flag is
+ # activated or not.
+ return types.BoolParamType.str_to_bool(rv)
+
+ # Split the envvar value if it is allowed to be repeated.
+ value_depth = (self.nargs != 1) + bool(self.multiple)
+ if value_depth > 0:
+ multi_rv = self.type.split_envvar_value(rv)
+ if self.multiple and self.nargs != 1:
+ multi_rv = batch(multi_rv, self.nargs) # type: ignore[assignment]
+
+ return multi_rv
+
+ return rv
+
+ def consume_value(
+ self, ctx: Context, opts: cabc.Mapping[str, Parameter]
+ ) -> tuple[t.Any, ParameterSource]:
+ """For :class:`Option`, the value can be collected from an interactive prompt
+ if the option is a flag that needs a value (and the :attr:`prompt` property is
+ set).
+
+ Additionally, this method handles flag option that are activated without a
+ value, in which case the :attr:`flag_value` is returned.
+
+ :meta private:
+ """
+ value, source = super().consume_value(ctx, opts)
+
+ # The parser will emit a sentinel value if the option is allowed to as a flag
+ # without a value.
+ if value is FLAG_NEEDS_VALUE:
+ # If the option allows for a prompt, we start an interaction with the user.
+ if self.prompt is not None and not ctx.resilient_parsing:
+ value = self.prompt_for_value(ctx)
+ source = ParameterSource.PROMPT
+ # Else the flag takes its flag_value as value.
+ else:
+ value = self.flag_value
+ source = ParameterSource.COMMANDLINE
+
+ # A flag which is activated always returns the flag value, unless the value
+ # comes from the explicitly sets default.
+ elif (
+ self.is_flag
+ and value is True
+ and not self.is_bool_flag
+ and source < ParameterSource.DEFAULT_MAP
+ ):
+ value = self.flag_value
+
+ # Re-interpret a multiple option which has been sent as-is by the parser.
+ # Here we replace each occurrence of value-less flags (marked by the
+ # FLAG_NEEDS_VALUE sentinel) with the flag_value.
+ elif (
+ self.multiple
+ and value is not UNSET
+ and source < ParameterSource.DEFAULT_MAP
+ and any(v is FLAG_NEEDS_VALUE for v in value)
+ ):
+ value = [self.flag_value if v is FLAG_NEEDS_VALUE else v for v in value]
+ source = ParameterSource.COMMANDLINE
+
+ # The value wasn't set, or used the param's default, prompt for one to the user
+ # if prompting is enabled.
+ elif (
+ (value is UNSET or source >= ParameterSource.DEFAULT_MAP)
+ and self.prompt is not None
+ and (self.required or self.prompt_required)
+ and not ctx.resilient_parsing
+ ):
+ value = self.prompt_for_value(ctx)
+ source = ParameterSource.PROMPT
+
+ return value, source
+
+ def process_value(self, ctx: Context, value: t.Any) -> t.Any:
+ # process_value has to be overridden on Options in order to capture
+ # `value == UNSET` cases before `type_cast_value()` gets called.
+ #
+ # Refs:
+ # https://github.com/pallets/click/issues/3069
+ if self.is_flag and not self.required and self.is_bool_flag and value is UNSET:
+ value = False
+
+ if self.callback is not None:
+ value = self.callback(ctx, self, value)
+
+ return value
+
+ # in the normal case, rely on Parameter.process_value
+ return super().process_value(ctx, value)
+
+
+class Argument(Parameter):
+ """Arguments are positional parameters to a command. They generally
+ provide fewer features than options but can have infinite ``nargs``
+ and are required by default.
+
+ All parameters are passed onwards to the constructor of :class:`Parameter`.
+ """
+
+ param_type_name = "argument"
+
+ def __init__(
+ self,
+ param_decls: cabc.Sequence[str],
+ required: bool | None = None,
+ **attrs: t.Any,
+ ) -> None:
+ # Auto-detect the requirement status of the argument if not explicitly set.
+ if required is None:
+ # The argument gets automatically required if it has no explicit default
+ # value set and is setup to match at least one value.
+ if attrs.get("default", UNSET) is UNSET:
+ required = attrs.get("nargs", 1) > 0
+ # If the argument has a default value, it is not required.
+ else:
+ required = False
+
+ if "multiple" in attrs:
+ raise TypeError("__init__() got an unexpected keyword argument 'multiple'.")
+
+ super().__init__(param_decls, required=required, **attrs)
+
+ @property
+ def human_readable_name(self) -> str:
+ if self.metavar is not None:
+ return self.metavar
+ return self.name.upper() # type: ignore
+
+ def make_metavar(self, ctx: Context) -> str:
+ if self.metavar is not None:
+ return self.metavar
+ var = self.type.get_metavar(param=self, ctx=ctx)
+ if not var:
+ var = self.name.upper() # type: ignore
+ if self.deprecated:
+ var += "!"
+ if not self.required:
+ var = f"[{var}]"
+ if self.nargs != 1:
+ var += "..."
+ return var
+
+ def _parse_decls(
+ self, decls: cabc.Sequence[str], expose_value: bool
+ ) -> tuple[str | None, list[str], list[str]]:
+ if not decls:
+ if not expose_value:
+ return None, [], []
+ raise TypeError("Argument is marked as exposed, but does not have a name.")
+ if len(decls) == 1:
+ name = arg = decls[0]
+ name = name.replace("-", "_").lower()
+ else:
+ raise TypeError(
+ "Arguments take exactly one parameter declaration, got"
+ f" {len(decls)}: {decls}."
+ )
+ return name, [arg], []
+
+ def get_usage_pieces(self, ctx: Context) -> list[str]:
+ return [self.make_metavar(ctx)]
+
+ def get_error_hint(self, ctx: Context) -> str:
+ return f"'{self.make_metavar(ctx)}'"
+
+ def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None:
+ parser.add_argument(dest=self.name, nargs=self.nargs, obj=self)
+
+
+def __getattr__(name: str) -> object:
+ import warnings
+
+ if name == "BaseCommand":
+ warnings.warn(
+ "'BaseCommand' is deprecated and will be removed in Click 9.0. Use"
+ " 'Command' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return _BaseCommand
+
+ if name == "MultiCommand":
+ warnings.warn(
+ "'MultiCommand' is deprecated and will be removed in Click 9.0. Use"
+ " 'Group' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return _MultiCommand
+
+ raise AttributeError(name)
diff --git a/venv/Lib/site-packages/click/decorators.py b/venv/Lib/site-packages/click/decorators.py
new file mode 100644
index 0000000..21f4c34
--- /dev/null
+++ b/venv/Lib/site-packages/click/decorators.py
@@ -0,0 +1,551 @@
+from __future__ import annotations
+
+import inspect
+import typing as t
+from functools import update_wrapper
+from gettext import gettext as _
+
+from .core import Argument
+from .core import Command
+from .core import Context
+from .core import Group
+from .core import Option
+from .core import Parameter
+from .globals import get_current_context
+from .utils import echo
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+ P = te.ParamSpec("P")
+
+R = t.TypeVar("R")
+T = t.TypeVar("T")
+_AnyCallable = t.Callable[..., t.Any]
+FC = t.TypeVar("FC", bound="_AnyCallable | Command")
+
+
+def pass_context(f: t.Callable[te.Concatenate[Context, P], R]) -> t.Callable[P, R]:
+ """Marks a callback as wanting to receive the current context
+ object as first argument.
+ """
+
+ def new_func(*args: P.args, **kwargs: P.kwargs) -> R:
+ return f(get_current_context(), *args, **kwargs)
+
+ return update_wrapper(new_func, f)
+
+
+def pass_obj(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]:
+ """Similar to :func:`pass_context`, but only pass the object on the
+ context onwards (:attr:`Context.obj`). This is useful if that object
+ represents the state of a nested system.
+ """
+
+ def new_func(*args: P.args, **kwargs: P.kwargs) -> R:
+ return f(get_current_context().obj, *args, **kwargs)
+
+ return update_wrapper(new_func, f)
+
+
+def make_pass_decorator(
+ object_type: type[T], ensure: bool = False
+) -> t.Callable[[t.Callable[te.Concatenate[T, P], R]], t.Callable[P, R]]:
+ """Given an object type this creates a decorator that will work
+ similar to :func:`pass_obj` but instead of passing the object of the
+ current context, it will find the innermost context of type
+ :func:`object_type`.
+
+ This generates a decorator that works roughly like this::
+
+ from functools import update_wrapper
+
+ def decorator(f):
+ @pass_context
+ def new_func(ctx, *args, **kwargs):
+ obj = ctx.find_object(object_type)
+ return ctx.invoke(f, obj, *args, **kwargs)
+ return update_wrapper(new_func, f)
+ return decorator
+
+ :param object_type: the type of the object to pass.
+ :param ensure: if set to `True`, a new object will be created and
+ remembered on the context if it's not there yet.
+ """
+
+ def decorator(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]:
+ def new_func(*args: P.args, **kwargs: P.kwargs) -> R:
+ ctx = get_current_context()
+
+ obj: T | None
+ if ensure:
+ obj = ctx.ensure_object(object_type)
+ else:
+ obj = ctx.find_object(object_type)
+
+ if obj is None:
+ raise RuntimeError(
+ "Managed to invoke callback without a context"
+ f" object of type {object_type.__name__!r}"
+ " existing."
+ )
+
+ return ctx.invoke(f, obj, *args, **kwargs)
+
+ return update_wrapper(new_func, f)
+
+ return decorator
+
+
+def pass_meta_key(
+ key: str, *, doc_description: str | None = None
+) -> t.Callable[[t.Callable[te.Concatenate[T, P], R]], t.Callable[P, R]]:
+ """Create a decorator that passes a key from
+ :attr:`click.Context.meta` as the first argument to the decorated
+ function.
+
+ :param key: Key in ``Context.meta`` to pass.
+ :param doc_description: Description of the object being passed,
+ inserted into the decorator's docstring. Defaults to "the 'key'
+ key from Context.meta".
+
+ .. versionadded:: 8.0
+ """
+
+ def decorator(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]:
+ def new_func(*args: P.args, **kwargs: P.kwargs) -> R:
+ ctx = get_current_context()
+ obj = ctx.meta[key]
+ return ctx.invoke(f, obj, *args, **kwargs)
+
+ return update_wrapper(new_func, f)
+
+ if doc_description is None:
+ doc_description = f"the {key!r} key from :attr:`click.Context.meta`"
+
+ decorator.__doc__ = (
+ f"Decorator that passes {doc_description} as the first argument"
+ " to the decorated function."
+ )
+ return decorator
+
+
+CmdType = t.TypeVar("CmdType", bound=Command)
+
+
+# variant: no call, directly as decorator for a function.
+@t.overload
+def command(name: _AnyCallable) -> Command: ...
+
+
+# variant: with positional name and with positional or keyword cls argument:
+# @command(namearg, CommandCls, ...) or @command(namearg, cls=CommandCls, ...)
+@t.overload
+def command(
+ name: str | None,
+ cls: type[CmdType],
+ **attrs: t.Any,
+) -> t.Callable[[_AnyCallable], CmdType]: ...
+
+
+# variant: name omitted, cls _must_ be a keyword argument, @command(cls=CommandCls, ...)
+@t.overload
+def command(
+ name: None = None,
+ *,
+ cls: type[CmdType],
+ **attrs: t.Any,
+) -> t.Callable[[_AnyCallable], CmdType]: ...
+
+
+# variant: with optional string name, no cls argument provided.
+@t.overload
+def command(
+ name: str | None = ..., cls: None = None, **attrs: t.Any
+) -> t.Callable[[_AnyCallable], Command]: ...
+
+
+def command(
+ name: str | _AnyCallable | None = None,
+ cls: type[CmdType] | None = None,
+ **attrs: t.Any,
+) -> Command | t.Callable[[_AnyCallable], Command | CmdType]:
+ r"""Creates a new :class:`Command` and uses the decorated function as
+ callback. This will also automatically attach all decorated
+ :func:`option`\s and :func:`argument`\s as parameters to the command.
+
+ The name of the command defaults to the name of the function, converted to
+ lowercase, with underscores ``_`` replaced by dashes ``-``, and the suffixes
+ ``_command``, ``_cmd``, ``_group``, and ``_grp`` are removed. For example,
+ ``init_data_command`` becomes ``init-data``.
+
+ All keyword arguments are forwarded to the underlying command class.
+ For the ``params`` argument, any decorated params are appended to
+ the end of the list.
+
+ Once decorated the function turns into a :class:`Command` instance
+ that can be invoked as a command line utility or be attached to a
+ command :class:`Group`.
+
+ :param name: The name of the command. Defaults to modifying the function's
+ name as described above.
+ :param cls: The command class to create. Defaults to :class:`Command`.
+
+ .. versionchanged:: 8.2
+ The suffixes ``_command``, ``_cmd``, ``_group``, and ``_grp`` are
+ removed when generating the name.
+
+ .. versionchanged:: 8.1
+ This decorator can be applied without parentheses.
+
+ .. versionchanged:: 8.1
+ The ``params`` argument can be used. Decorated params are
+ appended to the end of the list.
+ """
+
+ func: t.Callable[[_AnyCallable], t.Any] | None = None
+
+ if callable(name):
+ func = name
+ name = None
+ assert cls is None, "Use 'command(cls=cls)(callable)' to specify a class."
+ assert not attrs, "Use 'command(**kwargs)(callable)' to provide arguments."
+
+ if cls is None:
+ cls = t.cast("type[CmdType]", Command)
+
+ def decorator(f: _AnyCallable) -> CmdType:
+ if isinstance(f, Command):
+ raise TypeError("Attempted to convert a callback into a command twice.")
+
+ attr_params = attrs.pop("params", None)
+ params = attr_params if attr_params is not None else []
+
+ try:
+ decorator_params = f.__click_params__ # type: ignore
+ except AttributeError:
+ pass
+ else:
+ del f.__click_params__ # type: ignore
+ params.extend(reversed(decorator_params))
+
+ if attrs.get("help") is None:
+ attrs["help"] = f.__doc__
+
+ if t.TYPE_CHECKING:
+ assert cls is not None
+ assert not callable(name)
+
+ if name is not None:
+ cmd_name = name
+ else:
+ cmd_name = f.__name__.lower().replace("_", "-")
+ cmd_left, sep, suffix = cmd_name.rpartition("-")
+
+ if sep and suffix in {"command", "cmd", "group", "grp"}:
+ cmd_name = cmd_left
+
+ cmd = cls(name=cmd_name, callback=f, params=params, **attrs)
+ cmd.__doc__ = f.__doc__
+ return cmd
+
+ if func is not None:
+ return decorator(func)
+
+ return decorator
+
+
+GrpType = t.TypeVar("GrpType", bound=Group)
+
+
+# variant: no call, directly as decorator for a function.
+@t.overload
+def group(name: _AnyCallable) -> Group: ...
+
+
+# variant: with positional name and with positional or keyword cls argument:
+# @group(namearg, GroupCls, ...) or @group(namearg, cls=GroupCls, ...)
+@t.overload
+def group(
+ name: str | None,
+ cls: type[GrpType],
+ **attrs: t.Any,
+) -> t.Callable[[_AnyCallable], GrpType]: ...
+
+
+# variant: name omitted, cls _must_ be a keyword argument, @group(cmd=GroupCls, ...)
+@t.overload
+def group(
+ name: None = None,
+ *,
+ cls: type[GrpType],
+ **attrs: t.Any,
+) -> t.Callable[[_AnyCallable], GrpType]: ...
+
+
+# variant: with optional string name, no cls argument provided.
+@t.overload
+def group(
+ name: str | None = ..., cls: None = None, **attrs: t.Any
+) -> t.Callable[[_AnyCallable], Group]: ...
+
+
+def group(
+ name: str | _AnyCallable | None = None,
+ cls: type[GrpType] | None = None,
+ **attrs: t.Any,
+) -> Group | t.Callable[[_AnyCallable], Group | GrpType]:
+ """Creates a new :class:`Group` with a function as callback. This
+ works otherwise the same as :func:`command` just that the `cls`
+ parameter is set to :class:`Group`.
+
+ .. versionchanged:: 8.1
+ This decorator can be applied without parentheses.
+ """
+ if cls is None:
+ cls = t.cast("type[GrpType]", Group)
+
+ if callable(name):
+ return command(cls=cls, **attrs)(name)
+
+ return command(name, cls, **attrs)
+
+
+def _param_memo(f: t.Callable[..., t.Any], param: Parameter) -> None:
+ if isinstance(f, Command):
+ f.params.append(param)
+ else:
+ if not hasattr(f, "__click_params__"):
+ f.__click_params__ = [] # type: ignore
+
+ f.__click_params__.append(param) # type: ignore
+
+
+def argument(
+ *param_decls: str, cls: type[Argument] | None = None, **attrs: t.Any
+) -> t.Callable[[FC], FC]:
+ """Attaches an argument to the command. All positional arguments are
+ passed as parameter declarations to :class:`Argument`; all keyword
+ arguments are forwarded unchanged (except ``cls``).
+ This is equivalent to creating an :class:`Argument` instance manually
+ and attaching it to the :attr:`Command.params` list.
+
+ For the default argument class, refer to :class:`Argument` and
+ :class:`Parameter` for descriptions of parameters.
+
+ :param cls: the argument class to instantiate. This defaults to
+ :class:`Argument`.
+ :param param_decls: Passed as positional arguments to the constructor of
+ ``cls``.
+ :param attrs: Passed as keyword arguments to the constructor of ``cls``.
+ """
+ if cls is None:
+ cls = Argument
+
+ def decorator(f: FC) -> FC:
+ _param_memo(f, cls(param_decls, **attrs))
+ return f
+
+ return decorator
+
+
+def option(
+ *param_decls: str, cls: type[Option] | None = None, **attrs: t.Any
+) -> t.Callable[[FC], FC]:
+ """Attaches an option to the command. All positional arguments are
+ passed as parameter declarations to :class:`Option`; all keyword
+ arguments are forwarded unchanged (except ``cls``).
+ This is equivalent to creating an :class:`Option` instance manually
+ and attaching it to the :attr:`Command.params` list.
+
+ For the default option class, refer to :class:`Option` and
+ :class:`Parameter` for descriptions of parameters.
+
+ :param cls: the option class to instantiate. This defaults to
+ :class:`Option`.
+ :param param_decls: Passed as positional arguments to the constructor of
+ ``cls``.
+ :param attrs: Passed as keyword arguments to the constructor of ``cls``.
+ """
+ if cls is None:
+ cls = Option
+
+ def decorator(f: FC) -> FC:
+ _param_memo(f, cls(param_decls, **attrs))
+ return f
+
+ return decorator
+
+
+def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
+ """Add a ``--yes`` option which shows a prompt before continuing if
+ not passed. If the prompt is declined, the program will exit.
+
+ :param param_decls: One or more option names. Defaults to the single
+ value ``"--yes"``.
+ :param kwargs: Extra arguments are passed to :func:`option`.
+ """
+
+ def callback(ctx: Context, param: Parameter, value: bool) -> None:
+ if not value:
+ ctx.abort()
+
+ if not param_decls:
+ param_decls = ("--yes",)
+
+ kwargs.setdefault("is_flag", True)
+ kwargs.setdefault("callback", callback)
+ kwargs.setdefault("expose_value", False)
+ kwargs.setdefault("prompt", "Do you want to continue?")
+ kwargs.setdefault("help", "Confirm the action without prompting.")
+ return option(*param_decls, **kwargs)
+
+
+def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
+ """Add a ``--password`` option which prompts for a password, hiding
+ input and asking to enter the value again for confirmation.
+
+ :param param_decls: One or more option names. Defaults to the single
+ value ``"--password"``.
+ :param kwargs: Extra arguments are passed to :func:`option`.
+ """
+ if not param_decls:
+ param_decls = ("--password",)
+
+ kwargs.setdefault("prompt", True)
+ kwargs.setdefault("confirmation_prompt", True)
+ kwargs.setdefault("hide_input", True)
+ return option(*param_decls, **kwargs)
+
+
+def version_option(
+ version: str | None = None,
+ *param_decls: str,
+ package_name: str | None = None,
+ prog_name: str | None = None,
+ message: str | None = None,
+ **kwargs: t.Any,
+) -> t.Callable[[FC], FC]:
+ """Add a ``--version`` option which immediately prints the version
+ number and exits the program.
+
+ If ``version`` is not provided, Click will try to detect it using
+ :func:`importlib.metadata.version` to get the version for the
+ ``package_name``.
+
+ If ``package_name`` is not provided, Click will try to detect it by
+ inspecting the stack frames. This will be used to detect the
+ version, so it must match the name of the installed package.
+
+ :param version: The version number to show. If not provided, Click
+ will try to detect it.
+ :param param_decls: One or more option names. Defaults to the single
+ value ``"--version"``.
+ :param package_name: The package name to detect the version from. If
+ not provided, Click will try to detect it.
+ :param prog_name: The name of the CLI to show in the message. If not
+ provided, it will be detected from the command.
+ :param message: The message to show. The values ``%(prog)s``,
+ ``%(package)s``, and ``%(version)s`` are available. Defaults to
+ ``"%(prog)s, version %(version)s"``.
+ :param kwargs: Extra arguments are passed to :func:`option`.
+ :raise RuntimeError: ``version`` could not be detected.
+
+ .. versionchanged:: 8.0
+ Add the ``package_name`` parameter, and the ``%(package)s``
+ value for messages.
+
+ .. versionchanged:: 8.0
+ Use :mod:`importlib.metadata` instead of ``pkg_resources``. The
+ version is detected based on the package name, not the entry
+ point name. The Python package name must match the installed
+ package name, or be passed with ``package_name=``.
+ """
+ if message is None:
+ message = _("%(prog)s, version %(version)s")
+
+ if version is None and package_name is None:
+ frame = inspect.currentframe()
+ f_back = frame.f_back if frame is not None else None
+ f_globals = f_back.f_globals if f_back is not None else None
+ # break reference cycle
+ # https://docs.python.org/3/library/inspect.html#the-interpreter-stack
+ del frame
+
+ if f_globals is not None:
+ package_name = f_globals.get("__name__")
+
+ if package_name == "__main__":
+ package_name = f_globals.get("__package__")
+
+ if package_name:
+ package_name = package_name.partition(".")[0]
+
+ def callback(ctx: Context, param: Parameter, value: bool) -> None:
+ if not value or ctx.resilient_parsing:
+ return
+
+ nonlocal prog_name
+ nonlocal version
+
+ if prog_name is None:
+ prog_name = ctx.find_root().info_name
+
+ if version is None and package_name is not None:
+ import importlib.metadata
+
+ try:
+ version = importlib.metadata.version(package_name)
+ except importlib.metadata.PackageNotFoundError:
+ raise RuntimeError(
+ f"{package_name!r} is not installed. Try passing"
+ " 'package_name' instead."
+ ) from None
+
+ if version is None:
+ raise RuntimeError(
+ f"Could not determine the version for {package_name!r} automatically."
+ )
+
+ echo(
+ message % {"prog": prog_name, "package": package_name, "version": version},
+ color=ctx.color,
+ )
+ ctx.exit()
+
+ if not param_decls:
+ param_decls = ("--version",)
+
+ kwargs.setdefault("is_flag", True)
+ kwargs.setdefault("expose_value", False)
+ kwargs.setdefault("is_eager", True)
+ kwargs.setdefault("help", _("Show the version and exit."))
+ kwargs["callback"] = callback
+ return option(*param_decls, **kwargs)
+
+
+def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
+ """Pre-configured ``--help`` option which immediately prints the help page
+ and exits the program.
+
+ :param param_decls: One or more option names. Defaults to the single
+ value ``"--help"``.
+ :param kwargs: Extra arguments are passed to :func:`option`.
+ """
+
+ def show_help(ctx: Context, param: Parameter, value: bool) -> None:
+ """Callback that print the help page on ```` and exits."""
+ if value and not ctx.resilient_parsing:
+ echo(ctx.get_help(), color=ctx.color)
+ ctx.exit()
+
+ if not param_decls:
+ param_decls = ("--help",)
+
+ kwargs.setdefault("is_flag", True)
+ kwargs.setdefault("expose_value", False)
+ kwargs.setdefault("is_eager", True)
+ kwargs.setdefault("help", _("Show this message and exit."))
+ kwargs.setdefault("callback", show_help)
+
+ return option(*param_decls, **kwargs)
diff --git a/venv/Lib/site-packages/click/exceptions.py b/venv/Lib/site-packages/click/exceptions.py
new file mode 100644
index 0000000..4d782ee
--- /dev/null
+++ b/venv/Lib/site-packages/click/exceptions.py
@@ -0,0 +1,308 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import typing as t
+from gettext import gettext as _
+from gettext import ngettext
+
+from ._compat import get_text_stderr
+from .globals import resolve_color_default
+from .utils import echo
+from .utils import format_filename
+
+if t.TYPE_CHECKING:
+ from .core import Command
+ from .core import Context
+ from .core import Parameter
+
+
+def _join_param_hints(param_hint: cabc.Sequence[str] | str | None) -> str | None:
+ if param_hint is not None and not isinstance(param_hint, str):
+ return " / ".join(repr(x) for x in param_hint)
+
+ return param_hint
+
+
+class ClickException(Exception):
+ """An exception that Click can handle and show to the user."""
+
+ #: The exit code for this exception.
+ exit_code = 1
+
+ def __init__(self, message: str) -> None:
+ super().__init__(message)
+ # The context will be removed by the time we print the message, so cache
+ # the color settings here to be used later on (in `show`)
+ self.show_color: bool | None = resolve_color_default()
+ self.message = message
+
+ def format_message(self) -> str:
+ return self.message
+
+ def __str__(self) -> str:
+ return self.message
+
+ def show(self, file: t.IO[t.Any] | None = None) -> None:
+ if file is None:
+ file = get_text_stderr()
+
+ echo(
+ _("Error: {message}").format(message=self.format_message()),
+ file=file,
+ color=self.show_color,
+ )
+
+
+class UsageError(ClickException):
+ """An internal exception that signals a usage error. This typically
+ aborts any further handling.
+
+ :param message: the error message to display.
+ :param ctx: optionally the context that caused this error. Click will
+ fill in the context automatically in some situations.
+ """
+
+ exit_code = 2
+
+ def __init__(self, message: str, ctx: Context | None = None) -> None:
+ super().__init__(message)
+ self.ctx = ctx
+ self.cmd: Command | None = self.ctx.command if self.ctx else None
+
+ def show(self, file: t.IO[t.Any] | None = None) -> None:
+ if file is None:
+ file = get_text_stderr()
+ color = None
+ hint = ""
+ if (
+ self.ctx is not None
+ and self.ctx.command.get_help_option(self.ctx) is not None
+ ):
+ hint = _("Try '{command} {option}' for help.").format(
+ command=self.ctx.command_path, option=self.ctx.help_option_names[0]
+ )
+ hint = f"{hint}\n"
+ if self.ctx is not None:
+ color = self.ctx.color
+ echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color)
+ echo(
+ _("Error: {message}").format(message=self.format_message()),
+ file=file,
+ color=color,
+ )
+
+
+class BadParameter(UsageError):
+ """An exception that formats out a standardized error message for a
+ bad parameter. This is useful when thrown from a callback or type as
+ Click will attach contextual information to it (for instance, which
+ parameter it is).
+
+ .. versionadded:: 2.0
+
+ :param param: the parameter object that caused this error. This can
+ be left out, and Click will attach this info itself
+ if possible.
+ :param param_hint: a string that shows up as parameter name. This
+ can be used as alternative to `param` in cases
+ where custom validation should happen. If it is
+ a string it's used as such, if it's a list then
+ each item is quoted and separated.
+ """
+
+ def __init__(
+ self,
+ message: str,
+ ctx: Context | None = None,
+ param: Parameter | None = None,
+ param_hint: cabc.Sequence[str] | str | None = None,
+ ) -> None:
+ super().__init__(message, ctx)
+ self.param = param
+ self.param_hint = param_hint
+
+ def format_message(self) -> str:
+ if self.param_hint is not None:
+ param_hint = self.param_hint
+ elif self.param is not None:
+ param_hint = self.param.get_error_hint(self.ctx) # type: ignore
+ else:
+ return _("Invalid value: {message}").format(message=self.message)
+
+ return _("Invalid value for {param_hint}: {message}").format(
+ param_hint=_join_param_hints(param_hint), message=self.message
+ )
+
+
+class MissingParameter(BadParameter):
+ """Raised if click required an option or argument but it was not
+ provided when invoking the script.
+
+ .. versionadded:: 4.0
+
+ :param param_type: a string that indicates the type of the parameter.
+ The default is to inherit the parameter type from
+ the given `param`. Valid values are ``'parameter'``,
+ ``'option'`` or ``'argument'``.
+ """
+
+ def __init__(
+ self,
+ message: str | None = None,
+ ctx: Context | None = None,
+ param: Parameter | None = None,
+ param_hint: cabc.Sequence[str] | str | None = None,
+ param_type: str | None = None,
+ ) -> None:
+ super().__init__(message or "", ctx, param, param_hint)
+ self.param_type = param_type
+
+ def format_message(self) -> str:
+ if self.param_hint is not None:
+ param_hint: cabc.Sequence[str] | str | None = self.param_hint
+ elif self.param is not None:
+ param_hint = self.param.get_error_hint(self.ctx) # type: ignore
+ else:
+ param_hint = None
+
+ param_hint = _join_param_hints(param_hint)
+ param_hint = f" {param_hint}" if param_hint else ""
+
+ param_type = self.param_type
+ if param_type is None and self.param is not None:
+ param_type = self.param.param_type_name
+
+ msg = self.message
+ if self.param is not None:
+ msg_extra = self.param.type.get_missing_message(
+ param=self.param, ctx=self.ctx
+ )
+ if msg_extra:
+ if msg:
+ msg += f". {msg_extra}"
+ else:
+ msg = msg_extra
+
+ msg = f" {msg}" if msg else ""
+
+ # Translate param_type for known types.
+ if param_type == "argument":
+ missing = _("Missing argument")
+ elif param_type == "option":
+ missing = _("Missing option")
+ elif param_type == "parameter":
+ missing = _("Missing parameter")
+ else:
+ missing = _("Missing {param_type}").format(param_type=param_type)
+
+ return f"{missing}{param_hint}.{msg}"
+
+ def __str__(self) -> str:
+ if not self.message:
+ param_name = self.param.name if self.param else None
+ return _("Missing parameter: {param_name}").format(param_name=param_name)
+ else:
+ return self.message
+
+
+class NoSuchOption(UsageError):
+ """Raised if click attempted to handle an option that does not
+ exist.
+
+ .. versionadded:: 4.0
+ """
+
+ def __init__(
+ self,
+ option_name: str,
+ message: str | None = None,
+ possibilities: cabc.Sequence[str] | None = None,
+ ctx: Context | None = None,
+ ) -> None:
+ if message is None:
+ message = _("No such option: {name}").format(name=option_name)
+
+ super().__init__(message, ctx)
+ self.option_name = option_name
+ self.possibilities = possibilities
+
+ def format_message(self) -> str:
+ if not self.possibilities:
+ return self.message
+
+ possibility_str = ", ".join(sorted(self.possibilities))
+ suggest = ngettext(
+ "Did you mean {possibility}?",
+ "(Possible options: {possibilities})",
+ len(self.possibilities),
+ ).format(possibility=possibility_str, possibilities=possibility_str)
+ return f"{self.message} {suggest}"
+
+
+class BadOptionUsage(UsageError):
+ """Raised if an option is generally supplied but the use of the option
+ was incorrect. This is for instance raised if the number of arguments
+ for an option is not correct.
+
+ .. versionadded:: 4.0
+
+ :param option_name: the name of the option being used incorrectly.
+ """
+
+ def __init__(
+ self, option_name: str, message: str, ctx: Context | None = None
+ ) -> None:
+ super().__init__(message, ctx)
+ self.option_name = option_name
+
+
+class BadArgumentUsage(UsageError):
+ """Raised if an argument is generally supplied but the use of the argument
+ was incorrect. This is for instance raised if the number of values
+ for an argument is not correct.
+
+ .. versionadded:: 6.0
+ """
+
+
+class NoArgsIsHelpError(UsageError):
+ def __init__(self, ctx: Context) -> None:
+ self.ctx: Context
+ super().__init__(ctx.get_help(), ctx=ctx)
+
+ def show(self, file: t.IO[t.Any] | None = None) -> None:
+ echo(self.format_message(), file=file, err=True, color=self.ctx.color)
+
+
+class FileError(ClickException):
+ """Raised if a file cannot be opened."""
+
+ def __init__(self, filename: str, hint: str | None = None) -> None:
+ if hint is None:
+ hint = _("unknown error")
+
+ super().__init__(hint)
+ self.ui_filename: str = format_filename(filename)
+ self.filename = filename
+
+ def format_message(self) -> str:
+ return _("Could not open file {filename!r}: {message}").format(
+ filename=self.ui_filename, message=self.message
+ )
+
+
+class Abort(RuntimeError):
+ """An internal signalling exception that signals Click to abort."""
+
+
+class Exit(RuntimeError):
+ """An exception that indicates that the application should exit with some
+ status code.
+
+ :param code: the status code to exit with.
+ """
+
+ __slots__ = ("exit_code",)
+
+ def __init__(self, code: int = 0) -> None:
+ self.exit_code: int = code
diff --git a/venv/Lib/site-packages/click/formatting.py b/venv/Lib/site-packages/click/formatting.py
new file mode 100644
index 0000000..0b64f83
--- /dev/null
+++ b/venv/Lib/site-packages/click/formatting.py
@@ -0,0 +1,301 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+from contextlib import contextmanager
+from gettext import gettext as _
+
+from ._compat import term_len
+from .parser import _split_opt
+
+# Can force a width. This is used by the test system
+FORCED_WIDTH: int | None = None
+
+
+def measure_table(rows: cabc.Iterable[tuple[str, str]]) -> tuple[int, ...]:
+ widths: dict[int, int] = {}
+
+ for row in rows:
+ for idx, col in enumerate(row):
+ widths[idx] = max(widths.get(idx, 0), term_len(col))
+
+ return tuple(y for x, y in sorted(widths.items()))
+
+
+def iter_rows(
+ rows: cabc.Iterable[tuple[str, str]], col_count: int
+) -> cabc.Iterator[tuple[str, ...]]:
+ for row in rows:
+ yield row + ("",) * (col_count - len(row))
+
+
+def wrap_text(
+ text: str,
+ width: int = 78,
+ initial_indent: str = "",
+ subsequent_indent: str = "",
+ preserve_paragraphs: bool = False,
+) -> str:
+ """A helper function that intelligently wraps text. By default, it
+ assumes that it operates on a single paragraph of text but if the
+ `preserve_paragraphs` parameter is provided it will intelligently
+ handle paragraphs (defined by two empty lines).
+
+ If paragraphs are handled, a paragraph can be prefixed with an empty
+ line containing the ``\\b`` character (``\\x08``) to indicate that
+ no rewrapping should happen in that block.
+
+ :param text: the text that should be rewrapped.
+ :param width: the maximum width for the text.
+ :param initial_indent: the initial indent that should be placed on the
+ first line as a string.
+ :param subsequent_indent: the indent string that should be placed on
+ each consecutive line.
+ :param preserve_paragraphs: if this flag is set then the wrapping will
+ intelligently handle paragraphs.
+ """
+ from ._textwrap import TextWrapper
+
+ text = text.expandtabs()
+ wrapper = TextWrapper(
+ width,
+ initial_indent=initial_indent,
+ subsequent_indent=subsequent_indent,
+ replace_whitespace=False,
+ )
+ if not preserve_paragraphs:
+ return wrapper.fill(text)
+
+ p: list[tuple[int, bool, str]] = []
+ buf: list[str] = []
+ indent = None
+
+ def _flush_par() -> None:
+ if not buf:
+ return
+ if buf[0].strip() == "\b":
+ p.append((indent or 0, True, "\n".join(buf[1:])))
+ else:
+ p.append((indent or 0, False, " ".join(buf)))
+ del buf[:]
+
+ for line in text.splitlines():
+ if not line:
+ _flush_par()
+ indent = None
+ else:
+ if indent is None:
+ orig_len = term_len(line)
+ line = line.lstrip()
+ indent = orig_len - term_len(line)
+ buf.append(line)
+ _flush_par()
+
+ rv = []
+ for indent, raw, text in p:
+ with wrapper.extra_indent(" " * indent):
+ if raw:
+ rv.append(wrapper.indent_only(text))
+ else:
+ rv.append(wrapper.fill(text))
+
+ return "\n\n".join(rv)
+
+
+class HelpFormatter:
+ """This class helps with formatting text-based help pages. It's
+ usually just needed for very special internal cases, but it's also
+ exposed so that developers can write their own fancy outputs.
+
+ At present, it always writes into memory.
+
+ :param indent_increment: the additional increment for each level.
+ :param width: the width for the text. This defaults to the terminal
+ width clamped to a maximum of 78.
+ """
+
+ def __init__(
+ self,
+ indent_increment: int = 2,
+ width: int | None = None,
+ max_width: int | None = None,
+ ) -> None:
+ self.indent_increment = indent_increment
+ if max_width is None:
+ max_width = 80
+ if width is None:
+ import shutil
+
+ width = FORCED_WIDTH
+ if width is None:
+ width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50)
+ self.width = width
+ self.current_indent: int = 0
+ self.buffer: list[str] = []
+
+ def write(self, string: str) -> None:
+ """Writes a unicode string into the internal buffer."""
+ self.buffer.append(string)
+
+ def indent(self) -> None:
+ """Increases the indentation."""
+ self.current_indent += self.indent_increment
+
+ def dedent(self) -> None:
+ """Decreases the indentation."""
+ self.current_indent -= self.indent_increment
+
+ def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> None:
+ """Writes a usage line into the buffer.
+
+ :param prog: the program name.
+ :param args: whitespace separated list of arguments.
+ :param prefix: The prefix for the first line. Defaults to
+ ``"Usage: "``.
+ """
+ if prefix is None:
+ prefix = f"{_('Usage:')} "
+
+ usage_prefix = f"{prefix:>{self.current_indent}}{prog} "
+ text_width = self.width - self.current_indent
+
+ if text_width >= (term_len(usage_prefix) + 20):
+ # The arguments will fit to the right of the prefix.
+ indent = " " * term_len(usage_prefix)
+ self.write(
+ wrap_text(
+ args,
+ text_width,
+ initial_indent=usage_prefix,
+ subsequent_indent=indent,
+ )
+ )
+ else:
+ # The prefix is too long, put the arguments on the next line.
+ self.write(usage_prefix)
+ self.write("\n")
+ indent = " " * (max(self.current_indent, term_len(prefix)) + 4)
+ self.write(
+ wrap_text(
+ args, text_width, initial_indent=indent, subsequent_indent=indent
+ )
+ )
+
+ self.write("\n")
+
+ def write_heading(self, heading: str) -> None:
+ """Writes a heading into the buffer."""
+ self.write(f"{'':>{self.current_indent}}{heading}:\n")
+
+ def write_paragraph(self) -> None:
+ """Writes a paragraph into the buffer."""
+ if self.buffer:
+ self.write("\n")
+
+ def write_text(self, text: str) -> None:
+ """Writes re-indented text into the buffer. This rewraps and
+ preserves paragraphs.
+ """
+ indent = " " * self.current_indent
+ self.write(
+ wrap_text(
+ text,
+ self.width,
+ initial_indent=indent,
+ subsequent_indent=indent,
+ preserve_paragraphs=True,
+ )
+ )
+ self.write("\n")
+
+ def write_dl(
+ self,
+ rows: cabc.Sequence[tuple[str, str]],
+ col_max: int = 30,
+ col_spacing: int = 2,
+ ) -> None:
+ """Writes a definition list into the buffer. This is how options
+ and commands are usually formatted.
+
+ :param rows: a list of two item tuples for the terms and values.
+ :param col_max: the maximum width of the first column.
+ :param col_spacing: the number of spaces between the first and
+ second column.
+ """
+ rows = list(rows)
+ widths = measure_table(rows)
+ if len(widths) != 2:
+ raise TypeError("Expected two columns for definition list")
+
+ first_col = min(widths[0], col_max) + col_spacing
+
+ for first, second in iter_rows(rows, len(widths)):
+ self.write(f"{'':>{self.current_indent}}{first}")
+ if not second:
+ self.write("\n")
+ continue
+ if term_len(first) <= first_col - col_spacing:
+ self.write(" " * (first_col - term_len(first)))
+ else:
+ self.write("\n")
+ self.write(" " * (first_col + self.current_indent))
+
+ text_width = max(self.width - first_col - 2, 10)
+ wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True)
+ lines = wrapped_text.splitlines()
+
+ if lines:
+ self.write(f"{lines[0]}\n")
+
+ for line in lines[1:]:
+ self.write(f"{'':>{first_col + self.current_indent}}{line}\n")
+ else:
+ self.write("\n")
+
+ @contextmanager
+ def section(self, name: str) -> cabc.Iterator[None]:
+ """Helpful context manager that writes a paragraph, a heading,
+ and the indents.
+
+ :param name: the section name that is written as heading.
+ """
+ self.write_paragraph()
+ self.write_heading(name)
+ self.indent()
+ try:
+ yield
+ finally:
+ self.dedent()
+
+ @contextmanager
+ def indentation(self) -> cabc.Iterator[None]:
+ """A context manager that increases the indentation."""
+ self.indent()
+ try:
+ yield
+ finally:
+ self.dedent()
+
+ def getvalue(self) -> str:
+ """Returns the buffer contents."""
+ return "".join(self.buffer)
+
+
+def join_options(options: cabc.Sequence[str]) -> tuple[str, bool]:
+ """Given a list of option strings this joins them in the most appropriate
+ way and returns them in the form ``(formatted_string,
+ any_prefix_is_slash)`` where the second item in the tuple is a flag that
+ indicates if any of the option prefixes was a slash.
+ """
+ rv = []
+ any_prefix_is_slash = False
+
+ for opt in options:
+ prefix = _split_opt(opt)[0]
+
+ if prefix == "/":
+ any_prefix_is_slash = True
+
+ rv.append((len(prefix), opt))
+
+ rv.sort(key=lambda x: x[0])
+ return ", ".join(x[1] for x in rv), any_prefix_is_slash
diff --git a/venv/Lib/site-packages/click/globals.py b/venv/Lib/site-packages/click/globals.py
new file mode 100644
index 0000000..a2f9172
--- /dev/null
+++ b/venv/Lib/site-packages/click/globals.py
@@ -0,0 +1,67 @@
+from __future__ import annotations
+
+import typing as t
+from threading import local
+
+if t.TYPE_CHECKING:
+ from .core import Context
+
+_local = local()
+
+
+@t.overload
+def get_current_context(silent: t.Literal[False] = False) -> Context: ...
+
+
+@t.overload
+def get_current_context(silent: bool = ...) -> Context | None: ...
+
+
+def get_current_context(silent: bool = False) -> Context | None:
+ """Returns the current click context. This can be used as a way to
+ access the current context object from anywhere. This is a more implicit
+ alternative to the :func:`pass_context` decorator. This function is
+ primarily useful for helpers such as :func:`echo` which might be
+ interested in changing its behavior based on the current context.
+
+ To push the current context, :meth:`Context.scope` can be used.
+
+ .. versionadded:: 5.0
+
+ :param silent: if set to `True` the return value is `None` if no context
+ is available. The default behavior is to raise a
+ :exc:`RuntimeError`.
+ """
+ try:
+ return t.cast("Context", _local.stack[-1])
+ except (AttributeError, IndexError) as e:
+ if not silent:
+ raise RuntimeError("There is no active click context.") from e
+
+ return None
+
+
+def push_context(ctx: Context) -> None:
+ """Pushes a new context to the current stack."""
+ _local.__dict__.setdefault("stack", []).append(ctx)
+
+
+def pop_context() -> None:
+ """Removes the top level from the stack."""
+ _local.stack.pop()
+
+
+def resolve_color_default(color: bool | None = None) -> bool | None:
+ """Internal helper to get the default value of the color flag. If a
+ value is passed it's returned unchanged, otherwise it's looked up from
+ the current context.
+ """
+ if color is not None:
+ return color
+
+ ctx = get_current_context(silent=True)
+
+ if ctx is not None:
+ return ctx.color
+
+ return None
diff --git a/venv/Lib/site-packages/click/parser.py b/venv/Lib/site-packages/click/parser.py
new file mode 100644
index 0000000..1ea1f71
--- /dev/null
+++ b/venv/Lib/site-packages/click/parser.py
@@ -0,0 +1,532 @@
+"""
+This module started out as largely a copy paste from the stdlib's
+optparse module with the features removed that we do not need from
+optparse because we implement them in Click on a higher level (for
+instance type handling, help formatting and a lot more).
+
+The plan is to remove more and more from here over time.
+
+The reason this is a different module and not optparse from the stdlib
+is that there are differences in 2.x and 3.x about the error messages
+generated and optparse in the stdlib uses gettext for no good reason
+and might cause us issues.
+
+Click uses parts of optparse written by Gregory P. Ward and maintained
+by the Python Software Foundation. This is limited to code in parser.py.
+
+Copyright 2001-2006 Gregory P. Ward. All rights reserved.
+Copyright 2002-2006 Python Software Foundation. All rights reserved.
+"""
+
+# This code uses parts of optparse written by Gregory P. Ward and
+# maintained by the Python Software Foundation.
+# Copyright 2001-2006 Gregory P. Ward
+# Copyright 2002-2006 Python Software Foundation
+from __future__ import annotations
+
+import collections.abc as cabc
+import typing as t
+from collections import deque
+from gettext import gettext as _
+from gettext import ngettext
+
+from ._utils import FLAG_NEEDS_VALUE
+from ._utils import UNSET
+from .exceptions import BadArgumentUsage
+from .exceptions import BadOptionUsage
+from .exceptions import NoSuchOption
+from .exceptions import UsageError
+
+if t.TYPE_CHECKING:
+ from ._utils import T_FLAG_NEEDS_VALUE
+ from ._utils import T_UNSET
+ from .core import Argument as CoreArgument
+ from .core import Context
+ from .core import Option as CoreOption
+ from .core import Parameter as CoreParameter
+
+V = t.TypeVar("V")
+
+
+def _unpack_args(
+ args: cabc.Sequence[str], nargs_spec: cabc.Sequence[int]
+) -> tuple[cabc.Sequence[str | cabc.Sequence[str | None] | None], list[str]]:
+ """Given an iterable of arguments and an iterable of nargs specifications,
+ it returns a tuple with all the unpacked arguments at the first index
+ and all remaining arguments as the second.
+
+ The nargs specification is the number of arguments that should be consumed
+ or `-1` to indicate that this position should eat up all the remainders.
+
+ Missing items are filled with ``UNSET``.
+ """
+ args = deque(args)
+ nargs_spec = deque(nargs_spec)
+ rv: list[str | tuple[str | T_UNSET, ...] | T_UNSET] = []
+ spos: int | None = None
+
+ def _fetch(c: deque[V]) -> V | T_UNSET:
+ try:
+ if spos is None:
+ return c.popleft()
+ else:
+ return c.pop()
+ except IndexError:
+ return UNSET
+
+ while nargs_spec:
+ nargs = _fetch(nargs_spec)
+
+ if nargs is None:
+ continue
+
+ if nargs == 1:
+ rv.append(_fetch(args)) # type: ignore[arg-type]
+ elif nargs > 1:
+ x = [_fetch(args) for _ in range(nargs)]
+
+ # If we're reversed, we're pulling in the arguments in reverse,
+ # so we need to turn them around.
+ if spos is not None:
+ x.reverse()
+
+ rv.append(tuple(x))
+ elif nargs < 0:
+ if spos is not None:
+ raise TypeError("Cannot have two nargs < 0")
+
+ spos = len(rv)
+ rv.append(UNSET)
+
+ # spos is the position of the wildcard (star). If it's not `None`,
+ # we fill it with the remainder.
+ if spos is not None:
+ rv[spos] = tuple(args)
+ args = []
+ rv[spos + 1 :] = reversed(rv[spos + 1 :])
+
+ return tuple(rv), list(args)
+
+
+def _split_opt(opt: str) -> tuple[str, str]:
+ first = opt[:1]
+ if first.isalnum():
+ return "", opt
+ if opt[1:2] == first:
+ return opt[:2], opt[2:]
+ return first, opt[1:]
+
+
+def _normalize_opt(opt: str, ctx: Context | None) -> str:
+ if ctx is None or ctx.token_normalize_func is None:
+ return opt
+ prefix, opt = _split_opt(opt)
+ return f"{prefix}{ctx.token_normalize_func(opt)}"
+
+
+class _Option:
+ def __init__(
+ self,
+ obj: CoreOption,
+ opts: cabc.Sequence[str],
+ dest: str | None,
+ action: str | None = None,
+ nargs: int = 1,
+ const: t.Any | None = None,
+ ):
+ self._short_opts = []
+ self._long_opts = []
+ self.prefixes: set[str] = set()
+
+ for opt in opts:
+ prefix, value = _split_opt(opt)
+ if not prefix:
+ raise ValueError(f"Invalid start character for option ({opt})")
+ self.prefixes.add(prefix[0])
+ if len(prefix) == 1 and len(value) == 1:
+ self._short_opts.append(opt)
+ else:
+ self._long_opts.append(opt)
+ self.prefixes.add(prefix)
+
+ if action is None:
+ action = "store"
+
+ self.dest = dest
+ self.action = action
+ self.nargs = nargs
+ self.const = const
+ self.obj = obj
+
+ @property
+ def takes_value(self) -> bool:
+ return self.action in ("store", "append")
+
+ def process(self, value: t.Any, state: _ParsingState) -> None:
+ if self.action == "store":
+ state.opts[self.dest] = value # type: ignore
+ elif self.action == "store_const":
+ state.opts[self.dest] = self.const # type: ignore
+ elif self.action == "append":
+ state.opts.setdefault(self.dest, []).append(value) # type: ignore
+ elif self.action == "append_const":
+ state.opts.setdefault(self.dest, []).append(self.const) # type: ignore
+ elif self.action == "count":
+ state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore
+ else:
+ raise ValueError(f"unknown action '{self.action}'")
+ state.order.append(self.obj)
+
+
+class _Argument:
+ def __init__(self, obj: CoreArgument, dest: str | None, nargs: int = 1):
+ self.dest = dest
+ self.nargs = nargs
+ self.obj = obj
+
+ def process(
+ self,
+ value: str | cabc.Sequence[str | None] | None | T_UNSET,
+ state: _ParsingState,
+ ) -> None:
+ if self.nargs > 1:
+ assert isinstance(value, cabc.Sequence)
+ holes = sum(1 for x in value if x is UNSET)
+ if holes == len(value):
+ value = UNSET
+ elif holes != 0:
+ raise BadArgumentUsage(
+ _("Argument {name!r} takes {nargs} values.").format(
+ name=self.dest, nargs=self.nargs
+ )
+ )
+
+ # We failed to collect any argument value so we consider the argument as unset.
+ if value == ():
+ value = UNSET
+
+ state.opts[self.dest] = value # type: ignore
+ state.order.append(self.obj)
+
+
+class _ParsingState:
+ def __init__(self, rargs: list[str]) -> None:
+ self.opts: dict[str, t.Any] = {}
+ self.largs: list[str] = []
+ self.rargs = rargs
+ self.order: list[CoreParameter] = []
+
+
+class _OptionParser:
+ """The option parser is an internal class that is ultimately used to
+ parse options and arguments. It's modelled after optparse and brings
+ a similar but vastly simplified API. It should generally not be used
+ directly as the high level Click classes wrap it for you.
+
+ It's not nearly as extensible as optparse or argparse as it does not
+ implement features that are implemented on a higher level (such as
+ types or defaults).
+
+ :param ctx: optionally the :class:`~click.Context` where this parser
+ should go with.
+
+ .. deprecated:: 8.2
+ Will be removed in Click 9.0.
+ """
+
+ def __init__(self, ctx: Context | None = None) -> None:
+ #: The :class:`~click.Context` for this parser. This might be
+ #: `None` for some advanced use cases.
+ self.ctx = ctx
+ #: This controls how the parser deals with interspersed arguments.
+ #: If this is set to `False`, the parser will stop on the first
+ #: non-option. Click uses this to implement nested subcommands
+ #: safely.
+ self.allow_interspersed_args: bool = True
+ #: This tells the parser how to deal with unknown options. By
+ #: default it will error out (which is sensible), but there is a
+ #: second mode where it will ignore it and continue processing
+ #: after shifting all the unknown options into the resulting args.
+ self.ignore_unknown_options: bool = False
+
+ if ctx is not None:
+ self.allow_interspersed_args = ctx.allow_interspersed_args
+ self.ignore_unknown_options = ctx.ignore_unknown_options
+
+ self._short_opt: dict[str, _Option] = {}
+ self._long_opt: dict[str, _Option] = {}
+ self._opt_prefixes = {"-", "--"}
+ self._args: list[_Argument] = []
+
+ def add_option(
+ self,
+ obj: CoreOption,
+ opts: cabc.Sequence[str],
+ dest: str | None,
+ action: str | None = None,
+ nargs: int = 1,
+ const: t.Any | None = None,
+ ) -> None:
+ """Adds a new option named `dest` to the parser. The destination
+ is not inferred (unlike with optparse) and needs to be explicitly
+ provided. Action can be any of ``store``, ``store_const``,
+ ``append``, ``append_const`` or ``count``.
+
+ The `obj` can be used to identify the option in the order list
+ that is returned from the parser.
+ """
+ opts = [_normalize_opt(opt, self.ctx) for opt in opts]
+ option = _Option(obj, opts, dest, action=action, nargs=nargs, const=const)
+ self._opt_prefixes.update(option.prefixes)
+ for opt in option._short_opts:
+ self._short_opt[opt] = option
+ for opt in option._long_opts:
+ self._long_opt[opt] = option
+
+ def add_argument(self, obj: CoreArgument, dest: str | None, nargs: int = 1) -> None:
+ """Adds a positional argument named `dest` to the parser.
+
+ The `obj` can be used to identify the option in the order list
+ that is returned from the parser.
+ """
+ self._args.append(_Argument(obj, dest=dest, nargs=nargs))
+
+ def parse_args(
+ self, args: list[str]
+ ) -> tuple[dict[str, t.Any], list[str], list[CoreParameter]]:
+ """Parses positional arguments and returns ``(values, args, order)``
+ for the parsed options and arguments as well as the leftover
+ arguments if there are any. The order is a list of objects as they
+ appear on the command line. If arguments appear multiple times they
+ will be memorized multiple times as well.
+ """
+ state = _ParsingState(args)
+ try:
+ self._process_args_for_options(state)
+ self._process_args_for_args(state)
+ except UsageError:
+ if self.ctx is None or not self.ctx.resilient_parsing:
+ raise
+ return state.opts, state.largs, state.order
+
+ def _process_args_for_args(self, state: _ParsingState) -> None:
+ pargs, args = _unpack_args(
+ state.largs + state.rargs, [x.nargs for x in self._args]
+ )
+
+ for idx, arg in enumerate(self._args):
+ arg.process(pargs[idx], state)
+
+ state.largs = args
+ state.rargs = []
+
+ def _process_args_for_options(self, state: _ParsingState) -> None:
+ while state.rargs:
+ arg = state.rargs.pop(0)
+ arglen = len(arg)
+ # Double dashes always handled explicitly regardless of what
+ # prefixes are valid.
+ if arg == "--":
+ return
+ elif arg[:1] in self._opt_prefixes and arglen > 1:
+ self._process_opts(arg, state)
+ elif self.allow_interspersed_args:
+ state.largs.append(arg)
+ else:
+ state.rargs.insert(0, arg)
+ return
+
+ # Say this is the original argument list:
+ # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]
+ # ^
+ # (we are about to process arg(i)).
+ #
+ # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of
+ # [arg0, ..., arg(i-1)] (any options and their arguments will have
+ # been removed from largs).
+ #
+ # The while loop will usually consume 1 or more arguments per pass.
+ # If it consumes 1 (eg. arg is an option that takes no arguments),
+ # then after _process_arg() is done the situation is:
+ #
+ # largs = subset of [arg0, ..., arg(i)]
+ # rargs = [arg(i+1), ..., arg(N-1)]
+ #
+ # If allow_interspersed_args is false, largs will always be
+ # *empty* -- still a subset of [arg0, ..., arg(i-1)], but
+ # not a very interesting subset!
+
+ def _match_long_opt(
+ self, opt: str, explicit_value: str | None, state: _ParsingState
+ ) -> None:
+ if opt not in self._long_opt:
+ from difflib import get_close_matches
+
+ possibilities = get_close_matches(opt, self._long_opt)
+ raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
+
+ option = self._long_opt[opt]
+ if option.takes_value:
+ # At this point it's safe to modify rargs by injecting the
+ # explicit value, because no exception is raised in this
+ # branch. This means that the inserted value will be fully
+ # consumed.
+ if explicit_value is not None:
+ state.rargs.insert(0, explicit_value)
+
+ value = self._get_value_from_state(opt, option, state)
+
+ elif explicit_value is not None:
+ raise BadOptionUsage(
+ opt, _("Option {name!r} does not take a value.").format(name=opt)
+ )
+
+ else:
+ value = UNSET
+
+ option.process(value, state)
+
+ def _match_short_opt(self, arg: str, state: _ParsingState) -> None:
+ stop = False
+ i = 1
+ prefix = arg[0]
+ unknown_options = []
+
+ for ch in arg[1:]:
+ opt = _normalize_opt(f"{prefix}{ch}", self.ctx)
+ option = self._short_opt.get(opt)
+ i += 1
+
+ if not option:
+ if self.ignore_unknown_options:
+ unknown_options.append(ch)
+ continue
+ raise NoSuchOption(opt, ctx=self.ctx)
+ if option.takes_value:
+ # Any characters left in arg? Pretend they're the
+ # next arg, and stop consuming characters of arg.
+ if i < len(arg):
+ state.rargs.insert(0, arg[i:])
+ stop = True
+
+ value = self._get_value_from_state(opt, option, state)
+
+ else:
+ value = UNSET
+
+ option.process(value, state)
+
+ if stop:
+ break
+
+ # If we got any unknown options we recombine the string of the
+ # remaining options and re-attach the prefix, then report that
+ # to the state as new larg. This way there is basic combinatorics
+ # that can be achieved while still ignoring unknown arguments.
+ if self.ignore_unknown_options and unknown_options:
+ state.largs.append(f"{prefix}{''.join(unknown_options)}")
+
+ def _get_value_from_state(
+ self, option_name: str, option: _Option, state: _ParsingState
+ ) -> str | cabc.Sequence[str] | T_FLAG_NEEDS_VALUE:
+ nargs = option.nargs
+
+ value: str | cabc.Sequence[str] | T_FLAG_NEEDS_VALUE
+
+ if len(state.rargs) < nargs:
+ if option.obj._flag_needs_value:
+ # Option allows omitting the value.
+ value = FLAG_NEEDS_VALUE
+ else:
+ raise BadOptionUsage(
+ option_name,
+ ngettext(
+ "Option {name!r} requires an argument.",
+ "Option {name!r} requires {nargs} arguments.",
+ nargs,
+ ).format(name=option_name, nargs=nargs),
+ )
+ elif nargs == 1:
+ next_rarg = state.rargs[0]
+
+ if (
+ option.obj._flag_needs_value
+ and isinstance(next_rarg, str)
+ and next_rarg[:1] in self._opt_prefixes
+ and len(next_rarg) > 1
+ ):
+ # The next arg looks like the start of an option, don't
+ # use it as the value if omitting the value is allowed.
+ value = FLAG_NEEDS_VALUE
+ else:
+ value = state.rargs.pop(0)
+ else:
+ value = tuple(state.rargs[:nargs])
+ del state.rargs[:nargs]
+
+ return value
+
+ def _process_opts(self, arg: str, state: _ParsingState) -> None:
+ explicit_value = None
+ # Long option handling happens in two parts. The first part is
+ # supporting explicitly attached values. In any case, we will try
+ # to long match the option first.
+ if "=" in arg:
+ long_opt, explicit_value = arg.split("=", 1)
+ else:
+ long_opt = arg
+ norm_long_opt = _normalize_opt(long_opt, self.ctx)
+
+ # At this point we will match the (assumed) long option through
+ # the long option matching code. Note that this allows options
+ # like "-foo" to be matched as long options.
+ try:
+ self._match_long_opt(norm_long_opt, explicit_value, state)
+ except NoSuchOption:
+ # At this point the long option matching failed, and we need
+ # to try with short options. However there is a special rule
+ # which says, that if we have a two character options prefix
+ # (applies to "--foo" for instance), we do not dispatch to the
+ # short option code and will instead raise the no option
+ # error.
+ if arg[:2] not in self._opt_prefixes:
+ self._match_short_opt(arg, state)
+ return
+
+ if not self.ignore_unknown_options:
+ raise
+
+ state.largs.append(arg)
+
+
+def __getattr__(name: str) -> object:
+ import warnings
+
+ if name in {
+ "OptionParser",
+ "Argument",
+ "Option",
+ "split_opt",
+ "normalize_opt",
+ "ParsingState",
+ }:
+ warnings.warn(
+ f"'parser.{name}' is deprecated and will be removed in Click 9.0."
+ " The old parser is available in 'optparse'.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return globals()[f"_{name}"]
+
+ if name == "split_arg_string":
+ from .shell_completion import split_arg_string
+
+ warnings.warn(
+ "Importing 'parser.split_arg_string' is deprecated, it will only be"
+ " available in 'shell_completion' in Click 9.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return split_arg_string
+
+ raise AttributeError(name)
diff --git a/venv/Lib/site-packages/click/py.typed b/venv/Lib/site-packages/click/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/click/shell_completion.py b/venv/Lib/site-packages/click/shell_completion.py
new file mode 100644
index 0000000..8f1564c
--- /dev/null
+++ b/venv/Lib/site-packages/click/shell_completion.py
@@ -0,0 +1,667 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import os
+import re
+import typing as t
+from gettext import gettext as _
+
+from .core import Argument
+from .core import Command
+from .core import Context
+from .core import Group
+from .core import Option
+from .core import Parameter
+from .core import ParameterSource
+from .utils import echo
+
+
+def shell_complete(
+ cli: Command,
+ ctx_args: cabc.MutableMapping[str, t.Any],
+ prog_name: str,
+ complete_var: str,
+ instruction: str,
+) -> int:
+ """Perform shell completion for the given CLI program.
+
+ :param cli: Command being called.
+ :param ctx_args: Extra arguments to pass to
+ ``cli.make_context``.
+ :param prog_name: Name of the executable in the shell.
+ :param complete_var: Name of the environment variable that holds
+ the completion instruction.
+ :param instruction: Value of ``complete_var`` with the completion
+ instruction and shell, in the form ``instruction_shell``.
+ :return: Status code to exit with.
+ """
+ shell, _, instruction = instruction.partition("_")
+ comp_cls = get_completion_class(shell)
+
+ if comp_cls is None:
+ return 1
+
+ comp = comp_cls(cli, ctx_args, prog_name, complete_var)
+
+ if instruction == "source":
+ echo(comp.source())
+ return 0
+
+ if instruction == "complete":
+ echo(comp.complete())
+ return 0
+
+ return 1
+
+
+class CompletionItem:
+ """Represents a completion value and metadata about the value. The
+ default metadata is ``type`` to indicate special shell handling,
+ and ``help`` if a shell supports showing a help string next to the
+ value.
+
+ Arbitrary parameters can be passed when creating the object, and
+ accessed using ``item.attr``. If an attribute wasn't passed,
+ accessing it returns ``None``.
+
+ :param value: The completion suggestion.
+ :param type: Tells the shell script to provide special completion
+ support for the type. Click uses ``"dir"`` and ``"file"``.
+ :param help: String shown next to the value if supported.
+ :param kwargs: Arbitrary metadata. The built-in implementations
+ don't use this, but custom type completions paired with custom
+ shell support could use it.
+ """
+
+ __slots__ = ("value", "type", "help", "_info")
+
+ def __init__(
+ self,
+ value: t.Any,
+ type: str = "plain",
+ help: str | None = None,
+ **kwargs: t.Any,
+ ) -> None:
+ self.value: t.Any = value
+ self.type: str = type
+ self.help: str | None = help
+ self._info = kwargs
+
+ def __getattr__(self, name: str) -> t.Any:
+ return self._info.get(name)
+
+
+# Only Bash >= 4.4 has the nosort option.
+_SOURCE_BASH = """\
+%(complete_func)s() {
+ local IFS=$'\\n'
+ local response
+
+ response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \
+%(complete_var)s=bash_complete $1)
+
+ for completion in $response; do
+ IFS=',' read type value <<< "$completion"
+
+ if [[ $type == 'dir' ]]; then
+ COMPREPLY=()
+ compopt -o dirnames
+ elif [[ $type == 'file' ]]; then
+ COMPREPLY=()
+ compopt -o default
+ elif [[ $type == 'plain' ]]; then
+ COMPREPLY+=($value)
+ fi
+ done
+
+ return 0
+}
+
+%(complete_func)s_setup() {
+ complete -o nosort -F %(complete_func)s %(prog_name)s
+}
+
+%(complete_func)s_setup;
+"""
+
+# See ZshComplete.format_completion below, and issue #2703, before
+# changing this script.
+#
+# (TL;DR: _describe is picky about the format, but this Zsh script snippet
+# is already widely deployed. So freeze this script, and use clever-ish
+# handling of colons in ZshComplet.format_completion.)
+_SOURCE_ZSH = """\
+#compdef %(prog_name)s
+
+%(complete_func)s() {
+ local -a completions
+ local -a completions_with_descriptions
+ local -a response
+ (( ! $+commands[%(prog_name)s] )) && return 1
+
+ response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \
+%(complete_var)s=zsh_complete %(prog_name)s)}")
+
+ for type key descr in ${response}; do
+ if [[ "$type" == "plain" ]]; then
+ if [[ "$descr" == "_" ]]; then
+ completions+=("$key")
+ else
+ completions_with_descriptions+=("$key":"$descr")
+ fi
+ elif [[ "$type" == "dir" ]]; then
+ _path_files -/
+ elif [[ "$type" == "file" ]]; then
+ _path_files -f
+ fi
+ done
+
+ if [ -n "$completions_with_descriptions" ]; then
+ _describe -V unsorted completions_with_descriptions -U
+ fi
+
+ if [ -n "$completions" ]; then
+ compadd -U -V unsorted -a completions
+ fi
+}
+
+if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
+ # autoload from fpath, call function directly
+ %(complete_func)s "$@"
+else
+ # eval/source/. command, register function for later
+ compdef %(complete_func)s %(prog_name)s
+fi
+"""
+
+_SOURCE_FISH = """\
+function %(complete_func)s;
+ set -l response (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \
+COMP_CWORD=(commandline -t) %(prog_name)s);
+
+ for completion in $response;
+ set -l metadata (string split "," $completion);
+
+ if test $metadata[1] = "dir";
+ __fish_complete_directories $metadata[2];
+ else if test $metadata[1] = "file";
+ __fish_complete_path $metadata[2];
+ else if test $metadata[1] = "plain";
+ echo $metadata[2];
+ end;
+ end;
+end;
+
+complete --no-files --command %(prog_name)s --arguments \
+"(%(complete_func)s)";
+"""
+
+
+class ShellComplete:
+ """Base class for providing shell completion support. A subclass for
+ a given shell will override attributes and methods to implement the
+ completion instructions (``source`` and ``complete``).
+
+ :param cli: Command being called.
+ :param prog_name: Name of the executable in the shell.
+ :param complete_var: Name of the environment variable that holds
+ the completion instruction.
+
+ .. versionadded:: 8.0
+ """
+
+ name: t.ClassVar[str]
+ """Name to register the shell as with :func:`add_completion_class`.
+ This is used in completion instructions (``{name}_source`` and
+ ``{name}_complete``).
+ """
+
+ source_template: t.ClassVar[str]
+ """Completion script template formatted by :meth:`source`. This must
+ be provided by subclasses.
+ """
+
+ def __init__(
+ self,
+ cli: Command,
+ ctx_args: cabc.MutableMapping[str, t.Any],
+ prog_name: str,
+ complete_var: str,
+ ) -> None:
+ self.cli = cli
+ self.ctx_args = ctx_args
+ self.prog_name = prog_name
+ self.complete_var = complete_var
+
+ @property
+ def func_name(self) -> str:
+ """The name of the shell function defined by the completion
+ script.
+ """
+ safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), flags=re.ASCII)
+ return f"_{safe_name}_completion"
+
+ def source_vars(self) -> dict[str, t.Any]:
+ """Vars for formatting :attr:`source_template`.
+
+ By default this provides ``complete_func``, ``complete_var``,
+ and ``prog_name``.
+ """
+ return {
+ "complete_func": self.func_name,
+ "complete_var": self.complete_var,
+ "prog_name": self.prog_name,
+ }
+
+ def source(self) -> str:
+ """Produce the shell script that defines the completion
+ function. By default this ``%``-style formats
+ :attr:`source_template` with the dict returned by
+ :meth:`source_vars`.
+ """
+ return self.source_template % self.source_vars()
+
+ def get_completion_args(self) -> tuple[list[str], str]:
+ """Use the env vars defined by the shell script to return a
+ tuple of ``args, incomplete``. This must be implemented by
+ subclasses.
+ """
+ raise NotImplementedError
+
+ def get_completions(self, args: list[str], incomplete: str) -> list[CompletionItem]:
+ """Determine the context and last complete command or parameter
+ from the complete args. Call that object's ``shell_complete``
+ method to get the completions for the incomplete value.
+
+ :param args: List of complete args before the incomplete value.
+ :param incomplete: Value being completed. May be empty.
+ """
+ ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args)
+ obj, incomplete = _resolve_incomplete(ctx, args, incomplete)
+ return obj.shell_complete(ctx, incomplete)
+
+ def format_completion(self, item: CompletionItem) -> str:
+ """Format a completion item into the form recognized by the
+ shell script. This must be implemented by subclasses.
+
+ :param item: Completion item to format.
+ """
+ raise NotImplementedError
+
+ def complete(self) -> str:
+ """Produce the completion data to send back to the shell.
+
+ By default this calls :meth:`get_completion_args`, gets the
+ completions, then calls :meth:`format_completion` for each
+ completion.
+ """
+ args, incomplete = self.get_completion_args()
+ completions = self.get_completions(args, incomplete)
+ out = [self.format_completion(item) for item in completions]
+ return "\n".join(out)
+
+
+class BashComplete(ShellComplete):
+ """Shell completion for Bash."""
+
+ name = "bash"
+ source_template = _SOURCE_BASH
+
+ @staticmethod
+ def _check_version() -> None:
+ import shutil
+ import subprocess
+
+ bash_exe = shutil.which("bash")
+
+ if bash_exe is None:
+ match = None
+ else:
+ output = subprocess.run(
+ [bash_exe, "--norc", "-c", 'echo "${BASH_VERSION}"'],
+ stdout=subprocess.PIPE,
+ )
+ match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode())
+
+ if match is not None:
+ major, minor = match.groups()
+
+ if major < "4" or major == "4" and minor < "4":
+ echo(
+ _(
+ "Shell completion is not supported for Bash"
+ " versions older than 4.4."
+ ),
+ err=True,
+ )
+ else:
+ echo(
+ _("Couldn't detect Bash version, shell completion is not supported."),
+ err=True,
+ )
+
+ def source(self) -> str:
+ self._check_version()
+ return super().source()
+
+ def get_completion_args(self) -> tuple[list[str], str]:
+ cwords = split_arg_string(os.environ["COMP_WORDS"])
+ cword = int(os.environ["COMP_CWORD"])
+ args = cwords[1:cword]
+
+ try:
+ incomplete = cwords[cword]
+ except IndexError:
+ incomplete = ""
+
+ return args, incomplete
+
+ def format_completion(self, item: CompletionItem) -> str:
+ return f"{item.type},{item.value}"
+
+
+class ZshComplete(ShellComplete):
+ """Shell completion for Zsh."""
+
+ name = "zsh"
+ source_template = _SOURCE_ZSH
+
+ def get_completion_args(self) -> tuple[list[str], str]:
+ cwords = split_arg_string(os.environ["COMP_WORDS"])
+ cword = int(os.environ["COMP_CWORD"])
+ args = cwords[1:cword]
+
+ try:
+ incomplete = cwords[cword]
+ except IndexError:
+ incomplete = ""
+
+ return args, incomplete
+
+ def format_completion(self, item: CompletionItem) -> str:
+ help_ = item.help or "_"
+ # The zsh completion script uses `_describe` on items with help
+ # texts (which splits the item help from the item value at the
+ # first unescaped colon) and `compadd` on items without help
+ # text (which uses the item value as-is and does not support
+ # colon escaping). So escape colons in the item value if and
+ # only if the item help is not the sentinel "_" value, as used
+ # by the completion script.
+ #
+ # (The zsh completion script is potentially widely deployed, and
+ # thus harder to fix than this method.)
+ #
+ # See issue #1812 and issue #2703 for further context.
+ value = item.value.replace(":", r"\:") if help_ != "_" else item.value
+ return f"{item.type}\n{value}\n{help_}"
+
+
+class FishComplete(ShellComplete):
+ """Shell completion for Fish."""
+
+ name = "fish"
+ source_template = _SOURCE_FISH
+
+ def get_completion_args(self) -> tuple[list[str], str]:
+ cwords = split_arg_string(os.environ["COMP_WORDS"])
+ incomplete = os.environ["COMP_CWORD"]
+ if incomplete:
+ incomplete = split_arg_string(incomplete)[0]
+ args = cwords[1:]
+
+ # Fish stores the partial word in both COMP_WORDS and
+ # COMP_CWORD, remove it from complete args.
+ if incomplete and args and args[-1] == incomplete:
+ args.pop()
+
+ return args, incomplete
+
+ def format_completion(self, item: CompletionItem) -> str:
+ if item.help:
+ return f"{item.type},{item.value}\t{item.help}"
+
+ return f"{item.type},{item.value}"
+
+
+ShellCompleteType = t.TypeVar("ShellCompleteType", bound="type[ShellComplete]")
+
+
+_available_shells: dict[str, type[ShellComplete]] = {
+ "bash": BashComplete,
+ "fish": FishComplete,
+ "zsh": ZshComplete,
+}
+
+
+def add_completion_class(
+ cls: ShellCompleteType, name: str | None = None
+) -> ShellCompleteType:
+ """Register a :class:`ShellComplete` subclass under the given name.
+ The name will be provided by the completion instruction environment
+ variable during completion.
+
+ :param cls: The completion class that will handle completion for the
+ shell.
+ :param name: Name to register the class under. Defaults to the
+ class's ``name`` attribute.
+ """
+ if name is None:
+ name = cls.name
+
+ _available_shells[name] = cls
+
+ return cls
+
+
+def get_completion_class(shell: str) -> type[ShellComplete] | None:
+ """Look up a registered :class:`ShellComplete` subclass by the name
+ provided by the completion instruction environment variable. If the
+ name isn't registered, returns ``None``.
+
+ :param shell: Name the class is registered under.
+ """
+ return _available_shells.get(shell)
+
+
+def split_arg_string(string: str) -> list[str]:
+ """Split an argument string as with :func:`shlex.split`, but don't
+ fail if the string is incomplete. Ignores a missing closing quote or
+ incomplete escape sequence and uses the partial token as-is.
+
+ .. code-block:: python
+
+ split_arg_string("example 'my file")
+ ["example", "my file"]
+
+ split_arg_string("example my\\")
+ ["example", "my"]
+
+ :param string: String to split.
+
+ .. versionchanged:: 8.2
+ Moved to ``shell_completion`` from ``parser``.
+ """
+ import shlex
+
+ lex = shlex.shlex(string, posix=True)
+ lex.whitespace_split = True
+ lex.commenters = ""
+ out = []
+
+ try:
+ for token in lex:
+ out.append(token)
+ except ValueError:
+ # Raised when end-of-string is reached in an invalid state. Use
+ # the partial token as-is. The quote or escape character is in
+ # lex.state, not lex.token.
+ out.append(lex.token)
+
+ return out
+
+
+def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool:
+ """Determine if the given parameter is an argument that can still
+ accept values.
+
+ :param ctx: Invocation context for the command represented by the
+ parsed complete args.
+ :param param: Argument object being checked.
+ """
+ if not isinstance(param, Argument):
+ return False
+
+ assert param.name is not None
+ # Will be None if expose_value is False.
+ value = ctx.params.get(param.name)
+ return (
+ param.nargs == -1
+ or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE
+ or (
+ param.nargs > 1
+ and isinstance(value, (tuple, list))
+ and len(value) < param.nargs
+ )
+ )
+
+
+def _start_of_option(ctx: Context, value: str) -> bool:
+ """Check if the value looks like the start of an option."""
+ if not value:
+ return False
+
+ c = value[0]
+ return c in ctx._opt_prefixes
+
+
+def _is_incomplete_option(ctx: Context, args: list[str], param: Parameter) -> bool:
+ """Determine if the given parameter is an option that needs a value.
+
+ :param args: List of complete args before the incomplete value.
+ :param param: Option object being checked.
+ """
+ if not isinstance(param, Option):
+ return False
+
+ if param.is_flag or param.count:
+ return False
+
+ last_option = None
+
+ for index, arg in enumerate(reversed(args)):
+ if index + 1 > param.nargs:
+ break
+
+ if _start_of_option(ctx, arg):
+ last_option = arg
+ break
+
+ return last_option is not None and last_option in param.opts
+
+
+def _resolve_context(
+ cli: Command,
+ ctx_args: cabc.MutableMapping[str, t.Any],
+ prog_name: str,
+ args: list[str],
+) -> Context:
+ """Produce the context hierarchy starting with the command and
+ traversing the complete arguments. This only follows the commands,
+ it doesn't trigger input prompts or callbacks.
+
+ :param cli: Command being called.
+ :param prog_name: Name of the executable in the shell.
+ :param args: List of complete args before the incomplete value.
+ """
+ ctx_args["resilient_parsing"] = True
+ with cli.make_context(prog_name, args.copy(), **ctx_args) as ctx:
+ args = ctx._protected_args + ctx.args
+
+ while args:
+ command = ctx.command
+
+ if isinstance(command, Group):
+ if not command.chain:
+ name, cmd, args = command.resolve_command(ctx, args)
+
+ if cmd is None:
+ return ctx
+
+ with cmd.make_context(
+ name, args, parent=ctx, resilient_parsing=True
+ ) as sub_ctx:
+ ctx = sub_ctx
+ args = ctx._protected_args + ctx.args
+ else:
+ sub_ctx = ctx
+
+ while args:
+ name, cmd, args = command.resolve_command(ctx, args)
+
+ if cmd is None:
+ return ctx
+
+ with cmd.make_context(
+ name,
+ args,
+ parent=ctx,
+ allow_extra_args=True,
+ allow_interspersed_args=False,
+ resilient_parsing=True,
+ ) as sub_sub_ctx:
+ sub_ctx = sub_sub_ctx
+ args = sub_ctx.args
+
+ ctx = sub_ctx
+ args = [*sub_ctx._protected_args, *sub_ctx.args]
+ else:
+ break
+
+ return ctx
+
+
+def _resolve_incomplete(
+ ctx: Context, args: list[str], incomplete: str
+) -> tuple[Command | Parameter, str]:
+ """Find the Click object that will handle the completion of the
+ incomplete value. Return the object and the incomplete value.
+
+ :param ctx: Invocation context for the command represented by
+ the parsed complete args.
+ :param args: List of complete args before the incomplete value.
+ :param incomplete: Value being completed. May be empty.
+ """
+ # Different shells treat an "=" between a long option name and
+ # value differently. Might keep the value joined, return the "="
+ # as a separate item, or return the split name and value. Always
+ # split and discard the "=" to make completion easier.
+ if incomplete == "=":
+ incomplete = ""
+ elif "=" in incomplete and _start_of_option(ctx, incomplete):
+ name, _, incomplete = incomplete.partition("=")
+ args.append(name)
+
+ # The "--" marker tells Click to stop treating values as options
+ # even if they start with the option character. If it hasn't been
+ # given and the incomplete arg looks like an option, the current
+ # command will provide option name completions.
+ if "--" not in args and _start_of_option(ctx, incomplete):
+ return ctx.command, incomplete
+
+ params = ctx.command.get_params(ctx)
+
+ # If the last complete arg is an option name with an incomplete
+ # value, the option will provide value completions.
+ for param in params:
+ if _is_incomplete_option(ctx, args, param):
+ return param, incomplete
+
+ # It's not an option name or value. The first argument without a
+ # parsed value will provide value completions.
+ for param in params:
+ if _is_incomplete_argument(ctx, param):
+ return param, incomplete
+
+ # There were no unparsed arguments, the command may be a group that
+ # will provide command name completions.
+ return ctx.command, incomplete
diff --git a/venv/Lib/site-packages/click/termui.py b/venv/Lib/site-packages/click/termui.py
new file mode 100644
index 0000000..48f671b
--- /dev/null
+++ b/venv/Lib/site-packages/click/termui.py
@@ -0,0 +1,891 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import inspect
+import io
+import itertools
+import sys
+import typing as t
+from contextlib import AbstractContextManager
+from gettext import gettext as _
+
+from ._compat import isatty
+from ._compat import strip_ansi
+from .exceptions import Abort
+from .exceptions import UsageError
+from .globals import resolve_color_default
+from .types import Choice
+from .types import convert_type
+from .types import ParamType
+from .utils import echo
+from .utils import LazyFile
+
+if t.TYPE_CHECKING:
+ from ._termui_impl import ProgressBar
+
+V = t.TypeVar("V")
+
+# The prompt functions to use. The doc tools currently override these
+# functions to customize how they work.
+visible_prompt_func: t.Callable[[str], str] = input
+
+_ansi_colors = {
+ "black": 30,
+ "red": 31,
+ "green": 32,
+ "yellow": 33,
+ "blue": 34,
+ "magenta": 35,
+ "cyan": 36,
+ "white": 37,
+ "reset": 39,
+ "bright_black": 90,
+ "bright_red": 91,
+ "bright_green": 92,
+ "bright_yellow": 93,
+ "bright_blue": 94,
+ "bright_magenta": 95,
+ "bright_cyan": 96,
+ "bright_white": 97,
+}
+_ansi_reset_all = "\033[0m"
+
+
+def hidden_prompt_func(prompt: str) -> str:
+ import getpass
+
+ return getpass.getpass(prompt)
+
+
+def _build_prompt(
+ text: str,
+ suffix: str,
+ show_default: bool | str = False,
+ default: t.Any | None = None,
+ show_choices: bool = True,
+ type: ParamType | None = None,
+) -> str:
+ prompt = text
+ if type is not None and show_choices and isinstance(type, Choice):
+ prompt += f" ({', '.join(map(str, type.choices))})"
+ if isinstance(show_default, str):
+ default = f"({show_default})"
+ if default is not None and show_default:
+ prompt = f"{prompt} [{_format_default(default)}]"
+ return f"{prompt}{suffix}"
+
+
+def _format_default(default: t.Any) -> t.Any:
+ if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"):
+ return default.name
+
+ return default
+
+
+def prompt(
+ text: str,
+ default: t.Any | None = None,
+ hide_input: bool = False,
+ confirmation_prompt: bool | str = False,
+ type: ParamType | t.Any | None = None,
+ value_proc: t.Callable[[str], t.Any] | None = None,
+ prompt_suffix: str = ": ",
+ show_default: bool | str = True,
+ err: bool = False,
+ show_choices: bool = True,
+) -> t.Any:
+ """Prompts a user for input. This is a convenience function that can
+ be used to prompt a user for input later.
+
+ If the user aborts the input by sending an interrupt signal, this
+ function will catch it and raise a :exc:`Abort` exception.
+
+ :param text: the text to show for the prompt.
+ :param default: the default value to use if no input happens. If this
+ is not given it will prompt until it's aborted.
+ :param hide_input: if this is set to true then the input value will
+ be hidden.
+ :param confirmation_prompt: Prompt a second time to confirm the
+ value. Can be set to a string instead of ``True`` to customize
+ the message.
+ :param type: the type to use to check the value against.
+ :param value_proc: if this parameter is provided it's a function that
+ is invoked instead of the type conversion to
+ convert a value.
+ :param prompt_suffix: a suffix that should be added to the prompt.
+ :param show_default: shows or hides the default value in the prompt.
+ If this value is a string, it shows that string
+ in parentheses instead of the actual value.
+ :param err: if set to true the file defaults to ``stderr`` instead of
+ ``stdout``, the same as with echo.
+ :param show_choices: Show or hide choices if the passed type is a Choice.
+ For example if type is a Choice of either day or week,
+ show_choices is true and text is "Group by" then the
+ prompt will be "Group by (day, week): ".
+
+ .. versionchanged:: 8.3.3
+ ``show_default`` can be a string to show a custom value instead
+ of the actual default, matching the help text behavior.
+
+ .. versionchanged:: 8.3.1
+ A space is no longer appended to the prompt.
+
+ .. versionadded:: 8.0
+ ``confirmation_prompt`` can be a custom string.
+
+ .. versionadded:: 7.0
+ Added the ``show_choices`` parameter.
+
+ .. versionadded:: 6.0
+ Added unicode support for cmd.exe on Windows.
+
+ .. versionadded:: 4.0
+ Added the `err` parameter.
+
+ """
+
+ def prompt_func(text: str) -> str:
+ f = hidden_prompt_func if hide_input else visible_prompt_func
+ try:
+ # Write the prompt separately so that we get nice
+ # coloring through colorama on Windows
+ echo(text[:-1], nl=False, err=err)
+ # Echo the last character to stdout to work around an issue where
+ # readline causes backspace to clear the whole line.
+ return f(text[-1:])
+ except (KeyboardInterrupt, EOFError):
+ # getpass doesn't print a newline if the user aborts input with ^C.
+ # Allegedly this behavior is inherited from getpass(3).
+ # A doc bug has been filed at https://bugs.python.org/issue24711
+ if hide_input:
+ echo(None, err=err)
+ raise Abort() from None
+
+ if value_proc is None:
+ value_proc = convert_type(type, default)
+
+ prompt = _build_prompt(
+ text, prompt_suffix, show_default, default, show_choices, type
+ )
+
+ if confirmation_prompt:
+ if confirmation_prompt is True:
+ confirmation_prompt = _("Repeat for confirmation")
+
+ confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix)
+
+ while True:
+ while True:
+ value = prompt_func(prompt)
+ if value:
+ break
+ elif default is not None:
+ value = default
+ break
+ try:
+ result = value_proc(value)
+ except UsageError as e:
+ if hide_input:
+ echo(_("Error: The value you entered was invalid."), err=err)
+ else:
+ echo(_("Error: {e.message}").format(e=e), err=err)
+ continue
+ if not confirmation_prompt:
+ return result
+ while True:
+ value2 = prompt_func(confirmation_prompt)
+ is_empty = not value and not value2
+ if value2 or is_empty:
+ break
+ if value == value2:
+ return result
+ echo(_("Error: The two entered values do not match."), err=err)
+
+
+def confirm(
+ text: str,
+ default: bool | None = False,
+ abort: bool = False,
+ prompt_suffix: str = ": ",
+ show_default: bool = True,
+ err: bool = False,
+) -> bool:
+ """Prompts for confirmation (yes/no question).
+
+ If the user aborts the input by sending a interrupt signal this
+ function will catch it and raise a :exc:`Abort` exception.
+
+ :param text: the question to ask.
+ :param default: The default value to use when no input is given. If
+ ``None``, repeat until input is given.
+ :param abort: if this is set to `True` a negative answer aborts the
+ exception by raising :exc:`Abort`.
+ :param prompt_suffix: a suffix that should be added to the prompt.
+ :param show_default: shows or hides the default value in the prompt.
+ :param err: if set to true the file defaults to ``stderr`` instead of
+ ``stdout``, the same as with echo.
+
+ .. versionchanged:: 8.3.1
+ A space is no longer appended to the prompt.
+
+ .. versionchanged:: 8.0
+ Repeat until input is given if ``default`` is ``None``.
+
+ .. versionadded:: 4.0
+ Added the ``err`` parameter.
+ """
+ prompt = _build_prompt(
+ text,
+ prompt_suffix,
+ show_default,
+ "y/n" if default is None else ("Y/n" if default else "y/N"),
+ )
+
+ while True:
+ try:
+ # Write the prompt separately so that we get nice
+ # coloring through colorama on Windows
+ echo(prompt[:-1], nl=False, err=err)
+ # Echo the last character to stdout to work around an issue where
+ # readline causes backspace to clear the whole line.
+ value = visible_prompt_func(prompt[-1:]).lower().strip()
+ except (KeyboardInterrupt, EOFError):
+ raise Abort() from None
+ if value in ("y", "yes"):
+ rv = True
+ elif value in ("n", "no"):
+ rv = False
+ elif default is not None and value == "":
+ rv = default
+ else:
+ echo(_("Error: invalid input"), err=err)
+ continue
+ break
+ if abort and not rv:
+ raise Abort()
+ return rv
+
+
+def echo_via_pager(
+ text_or_generator: cabc.Iterable[str] | t.Callable[[], cabc.Iterable[str]] | str,
+ color: bool | None = None,
+) -> None:
+ """This function takes a text and shows it via an environment specific
+ pager on stdout.
+
+ .. versionchanged:: 3.0
+ Added the `color` flag.
+
+ :param text_or_generator: the text to page, or alternatively, a
+ generator emitting the text to page.
+ :param color: controls if the pager supports ANSI colors or not. The
+ default is autodetection.
+ """
+ color = resolve_color_default(color)
+
+ if inspect.isgeneratorfunction(text_or_generator):
+ i = t.cast("t.Callable[[], cabc.Iterable[str]]", text_or_generator)()
+ elif isinstance(text_or_generator, str):
+ i = [text_or_generator]
+ else:
+ i = iter(t.cast("cabc.Iterable[str]", text_or_generator))
+
+ # convert every element of i to a text type if necessary
+ text_generator = (el if isinstance(el, str) else str(el) for el in i)
+
+ from ._termui_impl import pager
+
+ return pager(itertools.chain(text_generator, "\n"), color)
+
+
+@t.overload
+def progressbar(
+ *,
+ length: int,
+ label: str | None = None,
+ hidden: bool = False,
+ show_eta: bool = True,
+ show_percent: bool | None = None,
+ show_pos: bool = False,
+ fill_char: str = "#",
+ empty_char: str = "-",
+ bar_template: str = "%(label)s [%(bar)s] %(info)s",
+ info_sep: str = " ",
+ width: int = 36,
+ file: t.TextIO | None = None,
+ color: bool | None = None,
+ update_min_steps: int = 1,
+) -> ProgressBar[int]: ...
+
+
+@t.overload
+def progressbar(
+ iterable: cabc.Iterable[V] | None = None,
+ length: int | None = None,
+ label: str | None = None,
+ hidden: bool = False,
+ show_eta: bool = True,
+ show_percent: bool | None = None,
+ show_pos: bool = False,
+ item_show_func: t.Callable[[V | None], str | None] | None = None,
+ fill_char: str = "#",
+ empty_char: str = "-",
+ bar_template: str = "%(label)s [%(bar)s] %(info)s",
+ info_sep: str = " ",
+ width: int = 36,
+ file: t.TextIO | None = None,
+ color: bool | None = None,
+ update_min_steps: int = 1,
+) -> ProgressBar[V]: ...
+
+
+def progressbar(
+ iterable: cabc.Iterable[V] | None = None,
+ length: int | None = None,
+ label: str | None = None,
+ hidden: bool = False,
+ show_eta: bool = True,
+ show_percent: bool | None = None,
+ show_pos: bool = False,
+ item_show_func: t.Callable[[V | None], str | None] | None = None,
+ fill_char: str = "#",
+ empty_char: str = "-",
+ bar_template: str = "%(label)s [%(bar)s] %(info)s",
+ info_sep: str = " ",
+ width: int = 36,
+ file: t.TextIO | None = None,
+ color: bool | None = None,
+ update_min_steps: int = 1,
+) -> ProgressBar[V]:
+ """This function creates an iterable context manager that can be used
+ to iterate over something while showing a progress bar. It will
+ either iterate over the `iterable` or `length` items (that are counted
+ up). While iteration happens, this function will print a rendered
+ progress bar to the given `file` (defaults to stdout) and will attempt
+ to calculate remaining time and more. By default, this progress bar
+ will not be rendered if the file is not a terminal.
+
+ The context manager creates the progress bar. When the context
+ manager is entered the progress bar is already created. With every
+ iteration over the progress bar, the iterable passed to the bar is
+ advanced and the bar is updated. When the context manager exits,
+ a newline is printed and the progress bar is finalized on screen.
+
+ Note: The progress bar is currently designed for use cases where the
+ total progress can be expected to take at least several seconds.
+ Because of this, the ProgressBar class object won't display
+ progress that is considered too fast, and progress where the time
+ between steps is less than a second.
+
+ No printing must happen or the progress bar will be unintentionally
+ destroyed.
+
+ Example usage::
+
+ with progressbar(items) as bar:
+ for item in bar:
+ do_something_with(item)
+
+ Alternatively, if no iterable is specified, one can manually update the
+ progress bar through the `update()` method instead of directly
+ iterating over the progress bar. The update method accepts the number
+ of steps to increment the bar with::
+
+ with progressbar(length=chunks.total_bytes) as bar:
+ for chunk in chunks:
+ process_chunk(chunk)
+ bar.update(chunks.bytes)
+
+ The ``update()`` method also takes an optional value specifying the
+ ``current_item`` at the new position. This is useful when used
+ together with ``item_show_func`` to customize the output for each
+ manual step::
+
+ with click.progressbar(
+ length=total_size,
+ label='Unzipping archive',
+ item_show_func=lambda a: a.filename
+ ) as bar:
+ for archive in zip_file:
+ archive.extract()
+ bar.update(archive.size, archive)
+
+ :param iterable: an iterable to iterate over. If not provided the length
+ is required.
+ :param length: the number of items to iterate over. By default the
+ progressbar will attempt to ask the iterator about its
+ length, which might or might not work. If an iterable is
+ also provided this parameter can be used to override the
+ length. If an iterable is not provided the progress bar
+ will iterate over a range of that length.
+ :param label: the label to show next to the progress bar.
+ :param hidden: hide the progressbar. Defaults to ``False``. When no tty is
+ detected, it will only print the progressbar label. Setting this to
+ ``False`` also disables that.
+ :param show_eta: enables or disables the estimated time display. This is
+ automatically disabled if the length cannot be
+ determined.
+ :param show_percent: enables or disables the percentage display. The
+ default is `True` if the iterable has a length or
+ `False` if not.
+ :param show_pos: enables or disables the absolute position display. The
+ default is `False`.
+ :param item_show_func: A function called with the current item which
+ can return a string to show next to the progress bar. If the
+ function returns ``None`` nothing is shown. The current item can
+ be ``None``, such as when entering and exiting the bar.
+ :param fill_char: the character to use to show the filled part of the
+ progress bar.
+ :param empty_char: the character to use to show the non-filled part of
+ the progress bar.
+ :param bar_template: the format string to use as template for the bar.
+ The parameters in it are ``label`` for the label,
+ ``bar`` for the progress bar and ``info`` for the
+ info section.
+ :param info_sep: the separator between multiple info items (eta etc.)
+ :param width: the width of the progress bar in characters, 0 means full
+ terminal width
+ :param file: The file to write to. If this is not a terminal then
+ only the label is printed.
+ :param color: controls if the terminal supports ANSI colors or not. The
+ default is autodetection. This is only needed if ANSI
+ codes are included anywhere in the progress bar output
+ which is not the case by default.
+ :param update_min_steps: Render only when this many updates have
+ completed. This allows tuning for very fast iterators.
+
+ .. versionadded:: 8.2
+ The ``hidden`` argument.
+
+ .. versionchanged:: 8.0
+ Output is shown even if execution time is less than 0.5 seconds.
+
+ .. versionchanged:: 8.0
+ ``item_show_func`` shows the current item, not the previous one.
+
+ .. versionchanged:: 8.0
+ Labels are echoed if the output is not a TTY. Reverts a change
+ in 7.0 that removed all output.
+
+ .. versionadded:: 8.0
+ The ``update_min_steps`` parameter.
+
+ .. versionadded:: 4.0
+ The ``color`` parameter and ``update`` method.
+
+ .. versionadded:: 2.0
+ """
+ from ._termui_impl import ProgressBar
+
+ color = resolve_color_default(color)
+ return ProgressBar(
+ iterable=iterable,
+ length=length,
+ hidden=hidden,
+ show_eta=show_eta,
+ show_percent=show_percent,
+ show_pos=show_pos,
+ item_show_func=item_show_func,
+ fill_char=fill_char,
+ empty_char=empty_char,
+ bar_template=bar_template,
+ info_sep=info_sep,
+ file=file,
+ label=label,
+ width=width,
+ color=color,
+ update_min_steps=update_min_steps,
+ )
+
+
+def clear() -> None:
+ """Clears the terminal screen. This will have the effect of clearing
+ the whole visible space of the terminal and moving the cursor to the
+ top left. This does not do anything if not connected to a terminal.
+
+ .. versionadded:: 2.0
+ """
+ if not isatty(sys.stdout):
+ return
+
+ # ANSI escape \033[2J clears the screen, \033[1;1H moves the cursor
+ echo("\033[2J\033[1;1H", nl=False)
+
+
+def _interpret_color(color: int | tuple[int, int, int] | str, offset: int = 0) -> str:
+ if isinstance(color, int):
+ return f"{38 + offset};5;{color:d}"
+
+ if isinstance(color, (tuple, list)):
+ r, g, b = color
+ return f"{38 + offset};2;{r:d};{g:d};{b:d}"
+
+ return str(_ansi_colors[color] + offset)
+
+
+def style(
+ text: t.Any,
+ fg: int | tuple[int, int, int] | str | None = None,
+ bg: int | tuple[int, int, int] | str | None = None,
+ bold: bool | None = None,
+ dim: bool | None = None,
+ underline: bool | None = None,
+ overline: bool | None = None,
+ italic: bool | None = None,
+ blink: bool | None = None,
+ reverse: bool | None = None,
+ strikethrough: bool | None = None,
+ reset: bool = True,
+) -> str:
+ """Styles a text with ANSI styles and returns the new string. By
+ default the styling is self contained which means that at the end
+ of the string a reset code is issued. This can be prevented by
+ passing ``reset=False``.
+
+ Examples::
+
+ click.echo(click.style('Hello World!', fg='green'))
+ click.echo(click.style('ATTENTION!', blink=True))
+ click.echo(click.style('Some things', reverse=True, fg='cyan'))
+ click.echo(click.style('More colors', fg=(255, 12, 128), bg=117))
+
+ Supported color names:
+
+ * ``black`` (might be a gray)
+ * ``red``
+ * ``green``
+ * ``yellow`` (might be an orange)
+ * ``blue``
+ * ``magenta``
+ * ``cyan``
+ * ``white`` (might be light gray)
+ * ``bright_black``
+ * ``bright_red``
+ * ``bright_green``
+ * ``bright_yellow``
+ * ``bright_blue``
+ * ``bright_magenta``
+ * ``bright_cyan``
+ * ``bright_white``
+ * ``reset`` (reset the color code only)
+
+ If the terminal supports it, color may also be specified as:
+
+ - An integer in the interval [0, 255]. The terminal must support
+ 8-bit/256-color mode.
+ - An RGB tuple of three integers in [0, 255]. The terminal must
+ support 24-bit/true-color mode.
+
+ See https://en.wikipedia.org/wiki/ANSI_color and
+ https://gist.github.com/XVilka/8346728 for more information.
+
+ :param text: the string to style with ansi codes.
+ :param fg: if provided this will become the foreground color.
+ :param bg: if provided this will become the background color.
+ :param bold: if provided this will enable or disable bold mode.
+ :param dim: if provided this will enable or disable dim mode. This is
+ badly supported.
+ :param underline: if provided this will enable or disable underline.
+ :param overline: if provided this will enable or disable overline.
+ :param italic: if provided this will enable or disable italic.
+ :param blink: if provided this will enable or disable blinking.
+ :param reverse: if provided this will enable or disable inverse
+ rendering (foreground becomes background and the
+ other way round).
+ :param strikethrough: if provided this will enable or disable
+ striking through text.
+ :param reset: by default a reset-all code is added at the end of the
+ string which means that styles do not carry over. This
+ can be disabled to compose styles.
+
+ .. versionchanged:: 8.0
+ A non-string ``message`` is converted to a string.
+
+ .. versionchanged:: 8.0
+ Added support for 256 and RGB color codes.
+
+ .. versionchanged:: 8.0
+ Added the ``strikethrough``, ``italic``, and ``overline``
+ parameters.
+
+ .. versionchanged:: 7.0
+ Added support for bright colors.
+
+ .. versionadded:: 2.0
+ """
+ if not isinstance(text, str):
+ text = str(text)
+
+ bits = []
+
+ if fg:
+ try:
+ bits.append(f"\033[{_interpret_color(fg)}m")
+ except KeyError:
+ raise TypeError(f"Unknown color {fg!r}") from None
+
+ if bg:
+ try:
+ bits.append(f"\033[{_interpret_color(bg, 10)}m")
+ except KeyError:
+ raise TypeError(f"Unknown color {bg!r}") from None
+
+ if bold is not None:
+ bits.append(f"\033[{1 if bold else 22}m")
+ if dim is not None:
+ bits.append(f"\033[{2 if dim else 22}m")
+ if underline is not None:
+ bits.append(f"\033[{4 if underline else 24}m")
+ if overline is not None:
+ bits.append(f"\033[{53 if overline else 55}m")
+ if italic is not None:
+ bits.append(f"\033[{3 if italic else 23}m")
+ if blink is not None:
+ bits.append(f"\033[{5 if blink else 25}m")
+ if reverse is not None:
+ bits.append(f"\033[{7 if reverse else 27}m")
+ if strikethrough is not None:
+ bits.append(f"\033[{9 if strikethrough else 29}m")
+ bits.append(text)
+ if reset:
+ bits.append(_ansi_reset_all)
+ return "".join(bits)
+
+
+def unstyle(text: str) -> str:
+ """Removes ANSI styling information from a string. Usually it's not
+ necessary to use this function as Click's echo function will
+ automatically remove styling if necessary.
+
+ .. versionadded:: 2.0
+
+ :param text: the text to remove style information from.
+ """
+ return strip_ansi(text)
+
+
+def secho(
+ message: t.Any | None = None,
+ file: t.IO[t.AnyStr] | None = None,
+ nl: bool = True,
+ err: bool = False,
+ color: bool | None = None,
+ **styles: t.Any,
+) -> None:
+ """This function combines :func:`echo` and :func:`style` into one
+ call. As such the following two calls are the same::
+
+ click.secho('Hello World!', fg='green')
+ click.echo(click.style('Hello World!', fg='green'))
+
+ All keyword arguments are forwarded to the underlying functions
+ depending on which one they go with.
+
+ Non-string types will be converted to :class:`str`. However,
+ :class:`bytes` are passed directly to :meth:`echo` without applying
+ style. If you want to style bytes that represent text, call
+ :meth:`bytes.decode` first.
+
+ .. versionchanged:: 8.0
+ A non-string ``message`` is converted to a string. Bytes are
+ passed through without style applied.
+
+ .. versionadded:: 2.0
+ """
+ if message is not None and not isinstance(message, (bytes, bytearray)):
+ message = style(message, **styles)
+
+ return echo(message, file=file, nl=nl, err=err, color=color)
+
+
+@t.overload
+def edit(
+ text: bytes | bytearray,
+ editor: str | None = None,
+ env: cabc.Mapping[str, str] | None = None,
+ require_save: bool = False,
+ extension: str = ".txt",
+) -> bytes | None: ...
+
+
+@t.overload
+def edit(
+ text: str,
+ editor: str | None = None,
+ env: cabc.Mapping[str, str] | None = None,
+ require_save: bool = True,
+ extension: str = ".txt",
+) -> str | None: ...
+
+
+@t.overload
+def edit(
+ text: None = None,
+ editor: str | None = None,
+ env: cabc.Mapping[str, str] | None = None,
+ require_save: bool = True,
+ extension: str = ".txt",
+ filename: str | cabc.Iterable[str] | None = None,
+) -> None: ...
+
+
+def edit(
+ text: str | bytes | bytearray | None = None,
+ editor: str | None = None,
+ env: cabc.Mapping[str, str] | None = None,
+ require_save: bool = True,
+ extension: str = ".txt",
+ filename: str | cabc.Iterable[str] | None = None,
+) -> str | bytes | bytearray | None:
+ r"""Edits the given text in the defined editor. If an editor is given
+ (should be the full path to the executable but the regular operating
+ system search path is used for finding the executable) it overrides
+ the detected editor. Optionally, some environment variables can be
+ used. If the editor is closed without changes, `None` is returned. In
+ case a file is edited directly the return value is always `None` and
+ `require_save` and `extension` are ignored.
+
+ If the editor cannot be opened a :exc:`UsageError` is raised.
+
+ Note for Windows: to simplify cross-platform usage, the newlines are
+ automatically converted from POSIX to Windows and vice versa. As such,
+ the message here will have ``\n`` as newline markers.
+
+ :param text: the text to edit.
+ :param editor: optionally the editor to use. Defaults to automatic
+ detection.
+ :param env: environment variables to forward to the editor.
+ :param require_save: if this is true, then not saving in the editor
+ will make the return value become `None`.
+ :param extension: the extension to tell the editor about. This defaults
+ to `.txt` but changing this might change syntax
+ highlighting.
+ :param filename: if provided it will edit this file instead of the
+ provided text contents. It will not use a temporary
+ file as an indirection in that case. If the editor supports
+ editing multiple files at once, a sequence of files may be
+ passed as well. Invoke `click.file` once per file instead
+ if multiple files cannot be managed at once or editing the
+ files serially is desired.
+
+ .. versionchanged:: 8.2.0
+ ``filename`` now accepts any ``Iterable[str]`` in addition to a ``str``
+ if the ``editor`` supports editing multiple files at once.
+
+ """
+ from ._termui_impl import Editor
+
+ ed = Editor(editor=editor, env=env, require_save=require_save, extension=extension)
+
+ if filename is None:
+ return ed.edit(text)
+
+ if isinstance(filename, str):
+ filename = (filename,)
+
+ ed.edit_files(filenames=filename)
+ return None
+
+
+def launch(url: str, wait: bool = False, locate: bool = False) -> int:
+ """This function launches the given URL (or filename) in the default
+ viewer application for this file type. If this is an executable, it
+ might launch the executable in a new session. The return value is
+ the exit code of the launched application. Usually, ``0`` indicates
+ success.
+
+ Examples::
+
+ click.launch('https://click.palletsprojects.com/')
+ click.launch('/my/downloaded/file', locate=True)
+
+ .. versionadded:: 2.0
+
+ :param url: URL or filename of the thing to launch.
+ :param wait: Wait for the program to exit before returning. This
+ only works if the launched program blocks. In particular,
+ ``xdg-open`` on Linux does not block.
+ :param locate: if this is set to `True` then instead of launching the
+ application associated with the URL it will attempt to
+ launch a file manager with the file located. This
+ might have weird effects if the URL does not point to
+ the filesystem.
+ """
+ from ._termui_impl import open_url
+
+ return open_url(url, wait=wait, locate=locate)
+
+
+# If this is provided, getchar() calls into this instead. This is used
+# for unittesting purposes.
+_getchar: t.Callable[[bool], str] | None = None
+
+
+def getchar(echo: bool = False) -> str:
+ """Fetches a single character from the terminal and returns it. This
+ will always return a unicode character and under certain rare
+ circumstances this might return more than one character. The
+ situations which more than one character is returned is when for
+ whatever reason multiple characters end up in the terminal buffer or
+ standard input was not actually a terminal.
+
+ Note that this will always read from the terminal, even if something
+ is piped into the standard input.
+
+ Note for Windows: in rare cases when typing non-ASCII characters, this
+ function might wait for a second character and then return both at once.
+ This is because certain Unicode characters look like special-key markers.
+
+ .. versionadded:: 2.0
+
+ :param echo: if set to `True`, the character read will also show up on
+ the terminal. The default is to not show it.
+ """
+ global _getchar
+
+ if _getchar is None:
+ from ._termui_impl import getchar as f
+
+ _getchar = f
+
+ return _getchar(echo)
+
+
+def raw_terminal() -> AbstractContextManager[int]:
+ from ._termui_impl import raw_terminal as f
+
+ return f()
+
+
+def pause(info: str | None = None, err: bool = False) -> None:
+ """This command stops execution and waits for the user to press any
+ key to continue. This is similar to the Windows batch "pause"
+ command. If the program is not run through a terminal, this command
+ will instead do nothing.
+
+ .. versionadded:: 2.0
+
+ .. versionadded:: 4.0
+ Added the `err` parameter.
+
+ :param info: The message to print before pausing. Defaults to
+ ``"Press any key to continue..."``.
+ :param err: if set to message goes to ``stderr`` instead of
+ ``stdout``, the same as with echo.
+ """
+ if not isatty(sys.stdin) or not isatty(sys.stdout):
+ return
+
+ if info is None:
+ info = _("Press any key to continue...")
+
+ try:
+ if info:
+ echo(info, nl=False, err=err)
+ try:
+ getchar()
+ except (KeyboardInterrupt, EOFError):
+ pass
+ finally:
+ if info:
+ echo(err=err)
diff --git a/venv/Lib/site-packages/click/testing.py b/venv/Lib/site-packages/click/testing.py
new file mode 100644
index 0000000..04e7f1d
--- /dev/null
+++ b/venv/Lib/site-packages/click/testing.py
@@ -0,0 +1,669 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import contextlib
+import io
+import os
+import pdb
+import shlex
+import sys
+import tempfile
+import typing as t
+from types import TracebackType
+
+from . import _compat
+from . import formatting
+from . import termui
+from . import utils
+from ._compat import _find_binary_reader
+
+if t.TYPE_CHECKING:
+ from _typeshed import ReadableBuffer
+
+ from .core import Command
+
+
+class EchoingStdin:
+ def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None:
+ self._input = input
+ self._output = output
+ self._paused = False
+
+ def __getattr__(self, x: str) -> t.Any:
+ return getattr(self._input, x)
+
+ def _echo(self, rv: bytes) -> bytes:
+ if not self._paused:
+ self._output.write(rv)
+
+ return rv
+
+ def read(self, n: int = -1) -> bytes:
+ return self._echo(self._input.read(n))
+
+ def read1(self, n: int = -1) -> bytes:
+ return self._echo(self._input.read1(n)) # type: ignore
+
+ def readline(self, n: int = -1) -> bytes:
+ return self._echo(self._input.readline(n))
+
+ def readlines(self) -> list[bytes]:
+ return [self._echo(x) for x in self._input.readlines()]
+
+ def __iter__(self) -> cabc.Iterator[bytes]:
+ return iter(self._echo(x) for x in self._input)
+
+ def __repr__(self) -> str:
+ return repr(self._input)
+
+
+@contextlib.contextmanager
+def _pause_echo(stream: EchoingStdin | None) -> cabc.Iterator[None]:
+ if stream is None:
+ yield
+ else:
+ stream._paused = True
+ yield
+ stream._paused = False
+
+
+class BytesIOCopy(io.BytesIO):
+ """Patch ``io.BytesIO`` to let the written stream be copied to another.
+
+ .. versionadded:: 8.2
+ """
+
+ def __init__(self, copy_to: io.BytesIO) -> None:
+ super().__init__()
+ self.copy_to = copy_to
+
+ def flush(self) -> None:
+ super().flush()
+ self.copy_to.flush()
+
+ def write(self, b: ReadableBuffer) -> int:
+ self.copy_to.write(b)
+ return super().write(b)
+
+
+class StreamMixer:
+ """Mixes `` and `` streams.
+
+ The result is available in the ``output`` attribute.
+
+ .. versionadded:: 8.2
+ """
+
+ def __init__(self) -> None:
+ self.output: io.BytesIO = io.BytesIO()
+ self.stdout: io.BytesIO = BytesIOCopy(copy_to=self.output)
+ self.stderr: io.BytesIO = BytesIOCopy(copy_to=self.output)
+
+
+class _NamedTextIOWrapper(io.TextIOWrapper):
+ """A :class:`~io.TextIOWrapper` with custom ``name`` and ``mode``
+ that does not close its underlying buffer.
+
+ An optional ``original_fd`` preserves the file descriptor of the
+ stream being replaced, so that C-level consumers that call
+ :meth:`fileno` (``faulthandler``, ``subprocess``, ...) still work.
+ Inspired by pytest's ``capsys``/``capfd`` split: see :doc:`/testing`
+ for details.
+
+ .. versionchanged:: 8.3.3
+ Added ``original_fd`` parameter and :meth:`fileno` override.
+ """
+
+ def __init__(
+ self,
+ buffer: t.BinaryIO,
+ name: str,
+ mode: str,
+ *,
+ original_fd: int = -1,
+ **kwargs: t.Any,
+ ) -> None:
+ super().__init__(buffer, **kwargs)
+ self._name = name
+ self._mode = mode
+ self._original_fd = original_fd
+
+ def close(self) -> None:
+ """The buffer this object contains belongs to some other object,
+ so prevent the default ``__del__`` implementation from closing
+ that buffer.
+
+ .. versionadded:: 8.3.2
+ """
+
+ def fileno(self) -> int:
+ """Return the file descriptor of the original stream, if one was
+ provided at construction time.
+
+ This allows C-level consumers (``faulthandler``, ``subprocess``,
+ signal handlers, ...) to obtain a valid fd without crashing, even
+ though the Python-level writes are redirected to an in-memory
+ buffer.
+
+ .. versionadded:: 8.3.3
+ """
+ if self._original_fd >= 0:
+ return self._original_fd
+ return super().fileno()
+
+ @property
+ def name(self) -> str:
+ return self._name
+
+ @property
+ def mode(self) -> str:
+ return self._mode
+
+
+def make_input_stream(
+ input: str | bytes | t.IO[t.Any] | None, charset: str
+) -> t.BinaryIO:
+ # Is already an input stream.
+ if hasattr(input, "read"):
+ rv = _find_binary_reader(t.cast("t.IO[t.Any]", input))
+
+ if rv is not None:
+ return rv
+
+ raise TypeError("Could not find binary reader for input stream.")
+
+ if input is None:
+ input = b""
+ elif isinstance(input, str):
+ input = input.encode(charset)
+
+ return io.BytesIO(input)
+
+
+class Result:
+ """Holds the captured result of an invoked CLI script.
+
+ :param runner: The runner that created the result
+ :param stdout_bytes: The standard output as bytes.
+ :param stderr_bytes: The standard error as bytes.
+ :param output_bytes: A mix of ``stdout_bytes`` and ``stderr_bytes``, as the
+ user would see it in its terminal.
+ :param return_value: The value returned from the invoked command.
+ :param exit_code: The exit code as integer.
+ :param exception: The exception that happened if one did.
+ :param exc_info: Exception information (exception type, exception instance,
+ traceback type).
+
+ .. versionchanged:: 8.2
+ ``stderr_bytes`` no longer optional, ``output_bytes`` introduced and
+ ``mix_stderr`` has been removed.
+
+ .. versionadded:: 8.0
+ Added ``return_value``.
+ """
+
+ def __init__(
+ self,
+ runner: CliRunner,
+ stdout_bytes: bytes,
+ stderr_bytes: bytes,
+ output_bytes: bytes,
+ return_value: t.Any,
+ exit_code: int,
+ exception: BaseException | None,
+ exc_info: tuple[type[BaseException], BaseException, TracebackType]
+ | None = None,
+ ):
+ self.runner = runner
+ self.stdout_bytes = stdout_bytes
+ self.stderr_bytes = stderr_bytes
+ self.output_bytes = output_bytes
+ self.return_value = return_value
+ self.exit_code = exit_code
+ self.exception = exception
+ self.exc_info = exc_info
+
+ @property
+ def output(self) -> str:
+ """The terminal output as unicode string, as the user would see it.
+
+ .. versionchanged:: 8.2
+ No longer a proxy for ``self.stdout``. Now has its own independent stream
+ that is mixing `` and ``, in the order they were written.
+ """
+ return self.output_bytes.decode(self.runner.charset, "replace").replace(
+ "\r\n", "\n"
+ )
+
+ @property
+ def stdout(self) -> str:
+ """The standard output as unicode string."""
+ return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
+ "\r\n", "\n"
+ )
+
+ @property
+ def stderr(self) -> str:
+ """The standard error as unicode string.
+
+ .. versionchanged:: 8.2
+ No longer raise an exception, always returns the `` string.
+ """
+ return self.stderr_bytes.decode(self.runner.charset, "replace").replace(
+ "\r\n", "\n"
+ )
+
+ def __repr__(self) -> str:
+ exc_str = repr(self.exception) if self.exception else "okay"
+ return f"<{type(self).__name__} {exc_str}>"
+
+
+class CliRunner:
+ """The CLI runner provides functionality to invoke a Click command line
+ script for unittesting purposes in a isolated environment. This only
+ works in single-threaded systems without any concurrency as it changes the
+ global interpreter state.
+
+ :param charset: the character set for the input and output data.
+ :param env: a dictionary with environment variables for overriding.
+ :param echo_stdin: if this is set to `True`, then reading from `` writes
+ to ``. This is useful for showing examples in
+ some circumstances. Note that regular prompts
+ will automatically echo the input.
+ :param catch_exceptions: Whether to catch any exceptions other than
+ ``SystemExit`` when running :meth:`~CliRunner.invoke`.
+
+ .. versionchanged:: 8.2
+ Added the ``catch_exceptions`` parameter.
+
+ .. versionchanged:: 8.2
+ ``mix_stderr`` parameter has been removed.
+ """
+
+ def __init__(
+ self,
+ charset: str = "utf-8",
+ env: cabc.Mapping[str, str | None] | None = None,
+ echo_stdin: bool = False,
+ catch_exceptions: bool = True,
+ ) -> None:
+ self.charset = charset
+ self.env: cabc.Mapping[str, str | None] = env or {}
+ self.echo_stdin = echo_stdin
+ self.catch_exceptions = catch_exceptions
+
+ def get_default_prog_name(self, cli: Command) -> str:
+ """Given a command object it will return the default program name
+ for it. The default is the `name` attribute or ``"root"`` if not
+ set.
+ """
+ return cli.name or "root"
+
+ def make_env(
+ self, overrides: cabc.Mapping[str, str | None] | None = None
+ ) -> cabc.Mapping[str, str | None]:
+ """Returns the environment overrides for invoking a script."""
+ rv = dict(self.env)
+ if overrides:
+ rv.update(overrides)
+ return rv
+
+ @contextlib.contextmanager
+ def isolation(
+ self,
+ input: str | bytes | t.IO[t.Any] | None = None,
+ env: cabc.Mapping[str, str | None] | None = None,
+ color: bool = False,
+ ) -> cabc.Iterator[tuple[io.BytesIO, io.BytesIO, io.BytesIO]]:
+ """A context manager that sets up the isolation for invoking of a
+ command line tool. This sets up `` with the given input data
+ and `os.environ` with the overrides from the given dictionary.
+ This also rebinds some internals in Click to be mocked (like the
+ prompt functionality).
+
+ This is automatically done in the :meth:`invoke` method.
+
+ :param input: the input stream to put into `sys.stdin`.
+ :param env: the environment overrides as dictionary.
+ :param color: whether the output should contain color codes. The
+ application can still override this explicitly.
+
+ .. versionadded:: 8.2
+ An additional output stream is returned, which is a mix of
+ `` and `` streams.
+
+ .. versionchanged:: 8.2
+ Always returns the `` stream.
+
+ .. versionchanged:: 8.0
+ `` is opened with ``errors="backslashreplace"``
+ instead of the default ``"strict"``.
+
+ .. versionchanged:: 4.0
+ Added the ``color`` parameter.
+ """
+ bytes_input = make_input_stream(input, self.charset)
+ echo_input = None
+
+ old_stdin = sys.stdin
+ old_stdout = sys.stdout
+ old_stderr = sys.stderr
+ old_forced_width = formatting.FORCED_WIDTH
+ formatting.FORCED_WIDTH = 80
+
+ env = self.make_env(env)
+
+ stream_mixer = StreamMixer()
+
+ # Preserve the original file descriptors so that C-level
+ # consumers (faulthandler, subprocess, etc.) can still obtain a
+ # valid fd from the redirected streams. The original streams
+ # may themselves lack a fileno() (e.g. when CliRunner is used
+ # inside pytest's capsys), so we fall back to -1.
+ def _safe_fileno(stream: t.IO[t.Any]) -> int:
+ try:
+ return stream.fileno()
+ except (AttributeError, io.UnsupportedOperation):
+ return -1
+
+ old_stdout_fd = _safe_fileno(old_stdout)
+ old_stderr_fd = _safe_fileno(old_stderr)
+
+ if self.echo_stdin:
+ bytes_input = echo_input = t.cast(
+ t.BinaryIO, EchoingStdin(bytes_input, stream_mixer.stdout)
+ )
+
+ sys.stdin = text_input = _NamedTextIOWrapper(
+ bytes_input, encoding=self.charset, name="", mode="r"
+ )
+
+ if self.echo_stdin:
+ # Force unbuffered reads, otherwise TextIOWrapper reads a
+ # large chunk which is echoed early.
+ text_input._CHUNK_SIZE = 1 # type: ignore
+
+ sys.stdout = _NamedTextIOWrapper(
+ stream_mixer.stdout,
+ encoding=self.charset,
+ name="",
+ mode="w",
+ original_fd=old_stdout_fd,
+ )
+
+ sys.stderr = _NamedTextIOWrapper(
+ stream_mixer.stderr,
+ encoding=self.charset,
+ name="",
+ mode="w",
+ errors="backslashreplace",
+ original_fd=old_stderr_fd,
+ )
+
+ @_pause_echo(echo_input) # type: ignore
+ def visible_input(prompt: str | None = None) -> str:
+ sys.stdout.write(prompt or "")
+ try:
+ val = next(text_input).rstrip("\r\n")
+ except StopIteration as e:
+ raise EOFError() from e
+ sys.stdout.write(f"{val}\n")
+ sys.stdout.flush()
+ return val
+
+ @_pause_echo(echo_input) # type: ignore
+ def hidden_input(prompt: str | None = None) -> str:
+ sys.stdout.write(f"{prompt or ''}\n")
+ sys.stdout.flush()
+ try:
+ return next(text_input).rstrip("\r\n")
+ except StopIteration as e:
+ raise EOFError() from e
+
+ @_pause_echo(echo_input) # type: ignore
+ def _getchar(echo: bool) -> str:
+ char = sys.stdin.read(1)
+
+ if echo:
+ sys.stdout.write(char)
+
+ sys.stdout.flush()
+ return char
+
+ default_color = color
+
+ def should_strip_ansi(
+ stream: t.IO[t.Any] | None = None, color: bool | None = None
+ ) -> bool:
+ if color is None:
+ return not default_color
+ return not color
+
+ old_visible_prompt_func = termui.visible_prompt_func
+ old_hidden_prompt_func = termui.hidden_prompt_func
+ old__getchar_func = termui._getchar
+ old_should_strip_ansi = utils.should_strip_ansi # type: ignore
+ old__compat_should_strip_ansi = _compat.should_strip_ansi
+ old_pdb_init = pdb.Pdb.__init__
+ termui.visible_prompt_func = visible_input
+ termui.hidden_prompt_func = hidden_input
+ termui._getchar = _getchar
+ utils.should_strip_ansi = should_strip_ansi # type: ignore
+ _compat.should_strip_ansi = should_strip_ansi
+
+ def _patched_pdb_init(
+ self: pdb.Pdb,
+ completekey: str = "tab",
+ stdin: t.IO[str] | None = None,
+ stdout: t.IO[str] | None = None,
+ **kwargs: t.Any,
+ ) -> None:
+ """Default ``pdb.Pdb`` to real terminal streams during
+ ``CliRunner`` isolation.
+
+ Without this patch, ``pdb.Pdb.__init__`` inherits from
+ ``cmd.Cmd`` which falls back to ``sys.stdin``/``sys.stdout``
+ when no explicit streams are provided. During isolation
+ those are ``BytesIO``-backed wrappers, so the debugger
+ reads from an empty buffer and writes to captured output,
+ making interactive debugging impossible.
+
+ By defaulting to ``sys.__stdin__``/``sys.__stdout__`` (the
+ original terminal streams Python preserves regardless of
+ redirection), debuggers can interact with the user while
+ ``click.echo`` output is still captured normally.
+
+ This covers ``pdb.set_trace()``, ``breakpoint()``,
+ ``pdb.post_mortem()``, and debuggers that subclass
+ ``pdb.Pdb`` (ipdb, pdbpp). Explicit ``stdin``/``stdout``
+ arguments are honored and not overridden. Debuggers that
+ do not subclass ``pdb.Pdb`` (pudb, debugpy) are not
+ covered.
+ """
+ if stdin is None:
+ stdin = sys.__stdin__
+ if stdout is None:
+ stdout = sys.__stdout__
+ old_pdb_init(
+ self, completekey=completekey, stdin=stdin, stdout=stdout, **kwargs
+ )
+
+ pdb.Pdb.__init__ = _patched_pdb_init # type: ignore[assignment]
+
+ old_env = {}
+ try:
+ for key, value in env.items():
+ old_env[key] = os.environ.get(key)
+ if value is None:
+ try:
+ del os.environ[key]
+ except Exception:
+ pass
+ else:
+ os.environ[key] = value
+ yield (stream_mixer.stdout, stream_mixer.stderr, stream_mixer.output)
+ finally:
+ for key, value in old_env.items():
+ if value is None:
+ try:
+ del os.environ[key]
+ except Exception:
+ pass
+ else:
+ os.environ[key] = value
+ sys.stdout = old_stdout
+ sys.stderr = old_stderr
+ sys.stdin = old_stdin
+ termui.visible_prompt_func = old_visible_prompt_func
+ termui.hidden_prompt_func = old_hidden_prompt_func
+ termui._getchar = old__getchar_func
+ utils.should_strip_ansi = old_should_strip_ansi # type: ignore
+ _compat.should_strip_ansi = old__compat_should_strip_ansi
+ formatting.FORCED_WIDTH = old_forced_width
+ pdb.Pdb.__init__ = old_pdb_init # type: ignore[method-assign]
+
+ def invoke(
+ self,
+ cli: Command,
+ args: str | cabc.Sequence[str] | None = None,
+ input: str | bytes | t.IO[t.Any] | None = None,
+ env: cabc.Mapping[str, str | None] | None = None,
+ catch_exceptions: bool | None = None,
+ color: bool = False,
+ **extra: t.Any,
+ ) -> Result:
+ """Invokes a command in an isolated environment. The arguments are
+ forwarded directly to the command line script, the `extra` keyword
+ arguments are passed to the :meth:`~clickpkg.Command.main` function of
+ the command.
+
+ This returns a :class:`Result` object.
+
+ :param cli: the command to invoke
+ :param args: the arguments to invoke. It may be given as an iterable
+ or a string. When given as string it will be interpreted
+ as a Unix shell command. More details at
+ :func:`shlex.split`.
+ :param input: the input data for `sys.stdin`.
+ :param env: the environment overrides.
+ :param catch_exceptions: Whether to catch any other exceptions than
+ ``SystemExit``. If :data:`None`, the value
+ from :class:`CliRunner` is used.
+ :param extra: the keyword arguments to pass to :meth:`main`.
+ :param color: whether the output should contain color codes. The
+ application can still override this explicitly.
+
+ .. versionadded:: 8.2
+ The result object has the ``output_bytes`` attribute with
+ the mix of ``stdout_bytes`` and ``stderr_bytes``, as the user would
+ see it in its terminal.
+
+ .. versionchanged:: 8.2
+ The result object always returns the ``stderr_bytes`` stream.
+
+ .. versionchanged:: 8.0
+ The result object has the ``return_value`` attribute with
+ the value returned from the invoked command.
+
+ .. versionchanged:: 4.0
+ Added the ``color`` parameter.
+
+ .. versionchanged:: 3.0
+ Added the ``catch_exceptions`` parameter.
+
+ .. versionchanged:: 3.0
+ The result object has the ``exc_info`` attribute with the
+ traceback if available.
+ """
+ exc_info = None
+ if catch_exceptions is None:
+ catch_exceptions = self.catch_exceptions
+
+ with self.isolation(input=input, env=env, color=color) as outstreams:
+ return_value = None
+ exception: BaseException | None = None
+ exit_code = 0
+
+ if isinstance(args, str):
+ args = shlex.split(args)
+
+ try:
+ prog_name = extra.pop("prog_name")
+ except KeyError:
+ prog_name = self.get_default_prog_name(cli)
+
+ try:
+ return_value = cli.main(args=args or (), prog_name=prog_name, **extra)
+ except SystemExit as e:
+ exc_info = sys.exc_info()
+ e_code = t.cast("int | t.Any | None", e.code)
+
+ if e_code is None:
+ e_code = 0
+
+ if e_code != 0:
+ exception = e
+
+ if not isinstance(e_code, int):
+ sys.stdout.write(str(e_code))
+ sys.stdout.write("\n")
+ e_code = 1
+
+ exit_code = e_code
+
+ except Exception as e:
+ if not catch_exceptions:
+ raise
+ exception = e
+ exit_code = 1
+ exc_info = sys.exc_info()
+ finally:
+ sys.stdout.flush()
+ sys.stderr.flush()
+ stdout = outstreams[0].getvalue()
+ stderr = outstreams[1].getvalue()
+ output = outstreams[2].getvalue()
+
+ return Result(
+ runner=self,
+ stdout_bytes=stdout,
+ stderr_bytes=stderr,
+ output_bytes=output,
+ return_value=return_value,
+ exit_code=exit_code,
+ exception=exception,
+ exc_info=exc_info, # type: ignore
+ )
+
+ @contextlib.contextmanager
+ def isolated_filesystem(
+ self, temp_dir: str | os.PathLike[str] | None = None
+ ) -> cabc.Iterator[str]:
+ """A context manager that creates a temporary directory and
+ changes the current working directory to it. This isolates tests
+ that affect the contents of the CWD to prevent them from
+ interfering with each other.
+
+ :param temp_dir: Create the temporary directory under this
+ directory. If given, the created directory is not removed
+ when exiting.
+
+ .. versionchanged:: 8.0
+ Added the ``temp_dir`` parameter.
+ """
+ cwd = os.getcwd()
+ dt = tempfile.mkdtemp(dir=temp_dir)
+ os.chdir(dt)
+
+ try:
+ yield dt
+ finally:
+ os.chdir(cwd)
+
+ if temp_dir is None:
+ import shutil
+
+ try:
+ shutil.rmtree(dt)
+ except OSError:
+ pass
diff --git a/venv/Lib/site-packages/click/types.py b/venv/Lib/site-packages/click/types.py
new file mode 100644
index 0000000..e71c1c2
--- /dev/null
+++ b/venv/Lib/site-packages/click/types.py
@@ -0,0 +1,1209 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import enum
+import os
+import stat
+import sys
+import typing as t
+from datetime import datetime
+from gettext import gettext as _
+from gettext import ngettext
+
+from ._compat import _get_argv_encoding
+from ._compat import open_stream
+from .exceptions import BadParameter
+from .utils import format_filename
+from .utils import LazyFile
+from .utils import safecall
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+ from .core import Context
+ from .core import Parameter
+ from .shell_completion import CompletionItem
+
+ParamTypeValue = t.TypeVar("ParamTypeValue")
+
+
+class ParamType:
+ """Represents the type of a parameter. Validates and converts values
+ from the command line or Python into the correct type.
+
+ To implement a custom type, subclass and implement at least the
+ following:
+
+ - The :attr:`name` class attribute must be set.
+ - Calling an instance of the type with ``None`` must return
+ ``None``. This is already implemented by default.
+ - :meth:`convert` must convert string values to the correct type.
+ - :meth:`convert` must accept values that are already the correct
+ type.
+ - It must be able to convert a value if the ``ctx`` and ``param``
+ arguments are ``None``. This can occur when converting prompt
+ input.
+ """
+
+ is_composite: t.ClassVar[bool] = False
+ arity: t.ClassVar[int] = 1
+
+ #: the descriptive name of this type
+ name: str
+
+ #: if a list of this type is expected and the value is pulled from a
+ #: string environment variable, this is what splits it up. `None`
+ #: means any whitespace. For all parameters the general rule is that
+ #: whitespace splits them up. The exception are paths and files which
+ #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on
+ #: Windows).
+ envvar_list_splitter: t.ClassVar[str | None] = None
+
+ def to_info_dict(self) -> dict[str, t.Any]:
+ """Gather information that could be useful for a tool generating
+ user-facing documentation.
+
+ Use :meth:`click.Context.to_info_dict` to traverse the entire
+ CLI structure.
+
+ .. versionadded:: 8.0
+ """
+ # The class name without the "ParamType" suffix.
+ param_type = type(self).__name__.partition("ParamType")[0]
+ param_type = param_type.partition("ParameterType")[0]
+
+ # Custom subclasses might not remember to set a name.
+ if hasattr(self, "name"):
+ name = self.name
+ else:
+ name = param_type
+
+ return {"param_type": param_type, "name": name}
+
+ def __call__(
+ self,
+ value: t.Any,
+ param: Parameter | None = None,
+ ctx: Context | None = None,
+ ) -> t.Any:
+ if value is not None:
+ return self.convert(value, param, ctx)
+
+ def get_metavar(self, param: Parameter, ctx: Context) -> str | None:
+ """Returns the metavar default for this param if it provides one."""
+
+ def get_missing_message(self, param: Parameter, ctx: Context | None) -> str | None:
+ """Optionally might return extra information about a missing
+ parameter.
+
+ .. versionadded:: 2.0
+ """
+
+ def convert(
+ self, value: t.Any, param: Parameter | None, ctx: Context | None
+ ) -> t.Any:
+ """Convert the value to the correct type. This is not called if
+ the value is ``None`` (the missing value).
+
+ This must accept string values from the command line, as well as
+ values that are already the correct type. It may also convert
+ other compatible types.
+
+ The ``param`` and ``ctx`` arguments may be ``None`` in certain
+ situations, such as when converting prompt input.
+
+ If the value cannot be converted, call :meth:`fail` with a
+ descriptive message.
+
+ :param value: The value to convert.
+ :param param: The parameter that is using this type to convert
+ its value. May be ``None``.
+ :param ctx: The current context that arrived at this value. May
+ be ``None``.
+ """
+ return value
+
+ def split_envvar_value(self, rv: str) -> cabc.Sequence[str]:
+ """Given a value from an environment variable this splits it up
+ into small chunks depending on the defined envvar list splitter.
+
+ If the splitter is set to `None`, which means that whitespace splits,
+ then leading and trailing whitespace is ignored. Otherwise, leading
+ and trailing splitters usually lead to empty items being included.
+ """
+ return (rv or "").split(self.envvar_list_splitter)
+
+ def fail(
+ self,
+ message: str,
+ param: Parameter | None = None,
+ ctx: Context | None = None,
+ ) -> t.NoReturn:
+ """Helper method to fail with an invalid value message."""
+ raise BadParameter(message, ctx=ctx, param=param)
+
+ def shell_complete(
+ self, ctx: Context, param: Parameter, incomplete: str
+ ) -> list[CompletionItem]:
+ """Return a list of
+ :class:`~click.shell_completion.CompletionItem` objects for the
+ incomplete value. Most types do not provide completions, but
+ some do, and this allows custom types to provide custom
+ completions as well.
+
+ :param ctx: Invocation context for this command.
+ :param param: The parameter that is requesting completion.
+ :param incomplete: Value being completed. May be empty.
+
+ .. versionadded:: 8.0
+ """
+ return []
+
+
+class CompositeParamType(ParamType):
+ is_composite = True
+
+ @property
+ def arity(self) -> int: # type: ignore
+ raise NotImplementedError()
+
+
+class FuncParamType(ParamType):
+ def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None:
+ self.name: str = func.__name__
+ self.func = func
+
+ def to_info_dict(self) -> dict[str, t.Any]:
+ info_dict = super().to_info_dict()
+ info_dict["func"] = self.func
+ return info_dict
+
+ def convert(
+ self, value: t.Any, param: Parameter | None, ctx: Context | None
+ ) -> t.Any:
+ try:
+ return self.func(value)
+ except ValueError:
+ try:
+ value = str(value)
+ except UnicodeError:
+ value = value.decode("utf-8", "replace")
+
+ self.fail(value, param, ctx)
+
+
+class UnprocessedParamType(ParamType):
+ name = "text"
+
+ def convert(
+ self, value: t.Any, param: Parameter | None, ctx: Context | None
+ ) -> t.Any:
+ return value
+
+ def __repr__(self) -> str:
+ return "UNPROCESSED"
+
+
+class StringParamType(ParamType):
+ name = "text"
+
+ def convert(
+ self, value: t.Any, param: Parameter | None, ctx: Context | None
+ ) -> t.Any:
+ if isinstance(value, bytes):
+ enc = _get_argv_encoding()
+ try:
+ value = value.decode(enc)
+ except UnicodeError:
+ fs_enc = sys.getfilesystemencoding()
+ if fs_enc != enc:
+ try:
+ value = value.decode(fs_enc)
+ except UnicodeError:
+ value = value.decode("utf-8", "replace")
+ else:
+ value = value.decode("utf-8", "replace")
+ return value
+ return str(value)
+
+ def __repr__(self) -> str:
+ return "STRING"
+
+
+class Choice(ParamType, t.Generic[ParamTypeValue]):
+ """The choice type allows a value to be checked against a fixed set
+ of supported values.
+
+ You may pass any iterable value which will be converted to a tuple
+ and thus will only be iterated once.
+
+ The resulting value will always be one of the originally passed choices.
+ See :meth:`normalize_choice` for more info on the mapping of strings
+ to choices. See :ref:`choice-opts` for an example.
+
+ :param case_sensitive: Set to false to make choices case
+ insensitive. Defaults to true.
+
+ .. versionchanged:: 8.2.0
+ Non-``str`` ``choices`` are now supported. It can additionally be any
+ iterable. Before you were not recommended to pass anything but a list or
+ tuple.
+
+ .. versionadded:: 8.2.0
+ Choice normalization can be overridden via :meth:`normalize_choice`.
+ """
+
+ name = "choice"
+
+ def __init__(
+ self, choices: cabc.Iterable[ParamTypeValue], case_sensitive: bool = True
+ ) -> None:
+ self.choices: cabc.Sequence[ParamTypeValue] = tuple(choices)
+ self.case_sensitive = case_sensitive
+
+ def to_info_dict(self) -> dict[str, t.Any]:
+ info_dict = super().to_info_dict()
+ info_dict["choices"] = self.choices
+ info_dict["case_sensitive"] = self.case_sensitive
+ return info_dict
+
+ def _normalized_mapping(
+ self, ctx: Context | None = None
+ ) -> cabc.Mapping[ParamTypeValue, str]:
+ """
+ Returns mapping where keys are the original choices and the values are
+ the normalized values that are accepted via the command line.
+
+ This is a simple wrapper around :meth:`normalize_choice`, use that
+ instead which is supported.
+ """
+ return {
+ choice: self.normalize_choice(
+ choice=choice,
+ ctx=ctx,
+ )
+ for choice in self.choices
+ }
+
+ def normalize_choice(self, choice: ParamTypeValue, ctx: Context | None) -> str:
+ """
+ Normalize a choice value, used to map a passed string to a choice.
+ Each choice must have a unique normalized value.
+
+ By default uses :meth:`Context.token_normalize_func` and if not case
+ sensitive, convert it to a casefolded value.
+
+ .. versionadded:: 8.2.0
+ """
+ normed_value = choice.name if isinstance(choice, enum.Enum) else str(choice)
+
+ if ctx is not None and ctx.token_normalize_func is not None:
+ normed_value = ctx.token_normalize_func(normed_value)
+
+ if not self.case_sensitive:
+ normed_value = normed_value.casefold()
+
+ return normed_value
+
+ def get_metavar(self, param: Parameter, ctx: Context) -> str | None:
+ if param.param_type_name == "option" and not param.show_choices: # type: ignore
+ choice_metavars = [
+ convert_type(type(choice)).name.upper() for choice in self.choices
+ ]
+ choices_str = "|".join([*dict.fromkeys(choice_metavars)])
+ else:
+ choices_str = "|".join(
+ [str(i) for i in self._normalized_mapping(ctx=ctx).values()]
+ )
+
+ # Use curly braces to indicate a required argument.
+ if param.required and param.param_type_name == "argument":
+ return f"{{{choices_str}}}"
+
+ # Use square braces to indicate an option or optional argument.
+ return f"[{choices_str}]"
+
+ def get_missing_message(self, param: Parameter, ctx: Context | None) -> str:
+ """
+ Message shown when no choice is passed.
+
+ .. versionchanged:: 8.2.0 Added ``ctx`` argument.
+ """
+ return _("Choose from:\n\t{choices}").format(
+ choices=",\n\t".join(self._normalized_mapping(ctx=ctx).values())
+ )
+
+ def convert(
+ self, value: t.Any, param: Parameter | None, ctx: Context | None
+ ) -> ParamTypeValue:
+ """
+ For a given value from the parser, normalize it and find its
+ matching normalized value in the list of choices. Then return the
+ matched "original" choice.
+ """
+ normed_value = self.normalize_choice(choice=value, ctx=ctx)
+ normalized_mapping = self._normalized_mapping(ctx=ctx)
+
+ try:
+ return next(
+ original
+ for original, normalized in normalized_mapping.items()
+ if normalized == normed_value
+ )
+ except StopIteration:
+ self.fail(
+ self.get_invalid_choice_message(value=value, ctx=ctx),
+ param=param,
+ ctx=ctx,
+ )
+
+ def get_invalid_choice_message(self, value: t.Any, ctx: Context | None) -> str:
+ """Get the error message when the given choice is invalid.
+
+ :param value: The invalid value.
+
+ .. versionadded:: 8.2
+ """
+ choices_str = ", ".join(map(repr, self._normalized_mapping(ctx=ctx).values()))
+ return ngettext(
+ "{value!r} is not {choice}.",
+ "{value!r} is not one of {choices}.",
+ len(self.choices),
+ ).format(value=value, choice=choices_str, choices=choices_str)
+
+ def __repr__(self) -> str:
+ return f"Choice({list(self.choices)})"
+
+ def shell_complete(
+ self, ctx: Context, param: Parameter, incomplete: str
+ ) -> list[CompletionItem]:
+ """Complete choices that start with the incomplete value.
+
+ :param ctx: Invocation context for this command.
+ :param param: The parameter that is requesting completion.
+ :param incomplete: Value being completed. May be empty.
+
+ .. versionadded:: 8.0
+ """
+ from click.shell_completion import CompletionItem
+
+ str_choices = map(str, self.choices)
+
+ if self.case_sensitive:
+ matched = (c for c in str_choices if c.startswith(incomplete))
+ else:
+ incomplete = incomplete.lower()
+ matched = (c for c in str_choices if c.lower().startswith(incomplete))
+
+ return [CompletionItem(c) for c in matched]
+
+
+class DateTime(ParamType):
+ """The DateTime type converts date strings into `datetime` objects.
+
+ The format strings which are checked are configurable, but default to some
+ common (non-timezone aware) ISO 8601 formats.
+
+ When specifying *DateTime* formats, you should only pass a list or a tuple.
+ Other iterables, like generators, may lead to surprising results.
+
+ The format strings are processed using ``datetime.strptime``, and this
+ consequently defines the format strings which are allowed.
+
+ Parsing is tried using each format, in order, and the first format which
+ parses successfully is used.
+
+ :param formats: A list or tuple of date format strings, in the order in
+ which they should be tried. Defaults to
+ ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``,
+ ``'%Y-%m-%d %H:%M:%S'``.
+ """
+
+ name = "datetime"
+
+ def __init__(self, formats: cabc.Sequence[str] | None = None):
+ self.formats: cabc.Sequence[str] = formats or [
+ "%Y-%m-%d",
+ "%Y-%m-%dT%H:%M:%S",
+ "%Y-%m-%d %H:%M:%S",
+ ]
+
+ def to_info_dict(self) -> dict[str, t.Any]:
+ info_dict = super().to_info_dict()
+ info_dict["formats"] = self.formats
+ return info_dict
+
+ def get_metavar(self, param: Parameter, ctx: Context) -> str | None:
+ return f"[{'|'.join(self.formats)}]"
+
+ def _try_to_convert_date(self, value: t.Any, format: str) -> datetime | None:
+ try:
+ return datetime.strptime(value, format)
+ except ValueError:
+ return None
+
+ def convert(
+ self, value: t.Any, param: Parameter | None, ctx: Context | None
+ ) -> t.Any:
+ if isinstance(value, datetime):
+ return value
+
+ for format in self.formats:
+ converted = self._try_to_convert_date(value, format)
+
+ if converted is not None:
+ return converted
+
+ formats_str = ", ".join(map(repr, self.formats))
+ self.fail(
+ ngettext(
+ "{value!r} does not match the format {format}.",
+ "{value!r} does not match the formats {formats}.",
+ len(self.formats),
+ ).format(value=value, format=formats_str, formats=formats_str),
+ param,
+ ctx,
+ )
+
+ def __repr__(self) -> str:
+ return "DateTime"
+
+
+class _NumberParamTypeBase(ParamType):
+ _number_class: t.ClassVar[type[t.Any]]
+
+ def convert(
+ self, value: t.Any, param: Parameter | None, ctx: Context | None
+ ) -> t.Any:
+ try:
+ return self._number_class(value)
+ except ValueError:
+ self.fail(
+ _("{value!r} is not a valid {number_type}.").format(
+ value=value, number_type=self.name
+ ),
+ param,
+ ctx,
+ )
+
+
+class _NumberRangeBase(_NumberParamTypeBase):
+ def __init__(
+ self,
+ min: float | None = None,
+ max: float | None = None,
+ min_open: bool = False,
+ max_open: bool = False,
+ clamp: bool = False,
+ ) -> None:
+ self.min = min
+ self.max = max
+ self.min_open = min_open
+ self.max_open = max_open
+ self.clamp = clamp
+
+ def to_info_dict(self) -> dict[str, t.Any]:
+ info_dict = super().to_info_dict()
+ info_dict.update(
+ min=self.min,
+ max=self.max,
+ min_open=self.min_open,
+ max_open=self.max_open,
+ clamp=self.clamp,
+ )
+ return info_dict
+
+ def convert(
+ self, value: t.Any, param: Parameter | None, ctx: Context | None
+ ) -> t.Any:
+ import operator
+
+ rv = super().convert(value, param, ctx)
+ lt_min: bool = self.min is not None and (
+ operator.le if self.min_open else operator.lt
+ )(rv, self.min)
+ gt_max: bool = self.max is not None and (
+ operator.ge if self.max_open else operator.gt
+ )(rv, self.max)
+
+ if self.clamp:
+ if lt_min:
+ return self._clamp(self.min, 1, self.min_open) # type: ignore
+
+ if gt_max:
+ return self._clamp(self.max, -1, self.max_open) # type: ignore
+
+ if lt_min or gt_max:
+ self.fail(
+ _("{value} is not in the range {range}.").format(
+ value=rv, range=self._describe_range()
+ ),
+ param,
+ ctx,
+ )
+
+ return rv
+
+ def _clamp(self, bound: float, dir: t.Literal[1, -1], open: bool) -> float:
+ """Find the valid value to clamp to bound in the given
+ direction.
+
+ :param bound: The boundary value.
+ :param dir: 1 or -1 indicating the direction to move.
+ :param open: If true, the range does not include the bound.
+ """
+ raise NotImplementedError
+
+ def _describe_range(self) -> str:
+ """Describe the range for use in help text."""
+ if self.min is None:
+ op = "<" if self.max_open else "<="
+ return f"x{op}{self.max}"
+
+ if self.max is None:
+ op = ">" if self.min_open else ">="
+ return f"x{op}{self.min}"
+
+ lop = "<" if self.min_open else "<="
+ rop = "<" if self.max_open else "<="
+ return f"{self.min}{lop}x{rop}{self.max}"
+
+ def __repr__(self) -> str:
+ clamp = " clamped" if self.clamp else ""
+ return f"<{type(self).__name__} {self._describe_range()}{clamp}>"
+
+
+class IntParamType(_NumberParamTypeBase):
+ name = "integer"
+ _number_class = int
+
+ def __repr__(self) -> str:
+ return "INT"
+
+
+class IntRange(_NumberRangeBase, IntParamType):
+ """Restrict an :data:`click.INT` value to a range of accepted
+ values. See :ref:`ranges`.
+
+ If ``min`` or ``max`` are not passed, any value is accepted in that
+ direction. If ``min_open`` or ``max_open`` are enabled, the
+ corresponding boundary is not included in the range.
+
+ If ``clamp`` is enabled, a value outside the range is clamped to the
+ boundary instead of failing.
+
+ .. versionchanged:: 8.0
+ Added the ``min_open`` and ``max_open`` parameters.
+ """
+
+ name = "integer range"
+
+ def _clamp( # type: ignore
+ self, bound: int, dir: t.Literal[1, -1], open: bool
+ ) -> int:
+ if not open:
+ return bound
+
+ return bound + dir
+
+
+class FloatParamType(_NumberParamTypeBase):
+ name = "float"
+ _number_class = float
+
+ def __repr__(self) -> str:
+ return "FLOAT"
+
+
+class FloatRange(_NumberRangeBase, FloatParamType):
+ """Restrict a :data:`click.FLOAT` value to a range of accepted
+ values. See :ref:`ranges`.
+
+ If ``min`` or ``max`` are not passed, any value is accepted in that
+ direction. If ``min_open`` or ``max_open`` are enabled, the
+ corresponding boundary is not included in the range.
+
+ If ``clamp`` is enabled, a value outside the range is clamped to the
+ boundary instead of failing. This is not supported if either
+ boundary is marked ``open``.
+
+ .. versionchanged:: 8.0
+ Added the ``min_open`` and ``max_open`` parameters.
+ """
+
+ name = "float range"
+
+ def __init__(
+ self,
+ min: float | None = None,
+ max: float | None = None,
+ min_open: bool = False,
+ max_open: bool = False,
+ clamp: bool = False,
+ ) -> None:
+ super().__init__(
+ min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp
+ )
+
+ if (min_open or max_open) and clamp:
+ raise TypeError("Clamping is not supported for open bounds.")
+
+ def _clamp(self, bound: float, dir: t.Literal[1, -1], open: bool) -> float:
+ if not open:
+ return bound
+
+ # Could use math.nextafter here, but clamping an
+ # open float range doesn't seem to be particularly useful. It's
+ # left up to the user to write a callback to do it if needed.
+ raise RuntimeError("Clamping is not supported for open bounds.")
+
+
+class BoolParamType(ParamType):
+ name = "boolean"
+
+ bool_states: dict[str, bool] = {
+ "1": True,
+ "0": False,
+ "yes": True,
+ "no": False,
+ "true": True,
+ "false": False,
+ "on": True,
+ "off": False,
+ "t": True,
+ "f": False,
+ "y": True,
+ "n": False,
+ # Absence of value is considered False.
+ "": False,
+ }
+ """A mapping of string values to boolean states.
+
+ Mapping is inspired by :py:attr:`configparser.ConfigParser.BOOLEAN_STATES`
+ and extends it.
+
+ .. caution::
+ String values are lower-cased, as the ``str_to_bool`` comparison function
+ below is case-insensitive.
+
+ .. warning::
+ The mapping is not exhaustive, and does not cover all possible boolean strings
+ representations. It will remains as it is to avoid endless bikeshedding.
+
+ Future work my be considered to make this mapping user-configurable from public
+ API.
+ """
+
+ @staticmethod
+ def str_to_bool(value: str | bool) -> bool | None:
+ """Convert a string to a boolean value.
+
+ If the value is already a boolean, it is returned as-is. If the value is a
+ string, it is stripped of whitespaces and lower-cased, then checked against
+ the known boolean states pre-defined in the `BoolParamType.bool_states` mapping
+ above.
+
+ Returns `None` if the value does not match any known boolean state.
+ """
+ if isinstance(value, bool):
+ return value
+ return BoolParamType.bool_states.get(value.strip().lower())
+
+ def convert(
+ self, value: t.Any, param: Parameter | None, ctx: Context | None
+ ) -> bool:
+ normalized = self.str_to_bool(value)
+ if normalized is None:
+ self.fail(
+ _(
+ "{value!r} is not a valid boolean. Recognized values: {states}"
+ ).format(value=value, states=", ".join(sorted(self.bool_states))),
+ param,
+ ctx,
+ )
+ return normalized
+
+ def __repr__(self) -> str:
+ return "BOOL"
+
+
+class UUIDParameterType(ParamType):
+ name = "uuid"
+
+ def convert(
+ self, value: t.Any, param: Parameter | None, ctx: Context | None
+ ) -> t.Any:
+ import uuid
+
+ if isinstance(value, uuid.UUID):
+ return value
+
+ value = value.strip()
+
+ try:
+ return uuid.UUID(value)
+ except ValueError:
+ self.fail(
+ _("{value!r} is not a valid UUID.").format(value=value), param, ctx
+ )
+
+ def __repr__(self) -> str:
+ return "UUID"
+
+
+class File(ParamType):
+ """Declares a parameter to be a file for reading or writing. The file
+ is automatically closed once the context tears down (after the command
+ finished working).
+
+ Files can be opened for reading or writing. The special value ``-``
+ indicates stdin or stdout depending on the mode.
+
+ By default, the file is opened for reading text data, but it can also be
+ opened in binary mode or for writing. The encoding parameter can be used
+ to force a specific encoding.
+
+ The `lazy` flag controls if the file should be opened immediately or upon
+ first IO. The default is to be non-lazy for standard input and output
+ streams as well as files opened for reading, `lazy` otherwise. When opening a
+ file lazily for reading, it is still opened temporarily for validation, but
+ will not be held open until first IO. lazy is mainly useful when opening
+ for writing to avoid creating the file until it is needed.
+
+ Files can also be opened atomically in which case all writes go into a
+ separate file in the same folder and upon completion the file will
+ be moved over to the original location. This is useful if a file
+ regularly read by other users is modified.
+
+ See :ref:`file-args` for more information.
+
+ .. versionchanged:: 2.0
+ Added the ``atomic`` parameter.
+ """
+
+ name = "filename"
+ envvar_list_splitter: t.ClassVar[str] = os.path.pathsep
+
+ def __init__(
+ self,
+ mode: str = "r",
+ encoding: str | None = None,
+ errors: str | None = "strict",
+ lazy: bool | None = None,
+ atomic: bool = False,
+ ) -> None:
+ self.mode = mode
+ self.encoding = encoding
+ self.errors = errors
+ self.lazy = lazy
+ self.atomic = atomic
+
+ def to_info_dict(self) -> dict[str, t.Any]:
+ info_dict = super().to_info_dict()
+ info_dict.update(mode=self.mode, encoding=self.encoding)
+ return info_dict
+
+ def resolve_lazy_flag(self, value: str | os.PathLike[str]) -> bool:
+ if self.lazy is not None:
+ return self.lazy
+ if os.fspath(value) == "-":
+ return False
+ elif "w" in self.mode:
+ return True
+ return False
+
+ def convert(
+ self,
+ value: str | os.PathLike[str] | t.IO[t.Any],
+ param: Parameter | None,
+ ctx: Context | None,
+ ) -> t.IO[t.Any]:
+ if _is_file_like(value):
+ return value
+
+ value = t.cast("str | os.PathLike[str]", value)
+
+ try:
+ lazy = self.resolve_lazy_flag(value)
+
+ if lazy:
+ lf = LazyFile(
+ value, self.mode, self.encoding, self.errors, atomic=self.atomic
+ )
+
+ if ctx is not None:
+ ctx.call_on_close(lf.close_intelligently)
+
+ return t.cast("t.IO[t.Any]", lf)
+
+ f, should_close = open_stream(
+ value, self.mode, self.encoding, self.errors, atomic=self.atomic
+ )
+
+ # If a context is provided, we automatically close the file
+ # at the end of the context execution (or flush out). If a
+ # context does not exist, it's the caller's responsibility to
+ # properly close the file. This for instance happens when the
+ # type is used with prompts.
+ if ctx is not None:
+ if should_close:
+ ctx.call_on_close(safecall(f.close))
+ else:
+ ctx.call_on_close(safecall(f.flush))
+
+ return f
+ except OSError as e:
+ self.fail(f"'{format_filename(value)}': {e.strerror}", param, ctx)
+
+ def shell_complete(
+ self, ctx: Context, param: Parameter, incomplete: str
+ ) -> list[CompletionItem]:
+ """Return a special completion marker that tells the completion
+ system to use the shell to provide file path completions.
+
+ :param ctx: Invocation context for this command.
+ :param param: The parameter that is requesting completion.
+ :param incomplete: Value being completed. May be empty.
+
+ .. versionadded:: 8.0
+ """
+ from click.shell_completion import CompletionItem
+
+ return [CompletionItem(incomplete, type="file")]
+
+
+def _is_file_like(value: t.Any) -> te.TypeGuard[t.IO[t.Any]]:
+ return hasattr(value, "read") or hasattr(value, "write")
+
+
+class Path(ParamType):
+ """The ``Path`` type is similar to the :class:`File` type, but
+ returns the filename instead of an open file. Various checks can be
+ enabled to validate the type of file and permissions.
+
+ :param exists: The file or directory needs to exist for the value to
+ be valid. If this is not set to ``True``, and the file does not
+ exist, then all further checks are silently skipped.
+ :param file_okay: Allow a file as a value.
+ :param dir_okay: Allow a directory as a value.
+ :param readable: if true, a readable check is performed.
+ :param writable: if true, a writable check is performed.
+ :param executable: if true, an executable check is performed.
+ :param resolve_path: Make the value absolute and resolve any
+ symlinks. A ``~`` is not expanded, as this is supposed to be
+ done by the shell only.
+ :param allow_dash: Allow a single dash as a value, which indicates
+ a standard stream (but does not open it). Use
+ :func:`~click.open_file` to handle opening this value.
+ :param path_type: Convert the incoming path value to this type. If
+ ``None``, keep Python's default, which is ``str``. Useful to
+ convert to :class:`pathlib.Path`.
+
+ .. versionchanged:: 8.1
+ Added the ``executable`` parameter.
+
+ .. versionchanged:: 8.0
+ Allow passing ``path_type=pathlib.Path``.
+
+ .. versionchanged:: 6.0
+ Added the ``allow_dash`` parameter.
+ """
+
+ envvar_list_splitter: t.ClassVar[str] = os.path.pathsep
+
+ def __init__(
+ self,
+ exists: bool = False,
+ file_okay: bool = True,
+ dir_okay: bool = True,
+ writable: bool = False,
+ readable: bool = True,
+ resolve_path: bool = False,
+ allow_dash: bool = False,
+ path_type: type[t.Any] | None = None,
+ executable: bool = False,
+ ):
+ self.exists = exists
+ self.file_okay = file_okay
+ self.dir_okay = dir_okay
+ self.readable = readable
+ self.writable = writable
+ self.executable = executable
+ self.resolve_path = resolve_path
+ self.allow_dash = allow_dash
+ self.type = path_type
+
+ if self.file_okay and not self.dir_okay:
+ self.name: str = _("file")
+ elif self.dir_okay and not self.file_okay:
+ self.name = _("directory")
+ else:
+ self.name = _("path")
+
+ def to_info_dict(self) -> dict[str, t.Any]:
+ info_dict = super().to_info_dict()
+ info_dict.update(
+ exists=self.exists,
+ file_okay=self.file_okay,
+ dir_okay=self.dir_okay,
+ writable=self.writable,
+ readable=self.readable,
+ allow_dash=self.allow_dash,
+ )
+ return info_dict
+
+ def coerce_path_result(
+ self, value: str | os.PathLike[str]
+ ) -> str | bytes | os.PathLike[str]:
+ if self.type is not None and not isinstance(value, self.type):
+ if self.type is str:
+ return os.fsdecode(value)
+ elif self.type is bytes:
+ return os.fsencode(value)
+ else:
+ return t.cast("os.PathLike[str]", self.type(value))
+
+ return value
+
+ def convert(
+ self,
+ value: str | os.PathLike[str],
+ param: Parameter | None,
+ ctx: Context | None,
+ ) -> str | bytes | os.PathLike[str]:
+ rv = value
+
+ is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-")
+
+ if not is_dash:
+ if self.resolve_path:
+ rv = os.path.realpath(rv)
+
+ try:
+ st = os.stat(rv)
+ except OSError:
+ if not self.exists:
+ return self.coerce_path_result(rv)
+ self.fail(
+ _("{name} {filename!r} does not exist.").format(
+ name=self.name.title(), filename=format_filename(value)
+ ),
+ param,
+ ctx,
+ )
+
+ if not self.file_okay and stat.S_ISREG(st.st_mode):
+ self.fail(
+ _("{name} {filename!r} is a file.").format(
+ name=self.name.title(), filename=format_filename(value)
+ ),
+ param,
+ ctx,
+ )
+ if not self.dir_okay and stat.S_ISDIR(st.st_mode):
+ self.fail(
+ _("{name} {filename!r} is a directory.").format(
+ name=self.name.title(), filename=format_filename(value)
+ ),
+ param,
+ ctx,
+ )
+
+ if self.readable and not os.access(rv, os.R_OK):
+ self.fail(
+ _("{name} {filename!r} is not readable.").format(
+ name=self.name.title(), filename=format_filename(value)
+ ),
+ param,
+ ctx,
+ )
+
+ if self.writable and not os.access(rv, os.W_OK):
+ self.fail(
+ _("{name} {filename!r} is not writable.").format(
+ name=self.name.title(), filename=format_filename(value)
+ ),
+ param,
+ ctx,
+ )
+
+ if self.executable and not os.access(value, os.X_OK):
+ self.fail(
+ _("{name} {filename!r} is not executable.").format(
+ name=self.name.title(), filename=format_filename(value)
+ ),
+ param,
+ ctx,
+ )
+
+ return self.coerce_path_result(rv)
+
+ def shell_complete(
+ self, ctx: Context, param: Parameter, incomplete: str
+ ) -> list[CompletionItem]:
+ """Return a special completion marker that tells the completion
+ system to use the shell to provide path completions for only
+ directories or any paths.
+
+ :param ctx: Invocation context for this command.
+ :param param: The parameter that is requesting completion.
+ :param incomplete: Value being completed. May be empty.
+
+ .. versionadded:: 8.0
+ """
+ from click.shell_completion import CompletionItem
+
+ type = "dir" if self.dir_okay and not self.file_okay else "file"
+ return [CompletionItem(incomplete, type=type)]
+
+
+class Tuple(CompositeParamType):
+ """The default behavior of Click is to apply a type on a value directly.
+ This works well in most cases, except for when `nargs` is set to a fixed
+ count and different types should be used for different items. In this
+ case the :class:`Tuple` type can be used. This type can only be used
+ if `nargs` is set to a fixed number.
+
+ For more information see :ref:`tuple-type`.
+
+ This can be selected by using a Python tuple literal as a type.
+
+ :param types: a list of types that should be used for the tuple items.
+ """
+
+ def __init__(self, types: cabc.Sequence[type[t.Any] | ParamType]) -> None:
+ self.types: cabc.Sequence[ParamType] = [convert_type(ty) for ty in types]
+
+ def to_info_dict(self) -> dict[str, t.Any]:
+ info_dict = super().to_info_dict()
+ info_dict["types"] = [t.to_info_dict() for t in self.types]
+ return info_dict
+
+ @property
+ def name(self) -> str: # type: ignore
+ return f"<{' '.join(ty.name for ty in self.types)}>"
+
+ @property
+ def arity(self) -> int: # type: ignore
+ return len(self.types)
+
+ def convert(
+ self, value: t.Any, param: Parameter | None, ctx: Context | None
+ ) -> t.Any:
+ len_type = len(self.types)
+ len_value = len(value)
+
+ if len_value != len_type:
+ self.fail(
+ ngettext(
+ "{len_type} values are required, but {len_value} was given.",
+ "{len_type} values are required, but {len_value} were given.",
+ len_value,
+ ).format(len_type=len_type, len_value=len_value),
+ param=param,
+ ctx=ctx,
+ )
+
+ return tuple(
+ ty(x, param, ctx) for ty, x in zip(self.types, value, strict=False)
+ )
+
+
+def convert_type(ty: t.Any | None, default: t.Any | None = None) -> ParamType:
+ """Find the most appropriate :class:`ParamType` for the given Python
+ type. If the type isn't provided, it can be inferred from a default
+ value.
+ """
+ guessed_type = False
+
+ if ty is None and default is not None:
+ if isinstance(default, (tuple, list)):
+ # If the default is empty, ty will remain None and will
+ # return STRING.
+ if default:
+ item = default[0]
+
+ # A tuple of tuples needs to detect the inner types.
+ # Can't call convert recursively because that would
+ # incorrectly unwind the tuple to a single type.
+ if isinstance(item, (tuple, list)):
+ ty = tuple(map(type, item))
+ else:
+ ty = type(item)
+ else:
+ ty = type(default)
+
+ guessed_type = True
+
+ if isinstance(ty, tuple):
+ return Tuple(ty)
+
+ if isinstance(ty, ParamType):
+ return ty
+
+ if ty is str or ty is None:
+ return STRING
+
+ if ty is int:
+ return INT
+
+ if ty is float:
+ return FLOAT
+
+ if ty is bool:
+ return BOOL
+
+ if guessed_type:
+ return STRING
+
+ if __debug__:
+ try:
+ if issubclass(ty, ParamType):
+ raise AssertionError(
+ f"Attempted to use an uninstantiated parameter type ({ty})."
+ )
+ except TypeError:
+ # ty is an instance (correct), so issubclass fails.
+ pass
+
+ return FuncParamType(ty)
+
+
+#: A dummy parameter type that just does nothing. From a user's
+#: perspective this appears to just be the same as `STRING` but
+#: internally no string conversion takes place if the input was bytes.
+#: This is usually useful when working with file paths as they can
+#: appear in bytes and unicode.
+#:
+#: For path related uses the :class:`Path` type is a better choice but
+#: there are situations where an unprocessed type is useful which is why
+#: it is is provided.
+#:
+#: .. versionadded:: 4.0
+UNPROCESSED = UnprocessedParamType()
+
+#: A unicode string parameter type which is the implicit default. This
+#: can also be selected by using ``str`` as type.
+STRING = StringParamType()
+
+#: An integer parameter. This can also be selected by using ``int`` as
+#: type.
+INT = IntParamType()
+
+#: A floating point value parameter. This can also be selected by using
+#: ``float`` as type.
+FLOAT = FloatParamType()
+
+#: A boolean parameter. This is the default for boolean flags. This can
+#: also be selected by using ``bool`` as a type.
+BOOL = BoolParamType()
+
+#: A UUID parameter.
+UUID = UUIDParameterType()
+
+
+class OptionHelpExtra(t.TypedDict, total=False):
+ envvars: tuple[str, ...]
+ default: str
+ range: str
+ required: str
diff --git a/venv/Lib/site-packages/click/utils.py b/venv/Lib/site-packages/click/utils.py
new file mode 100644
index 0000000..89cae3b
--- /dev/null
+++ b/venv/Lib/site-packages/click/utils.py
@@ -0,0 +1,630 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import os
+import re
+import sys
+import typing as t
+from functools import update_wrapper
+from types import ModuleType
+from types import TracebackType
+
+from ._compat import _default_text_stderr
+from ._compat import _default_text_stdout
+from ._compat import _find_binary_writer
+from ._compat import auto_wrap_for_ansi
+from ._compat import binary_streams
+from ._compat import open_stream
+from ._compat import should_strip_ansi
+from ._compat import strip_ansi
+from ._compat import text_streams
+from ._compat import WIN
+from .globals import resolve_color_default
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+ P = te.ParamSpec("P")
+
+R = t.TypeVar("R")
+
+
+def _posixify(name: str) -> str:
+ return "-".join(name.split()).lower()
+
+
+def safecall(func: t.Callable[P, R]) -> t.Callable[P, R | None]:
+ """Wraps a function so that it swallows exceptions."""
+
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
+ try:
+ return func(*args, **kwargs)
+ except Exception:
+ pass
+ return None
+
+ return update_wrapper(wrapper, func)
+
+
+def make_str(value: t.Any) -> str:
+ """Converts a value into a valid string."""
+ if isinstance(value, bytes):
+ try:
+ return value.decode(sys.getfilesystemencoding())
+ except UnicodeError:
+ return value.decode("utf-8", "replace")
+ return str(value)
+
+
+def make_default_short_help(help: str, max_length: int = 45) -> str:
+ """Returns a condensed version of help string.
+
+ :meta private:
+ """
+ # Consider only the first paragraph.
+ paragraph_end = help.find("\n\n")
+
+ if paragraph_end != -1:
+ help = help[:paragraph_end]
+
+ # Collapse newlines, tabs, and spaces.
+ words = help.split()
+
+ if not words:
+ return ""
+
+ # The first paragraph started with a "no rewrap" marker, ignore it.
+ if words[0] == "\b":
+ words = words[1:]
+
+ total_length = 0
+ last_index = len(words) - 1
+
+ for i, word in enumerate(words):
+ total_length += len(word) + (i > 0)
+
+ if total_length > max_length: # too long, truncate
+ break
+
+ if word[-1] == ".": # sentence end, truncate without "..."
+ return " ".join(words[: i + 1])
+
+ if total_length == max_length and i != last_index:
+ break # not at sentence end, truncate with "..."
+ else:
+ return " ".join(words) # no truncation needed
+
+ # Account for the length of the suffix.
+ total_length += len("...")
+
+ # remove words until the length is short enough
+ while i > 0:
+ total_length -= len(words[i]) + (i > 0)
+
+ if total_length <= max_length:
+ break
+
+ i -= 1
+
+ return " ".join(words[:i]) + "..."
+
+
+class LazyFile:
+ """A lazy file works like a regular file but it does not fully open
+ the file but it does perform some basic checks early to see if the
+ filename parameter does make sense. This is useful for safely opening
+ files for writing.
+ """
+
+ def __init__(
+ self,
+ filename: str | os.PathLike[str],
+ mode: str = "r",
+ encoding: str | None = None,
+ errors: str | None = "strict",
+ atomic: bool = False,
+ ):
+ self.name: str = os.fspath(filename)
+ self.mode = mode
+ self.encoding = encoding
+ self.errors = errors
+ self.atomic = atomic
+ self._f: t.IO[t.Any] | None
+ self.should_close: bool
+
+ if self.name == "-":
+ self._f, self.should_close = open_stream(filename, mode, encoding, errors)
+ else:
+ if "r" in mode:
+ # Open and close the file in case we're opening it for
+ # reading so that we can catch at least some errors in
+ # some cases early.
+ open(filename, mode).close()
+ self._f = None
+ self.should_close = True
+
+ def __getattr__(self, name: str) -> t.Any:
+ return getattr(self.open(), name)
+
+ def __repr__(self) -> str:
+ if self._f is not None:
+ return repr(self._f)
+ return f""
+
+ def open(self) -> t.IO[t.Any]:
+ """Opens the file if it's not yet open. This call might fail with
+ a :exc:`FileError`. Not handling this error will produce an error
+ that Click shows.
+ """
+ if self._f is not None:
+ return self._f
+ try:
+ rv, self.should_close = open_stream(
+ self.name, self.mode, self.encoding, self.errors, atomic=self.atomic
+ )
+ except OSError as e:
+ from .exceptions import FileError
+
+ raise FileError(self.name, hint=e.strerror) from e
+ self._f = rv
+ return rv
+
+ def close(self) -> None:
+ """Closes the underlying file, no matter what."""
+ if self._f is not None:
+ self._f.close()
+
+ def close_intelligently(self) -> None:
+ """This function only closes the file if it was opened by the lazy
+ file wrapper. For instance this will never close stdin.
+ """
+ if self.should_close:
+ self.close()
+
+ def __enter__(self) -> LazyFile:
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> None:
+ self.close_intelligently()
+
+ def __iter__(self) -> cabc.Iterator[t.AnyStr]:
+ self.open()
+ return iter(self._f) # type: ignore
+
+
+class KeepOpenFile:
+ def __init__(self, file: t.IO[t.Any]) -> None:
+ self._file: t.IO[t.Any] = file
+
+ def __getattr__(self, name: str) -> t.Any:
+ return getattr(self._file, name)
+
+ def __enter__(self) -> KeepOpenFile:
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> None:
+ pass
+
+ def __repr__(self) -> str:
+ return repr(self._file)
+
+ def __iter__(self) -> cabc.Iterator[t.AnyStr]:
+ return iter(self._file)
+
+
+def echo(
+ message: t.Any | None = None,
+ file: t.IO[t.Any] | None = None,
+ nl: bool = True,
+ err: bool = False,
+ color: bool | None = None,
+) -> None:
+ """Print a message and newline to stdout or a file. This should be
+ used instead of :func:`print` because it provides better support
+ for different data, files, and environments.
+
+ Compared to :func:`print`, this does the following:
+
+ - Ensures that the output encoding is not misconfigured on Linux.
+ - Supports Unicode in the Windows console.
+ - Supports writing to binary outputs, and supports writing bytes
+ to text outputs.
+ - Supports colors and styles on Windows.
+ - Removes ANSI color and style codes if the output does not look
+ like an interactive terminal.
+ - Always flushes the output.
+
+ :param message: The string or bytes to output. Other objects are
+ converted to strings.
+ :param file: The file to write to. Defaults to ``stdout``.
+ :param err: Write to ``stderr`` instead of ``stdout``.
+ :param nl: Print a newline after the message. Enabled by default.
+ :param color: Force showing or hiding colors and other styles. By
+ default Click will remove color if the output does not look like
+ an interactive terminal.
+
+ .. versionchanged:: 6.0
+ Support Unicode output on the Windows console. Click does not
+ modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()``
+ will still not support Unicode.
+
+ .. versionchanged:: 4.0
+ Added the ``color`` parameter.
+
+ .. versionadded:: 3.0
+ Added the ``err`` parameter.
+
+ .. versionchanged:: 2.0
+ Support colors on Windows if colorama is installed.
+ """
+ if file is None:
+ if err:
+ file = _default_text_stderr()
+ else:
+ file = _default_text_stdout()
+
+ # There are no standard streams attached to write to. For example,
+ # pythonw on Windows.
+ if file is None:
+ return
+
+ # Convert non bytes/text into the native string type.
+ if message is not None and not isinstance(message, (str, bytes, bytearray)):
+ out: str | bytes | bytearray | None = str(message)
+ else:
+ out = message
+
+ if nl:
+ out = out or ""
+ if isinstance(out, str):
+ out += "\n"
+ else:
+ out += b"\n"
+
+ if not out:
+ file.flush()
+ return
+
+ # If there is a message and the value looks like bytes, we manually
+ # need to find the binary stream and write the message in there.
+ # This is done separately so that most stream types will work as you
+ # would expect. Eg: you can write to StringIO for other cases.
+ if isinstance(out, (bytes, bytearray)):
+ binary_file = _find_binary_writer(file)
+
+ if binary_file is not None:
+ file.flush()
+ binary_file.write(out)
+ binary_file.flush()
+ return
+
+ # ANSI style code support. For no message or bytes, nothing happens.
+ # When outputting to a file instead of a terminal, strip codes.
+ else:
+ color = resolve_color_default(color)
+
+ if should_strip_ansi(file, color):
+ out = strip_ansi(out)
+ elif WIN:
+ if auto_wrap_for_ansi is not None:
+ file = auto_wrap_for_ansi(file, color) # type: ignore
+ elif not color:
+ out = strip_ansi(out)
+
+ file.write(out) # type: ignore
+ file.flush()
+
+
+def get_binary_stream(name: t.Literal["stdin", "stdout", "stderr"]) -> t.BinaryIO:
+ """Returns a system stream for byte processing.
+
+ :param name: the name of the stream to open. Valid names are ``'stdin'``,
+ ``'stdout'`` and ``'stderr'``
+ """
+ opener = binary_streams.get(name)
+ if opener is None:
+ raise TypeError(f"Unknown standard stream '{name}'")
+ return opener()
+
+
+def get_text_stream(
+ name: t.Literal["stdin", "stdout", "stderr"],
+ encoding: str | None = None,
+ errors: str | None = "strict",
+) -> t.TextIO:
+ """Returns a system stream for text processing. This usually returns
+ a wrapped stream around a binary stream returned from
+ :func:`get_binary_stream` but it also can take shortcuts for already
+ correctly configured streams.
+
+ :param name: the name of the stream to open. Valid names are ``'stdin'``,
+ ``'stdout'`` and ``'stderr'``
+ :param encoding: overrides the detected default encoding.
+ :param errors: overrides the default error mode.
+ """
+ opener = text_streams.get(name)
+ if opener is None:
+ raise TypeError(f"Unknown standard stream '{name}'")
+ return opener(encoding, errors)
+
+
+def open_file(
+ filename: str | os.PathLike[str],
+ mode: str = "r",
+ encoding: str | None = None,
+ errors: str | None = "strict",
+ lazy: bool = False,
+ atomic: bool = False,
+) -> t.IO[t.Any]:
+ """Open a file, with extra behavior to handle ``'-'`` to indicate
+ a standard stream, lazy open on write, and atomic write. Similar to
+ the behavior of the :class:`~click.File` param type.
+
+ If ``'-'`` is given to open ``stdout`` or ``stdin``, the stream is
+ wrapped so that using it in a context manager will not close it.
+ This makes it possible to use the function without accidentally
+ closing a standard stream:
+
+ .. code-block:: python
+
+ with open_file(filename) as f:
+ ...
+
+ :param filename: The name or Path of the file to open, or ``'-'`` for
+ ``stdin``/``stdout``.
+ :param mode: The mode in which to open the file.
+ :param encoding: The encoding to decode or encode a file opened in
+ text mode.
+ :param errors: The error handling mode.
+ :param lazy: Wait to open the file until it is accessed. For read
+ mode, the file is temporarily opened to raise access errors
+ early, then closed until it is read again.
+ :param atomic: Write to a temporary file and replace the given file
+ on close.
+
+ .. versionadded:: 3.0
+ """
+ if lazy:
+ return t.cast(
+ "t.IO[t.Any]", LazyFile(filename, mode, encoding, errors, atomic=atomic)
+ )
+
+ f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)
+
+ if not should_close:
+ f = t.cast("t.IO[t.Any]", KeepOpenFile(f))
+
+ return f
+
+
+def format_filename(
+ filename: str | bytes | os.PathLike[str] | os.PathLike[bytes],
+ shorten: bool = False,
+) -> str:
+ """Format a filename as a string for display. Ensures the filename can be
+ displayed by replacing any invalid bytes or surrogate escapes in the name
+ with the replacement character ``�``.
+
+ Invalid bytes or surrogate escapes will raise an error when written to a
+ stream with ``errors="strict"``. This will typically happen with ``stdout``
+ when the locale is something like ``en_GB.UTF-8``.
+
+ Many scenarios *are* safe to write surrogates though, due to PEP 538 and
+ PEP 540, including:
+
+ - Writing to ``stderr``, which uses ``errors="backslashreplace"``.
+ - The system has ``LANG=C.UTF-8``, ``C``, or ``POSIX``. Python opens
+ stdout and stderr with ``errors="surrogateescape"``.
+ - None of ``LANG/LC_*`` are set. Python assumes ``LANG=C.UTF-8``.
+ - Python is started in UTF-8 mode with ``PYTHONUTF8=1`` or ``-X utf8``.
+ Python opens stdout and stderr with ``errors="surrogateescape"``.
+
+ :param filename: formats a filename for UI display. This will also convert
+ the filename into unicode without failing.
+ :param shorten: this optionally shortens the filename to strip of the
+ path that leads up to it.
+ """
+ if shorten:
+ filename = os.path.basename(filename)
+ else:
+ filename = os.fspath(filename)
+
+ if isinstance(filename, bytes):
+ filename = filename.decode(sys.getfilesystemencoding(), "replace")
+ else:
+ filename = filename.encode("utf-8", "surrogateescape").decode(
+ "utf-8", "replace"
+ )
+
+ return filename
+
+
+def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str:
+ r"""Returns the config folder for the application. The default behavior
+ is to return whatever is most appropriate for the operating system.
+
+ To give you an idea, for an app called ``"Foo Bar"``, something like
+ the following folders could be returned:
+
+ Mac OS X:
+ ``~/Library/Application Support/Foo Bar``
+ Mac OS X (POSIX):
+ ``~/.foo-bar``
+ Unix:
+ ``~/.config/foo-bar``
+ Unix (POSIX):
+ ``~/.foo-bar``
+ Windows (roaming):
+ ``C:\Users\\AppData\Roaming\Foo Bar``
+ Windows (not roaming):
+ ``C:\Users\\AppData\Local\Foo Bar``
+
+ .. versionadded:: 2.0
+
+ :param app_name: the application name. This should be properly capitalized
+ and can contain whitespace.
+ :param roaming: controls if the folder should be roaming or not on Windows.
+ Has no effect otherwise.
+ :param force_posix: if this is set to `True` then on any POSIX system the
+ folder will be stored in the home folder with a leading
+ dot instead of the XDG config home or darwin's
+ application support folder.
+ """
+ if WIN:
+ key = "APPDATA" if roaming else "LOCALAPPDATA"
+ folder = os.environ.get(key)
+ if folder is None:
+ folder = os.path.expanduser("~")
+ return os.path.join(folder, app_name)
+ if force_posix:
+ return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}"))
+ if sys.platform == "darwin":
+ return os.path.join(
+ os.path.expanduser("~/Library/Application Support"), app_name
+ )
+ return os.path.join(
+ os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
+ _posixify(app_name),
+ )
+
+
+class PacifyFlushWrapper:
+ """This wrapper is used to catch and suppress BrokenPipeErrors resulting
+ from ``.flush()`` being called on broken pipe during the shutdown/final-GC
+ of the Python interpreter. Notably ``.flush()`` is always called on
+ ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any
+ other cleanup code, and the case where the underlying file is not a broken
+ pipe, all calls and attributes are proxied.
+ """
+
+ def __init__(self, wrapped: t.IO[t.Any]) -> None:
+ self.wrapped = wrapped
+
+ def flush(self) -> None:
+ try:
+ self.wrapped.flush()
+ except OSError as e:
+ import errno
+
+ if e.errno != errno.EPIPE:
+ raise
+
+ def __getattr__(self, attr: str) -> t.Any:
+ return getattr(self.wrapped, attr)
+
+
+def _detect_program_name(
+ path: str | None = None, _main: ModuleType | None = None
+) -> str:
+ """Determine the command used to run the program, for use in help
+ text. If a file or entry point was executed, the file name is
+ returned. If ``python -m`` was used to execute a module or package,
+ ``python -m name`` is returned.
+
+ This doesn't try to be too precise, the goal is to give a concise
+ name for help text. Files are only shown as their name without the
+ path. ``python`` is only shown for modules, and the full path to
+ ``sys.executable`` is not shown.
+
+ :param path: The Python file being executed. Python puts this in
+ ``sys.argv[0]``, which is used by default.
+ :param _main: The ``__main__`` module. This should only be passed
+ during internal testing.
+
+ .. versionadded:: 8.0
+ Based on command args detection in the Werkzeug reloader.
+
+ :meta private:
+ """
+ if _main is None:
+ _main = sys.modules["__main__"]
+
+ if not path:
+ path = sys.argv[0]
+
+ # The value of __package__ indicates how Python was called. It may
+ # not exist if a setuptools script is installed as an egg. It may be
+ # set incorrectly for entry points created with pip on Windows.
+ # It is set to "" inside a Shiv or PEX zipapp.
+ if getattr(_main, "__package__", None) in {None, ""} or (
+ os.name == "nt"
+ and _main.__package__ == ""
+ and not os.path.exists(path)
+ and os.path.exists(f"{path}.exe")
+ ):
+ # Executed a file, like "python app.py".
+ return os.path.basename(path)
+
+ # Executed a module, like "python -m example".
+ # Rewritten by Python from "-m script" to "/path/to/script.py".
+ # Need to look at main module to determine how it was executed.
+ py_module = t.cast(str, _main.__package__)
+ name = os.path.splitext(os.path.basename(path))[0]
+
+ # A submodule like "example.cli".
+ if name != "__main__":
+ py_module = f"{py_module}.{name}"
+
+ return f"python -m {py_module.lstrip('.')}"
+
+
+def _expand_args(
+ args: cabc.Iterable[str],
+ *,
+ user: bool = True,
+ env: bool = True,
+ glob_recursive: bool = True,
+) -> list[str]:
+ """Simulate Unix shell expansion with Python functions.
+
+ See :func:`glob.glob`, :func:`os.path.expanduser`, and
+ :func:`os.path.expandvars`.
+
+ This is intended for use on Windows, where the shell does not do any
+ expansion. It may not exactly match what a Unix shell would do.
+
+ :param args: List of command line arguments to expand.
+ :param user: Expand user home directory.
+ :param env: Expand environment variables.
+ :param glob_recursive: ``**`` matches directories recursively.
+
+ .. versionchanged:: 8.1
+ Invalid glob patterns are treated as empty expansions rather
+ than raising an error.
+
+ .. versionadded:: 8.0
+
+ :meta private:
+ """
+ from glob import glob
+
+ out = []
+
+ for arg in args:
+ if user:
+ arg = os.path.expanduser(arg)
+
+ if env:
+ arg = os.path.expandvars(arg)
+
+ try:
+ matches = glob(arg, recursive=glob_recursive)
+ except re.error:
+ matches = []
+
+ if not matches:
+ out.append(arg)
+ else:
+ out.extend(matches)
+
+ return out
diff --git a/venv/Lib/site-packages/colorama-0.4.6.dist-info/INSTALLER b/venv/Lib/site-packages/colorama-0.4.6.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/Lib/site-packages/colorama-0.4.6.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/colorama-0.4.6.dist-info/METADATA b/venv/Lib/site-packages/colorama-0.4.6.dist-info/METADATA
new file mode 100644
index 0000000..a1b5c57
--- /dev/null
+++ b/venv/Lib/site-packages/colorama-0.4.6.dist-info/METADATA
@@ -0,0 +1,441 @@
+Metadata-Version: 2.1
+Name: colorama
+Version: 0.4.6
+Summary: Cross-platform colored terminal text.
+Project-URL: Homepage, https://github.com/tartley/colorama
+Author-email: Jonathan Hartley
+License-File: LICENSE.txt
+Keywords: ansi,color,colour,crossplatform,terminal,text,windows,xplatform
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Console
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Terminals
+Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7
+Description-Content-Type: text/x-rst
+
+.. image:: https://img.shields.io/pypi/v/colorama.svg
+ :target: https://pypi.org/project/colorama/
+ :alt: Latest Version
+
+.. image:: https://img.shields.io/pypi/pyversions/colorama.svg
+ :target: https://pypi.org/project/colorama/
+ :alt: Supported Python versions
+
+.. image:: https://github.com/tartley/colorama/actions/workflows/test.yml/badge.svg
+ :target: https://github.com/tartley/colorama/actions/workflows/test.yml
+ :alt: Build Status
+
+Colorama
+========
+
+Makes ANSI escape character sequences (for producing colored terminal text and
+cursor positioning) work under MS Windows.
+
+.. |donate| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif
+ :target: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=2MZ9D2GMLYCUJ&item_name=Colorama¤cy_code=USD
+ :alt: Donate with Paypal
+
+`PyPI for releases `_ |
+`Github for source `_ |
+`Colorama for enterprise on Tidelift `_
+
+If you find Colorama useful, please |donate| to the authors. Thank you!
+
+Installation
+------------
+
+Tested on CPython 2.7, 3.7, 3.8, 3.9 and 3.10 and Pypy 2.7 and 3.8.
+
+No requirements other than the standard library.
+
+.. code-block:: bash
+
+ pip install colorama
+ # or
+ conda install -c anaconda colorama
+
+Description
+-----------
+
+ANSI escape character sequences have long been used to produce colored terminal
+text and cursor positioning on Unix and Macs. Colorama makes this work on
+Windows, too, by wrapping ``stdout``, stripping ANSI sequences it finds (which
+would appear as gobbledygook in the output), and converting them into the
+appropriate win32 calls to modify the state of the terminal. On other platforms,
+Colorama does nothing.
+
+This has the upshot of providing a simple cross-platform API for printing
+colored terminal text from Python, and has the happy side-effect that existing
+applications or libraries which use ANSI sequences to produce colored output on
+Linux or Macs can now also work on Windows, simply by calling
+``colorama.just_fix_windows_console()`` (since v0.4.6) or ``colorama.init()``
+(all versions, but may have other side-effects – see below).
+
+An alternative approach is to install ``ansi.sys`` on Windows machines, which
+provides the same behaviour for all applications running in terminals. Colorama
+is intended for situations where that isn't easy (e.g., maybe your app doesn't
+have an installer.)
+
+Demo scripts in the source code repository print some colored text using
+ANSI sequences. Compare their output under Gnome-terminal's built in ANSI
+handling, versus on Windows Command-Prompt using Colorama:
+
+.. image:: https://github.com/tartley/colorama/raw/master/screenshots/ubuntu-demo.png
+ :width: 661
+ :height: 357
+ :alt: ANSI sequences on Ubuntu under gnome-terminal.
+
+.. image:: https://github.com/tartley/colorama/raw/master/screenshots/windows-demo.png
+ :width: 668
+ :height: 325
+ :alt: Same ANSI sequences on Windows, using Colorama.
+
+These screenshots show that, on Windows, Colorama does not support ANSI 'dim
+text'; it looks the same as 'normal text'.
+
+Usage
+-----
+
+Initialisation
+..............
+
+If the only thing you want from Colorama is to get ANSI escapes to work on
+Windows, then run:
+
+.. code-block:: python
+
+ from colorama import just_fix_windows_console
+ just_fix_windows_console()
+
+If you're on a recent version of Windows 10 or better, and your stdout/stderr
+are pointing to a Windows console, then this will flip the magic configuration
+switch to enable Windows' built-in ANSI support.
+
+If you're on an older version of Windows, and your stdout/stderr are pointing to
+a Windows console, then this will wrap ``sys.stdout`` and/or ``sys.stderr`` in a
+magic file object that intercepts ANSI escape sequences and issues the
+appropriate Win32 calls to emulate them.
+
+In all other circumstances, it does nothing whatsoever. Basically the idea is
+that this makes Windows act like Unix with respect to ANSI escape handling.
+
+It's safe to call this function multiple times. It's safe to call this function
+on non-Windows platforms, but it won't do anything. It's safe to call this
+function when one or both of your stdout/stderr are redirected to a file – it
+won't do anything to those streams.
+
+Alternatively, you can use the older interface with more features (but also more
+potential footguns):
+
+.. code-block:: python
+
+ from colorama import init
+ init()
+
+This does the same thing as ``just_fix_windows_console``, except for the
+following differences:
+
+- It's not safe to call ``init`` multiple times; you can end up with multiple
+ layers of wrapping and broken ANSI support.
+
+- Colorama will apply a heuristic to guess whether stdout/stderr support ANSI,
+ and if it thinks they don't, then it will wrap ``sys.stdout`` and
+ ``sys.stderr`` in a magic file object that strips out ANSI escape sequences
+ before printing them. This happens on all platforms, and can be convenient if
+ you want to write your code to emit ANSI escape sequences unconditionally, and
+ let Colorama decide whether they should actually be output. But note that
+ Colorama's heuristic is not particularly clever.
+
+- ``init`` also accepts explicit keyword args to enable/disable various
+ functionality – see below.
+
+To stop using Colorama before your program exits, simply call ``deinit()``.
+This will restore ``stdout`` and ``stderr`` to their original values, so that
+Colorama is disabled. To resume using Colorama again, call ``reinit()``; it is
+cheaper than calling ``init()`` again (but does the same thing).
+
+Most users should depend on ``colorama >= 0.4.6``, and use
+``just_fix_windows_console``. The old ``init`` interface will be supported
+indefinitely for backwards compatibility, but we don't plan to fix any issues
+with it, also for backwards compatibility.
+
+Colored Output
+..............
+
+Cross-platform printing of colored text can then be done using Colorama's
+constant shorthand for ANSI escape sequences. These are deliberately
+rudimentary, see below.
+
+.. code-block:: python
+
+ from colorama import Fore, Back, Style
+ print(Fore.RED + 'some red text')
+ print(Back.GREEN + 'and with a green background')
+ print(Style.DIM + 'and in dim text')
+ print(Style.RESET_ALL)
+ print('back to normal now')
+
+...or simply by manually printing ANSI sequences from your own code:
+
+.. code-block:: python
+
+ print('\033[31m' + 'some red text')
+ print('\033[39m') # and reset to default color
+
+...or, Colorama can be used in conjunction with existing ANSI libraries
+such as the venerable `Termcolor `_
+the fabulous `Blessings `_,
+or the incredible `_Rich `_.
+
+If you wish Colorama's Fore, Back and Style constants were more capable,
+then consider using one of the above highly capable libraries to generate
+colors, etc, and use Colorama just for its primary purpose: to convert
+those ANSI sequences to also work on Windows:
+
+SIMILARLY, do not send PRs adding the generation of new ANSI types to Colorama.
+We are only interested in converting ANSI codes to win32 API calls, not
+shortcuts like the above to generate ANSI characters.
+
+.. code-block:: python
+
+ from colorama import just_fix_windows_console
+ from termcolor import colored
+
+ # use Colorama to make Termcolor work on Windows too
+ just_fix_windows_console()
+
+ # then use Termcolor for all colored text output
+ print(colored('Hello, World!', 'green', 'on_red'))
+
+Available formatting constants are::
+
+ Fore: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET.
+ Back: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET.
+ Style: DIM, NORMAL, BRIGHT, RESET_ALL
+
+``Style.RESET_ALL`` resets foreground, background, and brightness. Colorama will
+perform this reset automatically on program exit.
+
+These are fairly well supported, but not part of the standard::
+
+ Fore: LIGHTBLACK_EX, LIGHTRED_EX, LIGHTGREEN_EX, LIGHTYELLOW_EX, LIGHTBLUE_EX, LIGHTMAGENTA_EX, LIGHTCYAN_EX, LIGHTWHITE_EX
+ Back: LIGHTBLACK_EX, LIGHTRED_EX, LIGHTGREEN_EX, LIGHTYELLOW_EX, LIGHTBLUE_EX, LIGHTMAGENTA_EX, LIGHTCYAN_EX, LIGHTWHITE_EX
+
+Cursor Positioning
+..................
+
+ANSI codes to reposition the cursor are supported. See ``demos/demo06.py`` for
+an example of how to generate them.
+
+Init Keyword Args
+.................
+
+``init()`` accepts some ``**kwargs`` to override default behaviour.
+
+init(autoreset=False):
+ If you find yourself repeatedly sending reset sequences to turn off color
+ changes at the end of every print, then ``init(autoreset=True)`` will
+ automate that:
+
+ .. code-block:: python
+
+ from colorama import init
+ init(autoreset=True)
+ print(Fore.RED + 'some red text')
+ print('automatically back to default color again')
+
+init(strip=None):
+ Pass ``True`` or ``False`` to override whether ANSI codes should be
+ stripped from the output. The default behaviour is to strip if on Windows
+ or if output is redirected (not a tty).
+
+init(convert=None):
+ Pass ``True`` or ``False`` to override whether to convert ANSI codes in the
+ output into win32 calls. The default behaviour is to convert if on Windows
+ and output is to a tty (terminal).
+
+init(wrap=True):
+ On Windows, Colorama works by replacing ``sys.stdout`` and ``sys.stderr``
+ with proxy objects, which override the ``.write()`` method to do their work.
+ If this wrapping causes you problems, then this can be disabled by passing
+ ``init(wrap=False)``. The default behaviour is to wrap if ``autoreset`` or
+ ``strip`` or ``convert`` are True.
+
+ When wrapping is disabled, colored printing on non-Windows platforms will
+ continue to work as normal. To do cross-platform colored output, you can
+ use Colorama's ``AnsiToWin32`` proxy directly:
+
+ .. code-block:: python
+
+ import sys
+ from colorama import init, AnsiToWin32
+ init(wrap=False)
+ stream = AnsiToWin32(sys.stderr).stream
+
+ # Python 2
+ print >>stream, Fore.BLUE + 'blue text on stderr'
+
+ # Python 3
+ print(Fore.BLUE + 'blue text on stderr', file=stream)
+
+Recognised ANSI Sequences
+.........................
+
+ANSI sequences generally take the form::
+
+ ESC [ ; ...
+
+Where ```` is an integer, and ```` is a single letter. Zero or
+more params are passed to a ````. If no params are passed, it is
+generally synonymous with passing a single zero. No spaces exist in the
+sequence; they have been inserted here simply to read more easily.
+
+The only ANSI sequences that Colorama converts into win32 calls are::
+
+ ESC [ 0 m # reset all (colors and brightness)
+ ESC [ 1 m # bright
+ ESC [ 2 m # dim (looks same as normal brightness)
+ ESC [ 22 m # normal brightness
+
+ # FOREGROUND:
+ ESC [ 30 m # black
+ ESC [ 31 m # red
+ ESC [ 32 m # green
+ ESC [ 33 m # yellow
+ ESC [ 34 m # blue
+ ESC [ 35 m # magenta
+ ESC [ 36 m # cyan
+ ESC [ 37 m # white
+ ESC [ 39 m # reset
+
+ # BACKGROUND
+ ESC [ 40 m # black
+ ESC [ 41 m # red
+ ESC [ 42 m # green
+ ESC [ 43 m # yellow
+ ESC [ 44 m # blue
+ ESC [ 45 m # magenta
+ ESC [ 46 m # cyan
+ ESC [ 47 m # white
+ ESC [ 49 m # reset
+
+ # cursor positioning
+ ESC [ y;x H # position cursor at x across, y down
+ ESC [ y;x f # position cursor at x across, y down
+ ESC [ n A # move cursor n lines up
+ ESC [ n B # move cursor n lines down
+ ESC [ n C # move cursor n characters forward
+ ESC [ n D # move cursor n characters backward
+
+ # clear the screen
+ ESC [ mode J # clear the screen
+
+ # clear the line
+ ESC [ mode K # clear the line
+
+Multiple numeric params to the ``'m'`` command can be combined into a single
+sequence::
+
+ ESC [ 36 ; 45 ; 1 m # bright cyan text on magenta background
+
+All other ANSI sequences of the form ``ESC [ ; ... ``
+are silently stripped from the output on Windows.
+
+Any other form of ANSI sequence, such as single-character codes or alternative
+initial characters, are not recognised or stripped. It would be cool to add
+them though. Let me know if it would be useful for you, via the Issues on
+GitHub.
+
+Status & Known Problems
+-----------------------
+
+I've personally only tested it on Windows XP (CMD, Console2), Ubuntu
+(gnome-terminal, xterm), and OS X.
+
+Some valid ANSI sequences aren't recognised.
+
+If you're hacking on the code, see `README-hacking.md`_. ESPECIALLY, see the
+explanation there of why we do not want PRs that allow Colorama to generate new
+types of ANSI codes.
+
+See outstanding issues and wish-list:
+https://github.com/tartley/colorama/issues
+
+If anything doesn't work for you, or doesn't do what you expected or hoped for,
+I'd love to hear about it on that issues list, would be delighted by patches,
+and would be happy to grant commit access to anyone who submits a working patch
+or two.
+
+.. _README-hacking.md: README-hacking.md
+
+License
+-------
+
+Copyright Jonathan Hartley & Arnon Yaari, 2013-2020. BSD 3-Clause license; see
+LICENSE file.
+
+Professional support
+--------------------
+
+.. |tideliftlogo| image:: https://cdn2.hubspot.net/hubfs/4008838/website/logos/logos_for_download/Tidelift_primary-shorthand-logo.png
+ :alt: Tidelift
+ :target: https://tidelift.com/subscription/pkg/pypi-colorama?utm_source=pypi-colorama&utm_medium=referral&utm_campaign=readme
+
+.. list-table::
+ :widths: 10 100
+
+ * - |tideliftlogo|
+ - Professional support for colorama is available as part of the
+ `Tidelift Subscription`_.
+ Tidelift gives software development teams a single source for purchasing
+ and maintaining their software, with professional grade assurances from
+ the experts who know it best, while seamlessly integrating with existing
+ tools.
+
+.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-colorama?utm_source=pypi-colorama&utm_medium=referral&utm_campaign=readme
+
+Thanks
+------
+
+See the CHANGELOG for more thanks!
+
+* Marc Schlaich (schlamar) for a ``setup.py`` fix for Python2.5.
+* Marc Abramowitz, reported & fixed a crash on exit with closed ``stdout``,
+ providing a solution to issue #7's setuptools/distutils debate,
+ and other fixes.
+* User 'eryksun', for guidance on correctly instantiating ``ctypes.windll``.
+* Matthew McCormick for politely pointing out a longstanding crash on non-Win.
+* Ben Hoyt, for a magnificent fix under 64-bit Windows.
+* Jesse at Empty Square for submitting a fix for examples in the README.
+* User 'jamessp', an observant documentation fix for cursor positioning.
+* User 'vaal1239', Dave Mckee & Lackner Kristof for a tiny but much-needed Win7
+ fix.
+* Julien Stuyck, for wisely suggesting Python3 compatible updates to README.
+* Daniel Griffith for multiple fabulous patches.
+* Oscar Lesta for a valuable fix to stop ANSI chars being sent to non-tty
+ output.
+* Roger Binns, for many suggestions, valuable feedback, & bug reports.
+* Tim Golden for thought and much appreciated feedback on the initial idea.
+* User 'Zearin' for updates to the README file.
+* John Szakmeister for adding support for light colors
+* Charles Merriam for adding documentation to demos
+* Jurko for a fix on 64-bit Windows CPython2.5 w/o ctypes
+* Florian Bruhin for a fix when stdout or stderr are None
+* Thomas Weininger for fixing ValueError on Windows
+* Remi Rampin for better Github integration and fixes to the README file
+* Simeon Visser for closing a file handle using 'with' and updating classifiers
+ to include Python 3.3 and 3.4
+* Andy Neff for fixing RESET of LIGHT_EX colors.
+* Jonathan Hartley for the initial idea and implementation.
diff --git a/venv/Lib/site-packages/colorama-0.4.6.dist-info/RECORD b/venv/Lib/site-packages/colorama-0.4.6.dist-info/RECORD
new file mode 100644
index 0000000..8c5f12d
--- /dev/null
+++ b/venv/Lib/site-packages/colorama-0.4.6.dist-info/RECORD
@@ -0,0 +1,31 @@
+colorama-0.4.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+colorama-0.4.6.dist-info/METADATA,sha256=e67SnrUMOym9sz_4TjF3vxvAV4T3aF7NyqRHHH3YEMw,17158
+colorama-0.4.6.dist-info/RECORD,,
+colorama-0.4.6.dist-info/WHEEL,sha256=cdcF4Fbd0FPtw2EMIOwH-3rSOTUdTCeOSXRMD1iLUb8,105
+colorama-0.4.6.dist-info/licenses/LICENSE.txt,sha256=ysNcAmhuXQSlpxQL-zs25zrtSWZW6JEQLkKIhteTAxg,1491
+colorama/__init__.py,sha256=wePQA4U20tKgYARySLEC047ucNX-g8pRLpYBuiHlLb8,266
+colorama/__pycache__/__init__.cpython-310.pyc,,
+colorama/__pycache__/ansi.cpython-310.pyc,,
+colorama/__pycache__/ansitowin32.cpython-310.pyc,,
+colorama/__pycache__/initialise.cpython-310.pyc,,
+colorama/__pycache__/win32.cpython-310.pyc,,
+colorama/__pycache__/winterm.cpython-310.pyc,,
+colorama/ansi.py,sha256=Top4EeEuaQdBWdteKMEcGOTeKeF19Q-Wo_6_Cj5kOzQ,2522
+colorama/ansitowin32.py,sha256=vPNYa3OZbxjbuFyaVo0Tmhmy1FZ1lKMWCnT7odXpItk,11128
+colorama/initialise.py,sha256=-hIny86ClXo39ixh5iSCfUIa2f_h_bgKRDW7gqs-KLU,3325
+colorama/tests/__init__.py,sha256=MkgPAEzGQd-Rq0w0PZXSX2LadRWhUECcisJY8lSrm4Q,75
+colorama/tests/__pycache__/__init__.cpython-310.pyc,,
+colorama/tests/__pycache__/ansi_test.cpython-310.pyc,,
+colorama/tests/__pycache__/ansitowin32_test.cpython-310.pyc,,
+colorama/tests/__pycache__/initialise_test.cpython-310.pyc,,
+colorama/tests/__pycache__/isatty_test.cpython-310.pyc,,
+colorama/tests/__pycache__/utils.cpython-310.pyc,,
+colorama/tests/__pycache__/winterm_test.cpython-310.pyc,,
+colorama/tests/ansi_test.py,sha256=FeViDrUINIZcr505PAxvU4AjXz1asEiALs9GXMhwRaE,2839
+colorama/tests/ansitowin32_test.py,sha256=RN7AIhMJ5EqDsYaCjVo-o4u8JzDD4ukJbmevWKS70rY,10678
+colorama/tests/initialise_test.py,sha256=BbPy-XfyHwJ6zKozuQOvNvQZzsx9vdb_0bYXn7hsBTc,6741
+colorama/tests/isatty_test.py,sha256=Pg26LRpv0yQDB5Ac-sxgVXG7hsA1NYvapFgApZfYzZg,1866
+colorama/tests/utils.py,sha256=1IIRylG39z5-dzq09R_ngufxyPZxgldNbrxKxUGwGKE,1079
+colorama/tests/winterm_test.py,sha256=qoWFPEjym5gm2RuMwpf3pOis3a5r_PJZFCzK254JL8A,3709
+colorama/win32.py,sha256=YQOKwMTwtGBbsY4dL5HYTvwTeP9wIQra5MvPNddpxZs,6181
+colorama/winterm.py,sha256=XCQFDHjPi6AHYNdZwy0tA02H-Jh48Jp-HvCjeLeLp3U,7134
diff --git a/venv/Lib/site-packages/colorama-0.4.6.dist-info/WHEEL b/venv/Lib/site-packages/colorama-0.4.6.dist-info/WHEEL
new file mode 100644
index 0000000..d79189f
--- /dev/null
+++ b/venv/Lib/site-packages/colorama-0.4.6.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: hatchling 1.11.1
+Root-Is-Purelib: true
+Tag: py2-none-any
+Tag: py3-none-any
diff --git a/venv/Lib/site-packages/colorama-0.4.6.dist-info/licenses/LICENSE.txt b/venv/Lib/site-packages/colorama-0.4.6.dist-info/licenses/LICENSE.txt
new file mode 100644
index 0000000..3105888
--- /dev/null
+++ b/venv/Lib/site-packages/colorama-0.4.6.dist-info/licenses/LICENSE.txt
@@ -0,0 +1,27 @@
+Copyright (c) 2010 Jonathan Hartley
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holders, nor those of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/venv/Lib/site-packages/colorama/__init__.py b/venv/Lib/site-packages/colorama/__init__.py
new file mode 100644
index 0000000..383101c
--- /dev/null
+++ b/venv/Lib/site-packages/colorama/__init__.py
@@ -0,0 +1,7 @@
+# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
+from .initialise import init, deinit, reinit, colorama_text, just_fix_windows_console
+from .ansi import Fore, Back, Style, Cursor
+from .ansitowin32 import AnsiToWin32
+
+__version__ = '0.4.6'
+
diff --git a/venv/Lib/site-packages/colorama/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/colorama/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..a48ecbd
Binary files /dev/null and b/venv/Lib/site-packages/colorama/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/colorama/__pycache__/ansi.cpython-310.pyc b/venv/Lib/site-packages/colorama/__pycache__/ansi.cpython-310.pyc
new file mode 100644
index 0000000..2061bc9
Binary files /dev/null and b/venv/Lib/site-packages/colorama/__pycache__/ansi.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/colorama/__pycache__/ansitowin32.cpython-310.pyc b/venv/Lib/site-packages/colorama/__pycache__/ansitowin32.cpython-310.pyc
new file mode 100644
index 0000000..96d0a6a
Binary files /dev/null and b/venv/Lib/site-packages/colorama/__pycache__/ansitowin32.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/colorama/__pycache__/initialise.cpython-310.pyc b/venv/Lib/site-packages/colorama/__pycache__/initialise.cpython-310.pyc
new file mode 100644
index 0000000..b6391f2
Binary files /dev/null and b/venv/Lib/site-packages/colorama/__pycache__/initialise.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/colorama/__pycache__/win32.cpython-310.pyc b/venv/Lib/site-packages/colorama/__pycache__/win32.cpython-310.pyc
new file mode 100644
index 0000000..8ae41cc
Binary files /dev/null and b/venv/Lib/site-packages/colorama/__pycache__/win32.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/colorama/__pycache__/winterm.cpython-310.pyc b/venv/Lib/site-packages/colorama/__pycache__/winterm.cpython-310.pyc
new file mode 100644
index 0000000..949e9c2
Binary files /dev/null and b/venv/Lib/site-packages/colorama/__pycache__/winterm.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/colorama/ansi.py b/venv/Lib/site-packages/colorama/ansi.py
new file mode 100644
index 0000000..11ec695
--- /dev/null
+++ b/venv/Lib/site-packages/colorama/ansi.py
@@ -0,0 +1,102 @@
+# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
+'''
+This module generates ANSI character codes to printing colors to terminals.
+See: http://en.wikipedia.org/wiki/ANSI_escape_code
+'''
+
+CSI = '\033['
+OSC = '\033]'
+BEL = '\a'
+
+
+def code_to_chars(code):
+ return CSI + str(code) + 'm'
+
+def set_title(title):
+ return OSC + '2;' + title + BEL
+
+def clear_screen(mode=2):
+ return CSI + str(mode) + 'J'
+
+def clear_line(mode=2):
+ return CSI + str(mode) + 'K'
+
+
+class AnsiCodes(object):
+ def __init__(self):
+ # the subclasses declare class attributes which are numbers.
+ # Upon instantiation we define instance attributes, which are the same
+ # as the class attributes but wrapped with the ANSI escape sequence
+ for name in dir(self):
+ if not name.startswith('_'):
+ value = getattr(self, name)
+ setattr(self, name, code_to_chars(value))
+
+
+class AnsiCursor(object):
+ def UP(self, n=1):
+ return CSI + str(n) + 'A'
+ def DOWN(self, n=1):
+ return CSI + str(n) + 'B'
+ def FORWARD(self, n=1):
+ return CSI + str(n) + 'C'
+ def BACK(self, n=1):
+ return CSI + str(n) + 'D'
+ def POS(self, x=1, y=1):
+ return CSI + str(y) + ';' + str(x) + 'H'
+
+
+class AnsiFore(AnsiCodes):
+ BLACK = 30
+ RED = 31
+ GREEN = 32
+ YELLOW = 33
+ BLUE = 34
+ MAGENTA = 35
+ CYAN = 36
+ WHITE = 37
+ RESET = 39
+
+ # These are fairly well supported, but not part of the standard.
+ LIGHTBLACK_EX = 90
+ LIGHTRED_EX = 91
+ LIGHTGREEN_EX = 92
+ LIGHTYELLOW_EX = 93
+ LIGHTBLUE_EX = 94
+ LIGHTMAGENTA_EX = 95
+ LIGHTCYAN_EX = 96
+ LIGHTWHITE_EX = 97
+
+
+class AnsiBack(AnsiCodes):
+ BLACK = 40
+ RED = 41
+ GREEN = 42
+ YELLOW = 43
+ BLUE = 44
+ MAGENTA = 45
+ CYAN = 46
+ WHITE = 47
+ RESET = 49
+
+ # These are fairly well supported, but not part of the standard.
+ LIGHTBLACK_EX = 100
+ LIGHTRED_EX = 101
+ LIGHTGREEN_EX = 102
+ LIGHTYELLOW_EX = 103
+ LIGHTBLUE_EX = 104
+ LIGHTMAGENTA_EX = 105
+ LIGHTCYAN_EX = 106
+ LIGHTWHITE_EX = 107
+
+
+class AnsiStyle(AnsiCodes):
+ BRIGHT = 1
+ DIM = 2
+ NORMAL = 22
+ RESET_ALL = 0
+
+Fore = AnsiFore()
+Back = AnsiBack()
+Style = AnsiStyle()
+Cursor = AnsiCursor()
diff --git a/venv/Lib/site-packages/colorama/ansitowin32.py b/venv/Lib/site-packages/colorama/ansitowin32.py
new file mode 100644
index 0000000..abf209e
--- /dev/null
+++ b/venv/Lib/site-packages/colorama/ansitowin32.py
@@ -0,0 +1,277 @@
+# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
+import re
+import sys
+import os
+
+from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style, BEL
+from .winterm import enable_vt_processing, WinTerm, WinColor, WinStyle
+from .win32 import windll, winapi_test
+
+
+winterm = None
+if windll is not None:
+ winterm = WinTerm()
+
+
+class StreamWrapper(object):
+ '''
+ Wraps a stream (such as stdout), acting as a transparent proxy for all
+ attribute access apart from method 'write()', which is delegated to our
+ Converter instance.
+ '''
+ def __init__(self, wrapped, converter):
+ # double-underscore everything to prevent clashes with names of
+ # attributes on the wrapped stream object.
+ self.__wrapped = wrapped
+ self.__convertor = converter
+
+ def __getattr__(self, name):
+ return getattr(self.__wrapped, name)
+
+ def __enter__(self, *args, **kwargs):
+ # special method lookup bypasses __getattr__/__getattribute__, see
+ # https://stackoverflow.com/questions/12632894/why-doesnt-getattr-work-with-exit
+ # thus, contextlib magic methods are not proxied via __getattr__
+ return self.__wrapped.__enter__(*args, **kwargs)
+
+ def __exit__(self, *args, **kwargs):
+ return self.__wrapped.__exit__(*args, **kwargs)
+
+ def __setstate__(self, state):
+ self.__dict__ = state
+
+ def __getstate__(self):
+ return self.__dict__
+
+ def write(self, text):
+ self.__convertor.write(text)
+
+ def isatty(self):
+ stream = self.__wrapped
+ if 'PYCHARM_HOSTED' in os.environ:
+ if stream is not None and (stream is sys.__stdout__ or stream is sys.__stderr__):
+ return True
+ try:
+ stream_isatty = stream.isatty
+ except AttributeError:
+ return False
+ else:
+ return stream_isatty()
+
+ @property
+ def closed(self):
+ stream = self.__wrapped
+ try:
+ return stream.closed
+ # AttributeError in the case that the stream doesn't support being closed
+ # ValueError for the case that the stream has already been detached when atexit runs
+ except (AttributeError, ValueError):
+ return True
+
+
+class AnsiToWin32(object):
+ '''
+ Implements a 'write()' method which, on Windows, will strip ANSI character
+ sequences from the text, and if outputting to a tty, will convert them into
+ win32 function calls.
+ '''
+ ANSI_CSI_RE = re.compile('\001?\033\\[((?:\\d|;)*)([a-zA-Z])\002?') # Control Sequence Introducer
+ ANSI_OSC_RE = re.compile('\001?\033\\]([^\a]*)(\a)\002?') # Operating System Command
+
+ def __init__(self, wrapped, convert=None, strip=None, autoreset=False):
+ # The wrapped stream (normally sys.stdout or sys.stderr)
+ self.wrapped = wrapped
+
+ # should we reset colors to defaults after every .write()
+ self.autoreset = autoreset
+
+ # create the proxy wrapping our output stream
+ self.stream = StreamWrapper(wrapped, self)
+
+ on_windows = os.name == 'nt'
+ # We test if the WinAPI works, because even if we are on Windows
+ # we may be using a terminal that doesn't support the WinAPI
+ # (e.g. Cygwin Terminal). In this case it's up to the terminal
+ # to support the ANSI codes.
+ conversion_supported = on_windows and winapi_test()
+ try:
+ fd = wrapped.fileno()
+ except Exception:
+ fd = -1
+ system_has_native_ansi = not on_windows or enable_vt_processing(fd)
+ have_tty = not self.stream.closed and self.stream.isatty()
+ need_conversion = conversion_supported and not system_has_native_ansi
+
+ # should we strip ANSI sequences from our output?
+ if strip is None:
+ strip = need_conversion or not have_tty
+ self.strip = strip
+
+ # should we should convert ANSI sequences into win32 calls?
+ if convert is None:
+ convert = need_conversion and have_tty
+ self.convert = convert
+
+ # dict of ansi codes to win32 functions and parameters
+ self.win32_calls = self.get_win32_calls()
+
+ # are we wrapping stderr?
+ self.on_stderr = self.wrapped is sys.stderr
+
+ def should_wrap(self):
+ '''
+ True if this class is actually needed. If false, then the output
+ stream will not be affected, nor will win32 calls be issued, so
+ wrapping stdout is not actually required. This will generally be
+ False on non-Windows platforms, unless optional functionality like
+ autoreset has been requested using kwargs to init()
+ '''
+ return self.convert or self.strip or self.autoreset
+
+ def get_win32_calls(self):
+ if self.convert and winterm:
+ return {
+ AnsiStyle.RESET_ALL: (winterm.reset_all, ),
+ AnsiStyle.BRIGHT: (winterm.style, WinStyle.BRIGHT),
+ AnsiStyle.DIM: (winterm.style, WinStyle.NORMAL),
+ AnsiStyle.NORMAL: (winterm.style, WinStyle.NORMAL),
+ AnsiFore.BLACK: (winterm.fore, WinColor.BLACK),
+ AnsiFore.RED: (winterm.fore, WinColor.RED),
+ AnsiFore.GREEN: (winterm.fore, WinColor.GREEN),
+ AnsiFore.YELLOW: (winterm.fore, WinColor.YELLOW),
+ AnsiFore.BLUE: (winterm.fore, WinColor.BLUE),
+ AnsiFore.MAGENTA: (winterm.fore, WinColor.MAGENTA),
+ AnsiFore.CYAN: (winterm.fore, WinColor.CYAN),
+ AnsiFore.WHITE: (winterm.fore, WinColor.GREY),
+ AnsiFore.RESET: (winterm.fore, ),
+ AnsiFore.LIGHTBLACK_EX: (winterm.fore, WinColor.BLACK, True),
+ AnsiFore.LIGHTRED_EX: (winterm.fore, WinColor.RED, True),
+ AnsiFore.LIGHTGREEN_EX: (winterm.fore, WinColor.GREEN, True),
+ AnsiFore.LIGHTYELLOW_EX: (winterm.fore, WinColor.YELLOW, True),
+ AnsiFore.LIGHTBLUE_EX: (winterm.fore, WinColor.BLUE, True),
+ AnsiFore.LIGHTMAGENTA_EX: (winterm.fore, WinColor.MAGENTA, True),
+ AnsiFore.LIGHTCYAN_EX: (winterm.fore, WinColor.CYAN, True),
+ AnsiFore.LIGHTWHITE_EX: (winterm.fore, WinColor.GREY, True),
+ AnsiBack.BLACK: (winterm.back, WinColor.BLACK),
+ AnsiBack.RED: (winterm.back, WinColor.RED),
+ AnsiBack.GREEN: (winterm.back, WinColor.GREEN),
+ AnsiBack.YELLOW: (winterm.back, WinColor.YELLOW),
+ AnsiBack.BLUE: (winterm.back, WinColor.BLUE),
+ AnsiBack.MAGENTA: (winterm.back, WinColor.MAGENTA),
+ AnsiBack.CYAN: (winterm.back, WinColor.CYAN),
+ AnsiBack.WHITE: (winterm.back, WinColor.GREY),
+ AnsiBack.RESET: (winterm.back, ),
+ AnsiBack.LIGHTBLACK_EX: (winterm.back, WinColor.BLACK, True),
+ AnsiBack.LIGHTRED_EX: (winterm.back, WinColor.RED, True),
+ AnsiBack.LIGHTGREEN_EX: (winterm.back, WinColor.GREEN, True),
+ AnsiBack.LIGHTYELLOW_EX: (winterm.back, WinColor.YELLOW, True),
+ AnsiBack.LIGHTBLUE_EX: (winterm.back, WinColor.BLUE, True),
+ AnsiBack.LIGHTMAGENTA_EX: (winterm.back, WinColor.MAGENTA, True),
+ AnsiBack.LIGHTCYAN_EX: (winterm.back, WinColor.CYAN, True),
+ AnsiBack.LIGHTWHITE_EX: (winterm.back, WinColor.GREY, True),
+ }
+ return dict()
+
+ def write(self, text):
+ if self.strip or self.convert:
+ self.write_and_convert(text)
+ else:
+ self.wrapped.write(text)
+ self.wrapped.flush()
+ if self.autoreset:
+ self.reset_all()
+
+
+ def reset_all(self):
+ if self.convert:
+ self.call_win32('m', (0,))
+ elif not self.strip and not self.stream.closed:
+ self.wrapped.write(Style.RESET_ALL)
+
+
+ def write_and_convert(self, text):
+ '''
+ Write the given text to our wrapped stream, stripping any ANSI
+ sequences from the text, and optionally converting them into win32
+ calls.
+ '''
+ cursor = 0
+ text = self.convert_osc(text)
+ for match in self.ANSI_CSI_RE.finditer(text):
+ start, end = match.span()
+ self.write_plain_text(text, cursor, start)
+ self.convert_ansi(*match.groups())
+ cursor = end
+ self.write_plain_text(text, cursor, len(text))
+
+
+ def write_plain_text(self, text, start, end):
+ if start < end:
+ self.wrapped.write(text[start:end])
+ self.wrapped.flush()
+
+
+ def convert_ansi(self, paramstring, command):
+ if self.convert:
+ params = self.extract_params(command, paramstring)
+ self.call_win32(command, params)
+
+
+ def extract_params(self, command, paramstring):
+ if command in 'Hf':
+ params = tuple(int(p) if len(p) != 0 else 1 for p in paramstring.split(';'))
+ while len(params) < 2:
+ # defaults:
+ params = params + (1,)
+ else:
+ params = tuple(int(p) for p in paramstring.split(';') if len(p) != 0)
+ if len(params) == 0:
+ # defaults:
+ if command in 'JKm':
+ params = (0,)
+ elif command in 'ABCD':
+ params = (1,)
+
+ return params
+
+
+ def call_win32(self, command, params):
+ if command == 'm':
+ for param in params:
+ if param in self.win32_calls:
+ func_args = self.win32_calls[param]
+ func = func_args[0]
+ args = func_args[1:]
+ kwargs = dict(on_stderr=self.on_stderr)
+ func(*args, **kwargs)
+ elif command in 'J':
+ winterm.erase_screen(params[0], on_stderr=self.on_stderr)
+ elif command in 'K':
+ winterm.erase_line(params[0], on_stderr=self.on_stderr)
+ elif command in 'Hf': # cursor position - absolute
+ winterm.set_cursor_position(params, on_stderr=self.on_stderr)
+ elif command in 'ABCD': # cursor position - relative
+ n = params[0]
+ # A - up, B - down, C - forward, D - back
+ x, y = {'A': (0, -n), 'B': (0, n), 'C': (n, 0), 'D': (-n, 0)}[command]
+ winterm.cursor_adjust(x, y, on_stderr=self.on_stderr)
+
+
+ def convert_osc(self, text):
+ for match in self.ANSI_OSC_RE.finditer(text):
+ start, end = match.span()
+ text = text[:start] + text[end:]
+ paramstring, command = match.groups()
+ if command == BEL:
+ if paramstring.count(";") == 1:
+ params = paramstring.split(";")
+ # 0 - change title and icon (we will only change title)
+ # 1 - change icon (we don't support this)
+ # 2 - change title
+ if params[0] in '02':
+ winterm.set_title(params[1])
+ return text
+
+
+ def flush(self):
+ self.wrapped.flush()
diff --git a/venv/Lib/site-packages/colorama/initialise.py b/venv/Lib/site-packages/colorama/initialise.py
new file mode 100644
index 0000000..d5fd4b7
--- /dev/null
+++ b/venv/Lib/site-packages/colorama/initialise.py
@@ -0,0 +1,121 @@
+# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
+import atexit
+import contextlib
+import sys
+
+from .ansitowin32 import AnsiToWin32
+
+
+def _wipe_internal_state_for_tests():
+ global orig_stdout, orig_stderr
+ orig_stdout = None
+ orig_stderr = None
+
+ global wrapped_stdout, wrapped_stderr
+ wrapped_stdout = None
+ wrapped_stderr = None
+
+ global atexit_done
+ atexit_done = False
+
+ global fixed_windows_console
+ fixed_windows_console = False
+
+ try:
+ # no-op if it wasn't registered
+ atexit.unregister(reset_all)
+ except AttributeError:
+ # python 2: no atexit.unregister. Oh well, we did our best.
+ pass
+
+
+def reset_all():
+ if AnsiToWin32 is not None: # Issue #74: objects might become None at exit
+ AnsiToWin32(orig_stdout).reset_all()
+
+
+def init(autoreset=False, convert=None, strip=None, wrap=True):
+
+ if not wrap and any([autoreset, convert, strip]):
+ raise ValueError('wrap=False conflicts with any other arg=True')
+
+ global wrapped_stdout, wrapped_stderr
+ global orig_stdout, orig_stderr
+
+ orig_stdout = sys.stdout
+ orig_stderr = sys.stderr
+
+ if sys.stdout is None:
+ wrapped_stdout = None
+ else:
+ sys.stdout = wrapped_stdout = \
+ wrap_stream(orig_stdout, convert, strip, autoreset, wrap)
+ if sys.stderr is None:
+ wrapped_stderr = None
+ else:
+ sys.stderr = wrapped_stderr = \
+ wrap_stream(orig_stderr, convert, strip, autoreset, wrap)
+
+ global atexit_done
+ if not atexit_done:
+ atexit.register(reset_all)
+ atexit_done = True
+
+
+def deinit():
+ if orig_stdout is not None:
+ sys.stdout = orig_stdout
+ if orig_stderr is not None:
+ sys.stderr = orig_stderr
+
+
+def just_fix_windows_console():
+ global fixed_windows_console
+
+ if sys.platform != "win32":
+ return
+ if fixed_windows_console:
+ return
+ if wrapped_stdout is not None or wrapped_stderr is not None:
+ # Someone already ran init() and it did stuff, so we won't second-guess them
+ return
+
+ # On newer versions of Windows, AnsiToWin32.__init__ will implicitly enable the
+ # native ANSI support in the console as a side-effect. We only need to actually
+ # replace sys.stdout/stderr if we're in the old-style conversion mode.
+ new_stdout = AnsiToWin32(sys.stdout, convert=None, strip=None, autoreset=False)
+ if new_stdout.convert:
+ sys.stdout = new_stdout
+ new_stderr = AnsiToWin32(sys.stderr, convert=None, strip=None, autoreset=False)
+ if new_stderr.convert:
+ sys.stderr = new_stderr
+
+ fixed_windows_console = True
+
+@contextlib.contextmanager
+def colorama_text(*args, **kwargs):
+ init(*args, **kwargs)
+ try:
+ yield
+ finally:
+ deinit()
+
+
+def reinit():
+ if wrapped_stdout is not None:
+ sys.stdout = wrapped_stdout
+ if wrapped_stderr is not None:
+ sys.stderr = wrapped_stderr
+
+
+def wrap_stream(stream, convert, strip, autoreset, wrap):
+ if wrap:
+ wrapper = AnsiToWin32(stream,
+ convert=convert, strip=strip, autoreset=autoreset)
+ if wrapper.should_wrap():
+ stream = wrapper.stream
+ return stream
+
+
+# Use this for initial setup as well, to reduce code duplication
+_wipe_internal_state_for_tests()
diff --git a/venv/Lib/site-packages/colorama/tests/__init__.py b/venv/Lib/site-packages/colorama/tests/__init__.py
new file mode 100644
index 0000000..8c5661e
--- /dev/null
+++ b/venv/Lib/site-packages/colorama/tests/__init__.py
@@ -0,0 +1 @@
+# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
diff --git a/venv/Lib/site-packages/colorama/tests/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/colorama/tests/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..1b75589
Binary files /dev/null and b/venv/Lib/site-packages/colorama/tests/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/colorama/tests/__pycache__/ansi_test.cpython-310.pyc b/venv/Lib/site-packages/colorama/tests/__pycache__/ansi_test.cpython-310.pyc
new file mode 100644
index 0000000..e8c56df
Binary files /dev/null and b/venv/Lib/site-packages/colorama/tests/__pycache__/ansi_test.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/colorama/tests/__pycache__/ansitowin32_test.cpython-310.pyc b/venv/Lib/site-packages/colorama/tests/__pycache__/ansitowin32_test.cpython-310.pyc
new file mode 100644
index 0000000..f9a0d2b
Binary files /dev/null and b/venv/Lib/site-packages/colorama/tests/__pycache__/ansitowin32_test.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/colorama/tests/__pycache__/initialise_test.cpython-310.pyc b/venv/Lib/site-packages/colorama/tests/__pycache__/initialise_test.cpython-310.pyc
new file mode 100644
index 0000000..07de042
Binary files /dev/null and b/venv/Lib/site-packages/colorama/tests/__pycache__/initialise_test.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/colorama/tests/__pycache__/isatty_test.cpython-310.pyc b/venv/Lib/site-packages/colorama/tests/__pycache__/isatty_test.cpython-310.pyc
new file mode 100644
index 0000000..4d4daef
Binary files /dev/null and b/venv/Lib/site-packages/colorama/tests/__pycache__/isatty_test.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/colorama/tests/__pycache__/utils.cpython-310.pyc b/venv/Lib/site-packages/colorama/tests/__pycache__/utils.cpython-310.pyc
new file mode 100644
index 0000000..909814d
Binary files /dev/null and b/venv/Lib/site-packages/colorama/tests/__pycache__/utils.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/colorama/tests/__pycache__/winterm_test.cpython-310.pyc b/venv/Lib/site-packages/colorama/tests/__pycache__/winterm_test.cpython-310.pyc
new file mode 100644
index 0000000..e61e93b
Binary files /dev/null and b/venv/Lib/site-packages/colorama/tests/__pycache__/winterm_test.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/colorama/tests/ansi_test.py b/venv/Lib/site-packages/colorama/tests/ansi_test.py
new file mode 100644
index 0000000..0a20c80
--- /dev/null
+++ b/venv/Lib/site-packages/colorama/tests/ansi_test.py
@@ -0,0 +1,76 @@
+# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
+import sys
+from unittest import TestCase, main
+
+from ..ansi import Back, Fore, Style
+from ..ansitowin32 import AnsiToWin32
+
+stdout_orig = sys.stdout
+stderr_orig = sys.stderr
+
+
+class AnsiTest(TestCase):
+
+ def setUp(self):
+ # sanity check: stdout should be a file or StringIO object.
+ # It will only be AnsiToWin32 if init() has previously wrapped it
+ self.assertNotEqual(type(sys.stdout), AnsiToWin32)
+ self.assertNotEqual(type(sys.stderr), AnsiToWin32)
+
+ def tearDown(self):
+ sys.stdout = stdout_orig
+ sys.stderr = stderr_orig
+
+
+ def testForeAttributes(self):
+ self.assertEqual(Fore.BLACK, '\033[30m')
+ self.assertEqual(Fore.RED, '\033[31m')
+ self.assertEqual(Fore.GREEN, '\033[32m')
+ self.assertEqual(Fore.YELLOW, '\033[33m')
+ self.assertEqual(Fore.BLUE, '\033[34m')
+ self.assertEqual(Fore.MAGENTA, '\033[35m')
+ self.assertEqual(Fore.CYAN, '\033[36m')
+ self.assertEqual(Fore.WHITE, '\033[37m')
+ self.assertEqual(Fore.RESET, '\033[39m')
+
+ # Check the light, extended versions.
+ self.assertEqual(Fore.LIGHTBLACK_EX, '\033[90m')
+ self.assertEqual(Fore.LIGHTRED_EX, '\033[91m')
+ self.assertEqual(Fore.LIGHTGREEN_EX, '\033[92m')
+ self.assertEqual(Fore.LIGHTYELLOW_EX, '\033[93m')
+ self.assertEqual(Fore.LIGHTBLUE_EX, '\033[94m')
+ self.assertEqual(Fore.LIGHTMAGENTA_EX, '\033[95m')
+ self.assertEqual(Fore.LIGHTCYAN_EX, '\033[96m')
+ self.assertEqual(Fore.LIGHTWHITE_EX, '\033[97m')
+
+
+ def testBackAttributes(self):
+ self.assertEqual(Back.BLACK, '\033[40m')
+ self.assertEqual(Back.RED, '\033[41m')
+ self.assertEqual(Back.GREEN, '\033[42m')
+ self.assertEqual(Back.YELLOW, '\033[43m')
+ self.assertEqual(Back.BLUE, '\033[44m')
+ self.assertEqual(Back.MAGENTA, '\033[45m')
+ self.assertEqual(Back.CYAN, '\033[46m')
+ self.assertEqual(Back.WHITE, '\033[47m')
+ self.assertEqual(Back.RESET, '\033[49m')
+
+ # Check the light, extended versions.
+ self.assertEqual(Back.LIGHTBLACK_EX, '\033[100m')
+ self.assertEqual(Back.LIGHTRED_EX, '\033[101m')
+ self.assertEqual(Back.LIGHTGREEN_EX, '\033[102m')
+ self.assertEqual(Back.LIGHTYELLOW_EX, '\033[103m')
+ self.assertEqual(Back.LIGHTBLUE_EX, '\033[104m')
+ self.assertEqual(Back.LIGHTMAGENTA_EX, '\033[105m')
+ self.assertEqual(Back.LIGHTCYAN_EX, '\033[106m')
+ self.assertEqual(Back.LIGHTWHITE_EX, '\033[107m')
+
+
+ def testStyleAttributes(self):
+ self.assertEqual(Style.DIM, '\033[2m')
+ self.assertEqual(Style.NORMAL, '\033[22m')
+ self.assertEqual(Style.BRIGHT, '\033[1m')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/venv/Lib/site-packages/colorama/tests/ansitowin32_test.py b/venv/Lib/site-packages/colorama/tests/ansitowin32_test.py
new file mode 100644
index 0000000..91ca551
--- /dev/null
+++ b/venv/Lib/site-packages/colorama/tests/ansitowin32_test.py
@@ -0,0 +1,294 @@
+# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
+from io import StringIO, TextIOWrapper
+from unittest import TestCase, main
+try:
+ from contextlib import ExitStack
+except ImportError:
+ # python 2
+ from contextlib2 import ExitStack
+
+try:
+ from unittest.mock import MagicMock, Mock, patch
+except ImportError:
+ from mock import MagicMock, Mock, patch
+
+from ..ansitowin32 import AnsiToWin32, StreamWrapper
+from ..win32 import ENABLE_VIRTUAL_TERMINAL_PROCESSING
+from .utils import osname
+
+
+class StreamWrapperTest(TestCase):
+
+ def testIsAProxy(self):
+ mockStream = Mock()
+ wrapper = StreamWrapper(mockStream, None)
+ self.assertTrue( wrapper.random_attr is mockStream.random_attr )
+
+ def testDelegatesWrite(self):
+ mockStream = Mock()
+ mockConverter = Mock()
+ wrapper = StreamWrapper(mockStream, mockConverter)
+ wrapper.write('hello')
+ self.assertTrue(mockConverter.write.call_args, (('hello',), {}))
+
+ def testDelegatesContext(self):
+ mockConverter = Mock()
+ s = StringIO()
+ with StreamWrapper(s, mockConverter) as fp:
+ fp.write(u'hello')
+ self.assertTrue(s.closed)
+
+ def testProxyNoContextManager(self):
+ mockStream = MagicMock()
+ mockStream.__enter__.side_effect = AttributeError()
+ mockConverter = Mock()
+ with self.assertRaises(AttributeError) as excinfo:
+ with StreamWrapper(mockStream, mockConverter) as wrapper:
+ wrapper.write('hello')
+
+ def test_closed_shouldnt_raise_on_closed_stream(self):
+ stream = StringIO()
+ stream.close()
+ wrapper = StreamWrapper(stream, None)
+ self.assertEqual(wrapper.closed, True)
+
+ def test_closed_shouldnt_raise_on_detached_stream(self):
+ stream = TextIOWrapper(StringIO())
+ stream.detach()
+ wrapper = StreamWrapper(stream, None)
+ self.assertEqual(wrapper.closed, True)
+
+class AnsiToWin32Test(TestCase):
+
+ def testInit(self):
+ mockStdout = Mock()
+ auto = Mock()
+ stream = AnsiToWin32(mockStdout, autoreset=auto)
+ self.assertEqual(stream.wrapped, mockStdout)
+ self.assertEqual(stream.autoreset, auto)
+
+ @patch('colorama.ansitowin32.winterm', None)
+ @patch('colorama.ansitowin32.winapi_test', lambda *_: True)
+ def testStripIsTrueOnWindows(self):
+ with osname('nt'):
+ mockStdout = Mock()
+ stream = AnsiToWin32(mockStdout)
+ self.assertTrue(stream.strip)
+
+ def testStripIsFalseOffWindows(self):
+ with osname('posix'):
+ mockStdout = Mock(closed=False)
+ stream = AnsiToWin32(mockStdout)
+ self.assertFalse(stream.strip)
+
+ def testWriteStripsAnsi(self):
+ mockStdout = Mock()
+ stream = AnsiToWin32(mockStdout)
+ stream.wrapped = Mock()
+ stream.write_and_convert = Mock()
+ stream.strip = True
+
+ stream.write('abc')
+
+ self.assertFalse(stream.wrapped.write.called)
+ self.assertEqual(stream.write_and_convert.call_args, (('abc',), {}))
+
+ def testWriteDoesNotStripAnsi(self):
+ mockStdout = Mock()
+ stream = AnsiToWin32(mockStdout)
+ stream.wrapped = Mock()
+ stream.write_and_convert = Mock()
+ stream.strip = False
+ stream.convert = False
+
+ stream.write('abc')
+
+ self.assertFalse(stream.write_and_convert.called)
+ self.assertEqual(stream.wrapped.write.call_args, (('abc',), {}))
+
+ def assert_autoresets(self, convert, autoreset=True):
+ stream = AnsiToWin32(Mock())
+ stream.convert = convert
+ stream.reset_all = Mock()
+ stream.autoreset = autoreset
+ stream.winterm = Mock()
+
+ stream.write('abc')
+
+ self.assertEqual(stream.reset_all.called, autoreset)
+
+ def testWriteAutoresets(self):
+ self.assert_autoresets(convert=True)
+ self.assert_autoresets(convert=False)
+ self.assert_autoresets(convert=True, autoreset=False)
+ self.assert_autoresets(convert=False, autoreset=False)
+
+ def testWriteAndConvertWritesPlainText(self):
+ stream = AnsiToWin32(Mock())
+ stream.write_and_convert( 'abc' )
+ self.assertEqual( stream.wrapped.write.call_args, (('abc',), {}) )
+
+ def testWriteAndConvertStripsAllValidAnsi(self):
+ stream = AnsiToWin32(Mock())
+ stream.call_win32 = Mock()
+ data = [
+ 'abc\033[mdef',
+ 'abc\033[0mdef',
+ 'abc\033[2mdef',
+ 'abc\033[02mdef',
+ 'abc\033[002mdef',
+ 'abc\033[40mdef',
+ 'abc\033[040mdef',
+ 'abc\033[0;1mdef',
+ 'abc\033[40;50mdef',
+ 'abc\033[50;30;40mdef',
+ 'abc\033[Adef',
+ 'abc\033[0Gdef',
+ 'abc\033[1;20;128Hdef',
+ ]
+ for datum in data:
+ stream.wrapped.write.reset_mock()
+ stream.write_and_convert( datum )
+ self.assertEqual(
+ [args[0] for args in stream.wrapped.write.call_args_list],
+ [ ('abc',), ('def',) ]
+ )
+
+ def testWriteAndConvertSkipsEmptySnippets(self):
+ stream = AnsiToWin32(Mock())
+ stream.call_win32 = Mock()
+ stream.write_and_convert( '\033[40m\033[41m' )
+ self.assertFalse( stream.wrapped.write.called )
+
+ def testWriteAndConvertCallsWin32WithParamsAndCommand(self):
+ stream = AnsiToWin32(Mock())
+ stream.convert = True
+ stream.call_win32 = Mock()
+ stream.extract_params = Mock(return_value='params')
+ data = {
+ 'abc\033[adef': ('a', 'params'),
+ 'abc\033[;;bdef': ('b', 'params'),
+ 'abc\033[0cdef': ('c', 'params'),
+ 'abc\033[;;0;;Gdef': ('G', 'params'),
+ 'abc\033[1;20;128Hdef': ('H', 'params'),
+ }
+ for datum, expected in data.items():
+ stream.call_win32.reset_mock()
+ stream.write_and_convert( datum )
+ self.assertEqual( stream.call_win32.call_args[0], expected )
+
+ def test_reset_all_shouldnt_raise_on_closed_orig_stdout(self):
+ stream = StringIO()
+ converter = AnsiToWin32(stream)
+ stream.close()
+
+ converter.reset_all()
+
+ def test_wrap_shouldnt_raise_on_closed_orig_stdout(self):
+ stream = StringIO()
+ stream.close()
+ with \
+ patch("colorama.ansitowin32.os.name", "nt"), \
+ patch("colorama.ansitowin32.winapi_test", lambda: True):
+ converter = AnsiToWin32(stream)
+ self.assertTrue(converter.strip)
+ self.assertFalse(converter.convert)
+
+ def test_wrap_shouldnt_raise_on_missing_closed_attr(self):
+ with \
+ patch("colorama.ansitowin32.os.name", "nt"), \
+ patch("colorama.ansitowin32.winapi_test", lambda: True):
+ converter = AnsiToWin32(object())
+ self.assertTrue(converter.strip)
+ self.assertFalse(converter.convert)
+
+ def testExtractParams(self):
+ stream = AnsiToWin32(Mock())
+ data = {
+ '': (0,),
+ ';;': (0,),
+ '2': (2,),
+ ';;002;;': (2,),
+ '0;1': (0, 1),
+ ';;003;;456;;': (3, 456),
+ '11;22;33;44;55': (11, 22, 33, 44, 55),
+ }
+ for datum, expected in data.items():
+ self.assertEqual(stream.extract_params('m', datum), expected)
+
+ def testCallWin32UsesLookup(self):
+ listener = Mock()
+ stream = AnsiToWin32(listener)
+ stream.win32_calls = {
+ 1: (lambda *_, **__: listener(11),),
+ 2: (lambda *_, **__: listener(22),),
+ 3: (lambda *_, **__: listener(33),),
+ }
+ stream.call_win32('m', (3, 1, 99, 2))
+ self.assertEqual(
+ [a[0][0] for a in listener.call_args_list],
+ [33, 11, 22] )
+
+ def test_osc_codes(self):
+ mockStdout = Mock()
+ stream = AnsiToWin32(mockStdout, convert=True)
+ with patch('colorama.ansitowin32.winterm') as winterm:
+ data = [
+ '\033]0\x07', # missing arguments
+ '\033]0;foo\x08', # wrong OSC command
+ '\033]0;colorama_test_title\x07', # should work
+ '\033]1;colorama_test_title\x07', # wrong set command
+ '\033]2;colorama_test_title\x07', # should work
+ '\033]' + ';' * 64 + '\x08', # see issue #247
+ ]
+ for code in data:
+ stream.write(code)
+ self.assertEqual(winterm.set_title.call_count, 2)
+
+ def test_native_windows_ansi(self):
+ with ExitStack() as stack:
+ def p(a, b):
+ stack.enter_context(patch(a, b, create=True))
+ # Pretend to be on Windows
+ p("colorama.ansitowin32.os.name", "nt")
+ p("colorama.ansitowin32.winapi_test", lambda: True)
+ p("colorama.win32.winapi_test", lambda: True)
+ p("colorama.winterm.win32.windll", "non-None")
+ p("colorama.winterm.get_osfhandle", lambda _: 1234)
+
+ # Pretend that our mock stream has native ANSI support
+ p(
+ "colorama.winterm.win32.GetConsoleMode",
+ lambda _: ENABLE_VIRTUAL_TERMINAL_PROCESSING,
+ )
+ SetConsoleMode = Mock()
+ p("colorama.winterm.win32.SetConsoleMode", SetConsoleMode)
+
+ stdout = Mock()
+ stdout.closed = False
+ stdout.isatty.return_value = True
+ stdout.fileno.return_value = 1
+
+ # Our fake console says it has native vt support, so AnsiToWin32 should
+ # enable that support and do nothing else.
+ stream = AnsiToWin32(stdout)
+ SetConsoleMode.assert_called_with(1234, ENABLE_VIRTUAL_TERMINAL_PROCESSING)
+ self.assertFalse(stream.strip)
+ self.assertFalse(stream.convert)
+ self.assertFalse(stream.should_wrap())
+
+ # Now let's pretend we're on an old Windows console, that doesn't have
+ # native ANSI support.
+ p("colorama.winterm.win32.GetConsoleMode", lambda _: 0)
+ SetConsoleMode = Mock()
+ p("colorama.winterm.win32.SetConsoleMode", SetConsoleMode)
+
+ stream = AnsiToWin32(stdout)
+ SetConsoleMode.assert_called_with(1234, ENABLE_VIRTUAL_TERMINAL_PROCESSING)
+ self.assertTrue(stream.strip)
+ self.assertTrue(stream.convert)
+ self.assertTrue(stream.should_wrap())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/venv/Lib/site-packages/colorama/tests/initialise_test.py b/venv/Lib/site-packages/colorama/tests/initialise_test.py
new file mode 100644
index 0000000..89f9b07
--- /dev/null
+++ b/venv/Lib/site-packages/colorama/tests/initialise_test.py
@@ -0,0 +1,189 @@
+# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
+import sys
+from unittest import TestCase, main, skipUnless
+
+try:
+ from unittest.mock import patch, Mock
+except ImportError:
+ from mock import patch, Mock
+
+from ..ansitowin32 import StreamWrapper
+from ..initialise import init, just_fix_windows_console, _wipe_internal_state_for_tests
+from .utils import osname, replace_by
+
+orig_stdout = sys.stdout
+orig_stderr = sys.stderr
+
+
+class InitTest(TestCase):
+
+ @skipUnless(sys.stdout.isatty(), "sys.stdout is not a tty")
+ def setUp(self):
+ # sanity check
+ self.assertNotWrapped()
+
+ def tearDown(self):
+ _wipe_internal_state_for_tests()
+ sys.stdout = orig_stdout
+ sys.stderr = orig_stderr
+
+ def assertWrapped(self):
+ self.assertIsNot(sys.stdout, orig_stdout, 'stdout should be wrapped')
+ self.assertIsNot(sys.stderr, orig_stderr, 'stderr should be wrapped')
+ self.assertTrue(isinstance(sys.stdout, StreamWrapper),
+ 'bad stdout wrapper')
+ self.assertTrue(isinstance(sys.stderr, StreamWrapper),
+ 'bad stderr wrapper')
+
+ def assertNotWrapped(self):
+ self.assertIs(sys.stdout, orig_stdout, 'stdout should not be wrapped')
+ self.assertIs(sys.stderr, orig_stderr, 'stderr should not be wrapped')
+
+ @patch('colorama.initialise.reset_all')
+ @patch('colorama.ansitowin32.winapi_test', lambda *_: True)
+ @patch('colorama.ansitowin32.enable_vt_processing', lambda *_: False)
+ def testInitWrapsOnWindows(self, _):
+ with osname("nt"):
+ init()
+ self.assertWrapped()
+
+ @patch('colorama.initialise.reset_all')
+ @patch('colorama.ansitowin32.winapi_test', lambda *_: False)
+ def testInitDoesntWrapOnEmulatedWindows(self, _):
+ with osname("nt"):
+ init()
+ self.assertNotWrapped()
+
+ def testInitDoesntWrapOnNonWindows(self):
+ with osname("posix"):
+ init()
+ self.assertNotWrapped()
+
+ def testInitDoesntWrapIfNone(self):
+ with replace_by(None):
+ init()
+ # We can't use assertNotWrapped here because replace_by(None)
+ # changes stdout/stderr already.
+ self.assertIsNone(sys.stdout)
+ self.assertIsNone(sys.stderr)
+
+ def testInitAutoresetOnWrapsOnAllPlatforms(self):
+ with osname("posix"):
+ init(autoreset=True)
+ self.assertWrapped()
+
+ def testInitWrapOffDoesntWrapOnWindows(self):
+ with osname("nt"):
+ init(wrap=False)
+ self.assertNotWrapped()
+
+ def testInitWrapOffIncompatibleWithAutoresetOn(self):
+ self.assertRaises(ValueError, lambda: init(autoreset=True, wrap=False))
+
+ @patch('colorama.win32.SetConsoleTextAttribute')
+ @patch('colorama.initialise.AnsiToWin32')
+ def testAutoResetPassedOn(self, mockATW32, _):
+ with osname("nt"):
+ init(autoreset=True)
+ self.assertEqual(len(mockATW32.call_args_list), 2)
+ self.assertEqual(mockATW32.call_args_list[1][1]['autoreset'], True)
+ self.assertEqual(mockATW32.call_args_list[0][1]['autoreset'], True)
+
+ @patch('colorama.initialise.AnsiToWin32')
+ def testAutoResetChangeable(self, mockATW32):
+ with osname("nt"):
+ init()
+
+ init(autoreset=True)
+ self.assertEqual(len(mockATW32.call_args_list), 4)
+ self.assertEqual(mockATW32.call_args_list[2][1]['autoreset'], True)
+ self.assertEqual(mockATW32.call_args_list[3][1]['autoreset'], True)
+
+ init()
+ self.assertEqual(len(mockATW32.call_args_list), 6)
+ self.assertEqual(
+ mockATW32.call_args_list[4][1]['autoreset'], False)
+ self.assertEqual(
+ mockATW32.call_args_list[5][1]['autoreset'], False)
+
+
+ @patch('colorama.initialise.atexit.register')
+ def testAtexitRegisteredOnlyOnce(self, mockRegister):
+ init()
+ self.assertTrue(mockRegister.called)
+ mockRegister.reset_mock()
+ init()
+ self.assertFalse(mockRegister.called)
+
+
+class JustFixWindowsConsoleTest(TestCase):
+ def _reset(self):
+ _wipe_internal_state_for_tests()
+ sys.stdout = orig_stdout
+ sys.stderr = orig_stderr
+
+ def tearDown(self):
+ self._reset()
+
+ @patch("colorama.ansitowin32.winapi_test", lambda: True)
+ def testJustFixWindowsConsole(self):
+ if sys.platform != "win32":
+ # just_fix_windows_console should be a no-op
+ just_fix_windows_console()
+ self.assertIs(sys.stdout, orig_stdout)
+ self.assertIs(sys.stderr, orig_stderr)
+ else:
+ def fake_std():
+ # Emulate stdout=not a tty, stderr=tty
+ # to check that we handle both cases correctly
+ stdout = Mock()
+ stdout.closed = False
+ stdout.isatty.return_value = False
+ stdout.fileno.return_value = 1
+ sys.stdout = stdout
+
+ stderr = Mock()
+ stderr.closed = False
+ stderr.isatty.return_value = True
+ stderr.fileno.return_value = 2
+ sys.stderr = stderr
+
+ for native_ansi in [False, True]:
+ with patch(
+ 'colorama.ansitowin32.enable_vt_processing',
+ lambda *_: native_ansi
+ ):
+ self._reset()
+ fake_std()
+
+ # Regular single-call test
+ prev_stdout = sys.stdout
+ prev_stderr = sys.stderr
+ just_fix_windows_console()
+ self.assertIs(sys.stdout, prev_stdout)
+ if native_ansi:
+ self.assertIs(sys.stderr, prev_stderr)
+ else:
+ self.assertIsNot(sys.stderr, prev_stderr)
+
+ # second call without resetting is always a no-op
+ prev_stdout = sys.stdout
+ prev_stderr = sys.stderr
+ just_fix_windows_console()
+ self.assertIs(sys.stdout, prev_stdout)
+ self.assertIs(sys.stderr, prev_stderr)
+
+ self._reset()
+ fake_std()
+
+ # If init() runs first, just_fix_windows_console should be a no-op
+ init()
+ prev_stdout = sys.stdout
+ prev_stderr = sys.stderr
+ just_fix_windows_console()
+ self.assertIs(prev_stdout, sys.stdout)
+ self.assertIs(prev_stderr, sys.stderr)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/venv/Lib/site-packages/colorama/tests/isatty_test.py b/venv/Lib/site-packages/colorama/tests/isatty_test.py
new file mode 100644
index 0000000..0f84e4b
--- /dev/null
+++ b/venv/Lib/site-packages/colorama/tests/isatty_test.py
@@ -0,0 +1,57 @@
+# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
+import sys
+from unittest import TestCase, main
+
+from ..ansitowin32 import StreamWrapper, AnsiToWin32
+from .utils import pycharm, replace_by, replace_original_by, StreamTTY, StreamNonTTY
+
+
+def is_a_tty(stream):
+ return StreamWrapper(stream, None).isatty()
+
+class IsattyTest(TestCase):
+
+ def test_TTY(self):
+ tty = StreamTTY()
+ self.assertTrue(is_a_tty(tty))
+ with pycharm():
+ self.assertTrue(is_a_tty(tty))
+
+ def test_nonTTY(self):
+ non_tty = StreamNonTTY()
+ self.assertFalse(is_a_tty(non_tty))
+ with pycharm():
+ self.assertFalse(is_a_tty(non_tty))
+
+ def test_withPycharm(self):
+ with pycharm():
+ self.assertTrue(is_a_tty(sys.stderr))
+ self.assertTrue(is_a_tty(sys.stdout))
+
+ def test_withPycharmTTYOverride(self):
+ tty = StreamTTY()
+ with pycharm(), replace_by(tty):
+ self.assertTrue(is_a_tty(tty))
+
+ def test_withPycharmNonTTYOverride(self):
+ non_tty = StreamNonTTY()
+ with pycharm(), replace_by(non_tty):
+ self.assertFalse(is_a_tty(non_tty))
+
+ def test_withPycharmNoneOverride(self):
+ with pycharm():
+ with replace_by(None), replace_original_by(None):
+ self.assertFalse(is_a_tty(None))
+ self.assertFalse(is_a_tty(StreamNonTTY()))
+ self.assertTrue(is_a_tty(StreamTTY()))
+
+ def test_withPycharmStreamWrapped(self):
+ with pycharm():
+ self.assertTrue(AnsiToWin32(StreamTTY()).stream.isatty())
+ self.assertFalse(AnsiToWin32(StreamNonTTY()).stream.isatty())
+ self.assertTrue(AnsiToWin32(sys.stdout).stream.isatty())
+ self.assertTrue(AnsiToWin32(sys.stderr).stream.isatty())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/venv/Lib/site-packages/colorama/tests/utils.py b/venv/Lib/site-packages/colorama/tests/utils.py
new file mode 100644
index 0000000..472fafb
--- /dev/null
+++ b/venv/Lib/site-packages/colorama/tests/utils.py
@@ -0,0 +1,49 @@
+# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
+from contextlib import contextmanager
+from io import StringIO
+import sys
+import os
+
+
+class StreamTTY(StringIO):
+ def isatty(self):
+ return True
+
+class StreamNonTTY(StringIO):
+ def isatty(self):
+ return False
+
+@contextmanager
+def osname(name):
+ orig = os.name
+ os.name = name
+ yield
+ os.name = orig
+
+@contextmanager
+def replace_by(stream):
+ orig_stdout = sys.stdout
+ orig_stderr = sys.stderr
+ sys.stdout = stream
+ sys.stderr = stream
+ yield
+ sys.stdout = orig_stdout
+ sys.stderr = orig_stderr
+
+@contextmanager
+def replace_original_by(stream):
+ orig_stdout = sys.__stdout__
+ orig_stderr = sys.__stderr__
+ sys.__stdout__ = stream
+ sys.__stderr__ = stream
+ yield
+ sys.__stdout__ = orig_stdout
+ sys.__stderr__ = orig_stderr
+
+@contextmanager
+def pycharm():
+ os.environ["PYCHARM_HOSTED"] = "1"
+ non_tty = StreamNonTTY()
+ with replace_by(non_tty), replace_original_by(non_tty):
+ yield
+ del os.environ["PYCHARM_HOSTED"]
diff --git a/venv/Lib/site-packages/colorama/tests/winterm_test.py b/venv/Lib/site-packages/colorama/tests/winterm_test.py
new file mode 100644
index 0000000..d0955f9
--- /dev/null
+++ b/venv/Lib/site-packages/colorama/tests/winterm_test.py
@@ -0,0 +1,131 @@
+# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
+import sys
+from unittest import TestCase, main, skipUnless
+
+try:
+ from unittest.mock import Mock, patch
+except ImportError:
+ from mock import Mock, patch
+
+from ..winterm import WinColor, WinStyle, WinTerm
+
+
+class WinTermTest(TestCase):
+
+ @patch('colorama.winterm.win32')
+ def testInit(self, mockWin32):
+ mockAttr = Mock()
+ mockAttr.wAttributes = 7 + 6 * 16 + 8
+ mockWin32.GetConsoleScreenBufferInfo.return_value = mockAttr
+ term = WinTerm()
+ self.assertEqual(term._fore, 7)
+ self.assertEqual(term._back, 6)
+ self.assertEqual(term._style, 8)
+
+ @skipUnless(sys.platform.startswith("win"), "requires Windows")
+ def testGetAttrs(self):
+ term = WinTerm()
+
+ term._fore = 0
+ term._back = 0
+ term._style = 0
+ self.assertEqual(term.get_attrs(), 0)
+
+ term._fore = WinColor.YELLOW
+ self.assertEqual(term.get_attrs(), WinColor.YELLOW)
+
+ term._back = WinColor.MAGENTA
+ self.assertEqual(
+ term.get_attrs(),
+ WinColor.YELLOW + WinColor.MAGENTA * 16)
+
+ term._style = WinStyle.BRIGHT
+ self.assertEqual(
+ term.get_attrs(),
+ WinColor.YELLOW + WinColor.MAGENTA * 16 + WinStyle.BRIGHT)
+
+ @patch('colorama.winterm.win32')
+ def testResetAll(self, mockWin32):
+ mockAttr = Mock()
+ mockAttr.wAttributes = 1 + 2 * 16 + 8
+ mockWin32.GetConsoleScreenBufferInfo.return_value = mockAttr
+ term = WinTerm()
+
+ term.set_console = Mock()
+ term._fore = -1
+ term._back = -1
+ term._style = -1
+
+ term.reset_all()
+
+ self.assertEqual(term._fore, 1)
+ self.assertEqual(term._back, 2)
+ self.assertEqual(term._style, 8)
+ self.assertEqual(term.set_console.called, True)
+
+ @skipUnless(sys.platform.startswith("win"), "requires Windows")
+ def testFore(self):
+ term = WinTerm()
+ term.set_console = Mock()
+ term._fore = 0
+
+ term.fore(5)
+
+ self.assertEqual(term._fore, 5)
+ self.assertEqual(term.set_console.called, True)
+
+ @skipUnless(sys.platform.startswith("win"), "requires Windows")
+ def testBack(self):
+ term = WinTerm()
+ term.set_console = Mock()
+ term._back = 0
+
+ term.back(5)
+
+ self.assertEqual(term._back, 5)
+ self.assertEqual(term.set_console.called, True)
+
+ @skipUnless(sys.platform.startswith("win"), "requires Windows")
+ def testStyle(self):
+ term = WinTerm()
+ term.set_console = Mock()
+ term._style = 0
+
+ term.style(22)
+
+ self.assertEqual(term._style, 22)
+ self.assertEqual(term.set_console.called, True)
+
+ @patch('colorama.winterm.win32')
+ def testSetConsole(self, mockWin32):
+ mockAttr = Mock()
+ mockAttr.wAttributes = 0
+ mockWin32.GetConsoleScreenBufferInfo.return_value = mockAttr
+ term = WinTerm()
+ term.windll = Mock()
+
+ term.set_console()
+
+ self.assertEqual(
+ mockWin32.SetConsoleTextAttribute.call_args,
+ ((mockWin32.STDOUT, term.get_attrs()), {})
+ )
+
+ @patch('colorama.winterm.win32')
+ def testSetConsoleOnStderr(self, mockWin32):
+ mockAttr = Mock()
+ mockAttr.wAttributes = 0
+ mockWin32.GetConsoleScreenBufferInfo.return_value = mockAttr
+ term = WinTerm()
+ term.windll = Mock()
+
+ term.set_console(on_stderr=True)
+
+ self.assertEqual(
+ mockWin32.SetConsoleTextAttribute.call_args,
+ ((mockWin32.STDERR, term.get_attrs()), {})
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/venv/Lib/site-packages/colorama/win32.py b/venv/Lib/site-packages/colorama/win32.py
new file mode 100644
index 0000000..841b0e2
--- /dev/null
+++ b/venv/Lib/site-packages/colorama/win32.py
@@ -0,0 +1,180 @@
+# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
+
+# from winbase.h
+STDOUT = -11
+STDERR = -12
+
+ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
+
+try:
+ import ctypes
+ from ctypes import LibraryLoader
+ windll = LibraryLoader(ctypes.WinDLL)
+ from ctypes import wintypes
+except (AttributeError, ImportError):
+ windll = None
+ SetConsoleTextAttribute = lambda *_: None
+ winapi_test = lambda *_: None
+else:
+ from ctypes import byref, Structure, c_char, POINTER
+
+ COORD = wintypes._COORD
+
+ class CONSOLE_SCREEN_BUFFER_INFO(Structure):
+ """struct in wincon.h."""
+ _fields_ = [
+ ("dwSize", COORD),
+ ("dwCursorPosition", COORD),
+ ("wAttributes", wintypes.WORD),
+ ("srWindow", wintypes.SMALL_RECT),
+ ("dwMaximumWindowSize", COORD),
+ ]
+ def __str__(self):
+ return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % (
+ self.dwSize.Y, self.dwSize.X
+ , self.dwCursorPosition.Y, self.dwCursorPosition.X
+ , self.wAttributes
+ , self.srWindow.Top, self.srWindow.Left, self.srWindow.Bottom, self.srWindow.Right
+ , self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X
+ )
+
+ _GetStdHandle = windll.kernel32.GetStdHandle
+ _GetStdHandle.argtypes = [
+ wintypes.DWORD,
+ ]
+ _GetStdHandle.restype = wintypes.HANDLE
+
+ _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo
+ _GetConsoleScreenBufferInfo.argtypes = [
+ wintypes.HANDLE,
+ POINTER(CONSOLE_SCREEN_BUFFER_INFO),
+ ]
+ _GetConsoleScreenBufferInfo.restype = wintypes.BOOL
+
+ _SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute
+ _SetConsoleTextAttribute.argtypes = [
+ wintypes.HANDLE,
+ wintypes.WORD,
+ ]
+ _SetConsoleTextAttribute.restype = wintypes.BOOL
+
+ _SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition
+ _SetConsoleCursorPosition.argtypes = [
+ wintypes.HANDLE,
+ COORD,
+ ]
+ _SetConsoleCursorPosition.restype = wintypes.BOOL
+
+ _FillConsoleOutputCharacterA = windll.kernel32.FillConsoleOutputCharacterA
+ _FillConsoleOutputCharacterA.argtypes = [
+ wintypes.HANDLE,
+ c_char,
+ wintypes.DWORD,
+ COORD,
+ POINTER(wintypes.DWORD),
+ ]
+ _FillConsoleOutputCharacterA.restype = wintypes.BOOL
+
+ _FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute
+ _FillConsoleOutputAttribute.argtypes = [
+ wintypes.HANDLE,
+ wintypes.WORD,
+ wintypes.DWORD,
+ COORD,
+ POINTER(wintypes.DWORD),
+ ]
+ _FillConsoleOutputAttribute.restype = wintypes.BOOL
+
+ _SetConsoleTitleW = windll.kernel32.SetConsoleTitleW
+ _SetConsoleTitleW.argtypes = [
+ wintypes.LPCWSTR
+ ]
+ _SetConsoleTitleW.restype = wintypes.BOOL
+
+ _GetConsoleMode = windll.kernel32.GetConsoleMode
+ _GetConsoleMode.argtypes = [
+ wintypes.HANDLE,
+ POINTER(wintypes.DWORD)
+ ]
+ _GetConsoleMode.restype = wintypes.BOOL
+
+ _SetConsoleMode = windll.kernel32.SetConsoleMode
+ _SetConsoleMode.argtypes = [
+ wintypes.HANDLE,
+ wintypes.DWORD
+ ]
+ _SetConsoleMode.restype = wintypes.BOOL
+
+ def _winapi_test(handle):
+ csbi = CONSOLE_SCREEN_BUFFER_INFO()
+ success = _GetConsoleScreenBufferInfo(
+ handle, byref(csbi))
+ return bool(success)
+
+ def winapi_test():
+ return any(_winapi_test(h) for h in
+ (_GetStdHandle(STDOUT), _GetStdHandle(STDERR)))
+
+ def GetConsoleScreenBufferInfo(stream_id=STDOUT):
+ handle = _GetStdHandle(stream_id)
+ csbi = CONSOLE_SCREEN_BUFFER_INFO()
+ success = _GetConsoleScreenBufferInfo(
+ handle, byref(csbi))
+ return csbi
+
+ def SetConsoleTextAttribute(stream_id, attrs):
+ handle = _GetStdHandle(stream_id)
+ return _SetConsoleTextAttribute(handle, attrs)
+
+ def SetConsoleCursorPosition(stream_id, position, adjust=True):
+ position = COORD(*position)
+ # If the position is out of range, do nothing.
+ if position.Y <= 0 or position.X <= 0:
+ return
+ # Adjust for Windows' SetConsoleCursorPosition:
+ # 1. being 0-based, while ANSI is 1-based.
+ # 2. expecting (x,y), while ANSI uses (y,x).
+ adjusted_position = COORD(position.Y - 1, position.X - 1)
+ if adjust:
+ # Adjust for viewport's scroll position
+ sr = GetConsoleScreenBufferInfo(STDOUT).srWindow
+ adjusted_position.Y += sr.Top
+ adjusted_position.X += sr.Left
+ # Resume normal processing
+ handle = _GetStdHandle(stream_id)
+ return _SetConsoleCursorPosition(handle, adjusted_position)
+
+ def FillConsoleOutputCharacter(stream_id, char, length, start):
+ handle = _GetStdHandle(stream_id)
+ char = c_char(char.encode())
+ length = wintypes.DWORD(length)
+ num_written = wintypes.DWORD(0)
+ # Note that this is hard-coded for ANSI (vs wide) bytes.
+ success = _FillConsoleOutputCharacterA(
+ handle, char, length, start, byref(num_written))
+ return num_written.value
+
+ def FillConsoleOutputAttribute(stream_id, attr, length, start):
+ ''' FillConsoleOutputAttribute( hConsole, csbi.wAttributes, dwConSize, coordScreen, &cCharsWritten )'''
+ handle = _GetStdHandle(stream_id)
+ attribute = wintypes.WORD(attr)
+ length = wintypes.DWORD(length)
+ num_written = wintypes.DWORD(0)
+ # Note that this is hard-coded for ANSI (vs wide) bytes.
+ return _FillConsoleOutputAttribute(
+ handle, attribute, length, start, byref(num_written))
+
+ def SetConsoleTitle(title):
+ return _SetConsoleTitleW(title)
+
+ def GetConsoleMode(handle):
+ mode = wintypes.DWORD()
+ success = _GetConsoleMode(handle, byref(mode))
+ if not success:
+ raise ctypes.WinError()
+ return mode.value
+
+ def SetConsoleMode(handle, mode):
+ success = _SetConsoleMode(handle, mode)
+ if not success:
+ raise ctypes.WinError()
diff --git a/venv/Lib/site-packages/colorama/winterm.py b/venv/Lib/site-packages/colorama/winterm.py
new file mode 100644
index 0000000..aad867e
--- /dev/null
+++ b/venv/Lib/site-packages/colorama/winterm.py
@@ -0,0 +1,195 @@
+# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
+try:
+ from msvcrt import get_osfhandle
+except ImportError:
+ def get_osfhandle(_):
+ raise OSError("This isn't windows!")
+
+
+from . import win32
+
+# from wincon.h
+class WinColor(object):
+ BLACK = 0
+ BLUE = 1
+ GREEN = 2
+ CYAN = 3
+ RED = 4
+ MAGENTA = 5
+ YELLOW = 6
+ GREY = 7
+
+# from wincon.h
+class WinStyle(object):
+ NORMAL = 0x00 # dim text, dim background
+ BRIGHT = 0x08 # bright text, dim background
+ BRIGHT_BACKGROUND = 0x80 # dim text, bright background
+
+class WinTerm(object):
+
+ def __init__(self):
+ self._default = win32.GetConsoleScreenBufferInfo(win32.STDOUT).wAttributes
+ self.set_attrs(self._default)
+ self._default_fore = self._fore
+ self._default_back = self._back
+ self._default_style = self._style
+ # In order to emulate LIGHT_EX in windows, we borrow the BRIGHT style.
+ # So that LIGHT_EX colors and BRIGHT style do not clobber each other,
+ # we track them separately, since LIGHT_EX is overwritten by Fore/Back
+ # and BRIGHT is overwritten by Style codes.
+ self._light = 0
+
+ def get_attrs(self):
+ return self._fore + self._back * 16 + (self._style | self._light)
+
+ def set_attrs(self, value):
+ self._fore = value & 7
+ self._back = (value >> 4) & 7
+ self._style = value & (WinStyle.BRIGHT | WinStyle.BRIGHT_BACKGROUND)
+
+ def reset_all(self, on_stderr=None):
+ self.set_attrs(self._default)
+ self.set_console(attrs=self._default)
+ self._light = 0
+
+ def fore(self, fore=None, light=False, on_stderr=False):
+ if fore is None:
+ fore = self._default_fore
+ self._fore = fore
+ # Emulate LIGHT_EX with BRIGHT Style
+ if light:
+ self._light |= WinStyle.BRIGHT
+ else:
+ self._light &= ~WinStyle.BRIGHT
+ self.set_console(on_stderr=on_stderr)
+
+ def back(self, back=None, light=False, on_stderr=False):
+ if back is None:
+ back = self._default_back
+ self._back = back
+ # Emulate LIGHT_EX with BRIGHT_BACKGROUND Style
+ if light:
+ self._light |= WinStyle.BRIGHT_BACKGROUND
+ else:
+ self._light &= ~WinStyle.BRIGHT_BACKGROUND
+ self.set_console(on_stderr=on_stderr)
+
+ def style(self, style=None, on_stderr=False):
+ if style is None:
+ style = self._default_style
+ self._style = style
+ self.set_console(on_stderr=on_stderr)
+
+ def set_console(self, attrs=None, on_stderr=False):
+ if attrs is None:
+ attrs = self.get_attrs()
+ handle = win32.STDOUT
+ if on_stderr:
+ handle = win32.STDERR
+ win32.SetConsoleTextAttribute(handle, attrs)
+
+ def get_position(self, handle):
+ position = win32.GetConsoleScreenBufferInfo(handle).dwCursorPosition
+ # Because Windows coordinates are 0-based,
+ # and win32.SetConsoleCursorPosition expects 1-based.
+ position.X += 1
+ position.Y += 1
+ return position
+
+ def set_cursor_position(self, position=None, on_stderr=False):
+ if position is None:
+ # I'm not currently tracking the position, so there is no default.
+ # position = self.get_position()
+ return
+ handle = win32.STDOUT
+ if on_stderr:
+ handle = win32.STDERR
+ win32.SetConsoleCursorPosition(handle, position)
+
+ def cursor_adjust(self, x, y, on_stderr=False):
+ handle = win32.STDOUT
+ if on_stderr:
+ handle = win32.STDERR
+ position = self.get_position(handle)
+ adjusted_position = (position.Y + y, position.X + x)
+ win32.SetConsoleCursorPosition(handle, adjusted_position, adjust=False)
+
+ def erase_screen(self, mode=0, on_stderr=False):
+ # 0 should clear from the cursor to the end of the screen.
+ # 1 should clear from the cursor to the beginning of the screen.
+ # 2 should clear the entire screen, and move cursor to (1,1)
+ handle = win32.STDOUT
+ if on_stderr:
+ handle = win32.STDERR
+ csbi = win32.GetConsoleScreenBufferInfo(handle)
+ # get the number of character cells in the current buffer
+ cells_in_screen = csbi.dwSize.X * csbi.dwSize.Y
+ # get number of character cells before current cursor position
+ cells_before_cursor = csbi.dwSize.X * csbi.dwCursorPosition.Y + csbi.dwCursorPosition.X
+ if mode == 0:
+ from_coord = csbi.dwCursorPosition
+ cells_to_erase = cells_in_screen - cells_before_cursor
+ elif mode == 1:
+ from_coord = win32.COORD(0, 0)
+ cells_to_erase = cells_before_cursor
+ elif mode == 2:
+ from_coord = win32.COORD(0, 0)
+ cells_to_erase = cells_in_screen
+ else:
+ # invalid mode
+ return
+ # fill the entire screen with blanks
+ win32.FillConsoleOutputCharacter(handle, ' ', cells_to_erase, from_coord)
+ # now set the buffer's attributes accordingly
+ win32.FillConsoleOutputAttribute(handle, self.get_attrs(), cells_to_erase, from_coord)
+ if mode == 2:
+ # put the cursor where needed
+ win32.SetConsoleCursorPosition(handle, (1, 1))
+
+ def erase_line(self, mode=0, on_stderr=False):
+ # 0 should clear from the cursor to the end of the line.
+ # 1 should clear from the cursor to the beginning of the line.
+ # 2 should clear the entire line.
+ handle = win32.STDOUT
+ if on_stderr:
+ handle = win32.STDERR
+ csbi = win32.GetConsoleScreenBufferInfo(handle)
+ if mode == 0:
+ from_coord = csbi.dwCursorPosition
+ cells_to_erase = csbi.dwSize.X - csbi.dwCursorPosition.X
+ elif mode == 1:
+ from_coord = win32.COORD(0, csbi.dwCursorPosition.Y)
+ cells_to_erase = csbi.dwCursorPosition.X
+ elif mode == 2:
+ from_coord = win32.COORD(0, csbi.dwCursorPosition.Y)
+ cells_to_erase = csbi.dwSize.X
+ else:
+ # invalid mode
+ return
+ # fill the entire screen with blanks
+ win32.FillConsoleOutputCharacter(handle, ' ', cells_to_erase, from_coord)
+ # now set the buffer's attributes accordingly
+ win32.FillConsoleOutputAttribute(handle, self.get_attrs(), cells_to_erase, from_coord)
+
+ def set_title(self, title):
+ win32.SetConsoleTitle(title)
+
+
+def enable_vt_processing(fd):
+ if win32.windll is None or not win32.winapi_test():
+ return False
+
+ try:
+ handle = get_osfhandle(fd)
+ mode = win32.GetConsoleMode(handle)
+ win32.SetConsoleMode(
+ handle,
+ mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING,
+ )
+
+ mode = win32.GetConsoleMode(handle)
+ if mode & win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING:
+ return True
+ # Can get TypeError in testsuite where 'fd' is a Mock()
+ except (OSError, TypeError):
+ return False
diff --git a/venv/Lib/site-packages/flask-3.1.3.dist-info/INSTALLER b/venv/Lib/site-packages/flask-3.1.3.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/Lib/site-packages/flask-3.1.3.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/flask-3.1.3.dist-info/METADATA b/venv/Lib/site-packages/flask-3.1.3.dist-info/METADATA
new file mode 100644
index 0000000..9d8623c
--- /dev/null
+++ b/venv/Lib/site-packages/flask-3.1.3.dist-info/METADATA
@@ -0,0 +1,91 @@
+Metadata-Version: 2.4
+Name: Flask
+Version: 3.1.3
+Summary: A simple framework for building complex web applications.
+Maintainer-email: Pallets
+Requires-Python: >=3.9
+Description-Content-Type: text/markdown
+License-Expression: BSD-3-Clause
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Framework :: Flask
+Classifier: Intended Audience :: Developers
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
+Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
+Classifier: Typing :: Typed
+License-File: LICENSE.txt
+Requires-Dist: blinker>=1.9.0
+Requires-Dist: click>=8.1.3
+Requires-Dist: importlib-metadata>=3.6.0; python_version < '3.10'
+Requires-Dist: itsdangerous>=2.2.0
+Requires-Dist: jinja2>=3.1.2
+Requires-Dist: markupsafe>=2.1.1
+Requires-Dist: werkzeug>=3.1.0
+Requires-Dist: asgiref>=3.2 ; extra == "async"
+Requires-Dist: python-dotenv ; extra == "dotenv"
+Project-URL: Changes, https://flask.palletsprojects.com/page/changes/
+Project-URL: Chat, https://discord.gg/pallets
+Project-URL: Documentation, https://flask.palletsprojects.com/
+Project-URL: Donate, https://palletsprojects.com/donate
+Project-URL: Source, https://github.com/pallets/flask/
+Provides-Extra: async
+Provides-Extra: dotenv
+
+
+
+# Flask
+
+Flask is a lightweight [WSGI] web application framework. It is designed
+to make getting started quick and easy, with the ability to scale up to
+complex applications. It began as a simple wrapper around [Werkzeug]
+and [Jinja], and has become one of the most popular Python web
+application frameworks.
+
+Flask offers suggestions, but doesn't enforce any dependencies or
+project layout. It is up to the developer to choose the tools and
+libraries they want to use. There are many extensions provided by the
+community that make adding new functionality easy.
+
+[WSGI]: https://wsgi.readthedocs.io/
+[Werkzeug]: https://werkzeug.palletsprojects.com/
+[Jinja]: https://jinja.palletsprojects.com/
+
+## A Simple Example
+
+```python
+# save this as app.py
+from flask import Flask
+
+app = Flask(__name__)
+
+@app.route("/")
+def hello():
+ return "Hello, World!"
+```
+
+```
+$ flask run
+ * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
+```
+
+## Donate
+
+The Pallets organization develops and supports Flask and the libraries
+it uses. In order to grow the community of contributors and users, and
+allow the maintainers to devote more time to the projects, [please
+donate today].
+
+[please donate today]: https://palletsprojects.com/donate
+
+## Contributing
+
+See our [detailed contributing documentation][contrib] for many ways to
+contribute, including reporting issues, requesting features, asking or answering
+questions, and making PRs.
+
+[contrib]: https://palletsprojects.com/contributing/
+
diff --git a/venv/Lib/site-packages/flask-3.1.3.dist-info/RECORD b/venv/Lib/site-packages/flask-3.1.3.dist-info/RECORD
new file mode 100644
index 0000000..55bf03f
--- /dev/null
+++ b/venv/Lib/site-packages/flask-3.1.3.dist-info/RECORD
@@ -0,0 +1,58 @@
+../../Scripts/flask.exe,sha256=XKcvacNn9jIjcO7b45I_46IF7OTCTFzm6p1b_yQJ224,108393
+flask-3.1.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+flask-3.1.3.dist-info/METADATA,sha256=qmdg7W9UVwRHTXBzPkpjp_FIHjdpc-3IlqE9AqciTHw,3167
+flask-3.1.3.dist-info/RECORD,,
+flask-3.1.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+flask-3.1.3.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
+flask-3.1.3.dist-info/entry_points.txt,sha256=bBP7hTOS5fz9zLtC7sPofBZAlMkEvBxu7KqS6l5lvc4,40
+flask-3.1.3.dist-info/licenses/LICENSE.txt,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475
+flask/__init__.py,sha256=mHvJN9Swtl1RDtjCqCIYyIniK_SZ_l_hqUynOzgpJ9o,2701
+flask/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30
+flask/__pycache__/__init__.cpython-310.pyc,,
+flask/__pycache__/__main__.cpython-310.pyc,,
+flask/__pycache__/app.cpython-310.pyc,,
+flask/__pycache__/blueprints.cpython-310.pyc,,
+flask/__pycache__/cli.cpython-310.pyc,,
+flask/__pycache__/config.cpython-310.pyc,,
+flask/__pycache__/ctx.cpython-310.pyc,,
+flask/__pycache__/debughelpers.cpython-310.pyc,,
+flask/__pycache__/globals.cpython-310.pyc,,
+flask/__pycache__/helpers.cpython-310.pyc,,
+flask/__pycache__/logging.cpython-310.pyc,,
+flask/__pycache__/sessions.cpython-310.pyc,,
+flask/__pycache__/signals.cpython-310.pyc,,
+flask/__pycache__/templating.cpython-310.pyc,,
+flask/__pycache__/testing.cpython-310.pyc,,
+flask/__pycache__/typing.cpython-310.pyc,,
+flask/__pycache__/views.cpython-310.pyc,,
+flask/__pycache__/wrappers.cpython-310.pyc,,
+flask/app.py,sha256=k7tW8LHRSldUi6zKsFKK7Axa_WL4zu1e2wPNthIsu7o,61719
+flask/blueprints.py,sha256=p5QE2lY18GItbdr_RKRpZ8Do17g0PvQGIgZkSUDhX2k,4541
+flask/cli.py,sha256=Pfh72-BxlvoH0QHCDOc1HvXG7Kq5Xetf3zzNz2kNSHk,37184
+flask/config.py,sha256=PiqF0DPam6HW0FH4CH1hpXTBe30NSzjPEOwrz1b6kt0,13219
+flask/ctx.py,sha256=oMe0TRsScW0qdaIqavVsk8P9qiEvAY5VHn1FAgkX8nk,15521
+flask/debughelpers.py,sha256=PGIDhStW_efRjpaa3zHIpo-htStJOR41Ip3OJWPYBwo,6080
+flask/globals.py,sha256=XdQZmStBmPIs8t93tjx6pO7Bm3gobAaONWkFcUHaGas,1713
+flask/helpers.py,sha256=rJZge7_J288J1UQv5-kNf4oEaw332PP8NTW0QRIBbXE,23517
+flask/json/__init__.py,sha256=hLNR898paqoefdeAhraa5wyJy-bmRB2k2dV4EgVy2Z8,5602
+flask/json/__pycache__/__init__.cpython-310.pyc,,
+flask/json/__pycache__/provider.cpython-310.pyc,,
+flask/json/__pycache__/tag.cpython-310.pyc,,
+flask/json/provider.py,sha256=5imEzY5HjV2HoUVrQbJLqXCzMNpZXfD0Y1XqdLV2XBA,7672
+flask/json/tag.py,sha256=DhaNwuIOhdt2R74oOC9Y4Z8ZprxFYiRb5dUP5byyINw,9281
+flask/logging.py,sha256=8sM3WMTubi1cBb2c_lPkWpN0J8dMAqrgKRYLLi1dCVI,2377
+flask/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+flask/sansio/README.md,sha256=-0X1tECnilmz1cogx-YhNw5d7guK7GKrq_DEV2OzlU0,228
+flask/sansio/__pycache__/app.cpython-310.pyc,,
+flask/sansio/__pycache__/blueprints.cpython-310.pyc,,
+flask/sansio/__pycache__/scaffold.cpython-310.pyc,,
+flask/sansio/app.py,sha256=whGURQDkN0jmhS4CHO7DQ96GGlZS0kETkKkAkoRjl4U,38106
+flask/sansio/blueprints.py,sha256=Tqe-7EkZ-tbWchm8iDoCfD848f0_3nLv6NNjeIPvHwM,24637
+flask/sansio/scaffold.py,sha256=wSASXYdFRWJmqcL0Xq-T7N-PDVUSiFGvjO9kPZg58bk,30371
+flask/sessions.py,sha256=eywRqmytTmYnX_EC78-YBGJoTc5XD_lRphQG5LbN1d0,14969
+flask/signals.py,sha256=V7lMUww7CqgJ2ThUBn1PiatZtQanOyt7OZpu2GZI-34,750
+flask/templating.py,sha256=vbIkwYAxsSEfDxQID1gKRvBQQcGWEuWYCnH0XK3EqOI,7678
+flask/testing.py,sha256=zzC7XxhBWOP9H697IV_4SG7Lg3Lzb5PWiyEP93_KQXE,10117
+flask/typing.py,sha256=L-L5t2jKgS0aOmVhioQ_ylqcgiVFnA6yxO-RLNhq-GU,3293
+flask/views.py,sha256=xzJx6oJqGElThtEghZN7ZQGMw5TDFyuRxUkecwRuAoA,6962
+flask/wrappers.py,sha256=jUkv4mVek2Iq4hwxd4RvqrIMb69Bv0PElDgWLmd5ORo,9406
diff --git a/venv/Lib/site-packages/flask-3.1.3.dist-info/REQUESTED b/venv/Lib/site-packages/flask-3.1.3.dist-info/REQUESTED
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/flask-3.1.3.dist-info/WHEEL b/venv/Lib/site-packages/flask-3.1.3.dist-info/WHEEL
new file mode 100644
index 0000000..d8b9936
--- /dev/null
+++ b/venv/Lib/site-packages/flask-3.1.3.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 1.0
+Generator: flit 3.12.0
+Root-Is-Purelib: true
+Tag: py3-none-any
diff --git a/venv/Lib/site-packages/flask-3.1.3.dist-info/entry_points.txt b/venv/Lib/site-packages/flask-3.1.3.dist-info/entry_points.txt
new file mode 100644
index 0000000..eec6733
--- /dev/null
+++ b/venv/Lib/site-packages/flask-3.1.3.dist-info/entry_points.txt
@@ -0,0 +1,3 @@
+[console_scripts]
+flask=flask.cli:main
+
diff --git a/venv/Lib/site-packages/flask-3.1.3.dist-info/licenses/LICENSE.txt b/venv/Lib/site-packages/flask-3.1.3.dist-info/licenses/LICENSE.txt
new file mode 100644
index 0000000..9d227a0
--- /dev/null
+++ b/venv/Lib/site-packages/flask-3.1.3.dist-info/licenses/LICENSE.txt
@@ -0,0 +1,28 @@
+Copyright 2010 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/venv/Lib/site-packages/flask/__init__.py b/venv/Lib/site-packages/flask/__init__.py
new file mode 100644
index 0000000..1fdc50c
--- /dev/null
+++ b/venv/Lib/site-packages/flask/__init__.py
@@ -0,0 +1,61 @@
+from __future__ import annotations
+
+import typing as t
+
+from . import json as json
+from .app import Flask as Flask
+from .blueprints import Blueprint as Blueprint
+from .config import Config as Config
+from .ctx import after_this_request as after_this_request
+from .ctx import copy_current_request_context as copy_current_request_context
+from .ctx import has_app_context as has_app_context
+from .ctx import has_request_context as has_request_context
+from .globals import current_app as current_app
+from .globals import g as g
+from .globals import request as request
+from .globals import session as session
+from .helpers import abort as abort
+from .helpers import flash as flash
+from .helpers import get_flashed_messages as get_flashed_messages
+from .helpers import get_template_attribute as get_template_attribute
+from .helpers import make_response as make_response
+from .helpers import redirect as redirect
+from .helpers import send_file as send_file
+from .helpers import send_from_directory as send_from_directory
+from .helpers import stream_with_context as stream_with_context
+from .helpers import url_for as url_for
+from .json import jsonify as jsonify
+from .signals import appcontext_popped as appcontext_popped
+from .signals import appcontext_pushed as appcontext_pushed
+from .signals import appcontext_tearing_down as appcontext_tearing_down
+from .signals import before_render_template as before_render_template
+from .signals import got_request_exception as got_request_exception
+from .signals import message_flashed as message_flashed
+from .signals import request_finished as request_finished
+from .signals import request_started as request_started
+from .signals import request_tearing_down as request_tearing_down
+from .signals import template_rendered as template_rendered
+from .templating import render_template as render_template
+from .templating import render_template_string as render_template_string
+from .templating import stream_template as stream_template
+from .templating import stream_template_string as stream_template_string
+from .wrappers import Request as Request
+from .wrappers import Response as Response
+
+if not t.TYPE_CHECKING:
+
+ def __getattr__(name: str) -> t.Any:
+ if name == "__version__":
+ import importlib.metadata
+ import warnings
+
+ warnings.warn(
+ "The '__version__' attribute is deprecated and will be removed in"
+ " Flask 3.2. Use feature detection or"
+ " 'importlib.metadata.version(\"flask\")' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return importlib.metadata.version("flask")
+
+ raise AttributeError(name)
diff --git a/venv/Lib/site-packages/flask/__main__.py b/venv/Lib/site-packages/flask/__main__.py
new file mode 100644
index 0000000..4e28416
--- /dev/null
+++ b/venv/Lib/site-packages/flask/__main__.py
@@ -0,0 +1,3 @@
+from .cli import main
+
+main()
diff --git a/venv/Lib/site-packages/flask/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..5d05a6e
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/__main__.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/__main__.cpython-310.pyc
new file mode 100644
index 0000000..6af57ac
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/__main__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/app.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/app.cpython-310.pyc
new file mode 100644
index 0000000..ceccc72
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/app.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/blueprints.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/blueprints.cpython-310.pyc
new file mode 100644
index 0000000..0352c53
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/blueprints.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/cli.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/cli.cpython-310.pyc
new file mode 100644
index 0000000..0d59534
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/cli.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/config.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/config.cpython-310.pyc
new file mode 100644
index 0000000..08da4fc
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/config.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/ctx.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/ctx.cpython-310.pyc
new file mode 100644
index 0000000..9dfe727
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/ctx.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/debughelpers.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/debughelpers.cpython-310.pyc
new file mode 100644
index 0000000..d84a071
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/debughelpers.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/globals.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/globals.cpython-310.pyc
new file mode 100644
index 0000000..6e0fc4d
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/globals.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/helpers.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/helpers.cpython-310.pyc
new file mode 100644
index 0000000..39ce274
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/helpers.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/logging.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/logging.cpython-310.pyc
new file mode 100644
index 0000000..c71b026
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/logging.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/sessions.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/sessions.cpython-310.pyc
new file mode 100644
index 0000000..c69a84e
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/sessions.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/signals.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/signals.cpython-310.pyc
new file mode 100644
index 0000000..9f2a82b
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/signals.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/templating.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/templating.cpython-310.pyc
new file mode 100644
index 0000000..df4eefb
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/templating.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/testing.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/testing.cpython-310.pyc
new file mode 100644
index 0000000..0730e11
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/testing.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/typing.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/typing.cpython-310.pyc
new file mode 100644
index 0000000..fc63091
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/typing.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/views.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/views.cpython-310.pyc
new file mode 100644
index 0000000..063178c
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/views.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/__pycache__/wrappers.cpython-310.pyc b/venv/Lib/site-packages/flask/__pycache__/wrappers.cpython-310.pyc
new file mode 100644
index 0000000..489a02a
Binary files /dev/null and b/venv/Lib/site-packages/flask/__pycache__/wrappers.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/app.py b/venv/Lib/site-packages/flask/app.py
new file mode 100644
index 0000000..cc326db
--- /dev/null
+++ b/venv/Lib/site-packages/flask/app.py
@@ -0,0 +1,1536 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import os
+import sys
+import typing as t
+import weakref
+from datetime import timedelta
+from inspect import iscoroutinefunction
+from itertools import chain
+from types import TracebackType
+from urllib.parse import quote as _url_quote
+
+import click
+from werkzeug.datastructures import Headers
+from werkzeug.datastructures import ImmutableDict
+from werkzeug.exceptions import BadRequestKeyError
+from werkzeug.exceptions import HTTPException
+from werkzeug.exceptions import InternalServerError
+from werkzeug.routing import BuildError
+from werkzeug.routing import MapAdapter
+from werkzeug.routing import RequestRedirect
+from werkzeug.routing import RoutingException
+from werkzeug.routing import Rule
+from werkzeug.serving import is_running_from_reloader
+from werkzeug.wrappers import Response as BaseResponse
+from werkzeug.wsgi import get_host
+
+from . import cli
+from . import typing as ft
+from .ctx import AppContext
+from .ctx import RequestContext
+from .globals import _cv_app
+from .globals import _cv_request
+from .globals import current_app
+from .globals import g
+from .globals import request
+from .globals import request_ctx
+from .globals import session
+from .helpers import get_debug_flag
+from .helpers import get_flashed_messages
+from .helpers import get_load_dotenv
+from .helpers import send_from_directory
+from .sansio.app import App
+from .sansio.scaffold import _sentinel
+from .sessions import SecureCookieSessionInterface
+from .sessions import SessionInterface
+from .signals import appcontext_tearing_down
+from .signals import got_request_exception
+from .signals import request_finished
+from .signals import request_started
+from .signals import request_tearing_down
+from .templating import Environment
+from .wrappers import Request
+from .wrappers import Response
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from _typeshed.wsgi import StartResponse
+ from _typeshed.wsgi import WSGIEnvironment
+
+ from .testing import FlaskClient
+ from .testing import FlaskCliRunner
+ from .typing import HeadersValue
+
+T_shell_context_processor = t.TypeVar(
+ "T_shell_context_processor", bound=ft.ShellContextProcessorCallable
+)
+T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable)
+T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable)
+T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable)
+T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable)
+
+
+def _make_timedelta(value: timedelta | int | None) -> timedelta | None:
+ if value is None or isinstance(value, timedelta):
+ return value
+
+ return timedelta(seconds=value)
+
+
+class Flask(App):
+ """The flask object implements a WSGI application and acts as the central
+ object. It is passed the name of the module or package of the
+ application. Once it is created it will act as a central registry for
+ the view functions, the URL rules, template configuration and much more.
+
+ The name of the package is used to resolve resources from inside the
+ package or the folder the module is contained in depending on if the
+ package parameter resolves to an actual python package (a folder with
+ an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file).
+
+ For more information about resource loading, see :func:`open_resource`.
+
+ Usually you create a :class:`Flask` instance in your main module or
+ in the :file:`__init__.py` file of your package like this::
+
+ from flask import Flask
+ app = Flask(__name__)
+
+ .. admonition:: About the First Parameter
+
+ The idea of the first parameter is to give Flask an idea of what
+ belongs to your application. This name is used to find resources
+ on the filesystem, can be used by extensions to improve debugging
+ information and a lot more.
+
+ So it's important what you provide there. If you are using a single
+ module, `__name__` is always the correct value. If you however are
+ using a package, it's usually recommended to hardcode the name of
+ your package there.
+
+ For example if your application is defined in :file:`yourapplication/app.py`
+ you should create it with one of the two versions below::
+
+ app = Flask('yourapplication')
+ app = Flask(__name__.split('.')[0])
+
+ Why is that? The application will work even with `__name__`, thanks
+ to how resources are looked up. However it will make debugging more
+ painful. Certain extensions can make assumptions based on the
+ import name of your application. For example the Flask-SQLAlchemy
+ extension will look for the code in your application that triggered
+ an SQL query in debug mode. If the import name is not properly set
+ up, that debugging information is lost. (For example it would only
+ pick up SQL queries in `yourapplication.app` and not
+ `yourapplication.views.frontend`)
+
+ .. versionadded:: 0.7
+ The `static_url_path`, `static_folder`, and `template_folder`
+ parameters were added.
+
+ .. versionadded:: 0.8
+ The `instance_path` and `instance_relative_config` parameters were
+ added.
+
+ .. versionadded:: 0.11
+ The `root_path` parameter was added.
+
+ .. versionadded:: 1.0
+ The ``host_matching`` and ``static_host`` parameters were added.
+
+ .. versionadded:: 1.0
+ The ``subdomain_matching`` parameter was added. Subdomain
+ matching needs to be enabled manually now. Setting
+ :data:`SERVER_NAME` does not implicitly enable it.
+
+ :param import_name: the name of the application package
+ :param static_url_path: can be used to specify a different path for the
+ static files on the web. Defaults to the name
+ of the `static_folder` folder.
+ :param static_folder: The folder with static files that is served at
+ ``static_url_path``. Relative to the application ``root_path``
+ or an absolute path. Defaults to ``'static'``.
+ :param static_host: the host to use when adding the static route.
+ Defaults to None. Required when using ``host_matching=True``
+ with a ``static_folder`` configured.
+ :param host_matching: set ``url_map.host_matching`` attribute.
+ Defaults to False.
+ :param subdomain_matching: consider the subdomain relative to
+ :data:`SERVER_NAME` when matching routes. Defaults to False.
+ :param template_folder: the folder that contains the templates that should
+ be used by the application. Defaults to
+ ``'templates'`` folder in the root path of the
+ application.
+ :param instance_path: An alternative instance path for the application.
+ By default the folder ``'instance'`` next to the
+ package or module is assumed to be the instance
+ path.
+ :param instance_relative_config: if set to ``True`` relative filenames
+ for loading the config are assumed to
+ be relative to the instance path instead
+ of the application root.
+ :param root_path: The path to the root of the application files.
+ This should only be set manually when it can't be detected
+ automatically, such as for namespace packages.
+ """
+
+ default_config = ImmutableDict(
+ {
+ "DEBUG": None,
+ "TESTING": False,
+ "PROPAGATE_EXCEPTIONS": None,
+ "SECRET_KEY": None,
+ "SECRET_KEY_FALLBACKS": None,
+ "PERMANENT_SESSION_LIFETIME": timedelta(days=31),
+ "USE_X_SENDFILE": False,
+ "TRUSTED_HOSTS": None,
+ "SERVER_NAME": None,
+ "APPLICATION_ROOT": "/",
+ "SESSION_COOKIE_NAME": "session",
+ "SESSION_COOKIE_DOMAIN": None,
+ "SESSION_COOKIE_PATH": None,
+ "SESSION_COOKIE_HTTPONLY": True,
+ "SESSION_COOKIE_SECURE": False,
+ "SESSION_COOKIE_PARTITIONED": False,
+ "SESSION_COOKIE_SAMESITE": None,
+ "SESSION_REFRESH_EACH_REQUEST": True,
+ "MAX_CONTENT_LENGTH": None,
+ "MAX_FORM_MEMORY_SIZE": 500_000,
+ "MAX_FORM_PARTS": 1_000,
+ "SEND_FILE_MAX_AGE_DEFAULT": None,
+ "TRAP_BAD_REQUEST_ERRORS": None,
+ "TRAP_HTTP_EXCEPTIONS": False,
+ "EXPLAIN_TEMPLATE_LOADING": False,
+ "PREFERRED_URL_SCHEME": "http",
+ "TEMPLATES_AUTO_RELOAD": None,
+ "MAX_COOKIE_SIZE": 4093,
+ "PROVIDE_AUTOMATIC_OPTIONS": True,
+ }
+ )
+
+ #: The class that is used for request objects. See :class:`~flask.Request`
+ #: for more information.
+ request_class: type[Request] = Request
+
+ #: The class that is used for response objects. See
+ #: :class:`~flask.Response` for more information.
+ response_class: type[Response] = Response
+
+ #: the session interface to use. By default an instance of
+ #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here.
+ #:
+ #: .. versionadded:: 0.8
+ session_interface: SessionInterface = SecureCookieSessionInterface()
+
+ def __init__(
+ self,
+ import_name: str,
+ static_url_path: str | None = None,
+ static_folder: str | os.PathLike[str] | None = "static",
+ static_host: str | None = None,
+ host_matching: bool = False,
+ subdomain_matching: bool = False,
+ template_folder: str | os.PathLike[str] | None = "templates",
+ instance_path: str | None = None,
+ instance_relative_config: bool = False,
+ root_path: str | None = None,
+ ):
+ super().__init__(
+ import_name=import_name,
+ static_url_path=static_url_path,
+ static_folder=static_folder,
+ static_host=static_host,
+ host_matching=host_matching,
+ subdomain_matching=subdomain_matching,
+ template_folder=template_folder,
+ instance_path=instance_path,
+ instance_relative_config=instance_relative_config,
+ root_path=root_path,
+ )
+
+ #: The Click command group for registering CLI commands for this
+ #: object. The commands are available from the ``flask`` command
+ #: once the application has been discovered and blueprints have
+ #: been registered.
+ self.cli = cli.AppGroup()
+
+ # Set the name of the Click group in case someone wants to add
+ # the app's commands to another CLI tool.
+ self.cli.name = self.name
+
+ # Add a static route using the provided static_url_path, static_host,
+ # and static_folder if there is a configured static_folder.
+ # Note we do this without checking if static_folder exists.
+ # For one, it might be created while the server is running (e.g. during
+ # development). Also, Google App Engine stores static files somewhere
+ if self.has_static_folder:
+ assert bool(static_host) == host_matching, (
+ "Invalid static_host/host_matching combination"
+ )
+ # Use a weakref to avoid creating a reference cycle between the app
+ # and the view function (see #3761).
+ self_ref = weakref.ref(self)
+ self.add_url_rule(
+ f"{self.static_url_path}/",
+ endpoint="static",
+ host=static_host,
+ view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore
+ )
+
+ def get_send_file_max_age(self, filename: str | None) -> int | None:
+ """Used by :func:`send_file` to determine the ``max_age`` cache
+ value for a given file path if it wasn't passed.
+
+ By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from
+ the configuration of :data:`~flask.current_app`. This defaults
+ to ``None``, which tells the browser to use conditional requests
+ instead of a timed cache, which is usually preferable.
+
+ Note this is a duplicate of the same method in the Flask
+ class.
+
+ .. versionchanged:: 2.0
+ The default configuration is ``None`` instead of 12 hours.
+
+ .. versionadded:: 0.9
+ """
+ value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"]
+
+ if value is None:
+ return None
+
+ if isinstance(value, timedelta):
+ return int(value.total_seconds())
+
+ return value # type: ignore[no-any-return]
+
+ def send_static_file(self, filename: str) -> Response:
+ """The view function used to serve files from
+ :attr:`static_folder`. A route is automatically registered for
+ this view at :attr:`static_url_path` if :attr:`static_folder` is
+ set.
+
+ Note this is a duplicate of the same method in the Flask
+ class.
+
+ .. versionadded:: 0.5
+
+ """
+ if not self.has_static_folder:
+ raise RuntimeError("'static_folder' must be set to serve static_files.")
+
+ # send_file only knows to call get_send_file_max_age on the app,
+ # call it here so it works for blueprints too.
+ max_age = self.get_send_file_max_age(filename)
+ return send_from_directory(
+ t.cast(str, self.static_folder), filename, max_age=max_age
+ )
+
+ def open_resource(
+ self, resource: str, mode: str = "rb", encoding: str | None = None
+ ) -> t.IO[t.AnyStr]:
+ """Open a resource file relative to :attr:`root_path` for reading.
+
+ For example, if the file ``schema.sql`` is next to the file
+ ``app.py`` where the ``Flask`` app is defined, it can be opened
+ with:
+
+ .. code-block:: python
+
+ with app.open_resource("schema.sql") as f:
+ conn.executescript(f.read())
+
+ :param resource: Path to the resource relative to :attr:`root_path`.
+ :param mode: Open the file in this mode. Only reading is supported,
+ valid values are ``"r"`` (or ``"rt"``) and ``"rb"``.
+ :param encoding: Open the file with this encoding when opening in text
+ mode. This is ignored when opening in binary mode.
+
+ .. versionchanged:: 3.1
+ Added the ``encoding`` parameter.
+ """
+ if mode not in {"r", "rt", "rb"}:
+ raise ValueError("Resources can only be opened for reading.")
+
+ path = os.path.join(self.root_path, resource)
+
+ if mode == "rb":
+ return open(path, mode) # pyright: ignore
+
+ return open(path, mode, encoding=encoding)
+
+ def open_instance_resource(
+ self, resource: str, mode: str = "rb", encoding: str | None = "utf-8"
+ ) -> t.IO[t.AnyStr]:
+ """Open a resource file relative to the application's instance folder
+ :attr:`instance_path`. Unlike :meth:`open_resource`, files in the
+ instance folder can be opened for writing.
+
+ :param resource: Path to the resource relative to :attr:`instance_path`.
+ :param mode: Open the file in this mode.
+ :param encoding: Open the file with this encoding when opening in text
+ mode. This is ignored when opening in binary mode.
+
+ .. versionchanged:: 3.1
+ Added the ``encoding`` parameter.
+ """
+ path = os.path.join(self.instance_path, resource)
+
+ if "b" in mode:
+ return open(path, mode)
+
+ return open(path, mode, encoding=encoding)
+
+ def create_jinja_environment(self) -> Environment:
+ """Create the Jinja environment based on :attr:`jinja_options`
+ and the various Jinja-related methods of the app. Changing
+ :attr:`jinja_options` after this will have no effect. Also adds
+ Flask-related globals and filters to the environment.
+
+ .. versionchanged:: 0.11
+ ``Environment.auto_reload`` set in accordance with
+ ``TEMPLATES_AUTO_RELOAD`` configuration option.
+
+ .. versionadded:: 0.5
+ """
+ options = dict(self.jinja_options)
+
+ if "autoescape" not in options:
+ options["autoescape"] = self.select_jinja_autoescape
+
+ if "auto_reload" not in options:
+ auto_reload = self.config["TEMPLATES_AUTO_RELOAD"]
+
+ if auto_reload is None:
+ auto_reload = self.debug
+
+ options["auto_reload"] = auto_reload
+
+ rv = self.jinja_environment(self, **options)
+ rv.globals.update(
+ url_for=self.url_for,
+ get_flashed_messages=get_flashed_messages,
+ config=self.config,
+ # request, session and g are normally added with the
+ # context processor for efficiency reasons but for imported
+ # templates we also want the proxies in there.
+ request=request,
+ session=session,
+ g=g,
+ )
+ rv.policies["json.dumps_function"] = self.json.dumps
+ return rv
+
+ def create_url_adapter(self, request: Request | None) -> MapAdapter | None:
+ """Creates a URL adapter for the given request. The URL adapter
+ is created at a point where the request context is not yet set
+ up so the request is passed explicitly.
+
+ .. versionchanged:: 3.1
+ If :data:`SERVER_NAME` is set, it does not restrict requests to
+ only that domain, for both ``subdomain_matching`` and
+ ``host_matching``.
+
+ .. versionchanged:: 1.0
+ :data:`SERVER_NAME` no longer implicitly enables subdomain
+ matching. Use :attr:`subdomain_matching` instead.
+
+ .. versionchanged:: 0.9
+ This can be called outside a request when the URL adapter is created
+ for an application context.
+
+ .. versionadded:: 0.6
+ """
+ if request is not None:
+ if (trusted_hosts := self.config["TRUSTED_HOSTS"]) is not None:
+ request.trusted_hosts = trusted_hosts
+
+ # Check trusted_hosts here until bind_to_environ does.
+ request.host = get_host(request.environ, request.trusted_hosts) # pyright: ignore
+ subdomain = None
+ server_name = self.config["SERVER_NAME"]
+
+ if self.url_map.host_matching:
+ # Don't pass SERVER_NAME, otherwise it's used and the actual
+ # host is ignored, which breaks host matching.
+ server_name = None
+ elif not self.subdomain_matching:
+ # Werkzeug doesn't implement subdomain matching yet. Until then,
+ # disable it by forcing the current subdomain to the default, or
+ # the empty string.
+ subdomain = self.url_map.default_subdomain or ""
+
+ return self.url_map.bind_to_environ(
+ request.environ, server_name=server_name, subdomain=subdomain
+ )
+
+ # Need at least SERVER_NAME to match/build outside a request.
+ if self.config["SERVER_NAME"] is not None:
+ return self.url_map.bind(
+ self.config["SERVER_NAME"],
+ script_name=self.config["APPLICATION_ROOT"],
+ url_scheme=self.config["PREFERRED_URL_SCHEME"],
+ )
+
+ return None
+
+ def raise_routing_exception(self, request: Request) -> t.NoReturn:
+ """Intercept routing exceptions and possibly do something else.
+
+ In debug mode, intercept a routing redirect and replace it with
+ an error if the body will be discarded.
+
+ With modern Werkzeug this shouldn't occur, since it now uses a
+ 308 status which tells the browser to resend the method and
+ body.
+
+ .. versionchanged:: 2.1
+ Don't intercept 307 and 308 redirects.
+
+ :meta private:
+ :internal:
+ """
+ if (
+ not self.debug
+ or not isinstance(request.routing_exception, RequestRedirect)
+ or request.routing_exception.code in {307, 308}
+ or request.method in {"GET", "HEAD", "OPTIONS"}
+ ):
+ raise request.routing_exception # type: ignore[misc]
+
+ from .debughelpers import FormDataRoutingRedirect
+
+ raise FormDataRoutingRedirect(request)
+
+ def update_template_context(self, context: dict[str, t.Any]) -> None:
+ """Update the template context with some commonly used variables.
+ This injects request, session, config and g into the template
+ context as well as everything template context processors want
+ to inject. Note that the as of Flask 0.6, the original values
+ in the context will not be overridden if a context processor
+ decides to return a value with the same key.
+
+ :param context: the context as a dictionary that is updated in place
+ to add extra variables.
+ """
+ names: t.Iterable[str | None] = (None,)
+
+ # A template may be rendered outside a request context.
+ if request:
+ names = chain(names, reversed(request.blueprints))
+
+ # The values passed to render_template take precedence. Keep a
+ # copy to re-apply after all context functions.
+ orig_ctx = context.copy()
+
+ for name in names:
+ if name in self.template_context_processors:
+ for func in self.template_context_processors[name]:
+ context.update(self.ensure_sync(func)())
+
+ context.update(orig_ctx)
+
+ def make_shell_context(self) -> dict[str, t.Any]:
+ """Returns the shell context for an interactive shell for this
+ application. This runs all the registered shell context
+ processors.
+
+ .. versionadded:: 0.11
+ """
+ rv = {"app": self, "g": g}
+ for processor in self.shell_context_processors:
+ rv.update(processor())
+ return rv
+
+ def run(
+ self,
+ host: str | None = None,
+ port: int | None = None,
+ debug: bool | None = None,
+ load_dotenv: bool = True,
+ **options: t.Any,
+ ) -> None:
+ """Runs the application on a local development server.
+
+ Do not use ``run()`` in a production setting. It is not intended to
+ meet security and performance requirements for a production server.
+ Instead, see :doc:`/deploying/index` for WSGI server recommendations.
+
+ If the :attr:`debug` flag is set the server will automatically reload
+ for code changes and show a debugger in case an exception happened.
+
+ If you want to run the application in debug mode, but disable the
+ code execution on the interactive debugger, you can pass
+ ``use_evalex=False`` as parameter. This will keep the debugger's
+ traceback screen active, but disable code execution.
+
+ It is not recommended to use this function for development with
+ automatic reloading as this is badly supported. Instead you should
+ be using the :command:`flask` command line script's ``run`` support.
+
+ .. admonition:: Keep in Mind
+
+ Flask will suppress any server error with a generic error page
+ unless it is in debug mode. As such to enable just the
+ interactive debugger without the code reloading, you have to
+ invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``.
+ Setting ``use_debugger`` to ``True`` without being in debug mode
+ won't catch any exceptions because there won't be any to
+ catch.
+
+ :param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to
+ have the server available externally as well. Defaults to
+ ``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config variable
+ if present.
+ :param port: the port of the webserver. Defaults to ``5000`` or the
+ port defined in the ``SERVER_NAME`` config variable if present.
+ :param debug: if given, enable or disable debug mode. See
+ :attr:`debug`.
+ :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv`
+ files to set environment variables. Will also change the working
+ directory to the directory containing the first file found.
+ :param options: the options to be forwarded to the underlying Werkzeug
+ server. See :func:`werkzeug.serving.run_simple` for more
+ information.
+
+ .. versionchanged:: 1.0
+ If installed, python-dotenv will be used to load environment
+ variables from :file:`.env` and :file:`.flaskenv` files.
+
+ The :envvar:`FLASK_DEBUG` environment variable will override :attr:`debug`.
+
+ Threaded mode is enabled by default.
+
+ .. versionchanged:: 0.10
+ The default port is now picked from the ``SERVER_NAME``
+ variable.
+ """
+ # Ignore this call so that it doesn't start another server if
+ # the 'flask run' command is used.
+ if os.environ.get("FLASK_RUN_FROM_CLI") == "true":
+ if not is_running_from_reloader():
+ click.secho(
+ " * Ignoring a call to 'app.run()' that would block"
+ " the current 'flask' CLI command.\n"
+ " Only call 'app.run()' in an 'if __name__ =="
+ ' "__main__"\' guard.',
+ fg="red",
+ )
+
+ return
+
+ if get_load_dotenv(load_dotenv):
+ cli.load_dotenv()
+
+ # if set, env var overrides existing value
+ if "FLASK_DEBUG" in os.environ:
+ self.debug = get_debug_flag()
+
+ # debug passed to method overrides all other sources
+ if debug is not None:
+ self.debug = bool(debug)
+
+ server_name = self.config.get("SERVER_NAME")
+ sn_host = sn_port = None
+
+ if server_name:
+ sn_host, _, sn_port = server_name.partition(":")
+
+ if not host:
+ if sn_host:
+ host = sn_host
+ else:
+ host = "127.0.0.1"
+
+ if port or port == 0:
+ port = int(port)
+ elif sn_port:
+ port = int(sn_port)
+ else:
+ port = 5000
+
+ options.setdefault("use_reloader", self.debug)
+ options.setdefault("use_debugger", self.debug)
+ options.setdefault("threaded", True)
+
+ cli.show_server_banner(self.debug, self.name)
+
+ from werkzeug.serving import run_simple
+
+ try:
+ run_simple(t.cast(str, host), port, self, **options)
+ finally:
+ # reset the first request information if the development server
+ # reset normally. This makes it possible to restart the server
+ # without reloader and that stuff from an interactive shell.
+ self._got_first_request = False
+
+ def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> FlaskClient:
+ """Creates a test client for this application. For information
+ about unit testing head over to :doc:`/testing`.
+
+ Note that if you are testing for assertions or exceptions in your
+ application code, you must set ``app.testing = True`` in order for the
+ exceptions to propagate to the test client. Otherwise, the exception
+ will be handled by the application (not visible to the test client) and
+ the only indication of an AssertionError or other exception will be a
+ 500 status code response to the test client. See the :attr:`testing`
+ attribute. For example::
+
+ app.testing = True
+ client = app.test_client()
+
+ The test client can be used in a ``with`` block to defer the closing down
+ of the context until the end of the ``with`` block. This is useful if
+ you want to access the context locals for testing::
+
+ with app.test_client() as c:
+ rv = c.get('/?vodka=42')
+ assert request.args['vodka'] == '42'
+
+ Additionally, you may pass optional keyword arguments that will then
+ be passed to the application's :attr:`test_client_class` constructor.
+ For example::
+
+ from flask.testing import FlaskClient
+
+ class CustomClient(FlaskClient):
+ def __init__(self, *args, **kwargs):
+ self._authentication = kwargs.pop("authentication")
+ super(CustomClient,self).__init__( *args, **kwargs)
+
+ app.test_client_class = CustomClient
+ client = app.test_client(authentication='Basic ....')
+
+ See :class:`~flask.testing.FlaskClient` for more information.
+
+ .. versionchanged:: 0.4
+ added support for ``with`` block usage for the client.
+
+ .. versionadded:: 0.7
+ The `use_cookies` parameter was added as well as the ability
+ to override the client to be used by setting the
+ :attr:`test_client_class` attribute.
+
+ .. versionchanged:: 0.11
+ Added `**kwargs` to support passing additional keyword arguments to
+ the constructor of :attr:`test_client_class`.
+ """
+ cls = self.test_client_class
+ if cls is None:
+ from .testing import FlaskClient as cls
+ return cls( # type: ignore
+ self, self.response_class, use_cookies=use_cookies, **kwargs
+ )
+
+ def test_cli_runner(self, **kwargs: t.Any) -> FlaskCliRunner:
+ """Create a CLI runner for testing CLI commands.
+ See :ref:`testing-cli`.
+
+ Returns an instance of :attr:`test_cli_runner_class`, by default
+ :class:`~flask.testing.FlaskCliRunner`. The Flask app object is
+ passed as the first argument.
+
+ .. versionadded:: 1.0
+ """
+ cls = self.test_cli_runner_class
+
+ if cls is None:
+ from .testing import FlaskCliRunner as cls
+
+ return cls(self, **kwargs) # type: ignore
+
+ def handle_http_exception(
+ self, e: HTTPException
+ ) -> HTTPException | ft.ResponseReturnValue:
+ """Handles an HTTP exception. By default this will invoke the
+ registered error handlers and fall back to returning the
+ exception as response.
+
+ .. versionchanged:: 1.0.3
+ ``RoutingException``, used internally for actions such as
+ slash redirects during routing, is not passed to error
+ handlers.
+
+ .. versionchanged:: 1.0
+ Exceptions are looked up by code *and* by MRO, so
+ ``HTTPException`` subclasses can be handled with a catch-all
+ handler for the base ``HTTPException``.
+
+ .. versionadded:: 0.3
+ """
+ # Proxy exceptions don't have error codes. We want to always return
+ # those unchanged as errors
+ if e.code is None:
+ return e
+
+ # RoutingExceptions are used internally to trigger routing
+ # actions, such as slash redirects raising RequestRedirect. They
+ # are not raised or handled in user code.
+ if isinstance(e, RoutingException):
+ return e
+
+ handler = self._find_error_handler(e, request.blueprints)
+ if handler is None:
+ return e
+ return self.ensure_sync(handler)(e) # type: ignore[no-any-return]
+
+ def handle_user_exception(
+ self, e: Exception
+ ) -> HTTPException | ft.ResponseReturnValue:
+ """This method is called whenever an exception occurs that
+ should be handled. A special case is :class:`~werkzeug
+ .exceptions.HTTPException` which is forwarded to the
+ :meth:`handle_http_exception` method. This function will either
+ return a response value or reraise the exception with the same
+ traceback.
+
+ .. versionchanged:: 1.0
+ Key errors raised from request data like ``form`` show the
+ bad key in debug mode rather than a generic bad request
+ message.
+
+ .. versionadded:: 0.7
+ """
+ if isinstance(e, BadRequestKeyError) and (
+ self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"]
+ ):
+ e.show_exception = True
+
+ if isinstance(e, HTTPException) and not self.trap_http_exception(e):
+ return self.handle_http_exception(e)
+
+ handler = self._find_error_handler(e, request.blueprints)
+
+ if handler is None:
+ raise
+
+ return self.ensure_sync(handler)(e) # type: ignore[no-any-return]
+
+ def handle_exception(self, e: Exception) -> Response:
+ """Handle an exception that did not have an error handler
+ associated with it, or that was raised from an error handler.
+ This always causes a 500 ``InternalServerError``.
+
+ Always sends the :data:`got_request_exception` signal.
+
+ If :data:`PROPAGATE_EXCEPTIONS` is ``True``, such as in debug
+ mode, the error will be re-raised so that the debugger can
+ display it. Otherwise, the original exception is logged, and
+ an :exc:`~werkzeug.exceptions.InternalServerError` is returned.
+
+ If an error handler is registered for ``InternalServerError`` or
+ ``500``, it will be used. For consistency, the handler will
+ always receive the ``InternalServerError``. The original
+ unhandled exception is available as ``e.original_exception``.
+
+ .. versionchanged:: 1.1.0
+ Always passes the ``InternalServerError`` instance to the
+ handler, setting ``original_exception`` to the unhandled
+ error.
+
+ .. versionchanged:: 1.1.0
+ ``after_request`` functions and other finalization is done
+ even for the default 500 response when there is no handler.
+
+ .. versionadded:: 0.3
+ """
+ exc_info = sys.exc_info()
+ got_request_exception.send(self, _async_wrapper=self.ensure_sync, exception=e)
+ propagate = self.config["PROPAGATE_EXCEPTIONS"]
+
+ if propagate is None:
+ propagate = self.testing or self.debug
+
+ if propagate:
+ # Re-raise if called with an active exception, otherwise
+ # raise the passed in exception.
+ if exc_info[1] is e:
+ raise
+
+ raise e
+
+ self.log_exception(exc_info)
+ server_error: InternalServerError | ft.ResponseReturnValue
+ server_error = InternalServerError(original_exception=e)
+ handler = self._find_error_handler(server_error, request.blueprints)
+
+ if handler is not None:
+ server_error = self.ensure_sync(handler)(server_error)
+
+ return self.finalize_request(server_error, from_error_handler=True)
+
+ def log_exception(
+ self,
+ exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]),
+ ) -> None:
+ """Logs an exception. This is called by :meth:`handle_exception`
+ if debugging is disabled and right before the handler is called.
+ The default implementation logs the exception as error on the
+ :attr:`logger`.
+
+ .. versionadded:: 0.8
+ """
+ self.logger.error(
+ f"Exception on {request.path} [{request.method}]", exc_info=exc_info
+ )
+
+ def dispatch_request(self) -> ft.ResponseReturnValue:
+ """Does the request dispatching. Matches the URL and returns the
+ return value of the view or error handler. This does not have to
+ be a response object. In order to convert the return value to a
+ proper response object, call :func:`make_response`.
+
+ .. versionchanged:: 0.7
+ This no longer does the exception handling, this code was
+ moved to the new :meth:`full_dispatch_request`.
+ """
+ req = request_ctx.request
+ if req.routing_exception is not None:
+ self.raise_routing_exception(req)
+ rule: Rule = req.url_rule # type: ignore[assignment]
+ # if we provide automatic options for this URL and the
+ # request came with the OPTIONS method, reply automatically
+ if (
+ getattr(rule, "provide_automatic_options", False)
+ and req.method == "OPTIONS"
+ ):
+ return self.make_default_options_response()
+ # otherwise dispatch to the handler for that endpoint
+ view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment]
+ return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
+
+ def full_dispatch_request(self) -> Response:
+ """Dispatches the request and on top of that performs request
+ pre and postprocessing as well as HTTP exception catching and
+ error handling.
+
+ .. versionadded:: 0.7
+ """
+ self._got_first_request = True
+
+ try:
+ request_started.send(self, _async_wrapper=self.ensure_sync)
+ rv = self.preprocess_request()
+ if rv is None:
+ rv = self.dispatch_request()
+ except Exception as e:
+ rv = self.handle_user_exception(e)
+ return self.finalize_request(rv)
+
+ def finalize_request(
+ self,
+ rv: ft.ResponseReturnValue | HTTPException,
+ from_error_handler: bool = False,
+ ) -> Response:
+ """Given the return value from a view function this finalizes
+ the request by converting it into a response and invoking the
+ postprocessing functions. This is invoked for both normal
+ request dispatching as well as error handlers.
+
+ Because this means that it might be called as a result of a
+ failure a special safe mode is available which can be enabled
+ with the `from_error_handler` flag. If enabled, failures in
+ response processing will be logged and otherwise ignored.
+
+ :internal:
+ """
+ response = self.make_response(rv)
+ try:
+ response = self.process_response(response)
+ request_finished.send(
+ self, _async_wrapper=self.ensure_sync, response=response
+ )
+ except Exception:
+ if not from_error_handler:
+ raise
+ self.logger.exception(
+ "Request finalizing failed with an error while handling an error"
+ )
+ return response
+
+ def make_default_options_response(self) -> Response:
+ """This method is called to create the default ``OPTIONS`` response.
+ This can be changed through subclassing to change the default
+ behavior of ``OPTIONS`` responses.
+
+ .. versionadded:: 0.7
+ """
+ adapter = request_ctx.url_adapter
+ methods = adapter.allowed_methods() # type: ignore[union-attr]
+ rv = self.response_class()
+ rv.allow.update(methods)
+ return rv
+
+ def ensure_sync(self, func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
+ """Ensure that the function is synchronous for WSGI workers.
+ Plain ``def`` functions are returned as-is. ``async def``
+ functions are wrapped to run and wait for the response.
+
+ Override this method to change how the app runs async views.
+
+ .. versionadded:: 2.0
+ """
+ if iscoroutinefunction(func):
+ return self.async_to_sync(func)
+
+ return func
+
+ def async_to_sync(
+ self, func: t.Callable[..., t.Coroutine[t.Any, t.Any, t.Any]]
+ ) -> t.Callable[..., t.Any]:
+ """Return a sync function that will run the coroutine function.
+
+ .. code-block:: python
+
+ result = app.async_to_sync(func)(*args, **kwargs)
+
+ Override this method to change how the app converts async code
+ to be synchronously callable.
+
+ .. versionadded:: 2.0
+ """
+ try:
+ from asgiref.sync import async_to_sync as asgiref_async_to_sync
+ except ImportError:
+ raise RuntimeError(
+ "Install Flask with the 'async' extra in order to use async views."
+ ) from None
+
+ return asgiref_async_to_sync(func)
+
+ def url_for(
+ self,
+ /,
+ endpoint: str,
+ *,
+ _anchor: str | None = None,
+ _method: str | None = None,
+ _scheme: str | None = None,
+ _external: bool | None = None,
+ **values: t.Any,
+ ) -> str:
+ """Generate a URL to the given endpoint with the given values.
+
+ This is called by :func:`flask.url_for`, and can be called
+ directly as well.
+
+ An *endpoint* is the name of a URL rule, usually added with
+ :meth:`@app.route() `, and usually the same name as the
+ view function. A route defined in a :class:`~flask.Blueprint`
+ will prepend the blueprint's name separated by a ``.`` to the
+ endpoint.
+
+ In some cases, such as email messages, you want URLs to include
+ the scheme and domain, like ``https://example.com/hello``. When
+ not in an active request, URLs will be external by default, but
+ this requires setting :data:`SERVER_NAME` so Flask knows what
+ domain to use. :data:`APPLICATION_ROOT` and
+ :data:`PREFERRED_URL_SCHEME` should also be configured as
+ needed. This config is only used when not in an active request.
+
+ Functions can be decorated with :meth:`url_defaults` to modify
+ keyword arguments before the URL is built.
+
+ If building fails for some reason, such as an unknown endpoint
+ or incorrect values, the app's :meth:`handle_url_build_error`
+ method is called. If that returns a string, that is returned,
+ otherwise a :exc:`~werkzeug.routing.BuildError` is raised.
+
+ :param endpoint: The endpoint name associated with the URL to
+ generate. If this starts with a ``.``, the current blueprint
+ name (if any) will be used.
+ :param _anchor: If given, append this as ``#anchor`` to the URL.
+ :param _method: If given, generate the URL associated with this
+ method for the endpoint.
+ :param _scheme: If given, the URL will have this scheme if it
+ is external.
+ :param _external: If given, prefer the URL to be internal
+ (False) or require it to be external (True). External URLs
+ include the scheme and domain. When not in an active
+ request, URLs are external by default.
+ :param values: Values to use for the variable parts of the URL
+ rule. Unknown keys are appended as query string arguments,
+ like ``?a=b&c=d``.
+
+ .. versionadded:: 2.2
+ Moved from ``flask.url_for``, which calls this method.
+ """
+ req_ctx = _cv_request.get(None)
+
+ if req_ctx is not None:
+ url_adapter = req_ctx.url_adapter
+ blueprint_name = req_ctx.request.blueprint
+
+ # If the endpoint starts with "." and the request matches a
+ # blueprint, the endpoint is relative to the blueprint.
+ if endpoint[:1] == ".":
+ if blueprint_name is not None:
+ endpoint = f"{blueprint_name}{endpoint}"
+ else:
+ endpoint = endpoint[1:]
+
+ # When in a request, generate a URL without scheme and
+ # domain by default, unless a scheme is given.
+ if _external is None:
+ _external = _scheme is not None
+ else:
+ app_ctx = _cv_app.get(None)
+
+ # If called by helpers.url_for, an app context is active,
+ # use its url_adapter. Otherwise, app.url_for was called
+ # directly, build an adapter.
+ if app_ctx is not None:
+ url_adapter = app_ctx.url_adapter
+ else:
+ url_adapter = self.create_url_adapter(None)
+
+ if url_adapter is None:
+ raise RuntimeError(
+ "Unable to build URLs outside an active request"
+ " without 'SERVER_NAME' configured. Also configure"
+ " 'APPLICATION_ROOT' and 'PREFERRED_URL_SCHEME' as"
+ " needed."
+ )
+
+ # When outside a request, generate a URL with scheme and
+ # domain by default.
+ if _external is None:
+ _external = True
+
+ # It is an error to set _scheme when _external=False, in order
+ # to avoid accidental insecure URLs.
+ if _scheme is not None and not _external:
+ raise ValueError("When specifying '_scheme', '_external' must be True.")
+
+ self.inject_url_defaults(endpoint, values)
+
+ try:
+ rv = url_adapter.build( # type: ignore[union-attr]
+ endpoint,
+ values,
+ method=_method,
+ url_scheme=_scheme,
+ force_external=_external,
+ )
+ except BuildError as error:
+ values.update(
+ _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external
+ )
+ return self.handle_url_build_error(error, endpoint, values)
+
+ if _anchor is not None:
+ _anchor = _url_quote(_anchor, safe="%!#$&'()*+,/:;=?@")
+ rv = f"{rv}#{_anchor}"
+
+ return rv
+
+ def make_response(self, rv: ft.ResponseReturnValue) -> Response:
+ """Convert the return value from a view function to an instance of
+ :attr:`response_class`.
+
+ :param rv: the return value from the view function. The view function
+ must return a response. Returning ``None``, or the view ending
+ without returning, is not allowed. The following types are allowed
+ for ``view_rv``:
+
+ ``str``
+ A response object is created with the string encoded to UTF-8
+ as the body.
+
+ ``bytes``
+ A response object is created with the bytes as the body.
+
+ ``dict``
+ A dictionary that will be jsonify'd before being returned.
+
+ ``list``
+ A list that will be jsonify'd before being returned.
+
+ ``generator`` or ``iterator``
+ A generator that returns ``str`` or ``bytes`` to be
+ streamed as the response.
+
+ ``tuple``
+ Either ``(body, status, headers)``, ``(body, status)``, or
+ ``(body, headers)``, where ``body`` is any of the other types
+ allowed here, ``status`` is a string or an integer, and
+ ``headers`` is a dictionary or a list of ``(key, value)``
+ tuples. If ``body`` is a :attr:`response_class` instance,
+ ``status`` overwrites the exiting value and ``headers`` are
+ extended.
+
+ :attr:`response_class`
+ The object is returned unchanged.
+
+ other :class:`~werkzeug.wrappers.Response` class
+ The object is coerced to :attr:`response_class`.
+
+ :func:`callable`
+ The function is called as a WSGI application. The result is
+ used to create a response object.
+
+ .. versionchanged:: 2.2
+ A generator will be converted to a streaming response.
+ A list will be converted to a JSON response.
+
+ .. versionchanged:: 1.1
+ A dict will be converted to a JSON response.
+
+ .. versionchanged:: 0.9
+ Previously a tuple was interpreted as the arguments for the
+ response object.
+ """
+
+ status: int | None = None
+ headers: HeadersValue | None = None
+
+ # unpack tuple returns
+ if isinstance(rv, tuple):
+ len_rv = len(rv)
+
+ # a 3-tuple is unpacked directly
+ if len_rv == 3:
+ rv, status, headers = rv # type: ignore[misc]
+ # decide if a 2-tuple has status or headers
+ elif len_rv == 2:
+ if isinstance(rv[1], (Headers, dict, tuple, list)):
+ rv, headers = rv # pyright: ignore
+ else:
+ rv, status = rv # type: ignore[assignment,misc]
+ # other sized tuples are not allowed
+ else:
+ raise TypeError(
+ "The view function did not return a valid response tuple."
+ " The tuple must have the form (body, status, headers),"
+ " (body, status), or (body, headers)."
+ )
+
+ # the body must not be None
+ if rv is None:
+ raise TypeError(
+ f"The view function for {request.endpoint!r} did not"
+ " return a valid response. The function either returned"
+ " None or ended without a return statement."
+ )
+
+ # make sure the body is an instance of the response class
+ if not isinstance(rv, self.response_class):
+ if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, cabc.Iterator):
+ # let the response class set the status and headers instead of
+ # waiting to do it manually, so that the class can handle any
+ # special logic
+ rv = self.response_class(
+ rv, # pyright: ignore
+ status=status,
+ headers=headers, # type: ignore[arg-type]
+ )
+ status = headers = None
+ elif isinstance(rv, (dict, list)):
+ rv = self.json.response(rv)
+ elif isinstance(rv, BaseResponse) or callable(rv):
+ # evaluate a WSGI callable, or coerce a different response
+ # class to the correct type
+ try:
+ rv = self.response_class.force_type(
+ rv, # type: ignore[arg-type]
+ request.environ,
+ )
+ except TypeError as e:
+ raise TypeError(
+ f"{e}\nThe view function did not return a valid"
+ " response. The return type must be a string,"
+ " dict, list, tuple with headers or status,"
+ " Response instance, or WSGI callable, but it"
+ f" was a {type(rv).__name__}."
+ ).with_traceback(sys.exc_info()[2]) from None
+ else:
+ raise TypeError(
+ "The view function did not return a valid"
+ " response. The return type must be a string,"
+ " dict, list, tuple with headers or status,"
+ " Response instance, or WSGI callable, but it was a"
+ f" {type(rv).__name__}."
+ )
+
+ rv = t.cast(Response, rv)
+ # prefer the status if it was provided
+ if status is not None:
+ if isinstance(status, (str, bytes, bytearray)):
+ rv.status = status
+ else:
+ rv.status_code = status
+
+ # extend existing headers with provided headers
+ if headers:
+ rv.headers.update(headers)
+
+ return rv
+
+ def preprocess_request(self) -> ft.ResponseReturnValue | None:
+ """Called before the request is dispatched. Calls
+ :attr:`url_value_preprocessors` registered with the app and the
+ current blueprint (if any). Then calls :attr:`before_request_funcs`
+ registered with the app and the blueprint.
+
+ If any :meth:`before_request` handler returns a non-None value, the
+ value is handled as if it was the return value from the view, and
+ further request handling is stopped.
+ """
+ names = (None, *reversed(request.blueprints))
+
+ for name in names:
+ if name in self.url_value_preprocessors:
+ for url_func in self.url_value_preprocessors[name]:
+ url_func(request.endpoint, request.view_args)
+
+ for name in names:
+ if name in self.before_request_funcs:
+ for before_func in self.before_request_funcs[name]:
+ rv = self.ensure_sync(before_func)()
+
+ if rv is not None:
+ return rv # type: ignore[no-any-return]
+
+ return None
+
+ def process_response(self, response: Response) -> Response:
+ """Can be overridden in order to modify the response object
+ before it's sent to the WSGI server. By default this will
+ call all the :meth:`after_request` decorated functions.
+
+ .. versionchanged:: 0.5
+ As of Flask 0.5 the functions registered for after request
+ execution are called in reverse order of registration.
+
+ :param response: a :attr:`response_class` object.
+ :return: a new response object or the same, has to be an
+ instance of :attr:`response_class`.
+ """
+ ctx = request_ctx._get_current_object() # type: ignore[attr-defined]
+
+ for func in ctx._after_request_functions:
+ response = self.ensure_sync(func)(response)
+
+ for name in chain(request.blueprints, (None,)):
+ if name in self.after_request_funcs:
+ for func in reversed(self.after_request_funcs[name]):
+ response = self.ensure_sync(func)(response)
+
+ if not self.session_interface.is_null_session(ctx._session):
+ self.session_interface.save_session(self, ctx._session, response)
+
+ return response
+
+ def do_teardown_request(
+ self,
+ exc: BaseException | None = _sentinel, # type: ignore[assignment]
+ ) -> None:
+ """Called after the request is dispatched and the response is
+ returned, right before the request context is popped.
+
+ This calls all functions decorated with
+ :meth:`teardown_request`, and :meth:`Blueprint.teardown_request`
+ if a blueprint handled the request. Finally, the
+ :data:`request_tearing_down` signal is sent.
+
+ This is called by
+ :meth:`RequestContext.pop() `,
+ which may be delayed during testing to maintain access to
+ resources.
+
+ :param exc: An unhandled exception raised while dispatching the
+ request. Detected from the current exception information if
+ not passed. Passed to each teardown function.
+
+ .. versionchanged:: 0.9
+ Added the ``exc`` argument.
+ """
+ if exc is _sentinel:
+ exc = sys.exc_info()[1]
+
+ for name in chain(request.blueprints, (None,)):
+ if name in self.teardown_request_funcs:
+ for func in reversed(self.teardown_request_funcs[name]):
+ self.ensure_sync(func)(exc)
+
+ request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc)
+
+ def do_teardown_appcontext(
+ self,
+ exc: BaseException | None = _sentinel, # type: ignore[assignment]
+ ) -> None:
+ """Called right before the application context is popped.
+
+ When handling a request, the application context is popped
+ after the request context. See :meth:`do_teardown_request`.
+
+ This calls all functions decorated with
+ :meth:`teardown_appcontext`. Then the
+ :data:`appcontext_tearing_down` signal is sent.
+
+ This is called by
+ :meth:`AppContext.pop() `.
+
+ .. versionadded:: 0.9
+ """
+ if exc is _sentinel:
+ exc = sys.exc_info()[1]
+
+ for func in reversed(self.teardown_appcontext_funcs):
+ self.ensure_sync(func)(exc)
+
+ appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc)
+
+ def app_context(self) -> AppContext:
+ """Create an :class:`~flask.ctx.AppContext`. Use as a ``with``
+ block to push the context, which will make :data:`current_app`
+ point at this application.
+
+ An application context is automatically pushed by
+ :meth:`RequestContext.push() `
+ when handling a request, and when running a CLI command. Use
+ this to manually create a context outside of these situations.
+
+ ::
+
+ with app.app_context():
+ init_db()
+
+ See :doc:`/appcontext`.
+
+ .. versionadded:: 0.9
+ """
+ return AppContext(self)
+
+ def request_context(self, environ: WSGIEnvironment) -> RequestContext:
+ """Create a :class:`~flask.ctx.RequestContext` representing a
+ WSGI environment. Use a ``with`` block to push the context,
+ which will make :data:`request` point at this request.
+
+ See :doc:`/reqcontext`.
+
+ Typically you should not call this from your own code. A request
+ context is automatically pushed by the :meth:`wsgi_app` when
+ handling a request. Use :meth:`test_request_context` to create
+ an environment and context instead of this method.
+
+ :param environ: a WSGI environment
+ """
+ return RequestContext(self, environ)
+
+ def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext:
+ """Create a :class:`~flask.ctx.RequestContext` for a WSGI
+ environment created from the given values. This is mostly useful
+ during testing, where you may want to run a function that uses
+ request data without dispatching a full request.
+
+ See :doc:`/reqcontext`.
+
+ Use a ``with`` block to push the context, which will make
+ :data:`request` point at the request for the created
+ environment. ::
+
+ with app.test_request_context(...):
+ generate_report()
+
+ When using the shell, it may be easier to push and pop the
+ context manually to avoid indentation. ::
+
+ ctx = app.test_request_context(...)
+ ctx.push()
+ ...
+ ctx.pop()
+
+ Takes the same arguments as Werkzeug's
+ :class:`~werkzeug.test.EnvironBuilder`, with some defaults from
+ the application. See the linked Werkzeug docs for most of the
+ available arguments. Flask-specific behavior is listed here.
+
+ :param path: URL path being requested.
+ :param base_url: Base URL where the app is being served, which
+ ``path`` is relative to. If not given, built from
+ :data:`PREFERRED_URL_SCHEME`, ``subdomain``,
+ :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`.
+ :param subdomain: Subdomain name to append to
+ :data:`SERVER_NAME`.
+ :param url_scheme: Scheme to use instead of
+ :data:`PREFERRED_URL_SCHEME`.
+ :param data: The request body, either as a string or a dict of
+ form keys and values.
+ :param json: If given, this is serialized as JSON and passed as
+ ``data``. Also defaults ``content_type`` to
+ ``application/json``.
+ :param args: other positional arguments passed to
+ :class:`~werkzeug.test.EnvironBuilder`.
+ :param kwargs: other keyword arguments passed to
+ :class:`~werkzeug.test.EnvironBuilder`.
+ """
+ from .testing import EnvironBuilder
+
+ builder = EnvironBuilder(self, *args, **kwargs)
+
+ try:
+ return self.request_context(builder.get_environ())
+ finally:
+ builder.close()
+
+ def wsgi_app(
+ self, environ: WSGIEnvironment, start_response: StartResponse
+ ) -> cabc.Iterable[bytes]:
+ """The actual WSGI application. This is not implemented in
+ :meth:`__call__` so that middlewares can be applied without
+ losing a reference to the app object. Instead of doing this::
+
+ app = MyMiddleware(app)
+
+ It's a better idea to do this instead::
+
+ app.wsgi_app = MyMiddleware(app.wsgi_app)
+
+ Then you still have the original application object around and
+ can continue to call methods on it.
+
+ .. versionchanged:: 0.7
+ Teardown events for the request and app contexts are called
+ even if an unhandled error occurs. Other events may not be
+ called depending on when an error occurs during dispatch.
+ See :ref:`callbacks-and-errors`.
+
+ :param environ: A WSGI environment.
+ :param start_response: A callable accepting a status code,
+ a list of headers, and an optional exception context to
+ start the response.
+ """
+ ctx = self.request_context(environ)
+ error: BaseException | None = None
+ try:
+ try:
+ ctx.push()
+ response = self.full_dispatch_request()
+ except Exception as e:
+ error = e
+ response = self.handle_exception(e)
+ except:
+ error = sys.exc_info()[1]
+ raise
+ return response(environ, start_response)
+ finally:
+ if "werkzeug.debug.preserve_context" in environ:
+ environ["werkzeug.debug.preserve_context"](_cv_app.get())
+ environ["werkzeug.debug.preserve_context"](_cv_request.get())
+
+ if error is not None and self.should_ignore_error(error):
+ error = None
+
+ ctx.pop(error)
+
+ def __call__(
+ self, environ: WSGIEnvironment, start_response: StartResponse
+ ) -> cabc.Iterable[bytes]:
+ """The WSGI server calls the Flask application object as the
+ WSGI application. This calls :meth:`wsgi_app`, which can be
+ wrapped to apply middleware.
+ """
+ return self.wsgi_app(environ, start_response)
diff --git a/venv/Lib/site-packages/flask/blueprints.py b/venv/Lib/site-packages/flask/blueprints.py
new file mode 100644
index 0000000..b6d4e43
--- /dev/null
+++ b/venv/Lib/site-packages/flask/blueprints.py
@@ -0,0 +1,128 @@
+from __future__ import annotations
+
+import os
+import typing as t
+from datetime import timedelta
+
+from .cli import AppGroup
+from .globals import current_app
+from .helpers import send_from_directory
+from .sansio.blueprints import Blueprint as SansioBlueprint
+from .sansio.blueprints import BlueprintSetupState as BlueprintSetupState # noqa
+from .sansio.scaffold import _sentinel
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from .wrappers import Response
+
+
+class Blueprint(SansioBlueprint):
+ def __init__(
+ self,
+ name: str,
+ import_name: str,
+ static_folder: str | os.PathLike[str] | None = None,
+ static_url_path: str | None = None,
+ template_folder: str | os.PathLike[str] | None = None,
+ url_prefix: str | None = None,
+ subdomain: str | None = None,
+ url_defaults: dict[str, t.Any] | None = None,
+ root_path: str | None = None,
+ cli_group: str | None = _sentinel, # type: ignore
+ ) -> None:
+ super().__init__(
+ name,
+ import_name,
+ static_folder,
+ static_url_path,
+ template_folder,
+ url_prefix,
+ subdomain,
+ url_defaults,
+ root_path,
+ cli_group,
+ )
+
+ #: The Click command group for registering CLI commands for this
+ #: object. The commands are available from the ``flask`` command
+ #: once the application has been discovered and blueprints have
+ #: been registered.
+ self.cli = AppGroup()
+
+ # Set the name of the Click group in case someone wants to add
+ # the app's commands to another CLI tool.
+ self.cli.name = self.name
+
+ def get_send_file_max_age(self, filename: str | None) -> int | None:
+ """Used by :func:`send_file` to determine the ``max_age`` cache
+ value for a given file path if it wasn't passed.
+
+ By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from
+ the configuration of :data:`~flask.current_app`. This defaults
+ to ``None``, which tells the browser to use conditional requests
+ instead of a timed cache, which is usually preferable.
+
+ Note this is a duplicate of the same method in the Flask
+ class.
+
+ .. versionchanged:: 2.0
+ The default configuration is ``None`` instead of 12 hours.
+
+ .. versionadded:: 0.9
+ """
+ value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"]
+
+ if value is None:
+ return None
+
+ if isinstance(value, timedelta):
+ return int(value.total_seconds())
+
+ return value # type: ignore[no-any-return]
+
+ def send_static_file(self, filename: str) -> Response:
+ """The view function used to serve files from
+ :attr:`static_folder`. A route is automatically registered for
+ this view at :attr:`static_url_path` if :attr:`static_folder` is
+ set.
+
+ Note this is a duplicate of the same method in the Flask
+ class.
+
+ .. versionadded:: 0.5
+
+ """
+ if not self.has_static_folder:
+ raise RuntimeError("'static_folder' must be set to serve static_files.")
+
+ # send_file only knows to call get_send_file_max_age on the app,
+ # call it here so it works for blueprints too.
+ max_age = self.get_send_file_max_age(filename)
+ return send_from_directory(
+ t.cast(str, self.static_folder), filename, max_age=max_age
+ )
+
+ def open_resource(
+ self, resource: str, mode: str = "rb", encoding: str | None = "utf-8"
+ ) -> t.IO[t.AnyStr]:
+ """Open a resource file relative to :attr:`root_path` for reading. The
+ blueprint-relative equivalent of the app's :meth:`~.Flask.open_resource`
+ method.
+
+ :param resource: Path to the resource relative to :attr:`root_path`.
+ :param mode: Open the file in this mode. Only reading is supported,
+ valid values are ``"r"`` (or ``"rt"``) and ``"rb"``.
+ :param encoding: Open the file with this encoding when opening in text
+ mode. This is ignored when opening in binary mode.
+
+ .. versionchanged:: 3.1
+ Added the ``encoding`` parameter.
+ """
+ if mode not in {"r", "rt", "rb"}:
+ raise ValueError("Resources can only be opened for reading.")
+
+ path = os.path.join(self.root_path, resource)
+
+ if mode == "rb":
+ return open(path, mode) # pyright: ignore
+
+ return open(path, mode, encoding=encoding)
diff --git a/venv/Lib/site-packages/flask/cli.py b/venv/Lib/site-packages/flask/cli.py
new file mode 100644
index 0000000..ed11f25
--- /dev/null
+++ b/venv/Lib/site-packages/flask/cli.py
@@ -0,0 +1,1135 @@
+from __future__ import annotations
+
+import ast
+import collections.abc as cabc
+import importlib.metadata
+import inspect
+import os
+import platform
+import re
+import sys
+import traceback
+import typing as t
+from functools import update_wrapper
+from operator import itemgetter
+from types import ModuleType
+
+import click
+from click.core import ParameterSource
+from werkzeug import run_simple
+from werkzeug.serving import is_running_from_reloader
+from werkzeug.utils import import_string
+
+from .globals import current_app
+from .helpers import get_debug_flag
+from .helpers import get_load_dotenv
+
+if t.TYPE_CHECKING:
+ import ssl
+
+ from _typeshed.wsgi import StartResponse
+ from _typeshed.wsgi import WSGIApplication
+ from _typeshed.wsgi import WSGIEnvironment
+
+ from .app import Flask
+
+
+class NoAppException(click.UsageError):
+ """Raised if an application cannot be found or loaded."""
+
+
+def find_best_app(module: ModuleType) -> Flask:
+ """Given a module instance this tries to find the best possible
+ application in the module or raises an exception.
+ """
+ from . import Flask
+
+ # Search for the most common names first.
+ for attr_name in ("app", "application"):
+ app = getattr(module, attr_name, None)
+
+ if isinstance(app, Flask):
+ return app
+
+ # Otherwise find the only object that is a Flask instance.
+ matches = [v for v in module.__dict__.values() if isinstance(v, Flask)]
+
+ if len(matches) == 1:
+ return matches[0]
+ elif len(matches) > 1:
+ raise NoAppException(
+ "Detected multiple Flask applications in module"
+ f" '{module.__name__}'. Use '{module.__name__}:name'"
+ " to specify the correct one."
+ )
+
+ # Search for app factory functions.
+ for attr_name in ("create_app", "make_app"):
+ app_factory = getattr(module, attr_name, None)
+
+ if inspect.isfunction(app_factory):
+ try:
+ app = app_factory()
+
+ if isinstance(app, Flask):
+ return app
+ except TypeError as e:
+ if not _called_with_wrong_args(app_factory):
+ raise
+
+ raise NoAppException(
+ f"Detected factory '{attr_name}' in module '{module.__name__}',"
+ " but could not call it without arguments. Use"
+ f" '{module.__name__}:{attr_name}(args)'"
+ " to specify arguments."
+ ) from e
+
+ raise NoAppException(
+ "Failed to find Flask application or factory in module"
+ f" '{module.__name__}'. Use '{module.__name__}:name'"
+ " to specify one."
+ )
+
+
+def _called_with_wrong_args(f: t.Callable[..., Flask]) -> bool:
+ """Check whether calling a function raised a ``TypeError`` because
+ the call failed or because something in the factory raised the
+ error.
+
+ :param f: The function that was called.
+ :return: ``True`` if the call failed.
+ """
+ tb = sys.exc_info()[2]
+
+ try:
+ while tb is not None:
+ if tb.tb_frame.f_code is f.__code__:
+ # In the function, it was called successfully.
+ return False
+
+ tb = tb.tb_next
+
+ # Didn't reach the function.
+ return True
+ finally:
+ # Delete tb to break a circular reference.
+ # https://docs.python.org/2/library/sys.html#sys.exc_info
+ del tb
+
+
+def find_app_by_string(module: ModuleType, app_name: str) -> Flask:
+ """Check if the given string is a variable name or a function. Call
+ a function to get the app instance, or return the variable directly.
+ """
+ from . import Flask
+
+ # Parse app_name as a single expression to determine if it's a valid
+ # attribute name or function call.
+ try:
+ expr = ast.parse(app_name.strip(), mode="eval").body
+ except SyntaxError:
+ raise NoAppException(
+ f"Failed to parse {app_name!r} as an attribute name or function call."
+ ) from None
+
+ if isinstance(expr, ast.Name):
+ name = expr.id
+ args = []
+ kwargs = {}
+ elif isinstance(expr, ast.Call):
+ # Ensure the function name is an attribute name only.
+ if not isinstance(expr.func, ast.Name):
+ raise NoAppException(
+ f"Function reference must be a simple name: {app_name!r}."
+ )
+
+ name = expr.func.id
+
+ # Parse the positional and keyword arguments as literals.
+ try:
+ args = [ast.literal_eval(arg) for arg in expr.args]
+ kwargs = {
+ kw.arg: ast.literal_eval(kw.value)
+ for kw in expr.keywords
+ if kw.arg is not None
+ }
+ except ValueError:
+ # literal_eval gives cryptic error messages, show a generic
+ # message with the full expression instead.
+ raise NoAppException(
+ f"Failed to parse arguments as literal values: {app_name!r}."
+ ) from None
+ else:
+ raise NoAppException(
+ f"Failed to parse {app_name!r} as an attribute name or function call."
+ )
+
+ try:
+ attr = getattr(module, name)
+ except AttributeError as e:
+ raise NoAppException(
+ f"Failed to find attribute {name!r} in {module.__name__!r}."
+ ) from e
+
+ # If the attribute is a function, call it with any args and kwargs
+ # to get the real application.
+ if inspect.isfunction(attr):
+ try:
+ app = attr(*args, **kwargs)
+ except TypeError as e:
+ if not _called_with_wrong_args(attr):
+ raise
+
+ raise NoAppException(
+ f"The factory {app_name!r} in module"
+ f" {module.__name__!r} could not be called with the"
+ " specified arguments."
+ ) from e
+ else:
+ app = attr
+
+ if isinstance(app, Flask):
+ return app
+
+ raise NoAppException(
+ "A valid Flask application was not obtained from"
+ f" '{module.__name__}:{app_name}'."
+ )
+
+
+def prepare_import(path: str) -> str:
+ """Given a filename this will try to calculate the python path, add it
+ to the search path and return the actual module name that is expected.
+ """
+ path = os.path.realpath(path)
+
+ fname, ext = os.path.splitext(path)
+ if ext == ".py":
+ path = fname
+
+ if os.path.basename(path) == "__init__":
+ path = os.path.dirname(path)
+
+ module_name = []
+
+ # move up until outside package structure (no __init__.py)
+ while True:
+ path, name = os.path.split(path)
+ module_name.append(name)
+
+ if not os.path.exists(os.path.join(path, "__init__.py")):
+ break
+
+ if sys.path[0] != path:
+ sys.path.insert(0, path)
+
+ return ".".join(module_name[::-1])
+
+
+@t.overload
+def locate_app(
+ module_name: str, app_name: str | None, raise_if_not_found: t.Literal[True] = True
+) -> Flask: ...
+
+
+@t.overload
+def locate_app(
+ module_name: str, app_name: str | None, raise_if_not_found: t.Literal[False] = ...
+) -> Flask | None: ...
+
+
+def locate_app(
+ module_name: str, app_name: str | None, raise_if_not_found: bool = True
+) -> Flask | None:
+ try:
+ __import__(module_name)
+ except ImportError:
+ # Reraise the ImportError if it occurred within the imported module.
+ # Determine this by checking whether the trace has a depth > 1.
+ if sys.exc_info()[2].tb_next: # type: ignore[union-attr]
+ raise NoAppException(
+ f"While importing {module_name!r}, an ImportError was"
+ f" raised:\n\n{traceback.format_exc()}"
+ ) from None
+ elif raise_if_not_found:
+ raise NoAppException(f"Could not import {module_name!r}.") from None
+ else:
+ return None
+
+ module = sys.modules[module_name]
+
+ if app_name is None:
+ return find_best_app(module)
+ else:
+ return find_app_by_string(module, app_name)
+
+
+def get_version(ctx: click.Context, param: click.Parameter, value: t.Any) -> None:
+ if not value or ctx.resilient_parsing:
+ return
+
+ flask_version = importlib.metadata.version("flask")
+ werkzeug_version = importlib.metadata.version("werkzeug")
+
+ click.echo(
+ f"Python {platform.python_version()}\n"
+ f"Flask {flask_version}\n"
+ f"Werkzeug {werkzeug_version}",
+ color=ctx.color,
+ )
+ ctx.exit()
+
+
+version_option = click.Option(
+ ["--version"],
+ help="Show the Flask version.",
+ expose_value=False,
+ callback=get_version,
+ is_flag=True,
+ is_eager=True,
+)
+
+
+class ScriptInfo:
+ """Helper object to deal with Flask applications. This is usually not
+ necessary to interface with as it's used internally in the dispatching
+ to click. In future versions of Flask this object will most likely play
+ a bigger role. Typically it's created automatically by the
+ :class:`FlaskGroup` but you can also manually create it and pass it
+ onwards as click object.
+
+ .. versionchanged:: 3.1
+ Added the ``load_dotenv_defaults`` parameter and attribute.
+ """
+
+ def __init__(
+ self,
+ app_import_path: str | None = None,
+ create_app: t.Callable[..., Flask] | None = None,
+ set_debug_flag: bool = True,
+ load_dotenv_defaults: bool = True,
+ ) -> None:
+ #: Optionally the import path for the Flask application.
+ self.app_import_path = app_import_path
+ #: Optionally a function that is passed the script info to create
+ #: the instance of the application.
+ self.create_app = create_app
+ #: A dictionary with arbitrary data that can be associated with
+ #: this script info.
+ self.data: dict[t.Any, t.Any] = {}
+ self.set_debug_flag = set_debug_flag
+
+ self.load_dotenv_defaults = get_load_dotenv(load_dotenv_defaults)
+ """Whether default ``.flaskenv`` and ``.env`` files should be loaded.
+
+ ``ScriptInfo`` doesn't load anything, this is for reference when doing
+ the load elsewhere during processing.
+
+ .. versionadded:: 3.1
+ """
+
+ self._loaded_app: Flask | None = None
+
+ def load_app(self) -> Flask:
+ """Loads the Flask app (if not yet loaded) and returns it. Calling
+ this multiple times will just result in the already loaded app to
+ be returned.
+ """
+ if self._loaded_app is not None:
+ return self._loaded_app
+ app: Flask | None = None
+ if self.create_app is not None:
+ app = self.create_app()
+ else:
+ if self.app_import_path:
+ path, name = (
+ re.split(r":(?![\\/])", self.app_import_path, maxsplit=1) + [None]
+ )[:2]
+ import_name = prepare_import(path)
+ app = locate_app(import_name, name)
+ else:
+ for path in ("wsgi.py", "app.py"):
+ import_name = prepare_import(path)
+ app = locate_app(import_name, None, raise_if_not_found=False)
+
+ if app is not None:
+ break
+
+ if app is None:
+ raise NoAppException(
+ "Could not locate a Flask application. Use the"
+ " 'flask --app' option, 'FLASK_APP' environment"
+ " variable, or a 'wsgi.py' or 'app.py' file in the"
+ " current directory."
+ )
+
+ if self.set_debug_flag:
+ # Update the app's debug flag through the descriptor so that
+ # other values repopulate as well.
+ app.debug = get_debug_flag()
+
+ self._loaded_app = app
+ return app
+
+
+pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True)
+
+F = t.TypeVar("F", bound=t.Callable[..., t.Any])
+
+
+def with_appcontext(f: F) -> F:
+ """Wraps a callback so that it's guaranteed to be executed with the
+ script's application context.
+
+ Custom commands (and their options) registered under ``app.cli`` or
+ ``blueprint.cli`` will always have an app context available, this
+ decorator is not required in that case.
+
+ .. versionchanged:: 2.2
+ The app context is active for subcommands as well as the
+ decorated callback. The app context is always available to
+ ``app.cli`` command and parameter callbacks.
+ """
+
+ @click.pass_context
+ def decorator(ctx: click.Context, /, *args: t.Any, **kwargs: t.Any) -> t.Any:
+ if not current_app:
+ app = ctx.ensure_object(ScriptInfo).load_app()
+ ctx.with_resource(app.app_context())
+
+ return ctx.invoke(f, *args, **kwargs)
+
+ return update_wrapper(decorator, f) # type: ignore[return-value]
+
+
+class AppGroup(click.Group):
+ """This works similar to a regular click :class:`~click.Group` but it
+ changes the behavior of the :meth:`command` decorator so that it
+ automatically wraps the functions in :func:`with_appcontext`.
+
+ Not to be confused with :class:`FlaskGroup`.
+ """
+
+ def command( # type: ignore[override]
+ self, *args: t.Any, **kwargs: t.Any
+ ) -> t.Callable[[t.Callable[..., t.Any]], click.Command]:
+ """This works exactly like the method of the same name on a regular
+ :class:`click.Group` but it wraps callbacks in :func:`with_appcontext`
+ unless it's disabled by passing ``with_appcontext=False``.
+ """
+ wrap_for_ctx = kwargs.pop("with_appcontext", True)
+
+ def decorator(f: t.Callable[..., t.Any]) -> click.Command:
+ if wrap_for_ctx:
+ f = with_appcontext(f)
+ return super(AppGroup, self).command(*args, **kwargs)(f) # type: ignore[no-any-return]
+
+ return decorator
+
+ def group( # type: ignore[override]
+ self, *args: t.Any, **kwargs: t.Any
+ ) -> t.Callable[[t.Callable[..., t.Any]], click.Group]:
+ """This works exactly like the method of the same name on a regular
+ :class:`click.Group` but it defaults the group class to
+ :class:`AppGroup`.
+ """
+ kwargs.setdefault("cls", AppGroup)
+ return super().group(*args, **kwargs) # type: ignore[no-any-return]
+
+
+def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None:
+ if value is None:
+ return None
+
+ info = ctx.ensure_object(ScriptInfo)
+ info.app_import_path = value
+ return value
+
+
+# This option is eager so the app will be available if --help is given.
+# --help is also eager, so --app must be before it in the param list.
+# no_args_is_help bypasses eager processing, so this option must be
+# processed manually in that case to ensure FLASK_APP gets picked up.
+_app_option = click.Option(
+ ["-A", "--app"],
+ metavar="IMPORT",
+ help=(
+ "The Flask application or factory function to load, in the form 'module:name'."
+ " Module can be a dotted import or file path. Name is not required if it is"
+ " 'app', 'application', 'create_app', or 'make_app', and can be 'name(args)' to"
+ " pass arguments."
+ ),
+ is_eager=True,
+ expose_value=False,
+ callback=_set_app,
+)
+
+
+def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None:
+ # If the flag isn't provided, it will default to False. Don't use
+ # that, let debug be set by env in that case.
+ source = ctx.get_parameter_source(param.name) # type: ignore[arg-type]
+
+ if source is not None and source in (
+ ParameterSource.DEFAULT,
+ ParameterSource.DEFAULT_MAP,
+ ):
+ return None
+
+ # Set with env var instead of ScriptInfo.load so that it can be
+ # accessed early during a factory function.
+ os.environ["FLASK_DEBUG"] = "1" if value else "0"
+ return value
+
+
+_debug_option = click.Option(
+ ["--debug/--no-debug"],
+ help="Set debug mode.",
+ expose_value=False,
+ callback=_set_debug,
+)
+
+
+def _env_file_callback(
+ ctx: click.Context, param: click.Option, value: str | None
+) -> str | None:
+ try:
+ import dotenv # noqa: F401
+ except ImportError:
+ # Only show an error if a value was passed, otherwise we still want to
+ # call load_dotenv and show a message without exiting.
+ if value is not None:
+ raise click.BadParameter(
+ "python-dotenv must be installed to load an env file.",
+ ctx=ctx,
+ param=param,
+ ) from None
+
+ # Load if a value was passed, or we want to load default files, or both.
+ if value is not None or ctx.obj.load_dotenv_defaults:
+ load_dotenv(value, load_defaults=ctx.obj.load_dotenv_defaults)
+
+ return value
+
+
+# This option is eager so env vars are loaded as early as possible to be
+# used by other options.
+_env_file_option = click.Option(
+ ["-e", "--env-file"],
+ type=click.Path(exists=True, dir_okay=False),
+ help=(
+ "Load environment variables from this file, taking precedence over"
+ " those set by '.env' and '.flaskenv'. Variables set directly in the"
+ " environment take highest precedence. python-dotenv must be installed."
+ ),
+ is_eager=True,
+ expose_value=False,
+ callback=_env_file_callback,
+)
+
+
+class FlaskGroup(AppGroup):
+ """Special subclass of the :class:`AppGroup` group that supports
+ loading more commands from the configured Flask app. Normally a
+ developer does not have to interface with this class but there are
+ some very advanced use cases for which it makes sense to create an
+ instance of this. see :ref:`custom-scripts`.
+
+ :param add_default_commands: if this is True then the default run and
+ shell commands will be added.
+ :param add_version_option: adds the ``--version`` option.
+ :param create_app: an optional callback that is passed the script info and
+ returns the loaded app.
+ :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv`
+ files to set environment variables. Will also change the working
+ directory to the directory containing the first file found.
+ :param set_debug_flag: Set the app's debug flag.
+
+ .. versionchanged:: 3.1
+ ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files.
+
+ .. versionchanged:: 2.2
+ Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options.
+
+ .. versionchanged:: 2.2
+ An app context is pushed when running ``app.cli`` commands, so
+ ``@with_appcontext`` is no longer required for those commands.
+
+ .. versionchanged:: 1.0
+ If installed, python-dotenv will be used to load environment variables
+ from :file:`.env` and :file:`.flaskenv` files.
+ """
+
+ def __init__(
+ self,
+ add_default_commands: bool = True,
+ create_app: t.Callable[..., Flask] | None = None,
+ add_version_option: bool = True,
+ load_dotenv: bool = True,
+ set_debug_flag: bool = True,
+ **extra: t.Any,
+ ) -> None:
+ params: list[click.Parameter] = list(extra.pop("params", None) or ())
+ # Processing is done with option callbacks instead of a group
+ # callback. This allows users to make a custom group callback
+ # without losing the behavior. --env-file must come first so
+ # that it is eagerly evaluated before --app.
+ params.extend((_env_file_option, _app_option, _debug_option))
+
+ if add_version_option:
+ params.append(version_option)
+
+ if "context_settings" not in extra:
+ extra["context_settings"] = {}
+
+ extra["context_settings"].setdefault("auto_envvar_prefix", "FLASK")
+
+ super().__init__(params=params, **extra)
+
+ self.create_app = create_app
+ self.load_dotenv = load_dotenv
+ self.set_debug_flag = set_debug_flag
+
+ if add_default_commands:
+ self.add_command(run_command)
+ self.add_command(shell_command)
+ self.add_command(routes_command)
+
+ self._loaded_plugin_commands = False
+
+ def _load_plugin_commands(self) -> None:
+ if self._loaded_plugin_commands:
+ return
+
+ if sys.version_info >= (3, 10):
+ from importlib import metadata
+ else:
+ # Use a backport on Python < 3.10. We technically have
+ # importlib.metadata on 3.8+, but the API changed in 3.10,
+ # so use the backport for consistency.
+ import importlib_metadata as metadata # pyright: ignore
+
+ for ep in metadata.entry_points(group="flask.commands"):
+ self.add_command(ep.load(), ep.name)
+
+ self._loaded_plugin_commands = True
+
+ def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
+ self._load_plugin_commands()
+ # Look up built-in and plugin commands, which should be
+ # available even if the app fails to load.
+ rv = super().get_command(ctx, name)
+
+ if rv is not None:
+ return rv
+
+ info = ctx.ensure_object(ScriptInfo)
+
+ # Look up commands provided by the app, showing an error and
+ # continuing if the app couldn't be loaded.
+ try:
+ app = info.load_app()
+ except NoAppException as e:
+ click.secho(f"Error: {e.format_message()}\n", err=True, fg="red")
+ return None
+
+ # Push an app context for the loaded app unless it is already
+ # active somehow. This makes the context available to parameter
+ # and command callbacks without needing @with_appcontext.
+ if not current_app or current_app._get_current_object() is not app: # type: ignore[attr-defined]
+ ctx.with_resource(app.app_context())
+
+ return app.cli.get_command(ctx, name)
+
+ def list_commands(self, ctx: click.Context) -> list[str]:
+ self._load_plugin_commands()
+ # Start with the built-in and plugin commands.
+ rv = set(super().list_commands(ctx))
+ info = ctx.ensure_object(ScriptInfo)
+
+ # Add commands provided by the app, showing an error and
+ # continuing if the app couldn't be loaded.
+ try:
+ rv.update(info.load_app().cli.list_commands(ctx))
+ except NoAppException as e:
+ # When an app couldn't be loaded, show the error message
+ # without the traceback.
+ click.secho(f"Error: {e.format_message()}\n", err=True, fg="red")
+ except Exception:
+ # When any other errors occurred during loading, show the
+ # full traceback.
+ click.secho(f"{traceback.format_exc()}\n", err=True, fg="red")
+
+ return sorted(rv)
+
+ def make_context(
+ self,
+ info_name: str | None,
+ args: list[str],
+ parent: click.Context | None = None,
+ **extra: t.Any,
+ ) -> click.Context:
+ # Set a flag to tell app.run to become a no-op. If app.run was
+ # not in a __name__ == __main__ guard, it would start the server
+ # when importing, blocking whatever command is being called.
+ os.environ["FLASK_RUN_FROM_CLI"] = "true"
+
+ if "obj" not in extra and "obj" not in self.context_settings:
+ extra["obj"] = ScriptInfo(
+ create_app=self.create_app,
+ set_debug_flag=self.set_debug_flag,
+ load_dotenv_defaults=self.load_dotenv,
+ )
+
+ return super().make_context(info_name, args, parent=parent, **extra)
+
+ def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
+ if (not args and self.no_args_is_help) or (
+ len(args) == 1 and args[0] in self.get_help_option_names(ctx)
+ ):
+ # Attempt to load --env-file and --app early in case they
+ # were given as env vars. Otherwise no_args_is_help will not
+ # see commands from app.cli.
+ _env_file_option.handle_parse_result(ctx, {}, [])
+ _app_option.handle_parse_result(ctx, {}, [])
+
+ return super().parse_args(ctx, args)
+
+
+def _path_is_ancestor(path: str, other: str) -> bool:
+ """Take ``other`` and remove the length of ``path`` from it. Then join it
+ to ``path``. If it is the original value, ``path`` is an ancestor of
+ ``other``."""
+ return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other
+
+
+def load_dotenv(
+ path: str | os.PathLike[str] | None = None, load_defaults: bool = True
+) -> bool:
+ """Load "dotenv" files to set environment variables. A given path takes
+ precedence over ``.env``, which takes precedence over ``.flaskenv``. After
+ loading and combining these files, values are only set if the key is not
+ already set in ``os.environ``.
+
+ This is a no-op if `python-dotenv`_ is not installed.
+
+ .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme
+
+ :param path: Load the file at this location.
+ :param load_defaults: Search for and load the default ``.flaskenv`` and
+ ``.env`` files.
+ :return: ``True`` if at least one env var was loaded.
+
+ .. versionchanged:: 3.1
+ Added the ``load_defaults`` parameter. A given path takes precedence
+ over default files.
+
+ .. versionchanged:: 2.0
+ The current directory is not changed to the location of the
+ loaded file.
+
+ .. versionchanged:: 2.0
+ When loading the env files, set the default encoding to UTF-8.
+
+ .. versionchanged:: 1.1.0
+ Returns ``False`` when python-dotenv is not installed, or when
+ the given path isn't a file.
+
+ .. versionadded:: 1.0
+ """
+ try:
+ import dotenv
+ except ImportError:
+ if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"):
+ click.secho(
+ " * Tip: There are .env files present. Install python-dotenv"
+ " to use them.",
+ fg="yellow",
+ err=True,
+ )
+
+ return False
+
+ data: dict[str, str | None] = {}
+
+ if load_defaults:
+ for default_name in (".flaskenv", ".env"):
+ if not (default_path := dotenv.find_dotenv(default_name, usecwd=True)):
+ continue
+
+ data |= dotenv.dotenv_values(default_path, encoding="utf-8")
+
+ if path is not None and os.path.isfile(path):
+ data |= dotenv.dotenv_values(path, encoding="utf-8")
+
+ for key, value in data.items():
+ if key in os.environ or value is None:
+ continue
+
+ os.environ[key] = value
+
+ return bool(data) # True if at least one env var was loaded.
+
+
+def show_server_banner(debug: bool, app_import_path: str | None) -> None:
+ """Show extra startup messages the first time the server is run,
+ ignoring the reloader.
+ """
+ if is_running_from_reloader():
+ return
+
+ if app_import_path is not None:
+ click.echo(f" * Serving Flask app '{app_import_path}'")
+
+ if debug is not None:
+ click.echo(f" * Debug mode: {'on' if debug else 'off'}")
+
+
+class CertParamType(click.ParamType):
+ """Click option type for the ``--cert`` option. Allows either an
+ existing file, the string ``'adhoc'``, or an import for a
+ :class:`~ssl.SSLContext` object.
+ """
+
+ name = "path"
+
+ def __init__(self) -> None:
+ self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True)
+
+ def convert(
+ self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None
+ ) -> t.Any:
+ try:
+ import ssl
+ except ImportError:
+ raise click.BadParameter(
+ 'Using "--cert" requires Python to be compiled with SSL support.',
+ ctx,
+ param,
+ ) from None
+
+ try:
+ return self.path_type(value, param, ctx)
+ except click.BadParameter:
+ value = click.STRING(value, param, ctx).lower()
+
+ if value == "adhoc":
+ try:
+ import cryptography # noqa: F401
+ except ImportError:
+ raise click.BadParameter(
+ "Using ad-hoc certificates requires the cryptography library.",
+ ctx,
+ param,
+ ) from None
+
+ return value
+
+ obj = import_string(value, silent=True)
+
+ if isinstance(obj, ssl.SSLContext):
+ return obj
+
+ raise
+
+
+def _validate_key(ctx: click.Context, param: click.Parameter, value: t.Any) -> t.Any:
+ """The ``--key`` option must be specified when ``--cert`` is a file.
+ Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed.
+ """
+ cert = ctx.params.get("cert")
+ is_adhoc = cert == "adhoc"
+
+ try:
+ import ssl
+ except ImportError:
+ is_context = False
+ else:
+ is_context = isinstance(cert, ssl.SSLContext)
+
+ if value is not None:
+ if is_adhoc:
+ raise click.BadParameter(
+ 'When "--cert" is "adhoc", "--key" is not used.', ctx, param
+ )
+
+ if is_context:
+ raise click.BadParameter(
+ 'When "--cert" is an SSLContext object, "--key" is not used.',
+ ctx,
+ param,
+ )
+
+ if not cert:
+ raise click.BadParameter('"--cert" must also be specified.', ctx, param)
+
+ ctx.params["cert"] = cert, value
+
+ else:
+ if cert and not (is_adhoc or is_context):
+ raise click.BadParameter('Required when using "--cert".', ctx, param)
+
+ return value
+
+
+class SeparatedPathType(click.Path):
+ """Click option type that accepts a list of values separated by the
+ OS's path separator (``:``, ``;`` on Windows). Each value is
+ validated as a :class:`click.Path` type.
+ """
+
+ def convert(
+ self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None
+ ) -> t.Any:
+ items = self.split_envvar_value(value)
+ # can't call no-arg super() inside list comprehension until Python 3.12
+ super_convert = super().convert
+ return [super_convert(item, param, ctx) for item in items]
+
+
+@click.command("run", short_help="Run a development server.")
+@click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.")
+@click.option("--port", "-p", default=5000, help="The port to bind to.")
+@click.option(
+ "--cert",
+ type=CertParamType(),
+ help="Specify a certificate file to use HTTPS.",
+ is_eager=True,
+)
+@click.option(
+ "--key",
+ type=click.Path(exists=True, dir_okay=False, resolve_path=True),
+ callback=_validate_key,
+ expose_value=False,
+ help="The key file to use when specifying a certificate.",
+)
+@click.option(
+ "--reload/--no-reload",
+ default=None,
+ help="Enable or disable the reloader. By default the reloader "
+ "is active if debug is enabled.",
+)
+@click.option(
+ "--debugger/--no-debugger",
+ default=None,
+ help="Enable or disable the debugger. By default the debugger "
+ "is active if debug is enabled.",
+)
+@click.option(
+ "--with-threads/--without-threads",
+ default=True,
+ help="Enable or disable multithreading.",
+)
+@click.option(
+ "--extra-files",
+ default=None,
+ type=SeparatedPathType(),
+ help=(
+ "Extra files that trigger a reload on change. Multiple paths"
+ f" are separated by {os.path.pathsep!r}."
+ ),
+)
+@click.option(
+ "--exclude-patterns",
+ default=None,
+ type=SeparatedPathType(),
+ help=(
+ "Files matching these fnmatch patterns will not trigger a reload"
+ " on change. Multiple patterns are separated by"
+ f" {os.path.pathsep!r}."
+ ),
+)
+@pass_script_info
+def run_command(
+ info: ScriptInfo,
+ host: str,
+ port: int,
+ reload: bool,
+ debugger: bool,
+ with_threads: bool,
+ cert: ssl.SSLContext | tuple[str, str | None] | t.Literal["adhoc"] | None,
+ extra_files: list[str] | None,
+ exclude_patterns: list[str] | None,
+) -> None:
+ """Run a local development server.
+
+ This server is for development purposes only. It does not provide
+ the stability, security, or performance of production WSGI servers.
+
+ The reloader and debugger are enabled by default with the '--debug'
+ option.
+ """
+ try:
+ app: WSGIApplication = info.load_app() # pyright: ignore
+ except Exception as e:
+ if is_running_from_reloader():
+ # When reloading, print out the error immediately, but raise
+ # it later so the debugger or server can handle it.
+ traceback.print_exc()
+ err = e
+
+ def app(
+ environ: WSGIEnvironment, start_response: StartResponse
+ ) -> cabc.Iterable[bytes]:
+ raise err from None
+
+ else:
+ # When not reloading, raise the error immediately so the
+ # command fails.
+ raise e from None
+
+ debug = get_debug_flag()
+
+ if reload is None:
+ reload = debug
+
+ if debugger is None:
+ debugger = debug
+
+ show_server_banner(debug, info.app_import_path)
+
+ run_simple(
+ host,
+ port,
+ app,
+ use_reloader=reload,
+ use_debugger=debugger,
+ threaded=with_threads,
+ ssl_context=cert,
+ extra_files=extra_files,
+ exclude_patterns=exclude_patterns,
+ )
+
+
+run_command.params.insert(0, _debug_option)
+
+
+@click.command("shell", short_help="Run a shell in the app context.")
+@with_appcontext
+def shell_command() -> None:
+ """Run an interactive Python shell in the context of a given
+ Flask application. The application will populate the default
+ namespace of this shell according to its configuration.
+
+ This is useful for executing small snippets of management code
+ without having to manually configure the application.
+ """
+ import code
+
+ banner = (
+ f"Python {sys.version} on {sys.platform}\n"
+ f"App: {current_app.import_name}\n"
+ f"Instance: {current_app.instance_path}"
+ )
+ ctx: dict[str, t.Any] = {}
+
+ # Support the regular Python interpreter startup script if someone
+ # is using it.
+ startup = os.environ.get("PYTHONSTARTUP")
+ if startup and os.path.isfile(startup):
+ with open(startup) as f:
+ eval(compile(f.read(), startup, "exec"), ctx)
+
+ ctx.update(current_app.make_shell_context())
+
+ # Site, customize, or startup script can set a hook to call when
+ # entering interactive mode. The default one sets up readline with
+ # tab and history completion.
+ interactive_hook = getattr(sys, "__interactivehook__", None)
+
+ if interactive_hook is not None:
+ try:
+ import readline
+ from rlcompleter import Completer
+ except ImportError:
+ pass
+ else:
+ # rlcompleter uses __main__.__dict__ by default, which is
+ # flask.__main__. Use the shell context instead.
+ readline.set_completer(Completer(ctx).complete)
+
+ interactive_hook()
+
+ code.interact(banner=banner, local=ctx)
+
+
+@click.command("routes", short_help="Show the routes for the app.")
+@click.option(
+ "--sort",
+ "-s",
+ type=click.Choice(("endpoint", "methods", "domain", "rule", "match")),
+ default="endpoint",
+ help=(
+ "Method to sort routes by. 'match' is the order that Flask will match routes"
+ " when dispatching a request."
+ ),
+)
+@click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.")
+@with_appcontext
+def routes_command(sort: str, all_methods: bool) -> None:
+ """Show all registered routes with endpoints and methods."""
+ rules = list(current_app.url_map.iter_rules())
+
+ if not rules:
+ click.echo("No routes were registered.")
+ return
+
+ ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"}
+ host_matching = current_app.url_map.host_matching
+ has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules)
+ rows = []
+
+ for rule in rules:
+ row = [
+ rule.endpoint,
+ ", ".join(sorted((rule.methods or set()) - ignored_methods)),
+ ]
+
+ if has_domain:
+ row.append((rule.host if host_matching else rule.subdomain) or "")
+
+ row.append(rule.rule)
+ rows.append(row)
+
+ headers = ["Endpoint", "Methods"]
+ sorts = ["endpoint", "methods"]
+
+ if has_domain:
+ headers.append("Host" if host_matching else "Subdomain")
+ sorts.append("domain")
+
+ headers.append("Rule")
+ sorts.append("rule")
+
+ try:
+ rows.sort(key=itemgetter(sorts.index(sort)))
+ except ValueError:
+ pass
+
+ rows.insert(0, headers)
+ widths = [max(len(row[i]) for row in rows) for i in range(len(headers))]
+ rows.insert(1, ["-" * w for w in widths])
+ template = " ".join(f"{{{i}:<{w}}}" for i, w in enumerate(widths))
+
+ for row in rows:
+ click.echo(template.format(*row))
+
+
+cli = FlaskGroup(
+ name="flask",
+ help="""\
+A general utility script for Flask applications.
+
+An application to load must be given with the '--app' option,
+'FLASK_APP' environment variable, or with a 'wsgi.py' or 'app.py' file
+in the current directory.
+""",
+)
+
+
+def main() -> None:
+ cli.main()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/venv/Lib/site-packages/flask/config.py b/venv/Lib/site-packages/flask/config.py
new file mode 100644
index 0000000..34ef1a5
--- /dev/null
+++ b/venv/Lib/site-packages/flask/config.py
@@ -0,0 +1,367 @@
+from __future__ import annotations
+
+import errno
+import json
+import os
+import types
+import typing as t
+
+from werkzeug.utils import import_string
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+ from .sansio.app import App
+
+
+T = t.TypeVar("T")
+
+
+class ConfigAttribute(t.Generic[T]):
+ """Makes an attribute forward to the config"""
+
+ def __init__(
+ self, name: str, get_converter: t.Callable[[t.Any], T] | None = None
+ ) -> None:
+ self.__name__ = name
+ self.get_converter = get_converter
+
+ @t.overload
+ def __get__(self, obj: None, owner: None) -> te.Self: ...
+
+ @t.overload
+ def __get__(self, obj: App, owner: type[App]) -> T: ...
+
+ def __get__(self, obj: App | None, owner: type[App] | None = None) -> T | te.Self:
+ if obj is None:
+ return self
+
+ rv = obj.config[self.__name__]
+
+ if self.get_converter is not None:
+ rv = self.get_converter(rv)
+
+ return rv # type: ignore[no-any-return]
+
+ def __set__(self, obj: App, value: t.Any) -> None:
+ obj.config[self.__name__] = value
+
+
+class Config(dict): # type: ignore[type-arg]
+ """Works exactly like a dict but provides ways to fill it from files
+ or special dictionaries. There are two common patterns to populate the
+ config.
+
+ Either you can fill the config from a config file::
+
+ app.config.from_pyfile('yourconfig.cfg')
+
+ Or alternatively you can define the configuration options in the
+ module that calls :meth:`from_object` or provide an import path to
+ a module that should be loaded. It is also possible to tell it to
+ use the same module and with that provide the configuration values
+ just before the call::
+
+ DEBUG = True
+ SECRET_KEY = 'development key'
+ app.config.from_object(__name__)
+
+ In both cases (loading from any Python file or loading from modules),
+ only uppercase keys are added to the config. This makes it possible to use
+ lowercase values in the config file for temporary values that are not added
+ to the config or to define the config keys in the same file that implements
+ the application.
+
+ Probably the most interesting way to load configurations is from an
+ environment variable pointing to a file::
+
+ app.config.from_envvar('YOURAPPLICATION_SETTINGS')
+
+ In this case before launching the application you have to set this
+ environment variable to the file you want to use. On Linux and OS X
+ use the export statement::
+
+ export YOURAPPLICATION_SETTINGS='/path/to/config/file'
+
+ On windows use `set` instead.
+
+ :param root_path: path to which files are read relative from. When the
+ config object is created by the application, this is
+ the application's :attr:`~flask.Flask.root_path`.
+ :param defaults: an optional dictionary of default values
+ """
+
+ def __init__(
+ self,
+ root_path: str | os.PathLike[str],
+ defaults: dict[str, t.Any] | None = None,
+ ) -> None:
+ super().__init__(defaults or {})
+ self.root_path = root_path
+
+ def from_envvar(self, variable_name: str, silent: bool = False) -> bool:
+ """Loads a configuration from an environment variable pointing to
+ a configuration file. This is basically just a shortcut with nicer
+ error messages for this line of code::
+
+ app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS'])
+
+ :param variable_name: name of the environment variable
+ :param silent: set to ``True`` if you want silent failure for missing
+ files.
+ :return: ``True`` if the file was loaded successfully.
+ """
+ rv = os.environ.get(variable_name)
+ if not rv:
+ if silent:
+ return False
+ raise RuntimeError(
+ f"The environment variable {variable_name!r} is not set"
+ " and as such configuration could not be loaded. Set"
+ " this variable and make it point to a configuration"
+ " file"
+ )
+ return self.from_pyfile(rv, silent=silent)
+
+ def from_prefixed_env(
+ self, prefix: str = "FLASK", *, loads: t.Callable[[str], t.Any] = json.loads
+ ) -> bool:
+ """Load any environment variables that start with ``FLASK_``,
+ dropping the prefix from the env key for the config key. Values
+ are passed through a loading function to attempt to convert them
+ to more specific types than strings.
+
+ Keys are loaded in :func:`sorted` order.
+
+ The default loading function attempts to parse values as any
+ valid JSON type, including dicts and lists.
+
+ Specific items in nested dicts can be set by separating the
+ keys with double underscores (``__``). If an intermediate key
+ doesn't exist, it will be initialized to an empty dict.
+
+ :param prefix: Load env vars that start with this prefix,
+ separated with an underscore (``_``).
+ :param loads: Pass each string value to this function and use
+ the returned value as the config value. If any error is
+ raised it is ignored and the value remains a string. The
+ default is :func:`json.loads`.
+
+ .. versionadded:: 2.1
+ """
+ prefix = f"{prefix}_"
+
+ for key in sorted(os.environ):
+ if not key.startswith(prefix):
+ continue
+
+ value = os.environ[key]
+ key = key.removeprefix(prefix)
+
+ try:
+ value = loads(value)
+ except Exception:
+ # Keep the value as a string if loading failed.
+ pass
+
+ if "__" not in key:
+ # A non-nested key, set directly.
+ self[key] = value
+ continue
+
+ # Traverse nested dictionaries with keys separated by "__".
+ current = self
+ *parts, tail = key.split("__")
+
+ for part in parts:
+ # If an intermediate dict does not exist, create it.
+ if part not in current:
+ current[part] = {}
+
+ current = current[part]
+
+ current[tail] = value
+
+ return True
+
+ def from_pyfile(
+ self, filename: str | os.PathLike[str], silent: bool = False
+ ) -> bool:
+ """Updates the values in the config from a Python file. This function
+ behaves as if the file was imported as module with the
+ :meth:`from_object` function.
+
+ :param filename: the filename of the config. This can either be an
+ absolute filename or a filename relative to the
+ root path.
+ :param silent: set to ``True`` if you want silent failure for missing
+ files.
+ :return: ``True`` if the file was loaded successfully.
+
+ .. versionadded:: 0.7
+ `silent` parameter.
+ """
+ filename = os.path.join(self.root_path, filename)
+ d = types.ModuleType("config")
+ d.__file__ = filename
+ try:
+ with open(filename, mode="rb") as config_file:
+ exec(compile(config_file.read(), filename, "exec"), d.__dict__)
+ except OSError as e:
+ if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR):
+ return False
+ e.strerror = f"Unable to load configuration file ({e.strerror})"
+ raise
+ self.from_object(d)
+ return True
+
+ def from_object(self, obj: object | str) -> None:
+ """Updates the values from the given object. An object can be of one
+ of the following two types:
+
+ - a string: in this case the object with that name will be imported
+ - an actual object reference: that object is used directly
+
+ Objects are usually either modules or classes. :meth:`from_object`
+ loads only the uppercase attributes of the module/class. A ``dict``
+ object will not work with :meth:`from_object` because the keys of a
+ ``dict`` are not attributes of the ``dict`` class.
+
+ Example of module-based configuration::
+
+ app.config.from_object('yourapplication.default_config')
+ from yourapplication import default_config
+ app.config.from_object(default_config)
+
+ Nothing is done to the object before loading. If the object is a
+ class and has ``@property`` attributes, it needs to be
+ instantiated before being passed to this method.
+
+ You should not use this function to load the actual configuration but
+ rather configuration defaults. The actual config should be loaded
+ with :meth:`from_pyfile` and ideally from a location not within the
+ package because the package might be installed system wide.
+
+ See :ref:`config-dev-prod` for an example of class-based configuration
+ using :meth:`from_object`.
+
+ :param obj: an import name or object
+ """
+ if isinstance(obj, str):
+ obj = import_string(obj)
+ for key in dir(obj):
+ if key.isupper():
+ self[key] = getattr(obj, key)
+
+ def from_file(
+ self,
+ filename: str | os.PathLike[str],
+ load: t.Callable[[t.IO[t.Any]], t.Mapping[str, t.Any]],
+ silent: bool = False,
+ text: bool = True,
+ ) -> bool:
+ """Update the values in the config from a file that is loaded
+ using the ``load`` parameter. The loaded data is passed to the
+ :meth:`from_mapping` method.
+
+ .. code-block:: python
+
+ import json
+ app.config.from_file("config.json", load=json.load)
+
+ import tomllib
+ app.config.from_file("config.toml", load=tomllib.load, text=False)
+
+ :param filename: The path to the data file. This can be an
+ absolute path or relative to the config root path.
+ :param load: A callable that takes a file handle and returns a
+ mapping of loaded data from the file.
+ :type load: ``Callable[[Reader], Mapping]`` where ``Reader``
+ implements a ``read`` method.
+ :param silent: Ignore the file if it doesn't exist.
+ :param text: Open the file in text or binary mode.
+ :return: ``True`` if the file was loaded successfully.
+
+ .. versionchanged:: 2.3
+ The ``text`` parameter was added.
+
+ .. versionadded:: 2.0
+ """
+ filename = os.path.join(self.root_path, filename)
+
+ try:
+ with open(filename, "r" if text else "rb") as f:
+ obj = load(f)
+ except OSError as e:
+ if silent and e.errno in (errno.ENOENT, errno.EISDIR):
+ return False
+
+ e.strerror = f"Unable to load configuration file ({e.strerror})"
+ raise
+
+ return self.from_mapping(obj)
+
+ def from_mapping(
+ self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any
+ ) -> bool:
+ """Updates the config like :meth:`update` ignoring items with
+ non-upper keys.
+
+ :return: Always returns ``True``.
+
+ .. versionadded:: 0.11
+ """
+ mappings: dict[str, t.Any] = {}
+ if mapping is not None:
+ mappings.update(mapping)
+ mappings.update(kwargs)
+ for key, value in mappings.items():
+ if key.isupper():
+ self[key] = value
+ return True
+
+ def get_namespace(
+ self, namespace: str, lowercase: bool = True, trim_namespace: bool = True
+ ) -> dict[str, t.Any]:
+ """Returns a dictionary containing a subset of configuration options
+ that match the specified namespace/prefix. Example usage::
+
+ app.config['IMAGE_STORE_TYPE'] = 'fs'
+ app.config['IMAGE_STORE_PATH'] = '/var/app/images'
+ app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com'
+ image_store_config = app.config.get_namespace('IMAGE_STORE_')
+
+ The resulting dictionary `image_store_config` would look like::
+
+ {
+ 'type': 'fs',
+ 'path': '/var/app/images',
+ 'base_url': 'http://img.website.com'
+ }
+
+ This is often useful when configuration options map directly to
+ keyword arguments in functions or class constructors.
+
+ :param namespace: a configuration namespace
+ :param lowercase: a flag indicating if the keys of the resulting
+ dictionary should be lowercase
+ :param trim_namespace: a flag indicating if the keys of the resulting
+ dictionary should not include the namespace
+
+ .. versionadded:: 0.11
+ """
+ rv = {}
+ for k, v in self.items():
+ if not k.startswith(namespace):
+ continue
+ if trim_namespace:
+ key = k[len(namespace) :]
+ else:
+ key = k
+ if lowercase:
+ key = key.lower()
+ rv[key] = v
+ return rv
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} {dict.__repr__(self)}>"
diff --git a/venv/Lib/site-packages/flask/ctx.py b/venv/Lib/site-packages/flask/ctx.py
new file mode 100644
index 0000000..5f7b1f1
--- /dev/null
+++ b/venv/Lib/site-packages/flask/ctx.py
@@ -0,0 +1,459 @@
+from __future__ import annotations
+
+import contextvars
+import sys
+import typing as t
+from functools import update_wrapper
+from types import TracebackType
+
+from werkzeug.exceptions import HTTPException
+
+from . import typing as ft
+from .globals import _cv_app
+from .globals import _cv_request
+from .signals import appcontext_popped
+from .signals import appcontext_pushed
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from _typeshed.wsgi import WSGIEnvironment
+
+ from .app import Flask
+ from .sessions import SessionMixin
+ from .wrappers import Request
+
+
+# a singleton sentinel value for parameter defaults
+_sentinel = object()
+
+
+class _AppCtxGlobals:
+ """A plain object. Used as a namespace for storing data during an
+ application context.
+
+ Creating an app context automatically creates this object, which is
+ made available as the :data:`g` proxy.
+
+ .. describe:: 'key' in g
+
+ Check whether an attribute is present.
+
+ .. versionadded:: 0.10
+
+ .. describe:: iter(g)
+
+ Return an iterator over the attribute names.
+
+ .. versionadded:: 0.10
+ """
+
+ # Define attr methods to let mypy know this is a namespace object
+ # that has arbitrary attributes.
+
+ def __getattr__(self, name: str) -> t.Any:
+ try:
+ return self.__dict__[name]
+ except KeyError:
+ raise AttributeError(name) from None
+
+ def __setattr__(self, name: str, value: t.Any) -> None:
+ self.__dict__[name] = value
+
+ def __delattr__(self, name: str) -> None:
+ try:
+ del self.__dict__[name]
+ except KeyError:
+ raise AttributeError(name) from None
+
+ def get(self, name: str, default: t.Any | None = None) -> t.Any:
+ """Get an attribute by name, or a default value. Like
+ :meth:`dict.get`.
+
+ :param name: Name of attribute to get.
+ :param default: Value to return if the attribute is not present.
+
+ .. versionadded:: 0.10
+ """
+ return self.__dict__.get(name, default)
+
+ def pop(self, name: str, default: t.Any = _sentinel) -> t.Any:
+ """Get and remove an attribute by name. Like :meth:`dict.pop`.
+
+ :param name: Name of attribute to pop.
+ :param default: Value to return if the attribute is not present,
+ instead of raising a ``KeyError``.
+
+ .. versionadded:: 0.11
+ """
+ if default is _sentinel:
+ return self.__dict__.pop(name)
+ else:
+ return self.__dict__.pop(name, default)
+
+ def setdefault(self, name: str, default: t.Any = None) -> t.Any:
+ """Get the value of an attribute if it is present, otherwise
+ set and return a default value. Like :meth:`dict.setdefault`.
+
+ :param name: Name of attribute to get.
+ :param default: Value to set and return if the attribute is not
+ present.
+
+ .. versionadded:: 0.11
+ """
+ return self.__dict__.setdefault(name, default)
+
+ def __contains__(self, item: str) -> bool:
+ return item in self.__dict__
+
+ def __iter__(self) -> t.Iterator[str]:
+ return iter(self.__dict__)
+
+ def __repr__(self) -> str:
+ ctx = _cv_app.get(None)
+ if ctx is not None:
+ return f""
+ return object.__repr__(self)
+
+
+def after_this_request(
+ f: ft.AfterRequestCallable[t.Any],
+) -> ft.AfterRequestCallable[t.Any]:
+ """Executes a function after this request. This is useful to modify
+ response objects. The function is passed the response object and has
+ to return the same or a new one.
+
+ Example::
+
+ @app.route('/')
+ def index():
+ @after_this_request
+ def add_header(response):
+ response.headers['X-Foo'] = 'Parachute'
+ return response
+ return 'Hello World!'
+
+ This is more useful if a function other than the view function wants to
+ modify a response. For instance think of a decorator that wants to add
+ some headers without converting the return value into a response object.
+
+ .. versionadded:: 0.9
+ """
+ ctx = _cv_request.get(None)
+
+ if ctx is None:
+ raise RuntimeError(
+ "'after_this_request' can only be used when a request"
+ " context is active, such as in a view function."
+ )
+
+ ctx._after_request_functions.append(f)
+ return f
+
+
+F = t.TypeVar("F", bound=t.Callable[..., t.Any])
+
+
+def copy_current_request_context(f: F) -> F:
+ """A helper function that decorates a function to retain the current
+ request context. This is useful when working with greenlets. The moment
+ the function is decorated a copy of the request context is created and
+ then pushed when the function is called. The current session is also
+ included in the copied request context.
+
+ Example::
+
+ import gevent
+ from flask import copy_current_request_context
+
+ @app.route('/')
+ def index():
+ @copy_current_request_context
+ def do_some_work():
+ # do some work here, it can access flask.request or
+ # flask.session like you would otherwise in the view function.
+ ...
+ gevent.spawn(do_some_work)
+ return 'Regular response'
+
+ .. versionadded:: 0.10
+ """
+ ctx = _cv_request.get(None)
+
+ if ctx is None:
+ raise RuntimeError(
+ "'copy_current_request_context' can only be used when a"
+ " request context is active, such as in a view function."
+ )
+
+ ctx = ctx.copy()
+
+ def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
+ with ctx:
+ return ctx.app.ensure_sync(f)(*args, **kwargs)
+
+ return update_wrapper(wrapper, f) # type: ignore[return-value]
+
+
+def has_request_context() -> bool:
+ """If you have code that wants to test if a request context is there or
+ not this function can be used. For instance, you may want to take advantage
+ of request information if the request object is available, but fail
+ silently if it is unavailable.
+
+ ::
+
+ class User(db.Model):
+
+ def __init__(self, username, remote_addr=None):
+ self.username = username
+ if remote_addr is None and has_request_context():
+ remote_addr = request.remote_addr
+ self.remote_addr = remote_addr
+
+ Alternatively you can also just test any of the context bound objects
+ (such as :class:`request` or :class:`g`) for truthness::
+
+ class User(db.Model):
+
+ def __init__(self, username, remote_addr=None):
+ self.username = username
+ if remote_addr is None and request:
+ remote_addr = request.remote_addr
+ self.remote_addr = remote_addr
+
+ .. versionadded:: 0.7
+ """
+ return _cv_request.get(None) is not None
+
+
+def has_app_context() -> bool:
+ """Works like :func:`has_request_context` but for the application
+ context. You can also just do a boolean check on the
+ :data:`current_app` object instead.
+
+ .. versionadded:: 0.9
+ """
+ return _cv_app.get(None) is not None
+
+
+class AppContext:
+ """The app context contains application-specific information. An app
+ context is created and pushed at the beginning of each request if
+ one is not already active. An app context is also pushed when
+ running CLI commands.
+ """
+
+ def __init__(self, app: Flask) -> None:
+ self.app = app
+ self.url_adapter = app.create_url_adapter(None)
+ self.g: _AppCtxGlobals = app.app_ctx_globals_class()
+ self._cv_tokens: list[contextvars.Token[AppContext]] = []
+
+ def push(self) -> None:
+ """Binds the app context to the current context."""
+ self._cv_tokens.append(_cv_app.set(self))
+ appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync)
+
+ def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore
+ """Pops the app context."""
+ try:
+ if len(self._cv_tokens) == 1:
+ if exc is _sentinel:
+ exc = sys.exc_info()[1]
+ self.app.do_teardown_appcontext(exc)
+ finally:
+ ctx = _cv_app.get()
+ _cv_app.reset(self._cv_tokens.pop())
+
+ if ctx is not self:
+ raise AssertionError(
+ f"Popped wrong app context. ({ctx!r} instead of {self!r})"
+ )
+
+ appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync)
+
+ def __enter__(self) -> AppContext:
+ self.push()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> None:
+ self.pop(exc_value)
+
+
+class RequestContext:
+ """The request context contains per-request information. The Flask
+ app creates and pushes it at the beginning of the request, then pops
+ it at the end of the request. It will create the URL adapter and
+ request object for the WSGI environment provided.
+
+ Do not attempt to use this class directly, instead use
+ :meth:`~flask.Flask.test_request_context` and
+ :meth:`~flask.Flask.request_context` to create this object.
+
+ When the request context is popped, it will evaluate all the
+ functions registered on the application for teardown execution
+ (:meth:`~flask.Flask.teardown_request`).
+
+ The request context is automatically popped at the end of the
+ request. When using the interactive debugger, the context will be
+ restored so ``request`` is still accessible. Similarly, the test
+ client can preserve the context after the request ends. However,
+ teardown functions may already have closed some resources such as
+ database connections.
+ """
+
+ def __init__(
+ self,
+ app: Flask,
+ environ: WSGIEnvironment,
+ request: Request | None = None,
+ session: SessionMixin | None = None,
+ ) -> None:
+ self.app = app
+ if request is None:
+ request = app.request_class(environ)
+ request.json_module = app.json
+ self.request: Request = request
+ self.url_adapter = None
+ try:
+ self.url_adapter = app.create_url_adapter(self.request)
+ except HTTPException as e:
+ self.request.routing_exception = e
+ self.flashes: list[tuple[str, str]] | None = None
+ self._session: SessionMixin | None = session
+ # Functions that should be executed after the request on the response
+ # object. These will be called before the regular "after_request"
+ # functions.
+ self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = []
+
+ self._cv_tokens: list[
+ tuple[contextvars.Token[RequestContext], AppContext | None]
+ ] = []
+
+ def copy(self) -> RequestContext:
+ """Creates a copy of this request context with the same request object.
+ This can be used to move a request context to a different greenlet.
+ Because the actual request object is the same this cannot be used to
+ move a request context to a different thread unless access to the
+ request object is locked.
+
+ .. versionadded:: 0.10
+
+ .. versionchanged:: 1.1
+ The current session object is used instead of reloading the original
+ data. This prevents `flask.session` pointing to an out-of-date object.
+ """
+ return self.__class__(
+ self.app,
+ environ=self.request.environ,
+ request=self.request,
+ session=self._session,
+ )
+
+ def match_request(self) -> None:
+ """Can be overridden by a subclass to hook into the matching
+ of the request.
+ """
+ try:
+ result = self.url_adapter.match(return_rule=True) # type: ignore
+ self.request.url_rule, self.request.view_args = result # type: ignore
+ except HTTPException as e:
+ self.request.routing_exception = e
+
+ @property
+ def session(self) -> SessionMixin:
+ """The session data associated with this request. Not available until
+ this context has been pushed. Accessing this property, also accessed by
+ the :data:`~flask.session` proxy, sets :attr:`.SessionMixin.accessed`.
+ """
+ assert self._session is not None, "The session has not yet been opened."
+ self._session.accessed = True
+ return self._session
+
+ def push(self) -> None:
+ # Before we push the request context we have to ensure that there
+ # is an application context.
+ app_ctx = _cv_app.get(None)
+
+ if app_ctx is None or app_ctx.app is not self.app:
+ app_ctx = self.app.app_context()
+ app_ctx.push()
+ else:
+ app_ctx = None
+
+ self._cv_tokens.append((_cv_request.set(self), app_ctx))
+
+ # Open the session at the moment that the request context is available.
+ # This allows a custom open_session method to use the request context.
+ # Only open a new session if this is the first time the request was
+ # pushed, otherwise stream_with_context loses the session.
+ if self._session is None:
+ session_interface = self.app.session_interface
+ self._session = session_interface.open_session(self.app, self.request)
+
+ if self._session is None:
+ self._session = session_interface.make_null_session(self.app)
+
+ # Match the request URL after loading the session, so that the
+ # session is available in custom URL converters.
+ if self.url_adapter is not None:
+ self.match_request()
+
+ def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore
+ """Pops the request context and unbinds it by doing that. This will
+ also trigger the execution of functions registered by the
+ :meth:`~flask.Flask.teardown_request` decorator.
+
+ .. versionchanged:: 0.9
+ Added the `exc` argument.
+ """
+ clear_request = len(self._cv_tokens) == 1
+
+ try:
+ if clear_request:
+ if exc is _sentinel:
+ exc = sys.exc_info()[1]
+ self.app.do_teardown_request(exc)
+
+ request_close = getattr(self.request, "close", None)
+ if request_close is not None:
+ request_close()
+ finally:
+ ctx = _cv_request.get()
+ token, app_ctx = self._cv_tokens.pop()
+ _cv_request.reset(token)
+
+ # get rid of circular dependencies at the end of the request
+ # so that we don't require the GC to be active.
+ if clear_request:
+ ctx.request.environ["werkzeug.request"] = None
+
+ if app_ctx is not None:
+ app_ctx.pop(exc)
+
+ if ctx is not self:
+ raise AssertionError(
+ f"Popped wrong request context. ({ctx!r} instead of {self!r})"
+ )
+
+ def __enter__(self) -> RequestContext:
+ self.push()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> None:
+ self.pop(exc_value)
+
+ def __repr__(self) -> str:
+ return (
+ f"<{type(self).__name__} {self.request.url!r}"
+ f" [{self.request.method}] of {self.app.name}>"
+ )
diff --git a/venv/Lib/site-packages/flask/debughelpers.py b/venv/Lib/site-packages/flask/debughelpers.py
new file mode 100644
index 0000000..2c8c4c4
--- /dev/null
+++ b/venv/Lib/site-packages/flask/debughelpers.py
@@ -0,0 +1,178 @@
+from __future__ import annotations
+
+import typing as t
+
+from jinja2.loaders import BaseLoader
+from werkzeug.routing import RequestRedirect
+
+from .blueprints import Blueprint
+from .globals import request_ctx
+from .sansio.app import App
+
+if t.TYPE_CHECKING:
+ from .sansio.scaffold import Scaffold
+ from .wrappers import Request
+
+
+class UnexpectedUnicodeError(AssertionError, UnicodeError):
+ """Raised in places where we want some better error reporting for
+ unexpected unicode or binary data.
+ """
+
+
+class DebugFilesKeyError(KeyError, AssertionError):
+ """Raised from request.files during debugging. The idea is that it can
+ provide a better error message than just a generic KeyError/BadRequest.
+ """
+
+ def __init__(self, request: Request, key: str) -> None:
+ form_matches = request.form.getlist(key)
+ buf = [
+ f"You tried to access the file {key!r} in the request.files"
+ " dictionary but it does not exist. The mimetype for the"
+ f" request is {request.mimetype!r} instead of"
+ " 'multipart/form-data' which means that no file contents"
+ " were transmitted. To fix this error you should provide"
+ ' enctype="multipart/form-data" in your form.'
+ ]
+ if form_matches:
+ names = ", ".join(repr(x) for x in form_matches)
+ buf.append(
+ "\n\nThe browser instead transmitted some file names. "
+ f"This was submitted: {names}"
+ )
+ self.msg = "".join(buf)
+
+ def __str__(self) -> str:
+ return self.msg
+
+
+class FormDataRoutingRedirect(AssertionError):
+ """This exception is raised in debug mode if a routing redirect
+ would cause the browser to drop the method or body. This happens
+ when method is not GET, HEAD or OPTIONS and the status code is not
+ 307 or 308.
+ """
+
+ def __init__(self, request: Request) -> None:
+ exc = request.routing_exception
+ assert isinstance(exc, RequestRedirect)
+ buf = [
+ f"A request was sent to '{request.url}', but routing issued"
+ f" a redirect to the canonical URL '{exc.new_url}'."
+ ]
+
+ if f"{request.base_url}/" == exc.new_url.partition("?")[0]:
+ buf.append(
+ " The URL was defined with a trailing slash. Flask"
+ " will redirect to the URL with a trailing slash if it"
+ " was accessed without one."
+ )
+
+ buf.append(
+ " Send requests to the canonical URL, or use 307 or 308 for"
+ " routing redirects. Otherwise, browsers will drop form"
+ " data.\n\n"
+ "This exception is only raised in debug mode."
+ )
+ super().__init__("".join(buf))
+
+
+def attach_enctype_error_multidict(request: Request) -> None:
+ """Patch ``request.files.__getitem__`` to raise a descriptive error
+ about ``enctype=multipart/form-data``.
+
+ :param request: The request to patch.
+ :meta private:
+ """
+ oldcls = request.files.__class__
+
+ class newcls(oldcls): # type: ignore[valid-type, misc]
+ def __getitem__(self, key: str) -> t.Any:
+ try:
+ return super().__getitem__(key)
+ except KeyError as e:
+ if key not in request.form:
+ raise
+
+ raise DebugFilesKeyError(request, key).with_traceback(
+ e.__traceback__
+ ) from None
+
+ newcls.__name__ = oldcls.__name__
+ newcls.__module__ = oldcls.__module__
+ request.files.__class__ = newcls
+
+
+def _dump_loader_info(loader: BaseLoader) -> t.Iterator[str]:
+ yield f"class: {type(loader).__module__}.{type(loader).__name__}"
+ for key, value in sorted(loader.__dict__.items()):
+ if key.startswith("_"):
+ continue
+ if isinstance(value, (tuple, list)):
+ if not all(isinstance(x, str) for x in value):
+ continue
+ yield f"{key}:"
+ for item in value:
+ yield f" - {item}"
+ continue
+ elif not isinstance(value, (str, int, float, bool)):
+ continue
+ yield f"{key}: {value!r}"
+
+
+def explain_template_loading_attempts(
+ app: App,
+ template: str,
+ attempts: list[
+ tuple[
+ BaseLoader,
+ Scaffold,
+ tuple[str, str | None, t.Callable[[], bool] | None] | None,
+ ]
+ ],
+) -> None:
+ """This should help developers understand what failed"""
+ info = [f"Locating template {template!r}:"]
+ total_found = 0
+ blueprint = None
+ if request_ctx and request_ctx.request.blueprint is not None:
+ blueprint = request_ctx.request.blueprint
+
+ for idx, (loader, srcobj, triple) in enumerate(attempts):
+ if isinstance(srcobj, App):
+ src_info = f"application {srcobj.import_name!r}"
+ elif isinstance(srcobj, Blueprint):
+ src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})"
+ else:
+ src_info = repr(srcobj)
+
+ info.append(f"{idx + 1:5}: trying loader of {src_info}")
+
+ for line in _dump_loader_info(loader):
+ info.append(f" {line}")
+
+ if triple is None:
+ detail = "no match"
+ else:
+ detail = f"found ({triple[1] or ''!r})"
+ total_found += 1
+ info.append(f" -> {detail}")
+
+ seems_fishy = False
+ if total_found == 0:
+ info.append("Error: the template could not be found.")
+ seems_fishy = True
+ elif total_found > 1:
+ info.append("Warning: multiple loaders returned a match for the template.")
+ seems_fishy = True
+
+ if blueprint is not None and seems_fishy:
+ info.append(
+ " The template was looked up from an endpoint that belongs"
+ f" to the blueprint {blueprint!r}."
+ )
+ info.append(" Maybe you did not place a template in the right folder?")
+ info.append(" See https://flask.palletsprojects.com/blueprints/#templates")
+
+ app.logger.info("\n".join(info))
diff --git a/venv/Lib/site-packages/flask/globals.py b/venv/Lib/site-packages/flask/globals.py
new file mode 100644
index 0000000..e2c410c
--- /dev/null
+++ b/venv/Lib/site-packages/flask/globals.py
@@ -0,0 +1,51 @@
+from __future__ import annotations
+
+import typing as t
+from contextvars import ContextVar
+
+from werkzeug.local import LocalProxy
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from .app import Flask
+ from .ctx import _AppCtxGlobals
+ from .ctx import AppContext
+ from .ctx import RequestContext
+ from .sessions import SessionMixin
+ from .wrappers import Request
+
+
+_no_app_msg = """\
+Working outside of application context.
+
+This typically means that you attempted to use functionality that needed
+the current application. To solve this, set up an application context
+with app.app_context(). See the documentation for more information.\
+"""
+_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx")
+app_ctx: AppContext = LocalProxy( # type: ignore[assignment]
+ _cv_app, unbound_message=_no_app_msg
+)
+current_app: Flask = LocalProxy( # type: ignore[assignment]
+ _cv_app, "app", unbound_message=_no_app_msg
+)
+g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment]
+ _cv_app, "g", unbound_message=_no_app_msg
+)
+
+_no_req_msg = """\
+Working outside of request context.
+
+This typically means that you attempted to use functionality that needed
+an active HTTP request. Consult the documentation on testing for
+information about how to avoid this problem.\
+"""
+_cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx")
+request_ctx: RequestContext = LocalProxy( # type: ignore[assignment]
+ _cv_request, unbound_message=_no_req_msg
+)
+request: Request = LocalProxy( # type: ignore[assignment]
+ _cv_request, "request", unbound_message=_no_req_msg
+)
+session: SessionMixin = LocalProxy( # type: ignore[assignment]
+ _cv_request, "session", unbound_message=_no_req_msg
+)
diff --git a/venv/Lib/site-packages/flask/helpers.py b/venv/Lib/site-packages/flask/helpers.py
new file mode 100644
index 0000000..5d412c9
--- /dev/null
+++ b/venv/Lib/site-packages/flask/helpers.py
@@ -0,0 +1,641 @@
+from __future__ import annotations
+
+import importlib.util
+import os
+import sys
+import typing as t
+from datetime import datetime
+from functools import cache
+from functools import update_wrapper
+
+import werkzeug.utils
+from werkzeug.exceptions import abort as _wz_abort
+from werkzeug.utils import redirect as _wz_redirect
+from werkzeug.wrappers import Response as BaseResponse
+
+from .globals import _cv_app
+from .globals import _cv_request
+from .globals import current_app
+from .globals import request
+from .globals import request_ctx
+from .globals import session
+from .signals import message_flashed
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from .wrappers import Response
+
+
+def get_debug_flag() -> bool:
+ """Get whether debug mode should be enabled for the app, indicated by the
+ :envvar:`FLASK_DEBUG` environment variable. The default is ``False``.
+ """
+ val = os.environ.get("FLASK_DEBUG")
+ return bool(val and val.lower() not in {"0", "false", "no"})
+
+
+def get_load_dotenv(default: bool = True) -> bool:
+ """Get whether the user has disabled loading default dotenv files by
+ setting :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load
+ the files.
+
+ :param default: What to return if the env var isn't set.
+ """
+ val = os.environ.get("FLASK_SKIP_DOTENV")
+
+ if not val:
+ return default
+
+ return val.lower() in ("0", "false", "no")
+
+
+@t.overload
+def stream_with_context(
+ generator_or_function: t.Iterator[t.AnyStr],
+) -> t.Iterator[t.AnyStr]: ...
+
+
+@t.overload
+def stream_with_context(
+ generator_or_function: t.Callable[..., t.Iterator[t.AnyStr]],
+) -> t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: ...
+
+
+def stream_with_context(
+ generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]],
+) -> t.Iterator[t.AnyStr] | t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]:
+ """Wrap a response generator function so that it runs inside the current
+ request context. This keeps :data:`request`, :data:`session`, and :data:`g`
+ available, even though at the point the generator runs the request context
+ will typically have ended.
+
+ Use it as a decorator on a generator function:
+
+ .. code-block:: python
+
+ from flask import stream_with_context, request, Response
+
+ @app.get("/stream")
+ def streamed_response():
+ @stream_with_context
+ def generate():
+ yield "Hello "
+ yield request.args["name"]
+ yield "!"
+
+ return Response(generate())
+
+ Or use it as a wrapper around a created generator:
+
+ .. code-block:: python
+
+ from flask import stream_with_context, request, Response
+
+ @app.get("/stream")
+ def streamed_response():
+ def generate():
+ yield "Hello "
+ yield request.args["name"]
+ yield "!"
+
+ return Response(stream_with_context(generate()))
+
+ .. versionadded:: 0.9
+ """
+ try:
+ gen = iter(generator_or_function) # type: ignore[arg-type]
+ except TypeError:
+
+ def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any:
+ gen = generator_or_function(*args, **kwargs) # type: ignore[operator]
+ return stream_with_context(gen)
+
+ return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type]
+
+ def generator() -> t.Iterator[t.AnyStr]:
+ if (req_ctx := _cv_request.get(None)) is None:
+ raise RuntimeError(
+ "'stream_with_context' can only be used when a request"
+ " context is active, such as in a view function."
+ )
+
+ app_ctx = _cv_app.get()
+ # Setup code below will run the generator to this point, so that the
+ # current contexts are recorded. The contexts must be pushed after,
+ # otherwise their ContextVar will record the wrong event loop during
+ # async view functions.
+ yield None # type: ignore[misc]
+
+ # Push the app context first, so that the request context does not
+ # automatically create and push a different app context.
+ with app_ctx, req_ctx:
+ try:
+ yield from gen
+ finally:
+ # Clean up in case the user wrapped a WSGI iterator.
+ if hasattr(gen, "close"):
+ gen.close()
+
+ # Execute the generator to the sentinel value. This ensures the context is
+ # preserved in the generator's state. Further iteration will push the
+ # context and yield from the original iterator.
+ wrapped_g = generator()
+ next(wrapped_g)
+ return wrapped_g
+
+
+def make_response(*args: t.Any) -> Response:
+ """Sometimes it is necessary to set additional headers in a view. Because
+ views do not have to return response objects but can return a value that
+ is converted into a response object by Flask itself, it becomes tricky to
+ add headers to it. This function can be called instead of using a return
+ and you will get a response object which you can use to attach headers.
+
+ If view looked like this and you want to add a new header::
+
+ def index():
+ return render_template('index.html', foo=42)
+
+ You can now do something like this::
+
+ def index():
+ response = make_response(render_template('index.html', foo=42))
+ response.headers['X-Parachutes'] = 'parachutes are cool'
+ return response
+
+ This function accepts the very same arguments you can return from a
+ view function. This for example creates a response with a 404 error
+ code::
+
+ response = make_response(render_template('not_found.html'), 404)
+
+ The other use case of this function is to force the return value of a
+ view function into a response which is helpful with view
+ decorators::
+
+ response = make_response(view_function())
+ response.headers['X-Parachutes'] = 'parachutes are cool'
+
+ Internally this function does the following things:
+
+ - if no arguments are passed, it creates a new response argument
+ - if one argument is passed, :meth:`flask.Flask.make_response`
+ is invoked with it.
+ - if more than one argument is passed, the arguments are passed
+ to the :meth:`flask.Flask.make_response` function as tuple.
+
+ .. versionadded:: 0.6
+ """
+ if not args:
+ return current_app.response_class()
+ if len(args) == 1:
+ args = args[0]
+ return current_app.make_response(args)
+
+
+def url_for(
+ endpoint: str,
+ *,
+ _anchor: str | None = None,
+ _method: str | None = None,
+ _scheme: str | None = None,
+ _external: bool | None = None,
+ **values: t.Any,
+) -> str:
+ """Generate a URL to the given endpoint with the given values.
+
+ This requires an active request or application context, and calls
+ :meth:`current_app.url_for() `. See that method
+ for full documentation.
+
+ :param endpoint: The endpoint name associated with the URL to
+ generate. If this starts with a ``.``, the current blueprint
+ name (if any) will be used.
+ :param _anchor: If given, append this as ``#anchor`` to the URL.
+ :param _method: If given, generate the URL associated with this
+ method for the endpoint.
+ :param _scheme: If given, the URL will have this scheme if it is
+ external.
+ :param _external: If given, prefer the URL to be internal (False) or
+ require it to be external (True). External URLs include the
+ scheme and domain. When not in an active request, URLs are
+ external by default.
+ :param values: Values to use for the variable parts of the URL rule.
+ Unknown keys are appended as query string arguments, like
+ ``?a=b&c=d``.
+
+ .. versionchanged:: 2.2
+ Calls ``current_app.url_for``, allowing an app to override the
+ behavior.
+
+ .. versionchanged:: 0.10
+ The ``_scheme`` parameter was added.
+
+ .. versionchanged:: 0.9
+ The ``_anchor`` and ``_method`` parameters were added.
+
+ .. versionchanged:: 0.9
+ Calls ``app.handle_url_build_error`` on build errors.
+ """
+ return current_app.url_for(
+ endpoint,
+ _anchor=_anchor,
+ _method=_method,
+ _scheme=_scheme,
+ _external=_external,
+ **values,
+ )
+
+
+def redirect(
+ location: str, code: int = 302, Response: type[BaseResponse] | None = None
+) -> BaseResponse:
+ """Create a redirect response object.
+
+ If :data:`~flask.current_app` is available, it will use its
+ :meth:`~flask.Flask.redirect` method, otherwise it will use
+ :func:`werkzeug.utils.redirect`.
+
+ :param location: The URL to redirect to.
+ :param code: The status code for the redirect.
+ :param Response: The response class to use. Not used when
+ ``current_app`` is active, which uses ``app.response_class``.
+
+ .. versionadded:: 2.2
+ Calls ``current_app.redirect`` if available instead of always
+ using Werkzeug's default ``redirect``.
+ """
+ if current_app:
+ return current_app.redirect(location, code=code)
+
+ return _wz_redirect(location, code=code, Response=Response)
+
+
+def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
+ """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given
+ status code.
+
+ If :data:`~flask.current_app` is available, it will call its
+ :attr:`~flask.Flask.aborter` object, otherwise it will use
+ :func:`werkzeug.exceptions.abort`.
+
+ :param code: The status code for the exception, which must be
+ registered in ``app.aborter``.
+ :param args: Passed to the exception.
+ :param kwargs: Passed to the exception.
+
+ .. versionadded:: 2.2
+ Calls ``current_app.aborter`` if available instead of always
+ using Werkzeug's default ``abort``.
+ """
+ if current_app:
+ current_app.aborter(code, *args, **kwargs)
+
+ _wz_abort(code, *args, **kwargs)
+
+
+def get_template_attribute(template_name: str, attribute: str) -> t.Any:
+ """Loads a macro (or variable) a template exports. This can be used to
+ invoke a macro from within Python code. If you for example have a
+ template named :file:`_cider.html` with the following contents:
+
+ .. sourcecode:: html+jinja
+
+ {% macro hello(name) %}Hello {{ name }}!{% endmacro %}
+
+ You can access this from Python code like this::
+
+ hello = get_template_attribute('_cider.html', 'hello')
+ return hello('World')
+
+ .. versionadded:: 0.2
+
+ :param template_name: the name of the template
+ :param attribute: the name of the variable of macro to access
+ """
+ return getattr(current_app.jinja_env.get_template(template_name).module, attribute)
+
+
+def flash(message: str, category: str = "message") -> None:
+ """Flashes a message to the next request. In order to remove the
+ flashed message from the session and to display it to the user,
+ the template has to call :func:`get_flashed_messages`.
+
+ .. versionchanged:: 0.3
+ `category` parameter added.
+
+ :param message: the message to be flashed.
+ :param category: the category for the message. The following values
+ are recommended: ``'message'`` for any kind of message,
+ ``'error'`` for errors, ``'info'`` for information
+ messages and ``'warning'`` for warnings. However any
+ kind of string can be used as category.
+ """
+ # Original implementation:
+ #
+ # session.setdefault('_flashes', []).append((category, message))
+ #
+ # This assumed that changes made to mutable structures in the session are
+ # always in sync with the session object, which is not true for session
+ # implementations that use external storage for keeping their keys/values.
+ flashes = session.get("_flashes", [])
+ flashes.append((category, message))
+ session["_flashes"] = flashes
+ app = current_app._get_current_object() # type: ignore
+ message_flashed.send(
+ app,
+ _async_wrapper=app.ensure_sync,
+ message=message,
+ category=category,
+ )
+
+
+def get_flashed_messages(
+ with_categories: bool = False, category_filter: t.Iterable[str] = ()
+) -> list[str] | list[tuple[str, str]]:
+ """Pulls all flashed messages from the session and returns them.
+ Further calls in the same request to the function will return
+ the same messages. By default just the messages are returned,
+ but when `with_categories` is set to ``True``, the return value will
+ be a list of tuples in the form ``(category, message)`` instead.
+
+ Filter the flashed messages to one or more categories by providing those
+ categories in `category_filter`. This allows rendering categories in
+ separate html blocks. The `with_categories` and `category_filter`
+ arguments are distinct:
+
+ * `with_categories` controls whether categories are returned with message
+ text (``True`` gives a tuple, where ``False`` gives just the message text).
+ * `category_filter` filters the messages down to only those matching the
+ provided categories.
+
+ See :doc:`/patterns/flashing` for examples.
+
+ .. versionchanged:: 0.3
+ `with_categories` parameter added.
+
+ .. versionchanged:: 0.9
+ `category_filter` parameter added.
+
+ :param with_categories: set to ``True`` to also receive categories.
+ :param category_filter: filter of categories to limit return values. Only
+ categories in the list will be returned.
+ """
+ flashes = request_ctx.flashes
+ if flashes is None:
+ flashes = session.pop("_flashes") if "_flashes" in session else []
+ request_ctx.flashes = flashes
+ if category_filter:
+ flashes = list(filter(lambda f: f[0] in category_filter, flashes))
+ if not with_categories:
+ return [x[1] for x in flashes]
+ return flashes
+
+
+def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]:
+ if kwargs.get("max_age") is None:
+ kwargs["max_age"] = current_app.get_send_file_max_age
+
+ kwargs.update(
+ environ=request.environ,
+ use_x_sendfile=current_app.config["USE_X_SENDFILE"],
+ response_class=current_app.response_class,
+ _root_path=current_app.root_path,
+ )
+ return kwargs
+
+
+def send_file(
+ path_or_file: os.PathLike[t.AnyStr] | str | t.IO[bytes],
+ mimetype: str | None = None,
+ as_attachment: bool = False,
+ download_name: str | None = None,
+ conditional: bool = True,
+ etag: bool | str = True,
+ last_modified: datetime | int | float | None = None,
+ max_age: None | (int | t.Callable[[str | None], int | None]) = None,
+) -> Response:
+ """Send the contents of a file to the client.
+
+ The first argument can be a file path or a file-like object. Paths
+ are preferred in most cases because Werkzeug can manage the file and
+ get extra information from the path. Passing a file-like object
+ requires that the file is opened in binary mode, and is mostly
+ useful when building a file in memory with :class:`io.BytesIO`.
+
+ Never pass file paths provided by a user. The path is assumed to be
+ trusted, so a user could craft a path to access a file you didn't
+ intend. Use :func:`send_from_directory` to safely serve
+ user-requested paths from within a directory.
+
+ If the WSGI server sets a ``file_wrapper`` in ``environ``, it is
+ used, otherwise Werkzeug's built-in wrapper is used. Alternatively,
+ if the HTTP server supports ``X-Sendfile``, configuring Flask with
+ ``USE_X_SENDFILE = True`` will tell the server to send the given
+ path, which is much more efficient than reading it in Python.
+
+ :param path_or_file: The path to the file to send, relative to the
+ current working directory if a relative path is given.
+ Alternatively, a file-like object opened in binary mode. Make
+ sure the file pointer is seeked to the start of the data.
+ :param mimetype: The MIME type to send for the file. If not
+ provided, it will try to detect it from the file name.
+ :param as_attachment: Indicate to a browser that it should offer to
+ save the file instead of displaying it.
+ :param download_name: The default name browsers will use when saving
+ the file. Defaults to the passed file name.
+ :param conditional: Enable conditional and range responses based on
+ request headers. Requires passing a file path and ``environ``.
+ :param etag: Calculate an ETag for the file, which requires passing
+ a file path. Can also be a string to use instead.
+ :param last_modified: The last modified time to send for the file,
+ in seconds. If not provided, it will try to detect it from the
+ file path.
+ :param max_age: How long the client should cache the file, in
+ seconds. If set, ``Cache-Control`` will be ``public``, otherwise
+ it will be ``no-cache`` to prefer conditional caching.
+
+ .. versionchanged:: 2.0
+ ``download_name`` replaces the ``attachment_filename``
+ parameter. If ``as_attachment=False``, it is passed with
+ ``Content-Disposition: inline`` instead.
+
+ .. versionchanged:: 2.0
+ ``max_age`` replaces the ``cache_timeout`` parameter.
+ ``conditional`` is enabled and ``max_age`` is not set by
+ default.
+
+ .. versionchanged:: 2.0
+ ``etag`` replaces the ``add_etags`` parameter. It can be a
+ string to use instead of generating one.
+
+ .. versionchanged:: 2.0
+ Passing a file-like object that inherits from
+ :class:`~io.TextIOBase` will raise a :exc:`ValueError` rather
+ than sending an empty file.
+
+ .. versionadded:: 2.0
+ Moved the implementation to Werkzeug. This is now a wrapper to
+ pass some Flask-specific arguments.
+
+ .. versionchanged:: 1.1
+ ``filename`` may be a :class:`~os.PathLike` object.
+
+ .. versionchanged:: 1.1
+ Passing a :class:`~io.BytesIO` object supports range requests.
+
+ .. versionchanged:: 1.0.3
+ Filenames are encoded with ASCII instead of Latin-1 for broader
+ compatibility with WSGI servers.
+
+ .. versionchanged:: 1.0
+ UTF-8 filenames as specified in :rfc:`2231` are supported.
+
+ .. versionchanged:: 0.12
+ The filename is no longer automatically inferred from file
+ objects. If you want to use automatic MIME and etag support,
+ pass a filename via ``filename_or_fp`` or
+ ``attachment_filename``.
+
+ .. versionchanged:: 0.12
+ ``attachment_filename`` is preferred over ``filename`` for MIME
+ detection.
+
+ .. versionchanged:: 0.9
+ ``cache_timeout`` defaults to
+ :meth:`Flask.get_send_file_max_age`.
+
+ .. versionchanged:: 0.7
+ MIME guessing and etag support for file-like objects was
+ removed because it was unreliable. Pass a filename if you are
+ able to, otherwise attach an etag yourself.
+
+ .. versionchanged:: 0.5
+ The ``add_etags``, ``cache_timeout`` and ``conditional``
+ parameters were added. The default behavior is to add etags.
+
+ .. versionadded:: 0.2
+ """
+ return werkzeug.utils.send_file( # type: ignore[return-value]
+ **_prepare_send_file_kwargs(
+ path_or_file=path_or_file,
+ environ=request.environ,
+ mimetype=mimetype,
+ as_attachment=as_attachment,
+ download_name=download_name,
+ conditional=conditional,
+ etag=etag,
+ last_modified=last_modified,
+ max_age=max_age,
+ )
+ )
+
+
+def send_from_directory(
+ directory: os.PathLike[str] | str,
+ path: os.PathLike[str] | str,
+ **kwargs: t.Any,
+) -> Response:
+ """Send a file from within a directory using :func:`send_file`.
+
+ .. code-block:: python
+
+ @app.route("/uploads/")
+ def download_file(name):
+ return send_from_directory(
+ app.config['UPLOAD_FOLDER'], name, as_attachment=True
+ )
+
+ This is a secure way to serve files from a folder, such as static
+ files or uploads. Uses :func:`~werkzeug.security.safe_join` to
+ ensure the path coming from the client is not maliciously crafted to
+ point outside the specified directory.
+
+ If the final path does not point to an existing regular file,
+ raises a 404 :exc:`~werkzeug.exceptions.NotFound` error.
+
+ :param directory: The directory that ``path`` must be located under,
+ relative to the current application's root path. This *must not*
+ be a value provided by the client, otherwise it becomes insecure.
+ :param path: The path to the file to send, relative to
+ ``directory``.
+ :param kwargs: Arguments to pass to :func:`send_file`.
+
+ .. versionchanged:: 2.0
+ ``path`` replaces the ``filename`` parameter.
+
+ .. versionadded:: 2.0
+ Moved the implementation to Werkzeug. This is now a wrapper to
+ pass some Flask-specific arguments.
+
+ .. versionadded:: 0.5
+ """
+ return werkzeug.utils.send_from_directory( # type: ignore[return-value]
+ directory, path, **_prepare_send_file_kwargs(**kwargs)
+ )
+
+
+def get_root_path(import_name: str) -> str:
+ """Find the root path of a package, or the path that contains a
+ module. If it cannot be found, returns the current working
+ directory.
+
+ Not to be confused with the value returned by :func:`find_package`.
+
+ :meta private:
+ """
+ # Module already imported and has a file attribute. Use that first.
+ mod = sys.modules.get(import_name)
+
+ if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None:
+ return os.path.dirname(os.path.abspath(mod.__file__))
+
+ # Next attempt: check the loader.
+ try:
+ spec = importlib.util.find_spec(import_name)
+
+ if spec is None:
+ raise ValueError
+ except (ImportError, ValueError):
+ loader = None
+ else:
+ loader = spec.loader
+
+ # Loader does not exist or we're referring to an unloaded main
+ # module or a main module without path (interactive sessions), go
+ # with the current working directory.
+ if loader is None:
+ return os.getcwd()
+
+ if hasattr(loader, "get_filename"):
+ filepath = loader.get_filename(import_name) # pyright: ignore
+ else:
+ # Fall back to imports.
+ __import__(import_name)
+ mod = sys.modules[import_name]
+ filepath = getattr(mod, "__file__", None)
+
+ # If we don't have a file path it might be because it is a
+ # namespace package. In this case pick the root path from the
+ # first module that is contained in the package.
+ if filepath is None:
+ raise RuntimeError(
+ "No root path can be found for the provided module"
+ f" {import_name!r}. This can happen because the module"
+ " came from an import hook that does not provide file"
+ " name information or because it's a namespace package."
+ " In this case the root path needs to be explicitly"
+ " provided."
+ )
+
+ # filepath is import_name.py for a module, or __init__.py for a package.
+ return os.path.dirname(os.path.abspath(filepath)) # type: ignore[no-any-return]
+
+
+@cache
+def _split_blueprint_path(name: str) -> list[str]:
+ out: list[str] = [name]
+
+ if "." in name:
+ out.extend(_split_blueprint_path(name.rpartition(".")[0]))
+
+ return out
diff --git a/venv/Lib/site-packages/flask/json/__init__.py b/venv/Lib/site-packages/flask/json/__init__.py
new file mode 100644
index 0000000..c0941d0
--- /dev/null
+++ b/venv/Lib/site-packages/flask/json/__init__.py
@@ -0,0 +1,170 @@
+from __future__ import annotations
+
+import json as _json
+import typing as t
+
+from ..globals import current_app
+from .provider import _default
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from ..wrappers import Response
+
+
+def dumps(obj: t.Any, **kwargs: t.Any) -> str:
+ """Serialize data as JSON.
+
+ If :data:`~flask.current_app` is available, it will use its
+ :meth:`app.json.dumps() `
+ method, otherwise it will use :func:`json.dumps`.
+
+ :param obj: The data to serialize.
+ :param kwargs: Arguments passed to the ``dumps`` implementation.
+
+ .. versionchanged:: 2.3
+ The ``app`` parameter was removed.
+
+ .. versionchanged:: 2.2
+ Calls ``current_app.json.dumps``, allowing an app to override
+ the behavior.
+
+ .. versionchanged:: 2.0.2
+ :class:`decimal.Decimal` is supported by converting to a string.
+
+ .. versionchanged:: 2.0
+ ``encoding`` will be removed in Flask 2.1.
+
+ .. versionchanged:: 1.0.3
+ ``app`` can be passed directly, rather than requiring an app
+ context for configuration.
+ """
+ if current_app:
+ return current_app.json.dumps(obj, **kwargs)
+
+ kwargs.setdefault("default", _default)
+ return _json.dumps(obj, **kwargs)
+
+
+def dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None:
+ """Serialize data as JSON and write to a file.
+
+ If :data:`~flask.current_app` is available, it will use its
+ :meth:`app.json.dump() `
+ method, otherwise it will use :func:`json.dump`.
+
+ :param obj: The data to serialize.
+ :param fp: A file opened for writing text. Should use the UTF-8
+ encoding to be valid JSON.
+ :param kwargs: Arguments passed to the ``dump`` implementation.
+
+ .. versionchanged:: 2.3
+ The ``app`` parameter was removed.
+
+ .. versionchanged:: 2.2
+ Calls ``current_app.json.dump``, allowing an app to override
+ the behavior.
+
+ .. versionchanged:: 2.0
+ Writing to a binary file, and the ``encoding`` argument, will be
+ removed in Flask 2.1.
+ """
+ if current_app:
+ current_app.json.dump(obj, fp, **kwargs)
+ else:
+ kwargs.setdefault("default", _default)
+ _json.dump(obj, fp, **kwargs)
+
+
+def loads(s: str | bytes, **kwargs: t.Any) -> t.Any:
+ """Deserialize data as JSON.
+
+ If :data:`~flask.current_app` is available, it will use its
+ :meth:`app.json.loads() `
+ method, otherwise it will use :func:`json.loads`.
+
+ :param s: Text or UTF-8 bytes.
+ :param kwargs: Arguments passed to the ``loads`` implementation.
+
+ .. versionchanged:: 2.3
+ The ``app`` parameter was removed.
+
+ .. versionchanged:: 2.2
+ Calls ``current_app.json.loads``, allowing an app to override
+ the behavior.
+
+ .. versionchanged:: 2.0
+ ``encoding`` will be removed in Flask 2.1. The data must be a
+ string or UTF-8 bytes.
+
+ .. versionchanged:: 1.0.3
+ ``app`` can be passed directly, rather than requiring an app
+ context for configuration.
+ """
+ if current_app:
+ return current_app.json.loads(s, **kwargs)
+
+ return _json.loads(s, **kwargs)
+
+
+def load(fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any:
+ """Deserialize data as JSON read from a file.
+
+ If :data:`~flask.current_app` is available, it will use its
+ :meth:`app.json.load() `
+ method, otherwise it will use :func:`json.load`.
+
+ :param fp: A file opened for reading text or UTF-8 bytes.
+ :param kwargs: Arguments passed to the ``load`` implementation.
+
+ .. versionchanged:: 2.3
+ The ``app`` parameter was removed.
+
+ .. versionchanged:: 2.2
+ Calls ``current_app.json.load``, allowing an app to override
+ the behavior.
+
+ .. versionchanged:: 2.2
+ The ``app`` parameter will be removed in Flask 2.3.
+
+ .. versionchanged:: 2.0
+ ``encoding`` will be removed in Flask 2.1. The file must be text
+ mode, or binary mode with UTF-8 bytes.
+ """
+ if current_app:
+ return current_app.json.load(fp, **kwargs)
+
+ return _json.load(fp, **kwargs)
+
+
+def jsonify(*args: t.Any, **kwargs: t.Any) -> Response:
+ """Serialize the given arguments as JSON, and return a
+ :class:`~flask.Response` object with the ``application/json``
+ mimetype. A dict or list returned from a view will be converted to a
+ JSON response automatically without needing to call this.
+
+ This requires an active request or application context, and calls
+ :meth:`app.json.response() `.
+
+ In debug mode, the output is formatted with indentation to make it
+ easier to read. This may also be controlled by the provider.
+
+ Either positional or keyword arguments can be given, not both.
+ If no arguments are given, ``None`` is serialized.
+
+ :param args: A single value to serialize, or multiple values to
+ treat as a list to serialize.
+ :param kwargs: Treat as a dict to serialize.
+
+ .. versionchanged:: 2.2
+ Calls ``current_app.json.response``, allowing an app to override
+ the behavior.
+
+ .. versionchanged:: 2.0.2
+ :class:`decimal.Decimal` is supported by converting to a string.
+
+ .. versionchanged:: 0.11
+ Added support for serializing top-level arrays. This was a
+ security risk in ancient browsers. See :ref:`security-json`.
+
+ .. versionadded:: 0.2
+ """
+ return current_app.json.response(*args, **kwargs) # type: ignore[return-value]
diff --git a/venv/Lib/site-packages/flask/json/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/flask/json/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..e922f73
Binary files /dev/null and b/venv/Lib/site-packages/flask/json/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/json/__pycache__/provider.cpython-310.pyc b/venv/Lib/site-packages/flask/json/__pycache__/provider.cpython-310.pyc
new file mode 100644
index 0000000..1e69987
Binary files /dev/null and b/venv/Lib/site-packages/flask/json/__pycache__/provider.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/json/__pycache__/tag.cpython-310.pyc b/venv/Lib/site-packages/flask/json/__pycache__/tag.cpython-310.pyc
new file mode 100644
index 0000000..52cf038
Binary files /dev/null and b/venv/Lib/site-packages/flask/json/__pycache__/tag.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/json/provider.py b/venv/Lib/site-packages/flask/json/provider.py
new file mode 100644
index 0000000..ea7e475
--- /dev/null
+++ b/venv/Lib/site-packages/flask/json/provider.py
@@ -0,0 +1,215 @@
+from __future__ import annotations
+
+import dataclasses
+import decimal
+import json
+import typing as t
+import uuid
+import weakref
+from datetime import date
+
+from werkzeug.http import http_date
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from werkzeug.sansio.response import Response
+
+ from ..sansio.app import App
+
+
+class JSONProvider:
+ """A standard set of JSON operations for an application. Subclasses
+ of this can be used to customize JSON behavior or use different
+ JSON libraries.
+
+ To implement a provider for a specific library, subclass this base
+ class and implement at least :meth:`dumps` and :meth:`loads`. All
+ other methods have default implementations.
+
+ To use a different provider, either subclass ``Flask`` and set
+ :attr:`~flask.Flask.json_provider_class` to a provider class, or set
+ :attr:`app.json ` to an instance of the class.
+
+ :param app: An application instance. This will be stored as a
+ :class:`weakref.proxy` on the :attr:`_app` attribute.
+
+ .. versionadded:: 2.2
+ """
+
+ def __init__(self, app: App) -> None:
+ self._app: App = weakref.proxy(app)
+
+ def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
+ """Serialize data as JSON.
+
+ :param obj: The data to serialize.
+ :param kwargs: May be passed to the underlying JSON library.
+ """
+ raise NotImplementedError
+
+ def dump(self, obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None:
+ """Serialize data as JSON and write to a file.
+
+ :param obj: The data to serialize.
+ :param fp: A file opened for writing text. Should use the UTF-8
+ encoding to be valid JSON.
+ :param kwargs: May be passed to the underlying JSON library.
+ """
+ fp.write(self.dumps(obj, **kwargs))
+
+ def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any:
+ """Deserialize data as JSON.
+
+ :param s: Text or UTF-8 bytes.
+ :param kwargs: May be passed to the underlying JSON library.
+ """
+ raise NotImplementedError
+
+ def load(self, fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any:
+ """Deserialize data as JSON read from a file.
+
+ :param fp: A file opened for reading text or UTF-8 bytes.
+ :param kwargs: May be passed to the underlying JSON library.
+ """
+ return self.loads(fp.read(), **kwargs)
+
+ def _prepare_response_obj(
+ self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any]
+ ) -> t.Any:
+ if args and kwargs:
+ raise TypeError("app.json.response() takes either args or kwargs, not both")
+
+ if not args and not kwargs:
+ return None
+
+ if len(args) == 1:
+ return args[0]
+
+ return args or kwargs
+
+ def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
+ """Serialize the given arguments as JSON, and return a
+ :class:`~flask.Response` object with the ``application/json``
+ mimetype.
+
+ The :func:`~flask.json.jsonify` function calls this method for
+ the current application.
+
+ Either positional or keyword arguments can be given, not both.
+ If no arguments are given, ``None`` is serialized.
+
+ :param args: A single value to serialize, or multiple values to
+ treat as a list to serialize.
+ :param kwargs: Treat as a dict to serialize.
+ """
+ obj = self._prepare_response_obj(args, kwargs)
+ return self._app.response_class(self.dumps(obj), mimetype="application/json")
+
+
+def _default(o: t.Any) -> t.Any:
+ if isinstance(o, date):
+ return http_date(o)
+
+ if isinstance(o, (decimal.Decimal, uuid.UUID)):
+ return str(o)
+
+ if dataclasses and dataclasses.is_dataclass(o):
+ return dataclasses.asdict(o) # type: ignore[arg-type]
+
+ if hasattr(o, "__html__"):
+ return str(o.__html__())
+
+ raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
+
+
+class DefaultJSONProvider(JSONProvider):
+ """Provide JSON operations using Python's built-in :mod:`json`
+ library. Serializes the following additional data types:
+
+ - :class:`datetime.datetime` and :class:`datetime.date` are
+ serialized to :rfc:`822` strings. This is the same as the HTTP
+ date format.
+ - :class:`uuid.UUID` is serialized to a string.
+ - :class:`dataclasses.dataclass` is passed to
+ :func:`dataclasses.asdict`.
+ - :class:`~markupsafe.Markup` (or any object with a ``__html__``
+ method) will call the ``__html__`` method to get a string.
+ """
+
+ default: t.Callable[[t.Any], t.Any] = staticmethod(_default) # type: ignore[assignment]
+ """Apply this function to any object that :meth:`json.dumps` does
+ not know how to serialize. It should return a valid JSON type or
+ raise a ``TypeError``.
+ """
+
+ ensure_ascii = True
+ """Replace non-ASCII characters with escape sequences. This may be
+ more compatible with some clients, but can be disabled for better
+ performance and size.
+ """
+
+ sort_keys = True
+ """Sort the keys in any serialized dicts. This may be useful for
+ some caching situations, but can be disabled for better performance.
+ When enabled, keys must all be strings, they are not converted
+ before sorting.
+ """
+
+ compact: bool | None = None
+ """If ``True``, or ``None`` out of debug mode, the :meth:`response`
+ output will not add indentation, newlines, or spaces. If ``False``,
+ or ``None`` in debug mode, it will use a non-compact representation.
+ """
+
+ mimetype = "application/json"
+ """The mimetype set in :meth:`response`."""
+
+ def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
+ """Serialize data as JSON to a string.
+
+ Keyword arguments are passed to :func:`json.dumps`. Sets some
+ parameter defaults from the :attr:`default`,
+ :attr:`ensure_ascii`, and :attr:`sort_keys` attributes.
+
+ :param obj: The data to serialize.
+ :param kwargs: Passed to :func:`json.dumps`.
+ """
+ kwargs.setdefault("default", self.default)
+ kwargs.setdefault("ensure_ascii", self.ensure_ascii)
+ kwargs.setdefault("sort_keys", self.sort_keys)
+ return json.dumps(obj, **kwargs)
+
+ def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any:
+ """Deserialize data as JSON from a string or bytes.
+
+ :param s: Text or UTF-8 bytes.
+ :param kwargs: Passed to :func:`json.loads`.
+ """
+ return json.loads(s, **kwargs)
+
+ def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
+ """Serialize the given arguments as JSON, and return a
+ :class:`~flask.Response` object with it. The response mimetype
+ will be "application/json" and can be changed with
+ :attr:`mimetype`.
+
+ If :attr:`compact` is ``False`` or debug mode is enabled, the
+ output will be formatted to be easier to read.
+
+ Either positional or keyword arguments can be given, not both.
+ If no arguments are given, ``None`` is serialized.
+
+ :param args: A single value to serialize, or multiple values to
+ treat as a list to serialize.
+ :param kwargs: Treat as a dict to serialize.
+ """
+ obj = self._prepare_response_obj(args, kwargs)
+ dump_args: dict[str, t.Any] = {}
+
+ if (self.compact is None and self._app.debug) or self.compact is False:
+ dump_args.setdefault("indent", 2)
+ else:
+ dump_args.setdefault("separators", (",", ":"))
+
+ return self._app.response_class(
+ f"{self.dumps(obj, **dump_args)}\n", mimetype=self.mimetype
+ )
diff --git a/venv/Lib/site-packages/flask/json/tag.py b/venv/Lib/site-packages/flask/json/tag.py
new file mode 100644
index 0000000..8dc3629
--- /dev/null
+++ b/venv/Lib/site-packages/flask/json/tag.py
@@ -0,0 +1,327 @@
+"""
+Tagged JSON
+~~~~~~~~~~~
+
+A compact representation for lossless serialization of non-standard JSON
+types. :class:`~flask.sessions.SecureCookieSessionInterface` uses this
+to serialize the session data, but it may be useful in other places. It
+can be extended to support other types.
+
+.. autoclass:: TaggedJSONSerializer
+ :members:
+
+.. autoclass:: JSONTag
+ :members:
+
+Let's see an example that adds support for
+:class:`~collections.OrderedDict`. Dicts don't have an order in JSON, so
+to handle this we will dump the items as a list of ``[key, value]``
+pairs. Subclass :class:`JSONTag` and give it the new key ``' od'`` to
+identify the type. The session serializer processes dicts first, so
+insert the new tag at the front of the order since ``OrderedDict`` must
+be processed before ``dict``.
+
+.. code-block:: python
+
+ from flask.json.tag import JSONTag
+
+ class TagOrderedDict(JSONTag):
+ __slots__ = ('serializer',)
+ key = ' od'
+
+ def check(self, value):
+ return isinstance(value, OrderedDict)
+
+ def to_json(self, value):
+ return [[k, self.serializer.tag(v)] for k, v in iteritems(value)]
+
+ def to_python(self, value):
+ return OrderedDict(value)
+
+ app.session_interface.serializer.register(TagOrderedDict, index=0)
+"""
+
+from __future__ import annotations
+
+import typing as t
+from base64 import b64decode
+from base64 import b64encode
+from datetime import datetime
+from uuid import UUID
+
+from markupsafe import Markup
+from werkzeug.http import http_date
+from werkzeug.http import parse_date
+
+from ..json import dumps
+from ..json import loads
+
+
+class JSONTag:
+ """Base class for defining type tags for :class:`TaggedJSONSerializer`."""
+
+ __slots__ = ("serializer",)
+
+ #: The tag to mark the serialized object with. If empty, this tag is
+ #: only used as an intermediate step during tagging.
+ key: str = ""
+
+ def __init__(self, serializer: TaggedJSONSerializer) -> None:
+ """Create a tagger for the given serializer."""
+ self.serializer = serializer
+
+ def check(self, value: t.Any) -> bool:
+ """Check if the given value should be tagged by this tag."""
+ raise NotImplementedError
+
+ def to_json(self, value: t.Any) -> t.Any:
+ """Convert the Python object to an object that is a valid JSON type.
+ The tag will be added later."""
+ raise NotImplementedError
+
+ def to_python(self, value: t.Any) -> t.Any:
+ """Convert the JSON representation back to the correct type. The tag
+ will already be removed."""
+ raise NotImplementedError
+
+ def tag(self, value: t.Any) -> dict[str, t.Any]:
+ """Convert the value to a valid JSON type and add the tag structure
+ around it."""
+ return {self.key: self.to_json(value)}
+
+
+class TagDict(JSONTag):
+ """Tag for 1-item dicts whose only key matches a registered tag.
+
+ Internally, the dict key is suffixed with `__`, and the suffix is removed
+ when deserializing.
+ """
+
+ __slots__ = ()
+ key = " di"
+
+ def check(self, value: t.Any) -> bool:
+ return (
+ isinstance(value, dict)
+ and len(value) == 1
+ and next(iter(value)) in self.serializer.tags
+ )
+
+ def to_json(self, value: t.Any) -> t.Any:
+ key = next(iter(value))
+ return {f"{key}__": self.serializer.tag(value[key])}
+
+ def to_python(self, value: t.Any) -> t.Any:
+ key = next(iter(value))
+ return {key[:-2]: value[key]}
+
+
+class PassDict(JSONTag):
+ __slots__ = ()
+
+ def check(self, value: t.Any) -> bool:
+ return isinstance(value, dict)
+
+ def to_json(self, value: t.Any) -> t.Any:
+ # JSON objects may only have string keys, so don't bother tagging the
+ # key here.
+ return {k: self.serializer.tag(v) for k, v in value.items()}
+
+ tag = to_json
+
+
+class TagTuple(JSONTag):
+ __slots__ = ()
+ key = " t"
+
+ def check(self, value: t.Any) -> bool:
+ return isinstance(value, tuple)
+
+ def to_json(self, value: t.Any) -> t.Any:
+ return [self.serializer.tag(item) for item in value]
+
+ def to_python(self, value: t.Any) -> t.Any:
+ return tuple(value)
+
+
+class PassList(JSONTag):
+ __slots__ = ()
+
+ def check(self, value: t.Any) -> bool:
+ return isinstance(value, list)
+
+ def to_json(self, value: t.Any) -> t.Any:
+ return [self.serializer.tag(item) for item in value]
+
+ tag = to_json
+
+
+class TagBytes(JSONTag):
+ __slots__ = ()
+ key = " b"
+
+ def check(self, value: t.Any) -> bool:
+ return isinstance(value, bytes)
+
+ def to_json(self, value: t.Any) -> t.Any:
+ return b64encode(value).decode("ascii")
+
+ def to_python(self, value: t.Any) -> t.Any:
+ return b64decode(value)
+
+
+class TagMarkup(JSONTag):
+ """Serialize anything matching the :class:`~markupsafe.Markup` API by
+ having a ``__html__`` method to the result of that method. Always
+ deserializes to an instance of :class:`~markupsafe.Markup`."""
+
+ __slots__ = ()
+ key = " m"
+
+ def check(self, value: t.Any) -> bool:
+ return callable(getattr(value, "__html__", None))
+
+ def to_json(self, value: t.Any) -> t.Any:
+ return str(value.__html__())
+
+ def to_python(self, value: t.Any) -> t.Any:
+ return Markup(value)
+
+
+class TagUUID(JSONTag):
+ __slots__ = ()
+ key = " u"
+
+ def check(self, value: t.Any) -> bool:
+ return isinstance(value, UUID)
+
+ def to_json(self, value: t.Any) -> t.Any:
+ return value.hex
+
+ def to_python(self, value: t.Any) -> t.Any:
+ return UUID(value)
+
+
+class TagDateTime(JSONTag):
+ __slots__ = ()
+ key = " d"
+
+ def check(self, value: t.Any) -> bool:
+ return isinstance(value, datetime)
+
+ def to_json(self, value: t.Any) -> t.Any:
+ return http_date(value)
+
+ def to_python(self, value: t.Any) -> t.Any:
+ return parse_date(value)
+
+
+class TaggedJSONSerializer:
+ """Serializer that uses a tag system to compactly represent objects that
+ are not JSON types. Passed as the intermediate serializer to
+ :class:`itsdangerous.Serializer`.
+
+ The following extra types are supported:
+
+ * :class:`dict`
+ * :class:`tuple`
+ * :class:`bytes`
+ * :class:`~markupsafe.Markup`
+ * :class:`~uuid.UUID`
+ * :class:`~datetime.datetime`
+ """
+
+ __slots__ = ("tags", "order")
+
+ #: Tag classes to bind when creating the serializer. Other tags can be
+ #: added later using :meth:`~register`.
+ default_tags = [
+ TagDict,
+ PassDict,
+ TagTuple,
+ PassList,
+ TagBytes,
+ TagMarkup,
+ TagUUID,
+ TagDateTime,
+ ]
+
+ def __init__(self) -> None:
+ self.tags: dict[str, JSONTag] = {}
+ self.order: list[JSONTag] = []
+
+ for cls in self.default_tags:
+ self.register(cls)
+
+ def register(
+ self,
+ tag_class: type[JSONTag],
+ force: bool = False,
+ index: int | None = None,
+ ) -> None:
+ """Register a new tag with this serializer.
+
+ :param tag_class: tag class to register. Will be instantiated with this
+ serializer instance.
+ :param force: overwrite an existing tag. If false (default), a
+ :exc:`KeyError` is raised.
+ :param index: index to insert the new tag in the tag order. Useful when
+ the new tag is a special case of an existing tag. If ``None``
+ (default), the tag is appended to the end of the order.
+
+ :raise KeyError: if the tag key is already registered and ``force`` is
+ not true.
+ """
+ tag = tag_class(self)
+ key = tag.key
+
+ if key:
+ if not force and key in self.tags:
+ raise KeyError(f"Tag '{key}' is already registered.")
+
+ self.tags[key] = tag
+
+ if index is None:
+ self.order.append(tag)
+ else:
+ self.order.insert(index, tag)
+
+ def tag(self, value: t.Any) -> t.Any:
+ """Convert a value to a tagged representation if necessary."""
+ for tag in self.order:
+ if tag.check(value):
+ return tag.tag(value)
+
+ return value
+
+ def untag(self, value: dict[str, t.Any]) -> t.Any:
+ """Convert a tagged representation back to the original type."""
+ if len(value) != 1:
+ return value
+
+ key = next(iter(value))
+
+ if key not in self.tags:
+ return value
+
+ return self.tags[key].to_python(value[key])
+
+ def _untag_scan(self, value: t.Any) -> t.Any:
+ if isinstance(value, dict):
+ # untag each item recursively
+ value = {k: self._untag_scan(v) for k, v in value.items()}
+ # untag the dict itself
+ value = self.untag(value)
+ elif isinstance(value, list):
+ # untag each item recursively
+ value = [self._untag_scan(item) for item in value]
+
+ return value
+
+ def dumps(self, value: t.Any) -> str:
+ """Tag the value and dump it to a compact JSON string."""
+ return dumps(self.tag(value), separators=(",", ":"))
+
+ def loads(self, value: str) -> t.Any:
+ """Load data from a JSON string and deserialized any tagged objects."""
+ return self._untag_scan(loads(value))
diff --git a/venv/Lib/site-packages/flask/logging.py b/venv/Lib/site-packages/flask/logging.py
new file mode 100644
index 0000000..0cb8f43
--- /dev/null
+++ b/venv/Lib/site-packages/flask/logging.py
@@ -0,0 +1,79 @@
+from __future__ import annotations
+
+import logging
+import sys
+import typing as t
+
+from werkzeug.local import LocalProxy
+
+from .globals import request
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from .sansio.app import App
+
+
+@LocalProxy
+def wsgi_errors_stream() -> t.TextIO:
+ """Find the most appropriate error stream for the application. If a request
+ is active, log to ``wsgi.errors``, otherwise use ``sys.stderr``.
+
+ If you configure your own :class:`logging.StreamHandler`, you may want to
+ use this for the stream. If you are using file or dict configuration and
+ can't import this directly, you can refer to it as
+ ``ext://flask.logging.wsgi_errors_stream``.
+ """
+ if request:
+ return request.environ["wsgi.errors"] # type: ignore[no-any-return]
+
+ return sys.stderr
+
+
+def has_level_handler(logger: logging.Logger) -> bool:
+ """Check if there is a handler in the logging chain that will handle the
+ given logger's :meth:`effective level <~logging.Logger.getEffectiveLevel>`.
+ """
+ level = logger.getEffectiveLevel()
+ current = logger
+
+ while current:
+ if any(handler.level <= level for handler in current.handlers):
+ return True
+
+ if not current.propagate:
+ break
+
+ current = current.parent # type: ignore
+
+ return False
+
+
+#: Log messages to :func:`~flask.logging.wsgi_errors_stream` with the format
+#: ``[%(asctime)s] %(levelname)s in %(module)s: %(message)s``.
+default_handler = logging.StreamHandler(wsgi_errors_stream) # type: ignore
+default_handler.setFormatter(
+ logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
+)
+
+
+def create_logger(app: App) -> logging.Logger:
+ """Get the Flask app's logger and configure it if needed.
+
+ The logger name will be the same as
+ :attr:`app.import_name `.
+
+ When :attr:`~flask.Flask.debug` is enabled, set the logger level to
+ :data:`logging.DEBUG` if it is not set.
+
+ If there is no handler for the logger's effective level, add a
+ :class:`~logging.StreamHandler` for
+ :func:`~flask.logging.wsgi_errors_stream` with a basic format.
+ """
+ logger = logging.getLogger(app.name)
+
+ if app.debug and not logger.level:
+ logger.setLevel(logging.DEBUG)
+
+ if not has_level_handler(logger):
+ logger.addHandler(default_handler)
+
+ return logger
diff --git a/venv/Lib/site-packages/flask/py.typed b/venv/Lib/site-packages/flask/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/flask/sansio/README.md b/venv/Lib/site-packages/flask/sansio/README.md
new file mode 100644
index 0000000..623ac19
--- /dev/null
+++ b/venv/Lib/site-packages/flask/sansio/README.md
@@ -0,0 +1,6 @@
+# Sansio
+
+This folder contains code that can be used by alternative Flask
+implementations, for example Quart. The code therefore cannot do any
+IO, nor be part of a likely IO path. Finally this code cannot use the
+Flask globals.
diff --git a/venv/Lib/site-packages/flask/sansio/__pycache__/app.cpython-310.pyc b/venv/Lib/site-packages/flask/sansio/__pycache__/app.cpython-310.pyc
new file mode 100644
index 0000000..3377031
Binary files /dev/null and b/venv/Lib/site-packages/flask/sansio/__pycache__/app.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/sansio/__pycache__/blueprints.cpython-310.pyc b/venv/Lib/site-packages/flask/sansio/__pycache__/blueprints.cpython-310.pyc
new file mode 100644
index 0000000..58082c0
Binary files /dev/null and b/venv/Lib/site-packages/flask/sansio/__pycache__/blueprints.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/sansio/__pycache__/scaffold.cpython-310.pyc b/venv/Lib/site-packages/flask/sansio/__pycache__/scaffold.cpython-310.pyc
new file mode 100644
index 0000000..7271c5d
Binary files /dev/null and b/venv/Lib/site-packages/flask/sansio/__pycache__/scaffold.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/flask/sansio/app.py b/venv/Lib/site-packages/flask/sansio/app.py
new file mode 100644
index 0000000..58cb873
--- /dev/null
+++ b/venv/Lib/site-packages/flask/sansio/app.py
@@ -0,0 +1,964 @@
+from __future__ import annotations
+
+import logging
+import os
+import sys
+import typing as t
+from datetime import timedelta
+from itertools import chain
+
+from werkzeug.exceptions import Aborter
+from werkzeug.exceptions import BadRequest
+from werkzeug.exceptions import BadRequestKeyError
+from werkzeug.routing import BuildError
+from werkzeug.routing import Map
+from werkzeug.routing import Rule
+from werkzeug.sansio.response import Response
+from werkzeug.utils import cached_property
+from werkzeug.utils import redirect as _wz_redirect
+
+from .. import typing as ft
+from ..config import Config
+from ..config import ConfigAttribute
+from ..ctx import _AppCtxGlobals
+from ..helpers import _split_blueprint_path
+from ..helpers import get_debug_flag
+from ..json.provider import DefaultJSONProvider
+from ..json.provider import JSONProvider
+from ..logging import create_logger
+from ..templating import DispatchingJinjaLoader
+from ..templating import Environment
+from .scaffold import _endpoint_from_view_func
+from .scaffold import find_package
+from .scaffold import Scaffold
+from .scaffold import setupmethod
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from werkzeug.wrappers import Response as BaseResponse
+
+ from ..testing import FlaskClient
+ from ..testing import FlaskCliRunner
+ from .blueprints import Blueprint
+
+T_shell_context_processor = t.TypeVar(
+ "T_shell_context_processor", bound=ft.ShellContextProcessorCallable
+)
+T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable)
+T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable)
+T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable)
+T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable)
+
+
+def _make_timedelta(value: timedelta | int | None) -> timedelta | None:
+ if value is None or isinstance(value, timedelta):
+ return value
+
+ return timedelta(seconds=value)
+
+
+class App(Scaffold):
+ """The flask object implements a WSGI application and acts as the central
+ object. It is passed the name of the module or package of the
+ application. Once it is created it will act as a central registry for
+ the view functions, the URL rules, template configuration and much more.
+
+ The name of the package is used to resolve resources from inside the
+ package or the folder the module is contained in depending on if the
+ package parameter resolves to an actual python package (a folder with
+ an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file).
+
+ For more information about resource loading, see :func:`open_resource`.
+
+ Usually you create a :class:`Flask` instance in your main module or
+ in the :file:`__init__.py` file of your package like this::
+
+ from flask import Flask
+ app = Flask(__name__)
+
+ .. admonition:: About the First Parameter
+
+ The idea of the first parameter is to give Flask an idea of what
+ belongs to your application. This name is used to find resources
+ on the filesystem, can be used by extensions to improve debugging
+ information and a lot more.
+
+ So it's important what you provide there. If you are using a single
+ module, `__name__` is always the correct value. If you however are
+ using a package, it's usually recommended to hardcode the name of
+ your package there.
+
+ For example if your application is defined in :file:`yourapplication/app.py`
+ you should create it with one of the two versions below::
+
+ app = Flask('yourapplication')
+ app = Flask(__name__.split('.')[0])
+
+ Why is that? The application will work even with `__name__`, thanks
+ to how resources are looked up. However it will make debugging more
+ painful. Certain extensions can make assumptions based on the
+ import name of your application. For example the Flask-SQLAlchemy
+ extension will look for the code in your application that triggered
+ an SQL query in debug mode. If the import name is not properly set
+ up, that debugging information is lost. (For example it would only
+ pick up SQL queries in `yourapplication.app` and not
+ `yourapplication.views.frontend`)
+
+ .. versionadded:: 0.7
+ The `static_url_path`, `static_folder`, and `template_folder`
+ parameters were added.
+
+ .. versionadded:: 0.8
+ The `instance_path` and `instance_relative_config` parameters were
+ added.
+
+ .. versionadded:: 0.11
+ The `root_path` parameter was added.
+
+ .. versionadded:: 1.0
+ The ``host_matching`` and ``static_host`` parameters were added.
+
+ .. versionadded:: 1.0
+ The ``subdomain_matching`` parameter was added. Subdomain
+ matching needs to be enabled manually now. Setting
+ :data:`SERVER_NAME` does not implicitly enable it.
+
+ :param import_name: the name of the application package
+ :param static_url_path: can be used to specify a different path for the
+ static files on the web. Defaults to the name
+ of the `static_folder` folder.
+ :param static_folder: The folder with static files that is served at
+ ``static_url_path``. Relative to the application ``root_path``
+ or an absolute path. Defaults to ``'static'``.
+ :param static_host: the host to use when adding the static route.
+ Defaults to None. Required when using ``host_matching=True``
+ with a ``static_folder`` configured.
+ :param host_matching: set ``url_map.host_matching`` attribute.
+ Defaults to False.
+ :param subdomain_matching: consider the subdomain relative to
+ :data:`SERVER_NAME` when matching routes. Defaults to False.
+ :param template_folder: the folder that contains the templates that should
+ be used by the application. Defaults to
+ ``'templates'`` folder in the root path of the
+ application.
+ :param instance_path: An alternative instance path for the application.
+ By default the folder ``'instance'`` next to the
+ package or module is assumed to be the instance
+ path.
+ :param instance_relative_config: if set to ``True`` relative filenames
+ for loading the config are assumed to
+ be relative to the instance path instead
+ of the application root.
+ :param root_path: The path to the root of the application files.
+ This should only be set manually when it can't be detected
+ automatically, such as for namespace packages.
+ """
+
+ #: The class of the object assigned to :attr:`aborter`, created by
+ #: :meth:`create_aborter`. That object is called by
+ #: :func:`flask.abort` to raise HTTP errors, and can be
+ #: called directly as well.
+ #:
+ #: Defaults to :class:`werkzeug.exceptions.Aborter`.
+ #:
+ #: .. versionadded:: 2.2
+ aborter_class = Aborter
+
+ #: The class that is used for the Jinja environment.
+ #:
+ #: .. versionadded:: 0.11
+ jinja_environment = Environment
+
+ #: The class that is used for the :data:`~flask.g` instance.
+ #:
+ #: Example use cases for a custom class:
+ #:
+ #: 1. Store arbitrary attributes on flask.g.
+ #: 2. Add a property for lazy per-request database connectors.
+ #: 3. Return None instead of AttributeError on unexpected attributes.
+ #: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g.
+ #:
+ #: In Flask 0.9 this property was called `request_globals_class` but it
+ #: was changed in 0.10 to :attr:`app_ctx_globals_class` because the
+ #: flask.g object is now application context scoped.
+ #:
+ #: .. versionadded:: 0.10
+ app_ctx_globals_class = _AppCtxGlobals
+
+ #: The class that is used for the ``config`` attribute of this app.
+ #: Defaults to :class:`~flask.Config`.
+ #:
+ #: Example use cases for a custom class:
+ #:
+ #: 1. Default values for certain config options.
+ #: 2. Access to config values through attributes in addition to keys.
+ #:
+ #: .. versionadded:: 0.11
+ config_class = Config
+
+ #: The testing flag. Set this to ``True`` to enable the test mode of
+ #: Flask extensions (and in the future probably also Flask itself).
+ #: For example this might activate test helpers that have an
+ #: additional runtime cost which should not be enabled by default.
+ #:
+ #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the
+ #: default it's implicitly enabled.
+ #:
+ #: This attribute can also be configured from the config with the
+ #: ``TESTING`` configuration key. Defaults to ``False``.
+ testing = ConfigAttribute[bool]("TESTING")
+
+ #: If a secret key is set, cryptographic components can use this to
+ #: sign cookies and other things. Set this to a complex random value
+ #: when you want to use the secure cookie for instance.
+ #:
+ #: This attribute can also be configured from the config with the
+ #: :data:`SECRET_KEY` configuration key. Defaults to ``None``.
+ secret_key = ConfigAttribute[t.Union[str, bytes, None]]("SECRET_KEY")
+
+ #: A :class:`~datetime.timedelta` which is used to set the expiration
+ #: date of a permanent session. The default is 31 days which makes a
+ #: permanent session survive for roughly one month.
+ #:
+ #: This attribute can also be configured from the config with the
+ #: ``PERMANENT_SESSION_LIFETIME`` configuration key. Defaults to
+ #: ``timedelta(days=31)``
+ permanent_session_lifetime = ConfigAttribute[timedelta](
+ "PERMANENT_SESSION_LIFETIME",
+ get_converter=_make_timedelta, # type: ignore[arg-type]
+ )
+
+ json_provider_class: type[JSONProvider] = DefaultJSONProvider
+ """A subclass of :class:`~flask.json.provider.JSONProvider`. An
+ instance is created and assigned to :attr:`app.json` when creating
+ the app.
+
+ The default, :class:`~flask.json.provider.DefaultJSONProvider`, uses
+ Python's built-in :mod:`json` library. A different provider can use
+ a different JSON library.
+
+ .. versionadded:: 2.2
+ """
+
+ #: Options that are passed to the Jinja environment in
+ #: :meth:`create_jinja_environment`. Changing these options after
+ #: the environment is created (accessing :attr:`jinja_env`) will
+ #: have no effect.
+ #:
+ #: .. versionchanged:: 1.1.0
+ #: This is a ``dict`` instead of an ``ImmutableDict`` to allow
+ #: easier configuration.
+ #:
+ jinja_options: dict[str, t.Any] = {}
+
+ #: The rule object to use for URL rules created. This is used by
+ #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`.
+ #:
+ #: .. versionadded:: 0.7
+ url_rule_class = Rule
+
+ #: The map object to use for storing the URL rules and routing
+ #: configuration parameters. Defaults to :class:`werkzeug.routing.Map`.
+ #:
+ #: .. versionadded:: 1.1.0
+ url_map_class = Map
+
+ #: The :meth:`test_client` method creates an instance of this test
+ #: client class. Defaults to :class:`~flask.testing.FlaskClient`.
+ #:
+ #: .. versionadded:: 0.7
+ test_client_class: type[FlaskClient] | None = None
+
+ #: The :class:`~click.testing.CliRunner` subclass, by default
+ #: :class:`~flask.testing.FlaskCliRunner` that is used by
+ #: :meth:`test_cli_runner`. Its ``__init__`` method should take a
+ #: Flask app object as the first argument.
+ #:
+ #: .. versionadded:: 1.0
+ test_cli_runner_class: type[FlaskCliRunner] | None = None
+
+ default_config: dict[str, t.Any]
+ response_class: type[Response]
+
+ def __init__(
+ self,
+ import_name: str,
+ static_url_path: str | None = None,
+ static_folder: str | os.PathLike[str] | None = "static",
+ static_host: str | None = None,
+ host_matching: bool = False,
+ subdomain_matching: bool = False,
+ template_folder: str | os.PathLike[str] | None = "templates",
+ instance_path: str | None = None,
+ instance_relative_config: bool = False,
+ root_path: str | None = None,
+ ) -> None:
+ super().__init__(
+ import_name=import_name,
+ static_folder=static_folder,
+ static_url_path=static_url_path,
+ template_folder=template_folder,
+ root_path=root_path,
+ )
+
+ if instance_path is None:
+ instance_path = self.auto_find_instance_path()
+ elif not os.path.isabs(instance_path):
+ raise ValueError(
+ "If an instance path is provided it must be absolute."
+ " A relative path was given instead."
+ )
+
+ #: Holds the path to the instance folder.
+ #:
+ #: .. versionadded:: 0.8
+ self.instance_path = instance_path
+
+ #: The configuration dictionary as :class:`Config`. This behaves
+ #: exactly like a regular dictionary but supports additional methods
+ #: to load a config from files.
+ self.config = self.make_config(instance_relative_config)
+
+ #: An instance of :attr:`aborter_class` created by
+ #: :meth:`make_aborter`. This is called by :func:`flask.abort`
+ #: to raise HTTP errors, and can be called directly as well.
+ #:
+ #: .. versionadded:: 2.2
+ #: Moved from ``flask.abort``, which calls this object.
+ self.aborter = self.make_aborter()
+
+ self.json: JSONProvider = self.json_provider_class(self)
+ """Provides access to JSON methods. Functions in ``flask.json``
+ will call methods on this provider when the application context
+ is active. Used for handling JSON requests and responses.
+
+ An instance of :attr:`json_provider_class`. Can be customized by
+ changing that attribute on a subclass, or by assigning to this
+ attribute afterwards.
+
+ The default, :class:`~flask.json.provider.DefaultJSONProvider`,
+ uses Python's built-in :mod:`json` library. A different provider
+ can use a different JSON library.
+
+ .. versionadded:: 2.2
+ """
+
+ #: A list of functions that are called by
+ #: :meth:`handle_url_build_error` when :meth:`.url_for` raises a
+ #: :exc:`~werkzeug.routing.BuildError`. Each function is called
+ #: with ``error``, ``endpoint`` and ``values``. If a function
+ #: returns ``None`` or raises a ``BuildError``, it is skipped.
+ #: Otherwise, its return value is returned by ``url_for``.
+ #:
+ #: .. versionadded:: 0.9
+ self.url_build_error_handlers: list[
+ t.Callable[[Exception, str, dict[str, t.Any]], str]
+ ] = []
+
+ #: A list of functions that are called when the application context
+ #: is destroyed. Since the application context is also torn down
+ #: if the request ends this is the place to store code that disconnects
+ #: from databases.
+ #:
+ #: .. versionadded:: 0.9
+ self.teardown_appcontext_funcs: list[ft.TeardownCallable] = []
+
+ #: A list of shell context processor functions that should be run
+ #: when a shell context is created.
+ #:
+ #: .. versionadded:: 0.11
+ self.shell_context_processors: list[ft.ShellContextProcessorCallable] = []
+
+ #: Maps registered blueprint names to blueprint objects. The
+ #: dict retains the order the blueprints were registered in.
+ #: Blueprints can be registered multiple times, this dict does
+ #: not track how often they were attached.
+ #:
+ #: .. versionadded:: 0.7
+ self.blueprints: dict[str, Blueprint] = {}
+
+ #: a place where extensions can store application specific state. For
+ #: example this is where an extension could store database engines and
+ #: similar things.
+ #:
+ #: The key must match the name of the extension module. For example in
+ #: case of a "Flask-Foo" extension in `flask_foo`, the key would be
+ #: ``'foo'``.
+ #:
+ #: .. versionadded:: 0.7
+ self.extensions: dict[str, t.Any] = {}
+
+ #: The :class:`~werkzeug.routing.Map` for this instance. You can use
+ #: this to change the routing converters after the class was created
+ #: but before any routes are connected. Example::
+ #:
+ #: from werkzeug.routing import BaseConverter
+ #:
+ #: class ListConverter(BaseConverter):
+ #: def to_python(self, value):
+ #: return value.split(',')
+ #: def to_url(self, values):
+ #: return ','.join(super(ListConverter, self).to_url(value)
+ #: for value in values)
+ #:
+ #: app = Flask(__name__)
+ #: app.url_map.converters['list'] = ListConverter
+ self.url_map = self.url_map_class(host_matching=host_matching)
+
+ self.subdomain_matching = subdomain_matching
+
+ # tracks internally if the application already handled at least one
+ # request.
+ self._got_first_request = False
+
+ def _check_setup_finished(self, f_name: str) -> None:
+ if self._got_first_request:
+ raise AssertionError(
+ f"The setup method '{f_name}' can no longer be called"
+ " on the application. It has already handled its first"
+ " request, any changes will not be applied"
+ " consistently.\n"
+ "Make sure all imports, decorators, functions, etc."
+ " needed to set up the application are done before"
+ " running it."
+ )
+
+ @cached_property
+ def name(self) -> str:
+ """The name of the application. This is usually the import name
+ with the difference that it's guessed from the run file if the
+ import name is main. This name is used as a display name when
+ Flask needs the name of the application. It can be set and overridden
+ to change the value.
+
+ .. versionadded:: 0.8
+ """
+ if self.import_name == "__main__":
+ fn: str | None = getattr(sys.modules["__main__"], "__file__", None)
+ if fn is None:
+ return "__main__"
+ return os.path.splitext(os.path.basename(fn))[0]
+ return self.import_name
+
+ @cached_property
+ def logger(self) -> logging.Logger:
+ """A standard Python :class:`~logging.Logger` for the app, with
+ the same name as :attr:`name`.
+
+ In debug mode, the logger's :attr:`~logging.Logger.level` will
+ be set to :data:`~logging.DEBUG`.
+
+ If there are no handlers configured, a default handler will be
+ added. See :doc:`/logging` for more information.
+
+ .. versionchanged:: 1.1.0
+ The logger takes the same name as :attr:`name` rather than
+ hard-coding ``"flask.app"``.
+
+ .. versionchanged:: 1.0.0
+ Behavior was simplified. The logger is always named
+ ``"flask.app"``. The level is only set during configuration,
+ it doesn't check ``app.debug`` each time. Only one format is
+ used, not different ones depending on ``app.debug``. No
+ handlers are removed, and a handler is only added if no
+ handlers are already configured.
+
+ .. versionadded:: 0.3
+ """
+ return create_logger(self)
+
+ @cached_property
+ def jinja_env(self) -> Environment:
+ """The Jinja environment used to load templates.
+
+ The environment is created the first time this property is
+ accessed. Changing :attr:`jinja_options` after that will have no
+ effect.
+ """
+ return self.create_jinja_environment()
+
+ def create_jinja_environment(self) -> Environment:
+ raise NotImplementedError()
+
+ def make_config(self, instance_relative: bool = False) -> Config:
+ """Used to create the config attribute by the Flask constructor.
+ The `instance_relative` parameter is passed in from the constructor
+ of Flask (there named `instance_relative_config`) and indicates if
+ the config should be relative to the instance path or the root path
+ of the application.
+
+ .. versionadded:: 0.8
+ """
+ root_path = self.root_path
+ if instance_relative:
+ root_path = self.instance_path
+ defaults = dict(self.default_config)
+ defaults["DEBUG"] = get_debug_flag()
+ return self.config_class(root_path, defaults)
+
+ def make_aborter(self) -> Aborter:
+ """Create the object to assign to :attr:`aborter`. That object
+ is called by :func:`flask.abort` to raise HTTP errors, and can
+ be called directly as well.
+
+ By default, this creates an instance of :attr:`aborter_class`,
+ which defaults to :class:`werkzeug.exceptions.Aborter`.
+
+ .. versionadded:: 2.2
+ """
+ return self.aborter_class()
+
+ def auto_find_instance_path(self) -> str:
+ """Tries to locate the instance path if it was not provided to the
+ constructor of the application class. It will basically calculate
+ the path to a folder named ``instance`` next to your main file or
+ the package.
+
+ .. versionadded:: 0.8
+ """
+ prefix, package_path = find_package(self.import_name)
+ if prefix is None:
+ return os.path.join(package_path, "instance")
+ return os.path.join(prefix, "var", f"{self.name}-instance")
+
+ def create_global_jinja_loader(self) -> DispatchingJinjaLoader:
+ """Creates the loader for the Jinja environment. Can be used to
+ override just the loader and keeping the rest unchanged. It's
+ discouraged to override this function. Instead one should override
+ the :meth:`jinja_loader` function instead.
+
+ The global loader dispatches between the loaders of the application
+ and the individual blueprints.
+
+ .. versionadded:: 0.7
+ """
+ return DispatchingJinjaLoader(self)
+
+ def select_jinja_autoescape(self, filename: str | None) -> bool:
+ """Returns ``True`` if autoescaping should be active for the given
+ template name. If no template name is given, returns `True`.
+
+ .. versionchanged:: 2.2
+ Autoescaping is now enabled by default for ``.svg`` files.
+
+ .. versionadded:: 0.5
+ """
+ if filename is None:
+ return True
+ return filename.endswith((".html", ".htm", ".xml", ".xhtml", ".svg"))
+
+ @property
+ def debug(self) -> bool:
+ """Whether debug mode is enabled. When using ``flask run`` to start the
+ development server, an interactive debugger will be shown for unhandled
+ exceptions, and the server will be reloaded when code changes. This maps to the
+ :data:`DEBUG` config key. It may not behave as expected if set late.
+
+ **Do not enable debug mode when deploying in production.**
+
+ Default: ``False``
+ """
+ return self.config["DEBUG"] # type: ignore[no-any-return]
+
+ @debug.setter
+ def debug(self, value: bool) -> None:
+ self.config["DEBUG"] = value
+
+ if self.config["TEMPLATES_AUTO_RELOAD"] is None:
+ self.jinja_env.auto_reload = value
+
+ @setupmethod
+ def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None:
+ """Register a :class:`~flask.Blueprint` on the application. Keyword
+ arguments passed to this method will override the defaults set on the
+ blueprint.
+
+ Calls the blueprint's :meth:`~flask.Blueprint.register` method after
+ recording the blueprint in the application's :attr:`blueprints`.
+
+ :param blueprint: The blueprint to register.
+ :param url_prefix: Blueprint routes will be prefixed with this.
+ :param subdomain: Blueprint routes will match on this subdomain.
+ :param url_defaults: Blueprint routes will use these default values for
+ view arguments.
+ :param options: Additional keyword arguments are passed to
+ :class:`~flask.blueprints.BlueprintSetupState`. They can be
+ accessed in :meth:`~flask.Blueprint.record` callbacks.
+
+ .. versionchanged:: 2.0.1
+ The ``name`` option can be used to change the (pre-dotted)
+ name the blueprint is registered with. This allows the same
+ blueprint to be registered multiple times with unique names
+ for ``url_for``.
+
+ .. versionadded:: 0.7
+ """
+ blueprint.register(self, options)
+
+ def iter_blueprints(self) -> t.ValuesView[Blueprint]:
+ """Iterates over all blueprints by the order they were registered.
+
+ .. versionadded:: 0.11
+ """
+ return self.blueprints.values()
+
+ @setupmethod
+ def add_url_rule(
+ self,
+ rule: str,
+ endpoint: str | None = None,
+ view_func: ft.RouteCallable | None = None,
+ provide_automatic_options: bool | None = None,
+ **options: t.Any,
+ ) -> None:
+ if endpoint is None:
+ endpoint = _endpoint_from_view_func(view_func) # type: ignore
+ options["endpoint"] = endpoint
+ methods = options.pop("methods", None)
+
+ # if the methods are not given and the view_func object knows its
+ # methods we can use that instead. If neither exists, we go with
+ # a tuple of only ``GET`` as default.
+ if methods is None:
+ methods = getattr(view_func, "methods", None) or ("GET",)
+ if isinstance(methods, str):
+ raise TypeError(
+ "Allowed methods must be a list of strings, for"
+ ' example: @app.route(..., methods=["POST"])'
+ )
+ methods = {item.upper() for item in methods}
+
+ # Methods that should always be added
+ required_methods: set[str] = set(getattr(view_func, "required_methods", ()))
+
+ # starting with Flask 0.8 the view_func object can disable and
+ # force-enable the automatic options handling.
+ if provide_automatic_options is None:
+ provide_automatic_options = getattr(
+ view_func, "provide_automatic_options", None
+ )
+
+ if provide_automatic_options is None:
+ if "OPTIONS" not in methods and self.config["PROVIDE_AUTOMATIC_OPTIONS"]:
+ provide_automatic_options = True
+ required_methods.add("OPTIONS")
+ else:
+ provide_automatic_options = False
+
+ # Add the required methods now.
+ methods |= required_methods
+
+ rule_obj = self.url_rule_class(rule, methods=methods, **options)
+ rule_obj.provide_automatic_options = provide_automatic_options # type: ignore[attr-defined]
+
+ self.url_map.add(rule_obj)
+ if view_func is not None:
+ old_func = self.view_functions.get(endpoint)
+ if old_func is not None and old_func != view_func:
+ raise AssertionError(
+ "View function mapping is overwriting an existing"
+ f" endpoint function: {endpoint}"
+ )
+ self.view_functions[endpoint] = view_func
+
+ @setupmethod
+ def template_filter(
+ self, name: str | None = None
+ ) -> t.Callable[[T_template_filter], T_template_filter]:
+ """A decorator that is used to register custom template filter.
+ You can specify a name for the filter, otherwise the function
+ name will be used. Example::
+
+ @app.template_filter()
+ def reverse(s):
+ return s[::-1]
+
+ :param name: the optional name of the filter, otherwise the
+ function name will be used.
+ """
+
+ def decorator(f: T_template_filter) -> T_template_filter:
+ self.add_template_filter(f, name=name)
+ return f
+
+ return decorator
+
+ @setupmethod
+ def add_template_filter(
+ self, f: ft.TemplateFilterCallable, name: str | None = None
+ ) -> None:
+ """Register a custom template filter. Works exactly like the
+ :meth:`template_filter` decorator.
+
+ :param name: the optional name of the filter, otherwise the
+ function name will be used.
+ """
+ self.jinja_env.filters[name or f.__name__] = f
+
+ @setupmethod
+ def template_test(
+ self, name: str | None = None
+ ) -> t.Callable[[T_template_test], T_template_test]:
+ """A decorator that is used to register custom template test.
+ You can specify a name for the test, otherwise the function
+ name will be used. Example::
+
+ @app.template_test()
+ def is_prime(n):
+ if n == 2:
+ return True
+ for i in range(2, int(math.ceil(math.sqrt(n))) + 1):
+ if n % i == 0:
+ return False
+ return True
+
+ .. versionadded:: 0.10
+
+ :param name: the optional name of the test, otherwise the
+ function name will be used.
+ """
+
+ def decorator(f: T_template_test) -> T_template_test:
+ self.add_template_test(f, name=name)
+ return f
+
+ return decorator
+
+ @setupmethod
+ def add_template_test(
+ self, f: ft.TemplateTestCallable, name: str | None = None
+ ) -> None:
+ """Register a custom template test. Works exactly like the
+ :meth:`template_test` decorator.
+
+ .. versionadded:: 0.10
+
+ :param name: the optional name of the test, otherwise the
+ function name will be used.
+ """
+ self.jinja_env.tests[name or f.__name__] = f
+
+ @setupmethod
+ def template_global(
+ self, name: str | None = None
+ ) -> t.Callable[[T_template_global], T_template_global]:
+ """A decorator that is used to register a custom template global function.
+ You can specify a name for the global function, otherwise the function
+ name will be used. Example::
+
+ @app.template_global()
+ def double(n):
+ return 2 * n
+
+ .. versionadded:: 0.10
+
+ :param name: the optional name of the global function, otherwise the
+ function name will be used.
+ """
+
+ def decorator(f: T_template_global) -> T_template_global:
+ self.add_template_global(f, name=name)
+ return f
+
+ return decorator
+
+ @setupmethod
+ def add_template_global(
+ self, f: ft.TemplateGlobalCallable, name: str | None = None
+ ) -> None:
+ """Register a custom template global function. Works exactly like the
+ :meth:`template_global` decorator.
+
+ .. versionadded:: 0.10
+
+ :param name: the optional name of the global function, otherwise the
+ function name will be used.
+ """
+ self.jinja_env.globals[name or f.__name__] = f
+
+ @setupmethod
+ def teardown_appcontext(self, f: T_teardown) -> T_teardown:
+ """Registers a function to be called when the application
+ context is popped. The application context is typically popped
+ after the request context for each request, at the end of CLI
+ commands, or after a manually pushed context ends.
+
+ .. code-block:: python
+
+ with app.app_context():
+ ...
+
+ When the ``with`` block exits (or ``ctx.pop()`` is called), the
+ teardown functions are called just before the app context is
+ made inactive. Since a request context typically also manages an
+ application context it would also be called when you pop a
+ request context.
+
+ When a teardown function was called because of an unhandled
+ exception it will be passed an error object. If an
+ :meth:`errorhandler` is registered, it will handle the exception
+ and the teardown will not receive it.
+
+ Teardown functions must avoid raising exceptions. If they
+ execute code that might fail they must surround that code with a
+ ``try``/``except`` block and log any errors.
+
+ The return values of teardown functions are ignored.
+
+ .. versionadded:: 0.9
+ """
+ self.teardown_appcontext_funcs.append(f)
+ return f
+
+ @setupmethod
+ def shell_context_processor(
+ self, f: T_shell_context_processor
+ ) -> T_shell_context_processor:
+ """Registers a shell context processor function.
+
+ .. versionadded:: 0.11
+ """
+ self.shell_context_processors.append(f)
+ return f
+
+ def _find_error_handler(
+ self, e: Exception, blueprints: list[str]
+ ) -> ft.ErrorHandlerCallable | None:
+ """Return a registered error handler for an exception in this order:
+ blueprint handler for a specific code, app handler for a specific code,
+ blueprint handler for an exception class, app handler for an exception
+ class, or ``None`` if a suitable handler is not found.
+ """
+ exc_class, code = self._get_exc_class_and_code(type(e))
+ names = (*blueprints, None)
+
+ for c in (code, None) if code is not None else (None,):
+ for name in names:
+ handler_map = self.error_handler_spec[name][c]
+
+ if not handler_map:
+ continue
+
+ for cls in exc_class.__mro__:
+ handler = handler_map.get(cls)
+
+ if handler is not None:
+ return handler
+ return None
+
+ def trap_http_exception(self, e: Exception) -> bool:
+ """Checks if an HTTP exception should be trapped or not. By default
+ this will return ``False`` for all exceptions except for a bad request
+ key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to ``True``. It
+ also returns ``True`` if ``TRAP_HTTP_EXCEPTIONS`` is set to ``True``.
+
+ This is called for all HTTP exceptions raised by a view function.
+ If it returns ``True`` for any exception the error handler for this
+ exception is not called and it shows up as regular exception in the
+ traceback. This is helpful for debugging implicitly raised HTTP
+ exceptions.
+
+ .. versionchanged:: 1.0
+ Bad request errors are not trapped by default in debug mode.
+
+ .. versionadded:: 0.8
+ """
+ if self.config["TRAP_HTTP_EXCEPTIONS"]:
+ return True
+
+ trap_bad_request = self.config["TRAP_BAD_REQUEST_ERRORS"]
+
+ # if unset, trap key errors in debug mode
+ if (
+ trap_bad_request is None
+ and self.debug
+ and isinstance(e, BadRequestKeyError)
+ ):
+ return True
+
+ if trap_bad_request:
+ return isinstance(e, BadRequest)
+
+ return False
+
+ def should_ignore_error(self, error: BaseException | None) -> bool:
+ """This is called to figure out if an error should be ignored
+ or not as far as the teardown system is concerned. If this
+ function returns ``True`` then the teardown handlers will not be
+ passed the error.
+
+ .. versionadded:: 0.10
+ """
+ return False
+
+ def redirect(self, location: str, code: int = 302) -> BaseResponse:
+ """Create a redirect response object.
+
+ This is called by :func:`flask.redirect`, and can be called
+ directly as well.
+
+ :param location: The URL to redirect to.
+ :param code: The status code for the redirect.
+
+ .. versionadded:: 2.2
+ Moved from ``flask.redirect``, which calls this method.
+ """
+ return _wz_redirect(
+ location,
+ code=code,
+ Response=self.response_class, # type: ignore[arg-type]
+ )
+
+ def inject_url_defaults(self, endpoint: str, values: dict[str, t.Any]) -> None:
+ """Injects the URL defaults for the given endpoint directly into
+ the values dictionary passed. This is used internally and
+ automatically called on URL building.
+
+ .. versionadded:: 0.7
+ """
+ names: t.Iterable[str | None] = (None,)
+
+ # url_for may be called outside a request context, parse the
+ # passed endpoint instead of using request.blueprints.
+ if "." in endpoint:
+ names = chain(
+ names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0]))
+ )
+
+ for name in names:
+ if name in self.url_default_functions:
+ for func in self.url_default_functions[name]:
+ func(endpoint, values)
+
+ def handle_url_build_error(
+ self, error: BuildError, endpoint: str, values: dict[str, t.Any]
+ ) -> str:
+ """Called by :meth:`.url_for` if a
+ :exc:`~werkzeug.routing.BuildError` was raised. If this returns
+ a value, it will be returned by ``url_for``, otherwise the error
+ will be re-raised.
+
+ Each function in :attr:`url_build_error_handlers` is called with
+ ``error``, ``endpoint`` and ``values``. If a function returns
+ ``None`` or raises a ``BuildError``, it is skipped. Otherwise,
+ its return value is returned by ``url_for``.
+
+ :param error: The active ``BuildError`` being handled.
+ :param endpoint: The endpoint being built.
+ :param values: The keyword arguments passed to ``url_for``.
+ """
+ for handler in self.url_build_error_handlers:
+ try:
+ rv = handler(error, endpoint, values)
+ except BuildError as e:
+ # make error available outside except block
+ error = e
+ else:
+ if rv is not None:
+ return rv
+
+ # Re-raise if called with an active exception, otherwise raise
+ # the passed in exception.
+ if error is sys.exc_info()[1]:
+ raise
+
+ raise error
diff --git a/venv/Lib/site-packages/flask/sansio/blueprints.py b/venv/Lib/site-packages/flask/sansio/blueprints.py
new file mode 100644
index 0000000..4f912cc
--- /dev/null
+++ b/venv/Lib/site-packages/flask/sansio/blueprints.py
@@ -0,0 +1,632 @@
+from __future__ import annotations
+
+import os
+import typing as t
+from collections import defaultdict
+from functools import update_wrapper
+
+from .. import typing as ft
+from .scaffold import _endpoint_from_view_func
+from .scaffold import _sentinel
+from .scaffold import Scaffold
+from .scaffold import setupmethod
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from .app import App
+
+DeferredSetupFunction = t.Callable[["BlueprintSetupState"], None]
+T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any])
+T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable)
+T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable)
+T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable)
+T_template_context_processor = t.TypeVar(
+ "T_template_context_processor", bound=ft.TemplateContextProcessorCallable
+)
+T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable)
+T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable)
+T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable)
+T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable)
+T_url_value_preprocessor = t.TypeVar(
+ "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable
+)
+
+
+class BlueprintSetupState:
+ """Temporary holder object for registering a blueprint with the
+ application. An instance of this class is created by the
+ :meth:`~flask.Blueprint.make_setup_state` method and later passed
+ to all register callback functions.
+ """
+
+ def __init__(
+ self,
+ blueprint: Blueprint,
+ app: App,
+ options: t.Any,
+ first_registration: bool,
+ ) -> None:
+ #: a reference to the current application
+ self.app = app
+
+ #: a reference to the blueprint that created this setup state.
+ self.blueprint = blueprint
+
+ #: a dictionary with all options that were passed to the
+ #: :meth:`~flask.Flask.register_blueprint` method.
+ self.options = options
+
+ #: as blueprints can be registered multiple times with the
+ #: application and not everything wants to be registered
+ #: multiple times on it, this attribute can be used to figure
+ #: out if the blueprint was registered in the past already.
+ self.first_registration = first_registration
+
+ subdomain = self.options.get("subdomain")
+ if subdomain is None:
+ subdomain = self.blueprint.subdomain
+
+ #: The subdomain that the blueprint should be active for, ``None``
+ #: otherwise.
+ self.subdomain = subdomain
+
+ url_prefix = self.options.get("url_prefix")
+ if url_prefix is None:
+ url_prefix = self.blueprint.url_prefix
+ #: The prefix that should be used for all URLs defined on the
+ #: blueprint.
+ self.url_prefix = url_prefix
+
+ self.name = self.options.get("name", blueprint.name)
+ self.name_prefix = self.options.get("name_prefix", "")
+
+ #: A dictionary with URL defaults that is added to each and every
+ #: URL that was defined with the blueprint.
+ self.url_defaults = dict(self.blueprint.url_values_defaults)
+ self.url_defaults.update(self.options.get("url_defaults", ()))
+
+ def add_url_rule(
+ self,
+ rule: str,
+ endpoint: str | None = None,
+ view_func: ft.RouteCallable | None = None,
+ **options: t.Any,
+ ) -> None:
+ """A helper method to register a rule (and optionally a view function)
+ to the application. The endpoint is automatically prefixed with the
+ blueprint's name.
+ """
+ if self.url_prefix is not None:
+ if rule:
+ rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/")))
+ else:
+ rule = self.url_prefix
+ options.setdefault("subdomain", self.subdomain)
+ if endpoint is None:
+ endpoint = _endpoint_from_view_func(view_func) # type: ignore
+ defaults = self.url_defaults
+ if "defaults" in options:
+ defaults = dict(defaults, **options.pop("defaults"))
+
+ self.app.add_url_rule(
+ rule,
+ f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."),
+ view_func,
+ defaults=defaults,
+ **options,
+ )
+
+
+class Blueprint(Scaffold):
+ """Represents a blueprint, a collection of routes and other
+ app-related functions that can be registered on a real application
+ later.
+
+ A blueprint is an object that allows defining application functions
+ without requiring an application object ahead of time. It uses the
+ same decorators as :class:`~flask.Flask`, but defers the need for an
+ application by recording them for later registration.
+
+ Decorating a function with a blueprint creates a deferred function
+ that is called with :class:`~flask.blueprints.BlueprintSetupState`
+ when the blueprint is registered on an application.
+
+ See :doc:`/blueprints` for more information.
+
+ :param name: The name of the blueprint. Will be prepended to each
+ endpoint name.
+ :param import_name: The name of the blueprint package, usually
+ ``__name__``. This helps locate the ``root_path`` for the
+ blueprint.
+ :param static_folder: A folder with static files that should be
+ served by the blueprint's static route. The path is relative to
+ the blueprint's root path. Blueprint static files are disabled
+ by default.
+ :param static_url_path: The url to serve static files from.
+ Defaults to ``static_folder``. If the blueprint does not have
+ a ``url_prefix``, the app's static route will take precedence,
+ and the blueprint's static files won't be accessible.
+ :param template_folder: A folder with templates that should be added
+ to the app's template search path. The path is relative to the
+ blueprint's root path. Blueprint templates are disabled by
+ default. Blueprint templates have a lower precedence than those
+ in the app's templates folder.
+ :param url_prefix: A path to prepend to all of the blueprint's URLs,
+ to make them distinct from the rest of the app's routes.
+ :param subdomain: A subdomain that blueprint routes will match on by
+ default.
+ :param url_defaults: A dict of default values that blueprint routes
+ will receive by default.
+ :param root_path: By default, the blueprint will automatically set
+ this based on ``import_name``. In certain situations this
+ automatic detection can fail, so the path can be specified
+ manually instead.
+
+ .. versionchanged:: 1.1.0
+ Blueprints have a ``cli`` group to register nested CLI commands.
+ The ``cli_group`` parameter controls the name of the group under
+ the ``flask`` command.
+
+ .. versionadded:: 0.7
+ """
+
+ _got_registered_once = False
+
+ def __init__(
+ self,
+ name: str,
+ import_name: str,
+ static_folder: str | os.PathLike[str] | None = None,
+ static_url_path: str | None = None,
+ template_folder: str | os.PathLike[str] | None = None,
+ url_prefix: str | None = None,
+ subdomain: str | None = None,
+ url_defaults: dict[str, t.Any] | None = None,
+ root_path: str | None = None,
+ cli_group: str | None = _sentinel, # type: ignore[assignment]
+ ):
+ super().__init__(
+ import_name=import_name,
+ static_folder=static_folder,
+ static_url_path=static_url_path,
+ template_folder=template_folder,
+ root_path=root_path,
+ )
+
+ if not name:
+ raise ValueError("'name' may not be empty.")
+
+ if "." in name:
+ raise ValueError("'name' may not contain a dot '.' character.")
+
+ self.name = name
+ self.url_prefix = url_prefix
+ self.subdomain = subdomain
+ self.deferred_functions: list[DeferredSetupFunction] = []
+
+ if url_defaults is None:
+ url_defaults = {}
+
+ self.url_values_defaults = url_defaults
+ self.cli_group = cli_group
+ self._blueprints: list[tuple[Blueprint, dict[str, t.Any]]] = []
+
+ def _check_setup_finished(self, f_name: str) -> None:
+ if self._got_registered_once:
+ raise AssertionError(
+ f"The setup method '{f_name}' can no longer be called on the blueprint"
+ f" '{self.name}'. It has already been registered at least once, any"
+ " changes will not be applied consistently.\n"
+ "Make sure all imports, decorators, functions, etc. needed to set up"
+ " the blueprint are done before registering it."
+ )
+
+ @setupmethod
+ def record(self, func: DeferredSetupFunction) -> None:
+ """Registers a function that is called when the blueprint is
+ registered on the application. This function is called with the
+ state as argument as returned by the :meth:`make_setup_state`
+ method.
+ """
+ self.deferred_functions.append(func)
+
+ @setupmethod
+ def record_once(self, func: DeferredSetupFunction) -> None:
+ """Works like :meth:`record` but wraps the function in another
+ function that will ensure the function is only called once. If the
+ blueprint is registered a second time on the application, the
+ function passed is not called.
+ """
+
+ def wrapper(state: BlueprintSetupState) -> None:
+ if state.first_registration:
+ func(state)
+
+ self.record(update_wrapper(wrapper, func))
+
+ def make_setup_state(
+ self, app: App, options: dict[str, t.Any], first_registration: bool = False
+ ) -> BlueprintSetupState:
+ """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState`
+ object that is later passed to the register callback functions.
+ Subclasses can override this to return a subclass of the setup state.
+ """
+ return BlueprintSetupState(self, app, options, first_registration)
+
+ @setupmethod
+ def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None:
+ """Register a :class:`~flask.Blueprint` on this blueprint. Keyword
+ arguments passed to this method will override the defaults set
+ on the blueprint.
+
+ .. versionchanged:: 2.0.1
+ The ``name`` option can be used to change the (pre-dotted)
+ name the blueprint is registered with. This allows the same
+ blueprint to be registered multiple times with unique names
+ for ``url_for``.
+
+ .. versionadded:: 2.0
+ """
+ if blueprint is self:
+ raise ValueError("Cannot register a blueprint on itself")
+ self._blueprints.append((blueprint, options))
+
+ def register(self, app: App, options: dict[str, t.Any]) -> None:
+ """Called by :meth:`Flask.register_blueprint` to register all
+ views and callbacks registered on the blueprint with the
+ application. Creates a :class:`.BlueprintSetupState` and calls
+ each :meth:`record` callback with it.
+
+ :param app: The application this blueprint is being registered
+ with.
+ :param options: Keyword arguments forwarded from
+ :meth:`~Flask.register_blueprint`.
+
+ .. versionchanged:: 2.3
+ Nested blueprints now correctly apply subdomains.
+
+ .. versionchanged:: 2.1
+ Registering the same blueprint with the same name multiple
+ times is an error.
+
+ .. versionchanged:: 2.0.1
+ Nested blueprints are registered with their dotted name.
+ This allows different blueprints with the same name to be
+ nested at different locations.
+
+ .. versionchanged:: 2.0.1
+ The ``name`` option can be used to change the (pre-dotted)
+ name the blueprint is registered with. This allows the same
+ blueprint to be registered multiple times with unique names
+ for ``url_for``.
+ """
+ name_prefix = options.get("name_prefix", "")
+ self_name = options.get("name", self.name)
+ name = f"{name_prefix}.{self_name}".lstrip(".")
+
+ if name in app.blueprints:
+ bp_desc = "this" if app.blueprints[name] is self else "a different"
+ existing_at = f" '{name}'" if self_name != name else ""
+
+ raise ValueError(
+ f"The name '{self_name}' is already registered for"
+ f" {bp_desc} blueprint{existing_at}. Use 'name=' to"
+ f" provide a unique name."
+ )
+
+ first_bp_registration = not any(bp is self for bp in app.blueprints.values())
+ first_name_registration = name not in app.blueprints
+
+ app.blueprints[name] = self
+ self._got_registered_once = True
+ state = self.make_setup_state(app, options, first_bp_registration)
+
+ if self.has_static_folder:
+ state.add_url_rule(
+ f"{self.static_url_path}/",
+ view_func=self.send_static_file, # type: ignore[attr-defined]
+ endpoint="static",
+ )
+
+ # Merge blueprint data into parent.
+ if first_bp_registration or first_name_registration:
+ self._merge_blueprint_funcs(app, name)
+
+ for deferred in self.deferred_functions:
+ deferred(state)
+
+ cli_resolved_group = options.get("cli_group", self.cli_group)
+
+ if self.cli.commands:
+ if cli_resolved_group is None:
+ app.cli.commands.update(self.cli.commands)
+ elif cli_resolved_group is _sentinel:
+ self.cli.name = name
+ app.cli.add_command(self.cli)
+ else:
+ self.cli.name = cli_resolved_group
+ app.cli.add_command(self.cli)
+
+ for blueprint, bp_options in self._blueprints:
+ bp_options = bp_options.copy()
+ bp_url_prefix = bp_options.get("url_prefix")
+ bp_subdomain = bp_options.get("subdomain")
+
+ if bp_subdomain is None:
+ bp_subdomain = blueprint.subdomain
+
+ if state.subdomain is not None and bp_subdomain is not None:
+ bp_options["subdomain"] = bp_subdomain + "." + state.subdomain
+ elif bp_subdomain is not None:
+ bp_options["subdomain"] = bp_subdomain
+ elif state.subdomain is not None:
+ bp_options["subdomain"] = state.subdomain
+
+ if bp_url_prefix is None:
+ bp_url_prefix = blueprint.url_prefix
+
+ if state.url_prefix is not None and bp_url_prefix is not None:
+ bp_options["url_prefix"] = (
+ state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/")
+ )
+ elif bp_url_prefix is not None:
+ bp_options["url_prefix"] = bp_url_prefix
+ elif state.url_prefix is not None:
+ bp_options["url_prefix"] = state.url_prefix
+
+ bp_options["name_prefix"] = name
+ blueprint.register(app, bp_options)
+
+ def _merge_blueprint_funcs(self, app: App, name: str) -> None:
+ def extend(
+ bp_dict: dict[ft.AppOrBlueprintKey, list[t.Any]],
+ parent_dict: dict[ft.AppOrBlueprintKey, list[t.Any]],
+ ) -> None:
+ for key, values in bp_dict.items():
+ key = name if key is None else f"{name}.{key}"
+ parent_dict[key].extend(values)
+
+ for key, value in self.error_handler_spec.items():
+ key = name if key is None else f"{name}.{key}"
+ value = defaultdict(
+ dict,
+ {
+ code: {exc_class: func for exc_class, func in code_values.items()}
+ for code, code_values in value.items()
+ },
+ )
+ app.error_handler_spec[key] = value
+
+ for endpoint, func in self.view_functions.items():
+ app.view_functions[endpoint] = func
+
+ extend(self.before_request_funcs, app.before_request_funcs)
+ extend(self.after_request_funcs, app.after_request_funcs)
+ extend(
+ self.teardown_request_funcs,
+ app.teardown_request_funcs,
+ )
+ extend(self.url_default_functions, app.url_default_functions)
+ extend(self.url_value_preprocessors, app.url_value_preprocessors)
+ extend(self.template_context_processors, app.template_context_processors)
+
+ @setupmethod
+ def add_url_rule(
+ self,
+ rule: str,
+ endpoint: str | None = None,
+ view_func: ft.RouteCallable | None = None,
+ provide_automatic_options: bool | None = None,
+ **options: t.Any,
+ ) -> None:
+ """Register a URL rule with the blueprint. See :meth:`.Flask.add_url_rule` for
+ full documentation.
+
+ The URL rule is prefixed with the blueprint's URL prefix. The endpoint name,
+ used with :func:`url_for`, is prefixed with the blueprint's name.
+ """
+ if endpoint and "." in endpoint:
+ raise ValueError("'endpoint' may not contain a dot '.' character.")
+
+ if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__:
+ raise ValueError("'view_func' name may not contain a dot '.' character.")
+
+ self.record(
+ lambda s: s.add_url_rule(
+ rule,
+ endpoint,
+ view_func,
+ provide_automatic_options=provide_automatic_options,
+ **options,
+ )
+ )
+
+ @setupmethod
+ def app_template_filter(
+ self, name: str | None = None
+ ) -> t.Callable[[T_template_filter], T_template_filter]:
+ """Register a template filter, available in any template rendered by the
+ application. Equivalent to :meth:`.Flask.template_filter`.
+
+ :param name: the optional name of the filter, otherwise the
+ function name will be used.
+ """
+
+ def decorator(f: T_template_filter) -> T_template_filter:
+ self.add_app_template_filter(f, name=name)
+ return f
+
+ return decorator
+
+ @setupmethod
+ def add_app_template_filter(
+ self, f: ft.TemplateFilterCallable, name: str | None = None
+ ) -> None:
+ """Register a template filter, available in any template rendered by the
+ application. Works like the :meth:`app_template_filter` decorator. Equivalent to
+ :meth:`.Flask.add_template_filter`.
+
+ :param name: the optional name of the filter, otherwise the
+ function name will be used.
+ """
+
+ def register_template(state: BlueprintSetupState) -> None:
+ state.app.jinja_env.filters[name or f.__name__] = f
+
+ self.record_once(register_template)
+
+ @setupmethod
+ def app_template_test(
+ self, name: str | None = None
+ ) -> t.Callable[[T_template_test], T_template_test]:
+ """Register a template test, available in any template rendered by the
+ application. Equivalent to :meth:`.Flask.template_test`.
+
+ .. versionadded:: 0.10
+
+ :param name: the optional name of the test, otherwise the
+ function name will be used.
+ """
+
+ def decorator(f: T_template_test) -> T_template_test:
+ self.add_app_template_test(f, name=name)
+ return f
+
+ return decorator
+
+ @setupmethod
+ def add_app_template_test(
+ self, f: ft.TemplateTestCallable, name: str | None = None
+ ) -> None:
+ """Register a template test, available in any template rendered by the
+ application. Works like the :meth:`app_template_test` decorator. Equivalent to
+ :meth:`.Flask.add_template_test`.
+
+ .. versionadded:: 0.10
+
+ :param name: the optional name of the test, otherwise the
+ function name will be used.
+ """
+
+ def register_template(state: BlueprintSetupState) -> None:
+ state.app.jinja_env.tests[name or f.__name__] = f
+
+ self.record_once(register_template)
+
+ @setupmethod
+ def app_template_global(
+ self, name: str | None = None
+ ) -> t.Callable[[T_template_global], T_template_global]:
+ """Register a template global, available in any template rendered by the
+ application. Equivalent to :meth:`.Flask.template_global`.
+
+ .. versionadded:: 0.10
+
+ :param name: the optional name of the global, otherwise the
+ function name will be used.
+ """
+
+ def decorator(f: T_template_global) -> T_template_global:
+ self.add_app_template_global(f, name=name)
+ return f
+
+ return decorator
+
+ @setupmethod
+ def add_app_template_global(
+ self, f: ft.TemplateGlobalCallable, name: str | None = None
+ ) -> None:
+ """Register a template global, available in any template rendered by the
+ application. Works like the :meth:`app_template_global` decorator. Equivalent to
+ :meth:`.Flask.add_template_global`.
+
+ .. versionadded:: 0.10
+
+ :param name: the optional name of the global, otherwise the
+ function name will be used.
+ """
+
+ def register_template(state: BlueprintSetupState) -> None:
+ state.app.jinja_env.globals[name or f.__name__] = f
+
+ self.record_once(register_template)
+
+ @setupmethod
+ def before_app_request(self, f: T_before_request) -> T_before_request:
+ """Like :meth:`before_request`, but before every request, not only those handled
+ by the blueprint. Equivalent to :meth:`.Flask.before_request`.
+ """
+ self.record_once(
+ lambda s: s.app.before_request_funcs.setdefault(None, []).append(f)
+ )
+ return f
+
+ @setupmethod
+ def after_app_request(self, f: T_after_request) -> T_after_request:
+ """Like :meth:`after_request`, but after every request, not only those handled
+ by the blueprint. Equivalent to :meth:`.Flask.after_request`.
+ """
+ self.record_once(
+ lambda s: s.app.after_request_funcs.setdefault(None, []).append(f)
+ )
+ return f
+
+ @setupmethod
+ def teardown_app_request(self, f: T_teardown) -> T_teardown:
+ """Like :meth:`teardown_request`, but after every request, not only those
+ handled by the blueprint. Equivalent to :meth:`.Flask.teardown_request`.
+ """
+ self.record_once(
+ lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f)
+ )
+ return f
+
+ @setupmethod
+ def app_context_processor(
+ self, f: T_template_context_processor
+ ) -> T_template_context_processor:
+ """Like :meth:`context_processor`, but for templates rendered by every view, not
+ only by the blueprint. Equivalent to :meth:`.Flask.context_processor`.
+ """
+ self.record_once(
+ lambda s: s.app.template_context_processors.setdefault(None, []).append(f)
+ )
+ return f
+
+ @setupmethod
+ def app_errorhandler(
+ self, code: type[Exception] | int
+ ) -> t.Callable[[T_error_handler], T_error_handler]:
+ """Like :meth:`errorhandler`, but for every request, not only those handled by
+ the blueprint. Equivalent to :meth:`.Flask.errorhandler`.
+ """
+
+ def decorator(f: T_error_handler) -> T_error_handler:
+ def from_blueprint(state: BlueprintSetupState) -> None:
+ state.app.errorhandler(code)(f)
+
+ self.record_once(from_blueprint)
+ return f
+
+ return decorator
+
+ @setupmethod
+ def app_url_value_preprocessor(
+ self, f: T_url_value_preprocessor
+ ) -> T_url_value_preprocessor:
+ """Like :meth:`url_value_preprocessor`, but for every request, not only those
+ handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`.
+ """
+ self.record_once(
+ lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f)
+ )
+ return f
+
+ @setupmethod
+ def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults:
+ """Like :meth:`url_defaults`, but for every request, not only those handled by
+ the blueprint. Equivalent to :meth:`.Flask.url_defaults`.
+ """
+ self.record_once(
+ lambda s: s.app.url_default_functions.setdefault(None, []).append(f)
+ )
+ return f
diff --git a/venv/Lib/site-packages/flask/sansio/scaffold.py b/venv/Lib/site-packages/flask/sansio/scaffold.py
new file mode 100644
index 0000000..0e96f15
--- /dev/null
+++ b/venv/Lib/site-packages/flask/sansio/scaffold.py
@@ -0,0 +1,792 @@
+from __future__ import annotations
+
+import importlib.util
+import os
+import pathlib
+import sys
+import typing as t
+from collections import defaultdict
+from functools import update_wrapper
+
+from jinja2 import BaseLoader
+from jinja2 import FileSystemLoader
+from werkzeug.exceptions import default_exceptions
+from werkzeug.exceptions import HTTPException
+from werkzeug.utils import cached_property
+
+from .. import typing as ft
+from ..helpers import get_root_path
+from ..templating import _default_template_ctx_processor
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from click import Group
+
+# a singleton sentinel value for parameter defaults
+_sentinel = object()
+
+F = t.TypeVar("F", bound=t.Callable[..., t.Any])
+T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any])
+T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable)
+T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable)
+T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable)
+T_template_context_processor = t.TypeVar(
+ "T_template_context_processor", bound=ft.TemplateContextProcessorCallable
+)
+T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable)
+T_url_value_preprocessor = t.TypeVar(
+ "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable
+)
+T_route = t.TypeVar("T_route", bound=ft.RouteCallable)
+
+
+def setupmethod(f: F) -> F:
+ f_name = f.__name__
+
+ def wrapper_func(self: Scaffold, *args: t.Any, **kwargs: t.Any) -> t.Any:
+ self._check_setup_finished(f_name)
+ return f(self, *args, **kwargs)
+
+ return t.cast(F, update_wrapper(wrapper_func, f))
+
+
+class Scaffold:
+ """Common behavior shared between :class:`~flask.Flask` and
+ :class:`~flask.blueprints.Blueprint`.
+
+ :param import_name: The import name of the module where this object
+ is defined. Usually :attr:`__name__` should be used.
+ :param static_folder: Path to a folder of static files to serve.
+ If this is set, a static route will be added.
+ :param static_url_path: URL prefix for the static route.
+ :param template_folder: Path to a folder containing template files.
+ for rendering. If this is set, a Jinja loader will be added.
+ :param root_path: The path that static, template, and resource files
+ are relative to. Typically not set, it is discovered based on
+ the ``import_name``.
+
+ .. versionadded:: 2.0
+ """
+
+ cli: Group
+ name: str
+ _static_folder: str | None = None
+ _static_url_path: str | None = None
+
+ def __init__(
+ self,
+ import_name: str,
+ static_folder: str | os.PathLike[str] | None = None,
+ static_url_path: str | None = None,
+ template_folder: str | os.PathLike[str] | None = None,
+ root_path: str | None = None,
+ ):
+ #: The name of the package or module that this object belongs
+ #: to. Do not change this once it is set by the constructor.
+ self.import_name = import_name
+
+ self.static_folder = static_folder
+ self.static_url_path = static_url_path
+
+ #: The path to the templates folder, relative to
+ #: :attr:`root_path`, to add to the template loader. ``None`` if
+ #: templates should not be added.
+ self.template_folder = template_folder
+
+ if root_path is None:
+ root_path = get_root_path(self.import_name)
+
+ #: Absolute path to the package on the filesystem. Used to look
+ #: up resources contained in the package.
+ self.root_path = root_path
+
+ #: A dictionary mapping endpoint names to view functions.
+ #:
+ #: To register a view function, use the :meth:`route` decorator.
+ #:
+ #: This data structure is internal. It should not be modified
+ #: directly and its format may change at any time.
+ self.view_functions: dict[str, ft.RouteCallable] = {}
+
+ #: A data structure of registered error handlers, in the format
+ #: ``{scope: {code: {class: handler}}}``. The ``scope`` key is
+ #: the name of a blueprint the handlers are active for, or
+ #: ``None`` for all requests. The ``code`` key is the HTTP
+ #: status code for ``HTTPException``, or ``None`` for
+ #: other exceptions. The innermost dictionary maps exception
+ #: classes to handler functions.
+ #:
+ #: To register an error handler, use the :meth:`errorhandler`
+ #: decorator.
+ #:
+ #: This data structure is internal. It should not be modified
+ #: directly and its format may change at any time.
+ self.error_handler_spec: dict[
+ ft.AppOrBlueprintKey,
+ dict[int | None, dict[type[Exception], ft.ErrorHandlerCallable]],
+ ] = defaultdict(lambda: defaultdict(dict))
+
+ #: A data structure of functions to call at the beginning of
+ #: each request, in the format ``{scope: [functions]}``. The
+ #: ``scope`` key is the name of a blueprint the functions are
+ #: active for, or ``None`` for all requests.
+ #:
+ #: To register a function, use the :meth:`before_request`
+ #: decorator.
+ #:
+ #: This data structure is internal. It should not be modified
+ #: directly and its format may change at any time.
+ self.before_request_funcs: dict[
+ ft.AppOrBlueprintKey, list[ft.BeforeRequestCallable]
+ ] = defaultdict(list)
+
+ #: A data structure of functions to call at the end of each
+ #: request, in the format ``{scope: [functions]}``. The
+ #: ``scope`` key is the name of a blueprint the functions are
+ #: active for, or ``None`` for all requests.
+ #:
+ #: To register a function, use the :meth:`after_request`
+ #: decorator.
+ #:
+ #: This data structure is internal. It should not be modified
+ #: directly and its format may change at any time.
+ self.after_request_funcs: dict[
+ ft.AppOrBlueprintKey, list[ft.AfterRequestCallable[t.Any]]
+ ] = defaultdict(list)
+
+ #: A data structure of functions to call at the end of each
+ #: request even if an exception is raised, in the format
+ #: ``{scope: [functions]}``. The ``scope`` key is the name of a
+ #: blueprint the functions are active for, or ``None`` for all
+ #: requests.
+ #:
+ #: To register a function, use the :meth:`teardown_request`
+ #: decorator.
+ #:
+ #: This data structure is internal. It should not be modified
+ #: directly and its format may change at any time.
+ self.teardown_request_funcs: dict[
+ ft.AppOrBlueprintKey, list[ft.TeardownCallable]
+ ] = defaultdict(list)
+
+ #: A data structure of functions to call to pass extra context
+ #: values when rendering templates, in the format
+ #: ``{scope: [functions]}``. The ``scope`` key is the name of a
+ #: blueprint the functions are active for, or ``None`` for all
+ #: requests.
+ #:
+ #: To register a function, use the :meth:`context_processor`
+ #: decorator.
+ #:
+ #: This data structure is internal. It should not be modified
+ #: directly and its format may change at any time.
+ self.template_context_processors: dict[
+ ft.AppOrBlueprintKey, list[ft.TemplateContextProcessorCallable]
+ ] = defaultdict(list, {None: [_default_template_ctx_processor]})
+
+ #: A data structure of functions to call to modify the keyword
+ #: arguments passed to the view function, in the format
+ #: ``{scope: [functions]}``. The ``scope`` key is the name of a
+ #: blueprint the functions are active for, or ``None`` for all
+ #: requests.
+ #:
+ #: To register a function, use the
+ #: :meth:`url_value_preprocessor` decorator.
+ #:
+ #: This data structure is internal. It should not be modified
+ #: directly and its format may change at any time.
+ self.url_value_preprocessors: dict[
+ ft.AppOrBlueprintKey,
+ list[ft.URLValuePreprocessorCallable],
+ ] = defaultdict(list)
+
+ #: A data structure of functions to call to modify the keyword
+ #: arguments when generating URLs, in the format
+ #: ``{scope: [functions]}``. The ``scope`` key is the name of a
+ #: blueprint the functions are active for, or ``None`` for all
+ #: requests.
+ #:
+ #: To register a function, use the :meth:`url_defaults`
+ #: decorator.
+ #:
+ #: This data structure is internal. It should not be modified
+ #: directly and its format may change at any time.
+ self.url_default_functions: dict[
+ ft.AppOrBlueprintKey, list[ft.URLDefaultCallable]
+ ] = defaultdict(list)
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} {self.name!r}>"
+
+ def _check_setup_finished(self, f_name: str) -> None:
+ raise NotImplementedError
+
+ @property
+ def static_folder(self) -> str | None:
+ """The absolute path to the configured static folder. ``None``
+ if no static folder is set.
+ """
+ if self._static_folder is not None:
+ return os.path.join(self.root_path, self._static_folder)
+ else:
+ return None
+
+ @static_folder.setter
+ def static_folder(self, value: str | os.PathLike[str] | None) -> None:
+ if value is not None:
+ value = os.fspath(value).rstrip(r"\/")
+
+ self._static_folder = value
+
+ @property
+ def has_static_folder(self) -> bool:
+ """``True`` if :attr:`static_folder` is set.
+
+ .. versionadded:: 0.5
+ """
+ return self.static_folder is not None
+
+ @property
+ def static_url_path(self) -> str | None:
+ """The URL prefix that the static route will be accessible from.
+
+ If it was not configured during init, it is derived from
+ :attr:`static_folder`.
+ """
+ if self._static_url_path is not None:
+ return self._static_url_path
+
+ if self.static_folder is not None:
+ basename = os.path.basename(self.static_folder)
+ return f"/{basename}".rstrip("/")
+
+ return None
+
+ @static_url_path.setter
+ def static_url_path(self, value: str | None) -> None:
+ if value is not None:
+ value = value.rstrip("/")
+
+ self._static_url_path = value
+
+ @cached_property
+ def jinja_loader(self) -> BaseLoader | None:
+ """The Jinja loader for this object's templates. By default this
+ is a class :class:`jinja2.loaders.FileSystemLoader` to
+ :attr:`template_folder` if it is set.
+
+ .. versionadded:: 0.5
+ """
+ if self.template_folder is not None:
+ return FileSystemLoader(os.path.join(self.root_path, self.template_folder))
+ else:
+ return None
+
+ def _method_route(
+ self,
+ method: str,
+ rule: str,
+ options: dict[str, t.Any],
+ ) -> t.Callable[[T_route], T_route]:
+ if "methods" in options:
+ raise TypeError("Use the 'route' decorator to use the 'methods' argument.")
+
+ return self.route(rule, methods=[method], **options)
+
+ @setupmethod
+ def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
+ """Shortcut for :meth:`route` with ``methods=["GET"]``.
+
+ .. versionadded:: 2.0
+ """
+ return self._method_route("GET", rule, options)
+
+ @setupmethod
+ def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
+ """Shortcut for :meth:`route` with ``methods=["POST"]``.
+
+ .. versionadded:: 2.0
+ """
+ return self._method_route("POST", rule, options)
+
+ @setupmethod
+ def put(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
+ """Shortcut for :meth:`route` with ``methods=["PUT"]``.
+
+ .. versionadded:: 2.0
+ """
+ return self._method_route("PUT", rule, options)
+
+ @setupmethod
+ def delete(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
+ """Shortcut for :meth:`route` with ``methods=["DELETE"]``.
+
+ .. versionadded:: 2.0
+ """
+ return self._method_route("DELETE", rule, options)
+
+ @setupmethod
+ def patch(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
+ """Shortcut for :meth:`route` with ``methods=["PATCH"]``.
+
+ .. versionadded:: 2.0
+ """
+ return self._method_route("PATCH", rule, options)
+
+ @setupmethod
+ def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
+ """Decorate a view function to register it with the given URL
+ rule and options. Calls :meth:`add_url_rule`, which has more
+ details about the implementation.
+
+ .. code-block:: python
+
+ @app.route("/")
+ def index():
+ return "Hello, World!"
+
+ See :ref:`url-route-registrations`.
+
+ The endpoint name for the route defaults to the name of the view
+ function if the ``endpoint`` parameter isn't passed.
+
+ The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` and
+ ``OPTIONS`` are added automatically.
+
+ :param rule: The URL rule string.
+ :param options: Extra options passed to the
+ :class:`~werkzeug.routing.Rule` object.
+ """
+
+ def decorator(f: T_route) -> T_route:
+ endpoint = options.pop("endpoint", None)
+ self.add_url_rule(rule, endpoint, f, **options)
+ return f
+
+ return decorator
+
+ @setupmethod
+ def add_url_rule(
+ self,
+ rule: str,
+ endpoint: str | None = None,
+ view_func: ft.RouteCallable | None = None,
+ provide_automatic_options: bool | None = None,
+ **options: t.Any,
+ ) -> None:
+ """Register a rule for routing incoming requests and building
+ URLs. The :meth:`route` decorator is a shortcut to call this
+ with the ``view_func`` argument. These are equivalent:
+
+ .. code-block:: python
+
+ @app.route("/")
+ def index():
+ ...
+
+ .. code-block:: python
+
+ def index():
+ ...
+
+ app.add_url_rule("/", view_func=index)
+
+ See :ref:`url-route-registrations`.
+
+ The endpoint name for the route defaults to the name of the view
+ function if the ``endpoint`` parameter isn't passed. An error
+ will be raised if a function has already been registered for the
+ endpoint.
+
+ The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` is
+ always added automatically, and ``OPTIONS`` is added
+ automatically by default.
+
+ ``view_func`` does not necessarily need to be passed, but if the
+ rule should participate in routing an endpoint name must be
+ associated with a view function at some point with the
+ :meth:`endpoint` decorator.
+
+ .. code-block:: python
+
+ app.add_url_rule("/", endpoint="index")
+
+ @app.endpoint("index")
+ def index():
+ ...
+
+ If ``view_func`` has a ``required_methods`` attribute, those
+ methods are added to the passed and automatic methods. If it
+ has a ``provide_automatic_methods`` attribute, it is used as the
+ default if the parameter is not passed.
+
+ :param rule: The URL rule string.
+ :param endpoint: The endpoint name to associate with the rule
+ and view function. Used when routing and building URLs.
+ Defaults to ``view_func.__name__``.
+ :param view_func: The view function to associate with the
+ endpoint name.
+ :param provide_automatic_options: Add the ``OPTIONS`` method and
+ respond to ``OPTIONS`` requests automatically.
+ :param options: Extra options passed to the
+ :class:`~werkzeug.routing.Rule` object.
+ """
+ raise NotImplementedError
+
+ @setupmethod
+ def endpoint(self, endpoint: str) -> t.Callable[[F], F]:
+ """Decorate a view function to register it for the given
+ endpoint. Used if a rule is added without a ``view_func`` with
+ :meth:`add_url_rule`.
+
+ .. code-block:: python
+
+ app.add_url_rule("/ex", endpoint="example")
+
+ @app.endpoint("example")
+ def example():
+ ...
+
+ :param endpoint: The endpoint name to associate with the view
+ function.
+ """
+
+ def decorator(f: F) -> F:
+ self.view_functions[endpoint] = f
+ return f
+
+ return decorator
+
+ @setupmethod
+ def before_request(self, f: T_before_request) -> T_before_request:
+ """Register a function to run before each request.
+
+ For example, this can be used to open a database connection, or
+ to load the logged in user from the session.
+
+ .. code-block:: python
+
+ @app.before_request
+ def load_user():
+ if "user_id" in session:
+ g.user = db.session.get(session["user_id"])
+
+ The function will be called without any arguments. If it returns
+ a non-``None`` value, the value is handled as if it was the
+ return value from the view, and further request handling is
+ stopped.
+
+ This is available on both app and blueprint objects. When used on an app, this
+ executes before every request. When used on a blueprint, this executes before
+ every request that the blueprint handles. To register with a blueprint and
+ execute before every request, use :meth:`.Blueprint.before_app_request`.
+ """
+ self.before_request_funcs.setdefault(None, []).append(f)
+ return f
+
+ @setupmethod
+ def after_request(self, f: T_after_request) -> T_after_request:
+ """Register a function to run after each request to this object.
+
+ The function is called with the response object, and must return
+ a response object. This allows the functions to modify or
+ replace the response before it is sent.
+
+ If a function raises an exception, any remaining
+ ``after_request`` functions will not be called. Therefore, this
+ should not be used for actions that must execute, such as to
+ close resources. Use :meth:`teardown_request` for that.
+
+ This is available on both app and blueprint objects. When used on an app, this
+ executes after every request. When used on a blueprint, this executes after
+ every request that the blueprint handles. To register with a blueprint and
+ execute after every request, use :meth:`.Blueprint.after_app_request`.
+ """
+ self.after_request_funcs.setdefault(None, []).append(f)
+ return f
+
+ @setupmethod
+ def teardown_request(self, f: T_teardown) -> T_teardown:
+ """Register a function to be called when the request context is
+ popped. Typically this happens at the end of each request, but
+ contexts may be pushed manually as well during testing.
+
+ .. code-block:: python
+
+ with app.test_request_context():
+ ...
+
+ When the ``with`` block exits (or ``ctx.pop()`` is called), the
+ teardown functions are called just before the request context is
+ made inactive.
+
+ When a teardown function was called because of an unhandled
+ exception it will be passed an error object. If an
+ :meth:`errorhandler` is registered, it will handle the exception
+ and the teardown will not receive it.
+
+ Teardown functions must avoid raising exceptions. If they
+ execute code that might fail they must surround that code with a
+ ``try``/``except`` block and log any errors.
+
+ The return values of teardown functions are ignored.
+
+ This is available on both app and blueprint objects. When used on an app, this
+ executes after every request. When used on a blueprint, this executes after
+ every request that the blueprint handles. To register with a blueprint and
+ execute after every request, use :meth:`.Blueprint.teardown_app_request`.
+ """
+ self.teardown_request_funcs.setdefault(None, []).append(f)
+ return f
+
+ @setupmethod
+ def context_processor(
+ self,
+ f: T_template_context_processor,
+ ) -> T_template_context_processor:
+ """Registers a template context processor function. These functions run before
+ rendering a template. The keys of the returned dict are added as variables
+ available in the template.
+
+ This is available on both app and blueprint objects. When used on an app, this
+ is called for every rendered template. When used on a blueprint, this is called
+ for templates rendered from the blueprint's views. To register with a blueprint
+ and affect every template, use :meth:`.Blueprint.app_context_processor`.
+ """
+ self.template_context_processors[None].append(f)
+ return f
+
+ @setupmethod
+ def url_value_preprocessor(
+ self,
+ f: T_url_value_preprocessor,
+ ) -> T_url_value_preprocessor:
+ """Register a URL value preprocessor function for all view
+ functions in the application. These functions will be called before the
+ :meth:`before_request` functions.
+
+ The function can modify the values captured from the matched url before
+ they are passed to the view. For example, this can be used to pop a
+ common language code value and place it in ``g`` rather than pass it to
+ every view.
+
+ The function is passed the endpoint name and values dict. The return
+ value is ignored.
+
+ This is available on both app and blueprint objects. When used on an app, this
+ is called for every request. When used on a blueprint, this is called for
+ requests that the blueprint handles. To register with a blueprint and affect
+ every request, use :meth:`.Blueprint.app_url_value_preprocessor`.
+ """
+ self.url_value_preprocessors[None].append(f)
+ return f
+
+ @setupmethod
+ def url_defaults(self, f: T_url_defaults) -> T_url_defaults:
+ """Callback function for URL defaults for all view functions of the
+ application. It's called with the endpoint and values and should
+ update the values passed in place.
+
+ This is available on both app and blueprint objects. When used on an app, this
+ is called for every request. When used on a blueprint, this is called for
+ requests that the blueprint handles. To register with a blueprint and affect
+ every request, use :meth:`.Blueprint.app_url_defaults`.
+ """
+ self.url_default_functions[None].append(f)
+ return f
+
+ @setupmethod
+ def errorhandler(
+ self, code_or_exception: type[Exception] | int
+ ) -> t.Callable[[T_error_handler], T_error_handler]:
+ """Register a function to handle errors by code or exception class.
+
+ A decorator that is used to register a function given an
+ error code. Example::
+
+ @app.errorhandler(404)
+ def page_not_found(error):
+ return 'This page does not exist', 404
+
+ You can also register handlers for arbitrary exceptions::
+
+ @app.errorhandler(DatabaseError)
+ def special_exception_handler(error):
+ return 'Database connection failed', 500
+
+ This is available on both app and blueprint objects. When used on an app, this
+ can handle errors from every request. When used on a blueprint, this can handle
+ errors from requests that the blueprint handles. To register with a blueprint
+ and affect every request, use :meth:`.Blueprint.app_errorhandler`.
+
+ .. versionadded:: 0.7
+ Use :meth:`register_error_handler` instead of modifying
+ :attr:`error_handler_spec` directly, for application wide error
+ handlers.
+
+ .. versionadded:: 0.7
+ One can now additionally also register custom exception types
+ that do not necessarily have to be a subclass of the
+ :class:`~werkzeug.exceptions.HTTPException` class.
+
+ :param code_or_exception: the code as integer for the handler, or
+ an arbitrary exception
+ """
+
+ def decorator(f: T_error_handler) -> T_error_handler:
+ self.register_error_handler(code_or_exception, f)
+ return f
+
+ return decorator
+
+ @setupmethod
+ def register_error_handler(
+ self,
+ code_or_exception: type[Exception] | int,
+ f: ft.ErrorHandlerCallable,
+ ) -> None:
+ """Alternative error attach function to the :meth:`errorhandler`
+ decorator that is more straightforward to use for non decorator
+ usage.
+
+ .. versionadded:: 0.7
+ """
+ exc_class, code = self._get_exc_class_and_code(code_or_exception)
+ self.error_handler_spec[None][code][exc_class] = f
+
+ @staticmethod
+ def _get_exc_class_and_code(
+ exc_class_or_code: type[Exception] | int,
+ ) -> tuple[type[Exception], int | None]:
+ """Get the exception class being handled. For HTTP status codes
+ or ``HTTPException`` subclasses, return both the exception and
+ status code.
+
+ :param exc_class_or_code: Any exception class, or an HTTP status
+ code as an integer.
+ """
+ exc_class: type[Exception]
+
+ if isinstance(exc_class_or_code, int):
+ try:
+ exc_class = default_exceptions[exc_class_or_code]
+ except KeyError:
+ raise ValueError(
+ f"'{exc_class_or_code}' is not a recognized HTTP"
+ " error code. Use a subclass of HTTPException with"
+ " that code instead."
+ ) from None
+ else:
+ exc_class = exc_class_or_code
+
+ if isinstance(exc_class, Exception):
+ raise TypeError(
+ f"{exc_class!r} is an instance, not a class. Handlers"
+ " can only be registered for Exception classes or HTTP"
+ " error codes."
+ )
+
+ if not issubclass(exc_class, Exception):
+ raise ValueError(
+ f"'{exc_class.__name__}' is not a subclass of Exception."
+ " Handlers can only be registered for Exception classes"
+ " or HTTP error codes."
+ )
+
+ if issubclass(exc_class, HTTPException):
+ return exc_class, exc_class.code
+ else:
+ return exc_class, None
+
+
+def _endpoint_from_view_func(view_func: ft.RouteCallable) -> str:
+ """Internal helper that returns the default endpoint for a given
+ function. This always is the function name.
+ """
+ assert view_func is not None, "expected view func if endpoint is not provided."
+ return view_func.__name__
+
+
+def _find_package_path(import_name: str) -> str:
+ """Find the path that contains the package or module."""
+ root_mod_name, _, _ = import_name.partition(".")
+
+ try:
+ root_spec = importlib.util.find_spec(root_mod_name)
+
+ if root_spec is None:
+ raise ValueError("not found")
+ except (ImportError, ValueError):
+ # ImportError: the machinery told us it does not exist
+ # ValueError:
+ # - the module name was invalid
+ # - the module name is __main__
+ # - we raised `ValueError` due to `root_spec` being `None`
+ return os.getcwd()
+
+ if root_spec.submodule_search_locations:
+ if root_spec.origin is None or root_spec.origin == "namespace":
+ # namespace package
+ package_spec = importlib.util.find_spec(import_name)
+
+ if package_spec is not None and package_spec.submodule_search_locations:
+ # Pick the path in the namespace that contains the submodule.
+ package_path = pathlib.Path(
+ os.path.commonpath(package_spec.submodule_search_locations)
+ )
+ search_location = next(
+ location
+ for location in root_spec.submodule_search_locations
+ if package_path.is_relative_to(location)
+ )
+ else:
+ # Pick the first path.
+ search_location = root_spec.submodule_search_locations[0]
+
+ return os.path.dirname(search_location)
+ else:
+ # package with __init__.py
+ return os.path.dirname(os.path.dirname(root_spec.origin))
+ else:
+ # module
+ return os.path.dirname(root_spec.origin) # type: ignore[type-var, return-value]
+
+
+def find_package(import_name: str) -> tuple[str | None, str]:
+ """Find the prefix that a package is installed under, and the path
+ that it would be imported from.
+
+ The prefix is the directory containing the standard directory
+ hierarchy (lib, bin, etc.). If the package is not installed to the
+ system (:attr:`sys.prefix`) or a virtualenv (``site-packages``),
+ ``None`` is returned.
+
+ The path is the entry in :attr:`sys.path` that contains the package
+ for import. If the package is not installed, it's assumed that the
+ package was imported from the current working directory.
+ """
+ package_path = _find_package_path(import_name)
+ py_prefix = os.path.abspath(sys.prefix)
+
+ # installed to the system
+ if pathlib.PurePath(package_path).is_relative_to(py_prefix):
+ return py_prefix, package_path
+
+ site_parent, site_folder = os.path.split(package_path)
+
+ # installed to a virtualenv
+ if site_folder.lower() == "site-packages":
+ parent, folder = os.path.split(site_parent)
+
+ # Windows (prefix/lib/site-packages)
+ if folder.lower() == "lib":
+ return parent, package_path
+
+ # Unix (prefix/lib/pythonX.Y/site-packages)
+ if os.path.basename(parent).lower() == "lib":
+ return os.path.dirname(parent), package_path
+
+ # something else (prefix/site-packages)
+ return site_parent, package_path
+
+ # not installed
+ return None, package_path
diff --git a/venv/Lib/site-packages/flask/sessions.py b/venv/Lib/site-packages/flask/sessions.py
new file mode 100644
index 0000000..ad35770
--- /dev/null
+++ b/venv/Lib/site-packages/flask/sessions.py
@@ -0,0 +1,385 @@
+from __future__ import annotations
+
+import collections.abc as c
+import hashlib
+import typing as t
+from collections.abc import MutableMapping
+from datetime import datetime
+from datetime import timezone
+
+from itsdangerous import BadSignature
+from itsdangerous import URLSafeTimedSerializer
+from werkzeug.datastructures import CallbackDict
+
+from .json.tag import TaggedJSONSerializer
+
+if t.TYPE_CHECKING: # pragma: no cover
+ import typing_extensions as te
+
+ from .app import Flask
+ from .wrappers import Request
+ from .wrappers import Response
+
+
+class SessionMixin(MutableMapping[str, t.Any]):
+ """Expands a basic dictionary with session attributes."""
+
+ @property
+ def permanent(self) -> bool:
+ """This reflects the ``'_permanent'`` key in the dict."""
+ return self.get("_permanent", False) # type: ignore[no-any-return]
+
+ @permanent.setter
+ def permanent(self, value: bool) -> None:
+ self["_permanent"] = bool(value)
+
+ #: Some implementations can detect whether a session is newly
+ #: created, but that is not guaranteed. Use with caution. The mixin
+ # default is hard-coded ``False``.
+ new = False
+
+ #: Some implementations can detect changes to the session and set
+ #: this when that happens. The mixin default is hard coded to
+ #: ``True``.
+ modified = True
+
+ accessed = False
+ """Indicates if the session was accessed, even if it was not modified. This
+ is set when the session object is accessed through the request context,
+ including the global :data:`.session` proxy. A ``Vary: cookie`` header will
+ be added if this is ``True``.
+
+ .. versionchanged:: 3.1.3
+ This is tracked by the request context.
+ """
+
+
+class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
+ """Base class for sessions based on signed cookies.
+
+ This session backend will set the :attr:`modified` and
+ :attr:`accessed` attributes. It cannot reliably track whether a
+ session is new (vs. empty), so :attr:`new` remains hard coded to
+ ``False``.
+ """
+
+ #: When data is changed, this is set to ``True``. Only the session
+ #: dictionary itself is tracked; if the session contains mutable
+ #: data (for example a nested dict) then this must be set to
+ #: ``True`` manually when modifying that data. The session cookie
+ #: will only be written to the response if this is ``True``.
+ modified = False
+
+ def __init__(
+ self,
+ initial: c.Mapping[str, t.Any] | None = None,
+ ) -> None:
+ def on_update(self: te.Self) -> None:
+ self.modified = True
+
+ super().__init__(initial, on_update)
+
+
+class NullSession(SecureCookieSession):
+ """Class used to generate nicer error messages if sessions are not
+ available. Will still allow read-only access to the empty session
+ but fail on setting.
+ """
+
+ def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
+ raise RuntimeError(
+ "The session is unavailable because no secret "
+ "key was set. Set the secret_key on the "
+ "application to something unique and secret."
+ )
+
+ __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail
+ del _fail
+
+
+class SessionInterface:
+ """The basic interface you have to implement in order to replace the
+ default session interface which uses werkzeug's securecookie
+ implementation. The only methods you have to implement are
+ :meth:`open_session` and :meth:`save_session`, the others have
+ useful defaults which you don't need to change.
+
+ The session object returned by the :meth:`open_session` method has to
+ provide a dictionary like interface plus the properties and methods
+ from the :class:`SessionMixin`. We recommend just subclassing a dict
+ and adding that mixin::
+
+ class Session(dict, SessionMixin):
+ pass
+
+ If :meth:`open_session` returns ``None`` Flask will call into
+ :meth:`make_null_session` to create a session that acts as replacement
+ if the session support cannot work because some requirement is not
+ fulfilled. The default :class:`NullSession` class that is created
+ will complain that the secret key was not set.
+
+ To replace the session interface on an application all you have to do
+ is to assign :attr:`flask.Flask.session_interface`::
+
+ app = Flask(__name__)
+ app.session_interface = MySessionInterface()
+
+ Multiple requests with the same session may be sent and handled
+ concurrently. When implementing a new session interface, consider
+ whether reads or writes to the backing store must be synchronized.
+ There is no guarantee on the order in which the session for each
+ request is opened or saved, it will occur in the order that requests
+ begin and end processing.
+
+ .. versionadded:: 0.8
+ """
+
+ #: :meth:`make_null_session` will look here for the class that should
+ #: be created when a null session is requested. Likewise the
+ #: :meth:`is_null_session` method will perform a typecheck against
+ #: this type.
+ null_session_class = NullSession
+
+ #: A flag that indicates if the session interface is pickle based.
+ #: This can be used by Flask extensions to make a decision in regards
+ #: to how to deal with the session object.
+ #:
+ #: .. versionadded:: 0.10
+ pickle_based = False
+
+ def make_null_session(self, app: Flask) -> NullSession:
+ """Creates a null session which acts as a replacement object if the
+ real session support could not be loaded due to a configuration
+ error. This mainly aids the user experience because the job of the
+ null session is to still support lookup without complaining but
+ modifications are answered with a helpful error message of what
+ failed.
+
+ This creates an instance of :attr:`null_session_class` by default.
+ """
+ return self.null_session_class()
+
+ def is_null_session(self, obj: object) -> bool:
+ """Checks if a given object is a null session. Null sessions are
+ not asked to be saved.
+
+ This checks if the object is an instance of :attr:`null_session_class`
+ by default.
+ """
+ return isinstance(obj, self.null_session_class)
+
+ def get_cookie_name(self, app: Flask) -> str:
+ """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``."""
+ return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return]
+
+ def get_cookie_domain(self, app: Flask) -> str | None:
+ """The value of the ``Domain`` parameter on the session cookie. If not set,
+ browsers will only send the cookie to the exact domain it was set from.
+ Otherwise, they will send it to any subdomain of the given value as well.
+
+ Uses the :data:`SESSION_COOKIE_DOMAIN` config.
+
+ .. versionchanged:: 2.3
+ Not set by default, does not fall back to ``SERVER_NAME``.
+ """
+ return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return]
+
+ def get_cookie_path(self, app: Flask) -> str:
+ """Returns the path for which the cookie should be valid. The
+ default implementation uses the value from the ``SESSION_COOKIE_PATH``
+ config var if it's set, and falls back to ``APPLICATION_ROOT`` or
+ uses ``/`` if it's ``None``.
+ """
+ return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return]
+
+ def get_cookie_httponly(self, app: Flask) -> bool:
+ """Returns True if the session cookie should be httponly. This
+ currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
+ config var.
+ """
+ return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return]
+
+ def get_cookie_secure(self, app: Flask) -> bool:
+ """Returns True if the cookie should be secure. This currently
+ just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
+ """
+ return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return]
+
+ def get_cookie_samesite(self, app: Flask) -> str | None:
+ """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the
+ ``SameSite`` attribute. This currently just returns the value of
+ the :data:`SESSION_COOKIE_SAMESITE` setting.
+ """
+ return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return]
+
+ def get_cookie_partitioned(self, app: Flask) -> bool:
+ """Returns True if the cookie should be partitioned. By default, uses
+ the value of :data:`SESSION_COOKIE_PARTITIONED`.
+
+ .. versionadded:: 3.1
+ """
+ return app.config["SESSION_COOKIE_PARTITIONED"] # type: ignore[no-any-return]
+
+ def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None:
+ """A helper method that returns an expiration date for the session
+ or ``None`` if the session is linked to the browser session. The
+ default implementation returns now + the permanent session
+ lifetime configured on the application.
+ """
+ if session.permanent:
+ return datetime.now(timezone.utc) + app.permanent_session_lifetime
+ return None
+
+ def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool:
+ """Used by session backends to determine if a ``Set-Cookie`` header
+ should be set for this session cookie for this response. If the session
+ has been modified, the cookie is set. If the session is permanent and
+ the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
+ always set.
+
+ This check is usually skipped if the session was deleted.
+
+ .. versionadded:: 0.11
+ """
+
+ return session.modified or (
+ session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"]
+ )
+
+ def open_session(self, app: Flask, request: Request) -> SessionMixin | None:
+ """This is called at the beginning of each request, after
+ pushing the request context, before matching the URL.
+
+ This must return an object which implements a dictionary-like
+ interface as well as the :class:`SessionMixin` interface.
+
+ This will return ``None`` to indicate that loading failed in
+ some way that is not immediately an error. The request
+ context will fall back to using :meth:`make_null_session`
+ in this case.
+ """
+ raise NotImplementedError()
+
+ def save_session(
+ self, app: Flask, session: SessionMixin, response: Response
+ ) -> None:
+ """This is called at the end of each request, after generating
+ a response, before removing the request context. It is skipped
+ if :meth:`is_null_session` returns ``True``.
+ """
+ raise NotImplementedError()
+
+
+session_json_serializer = TaggedJSONSerializer()
+
+
+def _lazy_sha1(string: bytes = b"") -> t.Any:
+ """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include
+ SHA-1, in which case the import and use as a default would fail before the
+ developer can configure something else.
+ """
+ return hashlib.sha1(string)
+
+
+class SecureCookieSessionInterface(SessionInterface):
+ """The default session interface that stores sessions in signed cookies
+ through the :mod:`itsdangerous` module.
+ """
+
+ #: the salt that should be applied on top of the secret key for the
+ #: signing of cookie based sessions.
+ salt = "cookie-session"
+ #: the hash function to use for the signature. The default is sha1
+ digest_method = staticmethod(_lazy_sha1)
+ #: the name of the itsdangerous supported key derivation. The default
+ #: is hmac.
+ key_derivation = "hmac"
+ #: A python serializer for the payload. The default is a compact
+ #: JSON derived serializer with support for some extra Python types
+ #: such as datetime objects or tuples.
+ serializer = session_json_serializer
+ session_class = SecureCookieSession
+
+ def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None:
+ if not app.secret_key:
+ return None
+
+ keys: list[str | bytes] = []
+
+ if fallbacks := app.config["SECRET_KEY_FALLBACKS"]:
+ keys.extend(fallbacks)
+
+ keys.append(app.secret_key) # itsdangerous expects current key at top
+ return URLSafeTimedSerializer(
+ keys, # type: ignore[arg-type]
+ salt=self.salt,
+ serializer=self.serializer,
+ signer_kwargs={
+ "key_derivation": self.key_derivation,
+ "digest_method": self.digest_method,
+ },
+ )
+
+ def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None:
+ s = self.get_signing_serializer(app)
+ if s is None:
+ return None
+ val = request.cookies.get(self.get_cookie_name(app))
+ if not val:
+ return self.session_class()
+ max_age = int(app.permanent_session_lifetime.total_seconds())
+ try:
+ data = s.loads(val, max_age=max_age)
+ return self.session_class(data)
+ except BadSignature:
+ return self.session_class()
+
+ def save_session(
+ self, app: Flask, session: SessionMixin, response: Response
+ ) -> None:
+ name = self.get_cookie_name(app)
+ domain = self.get_cookie_domain(app)
+ path = self.get_cookie_path(app)
+ secure = self.get_cookie_secure(app)
+ partitioned = self.get_cookie_partitioned(app)
+ samesite = self.get_cookie_samesite(app)
+ httponly = self.get_cookie_httponly(app)
+
+ # Add a "Vary: Cookie" header if the session was accessed at all.
+ if session.accessed:
+ response.vary.add("Cookie")
+
+ # If the session is modified to be empty, remove the cookie.
+ # If the session is empty, return without setting the cookie.
+ if not session:
+ if session.modified:
+ response.delete_cookie(
+ name,
+ domain=domain,
+ path=path,
+ secure=secure,
+ partitioned=partitioned,
+ samesite=samesite,
+ httponly=httponly,
+ )
+ response.vary.add("Cookie")
+
+ return
+
+ if not self.should_set_cookie(app, session):
+ return
+
+ expires = self.get_expiration_time(app, session)
+ val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore[union-attr]
+ response.set_cookie(
+ name,
+ val,
+ expires=expires,
+ httponly=httponly,
+ domain=domain,
+ path=path,
+ secure=secure,
+ partitioned=partitioned,
+ samesite=samesite,
+ )
+ response.vary.add("Cookie")
diff --git a/venv/Lib/site-packages/flask/signals.py b/venv/Lib/site-packages/flask/signals.py
new file mode 100644
index 0000000..444fda9
--- /dev/null
+++ b/venv/Lib/site-packages/flask/signals.py
@@ -0,0 +1,17 @@
+from __future__ import annotations
+
+from blinker import Namespace
+
+# This namespace is only for signals provided by Flask itself.
+_signals = Namespace()
+
+template_rendered = _signals.signal("template-rendered")
+before_render_template = _signals.signal("before-render-template")
+request_started = _signals.signal("request-started")
+request_finished = _signals.signal("request-finished")
+request_tearing_down = _signals.signal("request-tearing-down")
+got_request_exception = _signals.signal("got-request-exception")
+appcontext_tearing_down = _signals.signal("appcontext-tearing-down")
+appcontext_pushed = _signals.signal("appcontext-pushed")
+appcontext_popped = _signals.signal("appcontext-popped")
+message_flashed = _signals.signal("message-flashed")
diff --git a/venv/Lib/site-packages/flask/templating.py b/venv/Lib/site-packages/flask/templating.py
new file mode 100644
index 0000000..c5fb5b9
--- /dev/null
+++ b/venv/Lib/site-packages/flask/templating.py
@@ -0,0 +1,220 @@
+from __future__ import annotations
+
+import typing as t
+
+from jinja2 import BaseLoader
+from jinja2 import Environment as BaseEnvironment
+from jinja2 import Template
+from jinja2 import TemplateNotFound
+
+from .globals import _cv_app
+from .globals import _cv_request
+from .globals import current_app
+from .globals import request
+from .helpers import stream_with_context
+from .signals import before_render_template
+from .signals import template_rendered
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from .app import Flask
+ from .sansio.app import App
+ from .sansio.scaffold import Scaffold
+
+
+def _default_template_ctx_processor() -> dict[str, t.Any]:
+ """Default template context processor. Replaces the ``request`` and ``g``
+ proxies with their concrete objects for faster access.
+ """
+ appctx = _cv_app.get(None)
+ reqctx = _cv_request.get(None)
+ rv: dict[str, t.Any] = {}
+ if appctx is not None:
+ rv["g"] = appctx.g
+ if reqctx is not None:
+ rv["request"] = reqctx.request
+ # The session proxy cannot be replaced, accessing it gets
+ # RequestContext.session, which sets session.accessed.
+ return rv
+
+
+class Environment(BaseEnvironment):
+ """Works like a regular Jinja environment but has some additional
+ knowledge of how Flask's blueprint works so that it can prepend the
+ name of the blueprint to referenced templates if necessary.
+ """
+
+ def __init__(self, app: App, **options: t.Any) -> None:
+ if "loader" not in options:
+ options["loader"] = app.create_global_jinja_loader()
+ BaseEnvironment.__init__(self, **options)
+ self.app = app
+
+
+class DispatchingJinjaLoader(BaseLoader):
+ """A loader that looks for templates in the application and all
+ the blueprint folders.
+ """
+
+ def __init__(self, app: App) -> None:
+ self.app = app
+
+ def get_source(
+ self, environment: BaseEnvironment, template: str
+ ) -> tuple[str, str | None, t.Callable[[], bool] | None]:
+ if self.app.config["EXPLAIN_TEMPLATE_LOADING"]:
+ return self._get_source_explained(environment, template)
+ return self._get_source_fast(environment, template)
+
+ def _get_source_explained(
+ self, environment: BaseEnvironment, template: str
+ ) -> tuple[str, str | None, t.Callable[[], bool] | None]:
+ attempts = []
+ rv: tuple[str, str | None, t.Callable[[], bool] | None] | None
+ trv: None | (tuple[str, str | None, t.Callable[[], bool] | None]) = None
+
+ for srcobj, loader in self._iter_loaders(template):
+ try:
+ rv = loader.get_source(environment, template)
+ if trv is None:
+ trv = rv
+ except TemplateNotFound:
+ rv = None
+ attempts.append((loader, srcobj, rv))
+
+ from .debughelpers import explain_template_loading_attempts
+
+ explain_template_loading_attempts(self.app, template, attempts)
+
+ if trv is not None:
+ return trv
+ raise TemplateNotFound(template)
+
+ def _get_source_fast(
+ self, environment: BaseEnvironment, template: str
+ ) -> tuple[str, str | None, t.Callable[[], bool] | None]:
+ for _srcobj, loader in self._iter_loaders(template):
+ try:
+ return loader.get_source(environment, template)
+ except TemplateNotFound:
+ continue
+ raise TemplateNotFound(template)
+
+ def _iter_loaders(self, template: str) -> t.Iterator[tuple[Scaffold, BaseLoader]]:
+ loader = self.app.jinja_loader
+ if loader is not None:
+ yield self.app, loader
+
+ for blueprint in self.app.iter_blueprints():
+ loader = blueprint.jinja_loader
+ if loader is not None:
+ yield blueprint, loader
+
+ def list_templates(self) -> list[str]:
+ result = set()
+ loader = self.app.jinja_loader
+ if loader is not None:
+ result.update(loader.list_templates())
+
+ for blueprint in self.app.iter_blueprints():
+ loader = blueprint.jinja_loader
+ if loader is not None:
+ for template in loader.list_templates():
+ result.add(template)
+
+ return list(result)
+
+
+def _render(app: Flask, template: Template, context: dict[str, t.Any]) -> str:
+ app.update_template_context(context)
+ before_render_template.send(
+ app, _async_wrapper=app.ensure_sync, template=template, context=context
+ )
+ rv = template.render(context)
+ template_rendered.send(
+ app, _async_wrapper=app.ensure_sync, template=template, context=context
+ )
+ return rv
+
+
+def render_template(
+ template_name_or_list: str | Template | list[str | Template],
+ **context: t.Any,
+) -> str:
+ """Render a template by name with the given context.
+
+ :param template_name_or_list: The name of the template to render. If
+ a list is given, the first name to exist will be rendered.
+ :param context: The variables to make available in the template.
+ """
+ app = current_app._get_current_object() # type: ignore[attr-defined]
+ template = app.jinja_env.get_or_select_template(template_name_or_list)
+ return _render(app, template, context)
+
+
+def render_template_string(source: str, **context: t.Any) -> str:
+ """Render a template from the given source string with the given
+ context.
+
+ :param source: The source code of the template to render.
+ :param context: The variables to make available in the template.
+ """
+ app = current_app._get_current_object() # type: ignore[attr-defined]
+ template = app.jinja_env.from_string(source)
+ return _render(app, template, context)
+
+
+def _stream(
+ app: Flask, template: Template, context: dict[str, t.Any]
+) -> t.Iterator[str]:
+ app.update_template_context(context)
+ before_render_template.send(
+ app, _async_wrapper=app.ensure_sync, template=template, context=context
+ )
+
+ def generate() -> t.Iterator[str]:
+ yield from template.generate(context)
+ template_rendered.send(
+ app, _async_wrapper=app.ensure_sync, template=template, context=context
+ )
+
+ rv = generate()
+
+ # If a request context is active, keep it while generating.
+ if request:
+ rv = stream_with_context(rv)
+
+ return rv
+
+
+def stream_template(
+ template_name_or_list: str | Template | list[str | Template],
+ **context: t.Any,
+) -> t.Iterator[str]:
+ """Render a template by name with the given context as a stream.
+ This returns an iterator of strings, which can be used as a
+ streaming response from a view.
+
+ :param template_name_or_list: The name of the template to render. If
+ a list is given, the first name to exist will be rendered.
+ :param context: The variables to make available in the template.
+
+ .. versionadded:: 2.2
+ """
+ app = current_app._get_current_object() # type: ignore[attr-defined]
+ template = app.jinja_env.get_or_select_template(template_name_or_list)
+ return _stream(app, template, context)
+
+
+def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]:
+ """Render a template from the given source string with the given
+ context as a stream. This returns an iterator of strings, which can
+ be used as a streaming response from a view.
+
+ :param source: The source code of the template to render.
+ :param context: The variables to make available in the template.
+
+ .. versionadded:: 2.2
+ """
+ app = current_app._get_current_object() # type: ignore[attr-defined]
+ template = app.jinja_env.from_string(source)
+ return _stream(app, template, context)
diff --git a/venv/Lib/site-packages/flask/testing.py b/venv/Lib/site-packages/flask/testing.py
new file mode 100644
index 0000000..55eb12f
--- /dev/null
+++ b/venv/Lib/site-packages/flask/testing.py
@@ -0,0 +1,298 @@
+from __future__ import annotations
+
+import importlib.metadata
+import typing as t
+from contextlib import contextmanager
+from contextlib import ExitStack
+from copy import copy
+from types import TracebackType
+from urllib.parse import urlsplit
+
+import werkzeug.test
+from click.testing import CliRunner
+from click.testing import Result
+from werkzeug.test import Client
+from werkzeug.wrappers import Request as BaseRequest
+
+from .cli import ScriptInfo
+from .sessions import SessionMixin
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from _typeshed.wsgi import WSGIEnvironment
+ from werkzeug.test import TestResponse
+
+ from .app import Flask
+
+
+class EnvironBuilder(werkzeug.test.EnvironBuilder):
+ """An :class:`~werkzeug.test.EnvironBuilder`, that takes defaults from the
+ application.
+
+ :param app: The Flask application to configure the environment from.
+ :param path: URL path being requested.
+ :param base_url: Base URL where the app is being served, which
+ ``path`` is relative to. If not given, built from
+ :data:`PREFERRED_URL_SCHEME`, ``subdomain``,
+ :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`.
+ :param subdomain: Subdomain name to append to :data:`SERVER_NAME`.
+ :param url_scheme: Scheme to use instead of
+ :data:`PREFERRED_URL_SCHEME`.
+ :param json: If given, this is serialized as JSON and passed as
+ ``data``. Also defaults ``content_type`` to
+ ``application/json``.
+ :param args: other positional arguments passed to
+ :class:`~werkzeug.test.EnvironBuilder`.
+ :param kwargs: other keyword arguments passed to
+ :class:`~werkzeug.test.EnvironBuilder`.
+ """
+
+ def __init__(
+ self,
+ app: Flask,
+ path: str = "/",
+ base_url: str | None = None,
+ subdomain: str | None = None,
+ url_scheme: str | None = None,
+ *args: t.Any,
+ **kwargs: t.Any,
+ ) -> None:
+ assert not (base_url or subdomain or url_scheme) or (
+ base_url is not None
+ ) != bool(subdomain or url_scheme), (
+ 'Cannot pass "subdomain" or "url_scheme" with "base_url".'
+ )
+
+ if base_url is None:
+ http_host = app.config.get("SERVER_NAME") or "localhost"
+ app_root = app.config["APPLICATION_ROOT"]
+
+ if subdomain:
+ http_host = f"{subdomain}.{http_host}"
+
+ if url_scheme is None:
+ url_scheme = app.config["PREFERRED_URL_SCHEME"]
+
+ url = urlsplit(path)
+ base_url = (
+ f"{url.scheme or url_scheme}://{url.netloc or http_host}"
+ f"/{app_root.lstrip('/')}"
+ )
+ path = url.path
+
+ if url.query:
+ path = f"{path}?{url.query}"
+
+ self.app = app
+ super().__init__(path, base_url, *args, **kwargs)
+
+ def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
+ """Serialize ``obj`` to a JSON-formatted string.
+
+ The serialization will be configured according to the config associated
+ with this EnvironBuilder's ``app``.
+ """
+ return self.app.json.dumps(obj, **kwargs)
+
+
+_werkzeug_version = ""
+
+
+def _get_werkzeug_version() -> str:
+ global _werkzeug_version
+
+ if not _werkzeug_version:
+ _werkzeug_version = importlib.metadata.version("werkzeug")
+
+ return _werkzeug_version
+
+
+class FlaskClient(Client):
+ """Works like a regular Werkzeug test client but has knowledge about
+ Flask's contexts to defer the cleanup of the request context until
+ the end of a ``with`` block. For general information about how to
+ use this class refer to :class:`werkzeug.test.Client`.
+
+ .. versionchanged:: 0.12
+ `app.test_client()` includes preset default environment, which can be
+ set after instantiation of the `app.test_client()` object in
+ `client.environ_base`.
+
+ Basic usage is outlined in the :doc:`/testing` chapter.
+ """
+
+ application: Flask
+
+ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
+ super().__init__(*args, **kwargs)
+ self.preserve_context = False
+ self._new_contexts: list[t.ContextManager[t.Any]] = []
+ self._context_stack = ExitStack()
+ self.environ_base = {
+ "REMOTE_ADDR": "127.0.0.1",
+ "HTTP_USER_AGENT": f"Werkzeug/{_get_werkzeug_version()}",
+ }
+
+ @contextmanager
+ def session_transaction(
+ self, *args: t.Any, **kwargs: t.Any
+ ) -> t.Iterator[SessionMixin]:
+ """When used in combination with a ``with`` statement this opens a
+ session transaction. This can be used to modify the session that
+ the test client uses. Once the ``with`` block is left the session is
+ stored back.
+
+ ::
+
+ with client.session_transaction() as session:
+ session['value'] = 42
+
+ Internally this is implemented by going through a temporary test
+ request context and since session handling could depend on
+ request variables this function accepts the same arguments as
+ :meth:`~flask.Flask.test_request_context` which are directly
+ passed through.
+ """
+ if self._cookies is None:
+ raise TypeError(
+ "Cookies are disabled. Create a client with 'use_cookies=True'."
+ )
+
+ app = self.application
+ ctx = app.test_request_context(*args, **kwargs)
+ self._add_cookies_to_wsgi(ctx.request.environ)
+
+ with ctx:
+ sess = app.session_interface.open_session(app, ctx.request)
+
+ if sess is None:
+ raise RuntimeError("Session backend did not open a session.")
+
+ yield sess
+ resp = app.response_class()
+
+ if app.session_interface.is_null_session(sess):
+ return
+
+ with ctx:
+ app.session_interface.save_session(app, sess, resp)
+
+ self._update_cookies_from_response(
+ ctx.request.host.partition(":")[0],
+ ctx.request.path,
+ resp.headers.getlist("Set-Cookie"),
+ )
+
+ def _copy_environ(self, other: WSGIEnvironment) -> WSGIEnvironment:
+ out = {**self.environ_base, **other}
+
+ if self.preserve_context:
+ out["werkzeug.debug.preserve_context"] = self._new_contexts.append
+
+ return out
+
+ def _request_from_builder_args(
+ self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any]
+ ) -> BaseRequest:
+ kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {}))
+ builder = EnvironBuilder(self.application, *args, **kwargs)
+
+ try:
+ return builder.get_request()
+ finally:
+ builder.close()
+
+ def open(
+ self,
+ *args: t.Any,
+ buffered: bool = False,
+ follow_redirects: bool = False,
+ **kwargs: t.Any,
+ ) -> TestResponse:
+ if args and isinstance(
+ args[0], (werkzeug.test.EnvironBuilder, dict, BaseRequest)
+ ):
+ if isinstance(args[0], werkzeug.test.EnvironBuilder):
+ builder = copy(args[0])
+ builder.environ_base = self._copy_environ(builder.environ_base or {}) # type: ignore[arg-type]
+ request = builder.get_request()
+ elif isinstance(args[0], dict):
+ request = EnvironBuilder.from_environ(
+ args[0], app=self.application, environ_base=self._copy_environ({})
+ ).get_request()
+ else:
+ # isinstance(args[0], BaseRequest)
+ request = copy(args[0])
+ request.environ = self._copy_environ(request.environ)
+ else:
+ # request is None
+ request = self._request_from_builder_args(args, kwargs)
+
+ # Pop any previously preserved contexts. This prevents contexts
+ # from being preserved across redirects or multiple requests
+ # within a single block.
+ self._context_stack.close()
+
+ response = super().open(
+ request,
+ buffered=buffered,
+ follow_redirects=follow_redirects,
+ )
+ response.json_module = self.application.json # type: ignore[assignment]
+
+ # Re-push contexts that were preserved during the request.
+ for cm in self._new_contexts:
+ self._context_stack.enter_context(cm)
+
+ self._new_contexts.clear()
+ return response
+
+ def __enter__(self) -> FlaskClient:
+ if self.preserve_context:
+ raise RuntimeError("Cannot nest client invocations")
+ self.preserve_context = True
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> None:
+ self.preserve_context = False
+ self._context_stack.close()
+
+
+class FlaskCliRunner(CliRunner):
+ """A :class:`~click.testing.CliRunner` for testing a Flask app's
+ CLI commands. Typically created using
+ :meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`.
+ """
+
+ def __init__(self, app: Flask, **kwargs: t.Any) -> None:
+ self.app = app
+ super().__init__(**kwargs)
+
+ def invoke( # type: ignore
+ self, cli: t.Any = None, args: t.Any = None, **kwargs: t.Any
+ ) -> Result:
+ """Invokes a CLI command in an isolated environment. See
+ :meth:`CliRunner.invoke ` for
+ full method documentation. See :ref:`testing-cli` for examples.
+
+ If the ``obj`` argument is not given, passes an instance of
+ :class:`~flask.cli.ScriptInfo` that knows how to load the Flask
+ app being tested.
+
+ :param cli: Command object to invoke. Default is the app's
+ :attr:`~flask.app.Flask.cli` group.
+ :param args: List of strings to invoke the command with.
+
+ :return: a :class:`~click.testing.Result` object.
+ """
+ if cli is None:
+ cli = self.app.cli
+
+ if "obj" not in kwargs:
+ kwargs["obj"] = ScriptInfo(create_app=lambda: self.app)
+
+ return super().invoke(cli, args, **kwargs)
diff --git a/venv/Lib/site-packages/flask/typing.py b/venv/Lib/site-packages/flask/typing.py
new file mode 100644
index 0000000..6b70c40
--- /dev/null
+++ b/venv/Lib/site-packages/flask/typing.py
@@ -0,0 +1,93 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import typing as t
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from _typeshed.wsgi import WSGIApplication # noqa: F401
+ from werkzeug.datastructures import Headers # noqa: F401
+ from werkzeug.sansio.response import Response # noqa: F401
+
+# The possible types that are directly convertible or are a Response object.
+ResponseValue = t.Union[
+ "Response",
+ str,
+ bytes,
+ list[t.Any],
+ # Only dict is actually accepted, but Mapping allows for TypedDict.
+ t.Mapping[str, t.Any],
+ t.Iterator[str],
+ t.Iterator[bytes],
+ cabc.AsyncIterable[str], # for Quart, until App is generic.
+ cabc.AsyncIterable[bytes],
+]
+
+# the possible types for an individual HTTP header
+# This should be a Union, but mypy doesn't pass unless it's a TypeVar.
+HeaderValue = t.Union[str, list[str], tuple[str, ...]]
+
+# the possible types for HTTP headers
+HeadersValue = t.Union[
+ "Headers",
+ t.Mapping[str, HeaderValue],
+ t.Sequence[tuple[str, HeaderValue]],
+]
+
+# The possible types returned by a route function.
+ResponseReturnValue = t.Union[
+ ResponseValue,
+ tuple[ResponseValue, HeadersValue],
+ tuple[ResponseValue, int],
+ tuple[ResponseValue, int, HeadersValue],
+ "WSGIApplication",
+]
+
+# Allow any subclass of werkzeug.Response, such as the one from Flask,
+# as a callback argument. Using werkzeug.Response directly makes a
+# callback annotated with flask.Response fail type checking.
+ResponseClass = t.TypeVar("ResponseClass", bound="Response")
+
+AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named
+AfterRequestCallable = t.Union[
+ t.Callable[[ResponseClass], ResponseClass],
+ t.Callable[[ResponseClass], t.Awaitable[ResponseClass]],
+]
+BeforeFirstRequestCallable = t.Union[
+ t.Callable[[], None], t.Callable[[], t.Awaitable[None]]
+]
+BeforeRequestCallable = t.Union[
+ t.Callable[[], t.Optional[ResponseReturnValue]],
+ t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]],
+]
+ShellContextProcessorCallable = t.Callable[[], dict[str, t.Any]]
+TeardownCallable = t.Union[
+ t.Callable[[t.Optional[BaseException]], None],
+ t.Callable[[t.Optional[BaseException]], t.Awaitable[None]],
+]
+TemplateContextProcessorCallable = t.Union[
+ t.Callable[[], dict[str, t.Any]],
+ t.Callable[[], t.Awaitable[dict[str, t.Any]]],
+]
+TemplateFilterCallable = t.Callable[..., t.Any]
+TemplateGlobalCallable = t.Callable[..., t.Any]
+TemplateTestCallable = t.Callable[..., bool]
+URLDefaultCallable = t.Callable[[str, dict[str, t.Any]], None]
+URLValuePreprocessorCallable = t.Callable[
+ [t.Optional[str], t.Optional[dict[str, t.Any]]], None
+]
+
+# This should take Exception, but that either breaks typing the argument
+# with a specific exception, or decorating multiple times with different
+# exceptions (and using a union type on the argument).
+# https://github.com/pallets/flask/issues/4095
+# https://github.com/pallets/flask/issues/4295
+# https://github.com/pallets/flask/issues/4297
+ErrorHandlerCallable = t.Union[
+ t.Callable[[t.Any], ResponseReturnValue],
+ t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]],
+]
+
+RouteCallable = t.Union[
+ t.Callable[..., ResponseReturnValue],
+ t.Callable[..., t.Awaitable[ResponseReturnValue]],
+]
diff --git a/venv/Lib/site-packages/flask/views.py b/venv/Lib/site-packages/flask/views.py
new file mode 100644
index 0000000..53fe976
--- /dev/null
+++ b/venv/Lib/site-packages/flask/views.py
@@ -0,0 +1,191 @@
+from __future__ import annotations
+
+import typing as t
+
+from . import typing as ft
+from .globals import current_app
+from .globals import request
+
+F = t.TypeVar("F", bound=t.Callable[..., t.Any])
+
+http_method_funcs = frozenset(
+ ["get", "post", "head", "options", "delete", "put", "trace", "patch"]
+)
+
+
+class View:
+ """Subclass this class and override :meth:`dispatch_request` to
+ create a generic class-based view. Call :meth:`as_view` to create a
+ view function that creates an instance of the class with the given
+ arguments and calls its ``dispatch_request`` method with any URL
+ variables.
+
+ See :doc:`views` for a detailed guide.
+
+ .. code-block:: python
+
+ class Hello(View):
+ init_every_request = False
+
+ def dispatch_request(self, name):
+ return f"Hello, {name}!"
+
+ app.add_url_rule(
+ "/hello/", view_func=Hello.as_view("hello")
+ )
+
+ Set :attr:`methods` on the class to change what methods the view
+ accepts.
+
+ Set :attr:`decorators` on the class to apply a list of decorators to
+ the generated view function. Decorators applied to the class itself
+ will not be applied to the generated view function!
+
+ Set :attr:`init_every_request` to ``False`` for efficiency, unless
+ you need to store request-global data on ``self``.
+ """
+
+ #: The methods this view is registered for. Uses the same default
+ #: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and
+ #: ``add_url_rule`` by default.
+ methods: t.ClassVar[t.Collection[str] | None] = None
+
+ #: Control whether the ``OPTIONS`` method is handled automatically.
+ #: Uses the same default (``True``) as ``route`` and
+ #: ``add_url_rule`` by default.
+ provide_automatic_options: t.ClassVar[bool | None] = None
+
+ #: A list of decorators to apply, in order, to the generated view
+ #: function. Remember that ``@decorator`` syntax is applied bottom
+ #: to top, so the first decorator in the list would be the bottom
+ #: decorator.
+ #:
+ #: .. versionadded:: 0.8
+ decorators: t.ClassVar[list[t.Callable[..., t.Any]]] = []
+
+ #: Create a new instance of this view class for every request by
+ #: default. If a view subclass sets this to ``False``, the same
+ #: instance is used for every request.
+ #:
+ #: A single instance is more efficient, especially if complex setup
+ #: is done during init. However, storing data on ``self`` is no
+ #: longer safe across requests, and :data:`~flask.g` should be used
+ #: instead.
+ #:
+ #: .. versionadded:: 2.2
+ init_every_request: t.ClassVar[bool] = True
+
+ def dispatch_request(self) -> ft.ResponseReturnValue:
+ """The actual view function behavior. Subclasses must override
+ this and return a valid response. Any variables from the URL
+ rule are passed as keyword arguments.
+ """
+ raise NotImplementedError()
+
+ @classmethod
+ def as_view(
+ cls, name: str, *class_args: t.Any, **class_kwargs: t.Any
+ ) -> ft.RouteCallable:
+ """Convert the class into a view function that can be registered
+ for a route.
+
+ By default, the generated view will create a new instance of the
+ view class for every request and call its
+ :meth:`dispatch_request` method. If the view class sets
+ :attr:`init_every_request` to ``False``, the same instance will
+ be used for every request.
+
+ Except for ``name``, all other arguments passed to this method
+ are forwarded to the view class ``__init__`` method.
+
+ .. versionchanged:: 2.2
+ Added the ``init_every_request`` class attribute.
+ """
+ if cls.init_every_request:
+
+ def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
+ self = view.view_class( # type: ignore[attr-defined]
+ *class_args, **class_kwargs
+ )
+ return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return]
+
+ else:
+ self = cls(*class_args, **class_kwargs) # pyright: ignore
+
+ def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
+ return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return]
+
+ if cls.decorators:
+ view.__name__ = name
+ view.__module__ = cls.__module__
+ for decorator in cls.decorators:
+ view = decorator(view)
+
+ # We attach the view class to the view function for two reasons:
+ # first of all it allows us to easily figure out what class-based
+ # view this thing came from, secondly it's also used for instantiating
+ # the view class so you can actually replace it with something else
+ # for testing purposes and debugging.
+ view.view_class = cls # type: ignore
+ view.__name__ = name
+ view.__doc__ = cls.__doc__
+ view.__module__ = cls.__module__
+ view.methods = cls.methods # type: ignore
+ view.provide_automatic_options = cls.provide_automatic_options # type: ignore
+ return view
+
+
+class MethodView(View):
+ """Dispatches request methods to the corresponding instance methods.
+ For example, if you implement a ``get`` method, it will be used to
+ handle ``GET`` requests.
+
+ This can be useful for defining a REST API.
+
+ :attr:`methods` is automatically set based on the methods defined on
+ the class.
+
+ See :doc:`views` for a detailed guide.
+
+ .. code-block:: python
+
+ class CounterAPI(MethodView):
+ def get(self):
+ return str(session.get("counter", 0))
+
+ def post(self):
+ session["counter"] = session.get("counter", 0) + 1
+ return redirect(url_for("counter"))
+
+ app.add_url_rule(
+ "/counter", view_func=CounterAPI.as_view("counter")
+ )
+ """
+
+ def __init_subclass__(cls, **kwargs: t.Any) -> None:
+ super().__init_subclass__(**kwargs)
+
+ if "methods" not in cls.__dict__:
+ methods = set()
+
+ for base in cls.__bases__:
+ if getattr(base, "methods", None):
+ methods.update(base.methods) # type: ignore[attr-defined]
+
+ for key in http_method_funcs:
+ if hasattr(cls, key):
+ methods.add(key.upper())
+
+ if methods:
+ cls.methods = methods
+
+ def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue:
+ meth = getattr(self, request.method.lower(), None)
+
+ # If the request method is HEAD and we don't have a handler for it
+ # retry with GET.
+ if meth is None and request.method == "HEAD":
+ meth = getattr(self, "get", None)
+
+ assert meth is not None, f"Unimplemented method {request.method!r}"
+ return current_app.ensure_sync(meth)(**kwargs) # type: ignore[no-any-return]
diff --git a/venv/Lib/site-packages/flask/wrappers.py b/venv/Lib/site-packages/flask/wrappers.py
new file mode 100644
index 0000000..bab6102
--- /dev/null
+++ b/venv/Lib/site-packages/flask/wrappers.py
@@ -0,0 +1,257 @@
+from __future__ import annotations
+
+import typing as t
+
+from werkzeug.exceptions import BadRequest
+from werkzeug.exceptions import HTTPException
+from werkzeug.wrappers import Request as RequestBase
+from werkzeug.wrappers import Response as ResponseBase
+
+from . import json
+from .globals import current_app
+from .helpers import _split_blueprint_path
+
+if t.TYPE_CHECKING: # pragma: no cover
+ from werkzeug.routing import Rule
+
+
+class Request(RequestBase):
+ """The request object used by default in Flask. Remembers the
+ matched endpoint and view arguments.
+
+ It is what ends up as :class:`~flask.request`. If you want to replace
+ the request object used you can subclass this and set
+ :attr:`~flask.Flask.request_class` to your subclass.
+
+ The request object is a :class:`~werkzeug.wrappers.Request` subclass and
+ provides all of the attributes Werkzeug defines plus a few Flask
+ specific ones.
+ """
+
+ json_module: t.Any = json
+
+ #: The internal URL rule that matched the request. This can be
+ #: useful to inspect which methods are allowed for the URL from
+ #: a before/after handler (``request.url_rule.methods``) etc.
+ #: Though if the request's method was invalid for the URL rule,
+ #: the valid list is available in ``routing_exception.valid_methods``
+ #: instead (an attribute of the Werkzeug exception
+ #: :exc:`~werkzeug.exceptions.MethodNotAllowed`)
+ #: because the request was never internally bound.
+ #:
+ #: .. versionadded:: 0.6
+ url_rule: Rule | None = None
+
+ #: A dict of view arguments that matched the request. If an exception
+ #: happened when matching, this will be ``None``.
+ view_args: dict[str, t.Any] | None = None
+
+ #: If matching the URL failed, this is the exception that will be
+ #: raised / was raised as part of the request handling. This is
+ #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or
+ #: something similar.
+ routing_exception: HTTPException | None = None
+
+ _max_content_length: int | None = None
+ _max_form_memory_size: int | None = None
+ _max_form_parts: int | None = None
+
+ @property
+ def max_content_length(self) -> int | None:
+ """The maximum number of bytes that will be read during this request. If
+ this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge`
+ error is raised. If it is set to ``None``, no limit is enforced at the
+ Flask application level. However, if it is ``None`` and the request has
+ no ``Content-Length`` header and the WSGI server does not indicate that
+ it terminates the stream, then no data is read to avoid an infinite
+ stream.
+
+ Each request defaults to the :data:`MAX_CONTENT_LENGTH` config, which
+ defaults to ``None``. It can be set on a specific ``request`` to apply
+ the limit to that specific view. This should be set appropriately based
+ on an application's or view's specific needs.
+
+ .. versionchanged:: 3.1
+ This can be set per-request.
+
+ .. versionchanged:: 0.6
+ This is configurable through Flask config.
+ """
+ if self._max_content_length is not None:
+ return self._max_content_length
+
+ if not current_app:
+ return super().max_content_length
+
+ return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return]
+
+ @max_content_length.setter
+ def max_content_length(self, value: int | None) -> None:
+ self._max_content_length = value
+
+ @property
+ def max_form_memory_size(self) -> int | None:
+ """The maximum size in bytes any non-file form field may be in a
+ ``multipart/form-data`` body. If this limit is exceeded, a 413
+ :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it
+ is set to ``None``, no limit is enforced at the Flask application level.
+
+ Each request defaults to the :data:`MAX_FORM_MEMORY_SIZE` config, which
+ defaults to ``500_000``. It can be set on a specific ``request`` to
+ apply the limit to that specific view. This should be set appropriately
+ based on an application's or view's specific needs.
+
+ .. versionchanged:: 3.1
+ This is configurable through Flask config.
+ """
+ if self._max_form_memory_size is not None:
+ return self._max_form_memory_size
+
+ if not current_app:
+ return super().max_form_memory_size
+
+ return current_app.config["MAX_FORM_MEMORY_SIZE"] # type: ignore[no-any-return]
+
+ @max_form_memory_size.setter
+ def max_form_memory_size(self, value: int | None) -> None:
+ self._max_form_memory_size = value
+
+ @property # type: ignore[override]
+ def max_form_parts(self) -> int | None:
+ """The maximum number of fields that may be present in a
+ ``multipart/form-data`` body. If this limit is exceeded, a 413
+ :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it
+ is set to ``None``, no limit is enforced at the Flask application level.
+
+ Each request defaults to the :data:`MAX_FORM_PARTS` config, which
+ defaults to ``1_000``. It can be set on a specific ``request`` to apply
+ the limit to that specific view. This should be set appropriately based
+ on an application's or view's specific needs.
+
+ .. versionchanged:: 3.1
+ This is configurable through Flask config.
+ """
+ if self._max_form_parts is not None:
+ return self._max_form_parts
+
+ if not current_app:
+ return super().max_form_parts
+
+ return current_app.config["MAX_FORM_PARTS"] # type: ignore[no-any-return]
+
+ @max_form_parts.setter
+ def max_form_parts(self, value: int | None) -> None:
+ self._max_form_parts = value
+
+ @property
+ def endpoint(self) -> str | None:
+ """The endpoint that matched the request URL.
+
+ This will be ``None`` if matching failed or has not been
+ performed yet.
+
+ This in combination with :attr:`view_args` can be used to
+ reconstruct the same URL or a modified URL.
+ """
+ if self.url_rule is not None:
+ return self.url_rule.endpoint # type: ignore[no-any-return]
+
+ return None
+
+ @property
+ def blueprint(self) -> str | None:
+ """The registered name of the current blueprint.
+
+ This will be ``None`` if the endpoint is not part of a
+ blueprint, or if URL matching failed or has not been performed
+ yet.
+
+ This does not necessarily match the name the blueprint was
+ created with. It may have been nested, or registered with a
+ different name.
+ """
+ endpoint = self.endpoint
+
+ if endpoint is not None and "." in endpoint:
+ return endpoint.rpartition(".")[0]
+
+ return None
+
+ @property
+ def blueprints(self) -> list[str]:
+ """The registered names of the current blueprint upwards through
+ parent blueprints.
+
+ This will be an empty list if there is no current blueprint, or
+ if URL matching failed.
+
+ .. versionadded:: 2.0.1
+ """
+ name = self.blueprint
+
+ if name is None:
+ return []
+
+ return _split_blueprint_path(name)
+
+ def _load_form_data(self) -> None:
+ super()._load_form_data()
+
+ # In debug mode we're replacing the files multidict with an ad-hoc
+ # subclass that raises a different error for key errors.
+ if (
+ current_app
+ and current_app.debug
+ and self.mimetype != "multipart/form-data"
+ and not self.files
+ ):
+ from .debughelpers import attach_enctype_error_multidict
+
+ attach_enctype_error_multidict(self)
+
+ def on_json_loading_failed(self, e: ValueError | None) -> t.Any:
+ try:
+ return super().on_json_loading_failed(e)
+ except BadRequest as ebr:
+ if current_app and current_app.debug:
+ raise
+
+ raise BadRequest() from ebr
+
+
+class Response(ResponseBase):
+ """The response object that is used by default in Flask. Works like the
+ response object from Werkzeug but is set to have an HTML mimetype by
+ default. Quite often you don't have to create this object yourself because
+ :meth:`~flask.Flask.make_response` will take care of that for you.
+
+ If you want to replace the response object used you can subclass this and
+ set :attr:`~flask.Flask.response_class` to your subclass.
+
+ .. versionchanged:: 1.0
+ JSON support is added to the response, like the request. This is useful
+ when testing to get the test client response data as JSON.
+
+ .. versionchanged:: 1.0
+
+ Added :attr:`max_cookie_size`.
+ """
+
+ default_mimetype: str | None = "text/html"
+
+ json_module = json
+
+ autocorrect_location_header = False
+
+ @property
+ def max_cookie_size(self) -> int: # type: ignore
+ """Read-only view of the :data:`MAX_COOKIE_SIZE` config key.
+
+ See :attr:`~werkzeug.wrappers.Response.max_cookie_size` in
+ Werkzeug's docs.
+ """
+ if current_app:
+ return current_app.config["MAX_COOKIE_SIZE"] # type: ignore[no-any-return]
+
+ # return Werkzeug's default when not in an app context
+ return super().max_cookie_size
diff --git a/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt
new file mode 100644
index 0000000..7b190ca
--- /dev/null
+++ b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt
@@ -0,0 +1,28 @@
+Copyright 2011 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/METADATA b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/METADATA
new file mode 100644
index 0000000..ddf5464
--- /dev/null
+++ b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/METADATA
@@ -0,0 +1,60 @@
+Metadata-Version: 2.1
+Name: itsdangerous
+Version: 2.2.0
+Summary: Safely pass data to untrusted environments and back.
+Maintainer-email: Pallets
+Requires-Python: >=3.8
+Description-Content-Type: text/markdown
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Typing :: Typed
+Project-URL: Changes, https://itsdangerous.palletsprojects.com/changes/
+Project-URL: Chat, https://discord.gg/pallets
+Project-URL: Documentation, https://itsdangerous.palletsprojects.com/
+Project-URL: Donate, https://palletsprojects.com/donate
+Project-URL: Source, https://github.com/pallets/itsdangerous/
+
+# ItsDangerous
+
+... so better sign this
+
+Various helpers to pass data to untrusted environments and to get it
+back safe and sound. Data is cryptographically signed to ensure that a
+token has not been tampered with.
+
+It's possible to customize how data is serialized. Data is compressed as
+needed. A timestamp can be added and verified automatically while
+loading a token.
+
+
+## A Simple Example
+
+Here's how you could generate a token for transmitting a user's id and
+name between web requests.
+
+```python
+from itsdangerous import URLSafeSerializer
+auth_s = URLSafeSerializer("secret key", "auth")
+token = auth_s.dumps({"id": 5, "name": "itsdangerous"})
+
+print(token)
+# eyJpZCI6NSwibmFtZSI6Iml0c2Rhbmdlcm91cyJ9.6YP6T0BaO67XP--9UzTrmurXSmg
+
+data = auth_s.loads(token)
+print(data["name"])
+# itsdangerous
+```
+
+
+## Donate
+
+The Pallets organization develops and supports ItsDangerous and other
+popular packages. In order to grow the community of contributors and
+users, and allow the maintainers to devote more time to the projects,
+[please donate today][].
+
+[please donate today]: https://palletsprojects.com/donate
+
diff --git a/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/RECORD b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/RECORD
new file mode 100644
index 0000000..e937872
--- /dev/null
+++ b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/RECORD
@@ -0,0 +1,22 @@
+itsdangerous-2.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+itsdangerous-2.2.0.dist-info/LICENSE.txt,sha256=Y68JiRtr6K0aQlLtQ68PTvun_JSOIoNnvtfzxa4LCdc,1475
+itsdangerous-2.2.0.dist-info/METADATA,sha256=0rk0-1ZwihuU5DnwJVwPWoEI4yWOyCexih3JyZHblhE,1924
+itsdangerous-2.2.0.dist-info/RECORD,,
+itsdangerous-2.2.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
+itsdangerous/__init__.py,sha256=4SK75sCe29xbRgQE1ZQtMHnKUuZYAf3bSpZOrff1IAY,1427
+itsdangerous/__pycache__/__init__.cpython-310.pyc,,
+itsdangerous/__pycache__/_json.cpython-310.pyc,,
+itsdangerous/__pycache__/encoding.cpython-310.pyc,,
+itsdangerous/__pycache__/exc.cpython-310.pyc,,
+itsdangerous/__pycache__/serializer.cpython-310.pyc,,
+itsdangerous/__pycache__/signer.cpython-310.pyc,,
+itsdangerous/__pycache__/timed.cpython-310.pyc,,
+itsdangerous/__pycache__/url_safe.cpython-310.pyc,,
+itsdangerous/_json.py,sha256=wPQGmge2yZ9328EHKF6gadGeyGYCJQKxtU-iLKE6UnA,473
+itsdangerous/encoding.py,sha256=wwTz5q_3zLcaAdunk6_vSoStwGqYWe307Zl_U87aRFM,1409
+itsdangerous/exc.py,sha256=Rr3exo0MRFEcPZltwecyK16VV1bE2K9_F1-d-ljcUn4,3201
+itsdangerous/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+itsdangerous/serializer.py,sha256=PmdwADLqkSyQLZ0jOKAgDsAW4k_H0TlA71Ei3z0C5aI,15601
+itsdangerous/signer.py,sha256=YO0CV7NBvHA6j549REHJFUjUojw2pHqwcUpQnU7yNYQ,9647
+itsdangerous/timed.py,sha256=6RvDMqNumGMxf0-HlpaZdN9PUQQmRvrQGplKhxuivUs,8083
+itsdangerous/url_safe.py,sha256=az4e5fXi_vs-YbWj8YZwn4wiVKfeD--GEKRT5Ueu4P4,2505
diff --git a/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/WHEEL b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/WHEEL
new file mode 100644
index 0000000..3b5e64b
--- /dev/null
+++ b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 1.0
+Generator: flit 3.9.0
+Root-Is-Purelib: true
+Tag: py3-none-any
diff --git a/venv/Lib/site-packages/itsdangerous/__init__.py b/venv/Lib/site-packages/itsdangerous/__init__.py
new file mode 100644
index 0000000..ea55256
--- /dev/null
+++ b/venv/Lib/site-packages/itsdangerous/__init__.py
@@ -0,0 +1,38 @@
+from __future__ import annotations
+
+import typing as t
+
+from .encoding import base64_decode as base64_decode
+from .encoding import base64_encode as base64_encode
+from .encoding import want_bytes as want_bytes
+from .exc import BadData as BadData
+from .exc import BadHeader as BadHeader
+from .exc import BadPayload as BadPayload
+from .exc import BadSignature as BadSignature
+from .exc import BadTimeSignature as BadTimeSignature
+from .exc import SignatureExpired as SignatureExpired
+from .serializer import Serializer as Serializer
+from .signer import HMACAlgorithm as HMACAlgorithm
+from .signer import NoneAlgorithm as NoneAlgorithm
+from .signer import Signer as Signer
+from .timed import TimedSerializer as TimedSerializer
+from .timed import TimestampSigner as TimestampSigner
+from .url_safe import URLSafeSerializer as URLSafeSerializer
+from .url_safe import URLSafeTimedSerializer as URLSafeTimedSerializer
+
+
+def __getattr__(name: str) -> t.Any:
+ if name == "__version__":
+ import importlib.metadata
+ import warnings
+
+ warnings.warn(
+ "The '__version__' attribute is deprecated and will be removed in"
+ " ItsDangerous 2.3. Use feature detection or"
+ " 'importlib.metadata.version(\"itsdangerous\")' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return importlib.metadata.version("itsdangerous")
+
+ raise AttributeError(name)
diff --git a/venv/Lib/site-packages/itsdangerous/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/itsdangerous/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..a9ffacd
Binary files /dev/null and b/venv/Lib/site-packages/itsdangerous/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/itsdangerous/__pycache__/_json.cpython-310.pyc b/venv/Lib/site-packages/itsdangerous/__pycache__/_json.cpython-310.pyc
new file mode 100644
index 0000000..4c11976
Binary files /dev/null and b/venv/Lib/site-packages/itsdangerous/__pycache__/_json.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/itsdangerous/__pycache__/encoding.cpython-310.pyc b/venv/Lib/site-packages/itsdangerous/__pycache__/encoding.cpython-310.pyc
new file mode 100644
index 0000000..63867ce
Binary files /dev/null and b/venv/Lib/site-packages/itsdangerous/__pycache__/encoding.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/itsdangerous/__pycache__/exc.cpython-310.pyc b/venv/Lib/site-packages/itsdangerous/__pycache__/exc.cpython-310.pyc
new file mode 100644
index 0000000..84f9916
Binary files /dev/null and b/venv/Lib/site-packages/itsdangerous/__pycache__/exc.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/itsdangerous/__pycache__/serializer.cpython-310.pyc b/venv/Lib/site-packages/itsdangerous/__pycache__/serializer.cpython-310.pyc
new file mode 100644
index 0000000..135e9da
Binary files /dev/null and b/venv/Lib/site-packages/itsdangerous/__pycache__/serializer.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/itsdangerous/__pycache__/signer.cpython-310.pyc b/venv/Lib/site-packages/itsdangerous/__pycache__/signer.cpython-310.pyc
new file mode 100644
index 0000000..f631193
Binary files /dev/null and b/venv/Lib/site-packages/itsdangerous/__pycache__/signer.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/itsdangerous/__pycache__/timed.cpython-310.pyc b/venv/Lib/site-packages/itsdangerous/__pycache__/timed.cpython-310.pyc
new file mode 100644
index 0000000..a8a1b25
Binary files /dev/null and b/venv/Lib/site-packages/itsdangerous/__pycache__/timed.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/itsdangerous/__pycache__/url_safe.cpython-310.pyc b/venv/Lib/site-packages/itsdangerous/__pycache__/url_safe.cpython-310.pyc
new file mode 100644
index 0000000..ded2fab
Binary files /dev/null and b/venv/Lib/site-packages/itsdangerous/__pycache__/url_safe.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/itsdangerous/_json.py b/venv/Lib/site-packages/itsdangerous/_json.py
new file mode 100644
index 0000000..fc23fea
--- /dev/null
+++ b/venv/Lib/site-packages/itsdangerous/_json.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import json as _json
+import typing as t
+
+
+class _CompactJSON:
+ """Wrapper around json module that strips whitespace."""
+
+ @staticmethod
+ def loads(payload: str | bytes) -> t.Any:
+ return _json.loads(payload)
+
+ @staticmethod
+ def dumps(obj: t.Any, **kwargs: t.Any) -> str:
+ kwargs.setdefault("ensure_ascii", False)
+ kwargs.setdefault("separators", (",", ":"))
+ return _json.dumps(obj, **kwargs)
diff --git a/venv/Lib/site-packages/itsdangerous/encoding.py b/venv/Lib/site-packages/itsdangerous/encoding.py
new file mode 100644
index 0000000..f5ca80f
--- /dev/null
+++ b/venv/Lib/site-packages/itsdangerous/encoding.py
@@ -0,0 +1,54 @@
+from __future__ import annotations
+
+import base64
+import string
+import struct
+import typing as t
+
+from .exc import BadData
+
+
+def want_bytes(
+ s: str | bytes, encoding: str = "utf-8", errors: str = "strict"
+) -> bytes:
+ if isinstance(s, str):
+ s = s.encode(encoding, errors)
+
+ return s
+
+
+def base64_encode(string: str | bytes) -> bytes:
+ """Base64 encode a string of bytes or text. The resulting bytes are
+ safe to use in URLs.
+ """
+ string = want_bytes(string)
+ return base64.urlsafe_b64encode(string).rstrip(b"=")
+
+
+def base64_decode(string: str | bytes) -> bytes:
+ """Base64 decode a URL-safe string of bytes or text. The result is
+ bytes.
+ """
+ string = want_bytes(string, encoding="ascii", errors="ignore")
+ string += b"=" * (-len(string) % 4)
+
+ try:
+ return base64.urlsafe_b64decode(string)
+ except (TypeError, ValueError) as e:
+ raise BadData("Invalid base64-encoded data") from e
+
+
+# The alphabet used by base64.urlsafe_*
+_base64_alphabet = f"{string.ascii_letters}{string.digits}-_=".encode("ascii")
+
+_int64_struct = struct.Struct(">Q")
+_int_to_bytes = _int64_struct.pack
+_bytes_to_int = t.cast("t.Callable[[bytes], tuple[int]]", _int64_struct.unpack)
+
+
+def int_to_bytes(num: int) -> bytes:
+ return _int_to_bytes(num).lstrip(b"\x00")
+
+
+def bytes_to_int(bytestr: bytes) -> int:
+ return _bytes_to_int(bytestr.rjust(8, b"\x00"))[0]
diff --git a/venv/Lib/site-packages/itsdangerous/exc.py b/venv/Lib/site-packages/itsdangerous/exc.py
new file mode 100644
index 0000000..a75adcd
--- /dev/null
+++ b/venv/Lib/site-packages/itsdangerous/exc.py
@@ -0,0 +1,106 @@
+from __future__ import annotations
+
+import typing as t
+from datetime import datetime
+
+
+class BadData(Exception):
+ """Raised if bad data of any sort was encountered. This is the base
+ for all exceptions that ItsDangerous defines.
+
+ .. versionadded:: 0.15
+ """
+
+ def __init__(self, message: str):
+ super().__init__(message)
+ self.message = message
+
+ def __str__(self) -> str:
+ return self.message
+
+
+class BadSignature(BadData):
+ """Raised if a signature does not match."""
+
+ def __init__(self, message: str, payload: t.Any | None = None):
+ super().__init__(message)
+
+ #: The payload that failed the signature test. In some
+ #: situations you might still want to inspect this, even if
+ #: you know it was tampered with.
+ #:
+ #: .. versionadded:: 0.14
+ self.payload: t.Any | None = payload
+
+
+class BadTimeSignature(BadSignature):
+ """Raised if a time-based signature is invalid. This is a subclass
+ of :class:`BadSignature`.
+ """
+
+ def __init__(
+ self,
+ message: str,
+ payload: t.Any | None = None,
+ date_signed: datetime | None = None,
+ ):
+ super().__init__(message, payload)
+
+ #: If the signature expired this exposes the date of when the
+ #: signature was created. This can be helpful in order to
+ #: tell the user how long a link has been gone stale.
+ #:
+ #: .. versionchanged:: 2.0
+ #: The datetime value is timezone-aware rather than naive.
+ #:
+ #: .. versionadded:: 0.14
+ self.date_signed = date_signed
+
+
+class SignatureExpired(BadTimeSignature):
+ """Raised if a signature timestamp is older than ``max_age``. This
+ is a subclass of :exc:`BadTimeSignature`.
+ """
+
+
+class BadHeader(BadSignature):
+ """Raised if a signed header is invalid in some form. This only
+ happens for serializers that have a header that goes with the
+ signature.
+
+ .. versionadded:: 0.24
+ """
+
+ def __init__(
+ self,
+ message: str,
+ payload: t.Any | None = None,
+ header: t.Any | None = None,
+ original_error: Exception | None = None,
+ ):
+ super().__init__(message, payload)
+
+ #: If the header is actually available but just malformed it
+ #: might be stored here.
+ self.header: t.Any | None = header
+
+ #: If available, the error that indicates why the payload was
+ #: not valid. This might be ``None``.
+ self.original_error: Exception | None = original_error
+
+
+class BadPayload(BadData):
+ """Raised if a payload is invalid. This could happen if the payload
+ is loaded despite an invalid signature, or if there is a mismatch
+ between the serializer and deserializer. The original exception
+ that occurred during loading is stored on as :attr:`original_error`.
+
+ .. versionadded:: 0.15
+ """
+
+ def __init__(self, message: str, original_error: Exception | None = None):
+ super().__init__(message)
+
+ #: If available, the error that indicates why the payload was
+ #: not valid. This might be ``None``.
+ self.original_error: Exception | None = original_error
diff --git a/venv/Lib/site-packages/itsdangerous/py.typed b/venv/Lib/site-packages/itsdangerous/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/itsdangerous/serializer.py b/venv/Lib/site-packages/itsdangerous/serializer.py
new file mode 100644
index 0000000..5ddf387
--- /dev/null
+++ b/venv/Lib/site-packages/itsdangerous/serializer.py
@@ -0,0 +1,406 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import json
+import typing as t
+
+from .encoding import want_bytes
+from .exc import BadPayload
+from .exc import BadSignature
+from .signer import _make_keys_list
+from .signer import Signer
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+ # This should be either be str or bytes. To avoid having to specify the
+ # bound type, it falls back to a union if structural matching fails.
+ _TSerialized = te.TypeVar(
+ "_TSerialized", bound=t.Union[str, bytes], default=t.Union[str, bytes]
+ )
+else:
+ # Still available at runtime on Python < 3.13, but without the default.
+ _TSerialized = t.TypeVar("_TSerialized", bound=t.Union[str, bytes])
+
+
+class _PDataSerializer(t.Protocol[_TSerialized]):
+ def loads(self, payload: _TSerialized, /) -> t.Any: ...
+ # A signature with additional arguments is not handled correctly by type
+ # checkers right now, so an overload is used below for serializers that
+ # don't match this strict protocol.
+ def dumps(self, obj: t.Any, /) -> _TSerialized: ...
+
+
+# Use TypeIs once it's available in typing_extensions or 3.13.
+def is_text_serializer(
+ serializer: _PDataSerializer[t.Any],
+) -> te.TypeGuard[_PDataSerializer[str]]:
+ """Checks whether a serializer generates text or binary."""
+ return isinstance(serializer.dumps({}), str)
+
+
+class Serializer(t.Generic[_TSerialized]):
+ """A serializer wraps a :class:`~itsdangerous.signer.Signer` to
+ enable serializing and securely signing data other than bytes. It
+ can unsign to verify that the data hasn't been changed.
+
+ The serializer provides :meth:`dumps` and :meth:`loads`, similar to
+ :mod:`json`, and by default uses :mod:`json` internally to serialize
+ the data to bytes.
+
+ The secret key should be a random string of ``bytes`` and should not
+ be saved to code or version control. Different salts should be used
+ to distinguish signing in different contexts. See :doc:`/concepts`
+ for information about the security of the secret key and salt.
+
+ :param secret_key: The secret key to sign and verify with. Can be a
+ list of keys, oldest to newest, to support key rotation.
+ :param salt: Extra key to combine with ``secret_key`` to distinguish
+ signatures in different contexts.
+ :param serializer: An object that provides ``dumps`` and ``loads``
+ methods for serializing data to a string. Defaults to
+ :attr:`default_serializer`, which defaults to :mod:`json`.
+ :param serializer_kwargs: Keyword arguments to pass when calling
+ ``serializer.dumps``.
+ :param signer: A ``Signer`` class to instantiate when signing data.
+ Defaults to :attr:`default_signer`, which defaults to
+ :class:`~itsdangerous.signer.Signer`.
+ :param signer_kwargs: Keyword arguments to pass when instantiating
+ the ``Signer`` class.
+ :param fallback_signers: List of signer parameters to try when
+ unsigning with the default signer fails. Each item can be a dict
+ of ``signer_kwargs``, a ``Signer`` class, or a tuple of
+ ``(signer, signer_kwargs)``. Defaults to
+ :attr:`default_fallback_signers`.
+
+ .. versionchanged:: 2.0
+ Added support for key rotation by passing a list to
+ ``secret_key``.
+
+ .. versionchanged:: 2.0
+ Removed the default SHA-512 fallback signer from
+ ``default_fallback_signers``.
+
+ .. versionchanged:: 1.1
+ Added support for ``fallback_signers`` and configured a default
+ SHA-512 fallback. This fallback is for users who used the yanked
+ 1.0.0 release which defaulted to SHA-512.
+
+ .. versionchanged:: 0.14
+ The ``signer`` and ``signer_kwargs`` parameters were added to
+ the constructor.
+ """
+
+ #: The default serialization module to use to serialize data to a
+ #: string internally. The default is :mod:`json`, but can be changed
+ #: to any object that provides ``dumps`` and ``loads`` methods.
+ default_serializer: _PDataSerializer[t.Any] = json
+
+ #: The default ``Signer`` class to instantiate when signing data.
+ #: The default is :class:`itsdangerous.signer.Signer`.
+ default_signer: type[Signer] = Signer
+
+ #: The default fallback signers to try when unsigning fails.
+ default_fallback_signers: list[
+ dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer]
+ ] = []
+
+ # Serializer[str] if no data serializer is provided, or if it returns str.
+ @t.overload
+ def __init__(
+ self: Serializer[str],
+ secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes],
+ salt: str | bytes | None = b"itsdangerous",
+ serializer: None | _PDataSerializer[str] = None,
+ serializer_kwargs: dict[str, t.Any] | None = None,
+ signer: type[Signer] | None = None,
+ signer_kwargs: dict[str, t.Any] | None = None,
+ fallback_signers: list[
+ dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer]
+ ]
+ | None = None,
+ ): ...
+
+ # Serializer[bytes] with a bytes data serializer positional argument.
+ @t.overload
+ def __init__(
+ self: Serializer[bytes],
+ secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes],
+ salt: str | bytes | None,
+ serializer: _PDataSerializer[bytes],
+ serializer_kwargs: dict[str, t.Any] | None = None,
+ signer: type[Signer] | None = None,
+ signer_kwargs: dict[str, t.Any] | None = None,
+ fallback_signers: list[
+ dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer]
+ ]
+ | None = None,
+ ): ...
+
+ # Serializer[bytes] with a bytes data serializer keyword argument.
+ @t.overload
+ def __init__(
+ self: Serializer[bytes],
+ secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes],
+ salt: str | bytes | None = b"itsdangerous",
+ *,
+ serializer: _PDataSerializer[bytes],
+ serializer_kwargs: dict[str, t.Any] | None = None,
+ signer: type[Signer] | None = None,
+ signer_kwargs: dict[str, t.Any] | None = None,
+ fallback_signers: list[
+ dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer]
+ ]
+ | None = None,
+ ): ...
+
+ # Fall back with a positional argument. If the strict signature of
+ # _PDataSerializer doesn't match, fall back to a union, requiring the user
+ # to specify the type.
+ @t.overload
+ def __init__(
+ self,
+ secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes],
+ salt: str | bytes | None,
+ serializer: t.Any,
+ serializer_kwargs: dict[str, t.Any] | None = None,
+ signer: type[Signer] | None = None,
+ signer_kwargs: dict[str, t.Any] | None = None,
+ fallback_signers: list[
+ dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer]
+ ]
+ | None = None,
+ ): ...
+
+ # Fall back with a keyword argument.
+ @t.overload
+ def __init__(
+ self,
+ secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes],
+ salt: str | bytes | None = b"itsdangerous",
+ *,
+ serializer: t.Any,
+ serializer_kwargs: dict[str, t.Any] | None = None,
+ signer: type[Signer] | None = None,
+ signer_kwargs: dict[str, t.Any] | None = None,
+ fallback_signers: list[
+ dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer]
+ ]
+ | None = None,
+ ): ...
+
+ def __init__(
+ self,
+ secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes],
+ salt: str | bytes | None = b"itsdangerous",
+ serializer: t.Any | None = None,
+ serializer_kwargs: dict[str, t.Any] | None = None,
+ signer: type[Signer] | None = None,
+ signer_kwargs: dict[str, t.Any] | None = None,
+ fallback_signers: list[
+ dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer]
+ ]
+ | None = None,
+ ):
+ #: The list of secret keys to try for verifying signatures, from
+ #: oldest to newest. The newest (last) key is used for signing.
+ #:
+ #: This allows a key rotation system to keep a list of allowed
+ #: keys and remove expired ones.
+ self.secret_keys: list[bytes] = _make_keys_list(secret_key)
+
+ if salt is not None:
+ salt = want_bytes(salt)
+ # if salt is None then the signer's default is used
+
+ self.salt = salt
+
+ if serializer is None:
+ serializer = self.default_serializer
+
+ self.serializer: _PDataSerializer[_TSerialized] = serializer
+ self.is_text_serializer: bool = is_text_serializer(serializer)
+
+ if signer is None:
+ signer = self.default_signer
+
+ self.signer: type[Signer] = signer
+ self.signer_kwargs: dict[str, t.Any] = signer_kwargs or {}
+
+ if fallback_signers is None:
+ fallback_signers = list(self.default_fallback_signers)
+
+ self.fallback_signers: list[
+ dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer]
+ ] = fallback_signers
+ self.serializer_kwargs: dict[str, t.Any] = serializer_kwargs or {}
+
+ @property
+ def secret_key(self) -> bytes:
+ """The newest (last) entry in the :attr:`secret_keys` list. This
+ is for compatibility from before key rotation support was added.
+ """
+ return self.secret_keys[-1]
+
+ def load_payload(
+ self, payload: bytes, serializer: _PDataSerializer[t.Any] | None = None
+ ) -> t.Any:
+ """Loads the encoded object. This function raises
+ :class:`.BadPayload` if the payload is not valid. The
+ ``serializer`` parameter can be used to override the serializer
+ stored on the class. The encoded ``payload`` should always be
+ bytes.
+ """
+ if serializer is None:
+ use_serializer = self.serializer
+ is_text = self.is_text_serializer
+ else:
+ use_serializer = serializer
+ is_text = is_text_serializer(serializer)
+
+ try:
+ if is_text:
+ return use_serializer.loads(payload.decode("utf-8")) # type: ignore[arg-type]
+
+ return use_serializer.loads(payload) # type: ignore[arg-type]
+ except Exception as e:
+ raise BadPayload(
+ "Could not load the payload because an exception"
+ " occurred on unserializing the data.",
+ original_error=e,
+ ) from e
+
+ def dump_payload(self, obj: t.Any) -> bytes:
+ """Dumps the encoded object. The return value is always bytes.
+ If the internal serializer returns text, the value will be
+ encoded as UTF-8.
+ """
+ return want_bytes(self.serializer.dumps(obj, **self.serializer_kwargs))
+
+ def make_signer(self, salt: str | bytes | None = None) -> Signer:
+ """Creates a new instance of the signer to be used. The default
+ implementation uses the :class:`.Signer` base class.
+ """
+ if salt is None:
+ salt = self.salt
+
+ return self.signer(self.secret_keys, salt=salt, **self.signer_kwargs)
+
+ def iter_unsigners(self, salt: str | bytes | None = None) -> cabc.Iterator[Signer]:
+ """Iterates over all signers to be tried for unsigning. Starts
+ with the configured signer, then constructs each signer
+ specified in ``fallback_signers``.
+ """
+ if salt is None:
+ salt = self.salt
+
+ yield self.make_signer(salt)
+
+ for fallback in self.fallback_signers:
+ if isinstance(fallback, dict):
+ kwargs = fallback
+ fallback = self.signer
+ elif isinstance(fallback, tuple):
+ fallback, kwargs = fallback
+ else:
+ kwargs = self.signer_kwargs
+
+ for secret_key in self.secret_keys:
+ yield fallback(secret_key, salt=salt, **kwargs)
+
+ def dumps(self, obj: t.Any, salt: str | bytes | None = None) -> _TSerialized:
+ """Returns a signed string serialized with the internal
+ serializer. The return value can be either a byte or unicode
+ string depending on the format of the internal serializer.
+ """
+ payload = want_bytes(self.dump_payload(obj))
+ rv = self.make_signer(salt).sign(payload)
+
+ if self.is_text_serializer:
+ return rv.decode("utf-8") # type: ignore[return-value]
+
+ return rv # type: ignore[return-value]
+
+ def dump(self, obj: t.Any, f: t.IO[t.Any], salt: str | bytes | None = None) -> None:
+ """Like :meth:`dumps` but dumps into a file. The file handle has
+ to be compatible with what the internal serializer expects.
+ """
+ f.write(self.dumps(obj, salt))
+
+ def loads(
+ self, s: str | bytes, salt: str | bytes | None = None, **kwargs: t.Any
+ ) -> t.Any:
+ """Reverse of :meth:`dumps`. Raises :exc:`.BadSignature` if the
+ signature validation fails.
+ """
+ s = want_bytes(s)
+ last_exception = None
+
+ for signer in self.iter_unsigners(salt):
+ try:
+ return self.load_payload(signer.unsign(s))
+ except BadSignature as err:
+ last_exception = err
+
+ raise t.cast(BadSignature, last_exception)
+
+ def load(self, f: t.IO[t.Any], salt: str | bytes | None = None) -> t.Any:
+ """Like :meth:`loads` but loads from a file."""
+ return self.loads(f.read(), salt)
+
+ def loads_unsafe(
+ self, s: str | bytes, salt: str | bytes | None = None
+ ) -> tuple[bool, t.Any]:
+ """Like :meth:`loads` but without verifying the signature. This
+ is potentially very dangerous to use depending on how your
+ serializer works. The return value is ``(signature_valid,
+ payload)`` instead of just the payload. The first item will be a
+ boolean that indicates if the signature is valid. This function
+ never fails.
+
+ Use it for debugging only and if you know that your serializer
+ module is not exploitable (for example, do not use it with a
+ pickle serializer).
+
+ .. versionadded:: 0.15
+ """
+ return self._loads_unsafe_impl(s, salt)
+
+ def _loads_unsafe_impl(
+ self,
+ s: str | bytes,
+ salt: str | bytes | None,
+ load_kwargs: dict[str, t.Any] | None = None,
+ load_payload_kwargs: dict[str, t.Any] | None = None,
+ ) -> tuple[bool, t.Any]:
+ """Low level helper function to implement :meth:`loads_unsafe`
+ in serializer subclasses.
+ """
+ if load_kwargs is None:
+ load_kwargs = {}
+
+ try:
+ return True, self.loads(s, salt=salt, **load_kwargs)
+ except BadSignature as e:
+ if e.payload is None:
+ return False, None
+
+ if load_payload_kwargs is None:
+ load_payload_kwargs = {}
+
+ try:
+ return (
+ False,
+ self.load_payload(e.payload, **load_payload_kwargs),
+ )
+ except BadPayload:
+ return False, None
+
+ def load_unsafe(
+ self, f: t.IO[t.Any], salt: str | bytes | None = None
+ ) -> tuple[bool, t.Any]:
+ """Like :meth:`loads_unsafe` but loads from a file.
+
+ .. versionadded:: 0.15
+ """
+ return self.loads_unsafe(f.read(), salt=salt)
diff --git a/venv/Lib/site-packages/itsdangerous/signer.py b/venv/Lib/site-packages/itsdangerous/signer.py
new file mode 100644
index 0000000..e324dc0
--- /dev/null
+++ b/venv/Lib/site-packages/itsdangerous/signer.py
@@ -0,0 +1,266 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import hashlib
+import hmac
+import typing as t
+
+from .encoding import _base64_alphabet
+from .encoding import base64_decode
+from .encoding import base64_encode
+from .encoding import want_bytes
+from .exc import BadSignature
+
+
+class SigningAlgorithm:
+ """Subclasses must implement :meth:`get_signature` to provide
+ signature generation functionality.
+ """
+
+ def get_signature(self, key: bytes, value: bytes) -> bytes:
+ """Returns the signature for the given key and value."""
+ raise NotImplementedError()
+
+ def verify_signature(self, key: bytes, value: bytes, sig: bytes) -> bool:
+ """Verifies the given signature matches the expected
+ signature.
+ """
+ return hmac.compare_digest(sig, self.get_signature(key, value))
+
+
+class NoneAlgorithm(SigningAlgorithm):
+ """Provides an algorithm that does not perform any signing and
+ returns an empty signature.
+ """
+
+ def get_signature(self, key: bytes, value: bytes) -> bytes:
+ return b""
+
+
+def _lazy_sha1(string: bytes = b"") -> t.Any:
+ """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include
+ SHA-1, in which case the import and use as a default would fail before the
+ developer can configure something else.
+ """
+ return hashlib.sha1(string)
+
+
+class HMACAlgorithm(SigningAlgorithm):
+ """Provides signature generation using HMACs."""
+
+ #: The digest method to use with the MAC algorithm. This defaults to
+ #: SHA1, but can be changed to any other function in the hashlib
+ #: module.
+ default_digest_method: t.Any = staticmethod(_lazy_sha1)
+
+ def __init__(self, digest_method: t.Any = None):
+ if digest_method is None:
+ digest_method = self.default_digest_method
+
+ self.digest_method: t.Any = digest_method
+
+ def get_signature(self, key: bytes, value: bytes) -> bytes:
+ mac = hmac.new(key, msg=value, digestmod=self.digest_method)
+ return mac.digest()
+
+
+def _make_keys_list(
+ secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes],
+) -> list[bytes]:
+ if isinstance(secret_key, (str, bytes)):
+ return [want_bytes(secret_key)]
+
+ return [want_bytes(s) for s in secret_key] # pyright: ignore
+
+
+class Signer:
+ """A signer securely signs bytes, then unsigns them to verify that
+ the value hasn't been changed.
+
+ The secret key should be a random string of ``bytes`` and should not
+ be saved to code or version control. Different salts should be used
+ to distinguish signing in different contexts. See :doc:`/concepts`
+ for information about the security of the secret key and salt.
+
+ :param secret_key: The secret key to sign and verify with. Can be a
+ list of keys, oldest to newest, to support key rotation.
+ :param salt: Extra key to combine with ``secret_key`` to distinguish
+ signatures in different contexts.
+ :param sep: Separator between the signature and value.
+ :param key_derivation: How to derive the signing key from the secret
+ key and salt. Possible values are ``concat``, ``django-concat``,
+ or ``hmac``. Defaults to :attr:`default_key_derivation`, which
+ defaults to ``django-concat``.
+ :param digest_method: Hash function to use when generating the HMAC
+ signature. Defaults to :attr:`default_digest_method`, which
+ defaults to :func:`hashlib.sha1`. Note that the security of the
+ hash alone doesn't apply when used intermediately in HMAC.
+ :param algorithm: A :class:`SigningAlgorithm` instance to use
+ instead of building a default :class:`HMACAlgorithm` with the
+ ``digest_method``.
+
+ .. versionchanged:: 2.0
+ Added support for key rotation by passing a list to
+ ``secret_key``.
+
+ .. versionchanged:: 0.18
+ ``algorithm`` was added as an argument to the class constructor.
+
+ .. versionchanged:: 0.14
+ ``key_derivation`` and ``digest_method`` were added as arguments
+ to the class constructor.
+ """
+
+ #: The default digest method to use for the signer. The default is
+ #: :func:`hashlib.sha1`, but can be changed to any :mod:`hashlib` or
+ #: compatible object. Note that the security of the hash alone
+ #: doesn't apply when used intermediately in HMAC.
+ #:
+ #: .. versionadded:: 0.14
+ default_digest_method: t.Any = staticmethod(_lazy_sha1)
+
+ #: The default scheme to use to derive the signing key from the
+ #: secret key and salt. The default is ``django-concat``. Possible
+ #: values are ``concat``, ``django-concat``, and ``hmac``.
+ #:
+ #: .. versionadded:: 0.14
+ default_key_derivation: str = "django-concat"
+
+ def __init__(
+ self,
+ secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes],
+ salt: str | bytes | None = b"itsdangerous.Signer",
+ sep: str | bytes = b".",
+ key_derivation: str | None = None,
+ digest_method: t.Any | None = None,
+ algorithm: SigningAlgorithm | None = None,
+ ):
+ #: The list of secret keys to try for verifying signatures, from
+ #: oldest to newest. The newest (last) key is used for signing.
+ #:
+ #: This allows a key rotation system to keep a list of allowed
+ #: keys and remove expired ones.
+ self.secret_keys: list[bytes] = _make_keys_list(secret_key)
+ self.sep: bytes = want_bytes(sep)
+
+ if self.sep in _base64_alphabet:
+ raise ValueError(
+ "The given separator cannot be used because it may be"
+ " contained in the signature itself. ASCII letters,"
+ " digits, and '-_=' must not be used."
+ )
+
+ if salt is not None:
+ salt = want_bytes(salt)
+ else:
+ salt = b"itsdangerous.Signer"
+
+ self.salt = salt
+
+ if key_derivation is None:
+ key_derivation = self.default_key_derivation
+
+ self.key_derivation: str = key_derivation
+
+ if digest_method is None:
+ digest_method = self.default_digest_method
+
+ self.digest_method: t.Any = digest_method
+
+ if algorithm is None:
+ algorithm = HMACAlgorithm(self.digest_method)
+
+ self.algorithm: SigningAlgorithm = algorithm
+
+ @property
+ def secret_key(self) -> bytes:
+ """The newest (last) entry in the :attr:`secret_keys` list. This
+ is for compatibility from before key rotation support was added.
+ """
+ return self.secret_keys[-1]
+
+ def derive_key(self, secret_key: str | bytes | None = None) -> bytes:
+ """This method is called to derive the key. The default key
+ derivation choices can be overridden here. Key derivation is not
+ intended to be used as a security method to make a complex key
+ out of a short password. Instead you should use large random
+ secret keys.
+
+ :param secret_key: A specific secret key to derive from.
+ Defaults to the last item in :attr:`secret_keys`.
+
+ .. versionchanged:: 2.0
+ Added the ``secret_key`` parameter.
+ """
+ if secret_key is None:
+ secret_key = self.secret_keys[-1]
+ else:
+ secret_key = want_bytes(secret_key)
+
+ if self.key_derivation == "concat":
+ return t.cast(bytes, self.digest_method(self.salt + secret_key).digest())
+ elif self.key_derivation == "django-concat":
+ return t.cast(
+ bytes, self.digest_method(self.salt + b"signer" + secret_key).digest()
+ )
+ elif self.key_derivation == "hmac":
+ mac = hmac.new(secret_key, digestmod=self.digest_method)
+ mac.update(self.salt)
+ return mac.digest()
+ elif self.key_derivation == "none":
+ return secret_key
+ else:
+ raise TypeError("Unknown key derivation method")
+
+ def get_signature(self, value: str | bytes) -> bytes:
+ """Returns the signature for the given value."""
+ value = want_bytes(value)
+ key = self.derive_key()
+ sig = self.algorithm.get_signature(key, value)
+ return base64_encode(sig)
+
+ def sign(self, value: str | bytes) -> bytes:
+ """Signs the given string."""
+ value = want_bytes(value)
+ return value + self.sep + self.get_signature(value)
+
+ def verify_signature(self, value: str | bytes, sig: str | bytes) -> bool:
+ """Verifies the signature for the given value."""
+ try:
+ sig = base64_decode(sig)
+ except Exception:
+ return False
+
+ value = want_bytes(value)
+
+ for secret_key in reversed(self.secret_keys):
+ key = self.derive_key(secret_key)
+
+ if self.algorithm.verify_signature(key, value, sig):
+ return True
+
+ return False
+
+ def unsign(self, signed_value: str | bytes) -> bytes:
+ """Unsigns the given string."""
+ signed_value = want_bytes(signed_value)
+
+ if self.sep not in signed_value:
+ raise BadSignature(f"No {self.sep!r} found in value")
+
+ value, sig = signed_value.rsplit(self.sep, 1)
+
+ if self.verify_signature(value, sig):
+ return value
+
+ raise BadSignature(f"Signature {sig!r} does not match", payload=value)
+
+ def validate(self, signed_value: str | bytes) -> bool:
+ """Only validates the given signed value. Returns ``True`` if
+ the signature exists and is valid.
+ """
+ try:
+ self.unsign(signed_value)
+ return True
+ except BadSignature:
+ return False
diff --git a/venv/Lib/site-packages/itsdangerous/timed.py b/venv/Lib/site-packages/itsdangerous/timed.py
new file mode 100644
index 0000000..7384375
--- /dev/null
+++ b/venv/Lib/site-packages/itsdangerous/timed.py
@@ -0,0 +1,228 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import time
+import typing as t
+from datetime import datetime
+from datetime import timezone
+
+from .encoding import base64_decode
+from .encoding import base64_encode
+from .encoding import bytes_to_int
+from .encoding import int_to_bytes
+from .encoding import want_bytes
+from .exc import BadSignature
+from .exc import BadTimeSignature
+from .exc import SignatureExpired
+from .serializer import _TSerialized
+from .serializer import Serializer
+from .signer import Signer
+
+
+class TimestampSigner(Signer):
+ """Works like the regular :class:`.Signer` but also records the time
+ of the signing and can be used to expire signatures. The
+ :meth:`unsign` method can raise :exc:`.SignatureExpired` if the
+ unsigning failed because the signature is expired.
+ """
+
+ def get_timestamp(self) -> int:
+ """Returns the current timestamp. The function must return an
+ integer.
+ """
+ return int(time.time())
+
+ def timestamp_to_datetime(self, ts: int) -> datetime:
+ """Convert the timestamp from :meth:`get_timestamp` into an
+ aware :class`datetime.datetime` in UTC.
+
+ .. versionchanged:: 2.0
+ The timestamp is returned as a timezone-aware ``datetime``
+ in UTC rather than a naive ``datetime`` assumed to be UTC.
+ """
+ return datetime.fromtimestamp(ts, tz=timezone.utc)
+
+ def sign(self, value: str | bytes) -> bytes:
+ """Signs the given string and also attaches time information."""
+ value = want_bytes(value)
+ timestamp = base64_encode(int_to_bytes(self.get_timestamp()))
+ sep = want_bytes(self.sep)
+ value = value + sep + timestamp
+ return value + sep + self.get_signature(value)
+
+ # Ignore overlapping signatures check, return_timestamp is the only
+ # parameter that affects the return type.
+
+ @t.overload
+ def unsign( # type: ignore[overload-overlap]
+ self,
+ signed_value: str | bytes,
+ max_age: int | None = None,
+ return_timestamp: t.Literal[False] = False,
+ ) -> bytes: ...
+
+ @t.overload
+ def unsign(
+ self,
+ signed_value: str | bytes,
+ max_age: int | None = None,
+ return_timestamp: t.Literal[True] = True,
+ ) -> tuple[bytes, datetime]: ...
+
+ def unsign(
+ self,
+ signed_value: str | bytes,
+ max_age: int | None = None,
+ return_timestamp: bool = False,
+ ) -> tuple[bytes, datetime] | bytes:
+ """Works like the regular :meth:`.Signer.unsign` but can also
+ validate the time. See the base docstring of the class for
+ the general behavior. If ``return_timestamp`` is ``True`` the
+ timestamp of the signature will be returned as an aware
+ :class:`datetime.datetime` object in UTC.
+
+ .. versionchanged:: 2.0
+ The timestamp is returned as a timezone-aware ``datetime``
+ in UTC rather than a naive ``datetime`` assumed to be UTC.
+ """
+ try:
+ result = super().unsign(signed_value)
+ sig_error = None
+ except BadSignature as e:
+ sig_error = e
+ result = e.payload or b""
+
+ sep = want_bytes(self.sep)
+
+ # If there is no timestamp in the result there is something
+ # seriously wrong. In case there was a signature error, we raise
+ # that one directly, otherwise we have a weird situation in
+ # which we shouldn't have come except someone uses a time-based
+ # serializer on non-timestamp data, so catch that.
+ if sep not in result:
+ if sig_error:
+ raise sig_error
+
+ raise BadTimeSignature("timestamp missing", payload=result)
+
+ value, ts_bytes = result.rsplit(sep, 1)
+ ts_int: int | None = None
+ ts_dt: datetime | None = None
+
+ try:
+ ts_int = bytes_to_int(base64_decode(ts_bytes))
+ except Exception:
+ pass
+
+ # Signature is *not* okay. Raise a proper error now that we have
+ # split the value and the timestamp.
+ if sig_error is not None:
+ if ts_int is not None:
+ try:
+ ts_dt = self.timestamp_to_datetime(ts_int)
+ except (ValueError, OSError, OverflowError) as exc:
+ # Windows raises OSError
+ # 32-bit raises OverflowError
+ raise BadTimeSignature(
+ "Malformed timestamp", payload=value
+ ) from exc
+
+ raise BadTimeSignature(str(sig_error), payload=value, date_signed=ts_dt)
+
+ # Signature was okay but the timestamp is actually not there or
+ # malformed. Should not happen, but we handle it anyway.
+ if ts_int is None:
+ raise BadTimeSignature("Malformed timestamp", payload=value)
+
+ # Check timestamp is not older than max_age
+ if max_age is not None:
+ age = self.get_timestamp() - ts_int
+
+ if age > max_age:
+ raise SignatureExpired(
+ f"Signature age {age} > {max_age} seconds",
+ payload=value,
+ date_signed=self.timestamp_to_datetime(ts_int),
+ )
+
+ if age < 0:
+ raise SignatureExpired(
+ f"Signature age {age} < 0 seconds",
+ payload=value,
+ date_signed=self.timestamp_to_datetime(ts_int),
+ )
+
+ if return_timestamp:
+ return value, self.timestamp_to_datetime(ts_int)
+
+ return value
+
+ def validate(self, signed_value: str | bytes, max_age: int | None = None) -> bool:
+ """Only validates the given signed value. Returns ``True`` if
+ the signature exists and is valid."""
+ try:
+ self.unsign(signed_value, max_age=max_age)
+ return True
+ except BadSignature:
+ return False
+
+
+class TimedSerializer(Serializer[_TSerialized]):
+ """Uses :class:`TimestampSigner` instead of the default
+ :class:`.Signer`.
+ """
+
+ default_signer: type[TimestampSigner] = TimestampSigner
+
+ def iter_unsigners(
+ self, salt: str | bytes | None = None
+ ) -> cabc.Iterator[TimestampSigner]:
+ return t.cast("cabc.Iterator[TimestampSigner]", super().iter_unsigners(salt))
+
+ # TODO: Signature is incompatible because parameters were added
+ # before salt.
+
+ def loads( # type: ignore[override]
+ self,
+ s: str | bytes,
+ max_age: int | None = None,
+ return_timestamp: bool = False,
+ salt: str | bytes | None = None,
+ ) -> t.Any:
+ """Reverse of :meth:`dumps`, raises :exc:`.BadSignature` if the
+ signature validation fails. If a ``max_age`` is provided it will
+ ensure the signature is not older than that time in seconds. In
+ case the signature is outdated, :exc:`.SignatureExpired` is
+ raised. All arguments are forwarded to the signer's
+ :meth:`~TimestampSigner.unsign` method.
+ """
+ s = want_bytes(s)
+ last_exception = None
+
+ for signer in self.iter_unsigners(salt):
+ try:
+ base64d, timestamp = signer.unsign(
+ s, max_age=max_age, return_timestamp=True
+ )
+ payload = self.load_payload(base64d)
+
+ if return_timestamp:
+ return payload, timestamp
+
+ return payload
+ except SignatureExpired:
+ # The signature was unsigned successfully but was
+ # expired. Do not try the next signer.
+ raise
+ except BadSignature as err:
+ last_exception = err
+
+ raise t.cast(BadSignature, last_exception)
+
+ def loads_unsafe( # type: ignore[override]
+ self,
+ s: str | bytes,
+ max_age: int | None = None,
+ salt: str | bytes | None = None,
+ ) -> tuple[bool, t.Any]:
+ return self._loads_unsafe_impl(s, salt, load_kwargs={"max_age": max_age})
diff --git a/venv/Lib/site-packages/itsdangerous/url_safe.py b/venv/Lib/site-packages/itsdangerous/url_safe.py
new file mode 100644
index 0000000..56a0793
--- /dev/null
+++ b/venv/Lib/site-packages/itsdangerous/url_safe.py
@@ -0,0 +1,83 @@
+from __future__ import annotations
+
+import typing as t
+import zlib
+
+from ._json import _CompactJSON
+from .encoding import base64_decode
+from .encoding import base64_encode
+from .exc import BadPayload
+from .serializer import _PDataSerializer
+from .serializer import Serializer
+from .timed import TimedSerializer
+
+
+class URLSafeSerializerMixin(Serializer[str]):
+ """Mixed in with a regular serializer it will attempt to zlib
+ compress the string to make it shorter if necessary. It will also
+ base64 encode the string so that it can safely be placed in a URL.
+ """
+
+ default_serializer: _PDataSerializer[str] = _CompactJSON
+
+ def load_payload(
+ self,
+ payload: bytes,
+ *args: t.Any,
+ serializer: t.Any | None = None,
+ **kwargs: t.Any,
+ ) -> t.Any:
+ decompress = False
+
+ if payload.startswith(b"."):
+ payload = payload[1:]
+ decompress = True
+
+ try:
+ json = base64_decode(payload)
+ except Exception as e:
+ raise BadPayload(
+ "Could not base64 decode the payload because of an exception",
+ original_error=e,
+ ) from e
+
+ if decompress:
+ try:
+ json = zlib.decompress(json)
+ except Exception as e:
+ raise BadPayload(
+ "Could not zlib decompress the payload before decoding the payload",
+ original_error=e,
+ ) from e
+
+ return super().load_payload(json, *args, **kwargs)
+
+ def dump_payload(self, obj: t.Any) -> bytes:
+ json = super().dump_payload(obj)
+ is_compressed = False
+ compressed = zlib.compress(json)
+
+ if len(compressed) < (len(json) - 1):
+ json = compressed
+ is_compressed = True
+
+ base64d = base64_encode(json)
+
+ if is_compressed:
+ base64d = b"." + base64d
+
+ return base64d
+
+
+class URLSafeSerializer(URLSafeSerializerMixin, Serializer[str]):
+ """Works like :class:`.Serializer` but dumps and loads into a URL
+ safe string consisting of the upper and lowercase character of the
+ alphabet as well as ``'_'``, ``'-'`` and ``'.'``.
+ """
+
+
+class URLSafeTimedSerializer(URLSafeSerializerMixin, TimedSerializer[str]):
+ """Works like :class:`.TimedSerializer` but dumps and loads into a
+ URL safe string consisting of the upper and lowercase character of
+ the alphabet as well as ``'_'``, ``'-'`` and ``'.'``.
+ """
diff --git a/venv/Lib/site-packages/jinja2-3.1.6.dist-info/INSTALLER b/venv/Lib/site-packages/jinja2-3.1.6.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2-3.1.6.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/jinja2-3.1.6.dist-info/METADATA b/venv/Lib/site-packages/jinja2-3.1.6.dist-info/METADATA
new file mode 100644
index 0000000..ffef2ff
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2-3.1.6.dist-info/METADATA
@@ -0,0 +1,84 @@
+Metadata-Version: 2.4
+Name: Jinja2
+Version: 3.1.6
+Summary: A very fast and expressive template engine.
+Maintainer-email: Pallets
+Requires-Python: >=3.7
+Description-Content-Type: text/markdown
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Topic :: Text Processing :: Markup :: HTML
+Classifier: Typing :: Typed
+License-File: LICENSE.txt
+Requires-Dist: MarkupSafe>=2.0
+Requires-Dist: Babel>=2.7 ; extra == "i18n"
+Project-URL: Changes, https://jinja.palletsprojects.com/changes/
+Project-URL: Chat, https://discord.gg/pallets
+Project-URL: Documentation, https://jinja.palletsprojects.com/
+Project-URL: Donate, https://palletsprojects.com/donate
+Project-URL: Source, https://github.com/pallets/jinja/
+Provides-Extra: i18n
+
+# Jinja
+
+Jinja is a fast, expressive, extensible templating engine. Special
+placeholders in the template allow writing code similar to Python
+syntax. Then the template is passed data to render the final document.
+
+It includes:
+
+- Template inheritance and inclusion.
+- Define and import macros within templates.
+- HTML templates can use autoescaping to prevent XSS from untrusted
+ user input.
+- A sandboxed environment can safely render untrusted templates.
+- AsyncIO support for generating templates and calling async
+ functions.
+- I18N support with Babel.
+- Templates are compiled to optimized Python code just-in-time and
+ cached, or can be compiled ahead-of-time.
+- Exceptions point to the correct line in templates to make debugging
+ easier.
+- Extensible filters, tests, functions, and even syntax.
+
+Jinja's philosophy is that while application logic belongs in Python if
+possible, it shouldn't make the template designer's job difficult by
+restricting functionality too much.
+
+
+## In A Nutshell
+
+```jinja
+{% extends "base.html" %}
+{% block title %}Members{% endblock %}
+{% block content %}
+
+{% endblock %}
+```
+
+## Donate
+
+The Pallets organization develops and supports Jinja and other popular
+packages. In order to grow the community of contributors and users, and
+allow the maintainers to devote more time to the projects, [please
+donate today][].
+
+[please donate today]: https://palletsprojects.com/donate
+
+## Contributing
+
+See our [detailed contributing documentation][contrib] for many ways to
+contribute, including reporting issues, requesting features, asking or answering
+questions, and making PRs.
+
+[contrib]: https://palletsprojects.com/contributing/
+
diff --git a/venv/Lib/site-packages/jinja2-3.1.6.dist-info/RECORD b/venv/Lib/site-packages/jinja2-3.1.6.dist-info/RECORD
new file mode 100644
index 0000000..ad1fdcf
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2-3.1.6.dist-info/RECORD
@@ -0,0 +1,57 @@
+jinja2-3.1.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+jinja2-3.1.6.dist-info/METADATA,sha256=aMVUj7Z8QTKhOJjZsx7FDGvqKr3ZFdkh8hQ1XDpkmcg,2871
+jinja2-3.1.6.dist-info/RECORD,,
+jinja2-3.1.6.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82
+jinja2-3.1.6.dist-info/entry_points.txt,sha256=OL85gYU1eD8cuPlikifFngXpeBjaxl6rIJ8KkC_3r-I,58
+jinja2-3.1.6.dist-info/licenses/LICENSE.txt,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475
+jinja2/__init__.py,sha256=xxepO9i7DHsqkQrgBEduLtfoz2QCuT6_gbL4XSN1hbU,1928
+jinja2/__pycache__/__init__.cpython-310.pyc,,
+jinja2/__pycache__/_identifier.cpython-310.pyc,,
+jinja2/__pycache__/async_utils.cpython-310.pyc,,
+jinja2/__pycache__/bccache.cpython-310.pyc,,
+jinja2/__pycache__/compiler.cpython-310.pyc,,
+jinja2/__pycache__/constants.cpython-310.pyc,,
+jinja2/__pycache__/debug.cpython-310.pyc,,
+jinja2/__pycache__/defaults.cpython-310.pyc,,
+jinja2/__pycache__/environment.cpython-310.pyc,,
+jinja2/__pycache__/exceptions.cpython-310.pyc,,
+jinja2/__pycache__/ext.cpython-310.pyc,,
+jinja2/__pycache__/filters.cpython-310.pyc,,
+jinja2/__pycache__/idtracking.cpython-310.pyc,,
+jinja2/__pycache__/lexer.cpython-310.pyc,,
+jinja2/__pycache__/loaders.cpython-310.pyc,,
+jinja2/__pycache__/meta.cpython-310.pyc,,
+jinja2/__pycache__/nativetypes.cpython-310.pyc,,
+jinja2/__pycache__/nodes.cpython-310.pyc,,
+jinja2/__pycache__/optimizer.cpython-310.pyc,,
+jinja2/__pycache__/parser.cpython-310.pyc,,
+jinja2/__pycache__/runtime.cpython-310.pyc,,
+jinja2/__pycache__/sandbox.cpython-310.pyc,,
+jinja2/__pycache__/tests.cpython-310.pyc,,
+jinja2/__pycache__/utils.cpython-310.pyc,,
+jinja2/__pycache__/visitor.cpython-310.pyc,,
+jinja2/_identifier.py,sha256=_zYctNKzRqlk_murTNlzrju1FFJL7Va_Ijqqd7ii2lU,1958
+jinja2/async_utils.py,sha256=vK-PdsuorOMnWSnEkT3iUJRIkTnYgO2T6MnGxDgHI5o,2834
+jinja2/bccache.py,sha256=gh0qs9rulnXo0PhX5jTJy2UHzI8wFnQ63o_vw7nhzRg,14061
+jinja2/compiler.py,sha256=9RpCQl5X88BHllJiPsHPh295Hh0uApvwFJNQuutULeM,74131
+jinja2/constants.py,sha256=GMoFydBF_kdpaRKPoM5cl5MviquVRLVyZtfp5-16jg0,1433
+jinja2/debug.py,sha256=CnHqCDHd-BVGvti_8ZsTolnXNhA3ECsY-6n_2pwU8Hw,6297
+jinja2/defaults.py,sha256=boBcSw78h-lp20YbaXSJsqkAI2uN_mD_TtCydpeq5wU,1267
+jinja2/environment.py,sha256=9nhrP7Ch-NbGX00wvyr4yy-uhNHq2OCc60ggGrni_fk,61513
+jinja2/exceptions.py,sha256=ioHeHrWwCWNaXX1inHmHVblvc4haO7AXsjCp3GfWvx0,5071
+jinja2/ext.py,sha256=5PF5eHfh8mXAIxXHHRB2xXbXohi8pE3nHSOxa66uS7E,31875
+jinja2/filters.py,sha256=PQ_Egd9n9jSgtnGQYyF4K5j2nYwhUIulhPnyimkdr-k,55212
+jinja2/idtracking.py,sha256=-ll5lIp73pML3ErUYiIJj7tdmWxcH_IlDv3yA_hiZYo,10555
+jinja2/lexer.py,sha256=LYiYio6br-Tep9nPcupWXsPEtjluw3p1mU-lNBVRUfk,29786
+jinja2/loaders.py,sha256=wIrnxjvcbqh5VwW28NSkfotiDq8qNCxIOSFbGUiSLB4,24055
+jinja2/meta.py,sha256=OTDPkaFvU2Hgvx-6akz7154F8BIWaRmvJcBFvwopHww,4397
+jinja2/nativetypes.py,sha256=7GIGALVJgdyL80oZJdQUaUfwSt5q2lSSZbXt0dNf_M4,4210
+jinja2/nodes.py,sha256=m1Duzcr6qhZI8JQ6VyJgUNinjAf5bQzijSmDnMsvUx8,34579
+jinja2/optimizer.py,sha256=rJnCRlQ7pZsEEmMhsQDgC_pKyDHxP5TPS6zVPGsgcu8,1651
+jinja2/parser.py,sha256=lLOFy3sEmHc5IaEHRiH1sQVnId2moUQzhyeJZTtdY30,40383
+jinja2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+jinja2/runtime.py,sha256=gDk-GvdriJXqgsGbHgrcKTP0Yp6zPXzhzrIpCFH3jAU,34249
+jinja2/sandbox.py,sha256=Mw2aitlY2I8la7FYhcX2YG9BtUYcLnD0Gh3d29cDWrY,15009
+jinja2/tests.py,sha256=VLsBhVFnWg-PxSBz1MhRnNWgP1ovXk3neO1FLQMeC9Q,5926
+jinja2/utils.py,sha256=rRp3o9e7ZKS4fyrWRbELyLcpuGVTFcnooaOa1qx_FIk,24129
+jinja2/visitor.py,sha256=EcnL1PIwf_4RVCOMxsRNuR8AXHbS1qfAdMOE2ngKJz4,3557
diff --git a/venv/Lib/site-packages/jinja2-3.1.6.dist-info/WHEEL b/venv/Lib/site-packages/jinja2-3.1.6.dist-info/WHEEL
new file mode 100644
index 0000000..23d2d7e
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2-3.1.6.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 1.0
+Generator: flit 3.11.0
+Root-Is-Purelib: true
+Tag: py3-none-any
diff --git a/venv/Lib/site-packages/jinja2-3.1.6.dist-info/entry_points.txt b/venv/Lib/site-packages/jinja2-3.1.6.dist-info/entry_points.txt
new file mode 100644
index 0000000..abc3eae
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2-3.1.6.dist-info/entry_points.txt
@@ -0,0 +1,3 @@
+[babel.extractors]
+jinja2=jinja2.ext:babel_extract[i18n]
+
diff --git a/venv/Lib/site-packages/jinja2-3.1.6.dist-info/licenses/LICENSE.txt b/venv/Lib/site-packages/jinja2-3.1.6.dist-info/licenses/LICENSE.txt
new file mode 100644
index 0000000..c37cae4
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2-3.1.6.dist-info/licenses/LICENSE.txt
@@ -0,0 +1,28 @@
+Copyright 2007 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/venv/Lib/site-packages/jinja2/__init__.py b/venv/Lib/site-packages/jinja2/__init__.py
new file mode 100644
index 0000000..1a423a3
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2/__init__.py
@@ -0,0 +1,38 @@
+"""Jinja is a template engine written in pure Python. It provides a
+non-XML syntax that supports inline expressions and an optional
+sandboxed environment.
+"""
+
+from .bccache import BytecodeCache as BytecodeCache
+from .bccache import FileSystemBytecodeCache as FileSystemBytecodeCache
+from .bccache import MemcachedBytecodeCache as MemcachedBytecodeCache
+from .environment import Environment as Environment
+from .environment import Template as Template
+from .exceptions import TemplateAssertionError as TemplateAssertionError
+from .exceptions import TemplateError as TemplateError
+from .exceptions import TemplateNotFound as TemplateNotFound
+from .exceptions import TemplateRuntimeError as TemplateRuntimeError
+from .exceptions import TemplatesNotFound as TemplatesNotFound
+from .exceptions import TemplateSyntaxError as TemplateSyntaxError
+from .exceptions import UndefinedError as UndefinedError
+from .loaders import BaseLoader as BaseLoader
+from .loaders import ChoiceLoader as ChoiceLoader
+from .loaders import DictLoader as DictLoader
+from .loaders import FileSystemLoader as FileSystemLoader
+from .loaders import FunctionLoader as FunctionLoader
+from .loaders import ModuleLoader as ModuleLoader
+from .loaders import PackageLoader as PackageLoader
+from .loaders import PrefixLoader as PrefixLoader
+from .runtime import ChainableUndefined as ChainableUndefined
+from .runtime import DebugUndefined as DebugUndefined
+from .runtime import make_logging_undefined as make_logging_undefined
+from .runtime import StrictUndefined as StrictUndefined
+from .runtime import Undefined as Undefined
+from .utils import clear_caches as clear_caches
+from .utils import is_undefined as is_undefined
+from .utils import pass_context as pass_context
+from .utils import pass_environment as pass_environment
+from .utils import pass_eval_context as pass_eval_context
+from .utils import select_autoescape as select_autoescape
+
+__version__ = "3.1.6"
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..412a4db
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/_identifier.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/_identifier.cpython-310.pyc
new file mode 100644
index 0000000..988db82
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/_identifier.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/async_utils.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/async_utils.cpython-310.pyc
new file mode 100644
index 0000000..7630378
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/async_utils.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/bccache.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/bccache.cpython-310.pyc
new file mode 100644
index 0000000..c793cc3
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/bccache.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/compiler.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/compiler.cpython-310.pyc
new file mode 100644
index 0000000..1eac1a2
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/compiler.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/constants.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/constants.cpython-310.pyc
new file mode 100644
index 0000000..f50782f
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/constants.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/debug.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/debug.cpython-310.pyc
new file mode 100644
index 0000000..e6fc05f
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/debug.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/defaults.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/defaults.cpython-310.pyc
new file mode 100644
index 0000000..004e19c
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/defaults.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/environment.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/environment.cpython-310.pyc
new file mode 100644
index 0000000..e4983b3
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/environment.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/exceptions.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/exceptions.cpython-310.pyc
new file mode 100644
index 0000000..618119e
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/exceptions.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/ext.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/ext.cpython-310.pyc
new file mode 100644
index 0000000..2c83876
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/ext.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/filters.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/filters.cpython-310.pyc
new file mode 100644
index 0000000..4b47d15
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/filters.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/idtracking.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/idtracking.cpython-310.pyc
new file mode 100644
index 0000000..2641877
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/idtracking.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/lexer.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/lexer.cpython-310.pyc
new file mode 100644
index 0000000..14d4ef5
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/lexer.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/loaders.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/loaders.cpython-310.pyc
new file mode 100644
index 0000000..e9a77b5
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/loaders.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/meta.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/meta.cpython-310.pyc
new file mode 100644
index 0000000..966aab8
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/meta.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/nativetypes.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/nativetypes.cpython-310.pyc
new file mode 100644
index 0000000..3da517d
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/nativetypes.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/nodes.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/nodes.cpython-310.pyc
new file mode 100644
index 0000000..445d040
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/nodes.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/optimizer.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/optimizer.cpython-310.pyc
new file mode 100644
index 0000000..8c65e0c
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/optimizer.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/parser.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/parser.cpython-310.pyc
new file mode 100644
index 0000000..2d1c910
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/parser.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/runtime.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/runtime.cpython-310.pyc
new file mode 100644
index 0000000..bd9256a
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/runtime.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/sandbox.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/sandbox.cpython-310.pyc
new file mode 100644
index 0000000..f121dc3
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/sandbox.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/tests.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/tests.cpython-310.pyc
new file mode 100644
index 0000000..85ac020
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/tests.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/utils.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/utils.cpython-310.pyc
new file mode 100644
index 0000000..f8e19bb
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/utils.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/__pycache__/visitor.cpython-310.pyc b/venv/Lib/site-packages/jinja2/__pycache__/visitor.cpython-310.pyc
new file mode 100644
index 0000000..806756a
Binary files /dev/null and b/venv/Lib/site-packages/jinja2/__pycache__/visitor.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/jinja2/_identifier.py b/venv/Lib/site-packages/jinja2/_identifier.py
new file mode 100644
index 0000000..928c150
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2/_identifier.py
@@ -0,0 +1,6 @@
+import re
+
+# generated by scripts/generate_identifier_pattern.py
+pattern = re.compile(
+ r"[\w·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߽߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛࣓-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣ৾ਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣૺ-૿ଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఄా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഀ-ഃ഻഼ാ-ൄെ-ൈൊ-്ൗൢൣංඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ູົຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭ᳲ-᳴᳷-᳹᷀-᷹᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-ꣅ꣠-꣱ꣿꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𐴤-𐽆𐴧-𐽐𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑄴𑅅𑅆𑅳𑆀-𑆂𑆳-𑇀𑇉-𑇌𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌻𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑑞𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑠬-𑠺𑨁-𑨊𑨳-𑨹𑨻-𑨾𑩇𑩑-𑩛𑪊-𑪙𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𑴱-𑴶𑴺𑴼𑴽𑴿-𑵅𑵇𑶊-𑶎𑶐𑶑𑶓-𑶗𑻳-𑻶𖫰-𖫴𖬰-𖬶𖽑-𖽾𖾏-𖾒𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞣐𞀪-𞣖𞥄-𞥊󠄀-󠇯]+" # noqa: B950
+)
diff --git a/venv/Lib/site-packages/jinja2/async_utils.py b/venv/Lib/site-packages/jinja2/async_utils.py
new file mode 100644
index 0000000..f0c1402
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2/async_utils.py
@@ -0,0 +1,99 @@
+import inspect
+import typing as t
+from functools import WRAPPER_ASSIGNMENTS
+from functools import wraps
+
+from .utils import _PassArg
+from .utils import pass_eval_context
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+V = t.TypeVar("V")
+
+
+def async_variant(normal_func): # type: ignore
+ def decorator(async_func): # type: ignore
+ pass_arg = _PassArg.from_obj(normal_func)
+ need_eval_context = pass_arg is None
+
+ if pass_arg is _PassArg.environment:
+
+ def is_async(args: t.Any) -> bool:
+ return t.cast(bool, args[0].is_async)
+
+ else:
+
+ def is_async(args: t.Any) -> bool:
+ return t.cast(bool, args[0].environment.is_async)
+
+ # Take the doc and annotations from the sync function, but the
+ # name from the async function. Pallets-Sphinx-Themes
+ # build_function_directive expects __wrapped__ to point to the
+ # sync function.
+ async_func_attrs = ("__module__", "__name__", "__qualname__")
+ normal_func_attrs = tuple(set(WRAPPER_ASSIGNMENTS).difference(async_func_attrs))
+
+ @wraps(normal_func, assigned=normal_func_attrs)
+ @wraps(async_func, assigned=async_func_attrs, updated=())
+ def wrapper(*args, **kwargs): # type: ignore
+ b = is_async(args)
+
+ if need_eval_context:
+ args = args[1:]
+
+ if b:
+ return async_func(*args, **kwargs)
+
+ return normal_func(*args, **kwargs)
+
+ if need_eval_context:
+ wrapper = pass_eval_context(wrapper)
+
+ wrapper.jinja_async_variant = True # type: ignore[attr-defined]
+ return wrapper
+
+ return decorator
+
+
+_common_primitives = {int, float, bool, str, list, dict, tuple, type(None)}
+
+
+async def auto_await(value: t.Union[t.Awaitable["V"], "V"]) -> "V":
+ # Avoid a costly call to isawaitable
+ if type(value) in _common_primitives:
+ return t.cast("V", value)
+
+ if inspect.isawaitable(value):
+ return await t.cast("t.Awaitable[V]", value)
+
+ return value
+
+
+class _IteratorToAsyncIterator(t.Generic[V]):
+ def __init__(self, iterator: "t.Iterator[V]"):
+ self._iterator = iterator
+
+ def __aiter__(self) -> "te.Self":
+ return self
+
+ async def __anext__(self) -> V:
+ try:
+ return next(self._iterator)
+ except StopIteration as e:
+ raise StopAsyncIteration(e.value) from e
+
+
+def auto_aiter(
+ iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
+) -> "t.AsyncIterator[V]":
+ if hasattr(iterable, "__aiter__"):
+ return iterable.__aiter__()
+ else:
+ return _IteratorToAsyncIterator(iter(iterable))
+
+
+async def auto_to_list(
+ value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
+) -> t.List["V"]:
+ return [x async for x in auto_aiter(value)]
diff --git a/venv/Lib/site-packages/jinja2/bccache.py b/venv/Lib/site-packages/jinja2/bccache.py
new file mode 100644
index 0000000..ada8b09
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2/bccache.py
@@ -0,0 +1,408 @@
+"""The optional bytecode cache system. This is useful if you have very
+complex template situations and the compilation of all those templates
+slows down your application too much.
+
+Situations where this is useful are often forking web applications that
+are initialized on the first request.
+"""
+
+import errno
+import fnmatch
+import marshal
+import os
+import pickle
+import stat
+import sys
+import tempfile
+import typing as t
+from hashlib import sha1
+from io import BytesIO
+from types import CodeType
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+ from .environment import Environment
+
+ class _MemcachedClient(te.Protocol):
+ def get(self, key: str) -> bytes: ...
+
+ def set(
+ self, key: str, value: bytes, timeout: t.Optional[int] = None
+ ) -> None: ...
+
+
+bc_version = 5
+# Magic bytes to identify Jinja bytecode cache files. Contains the
+# Python major and minor version to avoid loading incompatible bytecode
+# if a project upgrades its Python version.
+bc_magic = (
+ b"j2"
+ + pickle.dumps(bc_version, 2)
+ + pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1], 2)
+)
+
+
+class Bucket:
+ """Buckets are used to store the bytecode for one template. It's created
+ and initialized by the bytecode cache and passed to the loading functions.
+
+ The buckets get an internal checksum from the cache assigned and use this
+ to automatically reject outdated cache material. Individual bytecode
+ cache subclasses don't have to care about cache invalidation.
+ """
+
+ def __init__(self, environment: "Environment", key: str, checksum: str) -> None:
+ self.environment = environment
+ self.key = key
+ self.checksum = checksum
+ self.reset()
+
+ def reset(self) -> None:
+ """Resets the bucket (unloads the bytecode)."""
+ self.code: t.Optional[CodeType] = None
+
+ def load_bytecode(self, f: t.BinaryIO) -> None:
+ """Loads bytecode from a file or file like object."""
+ # make sure the magic header is correct
+ magic = f.read(len(bc_magic))
+ if magic != bc_magic:
+ self.reset()
+ return
+ # the source code of the file changed, we need to reload
+ checksum = pickle.load(f)
+ if self.checksum != checksum:
+ self.reset()
+ return
+ # if marshal_load fails then we need to reload
+ try:
+ self.code = marshal.load(f)
+ except (EOFError, ValueError, TypeError):
+ self.reset()
+ return
+
+ def write_bytecode(self, f: t.IO[bytes]) -> None:
+ """Dump the bytecode into the file or file like object passed."""
+ if self.code is None:
+ raise TypeError("can't write empty bucket")
+ f.write(bc_magic)
+ pickle.dump(self.checksum, f, 2)
+ marshal.dump(self.code, f)
+
+ def bytecode_from_string(self, string: bytes) -> None:
+ """Load bytecode from bytes."""
+ self.load_bytecode(BytesIO(string))
+
+ def bytecode_to_string(self) -> bytes:
+ """Return the bytecode as bytes."""
+ out = BytesIO()
+ self.write_bytecode(out)
+ return out.getvalue()
+
+
+class BytecodeCache:
+ """To implement your own bytecode cache you have to subclass this class
+ and override :meth:`load_bytecode` and :meth:`dump_bytecode`. Both of
+ these methods are passed a :class:`~jinja2.bccache.Bucket`.
+
+ A very basic bytecode cache that saves the bytecode on the file system::
+
+ from os import path
+
+ class MyCache(BytecodeCache):
+
+ def __init__(self, directory):
+ self.directory = directory
+
+ def load_bytecode(self, bucket):
+ filename = path.join(self.directory, bucket.key)
+ if path.exists(filename):
+ with open(filename, 'rb') as f:
+ bucket.load_bytecode(f)
+
+ def dump_bytecode(self, bucket):
+ filename = path.join(self.directory, bucket.key)
+ with open(filename, 'wb') as f:
+ bucket.write_bytecode(f)
+
+ A more advanced version of a filesystem based bytecode cache is part of
+ Jinja.
+ """
+
+ def load_bytecode(self, bucket: Bucket) -> None:
+ """Subclasses have to override this method to load bytecode into a
+ bucket. If they are not able to find code in the cache for the
+ bucket, it must not do anything.
+ """
+ raise NotImplementedError()
+
+ def dump_bytecode(self, bucket: Bucket) -> None:
+ """Subclasses have to override this method to write the bytecode
+ from a bucket back to the cache. If it unable to do so it must not
+ fail silently but raise an exception.
+ """
+ raise NotImplementedError()
+
+ def clear(self) -> None:
+ """Clears the cache. This method is not used by Jinja but should be
+ implemented to allow applications to clear the bytecode cache used
+ by a particular environment.
+ """
+
+ def get_cache_key(
+ self, name: str, filename: t.Optional[t.Union[str]] = None
+ ) -> str:
+ """Returns the unique hash key for this template name."""
+ hash = sha1(name.encode("utf-8"))
+
+ if filename is not None:
+ hash.update(f"|{filename}".encode())
+
+ return hash.hexdigest()
+
+ def get_source_checksum(self, source: str) -> str:
+ """Returns a checksum for the source."""
+ return sha1(source.encode("utf-8")).hexdigest()
+
+ def get_bucket(
+ self,
+ environment: "Environment",
+ name: str,
+ filename: t.Optional[str],
+ source: str,
+ ) -> Bucket:
+ """Return a cache bucket for the given template. All arguments are
+ mandatory but filename may be `None`.
+ """
+ key = self.get_cache_key(name, filename)
+ checksum = self.get_source_checksum(source)
+ bucket = Bucket(environment, key, checksum)
+ self.load_bytecode(bucket)
+ return bucket
+
+ def set_bucket(self, bucket: Bucket) -> None:
+ """Put the bucket into the cache."""
+ self.dump_bytecode(bucket)
+
+
+class FileSystemBytecodeCache(BytecodeCache):
+ """A bytecode cache that stores bytecode on the filesystem. It accepts
+ two arguments: The directory where the cache items are stored and a
+ pattern string that is used to build the filename.
+
+ If no directory is specified a default cache directory is selected. On
+ Windows the user's temp directory is used, on UNIX systems a directory
+ is created for the user in the system temp directory.
+
+ The pattern can be used to have multiple separate caches operate on the
+ same directory. The default pattern is ``'__jinja2_%s.cache'``. ``%s``
+ is replaced with the cache key.
+
+ >>> bcc = FileSystemBytecodeCache('/tmp/jinja_cache', '%s.cache')
+
+ This bytecode cache supports clearing of the cache using the clear method.
+ """
+
+ def __init__(
+ self, directory: t.Optional[str] = None, pattern: str = "__jinja2_%s.cache"
+ ) -> None:
+ if directory is None:
+ directory = self._get_default_cache_dir()
+ self.directory = directory
+ self.pattern = pattern
+
+ def _get_default_cache_dir(self) -> str:
+ def _unsafe_dir() -> "te.NoReturn":
+ raise RuntimeError(
+ "Cannot determine safe temp directory. You "
+ "need to explicitly provide one."
+ )
+
+ tmpdir = tempfile.gettempdir()
+
+ # On windows the temporary directory is used specific unless
+ # explicitly forced otherwise. We can just use that.
+ if os.name == "nt":
+ return tmpdir
+ if not hasattr(os, "getuid"):
+ _unsafe_dir()
+
+ dirname = f"_jinja2-cache-{os.getuid()}"
+ actual_dir = os.path.join(tmpdir, dirname)
+
+ try:
+ os.mkdir(actual_dir, stat.S_IRWXU)
+ except OSError as e:
+ if e.errno != errno.EEXIST:
+ raise
+ try:
+ os.chmod(actual_dir, stat.S_IRWXU)
+ actual_dir_stat = os.lstat(actual_dir)
+ if (
+ actual_dir_stat.st_uid != os.getuid()
+ or not stat.S_ISDIR(actual_dir_stat.st_mode)
+ or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU
+ ):
+ _unsafe_dir()
+ except OSError as e:
+ if e.errno != errno.EEXIST:
+ raise
+
+ actual_dir_stat = os.lstat(actual_dir)
+ if (
+ actual_dir_stat.st_uid != os.getuid()
+ or not stat.S_ISDIR(actual_dir_stat.st_mode)
+ or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU
+ ):
+ _unsafe_dir()
+
+ return actual_dir
+
+ def _get_cache_filename(self, bucket: Bucket) -> str:
+ return os.path.join(self.directory, self.pattern % (bucket.key,))
+
+ def load_bytecode(self, bucket: Bucket) -> None:
+ filename = self._get_cache_filename(bucket)
+
+ # Don't test for existence before opening the file, since the
+ # file could disappear after the test before the open.
+ try:
+ f = open(filename, "rb")
+ except (FileNotFoundError, IsADirectoryError, PermissionError):
+ # PermissionError can occur on Windows when an operation is
+ # in progress, such as calling clear().
+ return
+
+ with f:
+ bucket.load_bytecode(f)
+
+ def dump_bytecode(self, bucket: Bucket) -> None:
+ # Write to a temporary file, then rename to the real name after
+ # writing. This avoids another process reading the file before
+ # it is fully written.
+ name = self._get_cache_filename(bucket)
+ f = tempfile.NamedTemporaryFile(
+ mode="wb",
+ dir=os.path.dirname(name),
+ prefix=os.path.basename(name),
+ suffix=".tmp",
+ delete=False,
+ )
+
+ def remove_silent() -> None:
+ try:
+ os.remove(f.name)
+ except OSError:
+ # Another process may have called clear(). On Windows,
+ # another program may be holding the file open.
+ pass
+
+ try:
+ with f:
+ bucket.write_bytecode(f)
+ except BaseException:
+ remove_silent()
+ raise
+
+ try:
+ os.replace(f.name, name)
+ except OSError:
+ # Another process may have called clear(). On Windows,
+ # another program may be holding the file open.
+ remove_silent()
+ except BaseException:
+ remove_silent()
+ raise
+
+ def clear(self) -> None:
+ # imported lazily here because google app-engine doesn't support
+ # write access on the file system and the function does not exist
+ # normally.
+ from os import remove
+
+ files = fnmatch.filter(os.listdir(self.directory), self.pattern % ("*",))
+ for filename in files:
+ try:
+ remove(os.path.join(self.directory, filename))
+ except OSError:
+ pass
+
+
+class MemcachedBytecodeCache(BytecodeCache):
+ """This class implements a bytecode cache that uses a memcache cache for
+ storing the information. It does not enforce a specific memcache library
+ (tummy's memcache or cmemcache) but will accept any class that provides
+ the minimal interface required.
+
+ Libraries compatible with this class:
+
+ - `cachelib `_
+ - `python-memcached `_
+
+ (Unfortunately the django cache interface is not compatible because it
+ does not support storing binary data, only text. You can however pass
+ the underlying cache client to the bytecode cache which is available
+ as `django.core.cache.cache._client`.)
+
+ The minimal interface for the client passed to the constructor is this:
+
+ .. class:: MinimalClientInterface
+
+ .. method:: set(key, value[, timeout])
+
+ Stores the bytecode in the cache. `value` is a string and
+ `timeout` the timeout of the key. If timeout is not provided
+ a default timeout or no timeout should be assumed, if it's
+ provided it's an integer with the number of seconds the cache
+ item should exist.
+
+ .. method:: get(key)
+
+ Returns the value for the cache key. If the item does not
+ exist in the cache the return value must be `None`.
+
+ The other arguments to the constructor are the prefix for all keys that
+ is added before the actual cache key and the timeout for the bytecode in
+ the cache system. We recommend a high (or no) timeout.
+
+ This bytecode cache does not support clearing of used items in the cache.
+ The clear method is a no-operation function.
+
+ .. versionadded:: 2.7
+ Added support for ignoring memcache errors through the
+ `ignore_memcache_errors` parameter.
+ """
+
+ def __init__(
+ self,
+ client: "_MemcachedClient",
+ prefix: str = "jinja2/bytecode/",
+ timeout: t.Optional[int] = None,
+ ignore_memcache_errors: bool = True,
+ ):
+ self.client = client
+ self.prefix = prefix
+ self.timeout = timeout
+ self.ignore_memcache_errors = ignore_memcache_errors
+
+ def load_bytecode(self, bucket: Bucket) -> None:
+ try:
+ code = self.client.get(self.prefix + bucket.key)
+ except Exception:
+ if not self.ignore_memcache_errors:
+ raise
+ else:
+ bucket.bytecode_from_string(code)
+
+ def dump_bytecode(self, bucket: Bucket) -> None:
+ key = self.prefix + bucket.key
+ value = bucket.bytecode_to_string()
+
+ try:
+ if self.timeout is not None:
+ self.client.set(key, value, self.timeout)
+ else:
+ self.client.set(key, value)
+ except Exception:
+ if not self.ignore_memcache_errors:
+ raise
diff --git a/venv/Lib/site-packages/jinja2/compiler.py b/venv/Lib/site-packages/jinja2/compiler.py
new file mode 100644
index 0000000..a4ff6a1
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2/compiler.py
@@ -0,0 +1,1998 @@
+"""Compiles nodes from the parser into Python code."""
+
+import typing as t
+from contextlib import contextmanager
+from functools import update_wrapper
+from io import StringIO
+from itertools import chain
+from keyword import iskeyword as is_python_keyword
+
+from markupsafe import escape
+from markupsafe import Markup
+
+from . import nodes
+from .exceptions import TemplateAssertionError
+from .idtracking import Symbols
+from .idtracking import VAR_LOAD_ALIAS
+from .idtracking import VAR_LOAD_PARAMETER
+from .idtracking import VAR_LOAD_RESOLVE
+from .idtracking import VAR_LOAD_UNDEFINED
+from .nodes import EvalContext
+from .optimizer import Optimizer
+from .utils import _PassArg
+from .utils import concat
+from .visitor import NodeVisitor
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+ from .environment import Environment
+
+F = t.TypeVar("F", bound=t.Callable[..., t.Any])
+
+operators = {
+ "eq": "==",
+ "ne": "!=",
+ "gt": ">",
+ "gteq": ">=",
+ "lt": "<",
+ "lteq": "<=",
+ "in": "in",
+ "notin": "not in",
+}
+
+
+def optimizeconst(f: F) -> F:
+ def new_func(
+ self: "CodeGenerator", node: nodes.Expr, frame: "Frame", **kwargs: t.Any
+ ) -> t.Any:
+ # Only optimize if the frame is not volatile
+ if self.optimizer is not None and not frame.eval_ctx.volatile:
+ new_node = self.optimizer.visit(node, frame.eval_ctx)
+
+ if new_node != node:
+ return self.visit(new_node, frame)
+
+ return f(self, node, frame, **kwargs)
+
+ return update_wrapper(new_func, f) # type: ignore[return-value]
+
+
+def _make_binop(op: str) -> t.Callable[["CodeGenerator", nodes.BinExpr, "Frame"], None]:
+ @optimizeconst
+ def visitor(self: "CodeGenerator", node: nodes.BinExpr, frame: Frame) -> None:
+ if (
+ self.environment.sandboxed and op in self.environment.intercepted_binops # type: ignore
+ ):
+ self.write(f"environment.call_binop(context, {op!r}, ")
+ self.visit(node.left, frame)
+ self.write(", ")
+ self.visit(node.right, frame)
+ else:
+ self.write("(")
+ self.visit(node.left, frame)
+ self.write(f" {op} ")
+ self.visit(node.right, frame)
+
+ self.write(")")
+
+ return visitor
+
+
+def _make_unop(
+ op: str,
+) -> t.Callable[["CodeGenerator", nodes.UnaryExpr, "Frame"], None]:
+ @optimizeconst
+ def visitor(self: "CodeGenerator", node: nodes.UnaryExpr, frame: Frame) -> None:
+ if (
+ self.environment.sandboxed and op in self.environment.intercepted_unops # type: ignore
+ ):
+ self.write(f"environment.call_unop(context, {op!r}, ")
+ self.visit(node.node, frame)
+ else:
+ self.write("(" + op)
+ self.visit(node.node, frame)
+
+ self.write(")")
+
+ return visitor
+
+
+def generate(
+ node: nodes.Template,
+ environment: "Environment",
+ name: t.Optional[str],
+ filename: t.Optional[str],
+ stream: t.Optional[t.TextIO] = None,
+ defer_init: bool = False,
+ optimized: bool = True,
+) -> t.Optional[str]:
+ """Generate the python source for a node tree."""
+ if not isinstance(node, nodes.Template):
+ raise TypeError("Can't compile non template nodes")
+
+ generator = environment.code_generator_class(
+ environment, name, filename, stream, defer_init, optimized
+ )
+ generator.visit(node)
+
+ if stream is None:
+ return generator.stream.getvalue() # type: ignore
+
+ return None
+
+
+def has_safe_repr(value: t.Any) -> bool:
+ """Does the node have a safe representation?"""
+ if value is None or value is NotImplemented or value is Ellipsis:
+ return True
+
+ if type(value) in {bool, int, float, complex, range, str, Markup}:
+ return True
+
+ if type(value) in {tuple, list, set, frozenset}:
+ return all(has_safe_repr(v) for v in value)
+
+ if type(value) is dict: # noqa E721
+ return all(has_safe_repr(k) and has_safe_repr(v) for k, v in value.items())
+
+ return False
+
+
+def find_undeclared(
+ nodes: t.Iterable[nodes.Node], names: t.Iterable[str]
+) -> t.Set[str]:
+ """Check if the names passed are accessed undeclared. The return value
+ is a set of all the undeclared names from the sequence of names found.
+ """
+ visitor = UndeclaredNameVisitor(names)
+ try:
+ for node in nodes:
+ visitor.visit(node)
+ except VisitorExit:
+ pass
+ return visitor.undeclared
+
+
+class MacroRef:
+ def __init__(self, node: t.Union[nodes.Macro, nodes.CallBlock]) -> None:
+ self.node = node
+ self.accesses_caller = False
+ self.accesses_kwargs = False
+ self.accesses_varargs = False
+
+
+class Frame:
+ """Holds compile time information for us."""
+
+ def __init__(
+ self,
+ eval_ctx: EvalContext,
+ parent: t.Optional["Frame"] = None,
+ level: t.Optional[int] = None,
+ ) -> None:
+ self.eval_ctx = eval_ctx
+
+ # the parent of this frame
+ self.parent = parent
+
+ if parent is None:
+ self.symbols = Symbols(level=level)
+
+ # in some dynamic inheritance situations the compiler needs to add
+ # write tests around output statements.
+ self.require_output_check = False
+
+ # inside some tags we are using a buffer rather than yield statements.
+ # this for example affects {% filter %} or {% macro %}. If a frame
+ # is buffered this variable points to the name of the list used as
+ # buffer.
+ self.buffer: t.Optional[str] = None
+
+ # the name of the block we're in, otherwise None.
+ self.block: t.Optional[str] = None
+
+ else:
+ self.symbols = Symbols(parent.symbols, level=level)
+ self.require_output_check = parent.require_output_check
+ self.buffer = parent.buffer
+ self.block = parent.block
+
+ # a toplevel frame is the root + soft frames such as if conditions.
+ self.toplevel = False
+
+ # the root frame is basically just the outermost frame, so no if
+ # conditions. This information is used to optimize inheritance
+ # situations.
+ self.rootlevel = False
+
+ # variables set inside of loops and blocks should not affect outer frames,
+ # but they still needs to be kept track of as part of the active context.
+ self.loop_frame = False
+ self.block_frame = False
+
+ # track whether the frame is being used in an if-statement or conditional
+ # expression as it determines which errors should be raised during runtime
+ # or compile time.
+ self.soft_frame = False
+
+ def copy(self) -> "te.Self":
+ """Create a copy of the current one."""
+ rv = object.__new__(self.__class__)
+ rv.__dict__.update(self.__dict__)
+ rv.symbols = self.symbols.copy()
+ return rv
+
+ def inner(self, isolated: bool = False) -> "Frame":
+ """Return an inner frame."""
+ if isolated:
+ return Frame(self.eval_ctx, level=self.symbols.level + 1)
+ return Frame(self.eval_ctx, self)
+
+ def soft(self) -> "te.Self":
+ """Return a soft frame. A soft frame may not be modified as
+ standalone thing as it shares the resources with the frame it
+ was created of, but it's not a rootlevel frame any longer.
+
+ This is only used to implement if-statements and conditional
+ expressions.
+ """
+ rv = self.copy()
+ rv.rootlevel = False
+ rv.soft_frame = True
+ return rv
+
+ __copy__ = copy
+
+
+class VisitorExit(RuntimeError):
+ """Exception used by the `UndeclaredNameVisitor` to signal a stop."""
+
+
+class DependencyFinderVisitor(NodeVisitor):
+ """A visitor that collects filter and test calls."""
+
+ def __init__(self) -> None:
+ self.filters: t.Set[str] = set()
+ self.tests: t.Set[str] = set()
+
+ def visit_Filter(self, node: nodes.Filter) -> None:
+ self.generic_visit(node)
+ self.filters.add(node.name)
+
+ def visit_Test(self, node: nodes.Test) -> None:
+ self.generic_visit(node)
+ self.tests.add(node.name)
+
+ def visit_Block(self, node: nodes.Block) -> None:
+ """Stop visiting at blocks."""
+
+
+class UndeclaredNameVisitor(NodeVisitor):
+ """A visitor that checks if a name is accessed without being
+ declared. This is different from the frame visitor as it will
+ not stop at closure frames.
+ """
+
+ def __init__(self, names: t.Iterable[str]) -> None:
+ self.names = set(names)
+ self.undeclared: t.Set[str] = set()
+
+ def visit_Name(self, node: nodes.Name) -> None:
+ if node.ctx == "load" and node.name in self.names:
+ self.undeclared.add(node.name)
+ if self.undeclared == self.names:
+ raise VisitorExit()
+ else:
+ self.names.discard(node.name)
+
+ def visit_Block(self, node: nodes.Block) -> None:
+ """Stop visiting a blocks."""
+
+
+class CompilerExit(Exception):
+ """Raised if the compiler encountered a situation where it just
+ doesn't make sense to further process the code. Any block that
+ raises such an exception is not further processed.
+ """
+
+
+class CodeGenerator(NodeVisitor):
+ def __init__(
+ self,
+ environment: "Environment",
+ name: t.Optional[str],
+ filename: t.Optional[str],
+ stream: t.Optional[t.TextIO] = None,
+ defer_init: bool = False,
+ optimized: bool = True,
+ ) -> None:
+ if stream is None:
+ stream = StringIO()
+ self.environment = environment
+ self.name = name
+ self.filename = filename
+ self.stream = stream
+ self.created_block_context = False
+ self.defer_init = defer_init
+ self.optimizer: t.Optional[Optimizer] = None
+
+ if optimized:
+ self.optimizer = Optimizer(environment)
+
+ # aliases for imports
+ self.import_aliases: t.Dict[str, str] = {}
+
+ # a registry for all blocks. Because blocks are moved out
+ # into the global python scope they are registered here
+ self.blocks: t.Dict[str, nodes.Block] = {}
+
+ # the number of extends statements so far
+ self.extends_so_far = 0
+
+ # some templates have a rootlevel extends. In this case we
+ # can safely assume that we're a child template and do some
+ # more optimizations.
+ self.has_known_extends = False
+
+ # the current line number
+ self.code_lineno = 1
+
+ # registry of all filters and tests (global, not block local)
+ self.tests: t.Dict[str, str] = {}
+ self.filters: t.Dict[str, str] = {}
+
+ # the debug information
+ self.debug_info: t.List[t.Tuple[int, int]] = []
+ self._write_debug_info: t.Optional[int] = None
+
+ # the number of new lines before the next write()
+ self._new_lines = 0
+
+ # the line number of the last written statement
+ self._last_line = 0
+
+ # true if nothing was written so far.
+ self._first_write = True
+
+ # used by the `temporary_identifier` method to get new
+ # unique, temporary identifier
+ self._last_identifier = 0
+
+ # the current indentation
+ self._indentation = 0
+
+ # Tracks toplevel assignments
+ self._assign_stack: t.List[t.Set[str]] = []
+
+ # Tracks parameter definition blocks
+ self._param_def_block: t.List[t.Set[str]] = []
+
+ # Tracks the current context.
+ self._context_reference_stack = ["context"]
+
+ @property
+ def optimized(self) -> bool:
+ return self.optimizer is not None
+
+ # -- Various compilation helpers
+
+ def fail(self, msg: str, lineno: int) -> "te.NoReturn":
+ """Fail with a :exc:`TemplateAssertionError`."""
+ raise TemplateAssertionError(msg, lineno, self.name, self.filename)
+
+ def temporary_identifier(self) -> str:
+ """Get a new unique identifier."""
+ self._last_identifier += 1
+ return f"t_{self._last_identifier}"
+
+ def buffer(self, frame: Frame) -> None:
+ """Enable buffering for the frame from that point onwards."""
+ frame.buffer = self.temporary_identifier()
+ self.writeline(f"{frame.buffer} = []")
+
+ def return_buffer_contents(
+ self, frame: Frame, force_unescaped: bool = False
+ ) -> None:
+ """Return the buffer contents of the frame."""
+ if not force_unescaped:
+ if frame.eval_ctx.volatile:
+ self.writeline("if context.eval_ctx.autoescape:")
+ self.indent()
+ self.writeline(f"return Markup(concat({frame.buffer}))")
+ self.outdent()
+ self.writeline("else:")
+ self.indent()
+ self.writeline(f"return concat({frame.buffer})")
+ self.outdent()
+ return
+ elif frame.eval_ctx.autoescape:
+ self.writeline(f"return Markup(concat({frame.buffer}))")
+ return
+ self.writeline(f"return concat({frame.buffer})")
+
+ def indent(self) -> None:
+ """Indent by one."""
+ self._indentation += 1
+
+ def outdent(self, step: int = 1) -> None:
+ """Outdent by step."""
+ self._indentation -= step
+
+ def start_write(self, frame: Frame, node: t.Optional[nodes.Node] = None) -> None:
+ """Yield or write into the frame buffer."""
+ if frame.buffer is None:
+ self.writeline("yield ", node)
+ else:
+ self.writeline(f"{frame.buffer}.append(", node)
+
+ def end_write(self, frame: Frame) -> None:
+ """End the writing process started by `start_write`."""
+ if frame.buffer is not None:
+ self.write(")")
+
+ def simple_write(
+ self, s: str, frame: Frame, node: t.Optional[nodes.Node] = None
+ ) -> None:
+ """Simple shortcut for start_write + write + end_write."""
+ self.start_write(frame, node)
+ self.write(s)
+ self.end_write(frame)
+
+ def blockvisit(self, nodes: t.Iterable[nodes.Node], frame: Frame) -> None:
+ """Visit a list of nodes as block in a frame. If the current frame
+ is no buffer a dummy ``if 0: yield None`` is written automatically.
+ """
+ try:
+ self.writeline("pass")
+ for node in nodes:
+ self.visit(node, frame)
+ except CompilerExit:
+ pass
+
+ def write(self, x: str) -> None:
+ """Write a string into the output stream."""
+ if self._new_lines:
+ if not self._first_write:
+ self.stream.write("\n" * self._new_lines)
+ self.code_lineno += self._new_lines
+ if self._write_debug_info is not None:
+ self.debug_info.append((self._write_debug_info, self.code_lineno))
+ self._write_debug_info = None
+ self._first_write = False
+ self.stream.write(" " * self._indentation)
+ self._new_lines = 0
+ self.stream.write(x)
+
+ def writeline(
+ self, x: str, node: t.Optional[nodes.Node] = None, extra: int = 0
+ ) -> None:
+ """Combination of newline and write."""
+ self.newline(node, extra)
+ self.write(x)
+
+ def newline(self, node: t.Optional[nodes.Node] = None, extra: int = 0) -> None:
+ """Add one or more newlines before the next write."""
+ self._new_lines = max(self._new_lines, 1 + extra)
+ if node is not None and node.lineno != self._last_line:
+ self._write_debug_info = node.lineno
+ self._last_line = node.lineno
+
+ def signature(
+ self,
+ node: t.Union[nodes.Call, nodes.Filter, nodes.Test],
+ frame: Frame,
+ extra_kwargs: t.Optional[t.Mapping[str, t.Any]] = None,
+ ) -> None:
+ """Writes a function call to the stream for the current node.
+ A leading comma is added automatically. The extra keyword
+ arguments may not include python keywords otherwise a syntax
+ error could occur. The extra keyword arguments should be given
+ as python dict.
+ """
+ # if any of the given keyword arguments is a python keyword
+ # we have to make sure that no invalid call is created.
+ kwarg_workaround = any(
+ is_python_keyword(t.cast(str, k))
+ for k in chain((x.key for x in node.kwargs), extra_kwargs or ())
+ )
+
+ for arg in node.args:
+ self.write(", ")
+ self.visit(arg, frame)
+
+ if not kwarg_workaround:
+ for kwarg in node.kwargs:
+ self.write(", ")
+ self.visit(kwarg, frame)
+ if extra_kwargs is not None:
+ for key, value in extra_kwargs.items():
+ self.write(f", {key}={value}")
+ if node.dyn_args:
+ self.write(", *")
+ self.visit(node.dyn_args, frame)
+
+ if kwarg_workaround:
+ if node.dyn_kwargs is not None:
+ self.write(", **dict({")
+ else:
+ self.write(", **{")
+ for kwarg in node.kwargs:
+ self.write(f"{kwarg.key!r}: ")
+ self.visit(kwarg.value, frame)
+ self.write(", ")
+ if extra_kwargs is not None:
+ for key, value in extra_kwargs.items():
+ self.write(f"{key!r}: {value}, ")
+ if node.dyn_kwargs is not None:
+ self.write("}, **")
+ self.visit(node.dyn_kwargs, frame)
+ self.write(")")
+ else:
+ self.write("}")
+
+ elif node.dyn_kwargs is not None:
+ self.write(", **")
+ self.visit(node.dyn_kwargs, frame)
+
+ def pull_dependencies(self, nodes: t.Iterable[nodes.Node]) -> None:
+ """Find all filter and test names used in the template and
+ assign them to variables in the compiled namespace. Checking
+ that the names are registered with the environment is done when
+ compiling the Filter and Test nodes. If the node is in an If or
+ CondExpr node, the check is done at runtime instead.
+
+ .. versionchanged:: 3.0
+ Filters and tests in If and CondExpr nodes are checked at
+ runtime instead of compile time.
+ """
+ visitor = DependencyFinderVisitor()
+
+ for node in nodes:
+ visitor.visit(node)
+
+ for id_map, names, dependency in (
+ (self.filters, visitor.filters, "filters"),
+ (
+ self.tests,
+ visitor.tests,
+ "tests",
+ ),
+ ):
+ for name in sorted(names):
+ if name not in id_map:
+ id_map[name] = self.temporary_identifier()
+
+ # add check during runtime that dependencies used inside of executed
+ # blocks are defined, as this step may be skipped during compile time
+ self.writeline("try:")
+ self.indent()
+ self.writeline(f"{id_map[name]} = environment.{dependency}[{name!r}]")
+ self.outdent()
+ self.writeline("except KeyError:")
+ self.indent()
+ self.writeline("@internalcode")
+ self.writeline(f"def {id_map[name]}(*unused):")
+ self.indent()
+ self.writeline(
+ f'raise TemplateRuntimeError("No {dependency[:-1]}'
+ f' named {name!r} found.")'
+ )
+ self.outdent()
+ self.outdent()
+
+ def enter_frame(self, frame: Frame) -> None:
+ undefs = []
+ for target, (action, param) in frame.symbols.loads.items():
+ if action == VAR_LOAD_PARAMETER:
+ pass
+ elif action == VAR_LOAD_RESOLVE:
+ self.writeline(f"{target} = {self.get_resolve_func()}({param!r})")
+ elif action == VAR_LOAD_ALIAS:
+ self.writeline(f"{target} = {param}")
+ elif action == VAR_LOAD_UNDEFINED:
+ undefs.append(target)
+ else:
+ raise NotImplementedError("unknown load instruction")
+ if undefs:
+ self.writeline(f"{' = '.join(undefs)} = missing")
+
+ def leave_frame(self, frame: Frame, with_python_scope: bool = False) -> None:
+ if not with_python_scope:
+ undefs = []
+ for target in frame.symbols.loads:
+ undefs.append(target)
+ if undefs:
+ self.writeline(f"{' = '.join(undefs)} = missing")
+
+ def choose_async(self, async_value: str = "async ", sync_value: str = "") -> str:
+ return async_value if self.environment.is_async else sync_value
+
+ def func(self, name: str) -> str:
+ return f"{self.choose_async()}def {name}"
+
+ def macro_body(
+ self, node: t.Union[nodes.Macro, nodes.CallBlock], frame: Frame
+ ) -> t.Tuple[Frame, MacroRef]:
+ """Dump the function def of a macro or call block."""
+ frame = frame.inner()
+ frame.symbols.analyze_node(node)
+ macro_ref = MacroRef(node)
+
+ explicit_caller = None
+ skip_special_params = set()
+ args = []
+
+ for idx, arg in enumerate(node.args):
+ if arg.name == "caller":
+ explicit_caller = idx
+ if arg.name in ("kwargs", "varargs"):
+ skip_special_params.add(arg.name)
+ args.append(frame.symbols.ref(arg.name))
+
+ undeclared = find_undeclared(node.body, ("caller", "kwargs", "varargs"))
+
+ if "caller" in undeclared:
+ # In older Jinja versions there was a bug that allowed caller
+ # to retain the special behavior even if it was mentioned in
+ # the argument list. However thankfully this was only really
+ # working if it was the last argument. So we are explicitly
+ # checking this now and error out if it is anywhere else in
+ # the argument list.
+ if explicit_caller is not None:
+ try:
+ node.defaults[explicit_caller - len(node.args)]
+ except IndexError:
+ self.fail(
+ "When defining macros or call blocks the "
+ 'special "caller" argument must be omitted '
+ "or be given a default.",
+ node.lineno,
+ )
+ else:
+ args.append(frame.symbols.declare_parameter("caller"))
+ macro_ref.accesses_caller = True
+ if "kwargs" in undeclared and "kwargs" not in skip_special_params:
+ args.append(frame.symbols.declare_parameter("kwargs"))
+ macro_ref.accesses_kwargs = True
+ if "varargs" in undeclared and "varargs" not in skip_special_params:
+ args.append(frame.symbols.declare_parameter("varargs"))
+ macro_ref.accesses_varargs = True
+
+ # macros are delayed, they never require output checks
+ frame.require_output_check = False
+ frame.symbols.analyze_node(node)
+ self.writeline(f"{self.func('macro')}({', '.join(args)}):", node)
+ self.indent()
+
+ self.buffer(frame)
+ self.enter_frame(frame)
+
+ self.push_parameter_definitions(frame)
+ for idx, arg in enumerate(node.args):
+ ref = frame.symbols.ref(arg.name)
+ self.writeline(f"if {ref} is missing:")
+ self.indent()
+ try:
+ default = node.defaults[idx - len(node.args)]
+ except IndexError:
+ self.writeline(
+ f'{ref} = undefined("parameter {arg.name!r} was not provided",'
+ f" name={arg.name!r})"
+ )
+ else:
+ self.writeline(f"{ref} = ")
+ self.visit(default, frame)
+ self.mark_parameter_stored(ref)
+ self.outdent()
+ self.pop_parameter_definitions()
+
+ self.blockvisit(node.body, frame)
+ self.return_buffer_contents(frame, force_unescaped=True)
+ self.leave_frame(frame, with_python_scope=True)
+ self.outdent()
+
+ return frame, macro_ref
+
+ def macro_def(self, macro_ref: MacroRef, frame: Frame) -> None:
+ """Dump the macro definition for the def created by macro_body."""
+ arg_tuple = ", ".join(repr(x.name) for x in macro_ref.node.args)
+ name = getattr(macro_ref.node, "name", None)
+ if len(macro_ref.node.args) == 1:
+ arg_tuple += ","
+ self.write(
+ f"Macro(environment, macro, {name!r}, ({arg_tuple}),"
+ f" {macro_ref.accesses_kwargs!r}, {macro_ref.accesses_varargs!r},"
+ f" {macro_ref.accesses_caller!r}, context.eval_ctx.autoescape)"
+ )
+
+ def position(self, node: nodes.Node) -> str:
+ """Return a human readable position for the node."""
+ rv = f"line {node.lineno}"
+ if self.name is not None:
+ rv = f"{rv} in {self.name!r}"
+ return rv
+
+ def dump_local_context(self, frame: Frame) -> str:
+ items_kv = ", ".join(
+ f"{name!r}: {target}"
+ for name, target in frame.symbols.dump_stores().items()
+ )
+ return f"{{{items_kv}}}"
+
+ def write_commons(self) -> None:
+ """Writes a common preamble that is used by root and block functions.
+ Primarily this sets up common local helpers and enforces a generator
+ through a dead branch.
+ """
+ self.writeline("resolve = context.resolve_or_missing")
+ self.writeline("undefined = environment.undefined")
+ self.writeline("concat = environment.concat")
+ # always use the standard Undefined class for the implicit else of
+ # conditional expressions
+ self.writeline("cond_expr_undefined = Undefined")
+ self.writeline("if 0: yield None")
+
+ def push_parameter_definitions(self, frame: Frame) -> None:
+ """Pushes all parameter targets from the given frame into a local
+ stack that permits tracking of yet to be assigned parameters. In
+ particular this enables the optimization from `visit_Name` to skip
+ undefined expressions for parameters in macros as macros can reference
+ otherwise unbound parameters.
+ """
+ self._param_def_block.append(frame.symbols.dump_param_targets())
+
+ def pop_parameter_definitions(self) -> None:
+ """Pops the current parameter definitions set."""
+ self._param_def_block.pop()
+
+ def mark_parameter_stored(self, target: str) -> None:
+ """Marks a parameter in the current parameter definitions as stored.
+ This will skip the enforced undefined checks.
+ """
+ if self._param_def_block:
+ self._param_def_block[-1].discard(target)
+
+ def push_context_reference(self, target: str) -> None:
+ self._context_reference_stack.append(target)
+
+ def pop_context_reference(self) -> None:
+ self._context_reference_stack.pop()
+
+ def get_context_ref(self) -> str:
+ return self._context_reference_stack[-1]
+
+ def get_resolve_func(self) -> str:
+ target = self._context_reference_stack[-1]
+ if target == "context":
+ return "resolve"
+ return f"{target}.resolve"
+
+ def derive_context(self, frame: Frame) -> str:
+ return f"{self.get_context_ref()}.derived({self.dump_local_context(frame)})"
+
+ def parameter_is_undeclared(self, target: str) -> bool:
+ """Checks if a given target is an undeclared parameter."""
+ if not self._param_def_block:
+ return False
+ return target in self._param_def_block[-1]
+
+ def push_assign_tracking(self) -> None:
+ """Pushes a new layer for assignment tracking."""
+ self._assign_stack.append(set())
+
+ def pop_assign_tracking(self, frame: Frame) -> None:
+ """Pops the topmost level for assignment tracking and updates the
+ context variables if necessary.
+ """
+ vars = self._assign_stack.pop()
+ if (
+ not frame.block_frame
+ and not frame.loop_frame
+ and not frame.toplevel
+ or not vars
+ ):
+ return
+ public_names = [x for x in vars if x[:1] != "_"]
+ if len(vars) == 1:
+ name = next(iter(vars))
+ ref = frame.symbols.ref(name)
+ if frame.loop_frame:
+ self.writeline(f"_loop_vars[{name!r}] = {ref}")
+ return
+ if frame.block_frame:
+ self.writeline(f"_block_vars[{name!r}] = {ref}")
+ return
+ self.writeline(f"context.vars[{name!r}] = {ref}")
+ else:
+ if frame.loop_frame:
+ self.writeline("_loop_vars.update({")
+ elif frame.block_frame:
+ self.writeline("_block_vars.update({")
+ else:
+ self.writeline("context.vars.update({")
+ for idx, name in enumerate(sorted(vars)):
+ if idx:
+ self.write(", ")
+ ref = frame.symbols.ref(name)
+ self.write(f"{name!r}: {ref}")
+ self.write("})")
+ if not frame.block_frame and not frame.loop_frame and public_names:
+ if len(public_names) == 1:
+ self.writeline(f"context.exported_vars.add({public_names[0]!r})")
+ else:
+ names_str = ", ".join(map(repr, sorted(public_names)))
+ self.writeline(f"context.exported_vars.update(({names_str}))")
+
+ # -- Statement Visitors
+
+ def visit_Template(
+ self, node: nodes.Template, frame: t.Optional[Frame] = None
+ ) -> None:
+ assert frame is None, "no root frame allowed"
+ eval_ctx = EvalContext(self.environment, self.name)
+
+ from .runtime import async_exported
+ from .runtime import exported
+
+ if self.environment.is_async:
+ exported_names = sorted(exported + async_exported)
+ else:
+ exported_names = sorted(exported)
+
+ self.writeline("from jinja2.runtime import " + ", ".join(exported_names))
+
+ # if we want a deferred initialization we cannot move the
+ # environment into a local name
+ envenv = "" if self.defer_init else ", environment=environment"
+
+ # do we have an extends tag at all? If not, we can save some
+ # overhead by just not processing any inheritance code.
+ have_extends = node.find(nodes.Extends) is not None
+
+ # find all blocks
+ for block in node.find_all(nodes.Block):
+ if block.name in self.blocks:
+ self.fail(f"block {block.name!r} defined twice", block.lineno)
+ self.blocks[block.name] = block
+
+ # find all imports and import them
+ for import_ in node.find_all(nodes.ImportedName):
+ if import_.importname not in self.import_aliases:
+ imp = import_.importname
+ self.import_aliases[imp] = alias = self.temporary_identifier()
+ if "." in imp:
+ module, obj = imp.rsplit(".", 1)
+ self.writeline(f"from {module} import {obj} as {alias}")
+ else:
+ self.writeline(f"import {imp} as {alias}")
+
+ # add the load name
+ self.writeline(f"name = {self.name!r}")
+
+ # generate the root render function.
+ self.writeline(
+ f"{self.func('root')}(context, missing=missing{envenv}):", extra=1
+ )
+ self.indent()
+ self.write_commons()
+
+ # process the root
+ frame = Frame(eval_ctx)
+ if "self" in find_undeclared(node.body, ("self",)):
+ ref = frame.symbols.declare_parameter("self")
+ self.writeline(f"{ref} = TemplateReference(context)")
+ frame.symbols.analyze_node(node)
+ frame.toplevel = frame.rootlevel = True
+ frame.require_output_check = have_extends and not self.has_known_extends
+ if have_extends:
+ self.writeline("parent_template = None")
+ self.enter_frame(frame)
+ self.pull_dependencies(node.body)
+ self.blockvisit(node.body, frame)
+ self.leave_frame(frame, with_python_scope=True)
+ self.outdent()
+
+ # make sure that the parent root is called.
+ if have_extends:
+ if not self.has_known_extends:
+ self.indent()
+ self.writeline("if parent_template is not None:")
+ self.indent()
+ if not self.environment.is_async:
+ self.writeline("yield from parent_template.root_render_func(context)")
+ else:
+ self.writeline("agen = parent_template.root_render_func(context)")
+ self.writeline("try:")
+ self.indent()
+ self.writeline("async for event in agen:")
+ self.indent()
+ self.writeline("yield event")
+ self.outdent()
+ self.outdent()
+ self.writeline("finally: await agen.aclose()")
+ self.outdent(1 + (not self.has_known_extends))
+
+ # at this point we now have the blocks collected and can visit them too.
+ for name, block in self.blocks.items():
+ self.writeline(
+ f"{self.func('block_' + name)}(context, missing=missing{envenv}):",
+ block,
+ 1,
+ )
+ self.indent()
+ self.write_commons()
+ # It's important that we do not make this frame a child of the
+ # toplevel template. This would cause a variety of
+ # interesting issues with identifier tracking.
+ block_frame = Frame(eval_ctx)
+ block_frame.block_frame = True
+ undeclared = find_undeclared(block.body, ("self", "super"))
+ if "self" in undeclared:
+ ref = block_frame.symbols.declare_parameter("self")
+ self.writeline(f"{ref} = TemplateReference(context)")
+ if "super" in undeclared:
+ ref = block_frame.symbols.declare_parameter("super")
+ self.writeline(f"{ref} = context.super({name!r}, block_{name})")
+ block_frame.symbols.analyze_node(block)
+ block_frame.block = name
+ self.writeline("_block_vars = {}")
+ self.enter_frame(block_frame)
+ self.pull_dependencies(block.body)
+ self.blockvisit(block.body, block_frame)
+ self.leave_frame(block_frame, with_python_scope=True)
+ self.outdent()
+
+ blocks_kv_str = ", ".join(f"{x!r}: block_{x}" for x in self.blocks)
+ self.writeline(f"blocks = {{{blocks_kv_str}}}", extra=1)
+ debug_kv_str = "&".join(f"{k}={v}" for k, v in self.debug_info)
+ self.writeline(f"debug_info = {debug_kv_str!r}")
+
+ def visit_Block(self, node: nodes.Block, frame: Frame) -> None:
+ """Call a block and register it for the template."""
+ level = 0
+ if frame.toplevel:
+ # if we know that we are a child template, there is no need to
+ # check if we are one
+ if self.has_known_extends:
+ return
+ if self.extends_so_far > 0:
+ self.writeline("if parent_template is None:")
+ self.indent()
+ level += 1
+
+ if node.scoped:
+ context = self.derive_context(frame)
+ else:
+ context = self.get_context_ref()
+
+ if node.required:
+ self.writeline(f"if len(context.blocks[{node.name!r}]) <= 1:", node)
+ self.indent()
+ self.writeline(
+ f'raise TemplateRuntimeError("Required block {node.name!r} not found")',
+ node,
+ )
+ self.outdent()
+
+ if not self.environment.is_async and frame.buffer is None:
+ self.writeline(
+ f"yield from context.blocks[{node.name!r}][0]({context})", node
+ )
+ else:
+ self.writeline(f"gen = context.blocks[{node.name!r}][0]({context})")
+ self.writeline("try:")
+ self.indent()
+ self.writeline(
+ f"{self.choose_async()}for event in gen:",
+ node,
+ )
+ self.indent()
+ self.simple_write("event", frame)
+ self.outdent()
+ self.outdent()
+ self.writeline(
+ f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}"
+ )
+
+ self.outdent(level)
+
+ def visit_Extends(self, node: nodes.Extends, frame: Frame) -> None:
+ """Calls the extender."""
+ if not frame.toplevel:
+ self.fail("cannot use extend from a non top-level scope", node.lineno)
+
+ # if the number of extends statements in general is zero so
+ # far, we don't have to add a check if something extended
+ # the template before this one.
+ if self.extends_so_far > 0:
+ # if we have a known extends we just add a template runtime
+ # error into the generated code. We could catch that at compile
+ # time too, but i welcome it not to confuse users by throwing the
+ # same error at different times just "because we can".
+ if not self.has_known_extends:
+ self.writeline("if parent_template is not None:")
+ self.indent()
+ self.writeline('raise TemplateRuntimeError("extended multiple times")')
+
+ # if we have a known extends already we don't need that code here
+ # as we know that the template execution will end here.
+ if self.has_known_extends:
+ raise CompilerExit()
+ else:
+ self.outdent()
+
+ self.writeline("parent_template = environment.get_template(", node)
+ self.visit(node.template, frame)
+ self.write(f", {self.name!r})")
+ self.writeline("for name, parent_block in parent_template.blocks.items():")
+ self.indent()
+ self.writeline("context.blocks.setdefault(name, []).append(parent_block)")
+ self.outdent()
+
+ # if this extends statement was in the root level we can take
+ # advantage of that information and simplify the generated code
+ # in the top level from this point onwards
+ if frame.rootlevel:
+ self.has_known_extends = True
+
+ # and now we have one more
+ self.extends_so_far += 1
+
+ def visit_Include(self, node: nodes.Include, frame: Frame) -> None:
+ """Handles includes."""
+ if node.ignore_missing:
+ self.writeline("try:")
+ self.indent()
+
+ func_name = "get_or_select_template"
+ if isinstance(node.template, nodes.Const):
+ if isinstance(node.template.value, str):
+ func_name = "get_template"
+ elif isinstance(node.template.value, (tuple, list)):
+ func_name = "select_template"
+ elif isinstance(node.template, (nodes.Tuple, nodes.List)):
+ func_name = "select_template"
+
+ self.writeline(f"template = environment.{func_name}(", node)
+ self.visit(node.template, frame)
+ self.write(f", {self.name!r})")
+ if node.ignore_missing:
+ self.outdent()
+ self.writeline("except TemplateNotFound:")
+ self.indent()
+ self.writeline("pass")
+ self.outdent()
+ self.writeline("else:")
+ self.indent()
+
+ def loop_body() -> None:
+ self.indent()
+ self.simple_write("event", frame)
+ self.outdent()
+
+ if node.with_context:
+ self.writeline(
+ f"gen = template.root_render_func("
+ "template.new_context(context.get_all(), True,"
+ f" {self.dump_local_context(frame)}))"
+ )
+ self.writeline("try:")
+ self.indent()
+ self.writeline(f"{self.choose_async()}for event in gen:")
+ loop_body()
+ self.outdent()
+ self.writeline(
+ f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}"
+ )
+ elif self.environment.is_async:
+ self.writeline(
+ "for event in (await template._get_default_module_async())"
+ "._body_stream:"
+ )
+ loop_body()
+ else:
+ self.writeline("yield from template._get_default_module()._body_stream")
+
+ if node.ignore_missing:
+ self.outdent()
+
+ def _import_common(
+ self, node: t.Union[nodes.Import, nodes.FromImport], frame: Frame
+ ) -> None:
+ self.write(f"{self.choose_async('await ')}environment.get_template(")
+ self.visit(node.template, frame)
+ self.write(f", {self.name!r}).")
+
+ if node.with_context:
+ f_name = f"make_module{self.choose_async('_async')}"
+ self.write(
+ f"{f_name}(context.get_all(), True, {self.dump_local_context(frame)})"
+ )
+ else:
+ self.write(f"_get_default_module{self.choose_async('_async')}(context)")
+
+ def visit_Import(self, node: nodes.Import, frame: Frame) -> None:
+ """Visit regular imports."""
+ self.writeline(f"{frame.symbols.ref(node.target)} = ", node)
+ if frame.toplevel:
+ self.write(f"context.vars[{node.target!r}] = ")
+
+ self._import_common(node, frame)
+
+ if frame.toplevel and not node.target.startswith("_"):
+ self.writeline(f"context.exported_vars.discard({node.target!r})")
+
+ def visit_FromImport(self, node: nodes.FromImport, frame: Frame) -> None:
+ """Visit named imports."""
+ self.newline(node)
+ self.write("included_template = ")
+ self._import_common(node, frame)
+ var_names = []
+ discarded_names = []
+ for name in node.names:
+ if isinstance(name, tuple):
+ name, alias = name
+ else:
+ alias = name
+ self.writeline(
+ f"{frame.symbols.ref(alias)} ="
+ f" getattr(included_template, {name!r}, missing)"
+ )
+ self.writeline(f"if {frame.symbols.ref(alias)} is missing:")
+ self.indent()
+ # The position will contain the template name, and will be formatted
+ # into a string that will be compiled into an f-string. Curly braces
+ # in the name must be replaced with escapes so that they will not be
+ # executed as part of the f-string.
+ position = self.position(node).replace("{", "{{").replace("}", "}}")
+ message = (
+ "the template {included_template.__name__!r}"
+ f" (imported on {position})"
+ f" does not export the requested name {name!r}"
+ )
+ self.writeline(
+ f"{frame.symbols.ref(alias)} = undefined(f{message!r}, name={name!r})"
+ )
+ self.outdent()
+ if frame.toplevel:
+ var_names.append(alias)
+ if not alias.startswith("_"):
+ discarded_names.append(alias)
+
+ if var_names:
+ if len(var_names) == 1:
+ name = var_names[0]
+ self.writeline(f"context.vars[{name!r}] = {frame.symbols.ref(name)}")
+ else:
+ names_kv = ", ".join(
+ f"{name!r}: {frame.symbols.ref(name)}" for name in var_names
+ )
+ self.writeline(f"context.vars.update({{{names_kv}}})")
+ if discarded_names:
+ if len(discarded_names) == 1:
+ self.writeline(f"context.exported_vars.discard({discarded_names[0]!r})")
+ else:
+ names_str = ", ".join(map(repr, discarded_names))
+ self.writeline(
+ f"context.exported_vars.difference_update(({names_str}))"
+ )
+
+ def visit_For(self, node: nodes.For, frame: Frame) -> None:
+ loop_frame = frame.inner()
+ loop_frame.loop_frame = True
+ test_frame = frame.inner()
+ else_frame = frame.inner()
+
+ # try to figure out if we have an extended loop. An extended loop
+ # is necessary if the loop is in recursive mode if the special loop
+ # variable is accessed in the body if the body is a scoped block.
+ extended_loop = (
+ node.recursive
+ or "loop"
+ in find_undeclared(node.iter_child_nodes(only=("body",)), ("loop",))
+ or any(block.scoped for block in node.find_all(nodes.Block))
+ )
+
+ loop_ref = None
+ if extended_loop:
+ loop_ref = loop_frame.symbols.declare_parameter("loop")
+
+ loop_frame.symbols.analyze_node(node, for_branch="body")
+ if node.else_:
+ else_frame.symbols.analyze_node(node, for_branch="else")
+
+ if node.test:
+ loop_filter_func = self.temporary_identifier()
+ test_frame.symbols.analyze_node(node, for_branch="test")
+ self.writeline(f"{self.func(loop_filter_func)}(fiter):", node.test)
+ self.indent()
+ self.enter_frame(test_frame)
+ self.writeline(self.choose_async("async for ", "for "))
+ self.visit(node.target, loop_frame)
+ self.write(" in ")
+ self.write(self.choose_async("auto_aiter(fiter)", "fiter"))
+ self.write(":")
+ self.indent()
+ self.writeline("if ", node.test)
+ self.visit(node.test, test_frame)
+ self.write(":")
+ self.indent()
+ self.writeline("yield ")
+ self.visit(node.target, loop_frame)
+ self.outdent(3)
+ self.leave_frame(test_frame, with_python_scope=True)
+
+ # if we don't have an recursive loop we have to find the shadowed
+ # variables at that point. Because loops can be nested but the loop
+ # variable is a special one we have to enforce aliasing for it.
+ if node.recursive:
+ self.writeline(
+ f"{self.func('loop')}(reciter, loop_render_func, depth=0):", node
+ )
+ self.indent()
+ self.buffer(loop_frame)
+
+ # Use the same buffer for the else frame
+ else_frame.buffer = loop_frame.buffer
+
+ # make sure the loop variable is a special one and raise a template
+ # assertion error if a loop tries to write to loop
+ if extended_loop:
+ self.writeline(f"{loop_ref} = missing")
+
+ for name in node.find_all(nodes.Name):
+ if name.ctx == "store" and name.name == "loop":
+ self.fail(
+ "Can't assign to special loop variable in for-loop target",
+ name.lineno,
+ )
+
+ if node.else_:
+ iteration_indicator = self.temporary_identifier()
+ self.writeline(f"{iteration_indicator} = 1")
+
+ self.writeline(self.choose_async("async for ", "for "), node)
+ self.visit(node.target, loop_frame)
+ if extended_loop:
+ self.write(f", {loop_ref} in {self.choose_async('Async')}LoopContext(")
+ else:
+ self.write(" in ")
+
+ if node.test:
+ self.write(f"{loop_filter_func}(")
+ if node.recursive:
+ self.write("reciter")
+ else:
+ if self.environment.is_async and not extended_loop:
+ self.write("auto_aiter(")
+ self.visit(node.iter, frame)
+ if self.environment.is_async and not extended_loop:
+ self.write(")")
+ if node.test:
+ self.write(")")
+
+ if node.recursive:
+ self.write(", undefined, loop_render_func, depth):")
+ else:
+ self.write(", undefined):" if extended_loop else ":")
+
+ self.indent()
+ self.enter_frame(loop_frame)
+
+ self.writeline("_loop_vars = {}")
+ self.blockvisit(node.body, loop_frame)
+ if node.else_:
+ self.writeline(f"{iteration_indicator} = 0")
+ self.outdent()
+ self.leave_frame(
+ loop_frame, with_python_scope=node.recursive and not node.else_
+ )
+
+ if node.else_:
+ self.writeline(f"if {iteration_indicator}:")
+ self.indent()
+ self.enter_frame(else_frame)
+ self.blockvisit(node.else_, else_frame)
+ self.leave_frame(else_frame)
+ self.outdent()
+
+ # if the node was recursive we have to return the buffer contents
+ # and start the iteration code
+ if node.recursive:
+ self.return_buffer_contents(loop_frame)
+ self.outdent()
+ self.start_write(frame, node)
+ self.write(f"{self.choose_async('await ')}loop(")
+ if self.environment.is_async:
+ self.write("auto_aiter(")
+ self.visit(node.iter, frame)
+ if self.environment.is_async:
+ self.write(")")
+ self.write(", loop)")
+ self.end_write(frame)
+
+ # at the end of the iteration, clear any assignments made in the
+ # loop from the top level
+ if self._assign_stack:
+ self._assign_stack[-1].difference_update(loop_frame.symbols.stores)
+
+ def visit_If(self, node: nodes.If, frame: Frame) -> None:
+ if_frame = frame.soft()
+ self.writeline("if ", node)
+ self.visit(node.test, if_frame)
+ self.write(":")
+ self.indent()
+ self.blockvisit(node.body, if_frame)
+ self.outdent()
+ for elif_ in node.elif_:
+ self.writeline("elif ", elif_)
+ self.visit(elif_.test, if_frame)
+ self.write(":")
+ self.indent()
+ self.blockvisit(elif_.body, if_frame)
+ self.outdent()
+ if node.else_:
+ self.writeline("else:")
+ self.indent()
+ self.blockvisit(node.else_, if_frame)
+ self.outdent()
+
+ def visit_Macro(self, node: nodes.Macro, frame: Frame) -> None:
+ macro_frame, macro_ref = self.macro_body(node, frame)
+ self.newline()
+ if frame.toplevel:
+ if not node.name.startswith("_"):
+ self.write(f"context.exported_vars.add({node.name!r})")
+ self.writeline(f"context.vars[{node.name!r}] = ")
+ self.write(f"{frame.symbols.ref(node.name)} = ")
+ self.macro_def(macro_ref, macro_frame)
+
+ def visit_CallBlock(self, node: nodes.CallBlock, frame: Frame) -> None:
+ call_frame, macro_ref = self.macro_body(node, frame)
+ self.writeline("caller = ")
+ self.macro_def(macro_ref, call_frame)
+ self.start_write(frame, node)
+ self.visit_Call(node.call, frame, forward_caller=True)
+ self.end_write(frame)
+
+ def visit_FilterBlock(self, node: nodes.FilterBlock, frame: Frame) -> None:
+ filter_frame = frame.inner()
+ filter_frame.symbols.analyze_node(node)
+ self.enter_frame(filter_frame)
+ self.buffer(filter_frame)
+ self.blockvisit(node.body, filter_frame)
+ self.start_write(frame, node)
+ self.visit_Filter(node.filter, filter_frame)
+ self.end_write(frame)
+ self.leave_frame(filter_frame)
+
+ def visit_With(self, node: nodes.With, frame: Frame) -> None:
+ with_frame = frame.inner()
+ with_frame.symbols.analyze_node(node)
+ self.enter_frame(with_frame)
+ for target, expr in zip(node.targets, node.values):
+ self.newline()
+ self.visit(target, with_frame)
+ self.write(" = ")
+ self.visit(expr, frame)
+ self.blockvisit(node.body, with_frame)
+ self.leave_frame(with_frame)
+
+ def visit_ExprStmt(self, node: nodes.ExprStmt, frame: Frame) -> None:
+ self.newline(node)
+ self.visit(node.node, frame)
+
+ class _FinalizeInfo(t.NamedTuple):
+ const: t.Optional[t.Callable[..., str]]
+ src: t.Optional[str]
+
+ @staticmethod
+ def _default_finalize(value: t.Any) -> t.Any:
+ """The default finalize function if the environment isn't
+ configured with one. Or, if the environment has one, this is
+ called on that function's output for constants.
+ """
+ return str(value)
+
+ _finalize: t.Optional[_FinalizeInfo] = None
+
+ def _make_finalize(self) -> _FinalizeInfo:
+ """Build the finalize function to be used on constants and at
+ runtime. Cached so it's only created once for all output nodes.
+
+ Returns a ``namedtuple`` with the following attributes:
+
+ ``const``
+ A function to finalize constant data at compile time.
+
+ ``src``
+ Source code to output around nodes to be evaluated at
+ runtime.
+ """
+ if self._finalize is not None:
+ return self._finalize
+
+ finalize: t.Optional[t.Callable[..., t.Any]]
+ finalize = default = self._default_finalize
+ src = None
+
+ if self.environment.finalize:
+ src = "environment.finalize("
+ env_finalize = self.environment.finalize
+ pass_arg = {
+ _PassArg.context: "context",
+ _PassArg.eval_context: "context.eval_ctx",
+ _PassArg.environment: "environment",
+ }.get(
+ _PassArg.from_obj(env_finalize) # type: ignore
+ )
+ finalize = None
+
+ if pass_arg is None:
+
+ def finalize(value: t.Any) -> t.Any: # noqa: F811
+ return default(env_finalize(value))
+
+ else:
+ src = f"{src}{pass_arg}, "
+
+ if pass_arg == "environment":
+
+ def finalize(value: t.Any) -> t.Any: # noqa: F811
+ return default(env_finalize(self.environment, value))
+
+ self._finalize = self._FinalizeInfo(finalize, src)
+ return self._finalize
+
+ def _output_const_repr(self, group: t.Iterable[t.Any]) -> str:
+ """Given a group of constant values converted from ``Output``
+ child nodes, produce a string to write to the template module
+ source.
+ """
+ return repr(concat(group))
+
+ def _output_child_to_const(
+ self, node: nodes.Expr, frame: Frame, finalize: _FinalizeInfo
+ ) -> str:
+ """Try to optimize a child of an ``Output`` node by trying to
+ convert it to constant, finalized data at compile time.
+
+ If :exc:`Impossible` is raised, the node is not constant and
+ will be evaluated at runtime. Any other exception will also be
+ evaluated at runtime for easier debugging.
+ """
+ const = node.as_const(frame.eval_ctx)
+
+ if frame.eval_ctx.autoescape:
+ const = escape(const)
+
+ # Template data doesn't go through finalize.
+ if isinstance(node, nodes.TemplateData):
+ return str(const)
+
+ return finalize.const(const) # type: ignore
+
+ def _output_child_pre(
+ self, node: nodes.Expr, frame: Frame, finalize: _FinalizeInfo
+ ) -> None:
+ """Output extra source code before visiting a child of an
+ ``Output`` node.
+ """
+ if frame.eval_ctx.volatile:
+ self.write("(escape if context.eval_ctx.autoescape else str)(")
+ elif frame.eval_ctx.autoescape:
+ self.write("escape(")
+ else:
+ self.write("str(")
+
+ if finalize.src is not None:
+ self.write(finalize.src)
+
+ def _output_child_post(
+ self, node: nodes.Expr, frame: Frame, finalize: _FinalizeInfo
+ ) -> None:
+ """Output extra source code after visiting a child of an
+ ``Output`` node.
+ """
+ self.write(")")
+
+ if finalize.src is not None:
+ self.write(")")
+
+ def visit_Output(self, node: nodes.Output, frame: Frame) -> None:
+ # If an extends is active, don't render outside a block.
+ if frame.require_output_check:
+ # A top-level extends is known to exist at compile time.
+ if self.has_known_extends:
+ return
+
+ self.writeline("if parent_template is None:")
+ self.indent()
+
+ finalize = self._make_finalize()
+ body: t.List[t.Union[t.List[t.Any], nodes.Expr]] = []
+
+ # Evaluate constants at compile time if possible. Each item in
+ # body will be either a list of static data or a node to be
+ # evaluated at runtime.
+ for child in node.nodes:
+ try:
+ if not (
+ # If the finalize function requires runtime context,
+ # constants can't be evaluated at compile time.
+ finalize.const
+ # Unless it's basic template data that won't be
+ # finalized anyway.
+ or isinstance(child, nodes.TemplateData)
+ ):
+ raise nodes.Impossible()
+
+ const = self._output_child_to_const(child, frame, finalize)
+ except (nodes.Impossible, Exception):
+ # The node was not constant and needs to be evaluated at
+ # runtime. Or another error was raised, which is easier
+ # to debug at runtime.
+ body.append(child)
+ continue
+
+ if body and isinstance(body[-1], list):
+ body[-1].append(const)
+ else:
+ body.append([const])
+
+ if frame.buffer is not None:
+ if len(body) == 1:
+ self.writeline(f"{frame.buffer}.append(")
+ else:
+ self.writeline(f"{frame.buffer}.extend((")
+
+ self.indent()
+
+ for item in body:
+ if isinstance(item, list):
+ # A group of constant data to join and output.
+ val = self._output_const_repr(item)
+
+ if frame.buffer is None:
+ self.writeline("yield " + val)
+ else:
+ self.writeline(val + ",")
+ else:
+ if frame.buffer is None:
+ self.writeline("yield ", item)
+ else:
+ self.newline(item)
+
+ # A node to be evaluated at runtime.
+ self._output_child_pre(item, frame, finalize)
+ self.visit(item, frame)
+ self._output_child_post(item, frame, finalize)
+
+ if frame.buffer is not None:
+ self.write(",")
+
+ if frame.buffer is not None:
+ self.outdent()
+ self.writeline(")" if len(body) == 1 else "))")
+
+ if frame.require_output_check:
+ self.outdent()
+
+ def visit_Assign(self, node: nodes.Assign, frame: Frame) -> None:
+ self.push_assign_tracking()
+
+ # ``a.b`` is allowed for assignment, and is parsed as an NSRef. However,
+ # it is only valid if it references a Namespace object. Emit a check for
+ # that for each ref here, before assignment code is emitted. This can't
+ # be done in visit_NSRef as the ref could be in the middle of a tuple.
+ seen_refs: t.Set[str] = set()
+
+ for nsref in node.find_all(nodes.NSRef):
+ if nsref.name in seen_refs:
+ # Only emit the check for each reference once, in case the same
+ # ref is used multiple times in a tuple, `ns.a, ns.b = c, d`.
+ continue
+
+ seen_refs.add(nsref.name)
+ ref = frame.symbols.ref(nsref.name)
+ self.writeline(f"if not isinstance({ref}, Namespace):")
+ self.indent()
+ self.writeline(
+ "raise TemplateRuntimeError"
+ '("cannot assign attribute on non-namespace object")'
+ )
+ self.outdent()
+
+ self.newline(node)
+ self.visit(node.target, frame)
+ self.write(" = ")
+ self.visit(node.node, frame)
+ self.pop_assign_tracking(frame)
+
+ def visit_AssignBlock(self, node: nodes.AssignBlock, frame: Frame) -> None:
+ self.push_assign_tracking()
+ block_frame = frame.inner()
+ # This is a special case. Since a set block always captures we
+ # will disable output checks. This way one can use set blocks
+ # toplevel even in extended templates.
+ block_frame.require_output_check = False
+ block_frame.symbols.analyze_node(node)
+ self.enter_frame(block_frame)
+ self.buffer(block_frame)
+ self.blockvisit(node.body, block_frame)
+ self.newline(node)
+ self.visit(node.target, frame)
+ self.write(" = (Markup if context.eval_ctx.autoescape else identity)(")
+ if node.filter is not None:
+ self.visit_Filter(node.filter, block_frame)
+ else:
+ self.write(f"concat({block_frame.buffer})")
+ self.write(")")
+ self.pop_assign_tracking(frame)
+ self.leave_frame(block_frame)
+
+ # -- Expression Visitors
+
+ def visit_Name(self, node: nodes.Name, frame: Frame) -> None:
+ if node.ctx == "store" and (
+ frame.toplevel or frame.loop_frame or frame.block_frame
+ ):
+ if self._assign_stack:
+ self._assign_stack[-1].add(node.name)
+ ref = frame.symbols.ref(node.name)
+
+ # If we are looking up a variable we might have to deal with the
+ # case where it's undefined. We can skip that case if the load
+ # instruction indicates a parameter which are always defined.
+ if node.ctx == "load":
+ load = frame.symbols.find_load(ref)
+ if not (
+ load is not None
+ and load[0] == VAR_LOAD_PARAMETER
+ and not self.parameter_is_undeclared(ref)
+ ):
+ self.write(
+ f"(undefined(name={node.name!r}) if {ref} is missing else {ref})"
+ )
+ return
+
+ self.write(ref)
+
+ def visit_NSRef(self, node: nodes.NSRef, frame: Frame) -> None:
+ # NSRef is a dotted assignment target a.b=c, but uses a[b]=c internally.
+ # visit_Assign emits code to validate that each ref is to a Namespace
+ # object only. That can't be emitted here as the ref could be in the
+ # middle of a tuple assignment.
+ ref = frame.symbols.ref(node.name)
+ self.writeline(f"{ref}[{node.attr!r}]")
+
+ def visit_Const(self, node: nodes.Const, frame: Frame) -> None:
+ val = node.as_const(frame.eval_ctx)
+ if isinstance(val, float):
+ self.write(str(val))
+ else:
+ self.write(repr(val))
+
+ def visit_TemplateData(self, node: nodes.TemplateData, frame: Frame) -> None:
+ try:
+ self.write(repr(node.as_const(frame.eval_ctx)))
+ except nodes.Impossible:
+ self.write(
+ f"(Markup if context.eval_ctx.autoescape else identity)({node.data!r})"
+ )
+
+ def visit_Tuple(self, node: nodes.Tuple, frame: Frame) -> None:
+ self.write("(")
+ idx = -1
+ for idx, item in enumerate(node.items):
+ if idx:
+ self.write(", ")
+ self.visit(item, frame)
+ self.write(",)" if idx == 0 else ")")
+
+ def visit_List(self, node: nodes.List, frame: Frame) -> None:
+ self.write("[")
+ for idx, item in enumerate(node.items):
+ if idx:
+ self.write(", ")
+ self.visit(item, frame)
+ self.write("]")
+
+ def visit_Dict(self, node: nodes.Dict, frame: Frame) -> None:
+ self.write("{")
+ for idx, item in enumerate(node.items):
+ if idx:
+ self.write(", ")
+ self.visit(item.key, frame)
+ self.write(": ")
+ self.visit(item.value, frame)
+ self.write("}")
+
+ visit_Add = _make_binop("+")
+ visit_Sub = _make_binop("-")
+ visit_Mul = _make_binop("*")
+ visit_Div = _make_binop("/")
+ visit_FloorDiv = _make_binop("//")
+ visit_Pow = _make_binop("**")
+ visit_Mod = _make_binop("%")
+ visit_And = _make_binop("and")
+ visit_Or = _make_binop("or")
+ visit_Pos = _make_unop("+")
+ visit_Neg = _make_unop("-")
+ visit_Not = _make_unop("not ")
+
+ @optimizeconst
+ def visit_Concat(self, node: nodes.Concat, frame: Frame) -> None:
+ if frame.eval_ctx.volatile:
+ func_name = "(markup_join if context.eval_ctx.volatile else str_join)"
+ elif frame.eval_ctx.autoescape:
+ func_name = "markup_join"
+ else:
+ func_name = "str_join"
+ self.write(f"{func_name}((")
+ for arg in node.nodes:
+ self.visit(arg, frame)
+ self.write(", ")
+ self.write("))")
+
+ @optimizeconst
+ def visit_Compare(self, node: nodes.Compare, frame: Frame) -> None:
+ self.write("(")
+ self.visit(node.expr, frame)
+ for op in node.ops:
+ self.visit(op, frame)
+ self.write(")")
+
+ def visit_Operand(self, node: nodes.Operand, frame: Frame) -> None:
+ self.write(f" {operators[node.op]} ")
+ self.visit(node.expr, frame)
+
+ @optimizeconst
+ def visit_Getattr(self, node: nodes.Getattr, frame: Frame) -> None:
+ if self.environment.is_async:
+ self.write("(await auto_await(")
+
+ self.write("environment.getattr(")
+ self.visit(node.node, frame)
+ self.write(f", {node.attr!r})")
+
+ if self.environment.is_async:
+ self.write("))")
+
+ @optimizeconst
+ def visit_Getitem(self, node: nodes.Getitem, frame: Frame) -> None:
+ # slices bypass the environment getitem method.
+ if isinstance(node.arg, nodes.Slice):
+ self.visit(node.node, frame)
+ self.write("[")
+ self.visit(node.arg, frame)
+ self.write("]")
+ else:
+ if self.environment.is_async:
+ self.write("(await auto_await(")
+
+ self.write("environment.getitem(")
+ self.visit(node.node, frame)
+ self.write(", ")
+ self.visit(node.arg, frame)
+ self.write(")")
+
+ if self.environment.is_async:
+ self.write("))")
+
+ def visit_Slice(self, node: nodes.Slice, frame: Frame) -> None:
+ if node.start is not None:
+ self.visit(node.start, frame)
+ self.write(":")
+ if node.stop is not None:
+ self.visit(node.stop, frame)
+ if node.step is not None:
+ self.write(":")
+ self.visit(node.step, frame)
+
+ @contextmanager
+ def _filter_test_common(
+ self, node: t.Union[nodes.Filter, nodes.Test], frame: Frame, is_filter: bool
+ ) -> t.Iterator[None]:
+ if self.environment.is_async:
+ self.write("(await auto_await(")
+
+ if is_filter:
+ self.write(f"{self.filters[node.name]}(")
+ func = self.environment.filters.get(node.name)
+ else:
+ self.write(f"{self.tests[node.name]}(")
+ func = self.environment.tests.get(node.name)
+
+ # When inside an If or CondExpr frame, allow the filter to be
+ # undefined at compile time and only raise an error if it's
+ # actually called at runtime. See pull_dependencies.
+ if func is None and not frame.soft_frame:
+ type_name = "filter" if is_filter else "test"
+ self.fail(f"No {type_name} named {node.name!r}.", node.lineno)
+
+ pass_arg = {
+ _PassArg.context: "context",
+ _PassArg.eval_context: "context.eval_ctx",
+ _PassArg.environment: "environment",
+ }.get(
+ _PassArg.from_obj(func) # type: ignore
+ )
+
+ if pass_arg is not None:
+ self.write(f"{pass_arg}, ")
+
+ # Back to the visitor function to handle visiting the target of
+ # the filter or test.
+ yield
+
+ self.signature(node, frame)
+ self.write(")")
+
+ if self.environment.is_async:
+ self.write("))")
+
+ @optimizeconst
+ def visit_Filter(self, node: nodes.Filter, frame: Frame) -> None:
+ with self._filter_test_common(node, frame, True):
+ # if the filter node is None we are inside a filter block
+ # and want to write to the current buffer
+ if node.node is not None:
+ self.visit(node.node, frame)
+ elif frame.eval_ctx.volatile:
+ self.write(
+ f"(Markup(concat({frame.buffer}))"
+ f" if context.eval_ctx.autoescape else concat({frame.buffer}))"
+ )
+ elif frame.eval_ctx.autoescape:
+ self.write(f"Markup(concat({frame.buffer}))")
+ else:
+ self.write(f"concat({frame.buffer})")
+
+ @optimizeconst
+ def visit_Test(self, node: nodes.Test, frame: Frame) -> None:
+ with self._filter_test_common(node, frame, False):
+ self.visit(node.node, frame)
+
+ @optimizeconst
+ def visit_CondExpr(self, node: nodes.CondExpr, frame: Frame) -> None:
+ frame = frame.soft()
+
+ def write_expr2() -> None:
+ if node.expr2 is not None:
+ self.visit(node.expr2, frame)
+ return
+
+ self.write(
+ f'cond_expr_undefined("the inline if-expression on'
+ f" {self.position(node)} evaluated to false and no else"
+ f' section was defined.")'
+ )
+
+ self.write("(")
+ self.visit(node.expr1, frame)
+ self.write(" if ")
+ self.visit(node.test, frame)
+ self.write(" else ")
+ write_expr2()
+ self.write(")")
+
+ @optimizeconst
+ def visit_Call(
+ self, node: nodes.Call, frame: Frame, forward_caller: bool = False
+ ) -> None:
+ if self.environment.is_async:
+ self.write("(await auto_await(")
+ if self.environment.sandboxed:
+ self.write("environment.call(context, ")
+ else:
+ self.write("context.call(")
+ self.visit(node.node, frame)
+ extra_kwargs = {"caller": "caller"} if forward_caller else None
+ loop_kwargs = {"_loop_vars": "_loop_vars"} if frame.loop_frame else {}
+ block_kwargs = {"_block_vars": "_block_vars"} if frame.block_frame else {}
+ if extra_kwargs:
+ extra_kwargs.update(loop_kwargs, **block_kwargs)
+ elif loop_kwargs or block_kwargs:
+ extra_kwargs = dict(loop_kwargs, **block_kwargs)
+ self.signature(node, frame, extra_kwargs)
+ self.write(")")
+ if self.environment.is_async:
+ self.write("))")
+
+ def visit_Keyword(self, node: nodes.Keyword, frame: Frame) -> None:
+ self.write(node.key + "=")
+ self.visit(node.value, frame)
+
+ # -- Unused nodes for extensions
+
+ def visit_MarkSafe(self, node: nodes.MarkSafe, frame: Frame) -> None:
+ self.write("Markup(")
+ self.visit(node.expr, frame)
+ self.write(")")
+
+ def visit_MarkSafeIfAutoescape(
+ self, node: nodes.MarkSafeIfAutoescape, frame: Frame
+ ) -> None:
+ self.write("(Markup if context.eval_ctx.autoescape else identity)(")
+ self.visit(node.expr, frame)
+ self.write(")")
+
+ def visit_EnvironmentAttribute(
+ self, node: nodes.EnvironmentAttribute, frame: Frame
+ ) -> None:
+ self.write("environment." + node.name)
+
+ def visit_ExtensionAttribute(
+ self, node: nodes.ExtensionAttribute, frame: Frame
+ ) -> None:
+ self.write(f"environment.extensions[{node.identifier!r}].{node.name}")
+
+ def visit_ImportedName(self, node: nodes.ImportedName, frame: Frame) -> None:
+ self.write(self.import_aliases[node.importname])
+
+ def visit_InternalName(self, node: nodes.InternalName, frame: Frame) -> None:
+ self.write(node.name)
+
+ def visit_ContextReference(
+ self, node: nodes.ContextReference, frame: Frame
+ ) -> None:
+ self.write("context")
+
+ def visit_DerivedContextReference(
+ self, node: nodes.DerivedContextReference, frame: Frame
+ ) -> None:
+ self.write(self.derive_context(frame))
+
+ def visit_Continue(self, node: nodes.Continue, frame: Frame) -> None:
+ self.writeline("continue", node)
+
+ def visit_Break(self, node: nodes.Break, frame: Frame) -> None:
+ self.writeline("break", node)
+
+ def visit_Scope(self, node: nodes.Scope, frame: Frame) -> None:
+ scope_frame = frame.inner()
+ scope_frame.symbols.analyze_node(node)
+ self.enter_frame(scope_frame)
+ self.blockvisit(node.body, scope_frame)
+ self.leave_frame(scope_frame)
+
+ def visit_OverlayScope(self, node: nodes.OverlayScope, frame: Frame) -> None:
+ ctx = self.temporary_identifier()
+ self.writeline(f"{ctx} = {self.derive_context(frame)}")
+ self.writeline(f"{ctx}.vars = ")
+ self.visit(node.context, frame)
+ self.push_context_reference(ctx)
+
+ scope_frame = frame.inner(isolated=True)
+ scope_frame.symbols.analyze_node(node)
+ self.enter_frame(scope_frame)
+ self.blockvisit(node.body, scope_frame)
+ self.leave_frame(scope_frame)
+ self.pop_context_reference()
+
+ def visit_EvalContextModifier(
+ self, node: nodes.EvalContextModifier, frame: Frame
+ ) -> None:
+ for keyword in node.options:
+ self.writeline(f"context.eval_ctx.{keyword.key} = ")
+ self.visit(keyword.value, frame)
+ try:
+ val = keyword.value.as_const(frame.eval_ctx)
+ except nodes.Impossible:
+ frame.eval_ctx.volatile = True
+ else:
+ setattr(frame.eval_ctx, keyword.key, val)
+
+ def visit_ScopedEvalContextModifier(
+ self, node: nodes.ScopedEvalContextModifier, frame: Frame
+ ) -> None:
+ old_ctx_name = self.temporary_identifier()
+ saved_ctx = frame.eval_ctx.save()
+ self.writeline(f"{old_ctx_name} = context.eval_ctx.save()")
+ self.visit_EvalContextModifier(node, frame)
+ for child in node.body:
+ self.visit(child, frame)
+ frame.eval_ctx.revert(saved_ctx)
+ self.writeline(f"context.eval_ctx.revert({old_ctx_name})")
diff --git a/venv/Lib/site-packages/jinja2/constants.py b/venv/Lib/site-packages/jinja2/constants.py
new file mode 100644
index 0000000..41a1c23
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2/constants.py
@@ -0,0 +1,20 @@
+#: list of lorem ipsum words used by the lipsum() helper function
+LOREM_IPSUM_WORDS = """\
+a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at
+auctor augue bibendum blandit class commodo condimentum congue consectetuer
+consequat conubia convallis cras cubilia cum curabitur curae cursus dapibus
+diam dictum dictumst dignissim dis dolor donec dui duis egestas eget eleifend
+elementum elit enim erat eros est et etiam eu euismod facilisi facilisis fames
+faucibus felis fermentum feugiat fringilla fusce gravida habitant habitasse hac
+hendrerit hymenaeos iaculis id imperdiet in inceptos integer interdum ipsum
+justo lacinia lacus laoreet lectus leo libero ligula litora lobortis lorem
+luctus maecenas magna magnis malesuada massa mattis mauris metus mi molestie
+mollis montes morbi mus nam nascetur natoque nec neque netus nibh nisi nisl non
+nonummy nostra nulla nullam nunc odio orci ornare parturient pede pellentesque
+penatibus per pharetra phasellus placerat platea porta porttitor posuere
+potenti praesent pretium primis proin pulvinar purus quam quis quisque rhoncus
+ridiculus risus rutrum sagittis sapien scelerisque sed sem semper senectus sit
+sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor
+tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices
+ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus
+viverra volutpat vulputate"""
diff --git a/venv/Lib/site-packages/jinja2/debug.py b/venv/Lib/site-packages/jinja2/debug.py
new file mode 100644
index 0000000..eeeeee7
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2/debug.py
@@ -0,0 +1,191 @@
+import sys
+import typing as t
+from types import CodeType
+from types import TracebackType
+
+from .exceptions import TemplateSyntaxError
+from .utils import internal_code
+from .utils import missing
+
+if t.TYPE_CHECKING:
+ from .runtime import Context
+
+
+def rewrite_traceback_stack(source: t.Optional[str] = None) -> BaseException:
+ """Rewrite the current exception to replace any tracebacks from
+ within compiled template code with tracebacks that look like they
+ came from the template source.
+
+ This must be called within an ``except`` block.
+
+ :param source: For ``TemplateSyntaxError``, the original source if
+ known.
+ :return: The original exception with the rewritten traceback.
+ """
+ _, exc_value, tb = sys.exc_info()
+ exc_value = t.cast(BaseException, exc_value)
+ tb = t.cast(TracebackType, tb)
+
+ if isinstance(exc_value, TemplateSyntaxError) and not exc_value.translated:
+ exc_value.translated = True
+ exc_value.source = source
+ # Remove the old traceback, otherwise the frames from the
+ # compiler still show up.
+ exc_value.with_traceback(None)
+ # Outside of runtime, so the frame isn't executing template
+ # code, but it still needs to point at the template.
+ tb = fake_traceback(
+ exc_value, None, exc_value.filename or "", exc_value.lineno
+ )
+ else:
+ # Skip the frame for the render function.
+ tb = tb.tb_next
+
+ stack = []
+
+ # Build the stack of traceback object, replacing any in template
+ # code with the source file and line information.
+ while tb is not None:
+ # Skip frames decorated with @internalcode. These are internal
+ # calls that aren't useful in template debugging output.
+ if tb.tb_frame.f_code in internal_code:
+ tb = tb.tb_next
+ continue
+
+ template = tb.tb_frame.f_globals.get("__jinja_template__")
+
+ if template is not None:
+ lineno = template.get_corresponding_lineno(tb.tb_lineno)
+ fake_tb = fake_traceback(exc_value, tb, template.filename, lineno)
+ stack.append(fake_tb)
+ else:
+ stack.append(tb)
+
+ tb = tb.tb_next
+
+ tb_next = None
+
+ # Assign tb_next in reverse to avoid circular references.
+ for tb in reversed(stack):
+ tb.tb_next = tb_next
+ tb_next = tb
+
+ return exc_value.with_traceback(tb_next)
+
+
+def fake_traceback( # type: ignore
+ exc_value: BaseException, tb: t.Optional[TracebackType], filename: str, lineno: int
+) -> TracebackType:
+ """Produce a new traceback object that looks like it came from the
+ template source instead of the compiled code. The filename, line
+ number, and location name will point to the template, and the local
+ variables will be the current template context.
+
+ :param exc_value: The original exception to be re-raised to create
+ the new traceback.
+ :param tb: The original traceback to get the local variables and
+ code info from.
+ :param filename: The template filename.
+ :param lineno: The line number in the template source.
+ """
+ if tb is not None:
+ # Replace the real locals with the context that would be
+ # available at that point in the template.
+ locals = get_template_locals(tb.tb_frame.f_locals)
+ locals.pop("__jinja_exception__", None)
+ else:
+ locals = {}
+
+ globals = {
+ "__name__": filename,
+ "__file__": filename,
+ "__jinja_exception__": exc_value,
+ }
+ # Raise an exception at the correct line number.
+ code: CodeType = compile(
+ "\n" * (lineno - 1) + "raise __jinja_exception__", filename, "exec"
+ )
+
+ # Build a new code object that points to the template file and
+ # replaces the location with a block name.
+ location = "template"
+
+ if tb is not None:
+ function = tb.tb_frame.f_code.co_name
+
+ if function == "root":
+ location = "top-level template code"
+ elif function.startswith("block_"):
+ location = f"block {function[6:]!r}"
+
+ if sys.version_info >= (3, 8):
+ code = code.replace(co_name=location)
+ else:
+ code = CodeType(
+ code.co_argcount,
+ code.co_kwonlyargcount,
+ code.co_nlocals,
+ code.co_stacksize,
+ code.co_flags,
+ code.co_code,
+ code.co_consts,
+ code.co_names,
+ code.co_varnames,
+ code.co_filename,
+ location,
+ code.co_firstlineno,
+ code.co_lnotab,
+ code.co_freevars,
+ code.co_cellvars,
+ )
+
+ # Execute the new code, which is guaranteed to raise, and return
+ # the new traceback without this frame.
+ try:
+ exec(code, globals, locals)
+ except BaseException:
+ return sys.exc_info()[2].tb_next # type: ignore
+
+
+def get_template_locals(real_locals: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]:
+ """Based on the runtime locals, get the context that would be
+ available at that point in the template.
+ """
+ # Start with the current template context.
+ ctx: t.Optional[Context] = real_locals.get("context")
+
+ if ctx is not None:
+ data: t.Dict[str, t.Any] = ctx.get_all().copy()
+ else:
+ data = {}
+
+ # Might be in a derived context that only sets local variables
+ # rather than pushing a context. Local variables follow the scheme
+ # l_depth_name. Find the highest-depth local that has a value for
+ # each name.
+ local_overrides: t.Dict[str, t.Tuple[int, t.Any]] = {}
+
+ for name, value in real_locals.items():
+ if not name.startswith("l_") or value is missing:
+ # Not a template variable, or no longer relevant.
+ continue
+
+ try:
+ _, depth_str, name = name.split("_", 2)
+ depth = int(depth_str)
+ except ValueError:
+ continue
+
+ cur_depth = local_overrides.get(name, (-1,))[0]
+
+ if cur_depth < depth:
+ local_overrides[name] = (depth, value)
+
+ # Modify the context with any derived context.
+ for name, (_, value) in local_overrides.items():
+ if value is missing:
+ data.pop(name, None)
+ else:
+ data[name] = value
+
+ return data
diff --git a/venv/Lib/site-packages/jinja2/defaults.py b/venv/Lib/site-packages/jinja2/defaults.py
new file mode 100644
index 0000000..638cad3
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2/defaults.py
@@ -0,0 +1,48 @@
+import typing as t
+
+from .filters import FILTERS as DEFAULT_FILTERS # noqa: F401
+from .tests import TESTS as DEFAULT_TESTS # noqa: F401
+from .utils import Cycler
+from .utils import generate_lorem_ipsum
+from .utils import Joiner
+from .utils import Namespace
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+# defaults for the parser / lexer
+BLOCK_START_STRING = "{%"
+BLOCK_END_STRING = "%}"
+VARIABLE_START_STRING = "{{"
+VARIABLE_END_STRING = "}}"
+COMMENT_START_STRING = "{#"
+COMMENT_END_STRING = "#}"
+LINE_STATEMENT_PREFIX: t.Optional[str] = None
+LINE_COMMENT_PREFIX: t.Optional[str] = None
+TRIM_BLOCKS = False
+LSTRIP_BLOCKS = False
+NEWLINE_SEQUENCE: "te.Literal['\\n', '\\r\\n', '\\r']" = "\n"
+KEEP_TRAILING_NEWLINE = False
+
+# default filters, tests and namespace
+
+DEFAULT_NAMESPACE = {
+ "range": range,
+ "dict": dict,
+ "lipsum": generate_lorem_ipsum,
+ "cycler": Cycler,
+ "joiner": Joiner,
+ "namespace": Namespace,
+}
+
+# default policies
+DEFAULT_POLICIES: t.Dict[str, t.Any] = {
+ "compiler.ascii_str": True,
+ "urlize.rel": "noopener",
+ "urlize.target": None,
+ "urlize.extra_schemes": None,
+ "truncate.leeway": 5,
+ "json.dumps_function": None,
+ "json.dumps_kwargs": {"sort_keys": True},
+ "ext.i18n.trimmed": False,
+}
diff --git a/venv/Lib/site-packages/jinja2/environment.py b/venv/Lib/site-packages/jinja2/environment.py
new file mode 100644
index 0000000..0fc6e5b
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2/environment.py
@@ -0,0 +1,1672 @@
+"""Classes for managing templates and their runtime and compile time
+options.
+"""
+
+import os
+import typing
+import typing as t
+import weakref
+from collections import ChainMap
+from functools import lru_cache
+from functools import partial
+from functools import reduce
+from types import CodeType
+
+from markupsafe import Markup
+
+from . import nodes
+from .compiler import CodeGenerator
+from .compiler import generate
+from .defaults import BLOCK_END_STRING
+from .defaults import BLOCK_START_STRING
+from .defaults import COMMENT_END_STRING
+from .defaults import COMMENT_START_STRING
+from .defaults import DEFAULT_FILTERS # type: ignore[attr-defined]
+from .defaults import DEFAULT_NAMESPACE
+from .defaults import DEFAULT_POLICIES
+from .defaults import DEFAULT_TESTS # type: ignore[attr-defined]
+from .defaults import KEEP_TRAILING_NEWLINE
+from .defaults import LINE_COMMENT_PREFIX
+from .defaults import LINE_STATEMENT_PREFIX
+from .defaults import LSTRIP_BLOCKS
+from .defaults import NEWLINE_SEQUENCE
+from .defaults import TRIM_BLOCKS
+from .defaults import VARIABLE_END_STRING
+from .defaults import VARIABLE_START_STRING
+from .exceptions import TemplateNotFound
+from .exceptions import TemplateRuntimeError
+from .exceptions import TemplatesNotFound
+from .exceptions import TemplateSyntaxError
+from .exceptions import UndefinedError
+from .lexer import get_lexer
+from .lexer import Lexer
+from .lexer import TokenStream
+from .nodes import EvalContext
+from .parser import Parser
+from .runtime import Context
+from .runtime import new_context
+from .runtime import Undefined
+from .utils import _PassArg
+from .utils import concat
+from .utils import consume
+from .utils import import_string
+from .utils import internalcode
+from .utils import LRUCache
+from .utils import missing
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+ from .bccache import BytecodeCache
+ from .ext import Extension
+ from .loaders import BaseLoader
+
+_env_bound = t.TypeVar("_env_bound", bound="Environment")
+
+
+# for direct template usage we have up to ten living environments
+@lru_cache(maxsize=10)
+def get_spontaneous_environment(cls: t.Type[_env_bound], *args: t.Any) -> _env_bound:
+ """Return a new spontaneous environment. A spontaneous environment
+ is used for templates created directly rather than through an
+ existing environment.
+
+ :param cls: Environment class to create.
+ :param args: Positional arguments passed to environment.
+ """
+ env = cls(*args)
+ env.shared = True
+ return env
+
+
+def create_cache(
+ size: int,
+) -> t.Optional[t.MutableMapping[t.Tuple["weakref.ref[t.Any]", str], "Template"]]:
+ """Return the cache class for the given size."""
+ if size == 0:
+ return None
+
+ if size < 0:
+ return {}
+
+ return LRUCache(size) # type: ignore
+
+
+def copy_cache(
+ cache: t.Optional[t.MutableMapping[t.Any, t.Any]],
+) -> t.Optional[t.MutableMapping[t.Tuple["weakref.ref[t.Any]", str], "Template"]]:
+ """Create an empty copy of the given cache."""
+ if cache is None:
+ return None
+
+ if type(cache) is dict: # noqa E721
+ return {}
+
+ return LRUCache(cache.capacity) # type: ignore
+
+
+def load_extensions(
+ environment: "Environment",
+ extensions: t.Sequence[t.Union[str, t.Type["Extension"]]],
+) -> t.Dict[str, "Extension"]:
+ """Load the extensions from the list and bind it to the environment.
+ Returns a dict of instantiated extensions.
+ """
+ result = {}
+
+ for extension in extensions:
+ if isinstance(extension, str):
+ extension = t.cast(t.Type["Extension"], import_string(extension))
+
+ result[extension.identifier] = extension(environment)
+
+ return result
+
+
+def _environment_config_check(environment: _env_bound) -> _env_bound:
+ """Perform a sanity check on the environment."""
+ assert issubclass(
+ environment.undefined, Undefined
+ ), "'undefined' must be a subclass of 'jinja2.Undefined'."
+ assert (
+ environment.block_start_string
+ != environment.variable_start_string
+ != environment.comment_start_string
+ ), "block, variable and comment start strings must be different."
+ assert environment.newline_sequence in {
+ "\r",
+ "\r\n",
+ "\n",
+ }, "'newline_sequence' must be one of '\\n', '\\r\\n', or '\\r'."
+ return environment
+
+
+class Environment:
+ r"""The core component of Jinja is the `Environment`. It contains
+ important shared variables like configuration, filters, tests,
+ globals and others. Instances of this class may be modified if
+ they are not shared and if no template was loaded so far.
+ Modifications on environments after the first template was loaded
+ will lead to surprising effects and undefined behavior.
+
+ Here are the possible initialization parameters:
+
+ `block_start_string`
+ The string marking the beginning of a block. Defaults to ``'{%'``.
+
+ `block_end_string`
+ The string marking the end of a block. Defaults to ``'%}'``.
+
+ `variable_start_string`
+ The string marking the beginning of a print statement.
+ Defaults to ``'{{'``.
+
+ `variable_end_string`
+ The string marking the end of a print statement. Defaults to
+ ``'}}'``.
+
+ `comment_start_string`
+ The string marking the beginning of a comment. Defaults to ``'{#'``.
+
+ `comment_end_string`
+ The string marking the end of a comment. Defaults to ``'#}'``.
+
+ `line_statement_prefix`
+ If given and a string, this will be used as prefix for line based
+ statements. See also :ref:`line-statements`.
+
+ `line_comment_prefix`
+ If given and a string, this will be used as prefix for line based
+ comments. See also :ref:`line-statements`.
+
+ .. versionadded:: 2.2
+
+ `trim_blocks`
+ If this is set to ``True`` the first newline after a block is
+ removed (block, not variable tag!). Defaults to `False`.
+
+ `lstrip_blocks`
+ If this is set to ``True`` leading spaces and tabs are stripped
+ from the start of a line to a block. Defaults to `False`.
+
+ `newline_sequence`
+ The sequence that starts a newline. Must be one of ``'\r'``,
+ ``'\n'`` or ``'\r\n'``. The default is ``'\n'`` which is a
+ useful default for Linux and OS X systems as well as web
+ applications.
+
+ `keep_trailing_newline`
+ Preserve the trailing newline when rendering templates.
+ The default is ``False``, which causes a single newline,
+ if present, to be stripped from the end of the template.
+
+ .. versionadded:: 2.7
+
+ `extensions`
+ List of Jinja extensions to use. This can either be import paths
+ as strings or extension classes. For more information have a
+ look at :ref:`the extensions documentation `.
+
+ `optimized`
+ should the optimizer be enabled? Default is ``True``.
+
+ `undefined`
+ :class:`Undefined` or a subclass of it that is used to represent
+ undefined values in the template.
+
+ `finalize`
+ A callable that can be used to process the result of a variable
+ expression before it is output. For example one can convert
+ ``None`` implicitly into an empty string here.
+
+ `autoescape`
+ If set to ``True`` the XML/HTML autoescaping feature is enabled by
+ default. For more details about autoescaping see
+ :class:`~markupsafe.Markup`. As of Jinja 2.4 this can also
+ be a callable that is passed the template name and has to
+ return ``True`` or ``False`` depending on autoescape should be
+ enabled by default.
+
+ .. versionchanged:: 2.4
+ `autoescape` can now be a function
+
+ `loader`
+ The template loader for this environment.
+
+ `cache_size`
+ The size of the cache. Per default this is ``400`` which means
+ that if more than 400 templates are loaded the loader will clean
+ out the least recently used template. If the cache size is set to
+ ``0`` templates are recompiled all the time, if the cache size is
+ ``-1`` the cache will not be cleaned.
+
+ .. versionchanged:: 2.8
+ The cache size was increased to 400 from a low 50.
+
+ `auto_reload`
+ Some loaders load templates from locations where the template
+ sources may change (ie: file system or database). If
+ ``auto_reload`` is set to ``True`` (default) every time a template is
+ requested the loader checks if the source changed and if yes, it
+ will reload the template. For higher performance it's possible to
+ disable that.
+
+ `bytecode_cache`
+ If set to a bytecode cache object, this object will provide a
+ cache for the internal Jinja bytecode so that templates don't
+ have to be parsed if they were not changed.
+
+ See :ref:`bytecode-cache` for more information.
+
+ `enable_async`
+ If set to true this enables async template execution which
+ allows using async functions and generators.
+ """
+
+ #: if this environment is sandboxed. Modifying this variable won't make
+ #: the environment sandboxed though. For a real sandboxed environment
+ #: have a look at jinja2.sandbox. This flag alone controls the code
+ #: generation by the compiler.
+ sandboxed = False
+
+ #: True if the environment is just an overlay
+ overlayed = False
+
+ #: the environment this environment is linked to if it is an overlay
+ linked_to: t.Optional["Environment"] = None
+
+ #: shared environments have this set to `True`. A shared environment
+ #: must not be modified
+ shared = False
+
+ #: the class that is used for code generation. See
+ #: :class:`~jinja2.compiler.CodeGenerator` for more information.
+ code_generator_class: t.Type["CodeGenerator"] = CodeGenerator
+
+ concat = "".join
+
+ #: the context class that is used for templates. See
+ #: :class:`~jinja2.runtime.Context` for more information.
+ context_class: t.Type[Context] = Context
+
+ template_class: t.Type["Template"]
+
+ def __init__(
+ self,
+ block_start_string: str = BLOCK_START_STRING,
+ block_end_string: str = BLOCK_END_STRING,
+ variable_start_string: str = VARIABLE_START_STRING,
+ variable_end_string: str = VARIABLE_END_STRING,
+ comment_start_string: str = COMMENT_START_STRING,
+ comment_end_string: str = COMMENT_END_STRING,
+ line_statement_prefix: t.Optional[str] = LINE_STATEMENT_PREFIX,
+ line_comment_prefix: t.Optional[str] = LINE_COMMENT_PREFIX,
+ trim_blocks: bool = TRIM_BLOCKS,
+ lstrip_blocks: bool = LSTRIP_BLOCKS,
+ newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = NEWLINE_SEQUENCE,
+ keep_trailing_newline: bool = KEEP_TRAILING_NEWLINE,
+ extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = (),
+ optimized: bool = True,
+ undefined: t.Type[Undefined] = Undefined,
+ finalize: t.Optional[t.Callable[..., t.Any]] = None,
+ autoescape: t.Union[bool, t.Callable[[t.Optional[str]], bool]] = False,
+ loader: t.Optional["BaseLoader"] = None,
+ cache_size: int = 400,
+ auto_reload: bool = True,
+ bytecode_cache: t.Optional["BytecodeCache"] = None,
+ enable_async: bool = False,
+ ):
+ # !!Important notice!!
+ # The constructor accepts quite a few arguments that should be
+ # passed by keyword rather than position. However it's important to
+ # not change the order of arguments because it's used at least
+ # internally in those cases:
+ # - spontaneous environments (i18n extension and Template)
+ # - unittests
+ # If parameter changes are required only add parameters at the end
+ # and don't change the arguments (or the defaults!) of the arguments
+ # existing already.
+
+ # lexer / parser information
+ self.block_start_string = block_start_string
+ self.block_end_string = block_end_string
+ self.variable_start_string = variable_start_string
+ self.variable_end_string = variable_end_string
+ self.comment_start_string = comment_start_string
+ self.comment_end_string = comment_end_string
+ self.line_statement_prefix = line_statement_prefix
+ self.line_comment_prefix = line_comment_prefix
+ self.trim_blocks = trim_blocks
+ self.lstrip_blocks = lstrip_blocks
+ self.newline_sequence = newline_sequence
+ self.keep_trailing_newline = keep_trailing_newline
+
+ # runtime information
+ self.undefined: t.Type[Undefined] = undefined
+ self.optimized = optimized
+ self.finalize = finalize
+ self.autoescape = autoescape
+
+ # defaults
+ self.filters = DEFAULT_FILTERS.copy()
+ self.tests = DEFAULT_TESTS.copy()
+ self.globals = DEFAULT_NAMESPACE.copy()
+
+ # set the loader provided
+ self.loader = loader
+ self.cache = create_cache(cache_size)
+ self.bytecode_cache = bytecode_cache
+ self.auto_reload = auto_reload
+
+ # configurable policies
+ self.policies = DEFAULT_POLICIES.copy()
+
+ # load extensions
+ self.extensions = load_extensions(self, extensions)
+
+ self.is_async = enable_async
+ _environment_config_check(self)
+
+ def add_extension(self, extension: t.Union[str, t.Type["Extension"]]) -> None:
+ """Adds an extension after the environment was created.
+
+ .. versionadded:: 2.5
+ """
+ self.extensions.update(load_extensions(self, [extension]))
+
+ def extend(self, **attributes: t.Any) -> None:
+ """Add the items to the instance of the environment if they do not exist
+ yet. This is used by :ref:`extensions ` to register
+ callbacks and configuration values without breaking inheritance.
+ """
+ for key, value in attributes.items():
+ if not hasattr(self, key):
+ setattr(self, key, value)
+
+ def overlay(
+ self,
+ block_start_string: str = missing,
+ block_end_string: str = missing,
+ variable_start_string: str = missing,
+ variable_end_string: str = missing,
+ comment_start_string: str = missing,
+ comment_end_string: str = missing,
+ line_statement_prefix: t.Optional[str] = missing,
+ line_comment_prefix: t.Optional[str] = missing,
+ trim_blocks: bool = missing,
+ lstrip_blocks: bool = missing,
+ newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = missing,
+ keep_trailing_newline: bool = missing,
+ extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = missing,
+ optimized: bool = missing,
+ undefined: t.Type[Undefined] = missing,
+ finalize: t.Optional[t.Callable[..., t.Any]] = missing,
+ autoescape: t.Union[bool, t.Callable[[t.Optional[str]], bool]] = missing,
+ loader: t.Optional["BaseLoader"] = missing,
+ cache_size: int = missing,
+ auto_reload: bool = missing,
+ bytecode_cache: t.Optional["BytecodeCache"] = missing,
+ enable_async: bool = missing,
+ ) -> "te.Self":
+ """Create a new overlay environment that shares all the data with the
+ current environment except for cache and the overridden attributes.
+ Extensions cannot be removed for an overlayed environment. An overlayed
+ environment automatically gets all the extensions of the environment it
+ is linked to plus optional extra extensions.
+
+ Creating overlays should happen after the initial environment was set
+ up completely. Not all attributes are truly linked, some are just
+ copied over so modifications on the original environment may not shine
+ through.
+
+ .. versionchanged:: 3.1.5
+ ``enable_async`` is applied correctly.
+
+ .. versionchanged:: 3.1.2
+ Added the ``newline_sequence``, ``keep_trailing_newline``,
+ and ``enable_async`` parameters to match ``__init__``.
+ """
+ args = dict(locals())
+ del args["self"], args["cache_size"], args["extensions"], args["enable_async"]
+
+ rv = object.__new__(self.__class__)
+ rv.__dict__.update(self.__dict__)
+ rv.overlayed = True
+ rv.linked_to = self
+
+ for key, value in args.items():
+ if value is not missing:
+ setattr(rv, key, value)
+
+ if cache_size is not missing:
+ rv.cache = create_cache(cache_size)
+ else:
+ rv.cache = copy_cache(self.cache)
+
+ rv.extensions = {}
+ for key, value in self.extensions.items():
+ rv.extensions[key] = value.bind(rv)
+ if extensions is not missing:
+ rv.extensions.update(load_extensions(rv, extensions))
+
+ if enable_async is not missing:
+ rv.is_async = enable_async
+
+ return _environment_config_check(rv)
+
+ @property
+ def lexer(self) -> Lexer:
+ """The lexer for this environment."""
+ return get_lexer(self)
+
+ def iter_extensions(self) -> t.Iterator["Extension"]:
+ """Iterates over the extensions by priority."""
+ return iter(sorted(self.extensions.values(), key=lambda x: x.priority))
+
+ def getitem(
+ self, obj: t.Any, argument: t.Union[str, t.Any]
+ ) -> t.Union[t.Any, Undefined]:
+ """Get an item or attribute of an object but prefer the item."""
+ try:
+ return obj[argument]
+ except (AttributeError, TypeError, LookupError):
+ if isinstance(argument, str):
+ try:
+ attr = str(argument)
+ except Exception:
+ pass
+ else:
+ try:
+ return getattr(obj, attr)
+ except AttributeError:
+ pass
+ return self.undefined(obj=obj, name=argument)
+
+ def getattr(self, obj: t.Any, attribute: str) -> t.Any:
+ """Get an item or attribute of an object but prefer the attribute.
+ Unlike :meth:`getitem` the attribute *must* be a string.
+ """
+ try:
+ return getattr(obj, attribute)
+ except AttributeError:
+ pass
+ try:
+ return obj[attribute]
+ except (TypeError, LookupError, AttributeError):
+ return self.undefined(obj=obj, name=attribute)
+
+ def _filter_test_common(
+ self,
+ name: t.Union[str, Undefined],
+ value: t.Any,
+ args: t.Optional[t.Sequence[t.Any]],
+ kwargs: t.Optional[t.Mapping[str, t.Any]],
+ context: t.Optional[Context],
+ eval_ctx: t.Optional[EvalContext],
+ is_filter: bool,
+ ) -> t.Any:
+ if is_filter:
+ env_map = self.filters
+ type_name = "filter"
+ else:
+ env_map = self.tests
+ type_name = "test"
+
+ func = env_map.get(name) # type: ignore
+
+ if func is None:
+ msg = f"No {type_name} named {name!r}."
+
+ if isinstance(name, Undefined):
+ try:
+ name._fail_with_undefined_error()
+ except Exception as e:
+ msg = f"{msg} ({e}; did you forget to quote the callable name?)"
+
+ raise TemplateRuntimeError(msg)
+
+ args = [value, *(args if args is not None else ())]
+ kwargs = kwargs if kwargs is not None else {}
+ pass_arg = _PassArg.from_obj(func)
+
+ if pass_arg is _PassArg.context:
+ if context is None:
+ raise TemplateRuntimeError(
+ f"Attempted to invoke a context {type_name} without context."
+ )
+
+ args.insert(0, context)
+ elif pass_arg is _PassArg.eval_context:
+ if eval_ctx is None:
+ if context is not None:
+ eval_ctx = context.eval_ctx
+ else:
+ eval_ctx = EvalContext(self)
+
+ args.insert(0, eval_ctx)
+ elif pass_arg is _PassArg.environment:
+ args.insert(0, self)
+
+ return func(*args, **kwargs)
+
+ def call_filter(
+ self,
+ name: str,
+ value: t.Any,
+ args: t.Optional[t.Sequence[t.Any]] = None,
+ kwargs: t.Optional[t.Mapping[str, t.Any]] = None,
+ context: t.Optional[Context] = None,
+ eval_ctx: t.Optional[EvalContext] = None,
+ ) -> t.Any:
+ """Invoke a filter on a value the same way the compiler does.
+
+ This might return a coroutine if the filter is running from an
+ environment in async mode and the filter supports async
+ execution. It's your responsibility to await this if needed.
+
+ .. versionadded:: 2.7
+ """
+ return self._filter_test_common(
+ name, value, args, kwargs, context, eval_ctx, True
+ )
+
+ def call_test(
+ self,
+ name: str,
+ value: t.Any,
+ args: t.Optional[t.Sequence[t.Any]] = None,
+ kwargs: t.Optional[t.Mapping[str, t.Any]] = None,
+ context: t.Optional[Context] = None,
+ eval_ctx: t.Optional[EvalContext] = None,
+ ) -> t.Any:
+ """Invoke a test on a value the same way the compiler does.
+
+ This might return a coroutine if the test is running from an
+ environment in async mode and the test supports async execution.
+ It's your responsibility to await this if needed.
+
+ .. versionchanged:: 3.0
+ Tests support ``@pass_context``, etc. decorators. Added
+ the ``context`` and ``eval_ctx`` parameters.
+
+ .. versionadded:: 2.7
+ """
+ return self._filter_test_common(
+ name, value, args, kwargs, context, eval_ctx, False
+ )
+
+ @internalcode
+ def parse(
+ self,
+ source: str,
+ name: t.Optional[str] = None,
+ filename: t.Optional[str] = None,
+ ) -> nodes.Template:
+ """Parse the sourcecode and return the abstract syntax tree. This
+ tree of nodes is used by the compiler to convert the template into
+ executable source- or bytecode. This is useful for debugging or to
+ extract information from templates.
+
+ If you are :ref:`developing Jinja extensions `
+ this gives you a good overview of the node tree generated.
+ """
+ try:
+ return self._parse(source, name, filename)
+ except TemplateSyntaxError:
+ self.handle_exception(source=source)
+
+ def _parse(
+ self, source: str, name: t.Optional[str], filename: t.Optional[str]
+ ) -> nodes.Template:
+ """Internal parsing function used by `parse` and `compile`."""
+ return Parser(self, source, name, filename).parse()
+
+ def lex(
+ self,
+ source: str,
+ name: t.Optional[str] = None,
+ filename: t.Optional[str] = None,
+ ) -> t.Iterator[t.Tuple[int, str, str]]:
+ """Lex the given sourcecode and return a generator that yields
+ tokens as tuples in the form ``(lineno, token_type, value)``.
+ This can be useful for :ref:`extension development `
+ and debugging templates.
+
+ This does not perform preprocessing. If you want the preprocessing
+ of the extensions to be applied you have to filter source through
+ the :meth:`preprocess` method.
+ """
+ source = str(source)
+ try:
+ return self.lexer.tokeniter(source, name, filename)
+ except TemplateSyntaxError:
+ self.handle_exception(source=source)
+
+ def preprocess(
+ self,
+ source: str,
+ name: t.Optional[str] = None,
+ filename: t.Optional[str] = None,
+ ) -> str:
+ """Preprocesses the source with all extensions. This is automatically
+ called for all parsing and compiling methods but *not* for :meth:`lex`
+ because there you usually only want the actual source tokenized.
+ """
+ return reduce(
+ lambda s, e: e.preprocess(s, name, filename),
+ self.iter_extensions(),
+ str(source),
+ )
+
+ def _tokenize(
+ self,
+ source: str,
+ name: t.Optional[str],
+ filename: t.Optional[str] = None,
+ state: t.Optional[str] = None,
+ ) -> TokenStream:
+ """Called by the parser to do the preprocessing and filtering
+ for all the extensions. Returns a :class:`~jinja2.lexer.TokenStream`.
+ """
+ source = self.preprocess(source, name, filename)
+ stream = self.lexer.tokenize(source, name, filename, state)
+
+ for ext in self.iter_extensions():
+ stream = ext.filter_stream(stream) # type: ignore
+
+ if not isinstance(stream, TokenStream):
+ stream = TokenStream(stream, name, filename)
+
+ return stream
+
+ def _generate(
+ self,
+ source: nodes.Template,
+ name: t.Optional[str],
+ filename: t.Optional[str],
+ defer_init: bool = False,
+ ) -> str:
+ """Internal hook that can be overridden to hook a different generate
+ method in.
+
+ .. versionadded:: 2.5
+ """
+ return generate( # type: ignore
+ source,
+ self,
+ name,
+ filename,
+ defer_init=defer_init,
+ optimized=self.optimized,
+ )
+
+ def _compile(self, source: str, filename: str) -> CodeType:
+ """Internal hook that can be overridden to hook a different compile
+ method in.
+
+ .. versionadded:: 2.5
+ """
+ return compile(source, filename, "exec")
+
+ @typing.overload
+ def compile(
+ self,
+ source: t.Union[str, nodes.Template],
+ name: t.Optional[str] = None,
+ filename: t.Optional[str] = None,
+ raw: "te.Literal[False]" = False,
+ defer_init: bool = False,
+ ) -> CodeType: ...
+
+ @typing.overload
+ def compile(
+ self,
+ source: t.Union[str, nodes.Template],
+ name: t.Optional[str] = None,
+ filename: t.Optional[str] = None,
+ raw: "te.Literal[True]" = ...,
+ defer_init: bool = False,
+ ) -> str: ...
+
+ @internalcode
+ def compile(
+ self,
+ source: t.Union[str, nodes.Template],
+ name: t.Optional[str] = None,
+ filename: t.Optional[str] = None,
+ raw: bool = False,
+ defer_init: bool = False,
+ ) -> t.Union[str, CodeType]:
+ """Compile a node or template source code. The `name` parameter is
+ the load name of the template after it was joined using
+ :meth:`join_path` if necessary, not the filename on the file system.
+ the `filename` parameter is the estimated filename of the template on
+ the file system. If the template came from a database or memory this
+ can be omitted.
+
+ The return value of this method is a python code object. If the `raw`
+ parameter is `True` the return value will be a string with python
+ code equivalent to the bytecode returned otherwise. This method is
+ mainly used internally.
+
+ `defer_init` is use internally to aid the module code generator. This
+ causes the generated code to be able to import without the global
+ environment variable to be set.
+
+ .. versionadded:: 2.4
+ `defer_init` parameter added.
+ """
+ source_hint = None
+ try:
+ if isinstance(source, str):
+ source_hint = source
+ source = self._parse(source, name, filename)
+ source = self._generate(source, name, filename, defer_init=defer_init)
+ if raw:
+ return source
+ if filename is None:
+ filename = ""
+ return self._compile(source, filename)
+ except TemplateSyntaxError:
+ self.handle_exception(source=source_hint)
+
+ def compile_expression(
+ self, source: str, undefined_to_none: bool = True
+ ) -> "TemplateExpression":
+ """A handy helper method that returns a callable that accepts keyword
+ arguments that appear as variables in the expression. If called it
+ returns the result of the expression.
+
+ This is useful if applications want to use the same rules as Jinja
+ in template "configuration files" or similar situations.
+
+ Example usage:
+
+ >>> env = Environment()
+ >>> expr = env.compile_expression('foo == 42')
+ >>> expr(foo=23)
+ False
+ >>> expr(foo=42)
+ True
+
+ Per default the return value is converted to `None` if the
+ expression returns an undefined value. This can be changed
+ by setting `undefined_to_none` to `False`.
+
+ >>> env.compile_expression('var')() is None
+ True
+ >>> env.compile_expression('var', undefined_to_none=False)()
+ Undefined
+
+ .. versionadded:: 2.1
+ """
+ parser = Parser(self, source, state="variable")
+ try:
+ expr = parser.parse_expression()
+ if not parser.stream.eos:
+ raise TemplateSyntaxError(
+ "chunk after expression", parser.stream.current.lineno, None, None
+ )
+ expr.set_environment(self)
+ except TemplateSyntaxError:
+ self.handle_exception(source=source)
+
+ body = [nodes.Assign(nodes.Name("result", "store"), expr, lineno=1)]
+ template = self.from_string(nodes.Template(body, lineno=1))
+ return TemplateExpression(template, undefined_to_none)
+
+ def compile_templates(
+ self,
+ target: t.Union[str, "os.PathLike[str]"],
+ extensions: t.Optional[t.Collection[str]] = None,
+ filter_func: t.Optional[t.Callable[[str], bool]] = None,
+ zip: t.Optional[str] = "deflated",
+ log_function: t.Optional[t.Callable[[str], None]] = None,
+ ignore_errors: bool = True,
+ ) -> None:
+ """Finds all the templates the loader can find, compiles them
+ and stores them in `target`. If `zip` is `None`, instead of in a
+ zipfile, the templates will be stored in a directory.
+ By default a deflate zip algorithm is used. To switch to
+ the stored algorithm, `zip` can be set to ``'stored'``.
+
+ `extensions` and `filter_func` are passed to :meth:`list_templates`.
+ Each template returned will be compiled to the target folder or
+ zipfile.
+
+ By default template compilation errors are ignored. In case a
+ log function is provided, errors are logged. If you want template
+ syntax errors to abort the compilation you can set `ignore_errors`
+ to `False` and you will get an exception on syntax errors.
+
+ .. versionadded:: 2.4
+ """
+ from .loaders import ModuleLoader
+
+ if log_function is None:
+
+ def log_function(x: str) -> None:
+ pass
+
+ assert log_function is not None
+ assert self.loader is not None, "No loader configured."
+
+ def write_file(filename: str, data: str) -> None:
+ if zip:
+ info = ZipInfo(filename)
+ info.external_attr = 0o755 << 16
+ zip_file.writestr(info, data)
+ else:
+ with open(os.path.join(target, filename), "wb") as f:
+ f.write(data.encode("utf8"))
+
+ if zip is not None:
+ from zipfile import ZIP_DEFLATED
+ from zipfile import ZIP_STORED
+ from zipfile import ZipFile
+ from zipfile import ZipInfo
+
+ zip_file = ZipFile(
+ target, "w", dict(deflated=ZIP_DEFLATED, stored=ZIP_STORED)[zip]
+ )
+ log_function(f"Compiling into Zip archive {target!r}")
+ else:
+ if not os.path.isdir(target):
+ os.makedirs(target)
+ log_function(f"Compiling into folder {target!r}")
+
+ try:
+ for name in self.list_templates(extensions, filter_func):
+ source, filename, _ = self.loader.get_source(self, name)
+ try:
+ code = self.compile(source, name, filename, True, True)
+ except TemplateSyntaxError as e:
+ if not ignore_errors:
+ raise
+ log_function(f'Could not compile "{name}": {e}')
+ continue
+
+ filename = ModuleLoader.get_module_filename(name)
+
+ write_file(filename, code)
+ log_function(f'Compiled "{name}" as {filename}')
+ finally:
+ if zip:
+ zip_file.close()
+
+ log_function("Finished compiling templates")
+
+ def list_templates(
+ self,
+ extensions: t.Optional[t.Collection[str]] = None,
+ filter_func: t.Optional[t.Callable[[str], bool]] = None,
+ ) -> t.List[str]:
+ """Returns a list of templates for this environment. This requires
+ that the loader supports the loader's
+ :meth:`~BaseLoader.list_templates` method.
+
+ If there are other files in the template folder besides the
+ actual templates, the returned list can be filtered. There are two
+ ways: either `extensions` is set to a list of file extensions for
+ templates, or a `filter_func` can be provided which is a callable that
+ is passed a template name and should return `True` if it should end up
+ in the result list.
+
+ If the loader does not support that, a :exc:`TypeError` is raised.
+
+ .. versionadded:: 2.4
+ """
+ assert self.loader is not None, "No loader configured."
+ names = self.loader.list_templates()
+
+ if extensions is not None:
+ if filter_func is not None:
+ raise TypeError(
+ "either extensions or filter_func can be passed, but not both"
+ )
+
+ def filter_func(x: str) -> bool:
+ return "." in x and x.rsplit(".", 1)[1] in extensions
+
+ if filter_func is not None:
+ names = [name for name in names if filter_func(name)]
+
+ return names
+
+ def handle_exception(self, source: t.Optional[str] = None) -> "te.NoReturn":
+ """Exception handling helper. This is used internally to either raise
+ rewritten exceptions or return a rendered traceback for the template.
+ """
+ from .debug import rewrite_traceback_stack
+
+ raise rewrite_traceback_stack(source=source)
+
+ def join_path(self, template: str, parent: str) -> str:
+ """Join a template with the parent. By default all the lookups are
+ relative to the loader root so this method returns the `template`
+ parameter unchanged, but if the paths should be relative to the
+ parent template, this function can be used to calculate the real
+ template name.
+
+ Subclasses may override this method and implement template path
+ joining here.
+ """
+ return template
+
+ @internalcode
+ def _load_template(
+ self, name: str, globals: t.Optional[t.MutableMapping[str, t.Any]]
+ ) -> "Template":
+ if self.loader is None:
+ raise TypeError("no loader for this environment specified")
+ cache_key = (weakref.ref(self.loader), name)
+ if self.cache is not None:
+ template = self.cache.get(cache_key)
+ if template is not None and (
+ not self.auto_reload or template.is_up_to_date
+ ):
+ # template.globals is a ChainMap, modifying it will only
+ # affect the template, not the environment globals.
+ if globals:
+ template.globals.update(globals)
+
+ return template
+
+ template = self.loader.load(self, name, self.make_globals(globals))
+
+ if self.cache is not None:
+ self.cache[cache_key] = template
+ return template
+
+ @internalcode
+ def get_template(
+ self,
+ name: t.Union[str, "Template"],
+ parent: t.Optional[str] = None,
+ globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
+ ) -> "Template":
+ """Load a template by name with :attr:`loader` and return a
+ :class:`Template`. If the template does not exist a
+ :exc:`TemplateNotFound` exception is raised.
+
+ :param name: Name of the template to load. When loading
+ templates from the filesystem, "/" is used as the path
+ separator, even on Windows.
+ :param parent: The name of the parent template importing this
+ template. :meth:`join_path` can be used to implement name
+ transformations with this.
+ :param globals: Extend the environment :attr:`globals` with
+ these extra variables available for all renders of this
+ template. If the template has already been loaded and
+ cached, its globals are updated with any new items.
+
+ .. versionchanged:: 3.0
+ If a template is loaded from cache, ``globals`` will update
+ the template's globals instead of ignoring the new values.
+
+ .. versionchanged:: 2.4
+ If ``name`` is a :class:`Template` object it is returned
+ unchanged.
+ """
+ if isinstance(name, Template):
+ return name
+ if parent is not None:
+ name = self.join_path(name, parent)
+
+ return self._load_template(name, globals)
+
+ @internalcode
+ def select_template(
+ self,
+ names: t.Iterable[t.Union[str, "Template"]],
+ parent: t.Optional[str] = None,
+ globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
+ ) -> "Template":
+ """Like :meth:`get_template`, but tries loading multiple names.
+ If none of the names can be loaded a :exc:`TemplatesNotFound`
+ exception is raised.
+
+ :param names: List of template names to try loading in order.
+ :param parent: The name of the parent template importing this
+ template. :meth:`join_path` can be used to implement name
+ transformations with this.
+ :param globals: Extend the environment :attr:`globals` with
+ these extra variables available for all renders of this
+ template. If the template has already been loaded and
+ cached, its globals are updated with any new items.
+
+ .. versionchanged:: 3.0
+ If a template is loaded from cache, ``globals`` will update
+ the template's globals instead of ignoring the new values.
+
+ .. versionchanged:: 2.11
+ If ``names`` is :class:`Undefined`, an :exc:`UndefinedError`
+ is raised instead. If no templates were found and ``names``
+ contains :class:`Undefined`, the message is more helpful.
+
+ .. versionchanged:: 2.4
+ If ``names`` contains a :class:`Template` object it is
+ returned unchanged.
+
+ .. versionadded:: 2.3
+ """
+ if isinstance(names, Undefined):
+ names._fail_with_undefined_error()
+
+ if not names:
+ raise TemplatesNotFound(
+ message="Tried to select from an empty list of templates."
+ )
+
+ for name in names:
+ if isinstance(name, Template):
+ return name
+ if parent is not None:
+ name = self.join_path(name, parent)
+ try:
+ return self._load_template(name, globals)
+ except (TemplateNotFound, UndefinedError):
+ pass
+ raise TemplatesNotFound(names) # type: ignore
+
+ @internalcode
+ def get_or_select_template(
+ self,
+ template_name_or_list: t.Union[
+ str, "Template", t.List[t.Union[str, "Template"]]
+ ],
+ parent: t.Optional[str] = None,
+ globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
+ ) -> "Template":
+ """Use :meth:`select_template` if an iterable of template names
+ is given, or :meth:`get_template` if one name is given.
+
+ .. versionadded:: 2.3
+ """
+ if isinstance(template_name_or_list, (str, Undefined)):
+ return self.get_template(template_name_or_list, parent, globals)
+ elif isinstance(template_name_or_list, Template):
+ return template_name_or_list
+ return self.select_template(template_name_or_list, parent, globals)
+
+ def from_string(
+ self,
+ source: t.Union[str, nodes.Template],
+ globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
+ template_class: t.Optional[t.Type["Template"]] = None,
+ ) -> "Template":
+ """Load a template from a source string without using
+ :attr:`loader`.
+
+ :param source: Jinja source to compile into a template.
+ :param globals: Extend the environment :attr:`globals` with
+ these extra variables available for all renders of this
+ template. If the template has already been loaded and
+ cached, its globals are updated with any new items.
+ :param template_class: Return an instance of this
+ :class:`Template` class.
+ """
+ gs = self.make_globals(globals)
+ cls = template_class or self.template_class
+ return cls.from_code(self, self.compile(source), gs, None)
+
+ def make_globals(
+ self, d: t.Optional[t.MutableMapping[str, t.Any]]
+ ) -> t.MutableMapping[str, t.Any]:
+ """Make the globals map for a template. Any given template
+ globals overlay the environment :attr:`globals`.
+
+ Returns a :class:`collections.ChainMap`. This allows any changes
+ to a template's globals to only affect that template, while
+ changes to the environment's globals are still reflected.
+ However, avoid modifying any globals after a template is loaded.
+
+ :param d: Dict of template-specific globals.
+
+ .. versionchanged:: 3.0
+ Use :class:`collections.ChainMap` to always prevent mutating
+ environment globals.
+ """
+ if d is None:
+ d = {}
+
+ return ChainMap(d, self.globals)
+
+
+class Template:
+ """A compiled template that can be rendered.
+
+ Use the methods on :class:`Environment` to create or load templates.
+ The environment is used to configure how templates are compiled and
+ behave.
+
+ It is also possible to create a template object directly. This is
+ not usually recommended. The constructor takes most of the same
+ arguments as :class:`Environment`. All templates created with the
+ same environment arguments share the same ephemeral ``Environment``
+ instance behind the scenes.
+
+ A template object should be considered immutable. Modifications on
+ the object are not supported.
+ """
+
+ #: Type of environment to create when creating a template directly
+ #: rather than through an existing environment.
+ environment_class: t.Type[Environment] = Environment
+
+ environment: Environment
+ globals: t.MutableMapping[str, t.Any]
+ name: t.Optional[str]
+ filename: t.Optional[str]
+ blocks: t.Dict[str, t.Callable[[Context], t.Iterator[str]]]
+ root_render_func: t.Callable[[Context], t.Iterator[str]]
+ _module: t.Optional["TemplateModule"]
+ _debug_info: str
+ _uptodate: t.Optional[t.Callable[[], bool]]
+
+ def __new__(
+ cls,
+ source: t.Union[str, nodes.Template],
+ block_start_string: str = BLOCK_START_STRING,
+ block_end_string: str = BLOCK_END_STRING,
+ variable_start_string: str = VARIABLE_START_STRING,
+ variable_end_string: str = VARIABLE_END_STRING,
+ comment_start_string: str = COMMENT_START_STRING,
+ comment_end_string: str = COMMENT_END_STRING,
+ line_statement_prefix: t.Optional[str] = LINE_STATEMENT_PREFIX,
+ line_comment_prefix: t.Optional[str] = LINE_COMMENT_PREFIX,
+ trim_blocks: bool = TRIM_BLOCKS,
+ lstrip_blocks: bool = LSTRIP_BLOCKS,
+ newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = NEWLINE_SEQUENCE,
+ keep_trailing_newline: bool = KEEP_TRAILING_NEWLINE,
+ extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = (),
+ optimized: bool = True,
+ undefined: t.Type[Undefined] = Undefined,
+ finalize: t.Optional[t.Callable[..., t.Any]] = None,
+ autoescape: t.Union[bool, t.Callable[[t.Optional[str]], bool]] = False,
+ enable_async: bool = False,
+ ) -> t.Any: # it returns a `Template`, but this breaks the sphinx build...
+ env = get_spontaneous_environment(
+ cls.environment_class, # type: ignore
+ block_start_string,
+ block_end_string,
+ variable_start_string,
+ variable_end_string,
+ comment_start_string,
+ comment_end_string,
+ line_statement_prefix,
+ line_comment_prefix,
+ trim_blocks,
+ lstrip_blocks,
+ newline_sequence,
+ keep_trailing_newline,
+ frozenset(extensions),
+ optimized,
+ undefined, # type: ignore
+ finalize,
+ autoescape,
+ None,
+ 0,
+ False,
+ None,
+ enable_async,
+ )
+ return env.from_string(source, template_class=cls)
+
+ @classmethod
+ def from_code(
+ cls,
+ environment: Environment,
+ code: CodeType,
+ globals: t.MutableMapping[str, t.Any],
+ uptodate: t.Optional[t.Callable[[], bool]] = None,
+ ) -> "Template":
+ """Creates a template object from compiled code and the globals. This
+ is used by the loaders and environment to create a template object.
+ """
+ namespace = {"environment": environment, "__file__": code.co_filename}
+ exec(code, namespace)
+ rv = cls._from_namespace(environment, namespace, globals)
+ rv._uptodate = uptodate
+ return rv
+
+ @classmethod
+ def from_module_dict(
+ cls,
+ environment: Environment,
+ module_dict: t.MutableMapping[str, t.Any],
+ globals: t.MutableMapping[str, t.Any],
+ ) -> "Template":
+ """Creates a template object from a module. This is used by the
+ module loader to create a template object.
+
+ .. versionadded:: 2.4
+ """
+ return cls._from_namespace(environment, module_dict, globals)
+
+ @classmethod
+ def _from_namespace(
+ cls,
+ environment: Environment,
+ namespace: t.MutableMapping[str, t.Any],
+ globals: t.MutableMapping[str, t.Any],
+ ) -> "Template":
+ t: Template = object.__new__(cls)
+ t.environment = environment
+ t.globals = globals
+ t.name = namespace["name"]
+ t.filename = namespace["__file__"]
+ t.blocks = namespace["blocks"]
+
+ # render function and module
+ t.root_render_func = namespace["root"]
+ t._module = None
+
+ # debug and loader helpers
+ t._debug_info = namespace["debug_info"]
+ t._uptodate = None
+
+ # store the reference
+ namespace["environment"] = environment
+ namespace["__jinja_template__"] = t
+
+ return t
+
+ def render(self, *args: t.Any, **kwargs: t.Any) -> str:
+ """This method accepts the same arguments as the `dict` constructor:
+ A dict, a dict subclass or some keyword arguments. If no arguments
+ are given the context will be empty. These two calls do the same::
+
+ template.render(knights='that say nih')
+ template.render({'knights': 'that say nih'})
+
+ This will return the rendered template as a string.
+ """
+ if self.environment.is_async:
+ import asyncio
+
+ return asyncio.run(self.render_async(*args, **kwargs))
+
+ ctx = self.new_context(dict(*args, **kwargs))
+
+ try:
+ return self.environment.concat(self.root_render_func(ctx)) # type: ignore
+ except Exception:
+ self.environment.handle_exception()
+
+ async def render_async(self, *args: t.Any, **kwargs: t.Any) -> str:
+ """This works similar to :meth:`render` but returns a coroutine
+ that when awaited returns the entire rendered template string. This
+ requires the async feature to be enabled.
+
+ Example usage::
+
+ await template.render_async(knights='that say nih; asynchronously')
+ """
+ if not self.environment.is_async:
+ raise RuntimeError(
+ "The environment was not created with async mode enabled."
+ )
+
+ ctx = self.new_context(dict(*args, **kwargs))
+
+ try:
+ return self.environment.concat( # type: ignore
+ [n async for n in self.root_render_func(ctx)] # type: ignore
+ )
+ except Exception:
+ return self.environment.handle_exception()
+
+ def stream(self, *args: t.Any, **kwargs: t.Any) -> "TemplateStream":
+ """Works exactly like :meth:`generate` but returns a
+ :class:`TemplateStream`.
+ """
+ return TemplateStream(self.generate(*args, **kwargs))
+
+ def generate(self, *args: t.Any, **kwargs: t.Any) -> t.Iterator[str]:
+ """For very large templates it can be useful to not render the whole
+ template at once but evaluate each statement after another and yield
+ piece for piece. This method basically does exactly that and returns
+ a generator that yields one item after another as strings.
+
+ It accepts the same arguments as :meth:`render`.
+ """
+ if self.environment.is_async:
+ import asyncio
+
+ async def to_list() -> t.List[str]:
+ return [x async for x in self.generate_async(*args, **kwargs)]
+
+ yield from asyncio.run(to_list())
+ return
+
+ ctx = self.new_context(dict(*args, **kwargs))
+
+ try:
+ yield from self.root_render_func(ctx)
+ except Exception:
+ yield self.environment.handle_exception()
+
+ async def generate_async(
+ self, *args: t.Any, **kwargs: t.Any
+ ) -> t.AsyncGenerator[str, object]:
+ """An async version of :meth:`generate`. Works very similarly but
+ returns an async iterator instead.
+ """
+ if not self.environment.is_async:
+ raise RuntimeError(
+ "The environment was not created with async mode enabled."
+ )
+
+ ctx = self.new_context(dict(*args, **kwargs))
+
+ try:
+ agen = self.root_render_func(ctx)
+ try:
+ async for event in agen: # type: ignore
+ yield event
+ finally:
+ # we can't use async with aclosing(...) because that's only
+ # in 3.10+
+ await agen.aclose() # type: ignore
+ except Exception:
+ yield self.environment.handle_exception()
+
+ def new_context(
+ self,
+ vars: t.Optional[t.Dict[str, t.Any]] = None,
+ shared: bool = False,
+ locals: t.Optional[t.Mapping[str, t.Any]] = None,
+ ) -> Context:
+ """Create a new :class:`Context` for this template. The vars
+ provided will be passed to the template. Per default the globals
+ are added to the context. If shared is set to `True` the data
+ is passed as is to the context without adding the globals.
+
+ `locals` can be a dict of local variables for internal usage.
+ """
+ return new_context(
+ self.environment, self.name, self.blocks, vars, shared, self.globals, locals
+ )
+
+ def make_module(
+ self,
+ vars: t.Optional[t.Dict[str, t.Any]] = None,
+ shared: bool = False,
+ locals: t.Optional[t.Mapping[str, t.Any]] = None,
+ ) -> "TemplateModule":
+ """This method works like the :attr:`module` attribute when called
+ without arguments but it will evaluate the template on every call
+ rather than caching it. It's also possible to provide
+ a dict which is then used as context. The arguments are the same
+ as for the :meth:`new_context` method.
+ """
+ ctx = self.new_context(vars, shared, locals)
+ return TemplateModule(self, ctx)
+
+ async def make_module_async(
+ self,
+ vars: t.Optional[t.Dict[str, t.Any]] = None,
+ shared: bool = False,
+ locals: t.Optional[t.Mapping[str, t.Any]] = None,
+ ) -> "TemplateModule":
+ """As template module creation can invoke template code for
+ asynchronous executions this method must be used instead of the
+ normal :meth:`make_module` one. Likewise the module attribute
+ becomes unavailable in async mode.
+ """
+ ctx = self.new_context(vars, shared, locals)
+ return TemplateModule(
+ self,
+ ctx,
+ [x async for x in self.root_render_func(ctx)], # type: ignore
+ )
+
+ @internalcode
+ def _get_default_module(self, ctx: t.Optional[Context] = None) -> "TemplateModule":
+ """If a context is passed in, this means that the template was
+ imported. Imported templates have access to the current
+ template's globals by default, but they can only be accessed via
+ the context during runtime.
+
+ If there are new globals, we need to create a new module because
+ the cached module is already rendered and will not have access
+ to globals from the current context. This new module is not
+ cached because the template can be imported elsewhere, and it
+ should have access to only the current template's globals.
+ """
+ if self.environment.is_async:
+ raise RuntimeError("Module is not available in async mode.")
+
+ if ctx is not None:
+ keys = ctx.globals_keys - self.globals.keys()
+
+ if keys:
+ return self.make_module({k: ctx.parent[k] for k in keys})
+
+ if self._module is None:
+ self._module = self.make_module()
+
+ return self._module
+
+ async def _get_default_module_async(
+ self, ctx: t.Optional[Context] = None
+ ) -> "TemplateModule":
+ if ctx is not None:
+ keys = ctx.globals_keys - self.globals.keys()
+
+ if keys:
+ return await self.make_module_async({k: ctx.parent[k] for k in keys})
+
+ if self._module is None:
+ self._module = await self.make_module_async()
+
+ return self._module
+
+ @property
+ def module(self) -> "TemplateModule":
+ """The template as module. This is used for imports in the
+ template runtime but is also useful if one wants to access
+ exported template variables from the Python layer:
+
+ >>> t = Template('{% macro foo() %}42{% endmacro %}23')
+ >>> str(t.module)
+ '23'
+ >>> t.module.foo() == u'42'
+ True
+
+ This attribute is not available if async mode is enabled.
+ """
+ return self._get_default_module()
+
+ def get_corresponding_lineno(self, lineno: int) -> int:
+ """Return the source line number of a line number in the
+ generated bytecode as they are not in sync.
+ """
+ for template_line, code_line in reversed(self.debug_info):
+ if code_line <= lineno:
+ return template_line
+ return 1
+
+ @property
+ def is_up_to_date(self) -> bool:
+ """If this variable is `False` there is a newer version available."""
+ if self._uptodate is None:
+ return True
+ return self._uptodate()
+
+ @property
+ def debug_info(self) -> t.List[t.Tuple[int, int]]:
+ """The debug info mapping."""
+ if self._debug_info:
+ return [
+ tuple(map(int, x.split("="))) # type: ignore
+ for x in self._debug_info.split("&")
+ ]
+
+ return []
+
+ def __repr__(self) -> str:
+ if self.name is None:
+ name = f"memory:{id(self):x}"
+ else:
+ name = repr(self.name)
+ return f"<{type(self).__name__} {name}>"
+
+
+class TemplateModule:
+ """Represents an imported template. All the exported names of the
+ template are available as attributes on this object. Additionally
+ converting it into a string renders the contents.
+ """
+
+ def __init__(
+ self,
+ template: Template,
+ context: Context,
+ body_stream: t.Optional[t.Iterable[str]] = None,
+ ) -> None:
+ if body_stream is None:
+ if context.environment.is_async:
+ raise RuntimeError(
+ "Async mode requires a body stream to be passed to"
+ " a template module. Use the async methods of the"
+ " API you are using."
+ )
+
+ body_stream = list(template.root_render_func(context))
+
+ self._body_stream = body_stream
+ self.__dict__.update(context.get_exported())
+ self.__name__ = template.name
+
+ def __html__(self) -> Markup:
+ return Markup(concat(self._body_stream))
+
+ def __str__(self) -> str:
+ return concat(self._body_stream)
+
+ def __repr__(self) -> str:
+ if self.__name__ is None:
+ name = f"memory:{id(self):x}"
+ else:
+ name = repr(self.__name__)
+ return f"<{type(self).__name__} {name}>"
+
+
+class TemplateExpression:
+ """The :meth:`jinja2.Environment.compile_expression` method returns an
+ instance of this object. It encapsulates the expression-like access
+ to the template with an expression it wraps.
+ """
+
+ def __init__(self, template: Template, undefined_to_none: bool) -> None:
+ self._template = template
+ self._undefined_to_none = undefined_to_none
+
+ def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Optional[t.Any]:
+ context = self._template.new_context(dict(*args, **kwargs))
+ consume(self._template.root_render_func(context))
+ rv = context.vars["result"]
+ if self._undefined_to_none and isinstance(rv, Undefined):
+ rv = None
+ return rv
+
+
+class TemplateStream:
+ """A template stream works pretty much like an ordinary python generator
+ but it can buffer multiple items to reduce the number of total iterations.
+ Per default the output is unbuffered which means that for every unbuffered
+ instruction in the template one string is yielded.
+
+ If buffering is enabled with a buffer size of 5, five items are combined
+ into a new string. This is mainly useful if you are streaming
+ big templates to a client via WSGI which flushes after each iteration.
+ """
+
+ def __init__(self, gen: t.Iterator[str]) -> None:
+ self._gen = gen
+ self.disable_buffering()
+
+ def dump(
+ self,
+ fp: t.Union[str, t.IO[bytes]],
+ encoding: t.Optional[str] = None,
+ errors: t.Optional[str] = "strict",
+ ) -> None:
+ """Dump the complete stream into a file or file-like object.
+ Per default strings are written, if you want to encode
+ before writing specify an `encoding`.
+
+ Example usage::
+
+ Template('Hello {{ name }}!').stream(name='foo').dump('hello.html')
+ """
+ close = False
+
+ if isinstance(fp, str):
+ if encoding is None:
+ encoding = "utf-8"
+
+ real_fp: t.IO[bytes] = open(fp, "wb")
+ close = True
+ else:
+ real_fp = fp
+
+ try:
+ if encoding is not None:
+ iterable = (x.encode(encoding, errors) for x in self) # type: ignore
+ else:
+ iterable = self # type: ignore
+
+ if hasattr(real_fp, "writelines"):
+ real_fp.writelines(iterable)
+ else:
+ for item in iterable:
+ real_fp.write(item)
+ finally:
+ if close:
+ real_fp.close()
+
+ def disable_buffering(self) -> None:
+ """Disable the output buffering."""
+ self._next = partial(next, self._gen)
+ self.buffered = False
+
+ def _buffered_generator(self, size: int) -> t.Iterator[str]:
+ buf: t.List[str] = []
+ c_size = 0
+ push = buf.append
+
+ while True:
+ try:
+ while c_size < size:
+ c = next(self._gen)
+ push(c)
+ if c:
+ c_size += 1
+ except StopIteration:
+ if not c_size:
+ return
+ yield concat(buf)
+ del buf[:]
+ c_size = 0
+
+ def enable_buffering(self, size: int = 5) -> None:
+ """Enable buffering. Buffer `size` items before yielding them."""
+ if size <= 1:
+ raise ValueError("buffer size too small")
+
+ self.buffered = True
+ self._next = partial(next, self._buffered_generator(size))
+
+ def __iter__(self) -> "TemplateStream":
+ return self
+
+ def __next__(self) -> str:
+ return self._next() # type: ignore
+
+
+# hook in default template class. if anyone reads this comment: ignore that
+# it's possible to use custom templates ;-)
+Environment.template_class = Template
diff --git a/venv/Lib/site-packages/jinja2/exceptions.py b/venv/Lib/site-packages/jinja2/exceptions.py
new file mode 100644
index 0000000..082ebe8
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2/exceptions.py
@@ -0,0 +1,166 @@
+import typing as t
+
+if t.TYPE_CHECKING:
+ from .runtime import Undefined
+
+
+class TemplateError(Exception):
+ """Baseclass for all template errors."""
+
+ def __init__(self, message: t.Optional[str] = None) -> None:
+ super().__init__(message)
+
+ @property
+ def message(self) -> t.Optional[str]:
+ return self.args[0] if self.args else None
+
+
+class TemplateNotFound(IOError, LookupError, TemplateError):
+ """Raised if a template does not exist.
+
+ .. versionchanged:: 2.11
+ If the given name is :class:`Undefined` and no message was
+ provided, an :exc:`UndefinedError` is raised.
+ """
+
+ # Silence the Python warning about message being deprecated since
+ # it's not valid here.
+ message: t.Optional[str] = None
+
+ def __init__(
+ self,
+ name: t.Optional[t.Union[str, "Undefined"]],
+ message: t.Optional[str] = None,
+ ) -> None:
+ IOError.__init__(self, name)
+
+ if message is None:
+ from .runtime import Undefined
+
+ if isinstance(name, Undefined):
+ name._fail_with_undefined_error()
+
+ message = name
+
+ self.message = message
+ self.name = name
+ self.templates = [name]
+
+ def __str__(self) -> str:
+ return str(self.message)
+
+
+class TemplatesNotFound(TemplateNotFound):
+ """Like :class:`TemplateNotFound` but raised if multiple templates
+ are selected. This is a subclass of :class:`TemplateNotFound`
+ exception, so just catching the base exception will catch both.
+
+ .. versionchanged:: 2.11
+ If a name in the list of names is :class:`Undefined`, a message
+ about it being undefined is shown rather than the empty string.
+
+ .. versionadded:: 2.2
+ """
+
+ def __init__(
+ self,
+ names: t.Sequence[t.Union[str, "Undefined"]] = (),
+ message: t.Optional[str] = None,
+ ) -> None:
+ if message is None:
+ from .runtime import Undefined
+
+ parts = []
+
+ for name in names:
+ if isinstance(name, Undefined):
+ parts.append(name._undefined_message)
+ else:
+ parts.append(name)
+
+ parts_str = ", ".join(map(str, parts))
+ message = f"none of the templates given were found: {parts_str}"
+
+ super().__init__(names[-1] if names else None, message)
+ self.templates = list(names)
+
+
+class TemplateSyntaxError(TemplateError):
+ """Raised to tell the user that there is a problem with the template."""
+
+ def __init__(
+ self,
+ message: str,
+ lineno: int,
+ name: t.Optional[str] = None,
+ filename: t.Optional[str] = None,
+ ) -> None:
+ super().__init__(message)
+ self.lineno = lineno
+ self.name = name
+ self.filename = filename
+ self.source: t.Optional[str] = None
+
+ # this is set to True if the debug.translate_syntax_error
+ # function translated the syntax error into a new traceback
+ self.translated = False
+
+ def __str__(self) -> str:
+ # for translated errors we only return the message
+ if self.translated:
+ return t.cast(str, self.message)
+
+ # otherwise attach some stuff
+ location = f"line {self.lineno}"
+ name = self.filename or self.name
+ if name:
+ location = f'File "{name}", {location}'
+ lines = [t.cast(str, self.message), " " + location]
+
+ # if the source is set, add the line to the output
+ if self.source is not None:
+ try:
+ line = self.source.splitlines()[self.lineno - 1]
+ except IndexError:
+ pass
+ else:
+ lines.append(" " + line.strip())
+
+ return "\n".join(lines)
+
+ def __reduce__(self): # type: ignore
+ # https://bugs.python.org/issue1692335 Exceptions that take
+ # multiple required arguments have problems with pickling.
+ # Without this, raises TypeError: __init__() missing 1 required
+ # positional argument: 'lineno'
+ return self.__class__, (self.message, self.lineno, self.name, self.filename)
+
+
+class TemplateAssertionError(TemplateSyntaxError):
+ """Like a template syntax error, but covers cases where something in the
+ template caused an error at compile time that wasn't necessarily caused
+ by a syntax error. However it's a direct subclass of
+ :exc:`TemplateSyntaxError` and has the same attributes.
+ """
+
+
+class TemplateRuntimeError(TemplateError):
+ """A generic runtime error in the template engine. Under some situations
+ Jinja may raise this exception.
+ """
+
+
+class UndefinedError(TemplateRuntimeError):
+ """Raised if a template tries to operate on :class:`Undefined`."""
+
+
+class SecurityError(TemplateRuntimeError):
+ """Raised if a template tries to do something insecure if the
+ sandbox is enabled.
+ """
+
+
+class FilterArgumentError(TemplateRuntimeError):
+ """This error is raised if a filter was called with inappropriate
+ arguments
+ """
diff --git a/venv/Lib/site-packages/jinja2/ext.py b/venv/Lib/site-packages/jinja2/ext.py
new file mode 100644
index 0000000..c7af8d4
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2/ext.py
@@ -0,0 +1,870 @@
+"""Extension API for adding custom tags and behavior."""
+
+import pprint
+import re
+import typing as t
+
+from markupsafe import Markup
+
+from . import defaults
+from . import nodes
+from .environment import Environment
+from .exceptions import TemplateAssertionError
+from .exceptions import TemplateSyntaxError
+from .runtime import concat # type: ignore
+from .runtime import Context
+from .runtime import Undefined
+from .utils import import_string
+from .utils import pass_context
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+ from .lexer import Token
+ from .lexer import TokenStream
+ from .parser import Parser
+
+ class _TranslationsBasic(te.Protocol):
+ def gettext(self, message: str) -> str: ...
+
+ def ngettext(self, singular: str, plural: str, n: int) -> str:
+ pass
+
+ class _TranslationsContext(_TranslationsBasic):
+ def pgettext(self, context: str, message: str) -> str: ...
+
+ def npgettext(
+ self, context: str, singular: str, plural: str, n: int
+ ) -> str: ...
+
+ _SupportedTranslations = t.Union[_TranslationsBasic, _TranslationsContext]
+
+
+# I18N functions available in Jinja templates. If the I18N library
+# provides ugettext, it will be assigned to gettext.
+GETTEXT_FUNCTIONS: t.Tuple[str, ...] = (
+ "_",
+ "gettext",
+ "ngettext",
+ "pgettext",
+ "npgettext",
+)
+_ws_re = re.compile(r"\s*\n\s*")
+
+
+class Extension:
+ """Extensions can be used to add extra functionality to the Jinja template
+ system at the parser level. Custom extensions are bound to an environment
+ but may not store environment specific data on `self`. The reason for
+ this is that an extension can be bound to another environment (for
+ overlays) by creating a copy and reassigning the `environment` attribute.
+
+ As extensions are created by the environment they cannot accept any
+ arguments for configuration. One may want to work around that by using
+ a factory function, but that is not possible as extensions are identified
+ by their import name. The correct way to configure the extension is
+ storing the configuration values on the environment. Because this way the
+ environment ends up acting as central configuration storage the
+ attributes may clash which is why extensions have to ensure that the names
+ they choose for configuration are not too generic. ``prefix`` for example
+ is a terrible name, ``fragment_cache_prefix`` on the other hand is a good
+ name as includes the name of the extension (fragment cache).
+ """
+
+ identifier: t.ClassVar[str]
+
+ def __init_subclass__(cls) -> None:
+ cls.identifier = f"{cls.__module__}.{cls.__name__}"
+
+ #: if this extension parses this is the list of tags it's listening to.
+ tags: t.Set[str] = set()
+
+ #: the priority of that extension. This is especially useful for
+ #: extensions that preprocess values. A lower value means higher
+ #: priority.
+ #:
+ #: .. versionadded:: 2.4
+ priority = 100
+
+ def __init__(self, environment: Environment) -> None:
+ self.environment = environment
+
+ def bind(self, environment: Environment) -> "te.Self":
+ """Create a copy of this extension bound to another environment."""
+ rv = object.__new__(self.__class__)
+ rv.__dict__.update(self.__dict__)
+ rv.environment = environment
+ return rv
+
+ def preprocess(
+ self, source: str, name: t.Optional[str], filename: t.Optional[str] = None
+ ) -> str:
+ """This method is called before the actual lexing and can be used to
+ preprocess the source. The `filename` is optional. The return value
+ must be the preprocessed source.
+ """
+ return source
+
+ def filter_stream(
+ self, stream: "TokenStream"
+ ) -> t.Union["TokenStream", t.Iterable["Token"]]:
+ """It's passed a :class:`~jinja2.lexer.TokenStream` that can be used
+ to filter tokens returned. This method has to return an iterable of
+ :class:`~jinja2.lexer.Token`\\s, but it doesn't have to return a
+ :class:`~jinja2.lexer.TokenStream`.
+ """
+ return stream
+
+ def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]:
+ """If any of the :attr:`tags` matched this method is called with the
+ parser as first argument. The token the parser stream is pointing at
+ is the name token that matched. This method has to return one or a
+ list of multiple nodes.
+ """
+ raise NotImplementedError()
+
+ def attr(
+ self, name: str, lineno: t.Optional[int] = None
+ ) -> nodes.ExtensionAttribute:
+ """Return an attribute node for the current extension. This is useful
+ to pass constants on extensions to generated template code.
+
+ ::
+
+ self.attr('_my_attribute', lineno=lineno)
+ """
+ return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
+
+ def call_method(
+ self,
+ name: str,
+ args: t.Optional[t.List[nodes.Expr]] = None,
+ kwargs: t.Optional[t.List[nodes.Keyword]] = None,
+ dyn_args: t.Optional[nodes.Expr] = None,
+ dyn_kwargs: t.Optional[nodes.Expr] = None,
+ lineno: t.Optional[int] = None,
+ ) -> nodes.Call:
+ """Call a method of the extension. This is a shortcut for
+ :meth:`attr` + :class:`jinja2.nodes.Call`.
+ """
+ if args is None:
+ args = []
+ if kwargs is None:
+ kwargs = []
+ return nodes.Call(
+ self.attr(name, lineno=lineno),
+ args,
+ kwargs,
+ dyn_args,
+ dyn_kwargs,
+ lineno=lineno,
+ )
+
+
+@pass_context
+def _gettext_alias(
+ __context: Context, *args: t.Any, **kwargs: t.Any
+) -> t.Union[t.Any, Undefined]:
+ return __context.call(__context.resolve("gettext"), *args, **kwargs)
+
+
+def _make_new_gettext(func: t.Callable[[str], str]) -> t.Callable[..., str]:
+ @pass_context
+ def gettext(__context: Context, __string: str, **variables: t.Any) -> str:
+ rv = __context.call(func, __string)
+ if __context.eval_ctx.autoescape:
+ rv = Markup(rv)
+ # Always treat as a format string, even if there are no
+ # variables. This makes translation strings more consistent
+ # and predictable. This requires escaping
+ return rv % variables # type: ignore
+
+ return gettext
+
+
+def _make_new_ngettext(func: t.Callable[[str, str, int], str]) -> t.Callable[..., str]:
+ @pass_context
+ def ngettext(
+ __context: Context,
+ __singular: str,
+ __plural: str,
+ __num: int,
+ **variables: t.Any,
+ ) -> str:
+ variables.setdefault("num", __num)
+ rv = __context.call(func, __singular, __plural, __num)
+ if __context.eval_ctx.autoescape:
+ rv = Markup(rv)
+ # Always treat as a format string, see gettext comment above.
+ return rv % variables # type: ignore
+
+ return ngettext
+
+
+def _make_new_pgettext(func: t.Callable[[str, str], str]) -> t.Callable[..., str]:
+ @pass_context
+ def pgettext(
+ __context: Context, __string_ctx: str, __string: str, **variables: t.Any
+ ) -> str:
+ variables.setdefault("context", __string_ctx)
+ rv = __context.call(func, __string_ctx, __string)
+
+ if __context.eval_ctx.autoescape:
+ rv = Markup(rv)
+
+ # Always treat as a format string, see gettext comment above.
+ return rv % variables # type: ignore
+
+ return pgettext
+
+
+def _make_new_npgettext(
+ func: t.Callable[[str, str, str, int], str],
+) -> t.Callable[..., str]:
+ @pass_context
+ def npgettext(
+ __context: Context,
+ __string_ctx: str,
+ __singular: str,
+ __plural: str,
+ __num: int,
+ **variables: t.Any,
+ ) -> str:
+ variables.setdefault("context", __string_ctx)
+ variables.setdefault("num", __num)
+ rv = __context.call(func, __string_ctx, __singular, __plural, __num)
+
+ if __context.eval_ctx.autoescape:
+ rv = Markup(rv)
+
+ # Always treat as a format string, see gettext comment above.
+ return rv % variables # type: ignore
+
+ return npgettext
+
+
+class InternationalizationExtension(Extension):
+ """This extension adds gettext support to Jinja."""
+
+ tags = {"trans"}
+
+ # TODO: the i18n extension is currently reevaluating values in a few
+ # situations. Take this example:
+ # {% trans count=something() %}{{ count }} foo{% pluralize
+ # %}{{ count }} fooss{% endtrans %}
+ # something is called twice here. One time for the gettext value and
+ # the other time for the n-parameter of the ngettext function.
+
+ def __init__(self, environment: Environment) -> None:
+ super().__init__(environment)
+ environment.globals["_"] = _gettext_alias
+ environment.extend(
+ install_gettext_translations=self._install,
+ install_null_translations=self._install_null,
+ install_gettext_callables=self._install_callables,
+ uninstall_gettext_translations=self._uninstall,
+ extract_translations=self._extract,
+ newstyle_gettext=False,
+ )
+
+ def _install(
+ self, translations: "_SupportedTranslations", newstyle: t.Optional[bool] = None
+ ) -> None:
+ # ugettext and ungettext are preferred in case the I18N library
+ # is providing compatibility with older Python versions.
+ gettext = getattr(translations, "ugettext", None)
+ if gettext is None:
+ gettext = translations.gettext
+ ngettext = getattr(translations, "ungettext", None)
+ if ngettext is None:
+ ngettext = translations.ngettext
+
+ pgettext = getattr(translations, "pgettext", None)
+ npgettext = getattr(translations, "npgettext", None)
+ self._install_callables(
+ gettext, ngettext, newstyle=newstyle, pgettext=pgettext, npgettext=npgettext
+ )
+
+ def _install_null(self, newstyle: t.Optional[bool] = None) -> None:
+ import gettext
+
+ translations = gettext.NullTranslations()
+
+ if hasattr(translations, "pgettext"):
+ # Python < 3.8
+ pgettext = translations.pgettext
+ else:
+
+ def pgettext(c: str, s: str) -> str: # type: ignore[misc]
+ return s
+
+ if hasattr(translations, "npgettext"):
+ npgettext = translations.npgettext
+ else:
+
+ def npgettext(c: str, s: str, p: str, n: int) -> str: # type: ignore[misc]
+ return s if n == 1 else p
+
+ self._install_callables(
+ gettext=translations.gettext,
+ ngettext=translations.ngettext,
+ newstyle=newstyle,
+ pgettext=pgettext,
+ npgettext=npgettext,
+ )
+
+ def _install_callables(
+ self,
+ gettext: t.Callable[[str], str],
+ ngettext: t.Callable[[str, str, int], str],
+ newstyle: t.Optional[bool] = None,
+ pgettext: t.Optional[t.Callable[[str, str], str]] = None,
+ npgettext: t.Optional[t.Callable[[str, str, str, int], str]] = None,
+ ) -> None:
+ if newstyle is not None:
+ self.environment.newstyle_gettext = newstyle # type: ignore
+ if self.environment.newstyle_gettext: # type: ignore
+ gettext = _make_new_gettext(gettext)
+ ngettext = _make_new_ngettext(ngettext)
+
+ if pgettext is not None:
+ pgettext = _make_new_pgettext(pgettext)
+
+ if npgettext is not None:
+ npgettext = _make_new_npgettext(npgettext)
+
+ self.environment.globals.update(
+ gettext=gettext, ngettext=ngettext, pgettext=pgettext, npgettext=npgettext
+ )
+
+ def _uninstall(self, translations: "_SupportedTranslations") -> None:
+ for key in ("gettext", "ngettext", "pgettext", "npgettext"):
+ self.environment.globals.pop(key, None)
+
+ def _extract(
+ self,
+ source: t.Union[str, nodes.Template],
+ gettext_functions: t.Sequence[str] = GETTEXT_FUNCTIONS,
+ ) -> t.Iterator[
+ t.Tuple[int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]]
+ ]:
+ if isinstance(source, str):
+ source = self.environment.parse(source)
+ return extract_from_ast(source, gettext_functions)
+
+ def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]:
+ """Parse a translatable tag."""
+ lineno = next(parser.stream).lineno
+
+ context = None
+ context_token = parser.stream.next_if("string")
+
+ if context_token is not None:
+ context = context_token.value
+
+ # find all the variables referenced. Additionally a variable can be
+ # defined in the body of the trans block too, but this is checked at
+ # a later state.
+ plural_expr: t.Optional[nodes.Expr] = None
+ plural_expr_assignment: t.Optional[nodes.Assign] = None
+ num_called_num = False
+ variables: t.Dict[str, nodes.Expr] = {}
+ trimmed = None
+ while parser.stream.current.type != "block_end":
+ if variables:
+ parser.stream.expect("comma")
+
+ # skip colon for python compatibility
+ if parser.stream.skip_if("colon"):
+ break
+
+ token = parser.stream.expect("name")
+ if token.value in variables:
+ parser.fail(
+ f"translatable variable {token.value!r} defined twice.",
+ token.lineno,
+ exc=TemplateAssertionError,
+ )
+
+ # expressions
+ if parser.stream.current.type == "assign":
+ next(parser.stream)
+ variables[token.value] = var = parser.parse_expression()
+ elif trimmed is None and token.value in ("trimmed", "notrimmed"):
+ trimmed = token.value == "trimmed"
+ continue
+ else:
+ variables[token.value] = var = nodes.Name(token.value, "load")
+
+ if plural_expr is None:
+ if isinstance(var, nodes.Call):
+ plural_expr = nodes.Name("_trans", "load")
+ variables[token.value] = plural_expr
+ plural_expr_assignment = nodes.Assign(
+ nodes.Name("_trans", "store"), var
+ )
+ else:
+ plural_expr = var
+ num_called_num = token.value == "num"
+
+ parser.stream.expect("block_end")
+
+ plural = None
+ have_plural = False
+ referenced = set()
+
+ # now parse until endtrans or pluralize
+ singular_names, singular = self._parse_block(parser, True)
+ if singular_names:
+ referenced.update(singular_names)
+ if plural_expr is None:
+ plural_expr = nodes.Name(singular_names[0], "load")
+ num_called_num = singular_names[0] == "num"
+
+ # if we have a pluralize block, we parse that too
+ if parser.stream.current.test("name:pluralize"):
+ have_plural = True
+ next(parser.stream)
+ if parser.stream.current.type != "block_end":
+ token = parser.stream.expect("name")
+ if token.value not in variables:
+ parser.fail(
+ f"unknown variable {token.value!r} for pluralization",
+ token.lineno,
+ exc=TemplateAssertionError,
+ )
+ plural_expr = variables[token.value]
+ num_called_num = token.value == "num"
+ parser.stream.expect("block_end")
+ plural_names, plural = self._parse_block(parser, False)
+ next(parser.stream)
+ referenced.update(plural_names)
+ else:
+ next(parser.stream)
+
+ # register free names as simple name expressions
+ for name in referenced:
+ if name not in variables:
+ variables[name] = nodes.Name(name, "load")
+
+ if not have_plural:
+ plural_expr = None
+ elif plural_expr is None:
+ parser.fail("pluralize without variables", lineno)
+
+ if trimmed is None:
+ trimmed = self.environment.policies["ext.i18n.trimmed"]
+ if trimmed:
+ singular = self._trim_whitespace(singular)
+ if plural:
+ plural = self._trim_whitespace(plural)
+
+ node = self._make_node(
+ singular,
+ plural,
+ context,
+ variables,
+ plural_expr,
+ bool(referenced),
+ num_called_num and have_plural,
+ )
+ node.set_lineno(lineno)
+ if plural_expr_assignment is not None:
+ return [plural_expr_assignment, node]
+ else:
+ return node
+
+ def _trim_whitespace(self, string: str, _ws_re: t.Pattern[str] = _ws_re) -> str:
+ return _ws_re.sub(" ", string.strip())
+
+ def _parse_block(
+ self, parser: "Parser", allow_pluralize: bool
+ ) -> t.Tuple[t.List[str], str]:
+ """Parse until the next block tag with a given name."""
+ referenced = []
+ buf = []
+
+ while True:
+ if parser.stream.current.type == "data":
+ buf.append(parser.stream.current.value.replace("%", "%%"))
+ next(parser.stream)
+ elif parser.stream.current.type == "variable_begin":
+ next(parser.stream)
+ name = parser.stream.expect("name").value
+ referenced.append(name)
+ buf.append(f"%({name})s")
+ parser.stream.expect("variable_end")
+ elif parser.stream.current.type == "block_begin":
+ next(parser.stream)
+ block_name = (
+ parser.stream.current.value
+ if parser.stream.current.type == "name"
+ else None
+ )
+ if block_name == "endtrans":
+ break
+ elif block_name == "pluralize":
+ if allow_pluralize:
+ break
+ parser.fail(
+ "a translatable section can have only one pluralize section"
+ )
+ elif block_name == "trans":
+ parser.fail(
+ "trans blocks can't be nested; did you mean `endtrans`?"
+ )
+ parser.fail(
+ f"control structures in translatable sections are not allowed; "
+ f"saw `{block_name}`"
+ )
+ elif parser.stream.eos:
+ parser.fail("unclosed translation block")
+ else:
+ raise RuntimeError("internal parser error")
+
+ return referenced, concat(buf)
+
+ def _make_node(
+ self,
+ singular: str,
+ plural: t.Optional[str],
+ context: t.Optional[str],
+ variables: t.Dict[str, nodes.Expr],
+ plural_expr: t.Optional[nodes.Expr],
+ vars_referenced: bool,
+ num_called_num: bool,
+ ) -> nodes.Output:
+ """Generates a useful node from the data provided."""
+ newstyle = self.environment.newstyle_gettext # type: ignore
+ node: nodes.Expr
+
+ # no variables referenced? no need to escape for old style
+ # gettext invocations only if there are vars.
+ if not vars_referenced and not newstyle:
+ singular = singular.replace("%%", "%")
+ if plural:
+ plural = plural.replace("%%", "%")
+
+ func_name = "gettext"
+ func_args: t.List[nodes.Expr] = [nodes.Const(singular)]
+
+ if context is not None:
+ func_args.insert(0, nodes.Const(context))
+ func_name = f"p{func_name}"
+
+ if plural_expr is not None:
+ func_name = f"n{func_name}"
+ func_args.extend((nodes.Const(plural), plural_expr))
+
+ node = nodes.Call(nodes.Name(func_name, "load"), func_args, [], None, None)
+
+ # in case newstyle gettext is used, the method is powerful
+ # enough to handle the variable expansion and autoescape
+ # handling itself
+ if newstyle:
+ for key, value in variables.items():
+ # the function adds that later anyways in case num was
+ # called num, so just skip it.
+ if num_called_num and key == "num":
+ continue
+ node.kwargs.append(nodes.Keyword(key, value))
+
+ # otherwise do that here
+ else:
+ # mark the return value as safe if we are in an
+ # environment with autoescaping turned on
+ node = nodes.MarkSafeIfAutoescape(node)
+ if variables:
+ node = nodes.Mod(
+ node,
+ nodes.Dict(
+ [
+ nodes.Pair(nodes.Const(key), value)
+ for key, value in variables.items()
+ ]
+ ),
+ )
+ return nodes.Output([node])
+
+
+class ExprStmtExtension(Extension):
+ """Adds a `do` tag to Jinja that works like the print statement just
+ that it doesn't print the return value.
+ """
+
+ tags = {"do"}
+
+ def parse(self, parser: "Parser") -> nodes.ExprStmt:
+ node = nodes.ExprStmt(lineno=next(parser.stream).lineno)
+ node.node = parser.parse_tuple()
+ return node
+
+
+class LoopControlExtension(Extension):
+ """Adds break and continue to the template engine."""
+
+ tags = {"break", "continue"}
+
+ def parse(self, parser: "Parser") -> t.Union[nodes.Break, nodes.Continue]:
+ token = next(parser.stream)
+ if token.value == "break":
+ return nodes.Break(lineno=token.lineno)
+ return nodes.Continue(lineno=token.lineno)
+
+
+class DebugExtension(Extension):
+ """A ``{% debug %}`` tag that dumps the available variables,
+ filters, and tests.
+
+ .. code-block:: html+jinja
+
+
{% debug %}
+
+ .. code-block:: text
+
+ {'context': {'cycler': ,
+ ...,
+ 'namespace': },
+ 'filters': ['abs', 'attr', 'batch', 'capitalize', 'center', 'count', 'd',
+ ..., 'urlencode', 'urlize', 'wordcount', 'wordwrap', 'xmlattr'],
+ 'tests': ['!=', '<', '<=', '==', '>', '>=', 'callable', 'defined',
+ ..., 'odd', 'sameas', 'sequence', 'string', 'undefined', 'upper']}
+
+ .. versionadded:: 2.11.0
+ """
+
+ tags = {"debug"}
+
+ def parse(self, parser: "Parser") -> nodes.Output:
+ lineno = parser.stream.expect("name:debug").lineno
+ context = nodes.ContextReference()
+ result = self.call_method("_render", [context], lineno=lineno)
+ return nodes.Output([result], lineno=lineno)
+
+ def _render(self, context: Context) -> str:
+ result = {
+ "context": context.get_all(),
+ "filters": sorted(self.environment.filters.keys()),
+ "tests": sorted(self.environment.tests.keys()),
+ }
+
+ # Set the depth since the intent is to show the top few names.
+ return pprint.pformat(result, depth=3, compact=True)
+
+
+def extract_from_ast(
+ ast: nodes.Template,
+ gettext_functions: t.Sequence[str] = GETTEXT_FUNCTIONS,
+ babel_style: bool = True,
+) -> t.Iterator[
+ t.Tuple[int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]]
+]:
+ """Extract localizable strings from the given template node. Per
+ default this function returns matches in babel style that means non string
+ parameters as well as keyword arguments are returned as `None`. This
+ allows Babel to figure out what you really meant if you are using
+ gettext functions that allow keyword arguments for placeholder expansion.
+ If you don't want that behavior set the `babel_style` parameter to `False`
+ which causes only strings to be returned and parameters are always stored
+ in tuples. As a consequence invalid gettext calls (calls without a single
+ string parameter or string parameters after non-string parameters) are
+ skipped.
+
+ This example explains the behavior:
+
+ >>> from jinja2 import Environment
+ >>> env = Environment()
+ >>> node = env.parse('{{ (_("foo"), _(), ngettext("foo", "bar", 42)) }}')
+ >>> list(extract_from_ast(node))
+ [(1, '_', 'foo'), (1, '_', ()), (1, 'ngettext', ('foo', 'bar', None))]
+ >>> list(extract_from_ast(node, babel_style=False))
+ [(1, '_', ('foo',)), (1, 'ngettext', ('foo', 'bar'))]
+
+ For every string found this function yields a ``(lineno, function,
+ message)`` tuple, where:
+
+ * ``lineno`` is the number of the line on which the string was found,
+ * ``function`` is the name of the ``gettext`` function used (if the
+ string was extracted from embedded Python code), and
+ * ``message`` is the string, or a tuple of strings for functions
+ with multiple string arguments.
+
+ This extraction function operates on the AST and is because of that unable
+ to extract any comments. For comment support you have to use the babel
+ extraction interface or extract comments yourself.
+ """
+ out: t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]
+
+ for node in ast.find_all(nodes.Call):
+ if (
+ not isinstance(node.node, nodes.Name)
+ or node.node.name not in gettext_functions
+ ):
+ continue
+
+ strings: t.List[t.Optional[str]] = []
+
+ for arg in node.args:
+ if isinstance(arg, nodes.Const) and isinstance(arg.value, str):
+ strings.append(arg.value)
+ else:
+ strings.append(None)
+
+ for _ in node.kwargs:
+ strings.append(None)
+ if node.dyn_args is not None:
+ strings.append(None)
+ if node.dyn_kwargs is not None:
+ strings.append(None)
+
+ if not babel_style:
+ out = tuple(x for x in strings if x is not None)
+
+ if not out:
+ continue
+ else:
+ if len(strings) == 1:
+ out = strings[0]
+ else:
+ out = tuple(strings)
+
+ yield node.lineno, node.node.name, out
+
+
+class _CommentFinder:
+ """Helper class to find comments in a token stream. Can only
+ find comments for gettext calls forwards. Once the comment
+ from line 4 is found, a comment for line 1 will not return a
+ usable value.
+ """
+
+ def __init__(
+ self, tokens: t.Sequence[t.Tuple[int, str, str]], comment_tags: t.Sequence[str]
+ ) -> None:
+ self.tokens = tokens
+ self.comment_tags = comment_tags
+ self.offset = 0
+ self.last_lineno = 0
+
+ def find_backwards(self, offset: int) -> t.List[str]:
+ try:
+ for _, token_type, token_value in reversed(
+ self.tokens[self.offset : offset]
+ ):
+ if token_type in ("comment", "linecomment"):
+ try:
+ prefix, comment = token_value.split(None, 1)
+ except ValueError:
+ continue
+ if prefix in self.comment_tags:
+ return [comment.rstrip()]
+ return []
+ finally:
+ self.offset = offset
+
+ def find_comments(self, lineno: int) -> t.List[str]:
+ if not self.comment_tags or self.last_lineno > lineno:
+ return []
+ for idx, (token_lineno, _, _) in enumerate(self.tokens[self.offset :]):
+ if token_lineno > lineno:
+ return self.find_backwards(self.offset + idx)
+ return self.find_backwards(len(self.tokens))
+
+
+def babel_extract(
+ fileobj: t.BinaryIO,
+ keywords: t.Sequence[str],
+ comment_tags: t.Sequence[str],
+ options: t.Dict[str, t.Any],
+) -> t.Iterator[
+ t.Tuple[
+ int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]], t.List[str]
+ ]
+]:
+ """Babel extraction method for Jinja templates.
+
+ .. versionchanged:: 2.3
+ Basic support for translation comments was added. If `comment_tags`
+ is now set to a list of keywords for extraction, the extractor will
+ try to find the best preceding comment that begins with one of the
+ keywords. For best results, make sure to not have more than one
+ gettext call in one line of code and the matching comment in the
+ same line or the line before.
+
+ .. versionchanged:: 2.5.1
+ The `newstyle_gettext` flag can be set to `True` to enable newstyle
+ gettext calls.
+
+ .. versionchanged:: 2.7
+ A `silent` option can now be provided. If set to `False` template
+ syntax errors are propagated instead of being ignored.
+
+ :param fileobj: the file-like object the messages should be extracted from
+ :param keywords: a list of keywords (i.e. function names) that should be
+ recognized as translation functions
+ :param comment_tags: a list of translator tags to search for and include
+ in the results.
+ :param options: a dictionary of additional options (optional)
+ :return: an iterator over ``(lineno, funcname, message, comments)`` tuples.
+ (comments will be empty currently)
+ """
+ extensions: t.Dict[t.Type[Extension], None] = {}
+
+ for extension_name in options.get("extensions", "").split(","):
+ extension_name = extension_name.strip()
+
+ if not extension_name:
+ continue
+
+ extensions[import_string(extension_name)] = None
+
+ if InternationalizationExtension not in extensions:
+ extensions[InternationalizationExtension] = None
+
+ def getbool(options: t.Mapping[str, str], key: str, default: bool = False) -> bool:
+ return options.get(key, str(default)).lower() in {"1", "on", "yes", "true"}
+
+ silent = getbool(options, "silent", True)
+ environment = Environment(
+ options.get("block_start_string", defaults.BLOCK_START_STRING),
+ options.get("block_end_string", defaults.BLOCK_END_STRING),
+ options.get("variable_start_string", defaults.VARIABLE_START_STRING),
+ options.get("variable_end_string", defaults.VARIABLE_END_STRING),
+ options.get("comment_start_string", defaults.COMMENT_START_STRING),
+ options.get("comment_end_string", defaults.COMMENT_END_STRING),
+ options.get("line_statement_prefix") or defaults.LINE_STATEMENT_PREFIX,
+ options.get("line_comment_prefix") or defaults.LINE_COMMENT_PREFIX,
+ getbool(options, "trim_blocks", defaults.TRIM_BLOCKS),
+ getbool(options, "lstrip_blocks", defaults.LSTRIP_BLOCKS),
+ defaults.NEWLINE_SEQUENCE,
+ getbool(options, "keep_trailing_newline", defaults.KEEP_TRAILING_NEWLINE),
+ tuple(extensions),
+ cache_size=0,
+ auto_reload=False,
+ )
+
+ if getbool(options, "trimmed"):
+ environment.policies["ext.i18n.trimmed"] = True
+ if getbool(options, "newstyle_gettext"):
+ environment.newstyle_gettext = True # type: ignore
+
+ source = fileobj.read().decode(options.get("encoding", "utf-8"))
+ try:
+ node = environment.parse(source)
+ tokens = list(environment.lex(environment.preprocess(source)))
+ except TemplateSyntaxError:
+ if not silent:
+ raise
+ # skip templates with syntax errors
+ return
+
+ finder = _CommentFinder(tokens, comment_tags)
+ for lineno, func, message in extract_from_ast(node, keywords):
+ yield lineno, func, message, finder.find_comments(lineno)
+
+
+#: nicer import names
+i18n = InternationalizationExtension
+do = ExprStmtExtension
+loopcontrols = LoopControlExtension
+debug = DebugExtension
diff --git a/venv/Lib/site-packages/jinja2/filters.py b/venv/Lib/site-packages/jinja2/filters.py
new file mode 100644
index 0000000..2bcba4f
--- /dev/null
+++ b/venv/Lib/site-packages/jinja2/filters.py
@@ -0,0 +1,1873 @@
+"""Built-in template filters used with the ``|`` operator."""
+
+import math
+import random
+import re
+import typing
+import typing as t
+from collections import abc
+from inspect import getattr_static
+from itertools import chain
+from itertools import groupby
+
+from markupsafe import escape
+from markupsafe import Markup
+from markupsafe import soft_str
+
+from .async_utils import async_variant
+from .async_utils import auto_aiter
+from .async_utils import auto_await
+from .async_utils import auto_to_list
+from .exceptions import FilterArgumentError
+from .runtime import Undefined
+from .utils import htmlsafe_json_dumps
+from .utils import pass_context
+from .utils import pass_environment
+from .utils import pass_eval_context
+from .utils import pformat
+from .utils import url_quote
+from .utils import urlize
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+ from .environment import Environment
+ from .nodes import EvalContext
+ from .runtime import Context
+ from .sandbox import SandboxedEnvironment # noqa: F401
+
+ class HasHTML(te.Protocol):
+ def __html__(self) -> str:
+ pass
+
+
+F = t.TypeVar("F", bound=t.Callable[..., t.Any])
+K = t.TypeVar("K")
+V = t.TypeVar("V")
+
+
+def ignore_case(value: V) -> V:
+ """For use as a postprocessor for :func:`make_attrgetter`. Converts strings
+ to lowercase and returns other types as-is."""
+ if isinstance(value, str):
+ return t.cast(V, value.lower())
+
+ return value
+
+
+def make_attrgetter(
+ environment: "Environment",
+ attribute: t.Optional[t.Union[str, int]],
+ postprocess: t.Optional[t.Callable[[t.Any], t.Any]] = None,
+ default: t.Optional[t.Any] = None,
+) -> t.Callable[[t.Any], t.Any]:
+ """Returns a callable that looks up the given attribute from a
+ passed object with the rules of the environment. Dots are allowed
+ to access attributes of attributes. Integer parts in paths are
+ looked up as integers.
+ """
+ parts = _prepare_attribute_parts(attribute)
+
+ def attrgetter(item: t.Any) -> t.Any:
+ for part in parts:
+ item = environment.getitem(item, part)
+
+ if default is not None and isinstance(item, Undefined):
+ item = default
+
+ if postprocess is not None:
+ item = postprocess(item)
+
+ return item
+
+ return attrgetter
+
+
+def make_multi_attrgetter(
+ environment: "Environment",
+ attribute: t.Optional[t.Union[str, int]],
+ postprocess: t.Optional[t.Callable[[t.Any], t.Any]] = None,
+) -> t.Callable[[t.Any], t.List[t.Any]]:
+ """Returns a callable that looks up the given comma separated
+ attributes from a passed object with the rules of the environment.
+ Dots are allowed to access attributes of each attribute. Integer
+ parts in paths are looked up as integers.
+
+ The value returned by the returned callable is a list of extracted
+ attribute values.
+
+ Examples of attribute: "attr1,attr2", "attr1.inner1.0,attr2.inner2.0", etc.
+ """
+ if isinstance(attribute, str):
+ split: t.Sequence[t.Union[str, int, None]] = attribute.split(",")
+ else:
+ split = [attribute]
+
+ parts = [_prepare_attribute_parts(item) for item in split]
+
+ def attrgetter(item: t.Any) -> t.List[t.Any]:
+ items = [None] * len(parts)
+
+ for i, attribute_part in enumerate(parts):
+ item_i = item
+
+ for part in attribute_part:
+ item_i = environment.getitem(item_i, part)
+
+ if postprocess is not None:
+ item_i = postprocess(item_i)
+
+ items[i] = item_i
+
+ return items
+
+ return attrgetter
+
+
+def _prepare_attribute_parts(
+ attr: t.Optional[t.Union[str, int]],
+) -> t.List[t.Union[str, int]]:
+ if attr is None:
+ return []
+
+ if isinstance(attr, str):
+ return [int(x) if x.isdigit() else x for x in attr.split(".")]
+
+ return [attr]
+
+
+def do_forceescape(value: "t.Union[str, HasHTML]") -> Markup:
+ """Enforce HTML escaping. This will probably double escape variables."""
+ if hasattr(value, "__html__"):
+ value = t.cast("HasHTML", value).__html__()
+
+ return escape(str(value))
+
+
+def do_urlencode(
+ value: t.Union[str, t.Mapping[str, t.Any], t.Iterable[t.Tuple[str, t.Any]]],
+) -> str:
+ """Quote data for use in a URL path or query using UTF-8.
+
+ Basic wrapper around :func:`urllib.parse.quote` when given a
+ string, or :func:`urllib.parse.urlencode` for a dict or iterable.
+
+ :param value: Data to quote. A string will be quoted directly. A
+ dict or iterable of ``(key, value)`` pairs will be joined as a
+ query string.
+
+ When given a string, "/" is not quoted. HTTP servers treat "/" and
+ "%2F" equivalently in paths. If you need quoted slashes, use the
+ ``|replace("/", "%2F")`` filter.
+
+ .. versionadded:: 2.7
+ """
+ if isinstance(value, str) or not isinstance(value, abc.Iterable):
+ return url_quote(value)
+
+ if isinstance(value, dict):
+ items: t.Iterable[t.Tuple[str, t.Any]] = value.items()
+ else:
+ items = value # type: ignore
+
+ return "&".join(
+ f"{url_quote(k, for_qs=True)}={url_quote(v, for_qs=True)}" for k, v in items
+ )
+
+
+@pass_eval_context
+def do_replace(
+ eval_ctx: "EvalContext", s: str, old: str, new: str, count: t.Optional[int] = None
+) -> str:
+ """Return a copy of the value with all occurrences of a substring
+ replaced with a new one. The first argument is the substring
+ that should be replaced, the second is the replacement string.
+ If the optional third argument ``count`` is given, only the first
+ ``count`` occurrences are replaced:
+
+ .. sourcecode:: jinja
+
+ {{ "Hello World"|replace("Hello", "Goodbye") }}
+ -> Goodbye World
+
+ {{ "aaaaargh"|replace("a", "d'oh, ", 2) }}
+ -> d'oh, d'oh, aaargh
+ """
+ if count is None:
+ count = -1
+
+ if not eval_ctx.autoescape:
+ return str(s).replace(str(old), str(new), count)
+
+ if (
+ hasattr(old, "__html__")
+ or hasattr(new, "__html__")
+ and not hasattr(s, "__html__")
+ ):
+ s = escape(s)
+ else:
+ s = soft_str(s)
+
+ return s.replace(soft_str(old), soft_str(new), count)
+
+
+def do_upper(s: str) -> str:
+ """Convert a value to uppercase."""
+ return soft_str(s).upper()
+
+
+def do_lower(s: str) -> str:
+ """Convert a value to lowercase."""
+ return soft_str(s).lower()
+
+
+def do_items(value: t.Union[t.Mapping[K, V], Undefined]) -> t.Iterator[t.Tuple[K, V]]:
+ """Return an iterator over the ``(key, value)`` items of a mapping.
+
+ ``x|items`` is the same as ``x.items()``, except if ``x`` is
+ undefined an empty iterator is returned.
+
+ This filter is useful if you expect the template to be rendered with
+ an implementation of Jinja in another programming language that does
+ not have a ``.items()`` method on its mapping type.
+
+ .. code-block:: html+jinja
+
+
+ {% for key, value in my_dict|items %}
+
{{ key }}
+
{{ value }}
+ {% endfor %}
+
+
+ .. versionadded:: 3.1
+ """
+ if isinstance(value, Undefined):
+ return
+
+ if not isinstance(value, abc.Mapping):
+ raise TypeError("Can only get item pairs from a mapping.")
+
+ yield from value.items()
+
+
+# Check for characters that would move the parser state from key to value.
+# https://html.spec.whatwg.org/#attribute-name-state
+_attr_key_re = re.compile(r"[\s/>=]", flags=re.ASCII)
+
+
+@pass_eval_context
+def do_xmlattr(
+ eval_ctx: "EvalContext", d: t.Mapping[str, t.Any], autospace: bool = True
+) -> str:
+ """Create an SGML/XML attribute string based on the items in a dict.
+
+ **Values** that are neither ``none`` nor ``undefined`` are automatically
+ escaped, safely allowing untrusted user input.
+
+ User input should not be used as **keys** to this filter. If any key
+ contains a space, ``/`` solidus, ``>`` greater-than sign, or ``=`` equals
+ sign, this fails with a ``ValueError``. Regardless of this, user input
+ should never be used as keys to this filter, or must be separately validated
+ first.
+
+ .. sourcecode:: html+jinja
+
+
+ ...
+
+
+ Results in something like this:
+
+ .. sourcecode:: html
+
+
+ ...
+
+
+ As you can see it automatically prepends a space in front of the item
+ if the filter returned something unless the second parameter is false.
+
+ .. versionchanged:: 3.1.4
+ Keys with ``/`` solidus, ``>`` greater-than sign, or ``=`` equals sign
+ are not allowed.
+
+ .. versionchanged:: 3.1.3
+ Keys with spaces are not allowed.
+ """
+ items = []
+
+ for key, value in d.items():
+ if value is None or isinstance(value, Undefined):
+ continue
+
+ if _attr_key_re.search(key) is not None:
+ raise ValueError(f"Invalid character in attribute name: {key!r}")
+
+ items.append(f'{escape(key)}="{escape(value)}"')
+
+ rv = " ".join(items)
+
+ if autospace and rv:
+ rv = " " + rv
+
+ if eval_ctx.autoescape:
+ rv = Markup(rv)
+
+ return rv
+
+
+def do_capitalize(s: str) -> str:
+ """Capitalize a value. The first character will be uppercase, all others
+ lowercase.
+ """
+ return soft_str(s).capitalize()
+
+
+_word_beginning_split_re = re.compile(r"([-\s({\[<]+)")
+
+
+def do_title(s: str) -> str:
+ """Return a titlecased version of the value. I.e. words will start with
+ uppercase letters, all remaining characters are lowercase.
+ """
+ return "".join(
+ [
+ item[0].upper() + item[1:].lower()
+ for item in _word_beginning_split_re.split(soft_str(s))
+ if item
+ ]
+ )
+
+
+def do_dictsort(
+ value: t.Mapping[K, V],
+ case_sensitive: bool = False,
+ by: 'te.Literal["key", "value"]' = "key",
+ reverse: bool = False,
+) -> t.List[t.Tuple[K, V]]:
+ """Sort a dict and yield (key, value) pairs. Python dicts may not
+ be in the order you want to display them in, so sort them first.
+
+ .. sourcecode:: jinja
+
+ {% for key, value in mydict|dictsort %}
+ sort the dict by key, case insensitive
+
+ {% for key, value in mydict|dictsort(reverse=true) %}
+ sort the dict by key, case insensitive, reverse order
+
+ {% for key, value in mydict|dictsort(true) %}
+ sort the dict by key, case sensitive
+
+ {% for key, value in mydict|dictsort(false, 'value') %}
+ sort the dict by value, case insensitive
+ """
+ if by == "key":
+ pos = 0
+ elif by == "value":
+ pos = 1
+ else:
+ raise FilterArgumentError('You can only sort by either "key" or "value"')
+
+ def sort_func(item: t.Tuple[t.Any, t.Any]) -> t.Any:
+ value = item[pos]
+
+ if not case_sensitive:
+ value = ignore_case(value)
+
+ return value
+
+ return sorted(value.items(), key=sort_func, reverse=reverse)
+
+
+@pass_environment
+def do_sort(
+ environment: "Environment",
+ value: "t.Iterable[V]",
+ reverse: bool = False,
+ case_sensitive: bool = False,
+ attribute: t.Optional[t.Union[str, int]] = None,
+) -> "t.List[V]":
+ """Sort an iterable using Python's :func:`sorted`.
+
+ .. sourcecode:: jinja
+
+ {% for city in cities|sort %}
+ ...
+ {% endfor %}
+
+ :param reverse: Sort descending instead of ascending.
+ :param case_sensitive: When sorting strings, sort upper and lower
+ case separately.
+ :param attribute: When sorting objects or dicts, an attribute or
+ key to sort by. Can use dot notation like ``"address.city"``.
+ Can be a list of attributes like ``"age,name"``.
+
+ The sort is stable, it does not change the relative order of
+ elements that compare equal. This makes it is possible to chain
+ sorts on different attributes and ordering.
+
+ .. sourcecode:: jinja
+
+ {% for user in users|sort(attribute="name")
+ |sort(reverse=true, attribute="age") %}
+ ...
+ {% endfor %}
+
+ As a shortcut to chaining when the direction is the same for all
+ attributes, pass a comma separate list of attributes.
+
+ .. sourcecode:: jinja
+
+ {% for user in users|sort(attribute="age,name") %}
+ ...
+ {% endfor %}
+
+ .. versionchanged:: 2.11.0
+ The ``attribute`` parameter can be a comma separated list of
+ attributes, e.g. ``"age,name"``.
+
+ .. versionchanged:: 2.6
+ The ``attribute`` parameter was added.
+ """
+ key_func = make_multi_attrgetter(
+ environment, attribute, postprocess=ignore_case if not case_sensitive else None
+ )
+ return sorted(value, key=key_func, reverse=reverse)
+
+
+@pass_environment
+def sync_do_unique(
+ environment: "Environment",
+ value: "t.Iterable[V]",
+ case_sensitive: bool = False,
+ attribute: t.Optional[t.Union[str, int]] = None,
+) -> "t.Iterator[V]":
+ """Returns a list of unique items from the given iterable.
+
+ .. sourcecode:: jinja
+
+ {{ ['foo', 'bar', 'foobar', 'FooBar']|unique|list }}
+ -> ['foo', 'bar', 'foobar']
+
+ The unique items are yielded in the same order as their first occurrence in
+ the iterable passed to the filter.
+
+ :param case_sensitive: Treat upper and lower case strings as distinct.
+ :param attribute: Filter objects with unique values for this attribute.
+ """
+ getter = make_attrgetter(
+ environment, attribute, postprocess=ignore_case if not case_sensitive else None
+ )
+ seen = set()
+
+ for item in value:
+ key = getter(item)
+
+ if key not in seen:
+ seen.add(key)
+ yield item
+
+
+@async_variant(sync_do_unique) # type: ignore
+async def do_unique(
+ environment: "Environment",
+ value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
+ case_sensitive: bool = False,
+ attribute: t.Optional[t.Union[str, int]] = None,
+) -> "t.Iterator[V]":
+ return sync_do_unique(
+ environment, await auto_to_list(value), case_sensitive, attribute
+ )
+
+
+def _min_or_max(
+ environment: "Environment",
+ value: "t.Iterable[V]",
+ func: "t.Callable[..., V]",
+ case_sensitive: bool,
+ attribute: t.Optional[t.Union[str, int]],
+) -> "t.Union[V, Undefined]":
+ it = iter(value)
+
+ try:
+ first = next(it)
+ except StopIteration:
+ return environment.undefined("No aggregated item, sequence was empty.")
+
+ key_func = make_attrgetter(
+ environment, attribute, postprocess=ignore_case if not case_sensitive else None
+ )
+ return func(chain([first], it), key=key_func)
+
+
+@pass_environment
+def do_min(
+ environment: "Environment",
+ value: "t.Iterable[V]",
+ case_sensitive: bool = False,
+ attribute: t.Optional[t.Union[str, int]] = None,
+) -> "t.Union[V, Undefined]":
+ """Return the smallest item from the sequence.
+
+ .. sourcecode:: jinja
+
+ {{ [1, 2, 3]|min }}
+ -> 1
+
+ :param case_sensitive: Treat upper and lower case strings as distinct.
+ :param attribute: Get the object with the min value of this attribute.
+ """
+ return _min_or_max(environment, value, min, case_sensitive, attribute)
+
+
+@pass_environment
+def do_max(
+ environment: "Environment",
+ value: "t.Iterable[V]",
+ case_sensitive: bool = False,
+ attribute: t.Optional[t.Union[str, int]] = None,
+) -> "t.Union[V, Undefined]":
+ """Return the largest item from the sequence.
+
+ .. sourcecode:: jinja
+
+ {{ [1, 2, 3]|max }}
+ -> 3
+
+ :param case_sensitive: Treat upper and lower case strings as distinct.
+ :param attribute: Get the object with the max value of this attribute.
+ """
+ return _min_or_max(environment, value, max, case_sensitive, attribute)
+
+
+def do_default(
+ value: V,
+ default_value: V = "", # type: ignore
+ boolean: bool = False,
+) -> V:
+ """If the value is undefined it will return the passed default value,
+ otherwise the value of the variable:
+
+ .. sourcecode:: jinja
+
+ {{ my_variable|default('my_variable is not defined') }}
+
+ This will output the value of ``my_variable`` if the variable was
+ defined, otherwise ``'my_variable is not defined'``. If you want
+ to use default with variables that evaluate to false you have to
+ set the second parameter to `true`:
+
+ .. sourcecode:: jinja
+
+ {{ ''|default('the string was empty', true) }}
+
+ .. versionchanged:: 2.11
+ It's now possible to configure the :class:`~jinja2.Environment` with
+ :class:`~jinja2.ChainableUndefined` to make the `default` filter work
+ on nested elements and attributes that may contain undefined values
+ in the chain without getting an :exc:`~jinja2.UndefinedError`.
+ """
+ if isinstance(value, Undefined) or (boolean and not value):
+ return default_value
+
+ return value
+
+
+@pass_eval_context
+def sync_do_join(
+ eval_ctx: "EvalContext",
+ value: t.Iterable[t.Any],
+ d: str = "",
+ attribute: t.Optional[t.Union[str, int]] = None,
+) -> str:
+ """Return a string which is the concatenation of the strings in the
+ sequence. The separator between elements is an empty string per
+ default, you can define it with the optional parameter:
+
+ .. sourcecode:: jinja
+
+ {{ [1, 2, 3]|join('|') }}
+ -> 1|2|3
+
+ {{ [1, 2, 3]|join }}
+ -> 123
+
+ It is also possible to join certain attributes of an object:
+
+ .. sourcecode:: jinja
+
+ {{ users|join(', ', attribute='username') }}
+
+ .. versionadded:: 2.6
+ The `attribute` parameter was added.
+ """
+ if attribute is not None:
+ value = map(make_attrgetter(eval_ctx.environment, attribute), value)
+
+ # no automatic escaping? joining is a lot easier then
+ if not eval_ctx.autoescape:
+ return str(d).join(map(str, value))
+
+ # if the delimiter doesn't have an html representation we check
+ # if any of the items has. If yes we do a coercion to Markup
+ if not hasattr(d, "__html__"):
+ value = list(value)
+ do_escape = False
+
+ for idx, item in enumerate(value):
+ if hasattr(item, "__html__"):
+ do_escape = True
+ else:
+ value[idx] = str(item)
+
+ if do_escape:
+ d = escape(d)
+ else:
+ d = str(d)
+
+ return d.join(value)
+
+ # no html involved, to normal joining
+ return soft_str(d).join(map(soft_str, value))
+
+
+@async_variant(sync_do_join) # type: ignore
+async def do_join(
+ eval_ctx: "EvalContext",
+ value: t.Union[t.AsyncIterable[t.Any], t.Iterable[t.Any]],
+ d: str = "",
+ attribute: t.Optional[t.Union[str, int]] = None,
+) -> str:
+ return sync_do_join(eval_ctx, await auto_to_list(value), d, attribute)
+
+
+def do_center(value: str, width: int = 80) -> str:
+ """Centers the value in a field of a given width."""
+ return soft_str(value).center(width)
+
+
+@pass_environment
+def sync_do_first(
+ environment: "Environment", seq: "t.Iterable[V]"
+) -> "t.Union[V, Undefined]":
+ """Return the first item of a sequence."""
+ try:
+ return next(iter(seq))
+ except StopIteration:
+ return environment.undefined("No first item, sequence was empty.")
+
+
+@async_variant(sync_do_first) # type: ignore
+async def do_first(
+ environment: "Environment", seq: "t.Union[t.AsyncIterable[V], t.Iterable[V]]"
+) -> "t.Union[V, Undefined]":
+ try:
+ return await auto_aiter(seq).__anext__()
+ except StopAsyncIteration:
+ return environment.undefined("No first item, sequence was empty.")
+
+
+@pass_environment
+def do_last(
+ environment: "Environment", seq: "t.Reversible[V]"
+) -> "t.Union[V, Undefined]":
+ """Return the last item of a sequence.
+
+ Note: Does not work with generators. You may want to explicitly
+ convert it to a list:
+
+ .. sourcecode:: jinja
+
+ {{ data | selectattr('name', '==', 'Jinja') | list | last }}
+ """
+ try:
+ return next(iter(reversed(seq)))
+ except StopIteration:
+ return environment.undefined("No last item, sequence was empty.")
+
+
+# No async do_last, it may not be safe in async mode.
+
+
+@pass_context
+def do_random(context: "Context", seq: "t.Sequence[V]") -> "t.Union[V, Undefined]":
+ """Return a random item from the sequence."""
+ try:
+ return random.choice(seq)
+ except IndexError:
+ return context.environment.undefined("No random item, sequence was empty.")
+
+
+def do_filesizeformat(value: t.Union[str, float, int], binary: bool = False) -> str:
+ """Format the value like a 'human-readable' file size (i.e. 13 kB,
+ 4.1 MB, 102 Bytes, etc). Per default decimal prefixes are used (Mega,
+ Giga, etc.), if the second parameter is set to `True` the binary
+ prefixes are used (Mebi, Gibi).
+ """
+ bytes = float(value)
+ base = 1024 if binary else 1000
+ prefixes = [
+ ("KiB" if binary else "kB"),
+ ("MiB" if binary else "MB"),
+ ("GiB" if binary else "GB"),
+ ("TiB" if binary else "TB"),
+ ("PiB" if binary else "PB"),
+ ("EiB" if binary else "EB"),
+ ("ZiB" if binary else "ZB"),
+ ("YiB" if binary else "YB"),
+ ]
+
+ if bytes == 1:
+ return "1 Byte"
+ elif bytes < base:
+ return f"{int(bytes)} Bytes"
+ else:
+ for i, prefix in enumerate(prefixes):
+ unit = base ** (i + 2)
+
+ if bytes < unit:
+ return f"{base * bytes / unit:.1f} {prefix}"
+
+ return f"{base * bytes / unit:.1f} {prefix}"
+
+
+def do_pprint(value: t.Any) -> str:
+ """Pretty print a variable. Useful for debugging."""
+ return pformat(value)
+
+
+_uri_scheme_re = re.compile(r"^([\w.+-]{2,}:(/){0,2})$")
+
+
+@pass_eval_context
+def do_urlize(
+ eval_ctx: "EvalContext",
+ value: str,
+ trim_url_limit: t.Optional[int] = None,
+ nofollow: bool = False,
+ target: t.Optional[str] = None,
+ rel: t.Optional[str] = None,
+ extra_schemes: t.Optional[t.Iterable[str]] = None,
+) -> str:
+ """Convert URLs in text into clickable links.
+
+ This may not recognize links in some situations. Usually, a more
+ comprehensive formatter, such as a Markdown library, is a better
+ choice.
+
+ Works on ``http://``, ``https://``, ``www.``, ``mailto:``, and email
+ addresses. Links with trailing punctuation (periods, commas, closing
+ parentheses) and leading punctuation (opening parentheses) are
+ recognized excluding the punctuation. Email addresses that include
+ header fields are not recognized (for example,
+ ``mailto:address@example.com?cc=copy@example.com``).
+
+ :param value: Original text containing URLs to link.
+ :param trim_url_limit: Shorten displayed URL values to this length.
+ :param nofollow: Add the ``rel=nofollow`` attribute to links.
+ :param target: Add the ``target`` attribute to links.
+ :param rel: Add the ``rel`` attribute to links.
+ :param extra_schemes: Recognize URLs that start with these schemes
+ in addition to the default behavior. Defaults to
+ ``env.policies["urlize.extra_schemes"]``, which defaults to no
+ extra schemes.
+
+ .. versionchanged:: 3.0
+ The ``extra_schemes`` parameter was added.
+
+ .. versionchanged:: 3.0
+ Generate ``https://`` links for URLs without a scheme.
+
+ .. versionchanged:: 3.0
+ The parsing rules were updated. Recognize email addresses with
+ or without the ``mailto:`` scheme. Validate IP addresses. Ignore
+ parentheses and brackets in more cases.
+
+ .. versionchanged:: 2.8
+ The ``target`` parameter was added.
+ """
+ policies = eval_ctx.environment.policies
+ rel_parts = set((rel or "").split())
+
+ if nofollow:
+ rel_parts.add("nofollow")
+
+ rel_parts.update((policies["urlize.rel"] or "").split())
+ rel = " ".join(sorted(rel_parts)) or None
+
+ if target is None:
+ target = policies["urlize.target"]
+
+ if extra_schemes is None:
+ extra_schemes = policies["urlize.extra_schemes"] or ()
+
+ for scheme in extra_schemes:
+ if _uri_scheme_re.fullmatch(scheme) is None:
+ raise FilterArgumentError(f"{scheme!r} is not a valid URI scheme prefix.")
+
+ rv = urlize(
+ value,
+ trim_url_limit=trim_url_limit,
+ rel=rel,
+ target=target,
+ extra_schemes=extra_schemes,
+ )
+
+ if eval_ctx.autoescape:
+ rv = Markup(rv)
+
+ return rv
+
+
+def do_indent(
+ s: str, width: t.Union[int, str] = 4, first: bool = False, blank: bool = False
+) -> str:
+ """Return a copy of the string with each line indented by 4 spaces. The
+ first line and blank lines are not indented by default.
+
+ :param width: Number of spaces, or a string, to indent by.
+ :param first: Don't skip indenting the first line.
+ :param blank: Don't skip indenting empty lines.
+
+ .. versionchanged:: 3.0
+ ``width`` can be a string.
+
+ .. versionchanged:: 2.10
+ Blank lines are not indented by default.
+
+ Rename the ``indentfirst`` argument to ``first``.
+ """
+ if isinstance(width, str):
+ indention = width
+ else:
+ indention = " " * width
+
+ newline = "\n"
+
+ if isinstance(s, Markup):
+ indention = Markup(indention)
+ newline = Markup(newline)
+
+ s += newline # this quirk is necessary for splitlines method
+
+ if blank:
+ rv = (newline + indention).join(s.splitlines())
+ else:
+ lines = s.splitlines()
+ rv = lines.pop(0)
+
+ if lines:
+ rv += newline + newline.join(
+ indention + line if line else line for line in lines
+ )
+
+ if first:
+ rv = indention + rv
+
+ return rv
+
+
+@pass_environment
+def do_truncate(
+ env: "Environment",
+ s: str,
+ length: int = 255,
+ killwords: bool = False,
+ end: str = "...",
+ leeway: t.Optional[int] = None,
+) -> str:
+ """Return a truncated copy of the string. The length is specified
+ with the first parameter which defaults to ``255``. If the second
+ parameter is ``true`` the filter will cut the text at length. Otherwise
+ it will discard the last word. If the text was in fact
+ truncated it will append an ellipsis sign (``"..."``). If you want a
+ different ellipsis sign than ``"..."`` you can specify it using the
+ third parameter. Strings that only exceed the length by the tolerance
+ margin given in the fourth parameter will not be truncated.
+
+ .. sourcecode:: jinja
+
+ {{ "foo bar baz qux"|truncate(9) }}
+ -> "foo..."
+ {{ "foo bar baz qux"|truncate(9, True) }}
+ -> "foo ba..."
+ {{ "foo bar baz qux"|truncate(11) }}
+ -> "foo bar baz qux"
+ {{ "foo bar baz qux"|truncate(11, False, '...', 0) }}
+ -> "foo bar..."
+
+ The default leeway on newer Jinja versions is 5 and was 0 before but
+ can be reconfigured globally.
+ """
+ if leeway is None:
+ leeway = env.policies["truncate.leeway"]
+
+ assert length >= len(end), f"expected length >= {len(end)}, got {length}"
+ assert leeway >= 0, f"expected leeway >= 0, got {leeway}"
+
+ if len(s) <= length + leeway:
+ return s
+
+ if killwords:
+ return s[: length - len(end)] + end
+
+ result = s[: length - len(end)].rsplit(" ", 1)[0]
+ return result + end
+
+
+@pass_environment
+def do_wordwrap(
+ environment: "Environment",
+ s: str,
+ width: int = 79,
+ break_long_words: bool = True,
+ wrapstring: t.Optional[str] = None,
+ break_on_hyphens: bool = True,
+) -> str:
+ """Wrap a string to the given width. Existing newlines are treated
+ as paragraphs to be wrapped separately.
+
+ :param s: Original text to wrap.
+ :param width: Maximum length of wrapped lines.
+ :param break_long_words: If a word is longer than ``width``, break
+ it across lines.
+ :param break_on_hyphens: If a word contains hyphens, it may be split
+ across lines.
+ :param wrapstring: String to join each wrapped line. Defaults to
+ :attr:`Environment.newline_sequence`.
+
+ .. versionchanged:: 2.11
+ Existing newlines are treated as paragraphs wrapped separately.
+
+ .. versionchanged:: 2.11
+ Added the ``break_on_hyphens`` parameter.
+
+ .. versionchanged:: 2.7
+ Added the ``wrapstring`` parameter.
+ """
+ import textwrap
+
+ if wrapstring is None:
+ wrapstring = environment.newline_sequence
+
+ # textwrap.wrap doesn't consider existing newlines when wrapping.
+ # If the string has a newline before width, wrap will still insert
+ # a newline at width, resulting in a short line. Instead, split and
+ # wrap each paragraph individually.
+ return wrapstring.join(
+ [
+ wrapstring.join(
+ textwrap.wrap(
+ line,
+ width=width,
+ expand_tabs=False,
+ replace_whitespace=False,
+ break_long_words=break_long_words,
+ break_on_hyphens=break_on_hyphens,
+ )
+ )
+ for line in s.splitlines()
+ ]
+ )
+
+
+_word_re = re.compile(r"\w+")
+
+
+def do_wordcount(s: str) -> int:
+ """Count the words in that string."""
+ return len(_word_re.findall(soft_str(s)))
+
+
+def do_int(value: t.Any, default: int = 0, base: int = 10) -> int:
+ """Convert the value into an integer. If the
+ conversion doesn't work it will return ``0``. You can
+ override this default using the first parameter. You
+ can also override the default base (10) in the second
+ parameter, which handles input with prefixes such as
+ 0b, 0o and 0x for bases 2, 8 and 16 respectively.
+ The base is ignored for decimal numbers and non-string values.
+ """
+ try:
+ if isinstance(value, str):
+ return int(value, base)
+
+ return int(value)
+ except (TypeError, ValueError):
+ # this quirk is necessary so that "42.23"|int gives 42.
+ try:
+ return int(float(value))
+ except (TypeError, ValueError, OverflowError):
+ return default
+
+
+def do_float(value: t.Any, default: float = 0.0) -> float:
+ """Convert the value into a floating point number. If the
+ conversion doesn't work it will return ``0.0``. You can
+ override this default using the first parameter.
+ """
+ try:
+ return float(value)
+ except (TypeError, ValueError):
+ return default
+
+
+def do_format(value: str, *args: t.Any, **kwargs: t.Any) -> str:
+ """Apply the given values to a `printf-style`_ format string, like
+ ``string % values``.
+
+ .. sourcecode:: jinja
+
+ {{ "%s, %s!"|format(greeting, name) }}
+ Hello, World!
+
+ In most cases it should be more convenient and efficient to use the
+ ``%`` operator or :meth:`str.format`.
+
+ .. code-block:: text
+
+ {{ "%s, %s!" % (greeting, name) }}
+ {{ "{}, {}!".format(greeting, name) }}
+
+ .. _printf-style: https://docs.python.org/library/stdtypes.html
+ #printf-style-string-formatting
+ """
+ if args and kwargs:
+ raise FilterArgumentError(
+ "can't handle positional and keyword arguments at the same time"
+ )
+
+ return soft_str(value) % (kwargs or args)
+
+
+def do_trim(value: str, chars: t.Optional[str] = None) -> str:
+ """Strip leading and trailing characters, by default whitespace."""
+ return soft_str(value).strip(chars)
+
+
+def do_striptags(value: "t.Union[str, HasHTML]") -> str:
+ """Strip SGML/XML tags and replace adjacent whitespace by one space."""
+ if hasattr(value, "__html__"):
+ value = t.cast("HasHTML", value).__html__()
+
+ return Markup(str(value)).striptags()
+
+
+def sync_do_slice(
+ value: "t.Collection[V]", slices: int, fill_with: "t.Optional[V]" = None
+) -> "t.Iterator[t.List[V]]":
+ """Slice an iterator and return a list of lists containing
+ those items. Useful if you want to create a div containing
+ three ul tags that represent columns:
+
+ .. sourcecode:: html+jinja
+
+
+ {%- for column in items|slice(3) %}
+
+ {%- for item in column %}
+
{{ item }}
+ {%- endfor %}
+
+ {%- endfor %}
+
+
+ If you pass it a second argument it's used to fill missing
+ values on the last iteration.
+ """
+ seq = list(value)
+ length = len(seq)
+ items_per_slice = length // slices
+ slices_with_extra = length % slices
+ offset = 0
+
+ for slice_number in range(slices):
+ start = offset + slice_number * items_per_slice
+
+ if slice_number < slices_with_extra:
+ offset += 1
+
+ end = offset + (slice_number + 1) * items_per_slice
+ tmp = seq[start:end]
+
+ if fill_with is not None and slice_number >= slices_with_extra:
+ tmp.append(fill_with)
+
+ yield tmp
+
+
+@async_variant(sync_do_slice) # type: ignore
+async def do_slice(
+ value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
+ slices: int,
+ fill_with: t.Optional[t.Any] = None,
+) -> "t.Iterator[t.List[V]]":
+ return sync_do_slice(await auto_to_list(value), slices, fill_with)
+
+
+def do_batch(
+ value: "t.Iterable[V]", linecount: int, fill_with: "t.Optional[V]" = None
+) -> "t.Iterator[t.List[V]]":
+ """
+ A filter that batches items. It works pretty much like `slice`
+ just the other way round. It returns a list of lists with the
+ given number of items. If you provide a second parameter this
+ is used to fill up missing items. See this example:
+
+ .. sourcecode:: html+jinja
+
+
+ {%- for row in items|batch(3, ' ') %}
+
+ {%- for column in row %}
+
{{ column }}
+ {%- endfor %}
+
+ {%- endfor %}
+
+ """
+ tmp: t.List[V] = []
+
+ for item in value:
+ if len(tmp) == linecount:
+ yield tmp
+ tmp = []
+
+ tmp.append(item)
+
+ if tmp:
+ if fill_with is not None and len(tmp) < linecount:
+ tmp += [fill_with] * (linecount - len(tmp))
+
+ yield tmp
+
+
+def do_round(
+ value: float,
+ precision: int = 0,
+ method: 'te.Literal["common", "ceil", "floor"]' = "common",
+) -> float:
+ """Round the number to a given precision. The first
+ parameter specifies the precision (default is ``0``), the
+ second the rounding method:
+
+ - ``'common'`` rounds either up or down
+ - ``'ceil'`` always rounds up
+ - ``'floor'`` always rounds down
+
+ If you don't specify a method ``'common'`` is used.
+
+ .. sourcecode:: jinja
+
+ {{ 42.55|round }}
+ -> 43.0
+ {{ 42.55|round(1, 'floor') }}
+ -> 42.5
+
+ Note that even if rounded to 0 precision, a float is returned. If
+ you need a real integer, pipe it through `int`:
+
+ .. sourcecode:: jinja
+
+ {{ 42.55|round|int }}
+ -> 43
+ """
+ if method not in {"common", "ceil", "floor"}:
+ raise FilterArgumentError("method must be common, ceil or floor")
+
+ if method == "common":
+ return round(value, precision)
+
+ func = getattr(math, method)
+ return t.cast(float, func(value * (10**precision)) / (10**precision))
+
+
+class _GroupTuple(t.NamedTuple):
+ grouper: t.Any
+ list: t.List[t.Any]
+
+ # Use the regular tuple repr to hide this subclass if users print
+ # out the value during debugging.
+ def __repr__(self) -> str:
+ return tuple.__repr__(self)
+
+ def __str__(self) -> str:
+ return tuple.__str__(self)
+
+
+@pass_environment
+def sync_do_groupby(
+ environment: "Environment",
+ value: "t.Iterable[V]",
+ attribute: t.Union[str, int],
+ default: t.Optional[t.Any] = None,
+ case_sensitive: bool = False,
+) -> "t.List[_GroupTuple]":
+ """Group a sequence of objects by an attribute using Python's
+ :func:`itertools.groupby`. The attribute can use dot notation for
+ nested access, like ``"address.city"``. Unlike Python's ``groupby``,
+ the values are sorted first so only one group is returned for each
+ unique value.
+
+ For example, a list of ``User`` objects with a ``city`` attribute
+ can be rendered in groups. In this example, ``grouper`` refers to
+ the ``city`` value of the group.
+
+ .. sourcecode:: html+jinja
+
+
{% for city, items in users|groupby("city") %}
+
{{ city }}
+
{% for user in items %}
+
{{ user.name }}
+ {% endfor %}
+
+ {% endfor %}
+
+ ``groupby`` yields namedtuples of ``(grouper, list)``, which
+ can be used instead of the tuple unpacking above. ``grouper`` is the
+ value of the attribute, and ``list`` is the items with that value.
+
+ .. sourcecode:: html+jinja
+
+
+
+ You can specify a ``default`` value to use if an object in the list
+ does not have the given attribute.
+
+ .. sourcecode:: jinja
+
+
{% for city, items in users|groupby("city", default="NY") %}
+
{{ city }}: {{ items|map(attribute="name")|join(", ") }}
+ {% endfor %}
+
+ Like the :func:`~jinja-filters.sort` filter, sorting and grouping is
+ case-insensitive by default. The ``key`` for each group will have
+ the case of the first item in that group of values. For example, if
+ a list of users has cities ``["CA", "NY", "ca"]``, the "CA" group
+ will have two values. This can be disabled by passing
+ ``case_sensitive=True``.
+
+ .. versionchanged:: 3.1
+ Added the ``case_sensitive`` parameter. Sorting and grouping is
+ case-insensitive by default, matching other filters that do
+ comparisons.
+
+ .. versionchanged:: 3.0
+ Added the ``default`` parameter.
+
+ .. versionchanged:: 2.6
+ The attribute supports dot notation for nested access.
+ """
+ expr = make_attrgetter(
+ environment,
+ attribute,
+ postprocess=ignore_case if not case_sensitive else None,
+ default=default,
+ )
+ out = [
+ _GroupTuple(key, list(values))
+ for key, values in groupby(sorted(value, key=expr), expr)
+ ]
+
+ if not case_sensitive:
+ # Return the real key from the first value instead of the lowercase key.
+ output_expr = make_attrgetter(environment, attribute, default=default)
+ out = [_GroupTuple(output_expr(values[0]), values) for _, values in out]
+
+ return out
+
+
+@async_variant(sync_do_groupby) # type: ignore
+async def do_groupby(
+ environment: "Environment",
+ value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
+ attribute: t.Union[str, int],
+ default: t.Optional[t.Any] = None,
+ case_sensitive: bool = False,
+) -> "t.List[_GroupTuple]":
+ expr = make_attrgetter(
+ environment,
+ attribute,
+ postprocess=ignore_case if not case_sensitive else None,
+ default=default,
+ )
+ out = [
+ _GroupTuple(key, await auto_to_list(values))
+ for key, values in groupby(sorted(await auto_to_list(value), key=expr), expr)
+ ]
+
+ if not case_sensitive:
+ # Return the real key from the first value instead of the lowercase key.
+ output_expr = make_attrgetter(environment, attribute, default=default)
+ out = [_GroupTuple(output_expr(values[0]), values) for _, values in out]
+
+ return out
+
+
+@pass_environment
+def sync_do_sum(
+ environment: "Environment",
+ iterable: "t.Iterable[V]",
+ attribute: t.Optional[t.Union[str, int]] = None,
+ start: V = 0, # type: ignore
+) -> V:
+ """Returns the sum of a sequence of numbers plus the value of parameter
+ 'start' (which defaults to 0). When the sequence is empty it returns
+ start.
+
+ It is also possible to sum up only certain attributes:
+
+ .. sourcecode:: jinja
+
+ Total: {{ items|sum(attribute='price') }}
+
+ .. versionchanged:: 2.6
+ The ``attribute`` parameter was added to allow summing up over
+ attributes. Also the ``start`` parameter was moved on to the right.
+ """
+ if attribute is not None:
+ iterable = map(make_attrgetter(environment, attribute), iterable)
+
+ return sum(iterable, start) # type: ignore[no-any-return, call-overload]
+
+
+@async_variant(sync_do_sum) # type: ignore
+async def do_sum(
+ environment: "Environment",
+ iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
+ attribute: t.Optional[t.Union[str, int]] = None,
+ start: V = 0, # type: ignore
+) -> V:
+ rv = start
+
+ if attribute is not None:
+ func = make_attrgetter(environment, attribute)
+ else:
+
+ def func(x: V) -> V:
+ return x
+
+ async for item in auto_aiter(iterable):
+ rv += func(item)
+
+ return rv
+
+
+def sync_do_list(value: "t.Iterable[V]") -> "t.List[V]":
+ """Convert the value into a list. If it was a string the returned list
+ will be a list of characters.
+ """
+ return list(value)
+
+
+@async_variant(sync_do_list) # type: ignore
+async def do_list(value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]") -> "t.List[V]":
+ return await auto_to_list(value)
+
+
+def do_mark_safe(value: str) -> Markup:
+ """Mark the value as safe which means that in an environment with automatic
+ escaping enabled this variable will not be escaped.
+ """
+ return Markup(value)
+
+
+def do_mark_unsafe(value: str) -> str:
+ """Mark a value as unsafe. This is the reverse operation for :func:`safe`."""
+ return str(value)
+
+
+@typing.overload
+def do_reverse(value: str) -> str: ...
+
+
+@typing.overload
+def do_reverse(value: "t.Iterable[V]") -> "t.Iterable[V]": ...
+
+
+def do_reverse(value: t.Union[str, t.Iterable[V]]) -> t.Union[str, t.Iterable[V]]:
+ """Reverse the object or return an iterator that iterates over it the other
+ way round.
+ """
+ if isinstance(value, str):
+ return value[::-1]
+
+ try:
+ return reversed(value) # type: ignore
+ except TypeError:
+ try:
+ rv = list(value)
+ rv.reverse()
+ return rv
+ except TypeError as e:
+ raise FilterArgumentError("argument must be iterable") from e
+
+
+@pass_environment
+def do_attr(
+ environment: "Environment", obj: t.Any, name: str
+) -> t.Union[Undefined, t.Any]:
+ """Get an attribute of an object. ``foo|attr("bar")`` works like
+ ``foo.bar``, but returns undefined instead of falling back to ``foo["bar"]``
+ if the attribute doesn't exist.
+
+ See :ref:`Notes on subscriptions ` for more details.
+ """
+ # Environment.getattr will fall back to obj[name] if obj.name doesn't exist.
+ # But we want to call env.getattr to get behavior such as sandboxing.
+ # Determine if the attr exists first, so we know the fallback won't trigger.
+ try:
+ # This avoids executing properties/descriptors, but misses __getattr__
+ # and __getattribute__ dynamic attrs.
+ getattr_static(obj, name)
+ except AttributeError:
+ # This finds dynamic attrs, and we know it's not a descriptor at this point.
+ if not hasattr(obj, name):
+ return environment.undefined(obj=obj, name=name)
+
+ return environment.getattr(obj, name)
+
+
+@typing.overload
+def sync_do_map(
+ context: "Context",
+ value: t.Iterable[t.Any],
+ name: str,
+ *args: t.Any,
+ **kwargs: t.Any,
+) -> t.Iterable[t.Any]: ...
+
+
+@typing.overload
+def sync_do_map(
+ context: "Context",
+ value: t.Iterable[t.Any],
+ *,
+ attribute: str = ...,
+ default: t.Optional[t.Any] = None,
+) -> t.Iterable[t.Any]: ...
+
+
+@pass_context
+def sync_do_map(
+ context: "Context", value: t.Iterable[t.Any], *args: t.Any, **kwargs: t.Any
+) -> t.Iterable[t.Any]:
+ """Applies a filter on a sequence of objects or looks up an attribute.
+ This is useful when dealing with lists of objects but you are really
+ only interested in a certain value of it.
+
+ The basic usage is mapping on an attribute. Imagine you have a list
+ of users but you are only interested in a list of usernames:
+
+ .. sourcecode:: jinja
+
+ Users on this page: {{ users|map(attribute='username')|join(', ') }}
+
+ You can specify a ``default`` value to use if an object in the list
+ does not have the given attribute.
+
+ .. sourcecode:: jinja
+
+ {{ users|map(attribute="username", default="Anonymous")|join(", ") }}
+
+ Alternatively you can let it invoke a filter by passing the name of the
+ filter and the arguments afterwards. A good example would be applying a
+ text conversion filter on a sequence:
+
+ .. sourcecode:: jinja
+
+ Users on this page: {{ titles|map('lower')|join(', ') }}
+
+ Similar to a generator comprehension such as:
+
+ .. code-block:: python
+
+ (u.username for u in users)
+ (getattr(u, "username", "Anonymous") for u in users)
+ (do_lower(x) for x in titles)
+
+ .. versionchanged:: 2.11.0
+ Added the ``default`` parameter.
+
+ .. versionadded:: 2.7
+ """
+ if value:
+ func = prepare_map(context, args, kwargs)
+
+ for item in value:
+ yield func(item)
+
+
+@typing.overload
+def do_map(
+ context: "Context",
+ value: t.Union[t.AsyncIterable[t.Any], t.Iterable[t.Any]],
+ name: str,
+ *args: t.Any,
+ **kwargs: t.Any,
+) -> t.Iterable[t.Any]: ...
+
+
+@typing.overload
+def do_map(
+ context: "Context",
+ value: t.Union[t.AsyncIterable[t.Any], t.Iterable[t.Any]],
+ *,
+ attribute: str = ...,
+ default: t.Optional[t.Any] = None,
+) -> t.Iterable[t.Any]: ...
+
+
+@async_variant(sync_do_map) # type: ignore
+async def do_map(
+ context: "Context",
+ value: t.Union[t.AsyncIterable[t.Any], t.Iterable[t.Any]],
+ *args: t.Any,
+ **kwargs: t.Any,
+) -> t.AsyncIterable[t.Any]:
+ if value:
+ func = prepare_map(context, args, kwargs)
+
+ async for item in auto_aiter(value):
+ yield await auto_await(func(item))
+
+
+@pass_context
+def sync_do_select(
+ context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any
+) -> "t.Iterator[V]":
+ """Filters a sequence of objects by applying a test to each object,
+ and only selecting the objects with the test succeeding.
+
+ If no test is specified, each object will be evaluated as a boolean.
+
+ Example usage:
+
+ .. sourcecode:: jinja
+
+ {{ numbers|select("odd") }}
+ {{ numbers|select("odd") }}
+ {{ numbers|select("divisibleby", 3) }}
+ {{ numbers|select("lessthan", 42) }}
+ {{ strings|select("equalto", "mystring") }}
+
+ Similar to a generator comprehension such as:
+
+ .. code-block:: python
+
+ (n for n in numbers if test_odd(n))
+ (n for n in numbers if test_divisibleby(n, 3))
+
+ .. versionadded:: 2.7
+ """
+ return select_or_reject(context, value, args, kwargs, lambda x: x, False)
+
+
+@async_variant(sync_do_select) # type: ignore
+async def do_select(
+ context: "Context",
+ value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
+ *args: t.Any,
+ **kwargs: t.Any,
+) -> "t.AsyncIterator[V]":
+ return async_select_or_reject(context, value, args, kwargs, lambda x: x, False)
+
+
+@pass_context
+def sync_do_reject(
+ context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any
+) -> "t.Iterator[V]":
+ """Filters a sequence of objects by applying a test to each object,
+ and rejecting the objects with the test succeeding.
+
+ If no test is specified, each object will be evaluated as a boolean.
+
+ Example usage:
+
+ .. sourcecode:: jinja
+
+ {{ numbers|reject("odd") }}
+
+ Similar to a generator comprehension such as:
+
+ .. code-block:: python
+
+ (n for n in numbers if not test_odd(n))
+
+ .. versionadded:: 2.7
+ """
+ return select_or_reject(context, value, args, kwargs, lambda x: not x, False)
+
+
+@async_variant(sync_do_reject) # type: ignore
+async def do_reject(
+ context: "Context",
+ value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
+ *args: t.Any,
+ **kwargs: t.Any,
+) -> "t.AsyncIterator[V]":
+ return async_select_or_reject(context, value, args, kwargs, lambda x: not x, False)
+
+
+@pass_context
+def sync_do_selectattr(
+ context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any
+) -> "t.Iterator[V]":
+ """Filters a sequence of objects by applying a test to the specified
+ attribute of each object, and only selecting the objects with the
+ test succeeding.
+
+ If no test is specified, the attribute's value will be evaluated as
+ a boolean.
+
+ Example usage:
+
+ .. sourcecode:: jinja
+
+ {{ users|selectattr("is_active") }}
+ {{ users|selectattr("email", "none") }}
+
+ Similar to a generator comprehension such as:
+
+ .. code-block:: python
+
+ (user for user in users if user.is_active)
+ (user for user in users if test_none(user.email))
+
+ .. versionadded:: 2.7
+ """
+ return select_or_reject(context, value, args, kwargs, lambda x: x, True)
+
+
+@async_variant(sync_do_selectattr) # type: ignore
+async def do_selectattr(
+ context: "Context",
+ value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
+ *args: t.Any,
+ **kwargs: t.Any,
+) -> "t.AsyncIterator[V]":
+ return async_select_or_reject(context, value, args, kwargs, lambda x: x, True)
+
+
+@pass_context
+def sync_do_rejectattr(
+ context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any
+) -> "t.Iterator[V]":
+ """Filters a sequence of objects by applying a test to the specified
+ attribute of each object, and rejecting the objects with the test
+ succeeding.
+
+ If no test is specified, the attribute's value will be evaluated as
+ a boolean.
+
+ .. sourcecode:: jinja
+
+ {{ users|rejectattr("is_active") }}
+ {{ users|rejectattr("email", "none") }}
+
+ Similar to a generator comprehension such as:
+
+ .. code-block:: python
+
+ (user for user in users if not user.is_active)
+ (user for user in users if not test_none(user.email))
+
+ .. versionadded:: 2.7
+ """
+ return select_or_reject(context, value, args, kwargs, lambda x: not x, True)
+
+
+@async_variant(sync_do_rejectattr) # type: ignore
+async def do_rejectattr(
+ context: "Context",
+ value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
+ *args: t.Any,
+ **kwargs: t.Any,
+) -> "t.AsyncIterator[V]":
+ return async_select_or_reject(context, value, args, kwargs, lambda x: not x, True)
+
+
+@pass_eval_context
+def do_tojson(
+ eval_ctx: "EvalContext", value: t.Any, indent: t.Optional[int] = None
+) -> Markup:
+ """Serialize an object to a string of JSON, and mark it safe to
+ render in HTML. This filter is only for use in HTML documents.
+
+ The returned string is safe to render in HTML documents and
+ ``")
+Markup('<script>alert(document.cookie);</script>')
+
+>>> # wrap in Markup to mark text "safe" and prevent escaping
+>>> Markup("Hello")
+Markup('hello')
+
+>>> escape(Markup("Hello"))
+Markup('hello')
+
+>>> # Markup is a str subclass
+>>> # methods and operators escape their arguments
+>>> template = Markup("Hello {name}")
+>>> template.format(name='"World"')
+Markup('Hello "World"')
+```
+
+## Donate
+
+The Pallets organization develops and supports MarkupSafe and other
+popular packages. In order to grow the community of contributors and
+users, and allow the maintainers to devote more time to the projects,
+[please donate today][].
+
+[please donate today]: https://palletsprojects.com/donate
+
+## Contributing
+
+See our [detailed contributing documentation][contrib] for many ways to
+contribute, including reporting issues, requesting features, asking or answering
+questions, and making PRs.
+
+[contrib]: https://palletsprojects.com/contributing/
diff --git a/venv/Lib/site-packages/markupsafe-3.0.3.dist-info/RECORD b/venv/Lib/site-packages/markupsafe-3.0.3.dist-info/RECORD
new file mode 100644
index 0000000..a7c2277
--- /dev/null
+++ b/venv/Lib/site-packages/markupsafe-3.0.3.dist-info/RECORD
@@ -0,0 +1,14 @@
+markupsafe-3.0.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+markupsafe-3.0.3.dist-info/METADATA,sha256=8K5duwnVD7X3Yyw9U_AiCZXvdgGeWJLgpUV0ATak48s,2764
+markupsafe-3.0.3.dist-info/RECORD,,
+markupsafe-3.0.3.dist-info/WHEEL,sha256=KUuBC6lxAbHCKilKua8R9W_TM71_-9Sg5uEP3uDWcoU,101
+markupsafe-3.0.3.dist-info/licenses/LICENSE.txt,sha256=RjHsDbX9kKVH4zaBcmTGeYIUM4FG-KyUtKV_lu6MnsQ,1503
+markupsafe-3.0.3.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11
+markupsafe/__init__.py,sha256=ut2LXj-6sqkIVUdSAx-dboB5crAaRG5y7EO447hmaro,13644
+markupsafe/__pycache__/__init__.cpython-310.pyc,,
+markupsafe/__pycache__/_native.cpython-310.pyc,,
+markupsafe/_native.py,sha256=2ptkJ40yCcp9kq3L1NqpgjfpZB-obniYKFFKUOkHh4Q,218
+markupsafe/_speedups.c,sha256=efc6azc50WbKxSNinxV4rktIX2xzWfKsLvncC8Z3I_w,4527
+markupsafe/_speedups.cp310-win_amd64.pyd,sha256=y9ZP6XpRk2PbLfODpKy2H3EgQdVqHtk4pmrJZgkeGgs,13312
+markupsafe/_speedups.pyi,sha256=LSDmXYOefH4HVpAXuL8sl7AttLw0oXh1njVoVZp2wqQ,42
+markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
diff --git a/venv/Lib/site-packages/markupsafe-3.0.3.dist-info/WHEEL b/venv/Lib/site-packages/markupsafe-3.0.3.dist-info/WHEEL
new file mode 100644
index 0000000..2e7bc7f
--- /dev/null
+++ b/venv/Lib/site-packages/markupsafe-3.0.3.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: setuptools (80.9.0)
+Root-Is-Purelib: false
+Tag: cp310-cp310-win_amd64
+
diff --git a/venv/Lib/site-packages/markupsafe-3.0.3.dist-info/licenses/LICENSE.txt b/venv/Lib/site-packages/markupsafe-3.0.3.dist-info/licenses/LICENSE.txt
new file mode 100644
index 0000000..9d227a0
--- /dev/null
+++ b/venv/Lib/site-packages/markupsafe-3.0.3.dist-info/licenses/LICENSE.txt
@@ -0,0 +1,28 @@
+Copyright 2010 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/venv/Lib/site-packages/markupsafe-3.0.3.dist-info/top_level.txt b/venv/Lib/site-packages/markupsafe-3.0.3.dist-info/top_level.txt
new file mode 100644
index 0000000..75bf729
--- /dev/null
+++ b/venv/Lib/site-packages/markupsafe-3.0.3.dist-info/top_level.txt
@@ -0,0 +1 @@
+markupsafe
diff --git a/venv/Lib/site-packages/markupsafe/__init__.py b/venv/Lib/site-packages/markupsafe/__init__.py
new file mode 100644
index 0000000..4c395d7
--- /dev/null
+++ b/venv/Lib/site-packages/markupsafe/__init__.py
@@ -0,0 +1,396 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import string
+import typing as t
+
+try:
+ from ._speedups import _escape_inner
+except ImportError:
+ from ._native import _escape_inner
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+
+class _HasHTML(t.Protocol):
+ def __html__(self, /) -> str: ...
+
+
+class _TPEscape(t.Protocol):
+ def __call__(self, s: t.Any, /) -> Markup: ...
+
+
+def escape(s: t.Any, /) -> Markup:
+ """Replace the characters ``&``, ``<``, ``>``, ``'``, and ``"`` in
+ the string with HTML-safe sequences. Use this if you need to display
+ text that might contain such characters in HTML.
+
+ If the object has an ``__html__`` method, it is called and the
+ return value is assumed to already be safe for HTML.
+
+ :param s: An object to be converted to a string and escaped.
+ :return: A :class:`Markup` string with the escaped text.
+ """
+ # If the object is already a plain string, skip __html__ check and string
+ # conversion. This is the most common use case.
+ # Use type(s) instead of s.__class__ because a proxy object may be reporting
+ # the __class__ of the proxied value.
+ if type(s) is str:
+ return Markup(_escape_inner(s))
+
+ if hasattr(s, "__html__"):
+ return Markup(s.__html__())
+
+ return Markup(_escape_inner(str(s)))
+
+
+def escape_silent(s: t.Any | None, /) -> Markup:
+ """Like :func:`escape` but treats ``None`` as the empty string.
+ Useful with optional values, as otherwise you get the string
+ ``'None'`` when the value is ``None``.
+
+ >>> escape(None)
+ Markup('None')
+ >>> escape_silent(None)
+ Markup('')
+ """
+ if s is None:
+ return Markup()
+
+ return escape(s)
+
+
+def soft_str(s: t.Any, /) -> str:
+ """Convert an object to a string if it isn't already. This preserves
+ a :class:`Markup` string rather than converting it back to a basic
+ string, so it will still be marked as safe and won't be escaped
+ again.
+
+ >>> value = escape("")
+ >>> value
+ Markup('<User 1>')
+ >>> escape(str(value))
+ Markup('<User 1>')
+ >>> escape(soft_str(value))
+ Markup('<User 1>')
+ """
+ if not isinstance(s, str):
+ return str(s)
+
+ return s
+
+
+class Markup(str):
+ """A string that is ready to be safely inserted into an HTML or XML
+ document, either because it was escaped or because it was marked
+ safe.
+
+ Passing an object to the constructor converts it to text and wraps
+ it to mark it safe without escaping. To escape the text, use the
+ :meth:`escape` class method instead.
+
+ >>> Markup("Hello, World!")
+ Markup('Hello, World!')
+ >>> Markup(42)
+ Markup('42')
+ >>> Markup.escape("Hello, World!")
+ Markup('Hello <em>World</em>!')
+
+ This implements the ``__html__()`` interface that some frameworks
+ use. Passing an object that implements ``__html__()`` will wrap the
+ output of that method, marking it safe.
+
+ >>> class Foo:
+ ... def __html__(self):
+ ... return 'foo'
+ ...
+ >>> Markup(Foo())
+ Markup('foo')
+
+ This is a subclass of :class:`str`. It has the same methods, but
+ escapes their arguments and returns a ``Markup`` instance.
+
+ >>> Markup("%s") % ("foo & bar",)
+ Markup('foo & bar')
+ >>> Markup("Hello ") + ""
+ Markup('Hello <foo>')
+ """
+
+ __slots__ = ()
+
+ def __new__(
+ cls, object: t.Any = "", encoding: str | None = None, errors: str = "strict"
+ ) -> te.Self:
+ if hasattr(object, "__html__"):
+ object = object.__html__()
+
+ if encoding is None:
+ return super().__new__(cls, object)
+
+ return super().__new__(cls, object, encoding, errors)
+
+ def __html__(self, /) -> te.Self:
+ return self
+
+ def __add__(self, value: str | _HasHTML, /) -> te.Self:
+ if isinstance(value, str) or hasattr(value, "__html__"):
+ return self.__class__(super().__add__(self.escape(value)))
+
+ return NotImplemented
+
+ def __radd__(self, value: str | _HasHTML, /) -> te.Self:
+ if isinstance(value, str) or hasattr(value, "__html__"):
+ return self.escape(value).__add__(self)
+
+ return NotImplemented
+
+ def __mul__(self, value: t.SupportsIndex, /) -> te.Self:
+ return self.__class__(super().__mul__(value))
+
+ def __rmul__(self, value: t.SupportsIndex, /) -> te.Self:
+ return self.__class__(super().__mul__(value))
+
+ def __mod__(self, value: t.Any, /) -> te.Self:
+ if isinstance(value, tuple):
+ # a tuple of arguments, each wrapped
+ value = tuple(_MarkupEscapeHelper(x, self.escape) for x in value)
+ elif hasattr(type(value), "__getitem__") and not isinstance(value, str):
+ # a mapping of arguments, wrapped
+ value = _MarkupEscapeHelper(value, self.escape)
+ else:
+ # a single argument, wrapped with the helper and a tuple
+ value = (_MarkupEscapeHelper(value, self.escape),)
+
+ return self.__class__(super().__mod__(value))
+
+ def __repr__(self, /) -> str:
+ return f"{self.__class__.__name__}({super().__repr__()})"
+
+ def join(self, iterable: cabc.Iterable[str | _HasHTML], /) -> te.Self:
+ return self.__class__(super().join(map(self.escape, iterable)))
+
+ def split( # type: ignore[override]
+ self, /, sep: str | None = None, maxsplit: t.SupportsIndex = -1
+ ) -> list[te.Self]:
+ return [self.__class__(v) for v in super().split(sep, maxsplit)]
+
+ def rsplit( # type: ignore[override]
+ self, /, sep: str | None = None, maxsplit: t.SupportsIndex = -1
+ ) -> list[te.Self]:
+ return [self.__class__(v) for v in super().rsplit(sep, maxsplit)]
+
+ def splitlines( # type: ignore[override]
+ self, /, keepends: bool = False
+ ) -> list[te.Self]:
+ return [self.__class__(v) for v in super().splitlines(keepends)]
+
+ def unescape(self, /) -> str:
+ """Convert escaped markup back into a text string. This replaces
+ HTML entities with the characters they represent.
+
+ >>> Markup("Main » About").unescape()
+ 'Main » About'
+ """
+ from html import unescape
+
+ return unescape(str(self))
+
+ def striptags(self, /) -> str:
+ """:meth:`unescape` the markup, remove tags, and normalize
+ whitespace to single spaces.
+
+ >>> Markup("Main »\tAbout").striptags()
+ 'Main » About'
+ """
+ value = str(self)
+
+ # Look for comments then tags separately. Otherwise, a comment that
+ # contains a tag would end early, leaving some of the comment behind.
+
+ # keep finding comment start marks
+ while (start := value.find("", start)) == -1:
+ break
+
+ value = f"{value[:start]}{value[end + 3 :]}"
+
+ # remove tags using the same method
+ while (start := value.find("<")) != -1:
+ if (end := value.find(">", start)) == -1:
+ break
+
+ value = f"{value[:start]}{value[end + 1 :]}"
+
+ # collapse spaces
+ value = " ".join(value.split())
+ return self.__class__(value).unescape()
+
+ @classmethod
+ def escape(cls, s: t.Any, /) -> te.Self:
+ """Escape a string. Calls :func:`escape` and ensures that for
+ subclasses the correct type is returned.
+ """
+ rv = escape(s)
+
+ if rv.__class__ is not cls:
+ return cls(rv)
+
+ return rv # type: ignore[return-value]
+
+ def __getitem__(self, key: t.SupportsIndex | slice, /) -> te.Self:
+ return self.__class__(super().__getitem__(key))
+
+ def capitalize(self, /) -> te.Self:
+ return self.__class__(super().capitalize())
+
+ def title(self, /) -> te.Self:
+ return self.__class__(super().title())
+
+ def lower(self, /) -> te.Self:
+ return self.__class__(super().lower())
+
+ def upper(self, /) -> te.Self:
+ return self.__class__(super().upper())
+
+ def replace(self, old: str, new: str, count: t.SupportsIndex = -1, /) -> te.Self:
+ return self.__class__(super().replace(old, self.escape(new), count))
+
+ def ljust(self, width: t.SupportsIndex, fillchar: str = " ", /) -> te.Self:
+ return self.__class__(super().ljust(width, self.escape(fillchar)))
+
+ def rjust(self, width: t.SupportsIndex, fillchar: str = " ", /) -> te.Self:
+ return self.__class__(super().rjust(width, self.escape(fillchar)))
+
+ def lstrip(self, chars: str | None = None, /) -> te.Self:
+ return self.__class__(super().lstrip(chars))
+
+ def rstrip(self, chars: str | None = None, /) -> te.Self:
+ return self.__class__(super().rstrip(chars))
+
+ def center(self, width: t.SupportsIndex, fillchar: str = " ", /) -> te.Self:
+ return self.__class__(super().center(width, self.escape(fillchar)))
+
+ def strip(self, chars: str | None = None, /) -> te.Self:
+ return self.__class__(super().strip(chars))
+
+ def translate(
+ self,
+ table: cabc.Mapping[int, str | int | None], # type: ignore[override]
+ /,
+ ) -> str:
+ return self.__class__(super().translate(table))
+
+ def expandtabs(self, /, tabsize: t.SupportsIndex = 8) -> te.Self:
+ return self.__class__(super().expandtabs(tabsize))
+
+ def swapcase(self, /) -> te.Self:
+ return self.__class__(super().swapcase())
+
+ def zfill(self, width: t.SupportsIndex, /) -> te.Self:
+ return self.__class__(super().zfill(width))
+
+ def casefold(self, /) -> te.Self:
+ return self.__class__(super().casefold())
+
+ def removeprefix(self, prefix: str, /) -> te.Self:
+ return self.__class__(super().removeprefix(prefix))
+
+ def removesuffix(self, suffix: str) -> te.Self:
+ return self.__class__(super().removesuffix(suffix))
+
+ def partition(self, sep: str, /) -> tuple[te.Self, te.Self, te.Self]:
+ left, sep, right = super().partition(sep)
+ cls = self.__class__
+ return cls(left), cls(sep), cls(right)
+
+ def rpartition(self, sep: str, /) -> tuple[te.Self, te.Self, te.Self]:
+ left, sep, right = super().rpartition(sep)
+ cls = self.__class__
+ return cls(left), cls(sep), cls(right)
+
+ def format(self, *args: t.Any, **kwargs: t.Any) -> te.Self:
+ formatter = EscapeFormatter(self.escape)
+ return self.__class__(formatter.vformat(self, args, kwargs))
+
+ def format_map(
+ self,
+ mapping: cabc.Mapping[str, t.Any], # type: ignore[override]
+ /,
+ ) -> te.Self:
+ formatter = EscapeFormatter(self.escape)
+ return self.__class__(formatter.vformat(self, (), mapping))
+
+ def __html_format__(self, format_spec: str, /) -> te.Self:
+ if format_spec:
+ raise ValueError("Unsupported format specification for Markup.")
+
+ return self
+
+
+class EscapeFormatter(string.Formatter):
+ __slots__ = ("escape",)
+
+ def __init__(self, escape: _TPEscape) -> None:
+ self.escape: _TPEscape = escape
+ super().__init__()
+
+ def format_field(self, value: t.Any, format_spec: str) -> str:
+ if hasattr(value, "__html_format__"):
+ rv = value.__html_format__(format_spec)
+ elif hasattr(value, "__html__"):
+ if format_spec:
+ raise ValueError(
+ f"Format specifier {format_spec} given, but {type(value)} does not"
+ " define __html_format__. A class that defines __html__ must define"
+ " __html_format__ to work with format specifiers."
+ )
+ rv = value.__html__()
+ else:
+ # We need to make sure the format spec is str here as
+ # otherwise the wrong callback methods are invoked.
+ rv = super().format_field(value, str(format_spec))
+ return str(self.escape(rv))
+
+
+class _MarkupEscapeHelper:
+ """Helper for :meth:`Markup.__mod__`."""
+
+ __slots__ = ("obj", "escape")
+
+ def __init__(self, obj: t.Any, escape: _TPEscape) -> None:
+ self.obj: t.Any = obj
+ self.escape: _TPEscape = escape
+
+ def __getitem__(self, key: t.Any, /) -> te.Self:
+ return self.__class__(self.obj[key], self.escape)
+
+ def __str__(self, /) -> str:
+ return str(self.escape(self.obj))
+
+ def __repr__(self, /) -> str:
+ return str(self.escape(repr(self.obj)))
+
+ def __int__(self, /) -> int:
+ return int(self.obj)
+
+ def __float__(self, /) -> float:
+ return float(self.obj)
+
+
+def __getattr__(name: str) -> t.Any:
+ if name == "__version__":
+ import importlib.metadata
+ import warnings
+
+ warnings.warn(
+ "The '__version__' attribute is deprecated and will be removed in"
+ " MarkupSafe 3.1. Use feature detection, or"
+ ' `importlib.metadata.version("markupsafe")`, instead.',
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return importlib.metadata.version("markupsafe")
+
+ raise AttributeError(name)
diff --git a/venv/Lib/site-packages/markupsafe/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/markupsafe/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..92d72d9
Binary files /dev/null and b/venv/Lib/site-packages/markupsafe/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/markupsafe/__pycache__/_native.cpython-310.pyc b/venv/Lib/site-packages/markupsafe/__pycache__/_native.cpython-310.pyc
new file mode 100644
index 0000000..2b4c20b
Binary files /dev/null and b/venv/Lib/site-packages/markupsafe/__pycache__/_native.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/markupsafe/_native.py b/venv/Lib/site-packages/markupsafe/_native.py
new file mode 100644
index 0000000..088b3bc
--- /dev/null
+++ b/venv/Lib/site-packages/markupsafe/_native.py
@@ -0,0 +1,8 @@
+def _escape_inner(s: str, /) -> str:
+ return (
+ s.replace("&", "&")
+ .replace(">", ">")
+ .replace("<", "<")
+ .replace("'", "'")
+ .replace('"', """)
+ )
diff --git a/venv/Lib/site-packages/markupsafe/_speedups.c b/venv/Lib/site-packages/markupsafe/_speedups.c
new file mode 100644
index 0000000..8a315f2
--- /dev/null
+++ b/venv/Lib/site-packages/markupsafe/_speedups.c
@@ -0,0 +1,200 @@
+#include
+
+#define GET_DELTA(inp, inp_end, delta) \
+ while (inp < inp_end) { \
+ switch (*inp++) { \
+ case '"': \
+ case '\'': \
+ case '&': \
+ delta += 4; \
+ break; \
+ case '<': \
+ case '>': \
+ delta += 3; \
+ break; \
+ } \
+ }
+
+#define DO_ESCAPE(inp, inp_end, outp) \
+ { \
+ Py_ssize_t ncopy = 0; \
+ while (inp < inp_end) { \
+ switch (*inp) { \
+ case '"': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = '#'; \
+ *outp++ = '3'; \
+ *outp++ = '4'; \
+ *outp++ = ';'; \
+ break; \
+ case '\'': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = '#'; \
+ *outp++ = '3'; \
+ *outp++ = '9'; \
+ *outp++ = ';'; \
+ break; \
+ case '&': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = 'a'; \
+ *outp++ = 'm'; \
+ *outp++ = 'p'; \
+ *outp++ = ';'; \
+ break; \
+ case '<': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = 'l'; \
+ *outp++ = 't'; \
+ *outp++ = ';'; \
+ break; \
+ case '>': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = 'g'; \
+ *outp++ = 't'; \
+ *outp++ = ';'; \
+ break; \
+ default: \
+ ncopy++; \
+ } \
+ inp++; \
+ } \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ }
+
+static PyObject*
+escape_unicode_kind1(PyUnicodeObject *in)
+{
+ Py_UCS1 *inp = PyUnicode_1BYTE_DATA(in);
+ Py_UCS1 *inp_end = inp + PyUnicode_GET_LENGTH(in);
+ Py_UCS1 *outp;
+ PyObject *out;
+ Py_ssize_t delta = 0;
+
+ GET_DELTA(inp, inp_end, delta);
+ if (!delta) {
+ Py_INCREF(in);
+ return (PyObject*)in;
+ }
+
+ out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta,
+ PyUnicode_IS_ASCII(in) ? 127 : 255);
+ if (!out)
+ return NULL;
+
+ inp = PyUnicode_1BYTE_DATA(in);
+ outp = PyUnicode_1BYTE_DATA(out);
+ DO_ESCAPE(inp, inp_end, outp);
+ return out;
+}
+
+static PyObject*
+escape_unicode_kind2(PyUnicodeObject *in)
+{
+ Py_UCS2 *inp = PyUnicode_2BYTE_DATA(in);
+ Py_UCS2 *inp_end = inp + PyUnicode_GET_LENGTH(in);
+ Py_UCS2 *outp;
+ PyObject *out;
+ Py_ssize_t delta = 0;
+
+ GET_DELTA(inp, inp_end, delta);
+ if (!delta) {
+ Py_INCREF(in);
+ return (PyObject*)in;
+ }
+
+ out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 65535);
+ if (!out)
+ return NULL;
+
+ inp = PyUnicode_2BYTE_DATA(in);
+ outp = PyUnicode_2BYTE_DATA(out);
+ DO_ESCAPE(inp, inp_end, outp);
+ return out;
+}
+
+
+static PyObject*
+escape_unicode_kind4(PyUnicodeObject *in)
+{
+ Py_UCS4 *inp = PyUnicode_4BYTE_DATA(in);
+ Py_UCS4 *inp_end = inp + PyUnicode_GET_LENGTH(in);
+ Py_UCS4 *outp;
+ PyObject *out;
+ Py_ssize_t delta = 0;
+
+ GET_DELTA(inp, inp_end, delta);
+ if (!delta) {
+ Py_INCREF(in);
+ return (PyObject*)in;
+ }
+
+ out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 1114111);
+ if (!out)
+ return NULL;
+
+ inp = PyUnicode_4BYTE_DATA(in);
+ outp = PyUnicode_4BYTE_DATA(out);
+ DO_ESCAPE(inp, inp_end, outp);
+ return out;
+}
+
+static PyObject*
+escape_unicode(PyObject *self, PyObject *s)
+{
+ if (!PyUnicode_Check(s))
+ return NULL;
+
+ // This check is no longer needed in Python 3.12.
+ if (PyUnicode_READY(s))
+ return NULL;
+
+ switch (PyUnicode_KIND(s)) {
+ case PyUnicode_1BYTE_KIND:
+ return escape_unicode_kind1((PyUnicodeObject*) s);
+ case PyUnicode_2BYTE_KIND:
+ return escape_unicode_kind2((PyUnicodeObject*) s);
+ case PyUnicode_4BYTE_KIND:
+ return escape_unicode_kind4((PyUnicodeObject*) s);
+ }
+ assert(0); /* shouldn't happen */
+ return NULL;
+}
+
+static PyMethodDef module_methods[] = {
+ {"_escape_inner", (PyCFunction)escape_unicode, METH_O, NULL},
+ {NULL, NULL, 0, NULL} /* Sentinel */
+};
+
+static PyModuleDef_Slot module_slots[] = {
+#ifdef Py_mod_multiple_interpreters // Python 3.12+
+ {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
+#endif
+#ifdef Py_mod_gil // Python 3.13+
+ {Py_mod_gil, Py_MOD_GIL_NOT_USED},
+#endif
+ {0, NULL} /* Sentinel */
+};
+
+static struct PyModuleDef module_definition = {
+ .m_base = PyModuleDef_HEAD_INIT,
+ .m_name = "markupsafe._speedups",
+ .m_size = 0,
+ .m_methods = module_methods,
+ .m_slots = module_slots,
+};
+
+PyMODINIT_FUNC
+PyInit__speedups(void)
+{
+ return PyModuleDef_Init(&module_definition);
+}
diff --git a/venv/Lib/site-packages/markupsafe/_speedups.cp310-win_amd64.pyd b/venv/Lib/site-packages/markupsafe/_speedups.cp310-win_amd64.pyd
new file mode 100644
index 0000000..0ef63db
Binary files /dev/null and b/venv/Lib/site-packages/markupsafe/_speedups.cp310-win_amd64.pyd differ
diff --git a/venv/Lib/site-packages/markupsafe/_speedups.pyi b/venv/Lib/site-packages/markupsafe/_speedups.pyi
new file mode 100644
index 0000000..8c88858
--- /dev/null
+++ b/venv/Lib/site-packages/markupsafe/_speedups.pyi
@@ -0,0 +1 @@
+def _escape_inner(s: str, /) -> str: ...
diff --git a/venv/Lib/site-packages/markupsafe/py.typed b/venv/Lib/site-packages/markupsafe/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/INSTALLER b/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/METADATA b/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/METADATA
new file mode 100644
index 0000000..4974de1
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/METADATA
@@ -0,0 +1,109 @@
+Metadata-Version: 2.4
+Name: Werkzeug
+Version: 3.1.8
+Summary: The comprehensive WSGI web application library.
+Maintainer-email: Pallets
+Requires-Python: >=3.9
+Description-Content-Type: text/markdown
+License-Expression: BSD-3-Clause
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
+Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
+Classifier: Typing :: Typed
+License-File: LICENSE.txt
+Requires-Dist: markupsafe>=2.1.1
+Requires-Dist: watchdog>=2.3 ; extra == "watchdog"
+Project-URL: Changes, https://werkzeug.palletsprojects.com/page/changes/
+Project-URL: Chat, https://discord.gg/pallets
+Project-URL: Documentation, https://werkzeug.palletsprojects.com/
+Project-URL: Donate, https://palletsprojects.com/donate
+Project-URL: Source, https://github.com/pallets/werkzeug/
+Provides-Extra: watchdog
+
+
+
+# Werkzeug
+
+*werkzeug* German noun: "tool". Etymology: *werk* ("work"), *zeug* ("stuff")
+
+Werkzeug is a comprehensive [WSGI][] web application library. It began as
+a simple collection of various utilities for WSGI applications and has
+become one of the most advanced WSGI utility libraries.
+
+It includes:
+
+- An interactive debugger that allows inspecting stack traces and
+ source code in the browser with an interactive interpreter for any
+ frame in the stack.
+- A full-featured request object with objects to interact with
+ headers, query args, form data, files, and cookies.
+- A response object that can wrap other WSGI applications and handle
+ streaming data.
+- A routing system for matching URLs to endpoints and generating URLs
+ for endpoints, with an extensible system for capturing variables
+ from URLs.
+- HTTP utilities to handle entity tags, cache control, dates, user
+ agents, cookies, files, and more.
+- A threaded WSGI server for use while developing applications
+ locally.
+- A test client for simulating HTTP requests during testing without
+ requiring running a server.
+
+Werkzeug doesn't enforce any dependencies. It is up to the developer to
+choose a template engine, database adapter, and even how to handle
+requests. It can be used to build all sorts of end user applications
+such as blogs, wikis, or bulletin boards.
+
+[Flask][] wraps Werkzeug, using it to handle the details of WSGI while
+providing more structure and patterns for defining powerful
+applications.
+
+[WSGI]: https://wsgi.readthedocs.io/en/latest/
+[Flask]: https://www.palletsprojects.com/p/flask/
+
+
+## A Simple Example
+
+```python
+# save this as app.py
+from werkzeug.wrappers import Request, Response
+
+@Request.application
+def application(request: Request) -> Response:
+ return Response("Hello, World!")
+
+if __name__ == "__main__":
+ from werkzeug.serving import run_simple
+ run_simple("127.0.0.1", 5000, application)
+```
+
+```
+$ python -m app
+ * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
+```
+
+
+## Donate
+
+The Pallets organization develops and supports Werkzeug and other
+popular packages. In order to grow the community of contributors and
+users, and allow the maintainers to devote more time to the projects,
+[please donate today][].
+
+[please donate today]: https://palletsprojects.com/donate
+
+## Contributing
+
+See our [detailed contributing documentation][contrib] for many ways to
+contribute, including reporting issues, requesting features, asking or answering
+questions, and making PRs.
+
+[contrib]: https://palletsprojects.com/contributing/
+
diff --git a/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/RECORD b/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/RECORD
new file mode 100644
index 0000000..a74375d
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/RECORD
@@ -0,0 +1,116 @@
+werkzeug-3.1.8.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+werkzeug-3.1.8.dist-info/METADATA,sha256=9X0phk-wTGy_K06ETBCNYjMfRiH9Fjy87SiPkN_W05I,4029
+werkzeug-3.1.8.dist-info/RECORD,,
+werkzeug-3.1.8.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
+werkzeug-3.1.8.dist-info/licenses/LICENSE.txt,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475
+werkzeug/__init__.py,sha256=CejNWfCZKDaJ_FmshZFw8dFgjNAlu6q15ec7DwWJlM8,165
+werkzeug/__pycache__/__init__.cpython-310.pyc,,
+werkzeug/__pycache__/_internal.cpython-310.pyc,,
+werkzeug/__pycache__/_reloader.cpython-310.pyc,,
+werkzeug/__pycache__/exceptions.cpython-310.pyc,,
+werkzeug/__pycache__/formparser.cpython-310.pyc,,
+werkzeug/__pycache__/http.cpython-310.pyc,,
+werkzeug/__pycache__/local.cpython-310.pyc,,
+werkzeug/__pycache__/security.cpython-310.pyc,,
+werkzeug/__pycache__/serving.cpython-310.pyc,,
+werkzeug/__pycache__/test.cpython-310.pyc,,
+werkzeug/__pycache__/testapp.cpython-310.pyc,,
+werkzeug/__pycache__/urls.cpython-310.pyc,,
+werkzeug/__pycache__/user_agent.cpython-310.pyc,,
+werkzeug/__pycache__/utils.cpython-310.pyc,,
+werkzeug/__pycache__/wsgi.cpython-310.pyc,,
+werkzeug/_internal.py,sha256=QRynfUEOngEAmm4qzhlufbhk1K-n901Q1KQWKznsaNU,5547
+werkzeug/_reloader.py,sha256=IppO3kX_GxQwaV6h8SPWe0lSTSw6kd_HJXqLiUhR7AU,15100
+werkzeug/datastructures/__init__.py,sha256=0Vt8Lt8KUYSoAe8ADt1jIE-w3Ib9tkrk0HEhBOktqUw,2615
+werkzeug/datastructures/__pycache__/__init__.cpython-310.pyc,,
+werkzeug/datastructures/__pycache__/accept.cpython-310.pyc,,
+werkzeug/datastructures/__pycache__/auth.cpython-310.pyc,,
+werkzeug/datastructures/__pycache__/cache_control.cpython-310.pyc,,
+werkzeug/datastructures/__pycache__/csp.cpython-310.pyc,,
+werkzeug/datastructures/__pycache__/etag.cpython-310.pyc,,
+werkzeug/datastructures/__pycache__/file_storage.cpython-310.pyc,,
+werkzeug/datastructures/__pycache__/headers.cpython-310.pyc,,
+werkzeug/datastructures/__pycache__/mixins.cpython-310.pyc,,
+werkzeug/datastructures/__pycache__/range.cpython-310.pyc,,
+werkzeug/datastructures/__pycache__/structures.cpython-310.pyc,,
+werkzeug/datastructures/accept.py,sha256=xoDD4qwZeTjew_1Ze0gyyKWkbhtowwR5tHTWnobPeGI,12177
+werkzeug/datastructures/auth.py,sha256=iAhDYwIBjE6LDJw_iceE-GlJlLUvJRCMKtokzVKP1J4,10125
+werkzeug/datastructures/cache_control.py,sha256=omtSmmAh3KgQRJYzaW05LtEt5HoJlLkJTxjil-Q3cuM,9711
+werkzeug/datastructures/csp.py,sha256=jWl1dxhkbc6_eE9qGnTGBJSimWNQljKKYwmlys4dFOA,3816
+werkzeug/datastructures/etag.py,sha256=N2dFqhFsLR0k5hgjol3QTGpcSltJhpgLWeyz6Va80G8,3278
+werkzeug/datastructures/file_storage.py,sha256=J57paAQGxBnq2yjY0yfsEuN1wPRkDXLkWH3wk_pQvTs,6715
+werkzeug/datastructures/headers.py,sha256=J3WQKnQGmt9hUDn-d_YI62PtrjW7U7sJTDNTFtjWgRA,21540
+werkzeug/datastructures/mixins.py,sha256=h8K_A0Sw47s2cO2ZNFf60ENbmVDsi3rgHr_f77VFwJE,9039
+werkzeug/datastructures/range.py,sha256=KAWLRDyL8bzSBDm_OirxwA17xkFM_vdedZINmyDfNUI,7034
+werkzeug/datastructures/structures.py,sha256=dQ3n8KP5UHyad1TMsWmQBK3K9pNPByK5Nd0AV09oOAo,41385
+werkzeug/debug/__init__.py,sha256=8QoMJ1iGA7NGp3wxw6Sa3fPJHlrCggrRH30bQg9ei2o,20319
+werkzeug/debug/__pycache__/__init__.cpython-310.pyc,,
+werkzeug/debug/__pycache__/console.cpython-310.pyc,,
+werkzeug/debug/__pycache__/repr.cpython-310.pyc,,
+werkzeug/debug/__pycache__/tbtools.cpython-310.pyc,,
+werkzeug/debug/console.py,sha256=t4hZ0Qg1p6Uu2MWimqoMDi7S3WYZvLMjnc8v_dPaxAo,6089
+werkzeug/debug/repr.py,sha256=iHMYny8whiiMDasvUqj0nm4-1VHVvwe697KleiZVK1s,9303
+werkzeug/debug/shared/ICON_LICENSE.md,sha256=DhA6Y1gUl5Jwfg0NFN9Rj4VWITt8tUx0IvdGf0ux9-s,222
+werkzeug/debug/shared/console.png,sha256=bxax6RXXlvOij_KeqvSNX0ojJf83YbnZ7my-3Gx9w2A,507
+werkzeug/debug/shared/debugger.js,sha256=SRL9YZ9FTVngaYD-INQNincEVdZ-kBHa_-VJx0U7-rg,10068
+werkzeug/debug/shared/less.png,sha256=-4-kNRaXJSONVLahrQKUxMwXGm9R4OnZ9SxDGpHlIR4,191
+werkzeug/debug/shared/more.png,sha256=GngN7CioHQoV58rH6ojnkYi8c_qED2Aka5FO5UXrReY,200
+werkzeug/debug/shared/style.css,sha256=-xSxzUEZGw_IqlDR5iZxitNl8LQUjBM-_Y4UAvXVH8g,6078
+werkzeug/debug/tbtools.py,sha256=L_CD1ha5WmyWrKVZ3zxovJ4gI1CATsc4Yc1Zms41EvE,13645
+werkzeug/exceptions.py,sha256=7XNgddMV3UQR6UPr7lWlzSFxO8Ur8bSJgLdvJuauEX0,26888
+werkzeug/formparser.py,sha256=v0-F9OcmNHuPLqXePW_b0asWzpH-Z_ZlGj8ao0CMmuo,15847
+werkzeug/http.py,sha256=4HIKX--fVLTUePdvH4jsDmuiUZc38XziDPBghUJFb0o,45732
+werkzeug/local.py,sha256=o4Zz1ZAuNRBn8r-laGIxCdKXjxx1i0uJq3eNyKPy4cg,22184
+werkzeug/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+werkzeug/middleware/__pycache__/__init__.cpython-310.pyc,,
+werkzeug/middleware/__pycache__/dispatcher.cpython-310.pyc,,
+werkzeug/middleware/__pycache__/http_proxy.cpython-310.pyc,,
+werkzeug/middleware/__pycache__/lint.cpython-310.pyc,,
+werkzeug/middleware/__pycache__/profiler.cpython-310.pyc,,
+werkzeug/middleware/__pycache__/proxy_fix.cpython-310.pyc,,
+werkzeug/middleware/__pycache__/shared_data.cpython-310.pyc,,
+werkzeug/middleware/dispatcher.py,sha256=zWN5_lqJr_sc9UDv-PPoSlDHN_zR33z6B74F_4Cxpo8,2602
+werkzeug/middleware/http_proxy.py,sha256=sdk-V6GoZ6aMny-D0QNKNf5MWD2OTO3AGbBg6upp4Hc,7834
+werkzeug/middleware/lint.py,sha256=jVdyljRxpkz18MiMR8N7YdiElna8DJVvO4CCjZsnSQI,14476
+werkzeug/middleware/profiler.py,sha256=1ZAHlDeYNdhgp8THOXkV5lgmcLl307phAr2Ufy30-lY,5562
+werkzeug/middleware/proxy_fix.py,sha256=n-HW-MRWJquCIhmqiZKoGdbbEeHuWJqPRHhFpuj4pzY,6755
+werkzeug/middleware/shared_data.py,sha256=gsjmgeEFLzDdKsgd19pMhI7BfU_TbL6AyEyZPG2ykfQ,9542
+werkzeug/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+werkzeug/routing/__init__.py,sha256=d8TRxsk24IWu2BdoOYUfL--deolHwiGVCBJqLoEe3YM,4820
+werkzeug/routing/__pycache__/__init__.cpython-310.pyc,,
+werkzeug/routing/__pycache__/converters.cpython-310.pyc,,
+werkzeug/routing/__pycache__/exceptions.cpython-310.pyc,,
+werkzeug/routing/__pycache__/map.cpython-310.pyc,,
+werkzeug/routing/__pycache__/matcher.cpython-310.pyc,,
+werkzeug/routing/__pycache__/rules.cpython-310.pyc,,
+werkzeug/routing/converters.py,sha256=iqpee_mAjr1oGbq0etujYF9PiDv5U7DgNkARHXnMId0,7297
+werkzeug/routing/exceptions.py,sha256=wNBiUmUk4OtFOpbdDSr7KKKUjH7yn84JqwBicUup8p8,4846
+werkzeug/routing/map.py,sha256=a7PSRAELyNyoPFP_uhyNDAsmPjEbrcyewPnEZIl-0ZI,35863
+werkzeug/routing/matcher.py,sha256=Q_EazWPuLuQjJfd1vYO9AkCtLzx04bGj2YUJqULAfs0,7848
+werkzeug/routing/rules.py,sha256=AkozsbaVGcSEh1O_9FI_ZLWb8BNOjdzrvEFu-qa8VKQ,32536
+werkzeug/sansio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+werkzeug/sansio/__pycache__/__init__.cpython-310.pyc,,
+werkzeug/sansio/__pycache__/http.cpython-310.pyc,,
+werkzeug/sansio/__pycache__/multipart.cpython-310.pyc,,
+werkzeug/sansio/__pycache__/request.cpython-310.pyc,,
+werkzeug/sansio/__pycache__/response.cpython-310.pyc,,
+werkzeug/sansio/__pycache__/utils.cpython-310.pyc,,
+werkzeug/sansio/http.py,sha256=l-N1ktc7WVgfDA9t1MhyeVFNJGvnRTC8mJViZDQYVGA,5351
+werkzeug/sansio/multipart.py,sha256=gqzwp6YgyE_DepTlnoiuFN743Q3WPa6p7ki1xn5ixlo,11963
+werkzeug/sansio/request.py,sha256=-t8CJrvjWPMu2DCQ4W0ltCHQ4nOWg_ztXJLkyN6-Q_U,19891
+werkzeug/sansio/response.py,sha256=159j3TC2AbDvqppcegVmpLftot70UWtm5qZO0kqQpFk,27930
+werkzeug/sansio/utils.py,sha256=ZUkFKNtMDhTRQf9bkyMzoGOQAntyaTTvHB_6en2grgg,7477
+werkzeug/security.py,sha256=WHb2oiwyOie5XI8WNnKYd4YVjSwBhGf3nPyKBSGGlI0,6654
+werkzeug/serving.py,sha256=mcc9Vfn1dMXzvm87PE5hUtyY6dQB0e98lcxqDkpWiz8,39859
+werkzeug/test.py,sha256=Xf8bCYkjKJLtTXWTvw4E5AlYrK7CYjnmIolxSHkkKsc,52779
+werkzeug/testapp.py,sha256=5_IS5Dh_WfWfNcTLmbydj01lomgcKA_4l9PPCNZnmdI,6332
+werkzeug/urls.py,sha256=XyNKwHvK5IC37-wuIDMYWkiCJ3yLTLGv7wn2GF3ndqI,6430
+werkzeug/user_agent.py,sha256=lSlLYKCcbzCUSkbdAoO8zPk2UR-8Mdn6iu_iA2kYPBA,1416
+werkzeug/utils.py,sha256=1BirLZmTvJP9v6dxjMXpSrKCsfhqzaJPzUHC7dl5MYs,24615
+werkzeug/wrappers/__init__.py,sha256=b78jCM8x96kJUGLZ5FYFR3zlK-3pnFAmP9RJIGU0ses,138
+werkzeug/wrappers/__pycache__/__init__.cpython-310.pyc,,
+werkzeug/wrappers/__pycache__/request.cpython-310.pyc,,
+werkzeug/wrappers/__pycache__/response.cpython-310.pyc,,
+werkzeug/wrappers/request.py,sha256=f9Jpe9zqs_v0n-DTZj7FfMyU_y3LhBBBX4YCYsrqGUs,24730
+werkzeug/wrappers/response.py,sha256=y0OOpH58zGngrrzAdG50JPyiRW4JxZeEXLoYuBu1Qo0,32707
+werkzeug/wsgi.py,sha256=DvYEIcatqemE4qtcv33D9T-yvjAiTgri65Ol-ejmrbI,21676
diff --git a/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/WHEEL b/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/WHEEL
new file mode 100644
index 0000000..d8b9936
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 1.0
+Generator: flit 3.12.0
+Root-Is-Purelib: true
+Tag: py3-none-any
diff --git a/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/licenses/LICENSE.txt b/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/licenses/LICENSE.txt
new file mode 100644
index 0000000..c37cae4
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug-3.1.8.dist-info/licenses/LICENSE.txt
@@ -0,0 +1,28 @@
+Copyright 2007 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/venv/Lib/site-packages/werkzeug/__init__.py b/venv/Lib/site-packages/werkzeug/__init__.py
new file mode 100644
index 0000000..0b248fd
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/__init__.py
@@ -0,0 +1,4 @@
+from .serving import run_simple as run_simple
+from .test import Client as Client
+from .wrappers import Request as Request
+from .wrappers import Response as Response
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..71bbcbb
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/_internal.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/_internal.cpython-310.pyc
new file mode 100644
index 0000000..a5c2337
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/_internal.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/_reloader.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/_reloader.cpython-310.pyc
new file mode 100644
index 0000000..ae4860e
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/_reloader.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/exceptions.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/exceptions.cpython-310.pyc
new file mode 100644
index 0000000..b3c2ae3
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/exceptions.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/formparser.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/formparser.cpython-310.pyc
new file mode 100644
index 0000000..b0d01ba
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/formparser.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/http.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/http.cpython-310.pyc
new file mode 100644
index 0000000..af86c75
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/http.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/local.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/local.cpython-310.pyc
new file mode 100644
index 0000000..97f5e80
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/local.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/security.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/security.cpython-310.pyc
new file mode 100644
index 0000000..4ce8321
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/security.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/serving.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/serving.cpython-310.pyc
new file mode 100644
index 0000000..92bad85
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/serving.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/test.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/test.cpython-310.pyc
new file mode 100644
index 0000000..43195ce
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/test.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/testapp.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/testapp.cpython-310.pyc
new file mode 100644
index 0000000..52c030e
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/testapp.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/urls.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/urls.cpython-310.pyc
new file mode 100644
index 0000000..19e04c1
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/urls.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/user_agent.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/user_agent.cpython-310.pyc
new file mode 100644
index 0000000..745dcd2
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/user_agent.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/utils.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/utils.cpython-310.pyc
new file mode 100644
index 0000000..5e83092
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/utils.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/__pycache__/wsgi.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/__pycache__/wsgi.cpython-310.pyc
new file mode 100644
index 0000000..0335760
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/__pycache__/wsgi.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/_internal.py b/venv/Lib/site-packages/werkzeug/_internal.py
new file mode 100644
index 0000000..75e4335
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/_internal.py
@@ -0,0 +1,211 @@
+from __future__ import annotations
+
+import logging
+import re
+import sys
+import typing as t
+from datetime import datetime
+from datetime import timezone
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import WSGIEnvironment
+
+ from .wrappers.request import Request
+
+_logger: logging.Logger | None = None
+
+
+class _Missing:
+ def __repr__(self) -> str:
+ return "no value"
+
+ def __reduce__(self) -> str:
+ return "_missing"
+
+
+_missing = _Missing()
+
+
+def _wsgi_decoding_dance(s: str) -> str:
+ return s.encode("latin1").decode(errors="replace")
+
+
+def _wsgi_encoding_dance(s: str) -> str:
+ return s.encode().decode("latin1")
+
+
+def _get_environ(obj: WSGIEnvironment | Request) -> WSGIEnvironment:
+ env = getattr(obj, "environ", obj)
+ assert isinstance(env, dict), (
+ f"{type(obj).__name__!r} is not a WSGI environment (has to be a dict)"
+ )
+ return env
+
+
+def _has_level_handler(logger: logging.Logger) -> bool:
+ """Check if there is a handler in the logging chain that will handle
+ the given logger's effective level.
+ """
+ level = logger.getEffectiveLevel()
+ current = logger
+
+ while current:
+ if any(handler.level <= level for handler in current.handlers):
+ return True
+
+ if not current.propagate:
+ break
+
+ current = current.parent # type: ignore
+
+ return False
+
+
+class _ColorStreamHandler(logging.StreamHandler): # type: ignore[type-arg]
+ """On Windows, wrap stream with Colorama for ANSI style support."""
+
+ def __init__(self) -> None:
+ try:
+ import colorama
+ except ImportError:
+ stream = None
+ else:
+ stream = colorama.AnsiToWin32(sys.stderr)
+
+ super().__init__(stream)
+
+
+def _log(type: str, message: str, *args: t.Any, **kwargs: t.Any) -> None:
+ """Log a message to the 'werkzeug' logger.
+
+ The logger is created the first time it is needed. If there is no
+ level set, it is set to :data:`logging.INFO`. If there is no handler
+ for the logger's effective level, a :class:`logging.StreamHandler`
+ is added.
+ """
+ global _logger
+
+ if _logger is None:
+ _logger = logging.getLogger("werkzeug")
+
+ if _logger.level == logging.NOTSET:
+ _logger.setLevel(logging.INFO)
+
+ if not _has_level_handler(_logger):
+ _logger.addHandler(_ColorStreamHandler())
+
+ getattr(_logger, type)(message.rstrip(), *args, **kwargs)
+
+
+@t.overload
+def _dt_as_utc(dt: None) -> None: ...
+
+
+@t.overload
+def _dt_as_utc(dt: datetime) -> datetime: ...
+
+
+def _dt_as_utc(dt: datetime | None) -> datetime | None:
+ if dt is None:
+ return dt
+
+ if dt.tzinfo is None:
+ return dt.replace(tzinfo=timezone.utc)
+ elif dt.tzinfo != timezone.utc:
+ return dt.astimezone(timezone.utc)
+
+ return dt
+
+
+_TAccessorValue = t.TypeVar("_TAccessorValue")
+
+
+class _DictAccessorProperty(t.Generic[_TAccessorValue]):
+ """Baseclass for `environ_property` and `header_property`."""
+
+ read_only = False
+
+ def __init__(
+ self,
+ name: str,
+ default: _TAccessorValue | None = None,
+ load_func: t.Callable[[str], _TAccessorValue] | None = None,
+ dump_func: t.Callable[[_TAccessorValue], str] | None = None,
+ read_only: bool | None = None,
+ doc: str | None = None,
+ ) -> None:
+ self.name = name
+ self.default = default
+ self.load_func = load_func
+ self.dump_func = dump_func
+ if read_only is not None:
+ self.read_only = read_only
+ self.__doc__ = doc
+
+ def lookup(self, instance: t.Any) -> t.MutableMapping[str, t.Any]:
+ raise NotImplementedError
+
+ @t.overload
+ def __get__(
+ self, instance: None, owner: type
+ ) -> _DictAccessorProperty[_TAccessorValue]: ...
+
+ @t.overload
+ def __get__(self, instance: t.Any, owner: type) -> _TAccessorValue: ...
+
+ def __get__(
+ self, instance: t.Any | None, owner: type
+ ) -> _TAccessorValue | _DictAccessorProperty[_TAccessorValue]:
+ if instance is None:
+ return self
+
+ storage = self.lookup(instance)
+
+ if self.name not in storage:
+ return self.default # type: ignore
+
+ value = storage[self.name]
+
+ if self.load_func is not None:
+ try:
+ return self.load_func(value)
+ except (ValueError, TypeError):
+ return self.default # type: ignore
+
+ return value # type: ignore
+
+ def __set__(self, instance: t.Any, value: _TAccessorValue) -> None:
+ if self.read_only:
+ raise AttributeError("read only property")
+
+ if self.dump_func is not None:
+ self.lookup(instance)[self.name] = self.dump_func(value)
+ else:
+ self.lookup(instance)[self.name] = value
+
+ def __delete__(self, instance: t.Any) -> None:
+ if self.read_only:
+ raise AttributeError("read only property")
+
+ self.lookup(instance).pop(self.name, None)
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} {self.name}>"
+
+
+_plain_int_re = re.compile(r"-?\d+", re.ASCII)
+
+
+def _plain_int(value: str) -> int:
+ """Parse an int only if it is only ASCII digits and ``-``.
+
+ This disallows ``+``, ``_``, and non-ASCII digits, which are accepted by ``int`` but
+ are not allowed in HTTP header values.
+
+ Any leading or trailing whitespace is stripped
+ """
+ value = value.strip()
+ if _plain_int_re.fullmatch(value) is None:
+ raise ValueError
+
+ return int(value)
diff --git a/venv/Lib/site-packages/werkzeug/_reloader.py b/venv/Lib/site-packages/werkzeug/_reloader.py
new file mode 100644
index 0000000..5eb64bc
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/_reloader.py
@@ -0,0 +1,465 @@
+from __future__ import annotations
+
+import fnmatch
+import os
+import subprocess
+import sys
+import threading
+import time
+import typing as t
+from itertools import chain
+from pathlib import PurePath
+
+from ._internal import _log
+
+# The various system prefixes where imports are found. Base values are
+# different when running in a virtualenv. All reloaders will ignore the
+# base paths (usually the system installation). The stat reloader won't
+# scan the virtualenv paths, it will only include modules that are
+# already imported.
+_ignore_always = tuple({sys.base_prefix, sys.base_exec_prefix})
+prefix = {*_ignore_always, sys.prefix, sys.exec_prefix}
+
+if hasattr(sys, "real_prefix"):
+ # virtualenv < 20
+ prefix.add(sys.real_prefix)
+
+_stat_ignore_scan = tuple(prefix)
+del prefix
+# Ignore __pycache__ since a change there will always have a change to
+# the source file (or initial pyc file) as well. Ignore common version control
+# internals. Ignore common tool caches.
+_ignore_common_dirs = {
+ "__pycache__",
+ ".git",
+ ".hg",
+ ".tox",
+ ".nox",
+ ".pytest_cache",
+ ".mypy_cache",
+}
+
+
+def _iter_module_paths() -> t.Iterator[str]:
+ """Find the filesystem paths associated with imported modules."""
+ # List is in case the value is modified by the app while updating.
+ for module in list(sys.modules.values()):
+ name = getattr(module, "__file__", None)
+
+ if name is None or name.startswith(_ignore_always):
+ continue
+
+ while not os.path.isfile(name):
+ # Zip file, find the base file without the module path.
+ old = name
+ name = os.path.dirname(name)
+
+ if name == old: # skip if it was all directories somehow
+ break
+ else:
+ yield name
+
+
+def _remove_by_pattern(paths: set[str], exclude_patterns: set[str]) -> None:
+ for pattern in exclude_patterns:
+ paths.difference_update(fnmatch.filter(paths, pattern))
+
+
+def _find_stat_paths(
+ extra_files: set[str], exclude_patterns: set[str]
+) -> t.Iterable[str]:
+ """Find paths for the stat reloader to watch. Returns imported
+ module files, Python files under non-system paths. Extra files and
+ Python files under extra directories can also be scanned.
+
+ System paths have to be excluded for efficiency. Non-system paths,
+ such as a project root or ``sys.path.insert``, should be the paths
+ of interest to the user anyway.
+ """
+ paths = set()
+
+ for path in chain(list(sys.path), extra_files):
+ path = os.path.abspath(path)
+
+ if os.path.isfile(path):
+ # zip file on sys.path, or extra file
+ paths.add(path)
+ continue
+
+ parent_has_py = {os.path.dirname(path): True}
+
+ for root, dirs, files in os.walk(path):
+ if (
+ root.startswith(_stat_ignore_scan)
+ or os.path.basename(root) in _ignore_common_dirs
+ ):
+ dirs.clear()
+ continue
+
+ has_py = False
+
+ for name in files:
+ if name.endswith((".py", ".pyc")):
+ has_py = True
+ paths.add(os.path.join(root, name))
+
+ # Optimization: stop scanning a directory if neither it nor
+ # its parent contained Python files.
+ if not (has_py or parent_has_py[os.path.dirname(root)]):
+ dirs.clear()
+ continue
+
+ parent_has_py[root] = has_py
+
+ paths.update(_iter_module_paths())
+ _remove_by_pattern(paths, exclude_patterns)
+ return paths
+
+
+def _find_watchdog_paths(
+ extra_files: set[str], exclude_patterns: set[str]
+) -> t.Iterable[str]:
+ """Find paths for the stat reloader to watch. Looks at the same
+ sources as the stat reloader, but watches everything under
+ directories instead of individual files.
+ """
+ dirs = set()
+
+ for name in chain(list(sys.path), extra_files):
+ name = os.path.abspath(name)
+
+ if os.path.isfile(name):
+ name = os.path.dirname(name)
+
+ dirs.add(name)
+
+ for name in _iter_module_paths():
+ dirs.add(os.path.dirname(name))
+
+ _remove_by_pattern(dirs, exclude_patterns)
+ return _find_common_roots(dirs)
+
+
+def _find_common_roots(paths: t.Iterable[str]) -> t.Iterable[str]:
+ root: dict[str, dict[str, t.Any]] = {}
+
+ for chunks in sorted((PurePath(x).parts for x in paths), key=len, reverse=True):
+ node = root
+
+ for chunk in chunks:
+ node = node.setdefault(chunk, {})
+
+ node.clear()
+
+ rv = set()
+
+ def _walk(node: t.Mapping[str, dict[str, t.Any]], path: tuple[str, ...]) -> None:
+ for prefix, child in node.items():
+ _walk(child, path + (prefix,))
+
+ # If there are no more nodes, and a path has been accumulated, add it.
+ # Path may be empty if the "" entry is in sys.path.
+ if not node and path:
+ rv.add(os.path.join(*path))
+
+ _walk(root, ())
+ return rv
+
+
+def _get_args_for_reloading() -> list[str]:
+ """Determine how the script was executed, and return the args needed
+ to execute it again in a new process.
+ """
+ if sys.version_info >= (3, 10):
+ # sys.orig_argv, added in Python 3.10, contains the exact args used to invoke
+ # Python. Still replace argv[0] with sys.executable for accuracy.
+ return [sys.executable, *sys.orig_argv[1:]]
+
+ rv = [sys.executable]
+ py_script = sys.argv[0]
+ args = sys.argv[1:]
+ # Need to look at main module to determine how it was executed.
+ __main__ = sys.modules["__main__"]
+
+ # The value of __package__ indicates how Python was called. It may
+ # not exist if a setuptools script is installed as an egg. It may be
+ # set incorrectly for entry points created with pip on Windows.
+ if getattr(__main__, "__package__", None) is None or (
+ os.name == "nt"
+ and __main__.__package__ == ""
+ and not os.path.exists(py_script)
+ and os.path.exists(f"{py_script}.exe")
+ ):
+ # Executed a file, like "python app.py".
+ py_script = os.path.abspath(py_script)
+
+ if os.name == "nt":
+ # Windows entry points have ".exe" extension and should be
+ # called directly.
+ if not os.path.exists(py_script) and os.path.exists(f"{py_script}.exe"):
+ py_script += ".exe"
+
+ if (
+ os.path.splitext(sys.executable)[1] == ".exe"
+ and os.path.splitext(py_script)[1] == ".exe"
+ ):
+ rv.pop(0)
+
+ rv.append(py_script)
+ else:
+ # Executed a module, like "python -m werkzeug.serving".
+ if os.path.isfile(py_script):
+ # Rewritten by Python from "-m script" to "/path/to/script.py".
+ py_module = t.cast(str, __main__.__package__)
+ name = os.path.splitext(os.path.basename(py_script))[0]
+
+ if name != "__main__":
+ py_module += f".{name}"
+ else:
+ # Incorrectly rewritten by pydevd debugger from "-m script" to "script".
+ py_module = py_script
+
+ rv.extend(("-m", py_module.lstrip(".")))
+
+ rv.extend(args)
+ return rv
+
+
+class ReloaderLoop:
+ name = ""
+
+ def __init__(
+ self,
+ extra_files: t.Iterable[str] | None = None,
+ exclude_patterns: t.Iterable[str] | None = None,
+ interval: int | float = 1,
+ ) -> None:
+ self.extra_files: set[str] = {os.path.abspath(x) for x in extra_files or ()}
+ self.exclude_patterns: set[str] = set(exclude_patterns or ())
+ self.interval = interval
+
+ def __enter__(self) -> ReloaderLoop:
+ """Do any setup, then run one step of the watch to populate the
+ initial filesystem state.
+ """
+ self.run_step()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore
+ """Clean up any resources associated with the reloader."""
+ pass
+
+ def run(self) -> None:
+ """Continually run the watch step, sleeping for the configured
+ interval after each step.
+ """
+ while True:
+ self.run_step()
+ time.sleep(self.interval)
+
+ def run_step(self) -> None:
+ """Run one step for watching the filesystem. Called once to set
+ up initial state, then repeatedly to update it.
+ """
+ pass
+
+ def restart_with_reloader(self) -> int:
+ """Spawn a new Python interpreter with the same arguments as the
+ current one, but running the reloader thread.
+ """
+ while True:
+ _log("info", f" * Restarting with {self.name}")
+ args = _get_args_for_reloading()
+ new_environ = os.environ.copy()
+ new_environ["WERKZEUG_RUN_MAIN"] = "true"
+ exit_code = subprocess.call(args, env=new_environ, close_fds=False)
+
+ if exit_code != 3:
+ return exit_code
+
+ def trigger_reload(self, filename: str) -> None:
+ self.log_reload(filename)
+ sys.exit(3)
+
+ def log_reload(self, filename: str | bytes) -> None:
+ filename = os.path.abspath(filename)
+ _log("info", f" * Detected change in {filename!r}, reloading")
+
+
+class StatReloaderLoop(ReloaderLoop):
+ name = "stat"
+
+ def __enter__(self) -> ReloaderLoop:
+ self.mtimes: dict[str, float] = {}
+ return super().__enter__()
+
+ def run_step(self) -> None:
+ for name in _find_stat_paths(self.extra_files, self.exclude_patterns):
+ try:
+ mtime = os.stat(name).st_mtime
+ except OSError:
+ continue
+
+ old_time = self.mtimes.get(name)
+
+ if old_time is None:
+ self.mtimes[name] = mtime
+ continue
+
+ if mtime > old_time:
+ self.trigger_reload(name)
+
+
+class WatchdogReloaderLoop(ReloaderLoop):
+ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
+ from watchdog.events import EVENT_TYPE_CLOSED
+ from watchdog.events import EVENT_TYPE_CREATED
+ from watchdog.events import EVENT_TYPE_DELETED
+ from watchdog.events import EVENT_TYPE_MODIFIED
+ from watchdog.events import EVENT_TYPE_MOVED
+ from watchdog.events import FileModifiedEvent
+ from watchdog.events import PatternMatchingEventHandler
+ from watchdog.observers import Observer
+
+ super().__init__(*args, **kwargs)
+ trigger_reload = self.trigger_reload
+
+ class EventHandler(PatternMatchingEventHandler):
+ def on_any_event(self, event: FileModifiedEvent) -> None: # type: ignore[override]
+ if event.event_type not in {
+ EVENT_TYPE_CLOSED,
+ EVENT_TYPE_CREATED,
+ EVENT_TYPE_DELETED,
+ EVENT_TYPE_MODIFIED,
+ EVENT_TYPE_MOVED,
+ }:
+ # skip events that don't involve changes to the file
+ return
+
+ trigger_reload(event.src_path)
+
+ reloader_name = Observer.__name__.lower() # type: ignore[attr-defined]
+
+ if reloader_name.endswith("observer"):
+ reloader_name = reloader_name[:-8]
+
+ self.name = f"watchdog ({reloader_name})"
+ self.observer = Observer()
+ extra_patterns = (p for p in self.extra_files if not os.path.isdir(p))
+ self.event_handler = EventHandler(
+ patterns=["*.py", "*.pyc", "*.zip", *extra_patterns],
+ ignore_patterns=[
+ *[f"*/{d}/*" for d in _ignore_common_dirs],
+ *self.exclude_patterns,
+ ],
+ )
+ self.should_reload = threading.Event()
+
+ def trigger_reload(self, filename: str | bytes) -> None:
+ # This is called inside an event handler, which means throwing
+ # SystemExit has no effect.
+ # https://github.com/gorakhargosh/watchdog/issues/294
+ self.should_reload.set()
+ self.log_reload(filename)
+
+ def __enter__(self) -> ReloaderLoop:
+ self.watches: dict[str, t.Any] = {}
+ self.observer.start()
+ return super().__enter__()
+
+ def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore
+ self.observer.stop()
+ self.observer.join()
+
+ def run(self) -> None:
+ while not self.should_reload.wait(timeout=self.interval):
+ self.run_step()
+
+ sys.exit(3)
+
+ def run_step(self) -> None:
+ to_delete = set(self.watches)
+
+ for path in _find_watchdog_paths(self.extra_files, self.exclude_patterns):
+ if path not in self.watches:
+ try:
+ self.watches[path] = self.observer.schedule(
+ self.event_handler, path, recursive=True
+ )
+ except OSError:
+ # Clear this path from list of watches. We don't want
+ # the same error message showing again in the next
+ # iteration.
+ self.watches[path] = None
+
+ to_delete.discard(path)
+
+ for path in to_delete:
+ watch = self.watches.pop(path, None)
+
+ if watch is not None:
+ self.observer.unschedule(watch)
+
+
+reloader_loops: dict[str, type[ReloaderLoop]] = {
+ "stat": StatReloaderLoop,
+ "watchdog": WatchdogReloaderLoop,
+}
+
+try:
+ __import__("watchdog.observers")
+except ImportError:
+ reloader_loops["auto"] = reloader_loops["stat"]
+else:
+ reloader_loops["auto"] = reloader_loops["watchdog"]
+
+
+def ensure_echo_on() -> None:
+ """Ensure that echo mode is enabled. Some tools such as PDB disable
+ it which causes usability issues after a reload."""
+ # tcgetattr will fail if stdin isn't a tty
+ if sys.stdin is None or not sys.stdin.isatty():
+ return
+
+ try:
+ import termios
+ except ImportError:
+ return
+
+ attributes = termios.tcgetattr(sys.stdin)
+
+ if not attributes[3] & termios.ECHO:
+ attributes[3] |= termios.ECHO
+ termios.tcsetattr(sys.stdin, termios.TCSANOW, attributes)
+
+
+def run_with_reloader(
+ main_func: t.Callable[[], None],
+ extra_files: t.Iterable[str] | None = None,
+ exclude_patterns: t.Iterable[str] | None = None,
+ interval: int | float = 1,
+ reloader_type: str = "auto",
+) -> None:
+ """Run the given function in an independent Python interpreter."""
+ import signal
+
+ signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
+ reloader = reloader_loops[reloader_type](
+ extra_files=extra_files, exclude_patterns=exclude_patterns, interval=interval
+ )
+
+ try:
+ if os.environ.get("WERKZEUG_RUN_MAIN") == "true":
+ ensure_echo_on()
+ t = threading.Thread(target=main_func, args=())
+ t.daemon = True
+
+ # Enter the reloader to set up initial state, then start
+ # the app thread and reloader update loop.
+ with reloader:
+ t.start()
+ reloader.run()
+ else:
+ sys.exit(reloader.restart_with_reloader())
+ except KeyboardInterrupt:
+ pass
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/__init__.py b/venv/Lib/site-packages/werkzeug/datastructures/__init__.py
new file mode 100644
index 0000000..6582de0
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/datastructures/__init__.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+import typing as t
+
+from .accept import Accept as Accept
+from .accept import CharsetAccept as CharsetAccept
+from .accept import LanguageAccept as LanguageAccept
+from .accept import MIMEAccept as MIMEAccept
+from .auth import Authorization as Authorization
+from .auth import WWWAuthenticate as WWWAuthenticate
+from .cache_control import RequestCacheControl as RequestCacheControl
+from .cache_control import ResponseCacheControl as ResponseCacheControl
+from .csp import ContentSecurityPolicy as ContentSecurityPolicy
+from .etag import ETags as ETags
+from .file_storage import FileMultiDict as FileMultiDict
+from .file_storage import FileStorage as FileStorage
+from .headers import EnvironHeaders as EnvironHeaders
+from .headers import Headers as Headers
+from .mixins import ImmutableDictMixin as ImmutableDictMixin
+from .mixins import ImmutableHeadersMixin as ImmutableHeadersMixin
+from .mixins import ImmutableListMixin as ImmutableListMixin
+from .mixins import ImmutableMultiDictMixin as ImmutableMultiDictMixin
+from .mixins import UpdateDictMixin as UpdateDictMixin
+from .range import ContentRange as ContentRange
+from .range import IfRange as IfRange
+from .range import Range as Range
+from .structures import CallbackDict as CallbackDict
+from .structures import CombinedMultiDict as CombinedMultiDict
+from .structures import HeaderSet as HeaderSet
+from .structures import ImmutableDict as ImmutableDict
+from .structures import ImmutableList as ImmutableList
+from .structures import ImmutableMultiDict as ImmutableMultiDict
+from .structures import ImmutableTypeConversionDict as ImmutableTypeConversionDict
+from .structures import iter_multi_items as iter_multi_items
+from .structures import MultiDict as MultiDict
+from .structures import TypeConversionDict as TypeConversionDict
+
+
+def __getattr__(name: str) -> t.Any:
+ import warnings
+
+ if name == "OrderedMultiDict":
+ from .structures import _OrderedMultiDict
+
+ warnings.warn(
+ "'OrderedMultiDict' is deprecated and will be removed in Werkzeug"
+ " 3.2. Use 'MultiDict' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return _OrderedMultiDict
+
+ if name == "ImmutableOrderedMultiDict":
+ from .structures import _ImmutableOrderedMultiDict
+
+ warnings.warn(
+ "'OrderedMultiDict' is deprecated and will be removed in Werkzeug"
+ " 3.2. Use 'ImmutableMultiDict' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return _ImmutableOrderedMultiDict
+
+ raise AttributeError(name)
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..77e6b94
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/accept.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/accept.cpython-310.pyc
new file mode 100644
index 0000000..ccfce1c
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/accept.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/auth.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/auth.cpython-310.pyc
new file mode 100644
index 0000000..91a9ce2
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/auth.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/cache_control.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/cache_control.cpython-310.pyc
new file mode 100644
index 0000000..84bd73a
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/cache_control.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/csp.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/csp.cpython-310.pyc
new file mode 100644
index 0000000..4ea212e
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/csp.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/etag.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/etag.cpython-310.pyc
new file mode 100644
index 0000000..405805d
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/etag.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/file_storage.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/file_storage.cpython-310.pyc
new file mode 100644
index 0000000..7993ed9
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/file_storage.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/headers.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/headers.cpython-310.pyc
new file mode 100644
index 0000000..2197d9c
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/headers.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/mixins.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/mixins.cpython-310.pyc
new file mode 100644
index 0000000..c146a35
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/mixins.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/range.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/range.cpython-310.pyc
new file mode 100644
index 0000000..f55e57b
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/range.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/structures.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/structures.cpython-310.pyc
new file mode 100644
index 0000000..3bb29a9
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/datastructures/__pycache__/structures.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/accept.py b/venv/Lib/site-packages/werkzeug/datastructures/accept.py
new file mode 100644
index 0000000..44179a9
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/datastructures/accept.py
@@ -0,0 +1,350 @@
+from __future__ import annotations
+
+import codecs
+import collections.abc as cabc
+import re
+import typing as t
+
+from .structures import ImmutableList
+
+
+class Accept(ImmutableList[tuple[str, float]]):
+ """An :class:`Accept` object is just a list subclass for lists of
+ ``(value, quality)`` tuples. It is automatically sorted by specificity
+ and quality.
+
+ All :class:`Accept` objects work similar to a list but provide extra
+ functionality for working with the data. Containment checks are
+ normalized to the rules of that header:
+
+ >>> a = CharsetAccept([('ISO-8859-1', 1), ('utf-8', 0.7)])
+ >>> a.best
+ 'ISO-8859-1'
+ >>> 'iso-8859-1' in a
+ True
+ >>> 'UTF8' in a
+ True
+ >>> 'utf7' in a
+ False
+
+ To get the quality for an item you can use normal item lookup:
+
+ >>> print a['utf-8']
+ 0.7
+ >>> a['utf7']
+ 0
+
+ .. versionchanged:: 0.5
+ :class:`Accept` objects are forced immutable now.
+
+ .. versionchanged:: 1.0.0
+ :class:`Accept` internal values are no longer ordered
+ alphabetically for equal quality tags. Instead the initial
+ order is preserved.
+
+ """
+
+ def __init__(
+ self, values: Accept | cabc.Iterable[tuple[str, float]] | None = ()
+ ) -> None:
+ if values is None:
+ super().__init__()
+ self.provided = False
+ elif isinstance(values, Accept):
+ self.provided = values.provided
+ super().__init__(values)
+ else:
+ self.provided = True
+ values = sorted(
+ values, key=lambda x: (self._specificity(x[0]), x[1]), reverse=True
+ )
+ super().__init__(values)
+
+ def _specificity(self, value: str) -> tuple[bool, ...]:
+ """Returns a tuple describing the value's specificity."""
+ return (value != "*",)
+
+ def _value_matches(self, value: str, item: str) -> bool:
+ """Check if a value matches a given accept item."""
+ return item == "*" or item.lower() == value.lower()
+
+ @t.overload
+ def __getitem__(self, key: str) -> float: ...
+ @t.overload
+ def __getitem__(self, key: t.SupportsIndex) -> tuple[str, float]: ...
+ @t.overload
+ def __getitem__(self, key: slice) -> list[tuple[str, float]]: ...
+ def __getitem__(
+ self, key: str | t.SupportsIndex | slice
+ ) -> float | tuple[str, float] | list[tuple[str, float]]:
+ """Besides index lookup (getting item n) you can also pass it a string
+ to get the quality for the item. If the item is not in the list, the
+ returned quality is ``0``.
+ """
+ if isinstance(key, str):
+ return self.quality(key)
+ return list.__getitem__(self, key)
+
+ def quality(self, key: str) -> float:
+ """Returns the quality of the key.
+
+ .. versionadded:: 0.6
+ In previous versions you had to use the item-lookup syntax
+ (eg: ``obj[key]`` instead of ``obj.quality(key)``)
+ """
+ for item, quality in self:
+ if self._value_matches(key, item):
+ return quality
+ return 0
+
+ def __contains__(self, value: str) -> bool: # type: ignore[override]
+ for item, _quality in self:
+ if self._value_matches(value, item):
+ return True
+ return False
+
+ def __repr__(self) -> str:
+ pairs_str = ", ".join(f"({x!r}, {y})" for x, y in self)
+ return f"{type(self).__name__}([{pairs_str}])"
+
+ def index(self, key: str | tuple[str, float]) -> int: # type: ignore[override]
+ """Get the position of an entry or raise :exc:`ValueError`.
+
+ :param key: The key to be looked up.
+
+ .. versionchanged:: 0.5
+ This used to raise :exc:`IndexError`, which was inconsistent
+ with the list API.
+ """
+ if isinstance(key, str):
+ for idx, (item, _quality) in enumerate(self):
+ if self._value_matches(key, item):
+ return idx
+ raise ValueError(key)
+ return list.index(self, key)
+
+ def find(self, key: str | tuple[str, float]) -> int:
+ """Get the position of an entry or return -1.
+
+ :param key: The key to be looked up.
+ """
+ try:
+ return self.index(key)
+ except ValueError:
+ return -1
+
+ def values(self) -> cabc.Iterator[str]:
+ """Iterate over all values."""
+ for item in self:
+ yield item[0]
+
+ def to_header(self) -> str:
+ """Convert the header set into an HTTP header string."""
+ result = []
+ for value, quality in self:
+ if quality != 1:
+ value = f"{value};q={quality}"
+ result.append(value)
+ return ",".join(result)
+
+ def __str__(self) -> str:
+ return self.to_header()
+
+ def _best_single_match(self, match: str) -> tuple[str, float] | None:
+ for client_item, quality in self:
+ if self._value_matches(match, client_item):
+ # self is sorted by specificity descending, we can exit
+ return client_item, quality
+ return None
+
+ @t.overload
+ def best_match(self, matches: cabc.Iterable[str]) -> str | None: ...
+ @t.overload
+ def best_match(self, matches: cabc.Iterable[str], default: str = ...) -> str: ...
+ def best_match(
+ self, matches: cabc.Iterable[str], default: str | None = None
+ ) -> str | None:
+ """Returns the best match from a list of possible matches based
+ on the specificity and quality of the client. If two items have the
+ same quality and specificity, the one is returned that comes first.
+
+ :param matches: a list of matches to check for
+ :param default: the value that is returned if none match
+ """
+ result = default
+ best_quality: float = -1
+ best_specificity: tuple[float, ...] = (-1,)
+ for server_item in matches:
+ match = self._best_single_match(server_item)
+ if not match:
+ continue
+ client_item, quality = match
+ specificity = self._specificity(client_item)
+ if quality <= 0 or quality < best_quality:
+ continue
+ # better quality or same quality but more specific => better match
+ if quality > best_quality or specificity > best_specificity:
+ result = server_item
+ best_quality = quality
+ best_specificity = specificity
+ return result
+
+ @property
+ def best(self) -> str | None:
+ """The best match as value."""
+ if self:
+ return self[0][0]
+
+ return None
+
+
+_mime_split_re = re.compile(r"/|(?:\s*;\s*)")
+
+
+def _normalize_mime(value: str) -> list[str]:
+ return _mime_split_re.split(value.lower())
+
+
+class MIMEAccept(Accept):
+ """Like :class:`Accept` but with special methods and behavior for
+ mimetypes.
+ """
+
+ def _specificity(self, value: str) -> tuple[bool, ...]:
+ return tuple(x != "*" for x in _mime_split_re.split(value))
+
+ def _value_matches(self, value: str, item: str) -> bool:
+ # item comes from the client, can't match if it's invalid.
+ if "/" not in item:
+ return False
+
+ # value comes from the application, tell the developer when it
+ # doesn't look valid.
+ if "/" not in value:
+ raise ValueError(f"invalid mimetype {value!r}")
+
+ # Split the match value into type, subtype, and a sorted list of parameters.
+ normalized_value = _normalize_mime(value)
+ value_type, value_subtype = normalized_value[:2]
+ value_params = sorted(normalized_value[2:])
+
+ # "*/*" is the only valid value that can start with "*".
+ if value_type == "*" and value_subtype != "*":
+ raise ValueError(f"invalid mimetype {value!r}")
+
+ # Split the accept item into type, subtype, and parameters.
+ normalized_item = _normalize_mime(item)
+ item_type, item_subtype = normalized_item[:2]
+ item_params = sorted(normalized_item[2:])
+
+ # "*/not-*" from the client is invalid, can't match.
+ if item_type == "*" and item_subtype != "*":
+ return False
+
+ return (
+ (item_type == "*" and item_subtype == "*")
+ or (value_type == "*" and value_subtype == "*")
+ ) or (
+ item_type == value_type
+ and (
+ item_subtype == "*"
+ or value_subtype == "*"
+ or (item_subtype == value_subtype and item_params == value_params)
+ )
+ )
+
+ @property
+ def accept_html(self) -> bool:
+ """True if this object accepts HTML."""
+ return "text/html" in self or self.accept_xhtml # type: ignore[comparison-overlap]
+
+ @property
+ def accept_xhtml(self) -> bool:
+ """True if this object accepts XHTML."""
+ return "application/xhtml+xml" in self or "application/xml" in self # type: ignore[comparison-overlap]
+
+ @property
+ def accept_json(self) -> bool:
+ """True if this object accepts JSON."""
+ return "application/json" in self # type: ignore[comparison-overlap]
+
+
+_locale_delim_re = re.compile(r"[_-]")
+
+
+def _normalize_lang(value: str) -> list[str]:
+ """Process a language tag for matching."""
+ return _locale_delim_re.split(value.lower())
+
+
+class LanguageAccept(Accept):
+ """Like :class:`Accept` but with normalization for language tags."""
+
+ def _value_matches(self, value: str, item: str) -> bool:
+ return item == "*" or _normalize_lang(value) == _normalize_lang(item)
+
+ @t.overload
+ def best_match(self, matches: cabc.Iterable[str]) -> str | None: ...
+ @t.overload
+ def best_match(self, matches: cabc.Iterable[str], default: str = ...) -> str: ...
+ def best_match(
+ self, matches: cabc.Iterable[str], default: str | None = None
+ ) -> str | None:
+ """Given a list of supported values, finds the best match from
+ the list of accepted values.
+
+ Language tags are normalized for the purpose of matching, but
+ are returned unchanged.
+
+ If no exact match is found, this will fall back to matching
+ the first subtag (primary language only), first with the
+ accepted values then with the match values. This partial is not
+ applied to any other language subtags.
+
+ The default is returned if no exact or fallback match is found.
+
+ :param matches: A list of supported languages to find a match.
+ :param default: The value that is returned if none match.
+ """
+ # Look for an exact match first. If a client accepts "en-US",
+ # "en-US" is a valid match at this point.
+ result = super().best_match(matches)
+
+ if result is not None:
+ return result
+
+ # Fall back to accepting primary tags. If a client accepts
+ # "en-US", "en" is a valid match at this point. Need to use
+ # re.split to account for 2 or 3 letter codes.
+ fallback = Accept(
+ [(_locale_delim_re.split(item[0], 1)[0], item[1]) for item in self]
+ )
+ result = fallback.best_match(matches)
+
+ if result is not None:
+ return result
+
+ # Fall back to matching primary tags. If the client accepts
+ # "en", "en-US" is a valid match at this point.
+ fallback_matches = [_locale_delim_re.split(item, 1)[0] for item in matches]
+ result = super().best_match(fallback_matches)
+
+ # Return a value from the original match list. Find the first
+ # original value that starts with the matched primary tag.
+ if result is not None:
+ return next(item for item in matches if item.startswith(result))
+
+ return default
+
+
+class CharsetAccept(Accept):
+ """Like :class:`Accept` but with normalization for charsets."""
+
+ def _value_matches(self, value: str, item: str) -> bool:
+ def _normalize(name: str) -> str:
+ try:
+ return codecs.lookup(name).name
+ except LookupError:
+ return name.lower()
+
+ return item == "*" or _normalize(value) == _normalize(item)
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/auth.py b/venv/Lib/site-packages/werkzeug/datastructures/auth.py
new file mode 100644
index 0000000..0a3141e
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/datastructures/auth.py
@@ -0,0 +1,320 @@
+from __future__ import annotations
+
+import base64
+import binascii
+import collections.abc as cabc
+import typing as t
+
+from ..http import dump_header
+from ..http import parse_dict_header
+from ..http import quote_header_value
+from .structures import CallbackDict
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+
+class Authorization:
+ """Represents the parts of an ``Authorization`` request header.
+
+ :attr:`.Request.authorization` returns an instance if the header is set.
+
+ An instance can be used with the test :class:`.Client` request methods' ``auth``
+ parameter to send the header in test requests.
+
+ Depending on the auth scheme, either :attr:`parameters` or :attr:`token` will be
+ set. The ``Basic`` scheme's token is decoded into the ``username`` and ``password``
+ parameters.
+
+ For convenience, ``auth["key"]`` and ``auth.key`` both access the key in the
+ :attr:`parameters` dict, along with ``auth.get("key")`` and ``"key" in auth``.
+
+ .. versionchanged:: 2.3
+ The ``token`` parameter and attribute was added to support auth schemes that use
+ a token instead of parameters, such as ``Bearer``.
+
+ .. versionchanged:: 2.3
+ The object is no longer a ``dict``.
+
+ .. versionchanged:: 0.5
+ The object is an immutable dict.
+ """
+
+ def __init__(
+ self,
+ auth_type: str,
+ data: dict[str, str | None] | None = None,
+ token: str | None = None,
+ ) -> None:
+ self.type = auth_type
+ """The authorization scheme, like ``basic``, ``digest``, or ``bearer``."""
+
+ if data is None:
+ data = {}
+
+ self.parameters = data
+ """A dict of parameters parsed from the header. Either this or :attr:`token`
+ will have a value for a given scheme.
+ """
+
+ self.token = token
+ """A token parsed from the header. Either this or :attr:`parameters` will have a
+ value for a given scheme.
+
+ .. versionadded:: 2.3
+ """
+
+ def __getattr__(self, name: str) -> str | None:
+ return self.parameters.get(name)
+
+ def __getitem__(self, name: str) -> str | None:
+ return self.parameters.get(name)
+
+ def get(self, key: str, default: str | None = None) -> str | None:
+ return self.parameters.get(key, default)
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.parameters
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Authorization):
+ return NotImplemented
+
+ return (
+ other.type == self.type
+ and other.token == self.token
+ and other.parameters == self.parameters
+ )
+
+ @classmethod
+ def from_header(cls, value: str | None) -> te.Self | None:
+ """Parse an ``Authorization`` header value and return an instance, or ``None``
+ if the value is empty.
+
+ :param value: The header value to parse.
+
+ .. versionadded:: 2.3
+ """
+ if not value:
+ return None
+
+ scheme, _, rest = value.partition(" ")
+ scheme = scheme.lower()
+ rest = rest.strip()
+
+ if scheme == "basic":
+ try:
+ username, _, password = base64.b64decode(rest).decode().partition(":")
+ except (binascii.Error, UnicodeError):
+ return None
+
+ return cls(scheme, {"username": username, "password": password})
+
+ if "=" in rest.rstrip("="):
+ # = that is not trailing, this is parameters.
+ return cls(scheme, parse_dict_header(rest), None)
+
+ # No = or only trailing =, this is a token.
+ return cls(scheme, None, rest)
+
+ def to_header(self) -> str:
+ """Produce an ``Authorization`` header value representing this data.
+
+ .. versionadded:: 2.0
+ """
+ if self.type == "basic":
+ value = base64.b64encode(
+ f"{self.username}:{self.password}".encode()
+ ).decode("ascii")
+ return f"Basic {value}"
+
+ if self.token is not None:
+ return f"{self.type.title()} {self.token}"
+
+ return f"{self.type.title()} {dump_header(self.parameters)}"
+
+ def __str__(self) -> str:
+ return self.to_header()
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} {self.to_header()}>"
+
+
+class WWWAuthenticate:
+ """Represents the parts of a ``WWW-Authenticate`` response header.
+
+ Set :attr:`.Response.www_authenticate` to an instance of list of instances to set
+ values for this header in the response. Modifying this instance will modify the
+ header value.
+
+ Depending on the auth scheme, either :attr:`parameters` or :attr:`token` should be
+ set. The ``Basic`` scheme will encode ``username`` and ``password`` parameters to a
+ token.
+
+ For convenience, ``auth["key"]`` and ``auth.key`` both act on the :attr:`parameters`
+ dict, and can be used to get, set, or delete parameters. ``auth.get("key")`` and
+ ``"key" in auth`` are also provided.
+
+ .. versionchanged:: 2.3
+ The ``token`` parameter and attribute was added to support auth schemes that use
+ a token instead of parameters, such as ``Bearer``.
+
+ .. versionchanged:: 2.3
+ The object is no longer a ``dict``.
+
+ .. versionchanged:: 2.3
+ The ``on_update`` parameter was removed.
+ """
+
+ def __init__(
+ self,
+ auth_type: str,
+ values: dict[str, str | None] | None = None,
+ token: str | None = None,
+ ):
+ self._type = auth_type.lower()
+ self._parameters: dict[str, str | None] = CallbackDict(
+ values, lambda _: self._trigger_on_update()
+ )
+ self._token = token
+ self._on_update: cabc.Callable[[WWWAuthenticate], None] | None = None
+
+ def _trigger_on_update(self) -> None:
+ if self._on_update is not None:
+ self._on_update(self)
+
+ @property
+ def type(self) -> str:
+ """The authorization scheme, like ``basic``, ``digest``, or ``bearer``."""
+ return self._type
+
+ @type.setter
+ def type(self, value: str) -> None:
+ self._type = value
+ self._trigger_on_update()
+
+ @property
+ def parameters(self) -> dict[str, str | None]:
+ """A dict of parameters for the header. Only one of this or :attr:`token` should
+ have a value for a given scheme.
+ """
+ return self._parameters
+
+ @parameters.setter
+ def parameters(self, value: dict[str, str]) -> None:
+ self._parameters = CallbackDict(value, lambda _: self._trigger_on_update())
+ self._trigger_on_update()
+
+ @property
+ def token(self) -> str | None:
+ """A dict of parameters for the header. Only one of this or :attr:`token` should
+ have a value for a given scheme.
+ """
+ return self._token
+
+ @token.setter
+ def token(self, value: str | None) -> None:
+ """A token for the header. Only one of this or :attr:`parameters` should have a
+ value for a given scheme.
+
+ .. versionadded:: 2.3
+ """
+ self._token = value
+ self._trigger_on_update()
+
+ def __getitem__(self, key: str) -> str | None:
+ return self.parameters.get(key)
+
+ def __setitem__(self, key: str, value: str | None) -> None:
+ if value is None:
+ if key in self.parameters:
+ del self.parameters[key]
+ else:
+ self.parameters[key] = value
+
+ self._trigger_on_update()
+
+ def __delitem__(self, key: str) -> None:
+ if key in self.parameters:
+ del self.parameters[key]
+ self._trigger_on_update()
+
+ def __getattr__(self, name: str) -> str | None:
+ return self[name]
+
+ def __setattr__(self, name: str, value: str | None) -> None:
+ if name in {"_type", "_parameters", "_token", "_on_update"}:
+ super().__setattr__(name, value)
+ else:
+ self[name] = value
+
+ def __delattr__(self, name: str) -> None:
+ del self[name]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.parameters
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, WWWAuthenticate):
+ return NotImplemented
+
+ return (
+ other.type == self.type
+ and other.token == self.token
+ and other.parameters == self.parameters
+ )
+
+ def get(self, key: str, default: str | None = None) -> str | None:
+ return self.parameters.get(key, default)
+
+ @classmethod
+ def from_header(cls, value: str | None) -> te.Self | None:
+ """Parse a ``WWW-Authenticate`` header value and return an instance, or ``None``
+ if the value is empty.
+
+ :param value: The header value to parse.
+
+ .. versionadded:: 2.3
+ """
+ if not value:
+ return None
+
+ scheme, _, rest = value.partition(" ")
+ scheme = scheme.lower()
+ rest = rest.strip()
+
+ if "=" in rest.rstrip("="):
+ # = that is not trailing, this is parameters.
+ return cls(scheme, parse_dict_header(rest), None)
+
+ # No = or only trailing =, this is a token.
+ return cls(scheme, None, rest)
+
+ def to_header(self) -> str:
+ """Produce a ``WWW-Authenticate`` header value representing this data."""
+ if self.token is not None:
+ return f"{self.type.title()} {self.token}"
+
+ if not self.parameters:
+ return self.type.title()
+
+ if self.type == "digest":
+ items = []
+
+ for key, value in self.parameters.items():
+ if key in {"realm", "domain", "nonce", "opaque", "qop"}:
+ value = quote_header_value(value, allow_token=False)
+ else:
+ value = quote_header_value(value)
+
+ items.append(f"{key}={value}")
+
+ return f"Digest {', '.join(items)}"
+
+ return f"{self.type.title()} {dump_header(self.parameters)}"
+
+ def __str__(self) -> str:
+ return self.to_header()
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} {self.to_header()}>"
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/cache_control.py b/venv/Lib/site-packages/werkzeug/datastructures/cache_control.py
new file mode 100644
index 0000000..3d96310
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/datastructures/cache_control.py
@@ -0,0 +1,273 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import typing as t
+from inspect import cleandoc
+
+from .mixins import ImmutableDictMixin
+from .structures import CallbackDict
+
+
+def cache_control_property(
+ key: str, empty: t.Any, type: type[t.Any] | None, *, doc: str | None = None
+) -> t.Any:
+ """Return a new property object for a cache header. Useful if you
+ want to add support for a cache extension in a subclass.
+
+ :param key: The attribute name present in the parsed cache-control header dict.
+ :param empty: The value to use if the key is present without a value.
+ :param type: The type to convert the string value to instead of a string. If
+ conversion raises a ``ValueError``, the returned value is ``None``.
+ :param doc: The docstring for the property. If not given, it is generated
+ based on the other params.
+
+ .. versionchanged:: 3.1
+ Added the ``doc`` param.
+
+ .. versionchanged:: 2.0
+ Renamed from ``cache_property``.
+ """
+ if doc is None:
+ parts = [f"The ``{key}`` attribute."]
+
+ if type is bool:
+ parts.append("A ``bool``, either present or not.")
+ else:
+ if type is None:
+ parts.append("A ``str``,")
+ else:
+ parts.append(f"A ``{type.__name__}``,")
+
+ if empty is not None:
+ parts.append(f"``{empty!r}`` if present with no value,")
+
+ parts.append("or ``None`` if not present.")
+
+ doc = " ".join(parts)
+
+ return property(
+ lambda x: x._get_cache_value(key, empty, type),
+ lambda x, v: x._set_cache_value(key, v, type),
+ lambda x: x._del_cache_value(key),
+ doc=cleandoc(doc),
+ )
+
+
+class _CacheControl(CallbackDict[str, t.Optional[str]]):
+ """Subclass of a dict that stores values for a Cache-Control header. It
+ has accessors for all the cache-control directives specified in RFC 2616.
+ The class does not differentiate between request and response directives.
+
+ Because the cache-control directives in the HTTP header use dashes the
+ python descriptors use underscores for that.
+
+ To get a header of the :class:`CacheControl` object again you can convert
+ the object into a string or call the :meth:`to_header` method. If you plan
+ to subclass it and add your own items have a look at the sourcecode for
+ that class.
+
+ .. versionchanged:: 3.1
+ Dict values are always ``str | None``. Setting properties will
+ convert the value to a string. Setting a non-bool property to
+ ``False`` is equivalent to setting it to ``None``. Getting typed
+ properties will return ``None`` if conversion raises
+ ``ValueError``, rather than the string.
+
+ .. versionchanged:: 2.1
+ Setting int properties such as ``max_age`` will convert the
+ value to an int.
+
+ .. versionchanged:: 0.4
+ Setting ``no_cache`` or ``private`` to ``True`` will set the
+ implicit value ``"*"``.
+ """
+
+ no_store: bool = cache_control_property("no-store", None, bool)
+ max_age: int | None = cache_control_property("max-age", None, int)
+ no_transform: bool = cache_control_property("no-transform", None, bool)
+ stale_if_error: int | None = cache_control_property("stale-if-error", None, int)
+
+ def __init__(
+ self,
+ values: cabc.Mapping[str, t.Any] | cabc.Iterable[tuple[str, t.Any]] | None = (),
+ on_update: cabc.Callable[[_CacheControl], None] | None = None,
+ ):
+ super().__init__(values, on_update)
+ self.provided = values is not None
+
+ def _get_cache_value(
+ self, key: str, empty: t.Any, type: type[t.Any] | None
+ ) -> t.Any:
+ """Used internally by the accessor properties."""
+ if type is bool:
+ return key in self
+
+ if key not in self:
+ return None
+
+ if (value := self[key]) is None:
+ return empty
+
+ if type is not None:
+ try:
+ value = type(value)
+ except ValueError:
+ return None
+
+ return value
+
+ def _set_cache_value(
+ self, key: str, value: t.Any, type: type[t.Any] | None
+ ) -> None:
+ """Used internally by the accessor properties."""
+ if type is bool:
+ if value:
+ self[key] = None
+ else:
+ self.pop(key, None)
+ elif value is None or value is False:
+ self.pop(key, None)
+ elif value is True:
+ self[key] = None
+ else:
+ if type is not None:
+ value = type(value)
+
+ self[key] = str(value)
+
+ def _del_cache_value(self, key: str) -> None:
+ """Used internally by the accessor properties."""
+ if key in self:
+ del self[key]
+
+ def to_header(self) -> str:
+ """Convert the stored values into a cache control header."""
+ return http.dump_header(self)
+
+ def __str__(self) -> str:
+ return self.to_header()
+
+ def __repr__(self) -> str:
+ kv_str = " ".join(f"{k}={v!r}" for k, v in sorted(self.items()))
+ return f"<{type(self).__name__} {kv_str}>"
+
+ cache_property = staticmethod(cache_control_property)
+
+
+class RequestCacheControl(ImmutableDictMixin[str, t.Optional[str]], _CacheControl): # type: ignore[misc]
+ """A cache control for requests. This is immutable and gives access
+ to all the request-relevant cache control headers.
+
+ To get a header of the :class:`RequestCacheControl` object again you can
+ convert the object into a string or call the :meth:`to_header` method. If
+ you plan to subclass it and add your own items have a look at the sourcecode
+ for that class.
+
+ .. versionchanged:: 3.1
+ Dict values are always ``str | None``. Setting properties will
+ convert the value to a string. Setting a non-bool property to
+ ``False`` is equivalent to setting it to ``None``. Getting typed
+ properties will return ``None`` if conversion raises
+ ``ValueError``, rather than the string.
+
+ .. versionchanged:: 3.1
+ ``max_age`` is ``None`` if present without a value, rather
+ than ``-1``.
+
+ .. versionchanged:: 3.1
+ ``no_cache`` is a boolean, it is ``True`` instead of ``"*"``
+ when present.
+
+ .. versionchanged:: 3.1
+ ``max_stale`` is ``True`` if present without a value, rather
+ than ``"*"``.
+
+ .. versionchanged:: 3.1
+ ``no_transform`` is a boolean. Previously it was mistakenly
+ always ``None``.
+
+ .. versionchanged:: 3.1
+ ``min_fresh`` is ``None`` if present without a value, rather
+ than ``"*"``.
+
+ .. versionchanged:: 2.1
+ Setting int properties such as ``max_age`` will convert the
+ value to an int.
+
+ .. versionadded:: 0.5
+ Response-only properties are not present on this request class.
+ """
+
+ no_cache: bool = cache_control_property("no-cache", None, bool)
+ max_stale: int | t.Literal[True] | None = cache_control_property(
+ "max-stale",
+ True,
+ int,
+ )
+ min_fresh: int | None = cache_control_property("min-fresh", None, int)
+ only_if_cached: bool = cache_control_property("only-if-cached", None, bool)
+
+
+class ResponseCacheControl(_CacheControl):
+ """A cache control for responses. Unlike :class:`RequestCacheControl`
+ this is mutable and gives access to response-relevant cache control
+ headers.
+
+ To get a header of the :class:`ResponseCacheControl` object again you can
+ convert the object into a string or call the :meth:`to_header` method. If
+ you plan to subclass it and add your own items have a look at the sourcecode
+ for that class.
+
+ .. versionchanged:: 3.1
+ Dict values are always ``str | None``. Setting properties will
+ convert the value to a string. Setting a non-bool property to
+ ``False`` is equivalent to setting it to ``None``. Getting typed
+ properties will return ``None`` if conversion raises
+ ``ValueError``, rather than the string.
+
+ .. versionchanged:: 3.1
+ ``no_cache`` is ``True`` if present without a value, rather than
+ ``"*"``.
+
+ .. versionchanged:: 3.1
+ ``private`` is ``True`` if present without a value, rather than
+ ``"*"``.
+
+ .. versionchanged:: 3.1
+ ``no_transform`` is a boolean. Previously it was mistakenly
+ always ``None``.
+
+ .. versionchanged:: 3.1
+ Added the ``must_understand``, ``stale_while_revalidate``, and
+ ``stale_if_error`` properties.
+
+ .. versionchanged:: 2.1.1
+ ``s_maxage`` converts the value to an int.
+
+ .. versionchanged:: 2.1
+ Setting int properties such as ``max_age`` will convert the
+ value to an int.
+
+ .. versionadded:: 0.5
+ Request-only properties are not present on this response class.
+ """
+
+ no_cache: str | t.Literal[True] | None = cache_control_property(
+ "no-cache", True, None
+ )
+ public: bool = cache_control_property("public", None, bool)
+ private: str | t.Literal[True] | None = cache_control_property(
+ "private", True, None
+ )
+ must_revalidate: bool = cache_control_property("must-revalidate", None, bool)
+ proxy_revalidate: bool = cache_control_property("proxy-revalidate", None, bool)
+ s_maxage: int | None = cache_control_property("s-maxage", None, int)
+ immutable: bool = cache_control_property("immutable", None, bool)
+ must_understand: bool = cache_control_property("must-understand", None, bool)
+ stale_while_revalidate: int | None = cache_control_property(
+ "stale-while-revalidate", None, int
+ )
+
+
+# circular dependencies
+from .. import http # noqa: E402
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/csp.py b/venv/Lib/site-packages/werkzeug/datastructures/csp.py
new file mode 100644
index 0000000..206c1e3
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/datastructures/csp.py
@@ -0,0 +1,100 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import typing as t
+
+from .structures import CallbackDict
+
+
+def csp_property(key: str) -> t.Any:
+ """Return a new property object for a content security policy header.
+ Useful if you want to add support for a csp extension in a
+ subclass.
+ """
+ return property(
+ lambda x: x._get_value(key),
+ lambda x, v: x._set_value(key, v),
+ lambda x: x._del_value(key),
+ f"accessor for {key!r}",
+ )
+
+
+class ContentSecurityPolicy(CallbackDict[str, str]):
+ """Subclass of a dict that stores values for a Content Security Policy
+ header. It has accessors for all the level 3 policies.
+
+ Because the csp directives in the HTTP header use dashes the
+ python descriptors use underscores for that.
+
+ To get a header of the :class:`ContentSecurityPolicy` object again
+ you can convert the object into a string or call the
+ :meth:`to_header` method. If you plan to subclass it and add your
+ own items have a look at the sourcecode for that class.
+
+ .. versionadded:: 1.0.0
+ Support for Content Security Policy headers was added.
+
+ """
+
+ base_uri: str | None = csp_property("base-uri")
+ child_src: str | None = csp_property("child-src")
+ connect_src: str | None = csp_property("connect-src")
+ default_src: str | None = csp_property("default-src")
+ font_src: str | None = csp_property("font-src")
+ form_action: str | None = csp_property("form-action")
+ frame_ancestors: str | None = csp_property("frame-ancestors")
+ frame_src: str | None = csp_property("frame-src")
+ img_src: str | None = csp_property("img-src")
+ manifest_src: str | None = csp_property("manifest-src")
+ media_src: str | None = csp_property("media-src")
+ navigate_to: str | None = csp_property("navigate-to")
+ object_src: str | None = csp_property("object-src")
+ prefetch_src: str | None = csp_property("prefetch-src")
+ plugin_types: str | None = csp_property("plugin-types")
+ report_to: str | None = csp_property("report-to")
+ report_uri: str | None = csp_property("report-uri")
+ sandbox: str | None = csp_property("sandbox")
+ script_src: str | None = csp_property("script-src")
+ script_src_attr: str | None = csp_property("script-src-attr")
+ script_src_elem: str | None = csp_property("script-src-elem")
+ style_src: str | None = csp_property("style-src")
+ style_src_attr: str | None = csp_property("style-src-attr")
+ style_src_elem: str | None = csp_property("style-src-elem")
+ worker_src: str | None = csp_property("worker-src")
+
+ def __init__(
+ self,
+ values: cabc.Mapping[str, str] | cabc.Iterable[tuple[str, str]] | None = (),
+ on_update: cabc.Callable[[ContentSecurityPolicy], None] | None = None,
+ ) -> None:
+ super().__init__(values, on_update)
+ self.provided = values is not None
+
+ def _get_value(self, key: str) -> str | None:
+ """Used internally by the accessor properties."""
+ return self.get(key)
+
+ def _set_value(self, key: str, value: str | None) -> None:
+ """Used internally by the accessor properties."""
+ if value is None:
+ self.pop(key, None)
+ else:
+ self[key] = value
+
+ def _del_value(self, key: str) -> None:
+ """Used internally by the accessor properties."""
+ if key in self:
+ del self[key]
+
+ def to_header(self) -> str:
+ """Convert the stored values into a cache control header."""
+ from ..http import dump_csp_header
+
+ return dump_csp_header(self)
+
+ def __str__(self) -> str:
+ return self.to_header()
+
+ def __repr__(self) -> str:
+ kv_str = " ".join(f"{k}={v!r}" for k, v in sorted(self.items()))
+ return f"<{type(self).__name__} {kv_str}>"
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/etag.py b/venv/Lib/site-packages/werkzeug/datastructures/etag.py
new file mode 100644
index 0000000..a4ef342
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/datastructures/etag.py
@@ -0,0 +1,106 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+
+
+class ETags(cabc.Collection[str]):
+ """A set that can be used to check if one etag is present in a collection
+ of etags.
+ """
+
+ def __init__(
+ self,
+ strong_etags: cabc.Iterable[str] | None = None,
+ weak_etags: cabc.Iterable[str] | None = None,
+ star_tag: bool = False,
+ ):
+ if not star_tag and strong_etags:
+ self._strong = frozenset(strong_etags)
+ else:
+ self._strong = frozenset()
+
+ self._weak = frozenset(weak_etags or ())
+ self.star_tag = star_tag
+
+ def as_set(self, include_weak: bool = False) -> set[str]:
+ """Convert the `ETags` object into a python set. Per default all the
+ weak etags are not part of this set."""
+ rv = set(self._strong)
+ if include_weak:
+ rv.update(self._weak)
+ return rv
+
+ def is_weak(self, etag: str) -> bool:
+ """Check if an etag is weak."""
+ return etag in self._weak
+
+ def is_strong(self, etag: str) -> bool:
+ """Check if an etag is strong."""
+ return etag in self._strong
+
+ def contains_weak(self, etag: str) -> bool:
+ """Check if an etag is part of the set including weak and strong tags."""
+ return self.is_weak(etag) or self.contains(etag)
+
+ def contains(self, etag: str) -> bool:
+ """Check if an etag is part of the set ignoring weak tags.
+ It is also possible to use the ``in`` operator.
+ """
+ if self.star_tag:
+ return True
+ return self.is_strong(etag)
+
+ def contains_raw(self, etag: str) -> bool:
+ """When passed a quoted tag it will check if this tag is part of the
+ set. If the tag is weak it is checked against weak and strong tags,
+ otherwise strong only."""
+ from ..http import unquote_etag
+
+ etag, weak = unquote_etag(etag)
+ if weak:
+ return self.contains_weak(etag)
+ return self.contains(etag)
+
+ def to_header(self) -> str:
+ """Convert the etags set into a HTTP header string."""
+ if self.star_tag:
+ return "*"
+ return ", ".join(
+ [f'"{x}"' for x in self._strong] + [f'W/"{x}"' for x in self._weak]
+ )
+
+ def __call__(
+ self,
+ etag: str | None = None,
+ data: bytes | None = None,
+ include_weak: bool = False,
+ ) -> bool:
+ if etag is None:
+ if data is None:
+ raise TypeError("'data' is required when 'etag' is not given.")
+
+ from ..http import generate_etag
+
+ etag = generate_etag(data)
+ if include_weak:
+ if etag in self._weak:
+ return True
+ return etag in self._strong
+
+ def __bool__(self) -> bool:
+ return bool(self.star_tag or self._strong or self._weak)
+
+ def __str__(self) -> str:
+ return self.to_header()
+
+ def __len__(self) -> int:
+ return len(self._strong)
+
+ def __iter__(self) -> cabc.Iterator[str]:
+ return iter(self._strong)
+
+ def __contains__(self, etag: str) -> bool: # type: ignore[override]
+ return self.contains(etag)
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} {str(self)!r}>"
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/file_storage.py b/venv/Lib/site-packages/werkzeug/datastructures/file_storage.py
new file mode 100644
index 0000000..3a17f51
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/datastructures/file_storage.py
@@ -0,0 +1,209 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import mimetypes
+import os
+import typing as t
+from io import BytesIO
+from os import fsdecode
+from os import fspath
+
+from .._internal import _plain_int
+from .headers import Headers
+from .structures import MultiDict
+
+
+class FileStorage:
+ """The :class:`FileStorage` class is a thin wrapper over incoming files.
+ It is used by the request object to represent uploaded files. All the
+ attributes of the wrapper stream are proxied by the file storage so
+ it's possible to do ``storage.read()`` instead of the long form
+ ``storage.stream.read()``.
+ """
+
+ def __init__(
+ self,
+ stream: t.IO[bytes] | None = None,
+ filename: str | None = None,
+ name: str | None = None,
+ content_type: str | None = None,
+ content_length: int | None = None,
+ headers: Headers | None = None,
+ ):
+ self.name = name
+ self.stream = stream or BytesIO()
+
+ # If no filename is provided, attempt to get the filename from
+ # the stream object. Python names special streams like
+ # ```` with angular brackets, skip these streams.
+ if filename is None:
+ filename = getattr(stream, "name", None)
+
+ if filename is not None:
+ filename = fsdecode(filename)
+
+ if filename and filename[0] == "<" and filename[-1] == ">":
+ filename = None
+ else:
+ filename = fsdecode(filename)
+
+ self.filename = filename
+
+ if headers is None:
+ headers = Headers()
+ self.headers = headers
+ if content_type is not None:
+ headers["Content-Type"] = content_type
+ if content_length is not None:
+ headers["Content-Length"] = str(content_length)
+
+ def _parse_content_type(self) -> None:
+ if not hasattr(self, "_parsed_content_type"):
+ self._parsed_content_type = http.parse_options_header(self.content_type)
+
+ @property
+ def content_type(self) -> str | None:
+ """The content-type sent in the header. Usually not available"""
+ return self.headers.get("content-type")
+
+ @property
+ def content_length(self) -> int:
+ """The content-length sent in the header. Usually not available"""
+ if "content-length" in self.headers:
+ try:
+ return _plain_int(self.headers["content-length"])
+ except ValueError:
+ pass
+
+ return 0
+
+ @property
+ def mimetype(self) -> str:
+ """Like :attr:`content_type`, but without parameters (eg, without
+ charset, type etc.) and always lowercase. For example if the content
+ type is ``text/HTML; charset=utf-8`` the mimetype would be
+ ``'text/html'``.
+
+ .. versionadded:: 0.7
+ """
+ self._parse_content_type()
+ return self._parsed_content_type[0].lower()
+
+ @property
+ def mimetype_params(self) -> dict[str, str]:
+ """The mimetype parameters as dict. For example if the content
+ type is ``text/html; charset=utf-8`` the params would be
+ ``{'charset': 'utf-8'}``.
+
+ .. versionadded:: 0.7
+ """
+ self._parse_content_type()
+ return self._parsed_content_type[1]
+
+ def save(
+ self, dst: str | os.PathLike[str] | t.IO[bytes], buffer_size: int = 16384
+ ) -> None:
+ """Save the file to a destination path or file object. If the
+ destination is a file object you have to close it yourself after the
+ call. The buffer size is the number of bytes held in memory during
+ the copy process. It defaults to 16KB.
+
+ For secure file saving also have a look at :func:`secure_filename`.
+
+ :param dst: a filename, :class:`os.PathLike`, or open file
+ object to write to.
+ :param buffer_size: Passed as the ``length`` parameter of
+ :func:`shutil.copyfileobj`.
+
+ .. versionchanged:: 1.0
+ Supports :mod:`pathlib`.
+ """
+ from shutil import copyfileobj
+
+ close_dst = False
+
+ if hasattr(dst, "__fspath__"):
+ dst = fspath(dst)
+
+ if isinstance(dst, str):
+ dst = open(dst, "wb")
+ close_dst = True
+
+ try:
+ copyfileobj(self.stream, dst, buffer_size)
+ finally:
+ if close_dst:
+ dst.close()
+
+ def close(self) -> None:
+ """Close the underlying file if possible."""
+ try:
+ self.stream.close()
+ except Exception:
+ pass
+
+ def __bool__(self) -> bool:
+ return bool(self.filename)
+
+ def __getattr__(self, name: str) -> t.Any:
+ try:
+ return getattr(self.stream, name)
+ except AttributeError:
+ # SpooledTemporaryFile on Python < 3.11 doesn't implement IOBase,
+ # get the attribute from its backing file instead.
+ if hasattr(self.stream, "_file"):
+ return getattr(self.stream._file, name)
+ raise
+
+ def __iter__(self) -> cabc.Iterator[bytes]:
+ return iter(self.stream)
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__}: {self.filename!r} ({self.content_type!r})>"
+
+
+class FileMultiDict(MultiDict[str, FileStorage]):
+ """A special :class:`MultiDict` that has convenience methods to add
+ files to it. This is used for :class:`EnvironBuilder` and generally
+ useful for unittesting.
+
+ .. versionadded:: 0.5
+ """
+
+ def add_file(
+ self,
+ name: str,
+ file: str | os.PathLike[str] | t.IO[bytes] | FileStorage,
+ filename: str | None = None,
+ content_type: str | None = None,
+ ) -> None:
+ """Adds a new file to the dict. `file` can be a file name or
+ a :class:`file`-like or a :class:`FileStorage` object.
+
+ :param name: the name of the field.
+ :param file: a filename or :class:`file`-like object
+ :param filename: an optional filename
+ :param content_type: an optional content type
+ """
+ if isinstance(file, FileStorage):
+ self.add(name, file)
+ return
+
+ if isinstance(file, (str, os.PathLike)):
+ if filename is None:
+ filename = os.fspath(file)
+
+ file_obj: t.IO[bytes] = open(file, "rb")
+ else:
+ file_obj = file # type: ignore[assignment]
+
+ if filename and content_type is None:
+ content_type = (
+ mimetypes.guess_type(filename)[0] or "application/octet-stream"
+ )
+
+ self.add(name, FileStorage(file_obj, filename, name, content_type))
+
+
+# circular dependencies
+from .. import http # noqa: E402
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/headers.py b/venv/Lib/site-packages/werkzeug/datastructures/headers.py
new file mode 100644
index 0000000..b898362
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/datastructures/headers.py
@@ -0,0 +1,662 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import re
+import typing as t
+
+from .._internal import _missing
+from ..exceptions import BadRequestKeyError
+from .mixins import ImmutableHeadersMixin
+from .structures import iter_multi_items
+from .structures import MultiDict
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+ from _typeshed.wsgi import WSGIEnvironment
+
+T = t.TypeVar("T")
+
+
+class Headers:
+ """An object that stores some headers. It has a dict-like interface,
+ but is ordered, can store the same key multiple times, and iterating
+ yields ``(key, value)`` pairs instead of only keys.
+
+ This data structure is useful if you want a nicer way to handle WSGI
+ headers which are stored as tuples in a list.
+
+ From Werkzeug 0.3 onwards, the :exc:`KeyError` raised by this class is
+ also a subclass of the :class:`~exceptions.BadRequest` HTTP exception
+ and will render a page for a ``400 BAD REQUEST`` if caught in a
+ catch-all for HTTP exceptions.
+
+ Headers is mostly compatible with the Python :class:`wsgiref.headers.Headers`
+ class, with the exception of `__getitem__`. :mod:`wsgiref` will return
+ `None` for ``headers['missing']``, whereas :class:`Headers` will raise
+ a :class:`KeyError`.
+
+ To create a new ``Headers`` object, pass it a list, dict, or
+ other ``Headers`` object with default values. These values are
+ validated the same way values added later are.
+
+ :param defaults: The list of default values for the :class:`Headers`.
+
+ .. versionchanged:: 3.1
+ Implement ``|`` and ``|=`` operators.
+
+ .. versionchanged:: 2.1.0
+ Default values are validated the same as values added later.
+
+ .. versionchanged:: 0.9
+ This data structure now stores unicode values similar to how the
+ multi dicts do it. The main difference is that bytes can be set as
+ well which will automatically be latin1 decoded.
+
+ .. versionchanged:: 0.9
+ The :meth:`linked` function was removed without replacement as it
+ was an API that does not support the changes to the encoding model.
+ """
+
+ def __init__(
+ self,
+ defaults: (
+ Headers
+ | MultiDict[str, t.Any]
+ | cabc.Mapping[str, t.Any | list[t.Any] | tuple[t.Any, ...] | set[t.Any]]
+ | cabc.Iterable[tuple[str, t.Any]]
+ | None
+ ) = None,
+ ) -> None:
+ self._list: list[tuple[str, str]] = []
+
+ if defaults is not None:
+ self.extend(defaults)
+
+ @t.overload
+ def __getitem__(self, key: str) -> str: ...
+ @t.overload
+ def __getitem__(self, key: int) -> tuple[str, str]: ...
+ @t.overload
+ def __getitem__(self, key: slice) -> te.Self: ...
+ def __getitem__(self, key: str | int | slice) -> str | tuple[str, str] | te.Self:
+ if isinstance(key, str):
+ return self._get_key(key)
+
+ if isinstance(key, int):
+ return self._list[key]
+
+ return self.__class__(self._list[key])
+
+ def _get_key(self, key: str) -> str:
+ ikey = key.lower()
+
+ for k, v in self._list:
+ if k.lower() == ikey:
+ return v
+
+ raise BadRequestKeyError(key)
+
+ def __eq__(self, other: object) -> bool:
+ if other.__class__ is not self.__class__:
+ return NotImplemented
+
+ def lowered(item: tuple[str, ...]) -> tuple[str, ...]:
+ return item[0].lower(), *item[1:]
+
+ return set(map(lowered, other._list)) == set(map(lowered, self._list)) # type: ignore[attr-defined]
+
+ __hash__ = None # type: ignore[assignment]
+
+ @t.overload
+ def get(self, key: str) -> str | None: ...
+ @t.overload
+ def get(self, key: str, default: str) -> str: ...
+ @t.overload
+ def get(self, key: str, default: T) -> str | T: ...
+ @t.overload
+ def get(self, key: str, type: cabc.Callable[[str], T]) -> T | None: ...
+ @t.overload
+ def get(self, key: str, default: T, type: cabc.Callable[[str], T]) -> T: ...
+ def get( # type: ignore[misc]
+ self,
+ key: str,
+ default: str | T | None = None,
+ type: cabc.Callable[[str], T] | None = None,
+ ) -> str | T | None:
+ """Return the default value if the requested data doesn't exist.
+ If `type` is provided and is a callable it should convert the value,
+ return it or raise a :exc:`ValueError` if that is not possible. In
+ this case the function will return the default as if the value was not
+ found:
+
+ >>> d = Headers([('Content-Length', '42')])
+ >>> d.get('Content-Length', type=int)
+ 42
+
+ :param key: The key to be looked up.
+ :param default: The default value to be returned if the key can't
+ be looked up. If not further specified `None` is
+ returned.
+ :param type: A callable that is used to cast the value in the
+ :class:`Headers`. If a :exc:`ValueError` is raised
+ by this callable the default value is returned.
+
+ .. versionchanged:: 3.0
+ The ``as_bytes`` parameter was removed.
+
+ .. versionchanged:: 0.9
+ The ``as_bytes`` parameter was added.
+ """
+ try:
+ rv = self._get_key(key)
+ except KeyError:
+ return default
+
+ if type is None:
+ return rv
+
+ try:
+ return type(rv)
+ except ValueError:
+ return default
+
+ @t.overload
+ def getlist(self, key: str) -> list[str]: ...
+ @t.overload
+ def getlist(self, key: str, type: cabc.Callable[[str], T]) -> list[T]: ...
+ def getlist(
+ self, key: str, type: cabc.Callable[[str], T] | None = None
+ ) -> list[str] | list[T]:
+ """Return the list of items for a given key. If that key is not in the
+ :class:`Headers`, the return value will be an empty list. Just like
+ :meth:`get`, :meth:`getlist` accepts a `type` parameter. All items will
+ be converted with the callable defined there.
+
+ :param key: The key to be looked up.
+ :param type: A callable that is used to cast the value in the
+ :class:`Headers`. If a :exc:`ValueError` is raised
+ by this callable the value will be removed from the list.
+ :return: a :class:`list` of all the values for the key.
+
+ .. versionchanged:: 3.0
+ The ``as_bytes`` parameter was removed.
+
+ .. versionchanged:: 0.9
+ The ``as_bytes`` parameter was added.
+ """
+ ikey = key.lower()
+
+ if type is not None:
+ result = []
+
+ for k, v in self:
+ if k.lower() == ikey:
+ try:
+ result.append(type(v))
+ except ValueError:
+ continue
+
+ return result
+
+ return [v for k, v in self if k.lower() == ikey]
+
+ def get_all(self, name: str) -> list[str]:
+ """Return a list of all the values for the named field.
+
+ This method is compatible with the :mod:`wsgiref`
+ :meth:`~wsgiref.headers.Headers.get_all` method.
+ """
+ return self.getlist(name)
+
+ def items(self, lower: bool = False) -> t.Iterable[tuple[str, str]]:
+ for key, value in self:
+ if lower:
+ key = key.lower()
+ yield key, value
+
+ def keys(self, lower: bool = False) -> t.Iterable[str]:
+ for key, _ in self.items(lower):
+ yield key
+
+ def values(self) -> t.Iterable[str]:
+ for _, value in self.items():
+ yield value
+
+ def extend(
+ self,
+ arg: (
+ Headers
+ | MultiDict[str, t.Any]
+ | cabc.Mapping[str, t.Any | list[t.Any] | tuple[t.Any, ...] | set[t.Any]]
+ | cabc.Iterable[tuple[str, t.Any]]
+ | None
+ ) = None,
+ /,
+ **kwargs: str,
+ ) -> None:
+ """Extend headers in this object with items from another object
+ containing header items as well as keyword arguments.
+
+ To replace existing keys instead of extending, use
+ :meth:`update` instead.
+
+ If provided, the first argument can be another :class:`Headers`
+ object, a :class:`MultiDict`, :class:`dict`, or iterable of
+ pairs.
+
+ .. versionchanged:: 1.0
+ Support :class:`MultiDict`. Allow passing ``kwargs``.
+ """
+ if arg is not None:
+ for key, value in iter_multi_items(arg):
+ self.add(key, value)
+
+ for key, value in iter_multi_items(kwargs):
+ self.add(key, value)
+
+ def __delitem__(self, key: str | int | slice) -> None:
+ if isinstance(key, str):
+ self._del_key(key)
+ return
+
+ del self._list[key]
+
+ def _del_key(self, key: str) -> None:
+ key = key.lower()
+ new = []
+
+ for k, v in self._list:
+ if k.lower() != key:
+ new.append((k, v))
+
+ self._list[:] = new
+
+ def remove(self, key: str) -> None:
+ """Remove a key.
+
+ :param key: The key to be removed.
+ """
+ return self._del_key(key)
+
+ @t.overload
+ def pop(self) -> tuple[str, str]: ...
+ @t.overload
+ def pop(self, key: str) -> str: ...
+ @t.overload
+ def pop(self, key: int | None = ...) -> tuple[str, str]: ...
+ @t.overload
+ def pop(self, key: str, default: str) -> str: ...
+ @t.overload
+ def pop(self, key: str, default: T) -> str | T: ...
+ def pop(
+ self,
+ key: str | int | None = None,
+ default: str | T = _missing, # type: ignore[assignment]
+ ) -> str | tuple[str, str] | T:
+ """Removes and returns a key or index.
+
+ :param key: The key to be popped. If this is an integer the item at
+ that position is removed, if it's a string the value for
+ that key is. If the key is omitted or `None` the last
+ item is removed.
+ :return: an item.
+ """
+ if key is None:
+ return self._list.pop()
+
+ if isinstance(key, int):
+ return self._list.pop(key)
+
+ try:
+ rv = self._get_key(key)
+ except KeyError:
+ if default is not _missing:
+ return default
+
+ raise
+
+ self.remove(key)
+ return rv
+
+ def popitem(self) -> tuple[str, str]:
+ """Removes a key or index and returns a (key, value) item."""
+ return self._list.pop()
+
+ def __contains__(self, key: str) -> bool:
+ """Check if a key is present."""
+ try:
+ self._get_key(key)
+ except KeyError:
+ return False
+
+ return True
+
+ def __iter__(self) -> t.Iterator[tuple[str, str]]:
+ """Yield ``(key, value)`` tuples."""
+ return iter(self._list)
+
+ def __len__(self) -> int:
+ return len(self._list)
+
+ def add(self, key: str, value: t.Any, /, **kwargs: t.Any) -> None:
+ """Add a new header tuple to the list.
+
+ Keyword arguments can specify additional parameters for the header
+ value, with underscores converted to dashes::
+
+ >>> d = Headers()
+ >>> d.add('Content-Type', 'text/plain')
+ >>> d.add('Content-Disposition', 'attachment', filename='foo.png')
+
+ The keyword argument dumping uses :func:`dump_options_header`
+ behind the scenes.
+
+ .. versionchanged:: 0.4.1
+ keyword arguments were added for :mod:`wsgiref` compatibility.
+ """
+ if kwargs:
+ value = _options_header_vkw(value, kwargs)
+
+ value_str = _str_header_value(value)
+ self._list.append((key, value_str))
+
+ def add_header(self, key: str, value: t.Any, /, **kwargs: t.Any) -> None:
+ """Add a new header tuple to the list.
+
+ An alias for :meth:`add` for compatibility with the :mod:`wsgiref`
+ :meth:`~wsgiref.headers.Headers.add_header` method.
+ """
+ self.add(key, value, **kwargs)
+
+ def clear(self) -> None:
+ """Clears all headers."""
+ self._list.clear()
+
+ def set(self, key: str, value: t.Any, /, **kwargs: t.Any) -> None:
+ """Remove all header tuples for `key` and add a new one. The newly
+ added key either appears at the end of the list if there was no
+ entry or replaces the first one.
+
+ Keyword arguments can specify additional parameters for the header
+ value, with underscores converted to dashes. See :meth:`add` for
+ more information.
+
+ .. versionchanged:: 0.6.1
+ :meth:`set` now accepts the same arguments as :meth:`add`.
+
+ :param key: The key to be inserted.
+ :param value: The value to be inserted.
+ """
+ if kwargs:
+ value = _options_header_vkw(value, kwargs)
+
+ value_str = _str_header_value(value)
+
+ if not self._list:
+ self._list.append((key, value_str))
+ return
+
+ iter_list = iter(self._list)
+ ikey = key.lower()
+
+ for idx, (old_key, _) in enumerate(iter_list):
+ if old_key.lower() == ikey:
+ # replace first occurrence
+ self._list[idx] = (key, value_str)
+ break
+ else:
+ # no existing occurrences
+ self._list.append((key, value_str))
+ return
+
+ # remove remaining occurrences
+ self._list[idx + 1 :] = [t for t in iter_list if t[0].lower() != ikey]
+
+ def setlist(self, key: str, values: cabc.Iterable[t.Any]) -> None:
+ """Remove any existing values for a header and add new ones.
+
+ :param key: The header key to set.
+ :param values: An iterable of values to set for the key.
+
+ .. versionadded:: 1.0
+ """
+ if values:
+ values_iter = iter(values)
+ self.set(key, next(values_iter))
+
+ for value in values_iter:
+ self.add(key, value)
+ else:
+ self.remove(key)
+
+ def setdefault(self, key: str, default: t.Any) -> str:
+ """Return the first value for the key if it is in the headers,
+ otherwise set the header to the value given by ``default`` and
+ return that.
+
+ :param key: The header key to get.
+ :param default: The value to set for the key if it is not in the
+ headers.
+ """
+ try:
+ return self._get_key(key)
+ except KeyError:
+ pass
+
+ self.set(key, default)
+ return self._get_key(key)
+
+ def setlistdefault(self, key: str, default: cabc.Iterable[t.Any]) -> list[str]:
+ """Return the list of values for the key if it is in the
+ headers, otherwise set the header to the list of values given
+ by ``default`` and return that.
+
+ Unlike :meth:`MultiDict.setlistdefault`, modifying the returned
+ list will not affect the headers.
+
+ :param key: The header key to get.
+ :param default: An iterable of values to set for the key if it
+ is not in the headers.
+
+ .. versionadded:: 1.0
+ """
+ if key not in self:
+ self.setlist(key, default)
+
+ return self.getlist(key)
+
+ @t.overload
+ def __setitem__(self, key: str, value: t.Any) -> None: ...
+ @t.overload
+ def __setitem__(self, key: int, value: tuple[str, t.Any]) -> None: ...
+ @t.overload
+ def __setitem__(
+ self, key: slice, value: cabc.Iterable[tuple[str, t.Any]]
+ ) -> None: ...
+ def __setitem__(
+ self,
+ key: str | int | slice,
+ value: t.Any | tuple[str, t.Any] | cabc.Iterable[tuple[str, t.Any]],
+ ) -> None:
+ """Like :meth:`set` but also supports index/slice based setting."""
+ if isinstance(key, str):
+ self.set(key, value)
+ elif isinstance(key, int):
+ self._list[key] = value[0], _str_header_value(value[1]) # type: ignore[index]
+ else:
+ self._list[key] = [(k, _str_header_value(v)) for k, v in value] # type: ignore[misc]
+
+ def update(
+ self,
+ arg: (
+ Headers
+ | MultiDict[str, t.Any]
+ | cabc.Mapping[
+ str, t.Any | list[t.Any] | tuple[t.Any, ...] | cabc.Set[t.Any]
+ ]
+ | cabc.Iterable[tuple[str, t.Any]]
+ | None
+ ) = None,
+ /,
+ **kwargs: t.Any | list[t.Any] | tuple[t.Any, ...] | cabc.Set[t.Any],
+ ) -> None:
+ """Replace headers in this object with items from another
+ headers object and keyword arguments.
+
+ To extend existing keys instead of replacing, use :meth:`extend`
+ instead.
+
+ If provided, the first argument can be another :class:`Headers`
+ object, a :class:`MultiDict`, :class:`dict`, or iterable of
+ pairs.
+
+ .. versionadded:: 1.0
+ """
+ if arg is not None:
+ if isinstance(arg, (Headers, MultiDict)):
+ for key in arg.keys():
+ self.setlist(key, arg.getlist(key))
+ elif isinstance(arg, cabc.Mapping):
+ for key, value in arg.items():
+ if isinstance(value, (list, tuple, set)):
+ self.setlist(key, value)
+ else:
+ self.set(key, value)
+ else:
+ for key, value in arg:
+ self.set(key, value)
+
+ for key, value in kwargs.items():
+ if isinstance(value, (list, tuple, set)):
+ self.setlist(key, value)
+ else:
+ self.set(key, value)
+
+ def __or__(
+ self,
+ other: cabc.Mapping[
+ str, t.Any | list[t.Any] | tuple[t.Any, ...] | cabc.Set[t.Any]
+ ],
+ ) -> te.Self:
+ if not isinstance(other, cabc.Mapping):
+ return NotImplemented
+
+ rv = self.copy()
+ rv.update(other)
+ return rv
+
+ def __ior__(
+ self,
+ other: (
+ cabc.Mapping[str, t.Any | list[t.Any] | tuple[t.Any, ...] | cabc.Set[t.Any]]
+ | cabc.Iterable[tuple[str, t.Any]]
+ ),
+ ) -> te.Self:
+ if not isinstance(other, (cabc.Mapping, cabc.Iterable)):
+ return NotImplemented
+
+ self.update(other)
+ return self
+
+ def to_wsgi_list(self) -> list[tuple[str, str]]:
+ """Convert the headers into a list suitable for WSGI.
+
+ :return: list
+ """
+ return list(self)
+
+ def copy(self) -> te.Self:
+ return self.__class__(self._list)
+
+ def __copy__(self) -> te.Self:
+ return self.copy()
+
+ def __str__(self) -> str:
+ """Returns formatted headers suitable for HTTP transmission."""
+ strs = []
+ for key, value in self.to_wsgi_list():
+ strs.append(f"{key}: {value}")
+ strs.append("\r\n")
+ return "\r\n".join(strs)
+
+ def __repr__(self) -> str:
+ return f"{type(self).__name__}({list(self)!r})"
+
+
+def _options_header_vkw(value: str, kw: dict[str, t.Any]) -> str:
+ return http.dump_options_header(
+ value, {k.replace("_", "-"): v for k, v in kw.items()}
+ )
+
+
+_newline_re = re.compile(r"[\r\n]")
+
+
+def _str_header_value(value: t.Any) -> str:
+ if not isinstance(value, str):
+ value = str(value)
+
+ if _newline_re.search(value) is not None:
+ raise ValueError("Header values must not contain newline characters.")
+
+ return value # type: ignore[no-any-return]
+
+
+class EnvironHeaders(ImmutableHeadersMixin, Headers): # type: ignore[misc]
+ """Read only version of the headers from a WSGI environment. This
+ provides the same interface as `Headers` and is constructed from
+ a WSGI environment.
+ From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a
+ subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will
+ render a page for a ``400 BAD REQUEST`` if caught in a catch-all for
+ HTTP exceptions.
+ """
+
+ def __init__(self, environ: WSGIEnvironment) -> None:
+ super().__init__()
+ self.environ = environ
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, EnvironHeaders):
+ return NotImplemented
+
+ return self.environ is other.environ
+
+ __hash__ = None
+
+ def __getitem__(self, key: str) -> str: # type: ignore[override]
+ return self._get_key(key)
+
+ def _get_key(self, key: str) -> str:
+ if not isinstance(key, str):
+ raise BadRequestKeyError(key)
+
+ key = key.upper().replace("-", "_")
+
+ if key in {"CONTENT_TYPE", "CONTENT_LENGTH"}:
+ return self.environ[key] # type: ignore[no-any-return]
+
+ return self.environ[f"HTTP_{key}"] # type: ignore[no-any-return]
+
+ def __len__(self) -> int:
+ return sum(1 for _ in self)
+
+ def __iter__(self) -> cabc.Iterator[tuple[str, str]]:
+ for key, value in self.environ.items():
+ if key.startswith("HTTP_") and key not in {
+ "HTTP_CONTENT_TYPE",
+ "HTTP_CONTENT_LENGTH",
+ }:
+ yield key[5:].replace("_", "-").title(), value
+ elif key in {"CONTENT_TYPE", "CONTENT_LENGTH"} and value:
+ yield key.replace("_", "-").title(), value
+
+ def copy(self) -> t.NoReturn:
+ raise TypeError(f"cannot create {type(self).__name__!r} copies")
+
+ def __or__(self, other: t.Any) -> t.NoReturn:
+ raise TypeError(f"cannot create {type(self).__name__!r} copies")
+
+
+# circular dependencies
+from .. import http # noqa: E402
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/mixins.py b/venv/Lib/site-packages/werkzeug/datastructures/mixins.py
new file mode 100644
index 0000000..8e00bda
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/datastructures/mixins.py
@@ -0,0 +1,317 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import typing as t
+from functools import update_wrapper
+from itertools import repeat
+
+from .._internal import _missing
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+K = t.TypeVar("K")
+V = t.TypeVar("V")
+T = t.TypeVar("T")
+F = t.TypeVar("F", bound=cabc.Callable[..., t.Any])
+
+
+def _immutable_error(self: t.Any) -> t.NoReturn:
+ raise TypeError(f"{type(self).__name__!r} objects are immutable")
+
+
+class ImmutableListMixin:
+ """Makes a :class:`list` immutable.
+
+ .. versionadded:: 0.5
+
+ :private:
+ """
+
+ _hash_cache: int | None = None
+
+ def __hash__(self) -> int:
+ if self._hash_cache is not None:
+ return self._hash_cache
+ rv = self._hash_cache = hash(tuple(self)) # type: ignore[arg-type]
+ return rv
+
+ def __reduce_ex__(self, protocol: t.SupportsIndex) -> t.Any:
+ return type(self), (list(self),) # type: ignore[call-overload]
+
+ def __delitem__(self, key: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def __iadd__(self, other: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def __imul__(self, other: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def __setitem__(self, key: t.Any, value: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def append(self, item: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def remove(self, item: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def extend(self, iterable: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def insert(self, pos: t.Any, value: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def pop(self, index: t.Any = -1) -> t.NoReturn:
+ _immutable_error(self)
+
+ def reverse(self: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def sort(self, key: t.Any = None, reverse: t.Any = False) -> t.NoReturn:
+ _immutable_error(self)
+
+
+class ImmutableDictMixin(t.Generic[K, V]):
+ """Makes a :class:`dict` immutable.
+
+ .. versionchanged:: 3.1
+ Disallow ``|=`` operator.
+
+ .. versionadded:: 0.5
+
+ :private:
+ """
+
+ _hash_cache: int | None = None
+
+ @classmethod
+ @t.overload
+ def fromkeys(
+ cls, keys: cabc.Iterable[K], value: None
+ ) -> ImmutableDictMixin[K, t.Any | None]: ...
+ @classmethod
+ @t.overload
+ def fromkeys(cls, keys: cabc.Iterable[K], value: V) -> ImmutableDictMixin[K, V]: ...
+ @classmethod
+ def fromkeys(
+ cls, keys: cabc.Iterable[K], value: V | None = None
+ ) -> ImmutableDictMixin[K, t.Any | None] | ImmutableDictMixin[K, V]:
+ instance = super().__new__(cls)
+ instance.__init__(zip(keys, repeat(value))) # type: ignore[misc]
+ return instance
+
+ def __reduce_ex__(self, protocol: t.SupportsIndex) -> t.Any:
+ return type(self), (dict(self),) # type: ignore[call-overload]
+
+ def _iter_hashitems(self) -> t.Iterable[t.Any]:
+ return self.items() # type: ignore[attr-defined,no-any-return]
+
+ def __hash__(self) -> int:
+ if self._hash_cache is not None:
+ return self._hash_cache
+ rv = self._hash_cache = hash(frozenset(self._iter_hashitems()))
+ return rv
+
+ def setdefault(self, key: t.Any, default: t.Any = None) -> t.NoReturn:
+ _immutable_error(self)
+
+ def update(self, arg: t.Any, /, **kwargs: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def __ior__(self, other: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def pop(self, key: t.Any, default: t.Any = None) -> t.NoReturn:
+ _immutable_error(self)
+
+ def popitem(self) -> t.NoReturn:
+ _immutable_error(self)
+
+ def __setitem__(self, key: t.Any, value: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def __delitem__(self, key: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def clear(self) -> t.NoReturn:
+ _immutable_error(self)
+
+
+class ImmutableMultiDictMixin(ImmutableDictMixin[K, V]):
+ """Makes a :class:`MultiDict` immutable.
+
+ .. versionadded:: 0.5
+
+ :private:
+ """
+
+ def __reduce_ex__(self, protocol: t.SupportsIndex) -> t.Any:
+ return type(self), (list(self.items(multi=True)),) # type: ignore[attr-defined]
+
+ def _iter_hashitems(self) -> t.Iterable[t.Any]:
+ return self.items(multi=True) # type: ignore[attr-defined,no-any-return]
+
+ def add(self, key: t.Any, value: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def popitemlist(self) -> t.NoReturn:
+ _immutable_error(self)
+
+ def poplist(self, key: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def setlist(self, key: t.Any, new_list: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def setlistdefault(self, key: t.Any, default_list: t.Any = None) -> t.NoReturn:
+ _immutable_error(self)
+
+
+class ImmutableHeadersMixin:
+ """Makes a :class:`Headers` immutable. We do not mark them as
+ hashable though since the only usecase for this datastructure
+ in Werkzeug is a view on a mutable structure.
+
+ .. versionchanged:: 3.1
+ Disallow ``|=`` operator.
+
+ .. versionadded:: 0.5
+
+ :private:
+ """
+
+ def __delitem__(self, key: t.Any, **kwargs: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def __setitem__(self, key: t.Any, value: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def set(self, key: t.Any, value: t.Any, /, **kwargs: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def setlist(self, key: t.Any, values: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def add(self, key: t.Any, value: t.Any, /, **kwargs: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def add_header(self, key: t.Any, value: t.Any, /, **kwargs: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def remove(self, key: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def extend(self, arg: t.Any, /, **kwargs: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def update(self, arg: t.Any, /, **kwargs: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def __ior__(self, other: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def insert(self, pos: t.Any, value: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def pop(self, key: t.Any = None, default: t.Any = _missing) -> t.NoReturn:
+ _immutable_error(self)
+
+ def popitem(self) -> t.NoReturn:
+ _immutable_error(self)
+
+ def setdefault(self, key: t.Any, default: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+ def setlistdefault(self, key: t.Any, default: t.Any) -> t.NoReturn:
+ _immutable_error(self)
+
+
+def _always_update(f: F) -> F:
+ def wrapper(
+ self: UpdateDictMixin[t.Any, t.Any], /, *args: t.Any, **kwargs: t.Any
+ ) -> t.Any:
+ rv = f(self, *args, **kwargs)
+
+ if self.on_update is not None:
+ self.on_update(self)
+
+ return rv
+
+ return update_wrapper(wrapper, f) # type: ignore[return-value]
+
+
+class UpdateDictMixin(dict[K, V]):
+ """Makes dicts call `self.on_update` on modifications.
+
+ .. versionchanged:: 3.1
+ Implement ``|=`` operator.
+
+ .. versionadded:: 0.5
+
+ :private:
+ """
+
+ on_update: cabc.Callable[[te.Self], None] | None = None
+
+ def setdefault(self: te.Self, key: K, default: V | None = None) -> V:
+ modified = key not in self
+ rv = super().setdefault(key, default) # type: ignore[arg-type]
+ if modified and self.on_update is not None:
+ self.on_update(self)
+ return rv
+
+ @t.overload
+ def pop(self: te.Self, key: K) -> V: ...
+ @t.overload
+ def pop(self: te.Self, key: K, default: V) -> V: ...
+ @t.overload
+ def pop(self: te.Self, key: K, default: T) -> T: ...
+ def pop(
+ self: te.Self,
+ key: K,
+ default: V | T = _missing, # type: ignore[assignment]
+ ) -> V | T:
+ modified = key in self
+ if default is _missing:
+ rv: V | T = super().pop(key)
+ else:
+ rv = super().pop(key, default)
+ if modified and self.on_update is not None:
+ self.on_update(self)
+ return rv
+
+ @_always_update
+ def __setitem__(self, key: K, value: V) -> None:
+ super().__setitem__(key, value)
+
+ @_always_update
+ def __delitem__(self, key: K) -> None:
+ super().__delitem__(key)
+
+ @_always_update
+ def clear(self) -> None:
+ super().clear()
+
+ @_always_update
+ def popitem(self) -> tuple[K, V]:
+ return super().popitem()
+
+ @_always_update
+ def update( # type: ignore[override]
+ self,
+ arg: cabc.Mapping[K, V] | cabc.Iterable[tuple[K, V]] | None = None,
+ /,
+ **kwargs: V,
+ ) -> None:
+ if arg is None:
+ super().update(**kwargs) # type: ignore[call-overload]
+ else:
+ super().update(arg, **kwargs)
+
+ @_always_update
+ def __ior__( # type: ignore[override]
+ self, other: cabc.Mapping[K, V] | cabc.Iterable[tuple[K, V]]
+ ) -> te.Self:
+ return super().__ior__(other)
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/range.py b/venv/Lib/site-packages/werkzeug/datastructures/range.py
new file mode 100644
index 0000000..72f863d
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/datastructures/range.py
@@ -0,0 +1,214 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import typing as t
+from datetime import datetime
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+T = t.TypeVar("T")
+
+
+class IfRange:
+ """Very simple object that represents the `If-Range` header in parsed
+ form. It will either have neither a etag or date or one of either but
+ never both.
+
+ .. versionadded:: 0.7
+ """
+
+ def __init__(self, etag: str | None = None, date: datetime | None = None):
+ #: The etag parsed and unquoted. Ranges always operate on strong
+ #: etags so the weakness information is not necessary.
+ self.etag = etag
+ #: The date in parsed format or `None`.
+ self.date = date
+
+ def to_header(self) -> str:
+ """Converts the object back into an HTTP header."""
+ if self.date is not None:
+ return http.http_date(self.date)
+ if self.etag is not None:
+ return http.quote_etag(self.etag)
+ return ""
+
+ def __str__(self) -> str:
+ return self.to_header()
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} {str(self)!r}>"
+
+
+class Range:
+ """Represents a ``Range`` header. All methods only support only
+ bytes as the unit. Stores a list of ranges if given, but the methods
+ only work if only one range is provided.
+
+ :raise ValueError: If the ranges provided are invalid.
+
+ .. versionchanged:: 0.15
+ The ranges passed in are validated.
+
+ .. versionadded:: 0.7
+ """
+
+ def __init__(
+ self, units: str, ranges: cabc.Sequence[tuple[int, int | None]]
+ ) -> None:
+ #: The units of this range. Usually "bytes".
+ self.units = units
+ #: A list of ``(begin, end)`` tuples for the range header provided.
+ #: The ranges are non-inclusive.
+ self.ranges = ranges
+
+ for start, end in ranges:
+ if start is None or (end is not None and (start < 0 or start >= end)):
+ raise ValueError(f"{(start, end)} is not a valid range.")
+
+ def range_for_length(self, length: int | None) -> tuple[int, int] | None:
+ """If the range is for bytes, the length is not None and there is
+ exactly one range and it is satisfiable it returns a ``(start, stop)``
+ tuple, otherwise `None`.
+ """
+ if self.units != "bytes" or length is None or len(self.ranges) != 1:
+ return None
+ start, end = self.ranges[0]
+ if end is None:
+ end = length
+ if start < 0:
+ start += length
+ if http.is_byte_range_valid(start, end, length):
+ return start, min(end, length)
+ return None
+
+ def make_content_range(self, length: int | None) -> ContentRange | None:
+ """Creates a :class:`~werkzeug.datastructures.ContentRange` object
+ from the current range and given content length.
+ """
+ rng = self.range_for_length(length)
+ if rng is not None:
+ return ContentRange(self.units, rng[0], rng[1], length)
+ return None
+
+ def to_header(self) -> str:
+ """Converts the object back into an HTTP header."""
+ ranges = []
+ for begin, end in self.ranges:
+ if end is None:
+ ranges.append(f"{begin}-" if begin >= 0 else str(begin))
+ else:
+ ranges.append(f"{begin}-{end - 1}")
+ return f"{self.units}={','.join(ranges)}"
+
+ def to_content_range_header(self, length: int | None) -> str | None:
+ """Converts the object into `Content-Range` HTTP header,
+ based on given length
+ """
+ range = self.range_for_length(length)
+ if range is not None:
+ return f"{self.units} {range[0]}-{range[1] - 1}/{length}"
+ return None
+
+ def __str__(self) -> str:
+ return self.to_header()
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} {str(self)!r}>"
+
+
+class _CallbackProperty(t.Generic[T]):
+ def __set_name__(self, owner: type[ContentRange], name: str) -> None:
+ self.attr = f"_{name}"
+
+ @t.overload
+ def __get__(self, instance: None, owner: None) -> te.Self: ...
+ @t.overload
+ def __get__(self, instance: ContentRange, owner: type[ContentRange]) -> T: ...
+ def __get__(
+ self, instance: ContentRange | None, owner: type[ContentRange] | None
+ ) -> te.Self | T:
+ if instance is None:
+ return self
+
+ return instance.__dict__[self.attr] # type: ignore[no-any-return]
+
+ def __set__(self, instance: ContentRange, value: T) -> None:
+ instance.__dict__[self.attr] = value
+
+ if instance.on_update is not None:
+ instance.on_update(instance)
+
+
+class ContentRange:
+ """Represents the content range header.
+
+ .. versionadded:: 0.7
+ """
+
+ def __init__(
+ self,
+ units: str | None,
+ start: int | None,
+ stop: int | None,
+ length: int | None = None,
+ on_update: cabc.Callable[[ContentRange], None] | None = None,
+ ) -> None:
+ self.on_update = on_update
+ self.set(start, stop, length, units)
+
+ #: The units to use, usually "bytes"
+ units: str | None = _CallbackProperty() # type: ignore[assignment]
+ #: The start point of the range or `None`.
+ start: int | None = _CallbackProperty() # type: ignore[assignment]
+ #: The stop point of the range (non-inclusive) or `None`. Can only be
+ #: `None` if also start is `None`.
+ stop: int | None = _CallbackProperty() # type: ignore[assignment]
+ #: The length of the range or `None`.
+ length: int | None = _CallbackProperty() # type: ignore[assignment]
+
+ def set(
+ self,
+ start: int | None,
+ stop: int | None,
+ length: int | None = None,
+ units: str | None = "bytes",
+ ) -> None:
+ """Simple method to update the ranges."""
+ assert http.is_byte_range_valid(start, stop, length), "Bad range provided"
+ self._units: str | None = units
+ self._start: int | None = start
+ self._stop: int | None = stop
+ self._length: int | None = length
+ if self.on_update is not None:
+ self.on_update(self)
+
+ def unset(self) -> None:
+ """Sets the units to `None` which indicates that the header should
+ no longer be used.
+ """
+ self.set(None, None, units=None)
+
+ def to_header(self) -> str:
+ if self._units is None:
+ return ""
+ if self._length is None:
+ length: str | int = "*"
+ else:
+ length = self._length
+ if self._start is None:
+ return f"{self._units} */{length}"
+ return f"{self._units} {self._start}-{self._stop - 1}/{length}" # type: ignore[operator]
+
+ def __bool__(self) -> bool:
+ return self._units is not None
+
+ def __str__(self) -> str:
+ return self.to_header()
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} {str(self)!r}>"
+
+
+# circular dependencies
+from .. import http # noqa: E402
diff --git a/venv/Lib/site-packages/werkzeug/datastructures/structures.py b/venv/Lib/site-packages/werkzeug/datastructures/structures.py
new file mode 100644
index 0000000..8dc6437
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/datastructures/structures.py
@@ -0,0 +1,1239 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import typing as t
+from copy import deepcopy
+
+from .. import exceptions
+from .._internal import _missing
+from .mixins import ImmutableDictMixin
+from .mixins import ImmutableListMixin
+from .mixins import ImmutableMultiDictMixin
+from .mixins import UpdateDictMixin
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+K = t.TypeVar("K")
+V = t.TypeVar("V")
+T = t.TypeVar("T")
+
+
+def iter_multi_items(
+ mapping: (
+ MultiDict[K, V]
+ | cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]]
+ | cabc.Iterable[tuple[K, V]]
+ ),
+) -> cabc.Iterator[tuple[K, V]]:
+ """Iterates over the items of a mapping yielding keys and values
+ without dropping any from more complex structures.
+ """
+ if isinstance(mapping, MultiDict):
+ yield from mapping.items(multi=True)
+ elif isinstance(mapping, cabc.Mapping):
+ for key, value in mapping.items():
+ if isinstance(value, (list, tuple, set)):
+ for v in value:
+ yield key, v
+ else:
+ yield key, value
+ else:
+ yield from mapping
+
+
+class ImmutableList(ImmutableListMixin, list[V]): # type: ignore[misc]
+ """An immutable :class:`list`.
+
+ .. versionadded:: 0.5
+
+ :private:
+ """
+
+ def __repr__(self) -> str:
+ return f"{type(self).__name__}({list.__repr__(self)})"
+
+
+class TypeConversionDict(dict[K, V]):
+ """Works like a regular dict but the :meth:`get` method can perform
+ type conversions. :class:`MultiDict` and :class:`CombinedMultiDict`
+ are subclasses of this class and provide the same feature.
+
+ .. versionadded:: 0.5
+ """
+
+ @t.overload # type: ignore[override]
+ def get(self, key: K) -> V | None: ...
+ @t.overload
+ def get(self, key: K, default: V) -> V: ...
+ @t.overload
+ def get(self, key: K, default: T) -> V | T: ...
+ @t.overload
+ def get(self, key: str, type: cabc.Callable[[V], T]) -> T | None: ...
+ @t.overload
+ def get(self, key: str, default: T, type: cabc.Callable[[V], T]) -> T: ...
+ def get( # type: ignore[misc]
+ self,
+ key: K,
+ default: V | T | None = None,
+ type: cabc.Callable[[V], T] | None = None,
+ ) -> V | T | None:
+ """Return the default value if the requested data doesn't exist.
+ If `type` is provided and is a callable it should convert the value,
+ return it or raise a :exc:`ValueError` if that is not possible. In
+ this case the function will return the default as if the value was not
+ found:
+
+ >>> d = TypeConversionDict(foo='42', bar='blub')
+ >>> d.get('foo', type=int)
+ 42
+ >>> d.get('bar', -1, type=int)
+ -1
+
+ :param key: The key to be looked up.
+ :param default: The default value to be returned if the key can't
+ be looked up. If not further specified `None` is
+ returned.
+ :param type: A callable that is used to cast the value in the
+ :class:`MultiDict`. If a :exc:`ValueError` or a
+ :exc:`TypeError` is raised by this callable the default
+ value is returned.
+
+ .. versionchanged:: 3.0.2
+ Returns the default value on :exc:`TypeError`, too.
+ """
+ try:
+ rv = self[key]
+ except KeyError:
+ return default
+
+ if type is None:
+ return rv
+
+ try:
+ return type(rv)
+ except (ValueError, TypeError):
+ return default
+
+
+class ImmutableTypeConversionDict(ImmutableDictMixin[K, V], TypeConversionDict[K, V]): # type: ignore[misc]
+ """Works like a :class:`TypeConversionDict` but does not support
+ modifications.
+
+ .. versionadded:: 0.5
+ """
+
+ def copy(self) -> TypeConversionDict[K, V]:
+ """Return a shallow mutable copy of this object. Keep in mind that
+ the standard library's :func:`copy` function is a no-op for this class
+ like for any other python immutable type (eg: :class:`tuple`).
+ """
+ return TypeConversionDict(self)
+
+ def __copy__(self) -> te.Self:
+ return self
+
+
+class MultiDict(TypeConversionDict[K, V]):
+ """A :class:`MultiDict` is a dictionary subclass customized to deal with
+ multiple values for the same key which is for example used by the parsing
+ functions in the wrappers. This is necessary because some HTML form
+ elements pass multiple values for the same key.
+
+ :class:`MultiDict` implements all standard dictionary methods.
+ Internally, it saves all values for a key as a list, but the standard dict
+ access methods will only return the first value for a key. If you want to
+ gain access to the other values, too, you have to use the `list` methods as
+ explained below.
+
+ Basic Usage:
+
+ >>> d = MultiDict([('a', 'b'), ('a', 'c')])
+ >>> d
+ MultiDict([('a', 'b'), ('a', 'c')])
+ >>> d['a']
+ 'b'
+ >>> d.getlist('a')
+ ['b', 'c']
+ >>> 'a' in d
+ True
+
+ It behaves like a normal dict thus all dict functions will only return the
+ first value when multiple values for one key are found.
+
+ From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a
+ subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will
+ render a page for a ``400 BAD REQUEST`` if caught in a catch-all for HTTP
+ exceptions.
+
+ A :class:`MultiDict` can be constructed from an iterable of
+ ``(key, value)`` tuples, a dict, a :class:`MultiDict` or from Werkzeug 0.2
+ onwards some keyword parameters.
+
+ :param mapping: the initial value for the :class:`MultiDict`. Either a
+ regular dict, an iterable of ``(key, value)`` tuples
+ or `None`.
+
+ .. versionchanged:: 3.1
+ Implement ``|`` and ``|=`` operators.
+ """
+
+ def __init__(
+ self,
+ mapping: (
+ MultiDict[K, V]
+ | cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]]
+ | cabc.Iterable[tuple[K, V]]
+ | None
+ ) = None,
+ ) -> None:
+ if mapping is None:
+ super().__init__()
+ elif isinstance(mapping, MultiDict):
+ super().__init__((k, vs[:]) for k, vs in mapping.lists()) # type: ignore[misc]
+ elif isinstance(mapping, cabc.Mapping):
+ tmp = {}
+ for key, value in mapping.items():
+ if isinstance(value, (list, tuple, set)):
+ value = list(value)
+
+ if not value:
+ continue
+ else:
+ value = [value]
+ tmp[key] = value
+ super().__init__(tmp) # type: ignore[arg-type]
+ else:
+ tmp = {}
+ for key, value in mapping:
+ tmp.setdefault(key, []).append(value)
+ super().__init__(tmp) # type: ignore[arg-type]
+
+ def __getstate__(self) -> t.Any:
+ return dict(self.lists())
+
+ def __setstate__(self, value: t.Any) -> None:
+ super().clear()
+ super().update(value)
+
+ def __iter__(self) -> cabc.Iterator[K]:
+ # https://github.com/python/cpython/issues/87412
+ # If __iter__ is not overridden, Python uses a fast path for dict(md),
+ # taking the data directly and getting lists of values, rather than
+ # calling __getitem__ and getting only the first value.
+ return super().__iter__()
+
+ def __getitem__(self, key: K) -> V:
+ """Return the first data value for this key;
+ raises KeyError if not found.
+
+ :param key: The key to be looked up.
+ :raise KeyError: if the key does not exist.
+ """
+
+ if key in self:
+ lst = super().__getitem__(key)
+ if len(lst) > 0: # type: ignore[arg-type]
+ return lst[0] # type: ignore[index,no-any-return]
+ raise exceptions.BadRequestKeyError(key)
+
+ def __setitem__(self, key: K, value: V) -> None:
+ """Like :meth:`add` but removes an existing key first.
+
+ :param key: the key for the value.
+ :param value: the value to set.
+ """
+ super().__setitem__(key, [value]) # type: ignore[assignment]
+
+ def add(self, key: K, value: V) -> None:
+ """Adds a new value for the key.
+
+ .. versionadded:: 0.6
+
+ :param key: the key for the value.
+ :param value: the value to add.
+ """
+ super().setdefault(key, []).append(value) # type: ignore[arg-type,attr-defined]
+
+ @t.overload
+ def getlist(self, key: K) -> list[V]: ...
+ @t.overload
+ def getlist(self, key: K, type: cabc.Callable[[V], T]) -> list[T]: ...
+ def getlist(
+ self, key: K, type: cabc.Callable[[V], T] | None = None
+ ) -> list[V] | list[T]:
+ """Return the list of items for a given key. If that key is not in the
+ `MultiDict`, the return value will be an empty list. Just like `get`,
+ `getlist` accepts a `type` parameter. All items will be converted
+ with the callable defined there.
+
+ :param key: The key to be looked up.
+ :param type: Callable to convert each value. If a ``ValueError`` or
+ ``TypeError`` is raised, the value is omitted.
+ :return: a :class:`list` of all the values for the key.
+
+ .. versionchanged:: 3.1
+ Catches ``TypeError`` in addition to ``ValueError``.
+ """
+ try:
+ rv: list[V] = super().__getitem__(key) # type: ignore[assignment]
+ except KeyError:
+ return []
+ if type is None:
+ return list(rv)
+ result = []
+ for item in rv:
+ try:
+ result.append(type(item))
+ except (ValueError, TypeError):
+ pass
+ return result
+
+ def setlist(self, key: K, new_list: cabc.Iterable[V]) -> None:
+ """Remove the old values for a key and add new ones. Note that the list
+ you pass the values in will be shallow-copied before it is inserted in
+ the dictionary.
+
+ >>> d = MultiDict()
+ >>> d.setlist('foo', ['1', '2'])
+ >>> d['foo']
+ '1'
+ >>> d.getlist('foo')
+ ['1', '2']
+
+ :param key: The key for which the values are set.
+ :param new_list: An iterable with the new values for the key. Old values
+ are removed first.
+ """
+ super().__setitem__(key, list(new_list)) # type: ignore[assignment]
+
+ @t.overload
+ def setdefault(self, key: K) -> None: ...
+ @t.overload
+ def setdefault(self, key: K, default: V) -> V: ...
+ def setdefault(self, key: K, default: V | None = None) -> V | None:
+ """Returns the value for the key if it is in the dict, otherwise it
+ returns `default` and sets that value for `key`.
+
+ :param key: The key to be looked up.
+ :param default: The default value to be returned if the key is not
+ in the dict. If not further specified it's `None`.
+ """
+ if key not in self:
+ self[key] = default # type: ignore[assignment]
+
+ return self[key]
+
+ def setlistdefault(
+ self, key: K, default_list: cabc.Iterable[V] | None = None
+ ) -> list[V]:
+ """Like `setdefault` but sets multiple values. The list returned
+ is not a copy, but the list that is actually used internally. This
+ means that you can put new values into the dict by appending items
+ to the list:
+
+ >>> d = MultiDict({"foo": 1})
+ >>> d.setlistdefault("foo").extend([2, 3])
+ >>> d.getlist("foo")
+ [1, 2, 3]
+
+ :param key: The key to be looked up.
+ :param default_list: An iterable of default values. It is either copied
+ (in case it was a list) or converted into a list
+ before returned.
+ :return: a :class:`list`
+ """
+ if key not in self:
+ super().__setitem__(key, list(default_list or ())) # type: ignore[assignment]
+
+ return super().__getitem__(key) # type: ignore[return-value]
+
+ def items(self, multi: bool = False) -> cabc.Iterable[tuple[K, V]]: # type: ignore[override]
+ """Return an iterator of ``(key, value)`` pairs.
+
+ :param multi: If set to `True` the iterator returned will have a pair
+ for each value of each key. Otherwise it will only
+ contain pairs for the first value of each key.
+ """
+ values: list[V]
+
+ for key, values in super().items(): # type: ignore[assignment]
+ if multi:
+ for value in values:
+ yield key, value
+ else:
+ yield key, values[0]
+
+ def lists(self) -> cabc.Iterable[tuple[K, list[V]]]:
+ """Return a iterator of ``(key, values)`` pairs, where values is the list
+ of all values associated with the key."""
+ values: list[V]
+
+ for key, values in super().items(): # type: ignore[assignment]
+ yield key, list(values)
+
+ def values(self) -> cabc.Iterable[V]: # type: ignore[override]
+ """Returns an iterator of the first value on every key's value list."""
+ values: list[V]
+
+ for values in super().values(): # type: ignore[assignment]
+ yield values[0]
+
+ def listvalues(self) -> cabc.Iterable[list[V]]:
+ """Return an iterator of all values associated with a key. Zipping
+ :meth:`keys` and this is the same as calling :meth:`lists`:
+
+ >>> d = MultiDict({"foo": [1, 2, 3]})
+ >>> zip(d.keys(), d.listvalues()) == d.lists()
+ True
+ """
+ return super().values() # type: ignore[return-value]
+
+ def copy(self) -> te.Self:
+ """Return a shallow copy of this object."""
+ return self.__class__(self)
+
+ def deepcopy(self, memo: t.Any = None) -> te.Self:
+ """Return a deep copy of this object."""
+ return self.__class__(deepcopy(self.to_dict(flat=False), memo))
+
+ @t.overload
+ def to_dict(self, flat: t.Literal[True] = ...) -> dict[K, V]: ...
+ @t.overload
+ def to_dict(self, flat: t.Literal[False]) -> dict[K, list[V]]: ...
+ def to_dict(self, flat: bool = True) -> dict[K, V] | dict[K, list[V]]:
+ """Return the contents as regular dict. If `flat` is `True` the
+ returned dict will only have the first item present, if `flat` is
+ `False` all values will be returned as lists.
+
+ :param flat: If set to `False` the dict returned will have lists
+ with all the values in it. Otherwise it will only
+ contain the first value for each key.
+ :return: a :class:`dict`
+ """
+ if flat:
+ return dict(self.items())
+ return dict(self.lists())
+
+ def update( # type: ignore[override]
+ self,
+ mapping: (
+ MultiDict[K, V]
+ | cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]]
+ | cabc.Iterable[tuple[K, V]]
+ ),
+ ) -> None:
+ """update() extends rather than replaces existing key lists:
+
+ >>> a = MultiDict({'x': 1})
+ >>> b = MultiDict({'x': 2, 'y': 3})
+ >>> a.update(b)
+ >>> a
+ MultiDict([('y', 3), ('x', 1), ('x', 2)])
+
+ If the value list for a key in ``other_dict`` is empty, no new values
+ will be added to the dict and the key will not be created:
+
+ >>> x = {'empty_list': []}
+ >>> y = MultiDict()
+ >>> y.update(x)
+ >>> y
+ MultiDict([])
+ """
+ for key, value in iter_multi_items(mapping):
+ self.add(key, value)
+
+ def __or__( # type: ignore[override]
+ self, other: cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]]
+ ) -> MultiDict[K, V]:
+ if not isinstance(other, cabc.Mapping):
+ return NotImplemented
+
+ rv = self.copy()
+ rv.update(other)
+ return rv
+
+ def __ior__( # type: ignore[override]
+ self,
+ other: (
+ cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]]
+ | cabc.Iterable[tuple[K, V]]
+ ),
+ ) -> te.Self:
+ if not isinstance(other, (cabc.Mapping, cabc.Iterable)):
+ return NotImplemented
+
+ self.update(other)
+ return self
+
+ @t.overload
+ def pop(self, key: K) -> V: ...
+ @t.overload
+ def pop(self, key: K, default: V) -> V: ...
+ @t.overload
+ def pop(self, key: K, default: T) -> V | T: ...
+ def pop(
+ self,
+ key: K,
+ default: V | T = _missing, # type: ignore[assignment]
+ ) -> V | T:
+ """Pop the first item for a list on the dict. Afterwards the
+ key is removed from the dict, so additional values are discarded:
+
+ >>> d = MultiDict({"foo": [1, 2, 3]})
+ >>> d.pop("foo")
+ 1
+ >>> "foo" in d
+ False
+
+ :param key: the key to pop.
+ :param default: if provided the value to return if the key was
+ not in the dictionary.
+ """
+ lst: list[V]
+
+ try:
+ lst = super().pop(key) # type: ignore[assignment]
+
+ if len(lst) == 0:
+ raise exceptions.BadRequestKeyError(key)
+
+ return lst[0]
+ except KeyError:
+ if default is not _missing:
+ return default
+
+ raise exceptions.BadRequestKeyError(key) from None
+
+ def popitem(self) -> tuple[K, V]:
+ """Pop an item from the dict."""
+ item: tuple[K, list[V]]
+
+ try:
+ item = super().popitem() # type: ignore[assignment]
+
+ if len(item[1]) == 0:
+ raise exceptions.BadRequestKeyError(item[0])
+
+ return item[0], item[1][0]
+ except KeyError as e:
+ raise exceptions.BadRequestKeyError(e.args[0]) from None
+
+ def poplist(self, key: K) -> list[V]:
+ """Pop the list for a key from the dict. If the key is not in the dict
+ an empty list is returned.
+
+ .. versionchanged:: 0.5
+ If the key does no longer exist a list is returned instead of
+ raising an error.
+ """
+ return super().pop(key, []) # type: ignore[return-value]
+
+ def popitemlist(self) -> tuple[K, list[V]]:
+ """Pop a ``(key, list)`` tuple from the dict."""
+ try:
+ return super().popitem() # type: ignore[return-value]
+ except KeyError as e:
+ raise exceptions.BadRequestKeyError(e.args[0]) from None
+
+ def __copy__(self) -> te.Self:
+ return self.copy()
+
+ def __deepcopy__(self, memo: t.Any) -> te.Self:
+ return self.deepcopy(memo=memo)
+
+ def __repr__(self) -> str:
+ return f"{type(self).__name__}({list(self.items(multi=True))!r})"
+
+
+class _omd_bucket(t.Generic[K, V]):
+ """Wraps values in the :class:`OrderedMultiDict`. This makes it
+ possible to keep an order over multiple different keys. It requires
+ a lot of extra memory and slows down access a lot, but makes it
+ possible to access elements in O(1) and iterate in O(n).
+ """
+
+ __slots__ = ("prev", "key", "value", "next")
+
+ def __init__(self, omd: _OrderedMultiDict[K, V], key: K, value: V) -> None:
+ self.prev: _omd_bucket[K, V] | None = omd._last_bucket
+ self.key: K = key
+ self.value: V = value
+ self.next: _omd_bucket[K, V] | None = None
+
+ if omd._first_bucket is None:
+ omd._first_bucket = self
+ if omd._last_bucket is not None:
+ omd._last_bucket.next = self
+ omd._last_bucket = self
+
+ def unlink(self, omd: _OrderedMultiDict[K, V]) -> None:
+ if self.prev:
+ self.prev.next = self.next
+ if self.next:
+ self.next.prev = self.prev
+ if omd._first_bucket is self:
+ omd._first_bucket = self.next
+ if omd._last_bucket is self:
+ omd._last_bucket = self.prev
+
+
+class _OrderedMultiDict(MultiDict[K, V]):
+ """Works like a regular :class:`MultiDict` but preserves the
+ order of the fields. To convert the ordered multi dict into a
+ list you can use the :meth:`items` method and pass it ``multi=True``.
+
+ In general an :class:`OrderedMultiDict` is an order of magnitude
+ slower than a :class:`MultiDict`.
+
+ .. admonition:: note
+
+ Due to a limitation in Python you cannot convert an ordered
+ multi dict into a regular dict by using ``dict(multidict)``.
+ Instead you have to use the :meth:`to_dict` method, otherwise
+ the internal bucket objects are exposed.
+
+ .. deprecated:: 3.1
+ Will be removed in Werkzeug 3.2. Use ``MultiDict`` instead.
+ """
+
+ def __init__(
+ self,
+ mapping: (
+ MultiDict[K, V]
+ | cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]]
+ | cabc.Iterable[tuple[K, V]]
+ | None
+ ) = None,
+ ) -> None:
+ import warnings
+
+ warnings.warn(
+ "'OrderedMultiDict' is deprecated and will be removed in Werkzeug"
+ " 3.2. Use 'MultiDict' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ super().__init__()
+ self._first_bucket: _omd_bucket[K, V] | None = None
+ self._last_bucket: _omd_bucket[K, V] | None = None
+ if mapping is not None:
+ self.update(mapping)
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, MultiDict):
+ return NotImplemented
+ if isinstance(other, _OrderedMultiDict):
+ iter1 = iter(self.items(multi=True))
+ iter2 = iter(other.items(multi=True))
+ try:
+ for k1, v1 in iter1:
+ k2, v2 = next(iter2)
+ if k1 != k2 or v1 != v2:
+ return False
+ except StopIteration:
+ return False
+ try:
+ next(iter2)
+ except StopIteration:
+ return True
+ return False
+ if len(self) != len(other):
+ return False
+ for key, values in self.lists():
+ if other.getlist(key) != values:
+ return False
+ return True
+
+ __hash__ = None # type: ignore[assignment]
+
+ def __reduce_ex__(self, protocol: t.SupportsIndex) -> t.Any:
+ return type(self), (list(self.items(multi=True)),)
+
+ def __getstate__(self) -> t.Any:
+ return list(self.items(multi=True))
+
+ def __setstate__(self, values: t.Any) -> None:
+ self.clear()
+
+ for key, value in values:
+ self.add(key, value)
+
+ def __getitem__(self, key: K) -> V:
+ if key in self:
+ return dict.__getitem__(self, key)[0].value # type: ignore[index,no-any-return]
+ raise exceptions.BadRequestKeyError(key)
+
+ def __setitem__(self, key: K, value: V) -> None:
+ self.poplist(key)
+ self.add(key, value)
+
+ def __delitem__(self, key: K) -> None:
+ self.pop(key)
+
+ def keys(self) -> cabc.Iterable[K]: # type: ignore[override]
+ return (key for key, _ in self.items())
+
+ def __iter__(self) -> cabc.Iterator[K]:
+ return iter(self.keys())
+
+ def values(self) -> cabc.Iterable[V]: # type: ignore[override]
+ return (value for key, value in self.items())
+
+ def items(self, multi: bool = False) -> cabc.Iterable[tuple[K, V]]: # type: ignore[override]
+ ptr = self._first_bucket
+ if multi:
+ while ptr is not None:
+ yield ptr.key, ptr.value
+ ptr = ptr.next
+ else:
+ returned_keys = set()
+ while ptr is not None:
+ if ptr.key not in returned_keys:
+ returned_keys.add(ptr.key)
+ yield ptr.key, ptr.value
+ ptr = ptr.next
+
+ def lists(self) -> cabc.Iterable[tuple[K, list[V]]]:
+ returned_keys = set()
+ ptr = self._first_bucket
+ while ptr is not None:
+ if ptr.key not in returned_keys:
+ yield ptr.key, self.getlist(ptr.key)
+ returned_keys.add(ptr.key)
+ ptr = ptr.next
+
+ def listvalues(self) -> cabc.Iterable[list[V]]:
+ for _key, values in self.lists():
+ yield values
+
+ def add(self, key: K, value: V) -> None:
+ dict.setdefault(self, key, []).append(_omd_bucket(self, key, value)) # type: ignore[misc]
+
+ @t.overload
+ def getlist(self, key: K) -> list[V]: ...
+ @t.overload
+ def getlist(self, key: K, type: cabc.Callable[[V], T]) -> list[T]: ...
+ def getlist(
+ self, key: K, type: cabc.Callable[[V], T] | None = None
+ ) -> list[V] | list[T]:
+ rv: list[_omd_bucket[K, V]]
+
+ try:
+ rv = dict.__getitem__(self, key) # type: ignore[index]
+ except KeyError:
+ return []
+ if type is None:
+ return [x.value for x in rv]
+ result = []
+ for item in rv:
+ try:
+ result.append(type(item.value))
+ except (ValueError, TypeError):
+ pass
+ return result
+
+ def setlist(self, key: K, new_list: cabc.Iterable[V]) -> None:
+ self.poplist(key)
+ for value in new_list:
+ self.add(key, value)
+
+ def setlistdefault(self, key: t.Any, default_list: t.Any = None) -> t.NoReturn:
+ raise TypeError("setlistdefault is unsupported for ordered multi dicts")
+
+ def update( # type: ignore[override]
+ self,
+ mapping: (
+ MultiDict[K, V]
+ | cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]]
+ | cabc.Iterable[tuple[K, V]]
+ ),
+ ) -> None:
+ for key, value in iter_multi_items(mapping):
+ self.add(key, value)
+
+ def poplist(self, key: K) -> list[V]:
+ buckets: cabc.Iterable[_omd_bucket[K, V]] = dict.pop(self, key, ()) # type: ignore[arg-type]
+ for bucket in buckets:
+ bucket.unlink(self)
+ return [x.value for x in buckets]
+
+ @t.overload
+ def pop(self, key: K) -> V: ...
+ @t.overload
+ def pop(self, key: K, default: V) -> V: ...
+ @t.overload
+ def pop(self, key: K, default: T) -> V | T: ...
+ def pop(
+ self,
+ key: K,
+ default: V | T = _missing, # type: ignore[assignment]
+ ) -> V | T:
+ buckets: list[_omd_bucket[K, V]]
+
+ try:
+ buckets = dict.pop(self, key) # type: ignore[arg-type]
+ except KeyError:
+ if default is not _missing:
+ return default
+
+ raise exceptions.BadRequestKeyError(key) from None
+
+ for bucket in buckets:
+ bucket.unlink(self)
+
+ return buckets[0].value
+
+ def popitem(self) -> tuple[K, V]:
+ key: K
+ buckets: list[_omd_bucket[K, V]]
+
+ try:
+ key, buckets = dict.popitem(self) # type: ignore[arg-type]
+ except KeyError as e:
+ raise exceptions.BadRequestKeyError(e.args[0]) from None
+
+ for bucket in buckets:
+ bucket.unlink(self)
+
+ return key, buckets[0].value
+
+ def popitemlist(self) -> tuple[K, list[V]]:
+ key: K
+ buckets: list[_omd_bucket[K, V]]
+
+ try:
+ key, buckets = dict.popitem(self) # type: ignore[arg-type]
+ except KeyError as e:
+ raise exceptions.BadRequestKeyError(e.args[0]) from None
+
+ for bucket in buckets:
+ bucket.unlink(self)
+
+ return key, [x.value for x in buckets]
+
+
+class CombinedMultiDict(ImmutableMultiDictMixin[K, V], MultiDict[K, V]): # type: ignore[misc]
+ """A read only :class:`MultiDict` that you can pass multiple :class:`MultiDict`
+ instances as sequence and it will combine the return values of all wrapped
+ dicts:
+
+ >>> from werkzeug.datastructures import CombinedMultiDict, MultiDict
+ >>> post = MultiDict([('foo', 'bar')])
+ >>> get = MultiDict([('blub', 'blah')])
+ >>> combined = CombinedMultiDict([get, post])
+ >>> combined['foo']
+ 'bar'
+ >>> combined['blub']
+ 'blah'
+
+ This works for all read operations and will raise a `TypeError` for
+ methods that usually change data which isn't possible.
+
+ From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a
+ subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will
+ render a page for a ``400 BAD REQUEST`` if caught in a catch-all for HTTP
+ exceptions.
+ """
+
+ def __reduce_ex__(self, protocol: t.SupportsIndex) -> t.Any:
+ return type(self), (self.dicts,)
+
+ def __init__(self, dicts: cabc.Iterable[MultiDict[K, V]] | None = None) -> None:
+ super().__init__()
+ self.dicts: list[MultiDict[K, V]] = list(dicts or ())
+
+ @classmethod
+ def fromkeys(cls, keys: t.Any, value: t.Any = None) -> t.NoReturn:
+ raise TypeError(f"cannot create {cls.__name__!r} instances by fromkeys")
+
+ def __getitem__(self, key: K) -> V:
+ for d in self.dicts:
+ if key in d:
+ return d[key]
+ raise exceptions.BadRequestKeyError(key)
+
+ @t.overload # type: ignore[override]
+ def get(self, key: K) -> V | None: ...
+ @t.overload
+ def get(self, key: K, default: V) -> V: ...
+ @t.overload
+ def get(self, key: K, default: T) -> V | T: ...
+ @t.overload
+ def get(self, key: str, type: cabc.Callable[[V], T]) -> T | None: ...
+ @t.overload
+ def get(self, key: str, default: T, type: cabc.Callable[[V], T]) -> T: ...
+ def get( # type: ignore[misc]
+ self,
+ key: K,
+ default: V | T | None = None,
+ type: cabc.Callable[[V], T] | None = None,
+ ) -> V | T | None:
+ for d in self.dicts:
+ if key in d:
+ if type is not None:
+ try:
+ return type(d[key])
+ except (ValueError, TypeError):
+ continue
+ return d[key]
+ return default
+
+ @t.overload
+ def getlist(self, key: K) -> list[V]: ...
+ @t.overload
+ def getlist(self, key: K, type: cabc.Callable[[V], T]) -> list[T]: ...
+ def getlist(
+ self, key: K, type: cabc.Callable[[V], T] | None = None
+ ) -> list[V] | list[T]:
+ rv = []
+ for d in self.dicts:
+ rv.extend(d.getlist(key, type)) # type: ignore[arg-type]
+ return rv
+
+ def _keys_impl(self) -> set[K]:
+ """This function exists so __len__ can be implemented more efficiently,
+ saving one list creation from an iterator.
+ """
+ return set(k for d in self.dicts for k in d)
+
+ def keys(self) -> cabc.Iterable[K]: # type: ignore[override]
+ return self._keys_impl()
+
+ def __iter__(self) -> cabc.Iterator[K]:
+ return iter(self._keys_impl())
+
+ @t.overload # type: ignore[override]
+ def items(self) -> cabc.Iterable[tuple[K, V]]: ...
+ @t.overload
+ def items(self, multi: t.Literal[True]) -> cabc.Iterable[tuple[K, list[V]]]: ...
+ def items(
+ self, multi: bool = False
+ ) -> cabc.Iterable[tuple[K, V]] | cabc.Iterable[tuple[K, list[V]]]:
+ found = set()
+ for d in self.dicts:
+ for key, value in d.items(multi):
+ if multi:
+ yield key, value
+ elif key not in found:
+ found.add(key)
+ yield key, value
+
+ def values(self) -> cabc.Iterable[V]: # type: ignore[override]
+ for _, value in self.items():
+ yield value
+
+ def lists(self) -> cabc.Iterable[tuple[K, list[V]]]:
+ rv: dict[K, list[V]] = {}
+ for d in self.dicts:
+ for key, values in d.lists():
+ rv.setdefault(key, []).extend(values)
+ return rv.items()
+
+ def listvalues(self) -> cabc.Iterable[list[V]]:
+ return (x[1] for x in self.lists())
+
+ def copy(self) -> MultiDict[K, V]: # type: ignore[override]
+ """Return a shallow mutable copy of this object.
+
+ This returns a :class:`MultiDict` representing the data at the
+ time of copying. The copy will no longer reflect changes to the
+ wrapped dicts.
+
+ .. versionchanged:: 0.15
+ Return a mutable :class:`MultiDict`.
+ """
+ return MultiDict(self)
+
+ def __len__(self) -> int:
+ return len(self._keys_impl())
+
+ def __contains__(self, key: K) -> bool: # type: ignore[override]
+ for d in self.dicts:
+ if key in d:
+ return True
+ return False
+
+ def __repr__(self) -> str:
+ return f"{type(self).__name__}({self.dicts!r})"
+
+
+class ImmutableDict(ImmutableDictMixin[K, V], dict[K, V]): # type: ignore[misc]
+ """An immutable :class:`dict`.
+
+ .. versionadded:: 0.5
+ """
+
+ def __repr__(self) -> str:
+ return f"{type(self).__name__}({dict.__repr__(self)})"
+
+ def copy(self) -> dict[K, V]:
+ """Return a shallow mutable copy of this object. Keep in mind that
+ the standard library's :func:`copy` function is a no-op for this class
+ like for any other python immutable type (eg: :class:`tuple`).
+ """
+ return dict(self)
+
+ def __copy__(self) -> te.Self:
+ return self
+
+
+class ImmutableMultiDict(ImmutableMultiDictMixin[K, V], MultiDict[K, V]): # type: ignore[misc]
+ """An immutable :class:`MultiDict`.
+
+ .. versionadded:: 0.5
+ """
+
+ def copy(self) -> MultiDict[K, V]: # type: ignore[override]
+ """Return a shallow mutable copy of this object. Keep in mind that
+ the standard library's :func:`copy` function is a no-op for this class
+ like for any other python immutable type (eg: :class:`tuple`).
+ """
+ return MultiDict(self)
+
+ def __copy__(self) -> te.Self:
+ return self
+
+
+class _ImmutableOrderedMultiDict( # type: ignore[misc]
+ ImmutableMultiDictMixin[K, V], _OrderedMultiDict[K, V]
+):
+ """An immutable :class:`OrderedMultiDict`.
+
+ .. deprecated:: 3.1
+ Will be removed in Werkzeug 3.2. Use ``ImmutableMultiDict`` instead.
+
+ .. versionadded:: 0.6
+ """
+
+ def __init__(
+ self,
+ mapping: (
+ MultiDict[K, V]
+ | cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]]
+ | cabc.Iterable[tuple[K, V]]
+ | None
+ ) = None,
+ ) -> None:
+ super().__init__()
+
+ if mapping is not None:
+ for k, v in iter_multi_items(mapping):
+ _OrderedMultiDict.add(self, k, v)
+
+ def _iter_hashitems(self) -> cabc.Iterable[t.Any]:
+ return enumerate(self.items(multi=True))
+
+ def copy(self) -> _OrderedMultiDict[K, V]: # type: ignore[override]
+ """Return a shallow mutable copy of this object. Keep in mind that
+ the standard library's :func:`copy` function is a no-op for this class
+ like for any other python immutable type (eg: :class:`tuple`).
+ """
+ return _OrderedMultiDict(self)
+
+ def __copy__(self) -> te.Self:
+ return self
+
+
+class CallbackDict(UpdateDictMixin[K, V], dict[K, V]):
+ """A dict that calls a function passed every time something is changed.
+ The function is passed the dict instance.
+ """
+
+ def __init__(
+ self,
+ initial: cabc.Mapping[K, V] | cabc.Iterable[tuple[K, V]] | None = None,
+ on_update: cabc.Callable[[te.Self], None] | None = None,
+ ) -> None:
+ if initial is None:
+ super().__init__()
+ else:
+ super().__init__(initial)
+
+ self.on_update = on_update
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} {super().__repr__()}>"
+
+
+class HeaderSet(cabc.MutableSet[str]):
+ """Similar to the :class:`ETags` class this implements a set-like structure.
+ Unlike :class:`ETags` this is case insensitive and used for vary, allow, and
+ content-language headers.
+
+ If not constructed using the :func:`parse_set_header` function the
+ instantiation works like this:
+
+ >>> hs = HeaderSet(['foo', 'bar', 'baz'])
+ >>> hs
+ HeaderSet(['foo', 'bar', 'baz'])
+ """
+
+ def __init__(
+ self,
+ headers: cabc.Iterable[str] | None = None,
+ on_update: cabc.Callable[[te.Self], None] | None = None,
+ ) -> None:
+ self._headers = list(headers or ())
+ self._set = {x.lower() for x in self._headers}
+ self.on_update = on_update
+
+ def add(self, header: str) -> None:
+ """Add a new header to the set."""
+ self.update((header,))
+
+ def remove(self: te.Self, header: str) -> None:
+ """Remove a header from the set. This raises an :exc:`KeyError` if the
+ header is not in the set.
+
+ .. versionchanged:: 0.5
+ In older versions a :exc:`IndexError` was raised instead of a
+ :exc:`KeyError` if the object was missing.
+
+ :param header: the header to be removed.
+ """
+ key = header.lower()
+ if key not in self._set:
+ raise KeyError(header)
+ self._set.remove(key)
+ for idx, key in enumerate(self._headers):
+ if key.lower() == header:
+ del self._headers[idx]
+ break
+ if self.on_update is not None:
+ self.on_update(self)
+
+ def update(self: te.Self, iterable: cabc.Iterable[str]) -> None:
+ """Add all the headers from the iterable to the set.
+
+ :param iterable: updates the set with the items from the iterable.
+ """
+ inserted_any = False
+ for header in iterable:
+ key = header.lower()
+ if key not in self._set:
+ self._headers.append(header)
+ self._set.add(key)
+ inserted_any = True
+ if inserted_any and self.on_update is not None:
+ self.on_update(self)
+
+ def discard(self, header: str) -> None:
+ """Like :meth:`remove` but ignores errors.
+
+ :param header: the header to be discarded.
+ """
+ try:
+ self.remove(header)
+ except KeyError:
+ pass
+
+ def find(self, header: str) -> int:
+ """Return the index of the header in the set or return -1 if not found.
+
+ :param header: the header to be looked up.
+ """
+ header = header.lower()
+ for idx, item in enumerate(self._headers):
+ if item.lower() == header:
+ return idx
+ return -1
+
+ def index(self, header: str) -> int:
+ """Return the index of the header in the set or raise an
+ :exc:`IndexError`.
+
+ :param header: the header to be looked up.
+ """
+ rv = self.find(header)
+ if rv < 0:
+ raise IndexError(header)
+ return rv
+
+ def clear(self: te.Self) -> None:
+ """Clear the set."""
+ self._set.clear()
+ self._headers.clear()
+
+ if self.on_update is not None:
+ self.on_update(self)
+
+ def as_set(self, preserve_casing: bool = False) -> set[str]:
+ """Return the set as real python set type. When calling this, all
+ the items are converted to lowercase and the ordering is lost.
+
+ :param preserve_casing: if set to `True` the items in the set returned
+ will have the original case like in the
+ :class:`HeaderSet`, otherwise they will
+ be lowercase.
+ """
+ if preserve_casing:
+ return set(self._headers)
+ return set(self._set)
+
+ def to_header(self) -> str:
+ """Convert the header set into an HTTP header string."""
+ return ", ".join(map(http.quote_header_value, self._headers))
+
+ def __getitem__(self, idx: t.SupportsIndex) -> str:
+ return self._headers[idx]
+
+ def __delitem__(self: te.Self, idx: t.SupportsIndex) -> None:
+ rv = self._headers.pop(idx)
+ self._set.remove(rv.lower())
+ if self.on_update is not None:
+ self.on_update(self)
+
+ def __setitem__(self: te.Self, idx: t.SupportsIndex, value: str) -> None:
+ old = self._headers[idx]
+ self._set.remove(old.lower())
+ self._headers[idx] = value
+ self._set.add(value.lower())
+ if self.on_update is not None:
+ self.on_update(self)
+
+ def __contains__(self, header: str) -> bool: # type: ignore[override]
+ return header.lower() in self._set
+
+ def __len__(self) -> int:
+ return len(self._set)
+
+ def __iter__(self) -> cabc.Iterator[str]:
+ return iter(self._headers)
+
+ def __bool__(self) -> bool:
+ return bool(self._set)
+
+ def __str__(self) -> str:
+ return self.to_header()
+
+ def __repr__(self) -> str:
+ return f"{type(self).__name__}({self._headers!r})"
+
+
+# circular dependencies
+from .. import http # noqa: E402
+
+
+def __getattr__(name: str) -> t.Any:
+ import warnings
+
+ if name == "OrderedMultiDict":
+ warnings.warn(
+ "'OrderedMultiDict' is deprecated and will be removed in Werkzeug"
+ " 3.2. Use 'MultiDict' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return _OrderedMultiDict
+
+ if name == "ImmutableOrderedMultiDict":
+ warnings.warn(
+ "'ImmutableOrderedMultiDict' is deprecated and will be removed in"
+ " Werkzeug 3.2. Use 'ImmutableMultiDict' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return _ImmutableOrderedMultiDict
+
+ raise AttributeError(name)
diff --git a/venv/Lib/site-packages/werkzeug/debug/__init__.py b/venv/Lib/site-packages/werkzeug/debug/__init__.py
new file mode 100644
index 0000000..17cd4b2
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/debug/__init__.py
@@ -0,0 +1,574 @@
+from __future__ import annotations
+
+import getpass
+import hashlib
+import json
+import os
+import pkgutil
+import re
+import sys
+import time
+import typing as t
+import uuid
+from contextlib import ExitStack
+from io import BytesIO
+from itertools import chain
+from multiprocessing import Value
+from os.path import basename
+from os.path import join
+from zlib import adler32
+
+from .._internal import _log
+from ..exceptions import NotFound
+from ..exceptions import SecurityError
+from ..http import parse_cookie
+from ..sansio.utils import host_is_trusted
+from ..security import gen_salt
+from ..utils import send_file
+from ..wrappers.request import Request
+from ..wrappers.response import Response
+from .console import Console
+from .tbtools import DebugFrameSummary
+from .tbtools import DebugTraceback
+from .tbtools import render_console_html
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import StartResponse
+ from _typeshed.wsgi import WSGIApplication
+ from _typeshed.wsgi import WSGIEnvironment
+
+# A week
+PIN_TIME = 60 * 60 * 24 * 7
+
+
+def hash_pin(pin: str) -> str:
+ return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]
+
+
+_machine_id: str | bytes | None = None
+
+
+def get_machine_id() -> str | bytes | None:
+ global _machine_id
+
+ if _machine_id is not None:
+ return _machine_id
+
+ def _generate() -> str | bytes | None:
+ linux = b""
+
+ # machine-id is stable across boots, boot_id is not.
+ for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
+ try:
+ with open(filename, "rb") as f:
+ value = f.readline().strip()
+ except OSError:
+ continue
+
+ if value:
+ linux += value
+ break
+
+ # Containers share the same machine id, add some cgroup
+ # information. This is used outside containers too but should be
+ # relatively stable across boots.
+ try:
+ with open("/proc/self/cgroup", "rb") as f:
+ linux += f.readline().strip().rpartition(b"/")[2]
+ except OSError:
+ pass
+
+ if linux:
+ return linux
+
+ # On OS X, use ioreg to get the computer's serial number.
+ try:
+ # subprocess may not be available, e.g. Google App Engine
+ # https://github.com/pallets/werkzeug/issues/925
+ from subprocess import PIPE
+ from subprocess import Popen
+
+ dump = Popen(
+ ["ioreg", "-c", "IOPlatformExpertDevice", "-d", "2"], stdout=PIPE
+ ).communicate()[0]
+ match = re.search(b'"serial-number" = <([^>]+)', dump)
+
+ if match is not None:
+ return match.group(1)
+ except (OSError, ImportError):
+ pass
+
+ # On Windows, use winreg to get the machine guid.
+ if sys.platform == "win32":
+ import winreg
+
+ try:
+ with winreg.OpenKey(
+ winreg.HKEY_LOCAL_MACHINE,
+ "SOFTWARE\\Microsoft\\Cryptography",
+ 0,
+ winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
+ ) as rk:
+ guid: str | bytes
+ guid_type: int
+ guid, guid_type = winreg.QueryValueEx(rk, "MachineGuid")
+
+ if guid_type == winreg.REG_SZ:
+ return guid.encode()
+
+ return guid
+ except OSError:
+ pass
+
+ return None
+
+ _machine_id = _generate()
+ return _machine_id
+
+
+class _ConsoleFrame:
+ """Helper class so that we can reuse the frame console code for the
+ standalone console.
+ """
+
+ def __init__(self, namespace: dict[str, t.Any]):
+ self.console = Console(namespace)
+ self.id = 0
+
+ def eval(self, code: str) -> t.Any:
+ return self.console.eval(code)
+
+
+def get_pin_and_cookie_name(
+ app: WSGIApplication,
+) -> tuple[str, str] | tuple[None, None]:
+ """Given an application object this returns a semi-stable 9 digit pin
+ code and a random key. The hope is that this is stable between
+ restarts to not make debugging particularly frustrating. If the pin
+ was forcefully disabled this returns `None`.
+
+ Second item in the resulting tuple is the cookie name for remembering.
+ """
+ pin = os.environ.get("WERKZEUG_DEBUG_PIN")
+ rv = None
+ num = None
+
+ # Pin was explicitly disabled
+ if pin == "off":
+ return None, None
+
+ # Pin was provided explicitly
+ if pin is not None and pin.replace("-", "").isdecimal():
+ # If there are separators in the pin, return it directly
+ if "-" in pin:
+ rv = pin
+ else:
+ num = pin
+
+ modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
+ username: str | None
+
+ try:
+ # getuser imports the pwd module, which does not exist in Google
+ # App Engine. It may also raise a KeyError if the UID does not
+ # have a username, such as in Docker.
+ username = getpass.getuser()
+ # Python >= 3.13 only raises OSError
+ except (ImportError, KeyError, OSError):
+ username = None
+
+ mod = sys.modules.get(modname)
+
+ # This information only exists to make the cookie unique on the
+ # computer, not as a security feature.
+ probably_public_bits = [
+ username,
+ modname,
+ getattr(app, "__name__", type(app).__name__),
+ getattr(mod, "__file__", None),
+ ]
+
+ # This information is here to make it harder for an attacker to
+ # guess the cookie name. They are unlikely to be contained anywhere
+ # within the unauthenticated debug page.
+ private_bits = [str(uuid.getnode()), get_machine_id()]
+
+ h = hashlib.sha1()
+ for bit in chain(probably_public_bits, private_bits):
+ if not bit:
+ continue
+ if isinstance(bit, str):
+ bit = bit.encode()
+ h.update(bit)
+ h.update(b"cookiesalt")
+
+ cookie_name = f"__wzd{h.hexdigest()[:20]}"
+
+ # If we need to generate a pin we salt it a bit more so that we don't
+ # end up with the same value and generate out 9 digits
+ if num is None:
+ h.update(b"pinsalt")
+ num = f"{int(h.hexdigest(), 16):09d}"[:9]
+
+ # Format the pincode in groups of digits for easier remembering if
+ # we don't have a result yet.
+ if rv is None:
+ for group_size in 5, 4, 3:
+ if len(num) % group_size == 0:
+ rv = "-".join(
+ num[x : x + group_size].rjust(group_size, "0")
+ for x in range(0, len(num), group_size)
+ )
+ break
+ else:
+ rv = num
+
+ return rv, cookie_name
+
+
+class DebuggedApplication:
+ """Enables debugging support for a given application::
+
+ from werkzeug.debug import DebuggedApplication
+ from myapp import app
+ app = DebuggedApplication(app, evalex=True)
+
+ The ``evalex`` argument allows evaluating expressions in any frame
+ of a traceback. This works by preserving each frame with its local
+ state. Some state, such as context globals, cannot be restored with
+ the frame by default. When ``evalex`` is enabled,
+ ``environ["werkzeug.debug.preserve_context"]`` will be a callable
+ that takes a context manager, and can be called multiple times.
+ Each context manager will be entered before evaluating code in the
+ frame, then exited again, so they can perform setup and cleanup for
+ each call.
+
+ :param app: the WSGI application to run debugged.
+ :param evalex: enable exception evaluation feature (interactive
+ debugging). This requires a non-forking server.
+ :param request_key: The key that points to the request object in this
+ environment. This parameter is ignored in current
+ versions.
+ :param console_path: the URL for a general purpose console.
+ :param console_init_func: the function that is executed before starting
+ the general purpose console. The return value
+ is used as initial namespace.
+ :param show_hidden_frames: by default hidden traceback frames are skipped.
+ You can show them by setting this parameter
+ to `True`.
+ :param pin_security: can be used to disable the pin based security system.
+ :param pin_logging: enables the logging of the pin system.
+
+ .. versionchanged:: 2.2
+ Added the ``werkzeug.debug.preserve_context`` environ key.
+ """
+
+ _pin: str | None
+ _pin_cookie: str
+
+ def __init__(
+ self,
+ app: WSGIApplication,
+ evalex: bool = False,
+ request_key: str = "werkzeug.request",
+ console_path: str = "/console",
+ console_init_func: t.Callable[[], dict[str, t.Any]] | None = None,
+ show_hidden_frames: bool = False,
+ pin_security: bool = True,
+ pin_logging: bool = True,
+ ) -> None:
+ if not console_init_func:
+ console_init_func = None
+ self.app = app
+ self.evalex = evalex
+ self.frames: dict[int, DebugFrameSummary | _ConsoleFrame] = {}
+ self.frame_contexts: dict[int, list[t.ContextManager[None]]] = {}
+ self.request_key = request_key
+ self.console_path = console_path
+ self.console_init_func = console_init_func
+ self.show_hidden_frames = show_hidden_frames
+ self.secret = gen_salt(20)
+ self._failed_pin_auth = Value("B")
+
+ self.pin_logging = pin_logging
+ if pin_security:
+ # Print out the pin for the debugger on standard out.
+ if os.environ.get("WERKZEUG_RUN_MAIN") == "true" and pin_logging:
+ _log("warning", " * Debugger is active!")
+ if self.pin is None:
+ _log("warning", " * Debugger PIN disabled. DEBUGGER UNSECURED!")
+ else:
+ _log("info", " * Debugger PIN: %s", self.pin)
+ else:
+ self.pin = None
+
+ self.trusted_hosts: list[str] = [".localhost", "127.0.0.1"]
+ """List of domains to allow requests to the debugger from. A leading dot
+ allows all subdomains. This only allows ``".localhost"`` domains by
+ default.
+
+ .. versionadded:: 3.0.3
+ """
+
+ @property
+ def pin(self) -> str | None:
+ if not hasattr(self, "_pin"):
+ pin_cookie = get_pin_and_cookie_name(self.app)
+ self._pin, self._pin_cookie = pin_cookie # type: ignore
+ return self._pin
+
+ @pin.setter
+ def pin(self, value: str | None) -> None:
+ if value is None:
+ # Set _pin to None explicitly to prevent regeneration by the getter
+ self._pin = None
+ else:
+ self._pin = value
+
+ @property
+ def pin_cookie_name(self) -> str:
+ """The name of the pin cookie."""
+ if not hasattr(self, "_pin_cookie"):
+ pin_cookie = get_pin_and_cookie_name(self.app)
+ self._pin, self._pin_cookie = pin_cookie # type: ignore
+ return self._pin_cookie
+
+ def debug_application(
+ self, environ: WSGIEnvironment, start_response: StartResponse
+ ) -> t.Iterator[bytes]:
+ """Run the application and conserve the traceback frames."""
+ contexts: list[t.ContextManager[t.Any]] = []
+
+ if self.evalex:
+ environ["werkzeug.debug.preserve_context"] = contexts.append
+
+ app_iter = None
+ try:
+ app_iter = self.app(environ, start_response)
+ yield from app_iter
+ if hasattr(app_iter, "close"):
+ app_iter.close()
+ except Exception as e:
+ if hasattr(app_iter, "close"):
+ app_iter.close() # type: ignore
+
+ tb = DebugTraceback(e, skip=1, hide=not self.show_hidden_frames)
+
+ for frame in tb.all_frames:
+ self.frames[id(frame)] = frame
+ self.frame_contexts[id(frame)] = contexts
+
+ is_trusted = bool(self.check_pin_trust(environ))
+ html = tb.render_debugger_html(
+ evalex=self.evalex and self.check_host_trust(environ),
+ secret=self.secret,
+ evalex_trusted=is_trusted,
+ )
+ response = Response(html, status=500, mimetype="text/html")
+
+ try:
+ yield from response(environ, start_response)
+ except Exception:
+ # if we end up here there has been output but an error
+ # occurred. in that situation we can do nothing fancy any
+ # more, better log something into the error log and fall
+ # back gracefully.
+ environ["wsgi.errors"].write(
+ "Debugging middleware caught exception in streamed "
+ "response at a point where response headers were already "
+ "sent.\n"
+ )
+
+ environ["wsgi.errors"].write("".join(tb.render_traceback_text()))
+
+ def execute_command(
+ self,
+ request: Request,
+ command: str,
+ frame: DebugFrameSummary | _ConsoleFrame,
+ ) -> Response:
+ """Execute a command in a console."""
+ if not self.check_host_trust(request.environ):
+ return SecurityError() # type: ignore[return-value]
+
+ contexts = self.frame_contexts.get(id(frame), [])
+
+ with ExitStack() as exit_stack:
+ for cm in contexts:
+ exit_stack.enter_context(cm)
+
+ return Response(frame.eval(command), mimetype="text/html")
+
+ def display_console(self, request: Request) -> Response:
+ """Display a standalone shell."""
+ if not self.check_host_trust(request.environ):
+ return SecurityError() # type: ignore[return-value]
+
+ if 0 not in self.frames:
+ if self.console_init_func is None:
+ ns = {}
+ else:
+ ns = dict(self.console_init_func())
+ ns.setdefault("app", self.app)
+ self.frames[0] = _ConsoleFrame(ns)
+ is_trusted = bool(self.check_pin_trust(request.environ))
+ return Response(
+ render_console_html(secret=self.secret, evalex_trusted=is_trusted),
+ mimetype="text/html",
+ )
+
+ def get_resource(self, request: Request, filename: str) -> Response:
+ """Return a static resource from the shared folder."""
+ path = join("shared", basename(filename))
+
+ try:
+ data = pkgutil.get_data(__package__, path)
+ except OSError:
+ return NotFound() # type: ignore[return-value]
+ else:
+ if data is None:
+ return NotFound() # type: ignore[return-value]
+
+ etag = str(adler32(data) & 0xFFFFFFFF)
+ return send_file(
+ BytesIO(data), request.environ, download_name=filename, etag=etag
+ )
+
+ def check_pin_trust(self, environ: WSGIEnvironment) -> bool | None:
+ """Checks if the request passed the pin test. This returns `True` if the
+ request is trusted on a pin/cookie basis and returns `False` if not.
+ Additionally if the cookie's stored pin hash is wrong it will return
+ `None` so that appropriate action can be taken.
+ """
+ if self.pin is None:
+ return True
+
+ # If we failed too many times, then we're locked out.
+ if self._failed_pin_auth.value >= 10:
+ return False
+
+ val = parse_cookie(environ).get(self.pin_cookie_name)
+ if not val or "|" not in val:
+ return False
+ ts_str, pin_hash = val.split("|", 1)
+
+ try:
+ ts = int(ts_str)
+ except ValueError:
+ return False
+
+ if pin_hash != hash_pin(self.pin):
+ return None
+ return (time.time() - PIN_TIME) < ts
+
+ def check_host_trust(self, environ: WSGIEnvironment) -> bool:
+ return host_is_trusted(environ.get("HTTP_HOST"), self.trusted_hosts)
+
+ def _fail_pin_auth(self) -> None:
+ with self._failed_pin_auth.get_lock():
+ count = self._failed_pin_auth.value
+ self._failed_pin_auth.value = count + 1
+
+ time.sleep(5.0 if count > 5 else 0.5)
+
+ def pin_auth(self, request: Request) -> Response:
+ """Authenticates with the pin."""
+ if not self.check_host_trust(request.environ):
+ return SecurityError() # type: ignore[return-value]
+
+ exhausted = False
+ auth = False
+ trust = self.check_pin_trust(request.environ)
+ pin = t.cast(str, self.pin)
+
+ # If the trust return value is `None` it means that the cookie is
+ # set but the stored pin hash value is bad. This means that the
+ # pin was changed. In this case we count a bad auth and unset the
+ # cookie. This way it becomes harder to guess the cookie name
+ # instead of the pin as we still count up failures.
+ bad_cookie = False
+ if trust is None:
+ self._fail_pin_auth()
+ bad_cookie = True
+
+ # If we're trusted, we're authenticated.
+ elif trust:
+ auth = True
+
+ # If we failed too many times, then we're locked out.
+ elif self._failed_pin_auth.value >= 10:
+ exhausted = True
+
+ # Otherwise go through pin based authentication
+ else:
+ entered_pin = request.args["pin"]
+
+ if entered_pin.strip().replace("-", "") == pin.replace("-", ""):
+ self._failed_pin_auth.value = 0
+ auth = True
+ else:
+ self._fail_pin_auth()
+
+ rv = Response(
+ json.dumps({"auth": auth, "exhausted": exhausted}),
+ mimetype="application/json",
+ )
+ if auth:
+ rv.set_cookie(
+ self.pin_cookie_name,
+ f"{int(time.time())}|{hash_pin(pin)}",
+ httponly=True,
+ samesite="Strict",
+ secure=request.is_secure,
+ )
+ elif bad_cookie:
+ rv.delete_cookie(self.pin_cookie_name)
+ return rv
+
+ def log_pin_request(self, request: Request) -> Response:
+ """Log the pin if needed."""
+ if not self.check_host_trust(request.environ):
+ return SecurityError() # type: ignore[return-value]
+
+ if self.pin_logging and self.pin is not None:
+ _log(
+ "info", " * To enable the debugger you need to enter the security pin:"
+ )
+ _log("info", " * Debugger pin code: %s", self.pin)
+ return Response("")
+
+ def __call__(
+ self, environ: WSGIEnvironment, start_response: StartResponse
+ ) -> t.Iterable[bytes]:
+ """Dispatch the requests."""
+ # important: don't ever access a function here that reads the incoming
+ # form data! Otherwise the application won't have access to that data
+ # any more!
+ request = Request(environ)
+ response = self.debug_application
+ if request.args.get("__debugger__") == "yes":
+ cmd = request.args.get("cmd")
+ arg = request.args.get("f")
+ secret = request.args.get("s")
+ frame = self.frames.get(request.args.get("frm", type=int)) # type: ignore
+ if cmd == "resource" and arg:
+ response = self.get_resource(request, arg) # type: ignore
+ elif cmd == "pinauth" and secret == self.secret:
+ response = self.pin_auth(request) # type: ignore
+ elif cmd == "printpin" and secret == self.secret:
+ response = self.log_pin_request(request) # type: ignore
+ elif (
+ self.evalex
+ and cmd is not None
+ and frame is not None
+ and self.secret == secret
+ and self.check_pin_trust(environ)
+ ):
+ response = self.execute_command(request, cmd, frame) # type: ignore
+ elif (
+ self.evalex
+ and self.console_path is not None
+ and request.path == self.console_path
+ ):
+ response = self.display_console(request) # type: ignore
+ return response(environ, start_response)
diff --git a/venv/Lib/site-packages/werkzeug/debug/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/debug/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..0a4dae6
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/debug/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/debug/__pycache__/console.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/debug/__pycache__/console.cpython-310.pyc
new file mode 100644
index 0000000..e02768d
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/debug/__pycache__/console.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/debug/__pycache__/repr.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/debug/__pycache__/repr.cpython-310.pyc
new file mode 100644
index 0000000..530044f
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/debug/__pycache__/repr.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/debug/__pycache__/tbtools.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/debug/__pycache__/tbtools.cpython-310.pyc
new file mode 100644
index 0000000..d56afad
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/debug/__pycache__/tbtools.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/debug/console.py b/venv/Lib/site-packages/werkzeug/debug/console.py
new file mode 100644
index 0000000..4e40475
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/debug/console.py
@@ -0,0 +1,219 @@
+from __future__ import annotations
+
+import code
+import sys
+import typing as t
+from contextvars import ContextVar
+from types import CodeType
+
+from markupsafe import escape
+
+from .repr import debug_repr
+from .repr import dump
+from .repr import helper
+
+_stream: ContextVar[HTMLStringO] = ContextVar("werkzeug.debug.console.stream")
+_ipy: ContextVar[_InteractiveConsole] = ContextVar("werkzeug.debug.console.ipy")
+
+
+class HTMLStringO:
+ """A StringO version that HTML escapes on write."""
+
+ def __init__(self) -> None:
+ self._buffer: list[str] = []
+
+ def isatty(self) -> bool:
+ return False
+
+ def close(self) -> None:
+ pass
+
+ def flush(self) -> None:
+ pass
+
+ def seek(self, n: int, mode: int = 0) -> None:
+ pass
+
+ def readline(self) -> str:
+ if len(self._buffer) == 0:
+ return ""
+ ret = self._buffer[0]
+ del self._buffer[0]
+ return ret
+
+ def reset(self) -> str:
+ val = "".join(self._buffer)
+ del self._buffer[:]
+ return val
+
+ def _write(self, x: str) -> None:
+ self._buffer.append(x)
+
+ def write(self, x: str) -> None:
+ self._write(escape(x))
+
+ def writelines(self, x: t.Iterable[str]) -> None:
+ self._write(escape("".join(x)))
+
+
+class ThreadedStream:
+ """Thread-local wrapper for sys.stdout for the interactive console."""
+
+ @staticmethod
+ def push() -> None:
+ if not isinstance(sys.stdout, ThreadedStream):
+ sys.stdout = t.cast(t.TextIO, ThreadedStream())
+
+ _stream.set(HTMLStringO())
+
+ @staticmethod
+ def fetch() -> str:
+ try:
+ stream = _stream.get()
+ except LookupError:
+ return ""
+
+ return stream.reset()
+
+ @staticmethod
+ def displayhook(obj: object) -> None:
+ try:
+ stream = _stream.get()
+ except LookupError:
+ return _displayhook(obj) # type: ignore
+
+ # stream._write bypasses escaping as debug_repr is
+ # already generating HTML for us.
+ if obj is not None:
+ _ipy.get().locals["_"] = obj
+ stream._write(debug_repr(obj))
+
+ def __setattr__(self, name: str, value: t.Any) -> None:
+ raise AttributeError(f"read only attribute {name}")
+
+ def __dir__(self) -> list[str]:
+ return dir(sys.__stdout__)
+
+ def __getattribute__(self, name: str) -> t.Any:
+ try:
+ stream = _stream.get()
+ except LookupError:
+ stream = sys.__stdout__ # type: ignore[assignment]
+
+ return getattr(stream, name)
+
+ def __repr__(self) -> str:
+ return repr(sys.__stdout__)
+
+
+# add the threaded stream as display hook
+_displayhook = sys.displayhook
+sys.displayhook = ThreadedStream.displayhook
+
+
+class _ConsoleLoader:
+ def __init__(self) -> None:
+ self._storage: dict[int, str] = {}
+
+ def register(self, code: CodeType, source: str) -> None:
+ self._storage[id(code)] = source
+ # register code objects of wrapped functions too.
+ for var in code.co_consts:
+ if isinstance(var, CodeType):
+ self._storage[id(var)] = source
+
+ def get_source_by_code(self, code: CodeType) -> str | None:
+ try:
+ return self._storage[id(code)]
+ except KeyError:
+ return None
+
+
+class _InteractiveConsole(code.InteractiveInterpreter):
+ locals: dict[str, t.Any]
+
+ def __init__(self, globals: dict[str, t.Any], locals: dict[str, t.Any]) -> None:
+ self.loader = _ConsoleLoader()
+ locals = {
+ **globals,
+ **locals,
+ "dump": dump,
+ "help": helper,
+ "__loader__": self.loader,
+ }
+ super().__init__(locals)
+ original_compile = self.compile
+
+ def compile(source: str, filename: str, symbol: str) -> CodeType | None:
+ code = original_compile(source, filename, symbol)
+
+ if code is not None:
+ self.loader.register(code, source)
+
+ return code
+
+ self.compile = compile # type: ignore[assignment]
+ self.more = False
+ self.buffer: list[str] = []
+
+ def runsource(self, source: str, **kwargs: t.Any) -> str: # type: ignore
+ source = f"{source.rstrip()}\n"
+ ThreadedStream.push()
+ prompt = "... " if self.more else ">>> "
+ try:
+ source_to_eval = "".join(self.buffer + [source])
+ if super().runsource(source_to_eval, "", "single"):
+ self.more = True
+ self.buffer.append(source)
+ else:
+ self.more = False
+ del self.buffer[:]
+ finally:
+ output = ThreadedStream.fetch()
+ return f"{prompt}{escape(source)}{output}"
+
+ def runcode(self, code: CodeType) -> None:
+ try:
+ exec(code, self.locals)
+ except Exception:
+ self.showtraceback()
+
+ def showtraceback(self) -> None:
+ from .tbtools import DebugTraceback
+
+ exc = t.cast(BaseException, sys.exc_info()[1])
+ te = DebugTraceback(exc, skip=1)
+ sys.stdout._write(te.render_traceback_html()) # type: ignore
+
+ def showsyntaxerror(self, filename: str | None = None) -> None:
+ from .tbtools import DebugTraceback
+
+ exc = t.cast(BaseException, sys.exc_info()[1])
+ te = DebugTraceback(exc, skip=4)
+ sys.stdout._write(te.render_traceback_html()) # type: ignore
+
+ def write(self, data: str) -> None:
+ sys.stdout.write(data)
+
+
+class Console:
+ """An interactive console."""
+
+ def __init__(
+ self,
+ globals: dict[str, t.Any] | None = None,
+ locals: dict[str, t.Any] | None = None,
+ ) -> None:
+ if locals is None:
+ locals = {}
+ if globals is None:
+ globals = {}
+ self._ipy = _InteractiveConsole(globals, locals)
+
+ def eval(self, code: str) -> str:
+ _ipy.set(self._ipy)
+ old_sys_stdout = sys.stdout
+ try:
+ return self._ipy.runsource(code)
+ finally:
+ sys.stdout = old_sys_stdout
diff --git a/venv/Lib/site-packages/werkzeug/debug/repr.py b/venv/Lib/site-packages/werkzeug/debug/repr.py
new file mode 100644
index 0000000..2bbd9d5
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/debug/repr.py
@@ -0,0 +1,282 @@
+"""Object representations for debugging purposes. Unlike the default
+repr, these expose more information and produce HTML instead of ASCII.
+
+Together with the CSS and JavaScript of the debugger this gives a
+colorful and more compact output.
+"""
+
+from __future__ import annotations
+
+import codecs
+import re
+import sys
+import typing as t
+from collections import deque
+from traceback import format_exception_only
+
+from markupsafe import escape
+
+missing = object()
+_paragraph_re = re.compile(r"(?:\r\n|\r|\n){2,}")
+RegexType = type(_paragraph_re)
+
+HELP_HTML = """\
+
+
%(title)s
+
%(text)s
+
\
+"""
+OBJECT_DUMP_HTML = """\
+
+
%(title)s
+ %(repr)s
+
%(items)s
+
\
+"""
+
+
+def debug_repr(obj: object) -> str:
+ """Creates a debug repr of an object as HTML string."""
+ return DebugReprGenerator().repr(obj)
+
+
+def dump(obj: object = missing) -> None:
+ """Print the object details to stdout._write (for the interactive
+ console of the web debugger.
+ """
+ gen = DebugReprGenerator()
+ if obj is missing:
+ rv = gen.dump_locals(sys._getframe(1).f_locals)
+ else:
+ rv = gen.dump_object(obj)
+ sys.stdout._write(rv) # type: ignore
+
+
+class _Helper:
+ """Displays an HTML version of the normal help, for the interactive
+ debugger only because it requires a patched sys.stdout.
+ """
+
+ def __repr__(self) -> str:
+ return "Type help(object) for help about object."
+
+ def __call__(self, topic: t.Any | None = None) -> None:
+ if topic is None:
+ sys.stdout._write(f"{self!r}") # type: ignore
+ return
+ import pydoc
+
+ pydoc.help(topic)
+ rv = sys.stdout.reset() # type: ignore
+ paragraphs = _paragraph_re.split(rv)
+ if len(paragraphs) > 1:
+ title = paragraphs[0]
+ text = "\n\n".join(paragraphs[1:])
+ else:
+ title = "Help"
+ text = paragraphs[0]
+ sys.stdout._write(HELP_HTML % {"title": title, "text": text}) # type: ignore
+
+
+helper = _Helper()
+
+
+def _add_subclass_info(inner: str, obj: object, base: type | tuple[type, ...]) -> str:
+ if isinstance(base, tuple):
+ for cls in base:
+ if type(obj) is cls:
+ return inner
+ elif type(obj) is base:
+ return inner
+ module = ""
+ if obj.__class__.__module__ not in ("__builtin__", "exceptions"):
+ module = f'{obj.__class__.__module__}.'
+ return f"{module}{type(obj).__name__}({inner})"
+
+
+def _sequence_repr_maker(
+ left: str, right: str, base: type, limit: int = 8
+) -> t.Callable[[DebugReprGenerator, t.Iterable[t.Any], bool], str]:
+ def proxy(self: DebugReprGenerator, obj: t.Iterable[t.Any], recursive: bool) -> str:
+ if recursive:
+ return _add_subclass_info(f"{left}...{right}", obj, base)
+ buf = [left]
+ have_extended_section = False
+ for idx, item in enumerate(obj):
+ if idx:
+ buf.append(", ")
+ if idx == limit:
+ buf.append('')
+ have_extended_section = True
+ buf.append(self.repr(item))
+ if have_extended_section:
+ buf.append("")
+ buf.append(right)
+ return _add_subclass_info("".join(buf), obj, base)
+
+ return proxy
+
+
+class DebugReprGenerator:
+ def __init__(self) -> None:
+ self._stack: list[t.Any] = []
+
+ list_repr = _sequence_repr_maker("[", "]", list)
+ tuple_repr = _sequence_repr_maker("(", ")", tuple)
+ set_repr = _sequence_repr_maker("set([", "])", set)
+ frozenset_repr = _sequence_repr_maker("frozenset([", "])", frozenset)
+ deque_repr = _sequence_repr_maker(
+ 'collections.deque([', "])", deque
+ )
+
+ def regex_repr(self, obj: t.Pattern[t.AnyStr]) -> str:
+ pattern = repr(obj.pattern)
+ pattern = codecs.decode(pattern, "unicode-escape", "ignore")
+ pattern = f"r{pattern}"
+ return f're.compile({pattern})'
+
+ def string_repr(self, obj: str | bytes, limit: int = 70) -> str:
+ buf = ['']
+ r = repr(obj)
+
+ # shorten the repr when the hidden part would be at least 3 chars
+ if len(r) - limit > 2:
+ buf.extend(
+ (
+ escape(r[:limit]),
+ '',
+ escape(r[limit:]),
+ "",
+ )
+ )
+ else:
+ buf.append(escape(r))
+
+ buf.append("")
+ out = "".join(buf)
+
+ # if the repr looks like a standard string, add subclass info if needed
+ if r[0] in "'\"" or (r[0] == "b" and r[1] in "'\""):
+ return _add_subclass_info(out, obj, (bytes, str))
+
+ # otherwise, assume the repr distinguishes the subclass already
+ return out
+
+ def dict_repr(
+ self,
+ d: dict[int, None] | dict[str, int] | dict[str | int, int],
+ recursive: bool,
+ limit: int = 5,
+ ) -> str:
+ if recursive:
+ return _add_subclass_info("{...}", d, dict)
+ buf = ["{"]
+ have_extended_section = False
+ for idx, (key, value) in enumerate(d.items()):
+ if idx:
+ buf.append(", ")
+ if idx == limit - 1:
+ buf.append('')
+ have_extended_section = True
+ buf.append(
+ f'{self.repr(key)}:'
+ f' {self.repr(value)}'
+ )
+ if have_extended_section:
+ buf.append("")
+ buf.append("}")
+ return _add_subclass_info("".join(buf), d, dict)
+
+ def object_repr(self, obj: t.Any) -> str:
+ r = repr(obj)
+ return f'{escape(r)}'
+
+ def dispatch_repr(self, obj: t.Any, recursive: bool) -> str:
+ if obj is helper:
+ return f'{helper!r}'
+ if isinstance(obj, (int, float, complex)):
+ return f'{obj!r}'
+ if isinstance(obj, str) or isinstance(obj, bytes):
+ return self.string_repr(obj)
+ if isinstance(obj, RegexType):
+ return self.regex_repr(obj)
+ if isinstance(obj, list):
+ return self.list_repr(obj, recursive)
+ if isinstance(obj, tuple):
+ return self.tuple_repr(obj, recursive)
+ if isinstance(obj, set):
+ return self.set_repr(obj, recursive)
+ if isinstance(obj, frozenset):
+ return self.frozenset_repr(obj, recursive)
+ if isinstance(obj, dict):
+ return self.dict_repr(obj, recursive)
+ if isinstance(obj, deque):
+ return self.deque_repr(obj, recursive)
+ return self.object_repr(obj)
+
+ def fallback_repr(self) -> str:
+ try:
+ info = "".join(format_exception_only(*sys.exc_info()[:2]))
+ except Exception:
+ info = "?"
+ return (
+ ''
+ f"<broken repr ({escape(info.strip())})>"
+ )
+
+ def repr(self, obj: object) -> str:
+ recursive = False
+ for item in self._stack:
+ if item is obj:
+ recursive = True
+ break
+ self._stack.append(obj)
+ try:
+ try:
+ return self.dispatch_repr(obj, recursive)
+ except Exception:
+ return self.fallback_repr()
+ finally:
+ self._stack.pop()
+
+ def dump_object(self, obj: object) -> str:
+ repr = None
+ items: list[tuple[str, str]] | None = None
+
+ if isinstance(obj, dict):
+ title = "Contents of"
+ items = []
+ for key, value in obj.items():
+ if not isinstance(key, str):
+ items = None
+ break
+ items.append((key, self.repr(value)))
+ if items is None:
+ items = []
+ repr = self.repr(obj)
+ for key in dir(obj):
+ try:
+ items.append((key, self.repr(getattr(obj, key))))
+ except Exception:
+ pass
+ title = "Details for"
+ title += f" {object.__repr__(obj)[1:-1]}"
+ return self.render_object_dump(items, title, repr)
+
+ def dump_locals(self, d: dict[str, t.Any]) -> str:
+ items = [(key, self.repr(value)) for key, value in d.items()]
+ return self.render_object_dump(items, "Local variables in frame")
+
+ def render_object_dump(
+ self, items: list[tuple[str, str]], title: str, repr: str | None = None
+ ) -> str:
+ html_items = []
+ for key, value in items:
+ html_items.append(f"
To switch between the interactive traceback and the plaintext " +
+ 'one, you can click on the "Traceback" headline. From the text ' +
+ "traceback you can also create a paste of it. " +
+ (!EVALEX
+ ? ""
+ : "For code execution mouse-over the frame you want to debug and " +
+ "click on the console icon on the right side." +
+ "
You can execute arbitrary Python code in the stack frames and " +
+ "there are some extra helpers available for introspection:" +
+ "
+ The console is locked and needs to be unlocked by entering the PIN.
+ You can find the PIN printed out on the standard output of your
+ shell that runs the server.
+
+
+
+
+
+"""
+
+PAGE_HTML = (
+ HEADER
+ + """\
+
%(exception_type)s
+
+
%(exception)s
+
+
Traceback (most recent call last)
+%(summary)s
+
+
+ This is the Copy/Paste friendly version of the traceback.
+
+
+
+
+ The debugger caught an exception in your WSGI application. You can now
+ look at the traceback which led to the error.
+ If you enable JavaScript you can also use additional features such as code
+ execution (if the evalex feature is enabled), automatic pasting of the
+ exceptions and much more.
+
+In this console you can execute Python expressions in the context of the
+application. The initial namespace was created by the debugger automatically.
+
+
The Console requires JavaScript.
+"""
+ + FOOTER
+)
+
+SUMMARY_HTML = """\
+
+ %(title)s
+
%(frames)s
+ %(description)s
+
+"""
+
+FRAME_HTML = """\
+
+
File "%(filename)s",
+ line %(lineno)s,
+ in %(function_name)s
+
%(lines)s
+
+"""
+
+
+def _process_traceback(
+ exc: BaseException,
+ te: traceback.TracebackException | None = None,
+ *,
+ skip: int = 0,
+ hide: bool = True,
+) -> traceback.TracebackException:
+ if te is None:
+ te = traceback.TracebackException.from_exception(exc, lookup_lines=False)
+
+ # Get the frames the same way StackSummary.extract did, in order
+ # to match each frame with the FrameSummary to augment.
+ frame_gen = traceback.walk_tb(exc.__traceback__)
+ limit = getattr(sys, "tracebacklimit", None)
+
+ if limit is not None:
+ if limit < 0:
+ limit = 0
+
+ frame_gen = itertools.islice(frame_gen, limit)
+
+ if skip:
+ frame_gen = itertools.islice(frame_gen, skip, None)
+ del te.stack[:skip]
+
+ new_stack: list[DebugFrameSummary] = []
+ hidden = False
+
+ # Match each frame with the FrameSummary that was generated.
+ # Hide frames using Paste's __traceback_hide__ rules. Replace
+ # all visible FrameSummary with DebugFrameSummary.
+ for (f, _), fs in zip(frame_gen, te.stack):
+ if hide:
+ hide_value = f.f_locals.get("__traceback_hide__", False)
+
+ if hide_value in {"before", "before_and_this"}:
+ new_stack = []
+ hidden = False
+
+ if hide_value == "before_and_this":
+ continue
+ elif hide_value in {"reset", "reset_and_this"}:
+ hidden = False
+
+ if hide_value == "reset_and_this":
+ continue
+ elif hide_value in {"after", "after_and_this"}:
+ hidden = True
+
+ if hide_value == "after_and_this":
+ continue
+ elif hide_value or hidden:
+ continue
+
+ frame_args: dict[str, t.Any] = {
+ "filename": fs.filename,
+ "lineno": fs.lineno,
+ "name": fs.name,
+ "locals": f.f_locals,
+ "globals": f.f_globals,
+ }
+
+ if sys.version_info >= (3, 11):
+ frame_args["colno"] = fs.colno
+ frame_args["end_colno"] = fs.end_colno
+
+ new_stack.append(DebugFrameSummary(**frame_args))
+
+ # The codeop module is used to compile code from the interactive
+ # debugger. Hide any codeop frames from the bottom of the traceback.
+ while new_stack:
+ module = new_stack[0].global_ns.get("__name__")
+
+ if module is None:
+ module = new_stack[0].local_ns.get("__name__")
+
+ if module == "codeop":
+ del new_stack[0]
+ else:
+ break
+
+ te.stack[:] = new_stack
+
+ if te.__context__:
+ context_exc = t.cast(BaseException, exc.__context__)
+ te.__context__ = _process_traceback(context_exc, te.__context__, hide=hide)
+
+ if te.__cause__:
+ cause_exc = t.cast(BaseException, exc.__cause__)
+ te.__cause__ = _process_traceback(cause_exc, te.__cause__, hide=hide)
+
+ return te
+
+
+class DebugTraceback:
+ __slots__ = ("_te", "_cache_all_tracebacks", "_cache_all_frames")
+
+ def __init__(
+ self,
+ exc: BaseException,
+ te: traceback.TracebackException | None = None,
+ *,
+ skip: int = 0,
+ hide: bool = True,
+ ) -> None:
+ self._te = _process_traceback(exc, te, skip=skip, hide=hide)
+
+ def __str__(self) -> str:
+ return f"<{type(self).__name__} {self._te}>"
+
+ @cached_property
+ def all_tracebacks(
+ self,
+ ) -> list[tuple[str | None, traceback.TracebackException]]:
+ out: list[tuple[str | None, traceback.TracebackException]] = []
+ current: traceback.TracebackException | None = self._te
+
+ while current is not None:
+ if current.__cause__ is not None:
+ chained_msg = (
+ "The above exception was the direct cause of the"
+ " following exception"
+ )
+ chained_exc = current.__cause__
+ elif current.__context__ is not None and not current.__suppress_context__:
+ chained_msg = (
+ "During handling of the above exception, another exception occurred"
+ )
+ chained_exc = current.__context__
+ else:
+ chained_msg = None
+ chained_exc = None
+
+ out.append((chained_msg, current))
+ current = chained_exc
+
+ return out
+
+ @cached_property
+ def all_frames(self) -> list[DebugFrameSummary]:
+ return [
+ f # type: ignore[misc]
+ for _, te in self.all_tracebacks
+ for f in te.stack
+ ]
+
+ def render_traceback_text(self) -> str:
+ return "".join(self._te.format())
+
+ def render_traceback_html(self, include_title: bool = True) -> str:
+ library_frames = [f.is_library for f in self.all_frames]
+ mark_library = 0 < sum(library_frames) < len(library_frames)
+ rows = []
+
+ if not library_frames:
+ classes = "traceback noframe-traceback"
+ else:
+ classes = "traceback"
+
+ for msg, current in reversed(self.all_tracebacks):
+ row_parts = []
+
+ if msg is not None:
+ row_parts.append(f'
{msg}:
')
+
+ for frame in current.stack:
+ frame = t.cast(DebugFrameSummary, frame)
+ info = f' title="{escape(frame.info)}"' if frame.info else ""
+ row_parts.append(f"
{frame.render_html(mark_library)}")
+
+ rows.append("\n".join(row_parts))
+
+ if sys.version_info < (3, 13):
+ exc_type_str = self._te.exc_type.__name__
+ else:
+ exc_type_str = self._te.exc_type_str
+
+ is_syntax_error = exc_type_str == "SyntaxError"
+
+ if include_title:
+ if is_syntax_error:
+ title = "Syntax Error"
+ else:
+ title = "Traceback (most recent call last):"
+ else:
+ title = ""
+
+ exc_full = escape("".join(self._te.format_exception_only()))
+
+ if is_syntax_error:
+ description = f"
{" " * prefix}'
+ f"{escape(stripped_line) if stripped_line else ' '}"
+ f"{arrow if arrow else ''}
"
+ )
+
+ if line_idx < len(lines):
+ for line in lines[start_idx:line_idx]:
+ render_line(line, "before")
+
+ render_line(lines[line_idx], "current")
+
+ for line in lines[line_idx + 1 : stop_idx]:
+ render_line(line, "after")
+
+ return FRAME_HTML % {
+ "id": id(self),
+ "filename": escape(self.filename),
+ "lineno": self.lineno,
+ "function_name": escape(self.name),
+ "lines": "\n".join(rendered_lines),
+ "library": "library" if mark_library and self.is_library else "",
+ }
+
+
+def render_console_html(secret: str, evalex_trusted: bool) -> str:
+ return CONSOLE_HTML % {
+ "evalex": "true",
+ "evalex_trusted": "true" if evalex_trusted else "false",
+ "console": "true",
+ "title": "Console",
+ "secret": secret,
+ }
diff --git a/venv/Lib/site-packages/werkzeug/exceptions.py b/venv/Lib/site-packages/werkzeug/exceptions.py
new file mode 100644
index 0000000..15f534e
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/exceptions.py
@@ -0,0 +1,905 @@
+"""Implements a number of Python exceptions which can be raised from within
+a view to trigger a standard HTTP non-200 response.
+
+Usage Example
+-------------
+
+.. code-block:: python
+
+ from werkzeug.wrappers.request import Request
+ from werkzeug.exceptions import HTTPException, NotFound
+
+ def view(request):
+ raise NotFound()
+
+ @Request.application
+ def application(request):
+ try:
+ return view(request)
+ except HTTPException as e:
+ return e
+
+As you can see from this example those exceptions are callable WSGI
+applications. However, they are not Werkzeug response objects. You
+can get a response object by calling ``get_response()`` on a HTTP
+exception.
+
+Keep in mind that you may have to pass an environ (WSGI) or scope
+(ASGI) to ``get_response()`` because some errors fetch additional
+information relating to the request.
+
+If you want to hook in a different exception page to say, a 404 status
+code, you can add a second except for a specific subclass of an error:
+
+.. code-block:: python
+
+ @Request.application
+ def application(request):
+ try:
+ return view(request)
+ except NotFound as e:
+ return not_found(request)
+ except HTTPException as e:
+ return e
+
+"""
+
+from __future__ import annotations
+
+import typing as t
+from datetime import datetime
+
+from markupsafe import escape
+from markupsafe import Markup
+
+from ._internal import _get_environ
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import StartResponse
+ from _typeshed.wsgi import WSGIEnvironment
+
+ from .datastructures import WWWAuthenticate
+ from .sansio.response import Response as SansIOResponse
+ from .wrappers.request import Request as WSGIRequest
+ from .wrappers.response import Response as WSGIResponse
+
+
+class HTTPException(Exception):
+ """The base class for all HTTP exceptions. This exception can be called as a WSGI
+ application to render a default error page or you can catch the subclasses
+ of it independently and render nicer error messages.
+
+ .. versionchanged:: 2.1
+ Removed the ``wrap`` class method.
+ """
+
+ code: int | None = None
+ description: str | None = None
+
+ def __init__(
+ self,
+ description: str | None = None,
+ response: SansIOResponse | None = None,
+ ) -> None:
+ super().__init__()
+ if description is not None:
+ self.description = description
+ self.response = response
+
+ @property
+ def name(self) -> str:
+ """The status name."""
+ from .http import HTTP_STATUS_CODES
+
+ return HTTP_STATUS_CODES.get(self.code, "Unknown Error") # type: ignore
+
+ def get_description(
+ self,
+ environ: WSGIEnvironment | None = None,
+ scope: dict[str, t.Any] | None = None,
+ ) -> str:
+ """Get the description."""
+ if self.description is None:
+ description = ""
+ else:
+ description = self.description
+
+ description = escape(description).replace("\n", Markup(" "))
+ return f"
\n"
+ f"{self.get_description(environ)}\n"
+ )
+
+ def get_headers(
+ self,
+ environ: WSGIEnvironment | None = None,
+ scope: dict[str, t.Any] | None = None,
+ ) -> list[tuple[str, str]]:
+ """Get a list of headers."""
+ return [("Content-Type", "text/html; charset=utf-8")]
+
+ @t.overload
+ def get_response(
+ self,
+ environ: WSGIEnvironment | WSGIRequest | None = ...,
+ scope: None = None,
+ ) -> WSGIResponse: ...
+ @t.overload
+ def get_response(
+ self,
+ environ: None = None,
+ scope: dict[str, t.Any] = ...,
+ ) -> SansIOResponse: ...
+ def get_response(
+ self,
+ environ: WSGIEnvironment | WSGIRequest | None = None,
+ scope: dict[str, t.Any] | None = None,
+ ) -> WSGIResponse | SansIOResponse:
+ """Get a response object.
+
+ :param environ: A WSGI environ dict or request object. If given, may be
+ used to customize the response based on the request.
+ :param scope: An ASGI scope dict. If given, may be used to customize the
+ response based on the request.
+ :return: A WSGI :class:`werkzeug.wrappers.Response` if called without
+ arguments or with ``environ``. A sans-IO
+ :class:`werkzeug.sansio.Response` for ASGI if called with
+ ``scope``.
+ """
+ from .wrappers.response import Response
+
+ if self.response is not None:
+ return self.response
+ if environ is not None:
+ environ = _get_environ(environ)
+ headers = self.get_headers(environ, scope)
+ return Response(self.get_body(environ, scope), self.code, headers)
+
+ def __call__(
+ self, environ: WSGIEnvironment, start_response: StartResponse
+ ) -> t.Iterable[bytes]:
+ """Call the exception as WSGI application.
+
+ :param environ: the WSGI environment.
+ :param start_response: the response callable provided by the WSGI
+ server.
+ """
+ response = self.get_response(environ)
+ return response(environ, start_response)
+
+ def __str__(self) -> str:
+ code = self.code if self.code is not None else "???"
+ return f"{code} {self.name}: {self.description}"
+
+ def __repr__(self) -> str:
+ code = self.code if self.code is not None else "???"
+ return f"<{type(self).__name__} '{code}: {self.name}'>"
+
+
+class BadRequest(HTTPException):
+ """*400* `Bad Request`
+
+ Raise if the browser sends something to the application the application
+ or server cannot handle.
+ """
+
+ code = 400
+ description = (
+ "The browser (or proxy) sent a request that this server could not understand."
+ )
+
+
+class BadRequestKeyError(BadRequest, KeyError):
+ """An exception that is used to signal both a :exc:`KeyError` and a
+ :exc:`BadRequest`. Used by many of the datastructures.
+ """
+
+ _description = BadRequest.description
+ #: Show the KeyError along with the HTTP error message in the
+ #: response. This should be disabled in production, but can be
+ #: useful in a debug mode.
+ show_exception = False
+
+ def __init__(self, arg: object | None = None, *args: t.Any, **kwargs: t.Any):
+ super().__init__(*args, **kwargs)
+
+ if arg is None:
+ KeyError.__init__(self)
+ else:
+ KeyError.__init__(self, arg)
+
+ @property
+ def description(self) -> str:
+ if self.show_exception:
+ return f"{self._description}\n{KeyError.__name__}: {KeyError.__str__(self)}"
+
+ return self._description
+
+ @description.setter
+ def description(self, value: str) -> None:
+ self._description = value
+
+
+class ClientDisconnected(BadRequest):
+ """Internal exception that is raised if Werkzeug detects a disconnected
+ client. Since the client is already gone at that point attempting to
+ send the error message to the client might not work and might ultimately
+ result in another exception in the server. Mainly this is here so that
+ it is silenced by default as far as Werkzeug is concerned.
+
+ Since disconnections cannot be reliably detected and are unspecified
+ by WSGI to a large extent this might or might not be raised if a client
+ is gone.
+
+ .. versionadded:: 0.8
+ """
+
+
+class SecurityError(BadRequest):
+ """Raised if something triggers a security error. This is otherwise
+ exactly like a bad request error.
+
+ .. versionadded:: 0.9
+ """
+
+
+class BadHost(BadRequest):
+ """Raised if the submitted host is badly formatted.
+
+ .. versionadded:: 0.11.2
+ """
+
+
+class Unauthorized(HTTPException):
+ """*401* ``Unauthorized``
+
+ Raise if the user is not authorized to access a resource.
+
+ The ``www_authenticate`` argument should be used to set the
+ ``WWW-Authenticate`` header. This is used for HTTP basic auth and
+ other schemes. Use :class:`~werkzeug.datastructures.WWWAuthenticate`
+ to create correctly formatted values. Strictly speaking a 401
+ response is invalid if it doesn't provide at least one value for
+ this header, although real clients typically don't care.
+
+ :param description: Override the default message used for the body
+ of the response.
+ :param www-authenticate: A single value, or list of values, for the
+ WWW-Authenticate header(s).
+
+ .. versionchanged:: 2.0
+ Serialize multiple ``www_authenticate`` items into multiple
+ ``WWW-Authenticate`` headers, rather than joining them
+ into a single value, for better interoperability.
+
+ .. versionchanged:: 0.15.3
+ If the ``www_authenticate`` argument is not set, the
+ ``WWW-Authenticate`` header is not set.
+
+ .. versionchanged:: 0.15.3
+ The ``response`` argument was restored.
+
+ .. versionchanged:: 0.15.1
+ ``description`` was moved back as the first argument, restoring
+ its previous position.
+
+ .. versionchanged:: 0.15.0
+ ``www_authenticate`` was added as the first argument, ahead of
+ ``description``.
+ """
+
+ code = 401
+ description = (
+ "The server could not verify that you are authorized to access"
+ " the URL requested. You either supplied the wrong credentials"
+ " (e.g. a bad password), or your browser doesn't understand"
+ " how to supply the credentials required."
+ )
+
+ def __init__(
+ self,
+ description: str | None = None,
+ response: SansIOResponse | None = None,
+ www_authenticate: None | (WWWAuthenticate | t.Iterable[WWWAuthenticate]) = None,
+ ) -> None:
+ super().__init__(description, response)
+
+ from .datastructures import WWWAuthenticate
+
+ if isinstance(www_authenticate, WWWAuthenticate):
+ www_authenticate = (www_authenticate,)
+
+ self.www_authenticate = www_authenticate
+
+ def get_headers(
+ self,
+ environ: WSGIEnvironment | None = None,
+ scope: dict[str, t.Any] | None = None,
+ ) -> list[tuple[str, str]]:
+ headers = super().get_headers(environ, scope)
+ if self.www_authenticate:
+ headers.extend(("WWW-Authenticate", str(x)) for x in self.www_authenticate)
+ return headers
+
+
+class Forbidden(HTTPException):
+ """*403* `Forbidden`
+
+ Raise if the user doesn't have the permission for the requested resource
+ but was authenticated.
+ """
+
+ code = 403
+ description = (
+ "You don't have the permission to access the requested"
+ " resource. It is either read-protected or not readable by the"
+ " server."
+ )
+
+
+class NotFound(HTTPException):
+ """*404* `Not Found`
+
+ Raise if a resource does not exist and never existed.
+ """
+
+ code = 404
+ description = (
+ "The requested URL was not found on the server. If you entered"
+ " the URL manually please check your spelling and try again."
+ )
+
+
+class MethodNotAllowed(HTTPException):
+ """*405* `Method Not Allowed`
+
+ Raise if the server used a method the resource does not handle. For
+ example `POST` if the resource is view only. Especially useful for REST.
+
+ The first argument for this exception should be a list of allowed methods.
+ Strictly speaking the response would be invalid if you don't provide valid
+ methods in the header which you can do with that list.
+ """
+
+ code = 405
+ description = "The method is not allowed for the requested URL."
+
+ def __init__(
+ self,
+ valid_methods: t.Iterable[str] | None = None,
+ description: str | None = None,
+ response: SansIOResponse | None = None,
+ ) -> None:
+ """Takes an optional list of valid http methods
+ starting with werkzeug 0.3 the list will be mandatory."""
+ super().__init__(description=description, response=response)
+ self.valid_methods = valid_methods
+
+ def get_headers(
+ self,
+ environ: WSGIEnvironment | None = None,
+ scope: dict[str, t.Any] | None = None,
+ ) -> list[tuple[str, str]]:
+ headers = super().get_headers(environ, scope)
+ if self.valid_methods:
+ headers.append(("Allow", ", ".join(self.valid_methods)))
+ return headers
+
+
+class NotAcceptable(HTTPException):
+ """*406* `Not Acceptable`
+
+ Raise if the server can't return any content conforming to the
+ `Accept` headers of the client.
+ """
+
+ code = 406
+ description = (
+ "The resource identified by the request is only capable of"
+ " generating response entities which have content"
+ " characteristics not acceptable according to the accept"
+ " headers sent in the request."
+ )
+
+
+class RequestTimeout(HTTPException):
+ """*408* `Request Timeout`
+
+ Raise to signalize a timeout.
+ """
+
+ code = 408
+ description = (
+ "The server closed the network connection because the browser"
+ " didn't finish the request within the specified time."
+ )
+
+
+class Conflict(HTTPException):
+ """*409* `Conflict`
+
+ Raise to signal that a request cannot be completed because it conflicts
+ with the current state on the server.
+
+ .. versionadded:: 0.7
+ """
+
+ code = 409
+ description = (
+ "A conflict happened while processing the request. The"
+ " resource might have been modified while the request was being"
+ " processed."
+ )
+
+
+class Gone(HTTPException):
+ """*410* `Gone`
+
+ Raise if a resource existed previously and went away without new location.
+ """
+
+ code = 410
+ description = (
+ "The requested URL is no longer available on this server and"
+ " there is no forwarding address. If you followed a link from a"
+ " foreign page, please contact the author of this page."
+ )
+
+
+class LengthRequired(HTTPException):
+ """*411* `Length Required`
+
+ Raise if the browser submitted data but no ``Content-Length`` header which
+ is required for the kind of processing the server does.
+ """
+
+ code = 411
+ description = (
+ "A request with this method requires a valid Content-"
+ "Length header."
+ )
+
+
+class PreconditionFailed(HTTPException):
+ """*412* `Precondition Failed`
+
+ Status code used in combination with ``If-Match``, ``If-None-Match``, or
+ ``If-Unmodified-Since``.
+ """
+
+ code = 412
+ description = (
+ "The precondition on the request for the URL failed positive evaluation."
+ )
+
+
+class RequestEntityTooLarge(HTTPException):
+ """*413* `Request Entity Too Large`
+
+ The status code one should return if the data submitted exceeded a given
+ limit.
+ """
+
+ code = 413
+ description = "The data value transmitted exceeds the capacity limit."
+
+
+class RequestURITooLarge(HTTPException):
+ """*414* `Request URI Too Large`
+
+ Like *413* but for too long URLs.
+ """
+
+ code = 414
+ description = (
+ "The length of the requested URL exceeds the capacity limit for"
+ " this server. The request cannot be processed."
+ )
+
+
+class UnsupportedMediaType(HTTPException):
+ """*415* `Unsupported Media Type`
+
+ The status code returned if the server is unable to handle the media type
+ the client transmitted.
+ """
+
+ code = 415
+ description = (
+ "The server does not support the media type transmitted in the request."
+ )
+
+
+class RequestedRangeNotSatisfiable(HTTPException):
+ """*416* `Requested Range Not Satisfiable`
+
+ The client asked for an invalid part of the file.
+
+ .. versionadded:: 0.7
+ """
+
+ code = 416
+ description = "The server cannot provide the requested range."
+
+ def __init__(
+ self,
+ length: int | None = None,
+ units: str = "bytes",
+ description: str | None = None,
+ response: SansIOResponse | None = None,
+ ) -> None:
+ """Takes an optional `Content-Range` header value based on ``length``
+ parameter.
+ """
+ super().__init__(description=description, response=response)
+ self.length = length
+ self.units = units
+
+ def get_headers(
+ self,
+ environ: WSGIEnvironment | None = None,
+ scope: dict[str, t.Any] | None = None,
+ ) -> list[tuple[str, str]]:
+ headers = super().get_headers(environ, scope)
+ if self.length is not None:
+ headers.append(("Content-Range", f"{self.units} */{self.length}"))
+ return headers
+
+
+class ExpectationFailed(HTTPException):
+ """*417* `Expectation Failed`
+
+ The server cannot meet the requirements of the Expect request-header.
+
+ .. versionadded:: 0.7
+ """
+
+ code = 417
+ description = "The server could not meet the requirements of the Expect header"
+
+
+class ImATeapot(HTTPException):
+ """*418* `I'm a teapot`
+
+ The server should return this if it is a teapot and someone attempted
+ to brew coffee with it.
+
+ .. versionadded:: 0.7
+ """
+
+ code = 418
+ description = "This server is a teapot, not a coffee machine"
+
+
+class MisdirectedRequest(HTTPException):
+ """421 Misdirected Request
+
+ Indicates that the request was directed to a server that is not able to
+ produce a response.
+
+ .. versionadded:: 3.1
+ """
+
+ code = 421
+ description = "The server is not able to produce a response."
+
+
+class UnprocessableEntity(HTTPException):
+ """*422* `Unprocessable Entity`
+
+ Used if the request is well formed, but the instructions are otherwise
+ incorrect.
+ """
+
+ code = 422
+ description = (
+ "The request was well-formed but was unable to be followed due"
+ " to semantic errors."
+ )
+
+
+class Locked(HTTPException):
+ """*423* `Locked`
+
+ Used if the resource that is being accessed is locked.
+ """
+
+ code = 423
+ description = "The resource that is being accessed is locked."
+
+
+class FailedDependency(HTTPException):
+ """*424* `Failed Dependency`
+
+ Used if the method could not be performed on the resource
+ because the requested action depended on another action and that action failed.
+ """
+
+ code = 424
+ description = (
+ "The method could not be performed on the resource because the"
+ " requested action depended on another action and that action"
+ " failed."
+ )
+
+
+class PreconditionRequired(HTTPException):
+ """*428* `Precondition Required`
+
+ The server requires this request to be conditional, typically to prevent
+ the lost update problem, which is a race condition between two or more
+ clients attempting to update a resource through PUT or DELETE. By requiring
+ each client to include a conditional header ("If-Match" or "If-Unmodified-
+ Since") with the proper value retained from a recent GET request, the
+ server ensures that each client has at least seen the previous revision of
+ the resource.
+ """
+
+ code = 428
+ description = (
+ "This request is required to be conditional; try using"
+ ' "If-Match" or "If-Unmodified-Since".'
+ )
+
+
+class _RetryAfter(HTTPException):
+ """Adds an optional ``retry_after`` parameter which will set the
+ ``Retry-After`` header. May be an :class:`int` number of seconds or
+ a :class:`~datetime.datetime`.
+ """
+
+ def __init__(
+ self,
+ description: str | None = None,
+ response: SansIOResponse | None = None,
+ retry_after: datetime | int | None = None,
+ ) -> None:
+ super().__init__(description, response)
+ self.retry_after = retry_after
+
+ def get_headers(
+ self,
+ environ: WSGIEnvironment | None = None,
+ scope: dict[str, t.Any] | None = None,
+ ) -> list[tuple[str, str]]:
+ headers = super().get_headers(environ, scope)
+
+ if self.retry_after:
+ if isinstance(self.retry_after, datetime):
+ from .http import http_date
+
+ value = http_date(self.retry_after)
+ else:
+ value = str(self.retry_after)
+
+ headers.append(("Retry-After", value))
+
+ return headers
+
+
+class TooManyRequests(_RetryAfter):
+ """*429* `Too Many Requests`
+
+ The server is limiting the rate at which this user receives
+ responses, and this request exceeds that rate. (The server may use
+ any convenient method to identify users and their request rates).
+ The server may include a "Retry-After" header to indicate how long
+ the user should wait before retrying.
+
+ :param retry_after: If given, set the ``Retry-After`` header to this
+ value. May be an :class:`int` number of seconds or a
+ :class:`~datetime.datetime`.
+
+ .. versionchanged:: 1.0
+ Added ``retry_after`` parameter.
+ """
+
+ code = 429
+ description = "This user has exceeded an allotted request count. Try again later."
+
+
+class RequestHeaderFieldsTooLarge(HTTPException):
+ """*431* `Request Header Fields Too Large`
+
+ The server refuses to process the request because the header fields are too
+ large. One or more individual fields may be too large, or the set of all
+ headers is too large.
+ """
+
+ code = 431
+ description = "One or more header fields exceeds the maximum size."
+
+
+class UnavailableForLegalReasons(HTTPException):
+ """*451* `Unavailable For Legal Reasons`
+
+ This status code indicates that the server is denying access to the
+ resource as a consequence of a legal demand.
+ """
+
+ code = 451
+ description = "Unavailable for legal reasons."
+
+
+class InternalServerError(HTTPException):
+ """*500* `Internal Server Error`
+
+ Raise if an internal server error occurred. This is a good fallback if an
+ unknown error occurred in the dispatcher.
+
+ .. versionchanged:: 1.0.0
+ Added the :attr:`original_exception` attribute.
+ """
+
+ code = 500
+ description = (
+ "The server encountered an internal error and was unable to"
+ " complete your request. Either the server is overloaded or"
+ " there is an error in the application."
+ )
+
+ def __init__(
+ self,
+ description: str | None = None,
+ response: SansIOResponse | None = None,
+ original_exception: BaseException | None = None,
+ ) -> None:
+ #: The original exception that caused this 500 error. Can be
+ #: used by frameworks to provide context when handling
+ #: unexpected errors.
+ self.original_exception = original_exception
+ super().__init__(description=description, response=response)
+
+
+class NotImplemented(HTTPException):
+ """*501* `Not Implemented`
+
+ Raise if the application does not support the action requested by the
+ browser.
+ """
+
+ code = 501
+ description = "The server does not support the action requested by the browser."
+
+
+class BadGateway(HTTPException):
+ """*502* `Bad Gateway`
+
+ If you do proxying in your application you should return this status code
+ if you received an invalid response from the upstream server it accessed
+ in attempting to fulfill the request.
+ """
+
+ code = 502
+ description = (
+ "The proxy server received an invalid response from an upstream server."
+ )
+
+
+class ServiceUnavailable(_RetryAfter):
+ """*503* `Service Unavailable`
+
+ Status code you should return if a service is temporarily
+ unavailable.
+
+ :param retry_after: If given, set the ``Retry-After`` header to this
+ value. May be an :class:`int` number of seconds or a
+ :class:`~datetime.datetime`.
+
+ .. versionchanged:: 1.0
+ Added ``retry_after`` parameter.
+ """
+
+ code = 503
+ description = (
+ "The server is temporarily unable to service your request due"
+ " to maintenance downtime or capacity problems. Please try"
+ " again later."
+ )
+
+
+class GatewayTimeout(HTTPException):
+ """*504* `Gateway Timeout`
+
+ Status code you should return if a connection to an upstream server
+ times out.
+ """
+
+ code = 504
+ description = "The connection to an upstream server timed out."
+
+
+class HTTPVersionNotSupported(HTTPException):
+ """*505* `HTTP Version Not Supported`
+
+ The server does not support the HTTP protocol version used in the request.
+ """
+
+ code = 505
+ description = (
+ "The server does not support the HTTP protocol version used in the request."
+ )
+
+
+default_exceptions: dict[int, type[HTTPException]] = {}
+
+
+def _find_exceptions() -> None:
+ for obj in globals().values():
+ try:
+ is_http_exception = issubclass(obj, HTTPException)
+ except TypeError:
+ is_http_exception = False
+ if not is_http_exception or obj.code is None:
+ continue
+ old_obj = default_exceptions.get(obj.code, None)
+ if old_obj is not None and issubclass(obj, old_obj):
+ continue
+ default_exceptions[obj.code] = obj
+
+
+_find_exceptions()
+del _find_exceptions
+
+
+class Aborter:
+ """When passed a dict of code -> exception items it can be used as
+ callable that raises exceptions. If the first argument to the
+ callable is an integer it will be looked up in the mapping, if it's
+ a WSGI application it will be raised in a proxy exception.
+
+ The rest of the arguments are forwarded to the exception constructor.
+ """
+
+ def __init__(
+ self,
+ mapping: dict[int, type[HTTPException]] | None = None,
+ extra: dict[int, type[HTTPException]] | None = None,
+ ) -> None:
+ if mapping is None:
+ mapping = default_exceptions
+ self.mapping = dict(mapping)
+ if extra is not None:
+ self.mapping.update(extra)
+
+ def __call__(
+ self, code: int | SansIOResponse, *args: t.Any, **kwargs: t.Any
+ ) -> t.NoReturn:
+ from .sansio.response import Response
+
+ if isinstance(code, Response):
+ raise HTTPException(response=code)
+
+ if code not in self.mapping:
+ raise LookupError(f"no exception for {code!r}")
+
+ raise self.mapping[code](*args, **kwargs)
+
+
+def abort(status: int | SansIOResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
+ """Raises an :py:exc:`HTTPException` for the given status code or WSGI
+ application.
+
+ If a status code is given, it will be looked up in the list of
+ exceptions and will raise that exception. If passed a WSGI application,
+ it will wrap it in a proxy WSGI exception and raise that::
+
+ abort(404) # 404 Not Found
+ abort(Response('Hello World'))
+
+ """
+ _aborter(status, *args, **kwargs)
+
+
+_aborter: Aborter = Aborter()
diff --git a/venv/Lib/site-packages/werkzeug/formparser.py b/venv/Lib/site-packages/werkzeug/formparser.py
new file mode 100644
index 0000000..0103414
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/formparser.py
@@ -0,0 +1,430 @@
+from __future__ import annotations
+
+import typing as t
+from io import BytesIO
+from urllib.parse import parse_qsl
+
+from ._internal import _plain_int
+from .datastructures import FileStorage
+from .datastructures import Headers
+from .datastructures import MultiDict
+from .exceptions import RequestEntityTooLarge
+from .http import parse_options_header
+from .sansio.multipart import Data
+from .sansio.multipart import Epilogue
+from .sansio.multipart import Field
+from .sansio.multipart import File
+from .sansio.multipart import MultipartDecoder
+from .sansio.multipart import NeedData
+from .wsgi import get_content_length
+from .wsgi import get_input_stream
+
+# there are some platforms where SpooledTemporaryFile is not available.
+# In that case we need to provide a fallback.
+try:
+ from tempfile import SpooledTemporaryFile
+except ImportError:
+ from tempfile import TemporaryFile
+
+ SpooledTemporaryFile = None # type: ignore
+
+if t.TYPE_CHECKING:
+ import typing as te
+
+ from _typeshed.wsgi import WSGIEnvironment
+
+ t_parse_result = tuple[
+ t.IO[bytes], MultiDict[str, str], MultiDict[str, FileStorage]
+ ]
+
+ class TStreamFactory(te.Protocol):
+ def __call__(
+ self,
+ total_content_length: int | None,
+ content_type: str | None,
+ filename: str | None,
+ content_length: int | None = None,
+ ) -> t.IO[bytes]: ...
+
+
+F = t.TypeVar("F", bound=t.Callable[..., t.Any])
+
+
+def default_stream_factory(
+ total_content_length: int | None,
+ content_type: str | None,
+ filename: str | None,
+ content_length: int | None = None,
+) -> t.IO[bytes]:
+ max_size = 1024 * 500
+
+ if SpooledTemporaryFile is not None:
+ return t.cast(t.IO[bytes], SpooledTemporaryFile(max_size=max_size, mode="rb+"))
+ elif total_content_length is None or total_content_length > max_size:
+ return t.cast(t.IO[bytes], TemporaryFile("rb+"))
+
+ return BytesIO()
+
+
+def parse_form_data(
+ environ: WSGIEnvironment,
+ stream_factory: TStreamFactory | None = None,
+ max_form_memory_size: int | None = None,
+ max_content_length: int | None = None,
+ cls: type[MultiDict[str, t.Any]] | None = None,
+ silent: bool = True,
+ *,
+ max_form_parts: int | None = None,
+) -> t_parse_result:
+ """Parse the form data in the environ and return it as tuple in the form
+ ``(stream, form, files)``. You should only call this method if the
+ transport method is `POST`, `PUT`, or `PATCH`.
+
+ If the mimetype of the data transmitted is `multipart/form-data` the
+ files multidict will be filled with `FileStorage` objects. If the
+ mimetype is unknown the input stream is wrapped and returned as first
+ argument, else the stream is empty.
+
+ This is a shortcut for the common usage of :class:`FormDataParser`.
+
+ :param environ: the WSGI environment to be used for parsing.
+ :param stream_factory: An optional callable that returns a new read and
+ writeable file descriptor. This callable works
+ the same as :meth:`Response._get_file_stream`.
+ :param max_form_memory_size: the maximum number of bytes to be accepted for
+ in-memory stored form data. If the data
+ exceeds the value specified an
+ :exc:`~exceptions.RequestEntityTooLarge`
+ exception is raised.
+ :param max_content_length: If this is provided and the transmitted data
+ is longer than this value an
+ :exc:`~exceptions.RequestEntityTooLarge`
+ exception is raised.
+ :param cls: an optional dict class to use. If this is not specified
+ or `None` the default :class:`MultiDict` is used.
+ :param silent: If set to False parsing errors will not be caught.
+ :param max_form_parts: The maximum number of multipart parts to be parsed. If this
+ is exceeded, a :exc:`~exceptions.RequestEntityTooLarge` exception is raised.
+ :return: A tuple in the form ``(stream, form, files)``.
+
+ .. versionchanged:: 3.0
+ The ``charset`` and ``errors`` parameters were removed.
+
+ .. versionchanged:: 2.3
+ Added the ``max_form_parts`` parameter.
+
+ .. versionadded:: 0.5.1
+ Added the ``silent`` parameter.
+
+ .. versionadded:: 0.5
+ Added the ``max_form_memory_size``, ``max_content_length``, and ``cls``
+ parameters.
+ """
+ return FormDataParser(
+ stream_factory=stream_factory,
+ max_form_memory_size=max_form_memory_size,
+ max_content_length=max_content_length,
+ max_form_parts=max_form_parts,
+ silent=silent,
+ cls=cls,
+ ).parse_from_environ(environ)
+
+
+class FormDataParser:
+ """This class implements parsing of form data for Werkzeug. By itself
+ it can parse multipart and url encoded form data. It can be subclassed
+ and extended but for most mimetypes it is a better idea to use the
+ untouched stream and expose it as separate attributes on a request
+ object.
+
+ :param stream_factory: An optional callable that returns a new read and
+ writeable file descriptor. This callable works
+ the same as :meth:`Response._get_file_stream`.
+ :param max_form_memory_size: the maximum number of bytes to be accepted for
+ in-memory stored form data. If the data
+ exceeds the value specified an
+ :exc:`~exceptions.RequestEntityTooLarge`
+ exception is raised.
+ :param max_content_length: If this is provided and the transmitted data
+ is longer than this value an
+ :exc:`~exceptions.RequestEntityTooLarge`
+ exception is raised.
+ :param cls: an optional dict class to use. If this is not specified
+ or `None` the default :class:`MultiDict` is used.
+ :param silent: If set to False parsing errors will not be caught.
+ :param max_form_parts: The maximum number of multipart parts to be parsed. If this
+ is exceeded, a :exc:`~exceptions.RequestEntityTooLarge` exception is raised.
+
+ .. versionchanged:: 3.0
+ The ``charset`` and ``errors`` parameters were removed.
+
+ .. versionchanged:: 3.0
+ The ``parse_functions`` attribute and ``get_parse_func`` methods were removed.
+
+ .. versionchanged:: 2.2.3
+ Added the ``max_form_parts`` parameter.
+
+ .. versionadded:: 0.8
+ """
+
+ def __init__(
+ self,
+ stream_factory: TStreamFactory | None = None,
+ max_form_memory_size: int | None = None,
+ max_content_length: int | None = None,
+ cls: type[MultiDict[str, t.Any]] | None = None,
+ silent: bool = True,
+ *,
+ max_form_parts: int | None = None,
+ ) -> None:
+ if stream_factory is None:
+ stream_factory = default_stream_factory
+
+ self.stream_factory = stream_factory
+ self.max_form_memory_size = max_form_memory_size
+ self.max_content_length = max_content_length
+ self.max_form_parts = max_form_parts
+
+ if cls is None:
+ cls = t.cast("type[MultiDict[str, t.Any]]", MultiDict)
+
+ self.cls = cls
+ self.silent = silent
+
+ def parse_from_environ(self, environ: WSGIEnvironment) -> t_parse_result:
+ """Parses the information from the environment as form data.
+
+ :param environ: the WSGI environment to be used for parsing.
+ :return: A tuple in the form ``(stream, form, files)``.
+ """
+ stream = get_input_stream(environ, max_content_length=self.max_content_length)
+ content_length = get_content_length(environ)
+ mimetype, options = parse_options_header(environ.get("CONTENT_TYPE"))
+ return self.parse(
+ stream,
+ content_length=content_length,
+ mimetype=mimetype,
+ options=options,
+ )
+
+ def parse(
+ self,
+ stream: t.IO[bytes],
+ mimetype: str,
+ content_length: int | None,
+ options: dict[str, str] | None = None,
+ ) -> t_parse_result:
+ """Parses the information from the given stream, mimetype,
+ content length and mimetype parameters.
+
+ :param stream: an input stream
+ :param mimetype: the mimetype of the data
+ :param content_length: the content length of the incoming data
+ :param options: optional mimetype parameters (used for
+ the multipart boundary for instance)
+ :return: A tuple in the form ``(stream, form, files)``.
+
+ .. versionchanged:: 3.0
+ The invalid ``application/x-url-encoded`` content type is not
+ treated as ``application/x-www-form-urlencoded``.
+ """
+ if mimetype == "multipart/form-data":
+ parse_func = self._parse_multipart
+ elif mimetype == "application/x-www-form-urlencoded":
+ parse_func = self._parse_urlencoded
+ else:
+ return stream, self.cls(), self.cls()
+
+ if options is None:
+ options = {}
+
+ try:
+ return parse_func(stream, mimetype, content_length, options)
+ except ValueError:
+ if not self.silent:
+ raise
+
+ return stream, self.cls(), self.cls()
+
+ def _parse_multipart(
+ self,
+ stream: t.IO[bytes],
+ mimetype: str,
+ content_length: int | None,
+ options: dict[str, str],
+ ) -> t_parse_result:
+ parser = MultiPartParser(
+ stream_factory=self.stream_factory,
+ max_form_memory_size=self.max_form_memory_size,
+ max_form_parts=self.max_form_parts,
+ cls=self.cls,
+ )
+ boundary = options.get("boundary", "").encode("ascii")
+
+ if not boundary:
+ raise ValueError("Missing boundary")
+
+ form, files = parser.parse(stream, boundary, content_length)
+ return stream, form, files
+
+ def _parse_urlencoded(
+ self,
+ stream: t.IO[bytes],
+ mimetype: str,
+ content_length: int | None,
+ options: dict[str, str],
+ ) -> t_parse_result:
+ if (
+ self.max_form_memory_size is not None
+ and content_length is not None
+ and content_length > self.max_form_memory_size
+ ):
+ raise RequestEntityTooLarge()
+
+ items = parse_qsl(
+ stream.read().decode(),
+ keep_blank_values=True,
+ errors="werkzeug.url_quote",
+ )
+ return stream, self.cls(items), self.cls()
+
+
+class MultiPartParser:
+ def __init__(
+ self,
+ stream_factory: TStreamFactory | None = None,
+ max_form_memory_size: int | None = None,
+ cls: type[MultiDict[str, t.Any]] | None = None,
+ buffer_size: int = 64 * 1024,
+ max_form_parts: int | None = None,
+ ) -> None:
+ self.max_form_memory_size = max_form_memory_size
+ self.max_form_parts = max_form_parts
+
+ if stream_factory is None:
+ stream_factory = default_stream_factory
+
+ self.stream_factory = stream_factory
+
+ if cls is None:
+ cls = t.cast("type[MultiDict[str, t.Any]]", MultiDict)
+
+ self.cls = cls
+ self.buffer_size = buffer_size
+
+ def fail(self, message: str) -> te.NoReturn:
+ raise ValueError(message)
+
+ def get_part_charset(self, headers: Headers) -> str:
+ # Figure out input charset for current part
+ content_type = headers.get("content-type")
+
+ if content_type:
+ parameters = parse_options_header(content_type)[1]
+ ct_charset = parameters.get("charset", "").lower()
+
+ # A safe list of encodings. Modern clients should only send ASCII or UTF-8.
+ # This list will not be extended further.
+ if ct_charset in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:
+ return ct_charset
+
+ return "utf-8"
+
+ def start_file_streaming(
+ self, event: File, total_content_length: int | None
+ ) -> t.IO[bytes]:
+ content_type = event.headers.get("content-type")
+
+ try:
+ content_length = _plain_int(event.headers["content-length"])
+ except (KeyError, ValueError):
+ content_length = 0
+
+ container = self.stream_factory(
+ total_content_length=total_content_length,
+ filename=event.filename,
+ content_type=content_type,
+ content_length=content_length,
+ )
+ return container
+
+ def parse(
+ self, stream: t.IO[bytes], boundary: bytes, content_length: int | None
+ ) -> tuple[MultiDict[str, str], MultiDict[str, FileStorage]]:
+ current_part: Field | File
+ field_size: int | None = None
+ container: t.IO[bytes] | list[bytes]
+ _write: t.Callable[[bytes], t.Any]
+
+ parser = MultipartDecoder(
+ boundary,
+ max_form_memory_size=self.max_form_memory_size,
+ max_parts=self.max_form_parts,
+ )
+
+ fields = []
+ files = []
+
+ for data in _chunk_iter(stream.read, self.buffer_size):
+ parser.receive_data(data)
+ event = parser.next_event()
+ while not isinstance(event, (Epilogue, NeedData)):
+ if isinstance(event, Field):
+ current_part = event
+ field_size = 0
+ container = []
+ _write = container.append
+ elif isinstance(event, File):
+ current_part = event
+ field_size = None
+ container = self.start_file_streaming(event, content_length)
+ _write = container.write
+ elif isinstance(event, Data):
+ if self.max_form_memory_size is not None and field_size is not None:
+ # Ensure that accumulated data events do not exceed limit.
+ # Also checked within single event in MultipartDecoder.
+ field_size += len(event.data)
+
+ if field_size > self.max_form_memory_size:
+ raise RequestEntityTooLarge()
+
+ _write(event.data)
+ if not event.more_data:
+ if isinstance(current_part, Field):
+ value = b"".join(container).decode(
+ self.get_part_charset(current_part.headers), "replace"
+ )
+ fields.append((current_part.name, value))
+ else:
+ container = t.cast(t.IO[bytes], container)
+ container.seek(0)
+ files.append(
+ (
+ current_part.name,
+ FileStorage(
+ container,
+ current_part.filename,
+ current_part.name,
+ headers=current_part.headers,
+ ),
+ )
+ )
+
+ event = parser.next_event()
+
+ return self.cls(fields), self.cls(files)
+
+
+def _chunk_iter(read: t.Callable[[int], bytes], size: int) -> t.Iterator[bytes | None]:
+ """Read data in chunks for multipart/form-data parsing. Stop if no data is read.
+ Yield ``None`` at the end to signal end of parsing.
+ """
+ while True:
+ data = read(size)
+
+ if not data:
+ break
+
+ yield data
+
+ yield None
diff --git a/venv/Lib/site-packages/werkzeug/http.py b/venv/Lib/site-packages/werkzeug/http.py
new file mode 100644
index 0000000..fc22faa
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/http.py
@@ -0,0 +1,1443 @@
+from __future__ import annotations
+
+import email.utils
+import re
+import typing as t
+import warnings
+from datetime import date
+from datetime import datetime
+from datetime import time
+from datetime import timedelta
+from datetime import timezone
+from enum import Enum
+from hashlib import sha1
+from time import mktime
+from time import struct_time
+from urllib.parse import quote
+from urllib.parse import unquote
+
+from ._internal import _dt_as_utc
+from ._internal import _plain_int
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import WSGIEnvironment
+
+_token_chars = frozenset(
+ "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"
+)
+_etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)')
+_entity_headers = frozenset(
+ [
+ "allow",
+ "content-encoding",
+ "content-language",
+ "content-length",
+ "content-location",
+ "content-md5",
+ "content-range",
+ "content-type",
+ "expires",
+ "last-modified",
+ ]
+)
+_hop_by_hop_headers = frozenset(
+ [
+ "connection",
+ "keep-alive",
+ "proxy-authenticate",
+ "proxy-authorization",
+ "te",
+ "trailer",
+ "transfer-encoding",
+ "upgrade",
+ ]
+)
+HTTP_STATUS_CODES = {
+ 100: "Continue",
+ 101: "Switching Protocols",
+ 102: "Processing",
+ 103: "Early Hints", # see RFC 8297
+ 200: "OK",
+ 201: "Created",
+ 202: "Accepted",
+ 203: "Non Authoritative Information",
+ 204: "No Content",
+ 205: "Reset Content",
+ 206: "Partial Content",
+ 207: "Multi Status",
+ 208: "Already Reported", # see RFC 5842
+ 226: "IM Used", # see RFC 3229
+ 300: "Multiple Choices",
+ 301: "Moved Permanently",
+ 302: "Found",
+ 303: "See Other",
+ 304: "Not Modified",
+ 305: "Use Proxy",
+ 306: "Switch Proxy", # unused
+ 307: "Temporary Redirect",
+ 308: "Permanent Redirect",
+ 400: "Bad Request",
+ 401: "Unauthorized",
+ 402: "Payment Required", # unused
+ 403: "Forbidden",
+ 404: "Not Found",
+ 405: "Method Not Allowed",
+ 406: "Not Acceptable",
+ 407: "Proxy Authentication Required",
+ 408: "Request Timeout",
+ 409: "Conflict",
+ 410: "Gone",
+ 411: "Length Required",
+ 412: "Precondition Failed",
+ 413: "Request Entity Too Large",
+ 414: "Request URI Too Long",
+ 415: "Unsupported Media Type",
+ 416: "Requested Range Not Satisfiable",
+ 417: "Expectation Failed",
+ 418: "I'm a teapot", # see RFC 2324
+ 421: "Misdirected Request", # see RFC 7540
+ 422: "Unprocessable Entity",
+ 423: "Locked",
+ 424: "Failed Dependency",
+ 425: "Too Early", # see RFC 8470
+ 426: "Upgrade Required",
+ 428: "Precondition Required", # see RFC 6585
+ 429: "Too Many Requests",
+ 431: "Request Header Fields Too Large",
+ 449: "Retry With", # proprietary MS extension
+ 451: "Unavailable For Legal Reasons",
+ 500: "Internal Server Error",
+ 501: "Not Implemented",
+ 502: "Bad Gateway",
+ 503: "Service Unavailable",
+ 504: "Gateway Timeout",
+ 505: "HTTP Version Not Supported",
+ 506: "Variant Also Negotiates", # see RFC 2295
+ 507: "Insufficient Storage",
+ 508: "Loop Detected", # see RFC 5842
+ 510: "Not Extended",
+ 511: "Network Authentication Failed",
+}
+
+
+class COEP(Enum):
+ """Cross Origin Embedder Policies"""
+
+ UNSAFE_NONE = "unsafe-none"
+ REQUIRE_CORP = "require-corp"
+
+
+class COOP(Enum):
+ """Cross Origin Opener Policies"""
+
+ UNSAFE_NONE = "unsafe-none"
+ SAME_ORIGIN_ALLOW_POPUPS = "same-origin-allow-popups"
+ SAME_ORIGIN = "same-origin"
+
+
+def quote_header_value(value: t.Any, allow_token: bool = True) -> str:
+ """Add double quotes around a header value. If the header contains only ASCII token
+ characters, it will be returned unchanged. If the header contains ``"`` or ``\\``
+ characters, they will be escaped with an additional ``\\`` character.
+
+ This is the reverse of :func:`unquote_header_value`.
+
+ :param value: The value to quote. Will be converted to a string.
+ :param allow_token: Disable to quote the value even if it only has token characters.
+
+ .. versionchanged:: 3.0
+ Passing bytes is not supported.
+
+ .. versionchanged:: 3.0
+ The ``extra_chars`` parameter is removed.
+
+ .. versionchanged:: 2.3
+ The value is quoted if it is the empty string.
+
+ .. versionadded:: 0.5
+ """
+ value_str = str(value)
+
+ if not value_str:
+ return '""'
+
+ if allow_token:
+ token_chars = _token_chars
+
+ if token_chars.issuperset(value_str):
+ return value_str
+
+ value_str = value_str.replace("\\", "\\\\").replace('"', '\\"')
+ return f'"{value_str}"'
+
+
+_unslash_re = re.compile(r"\\(.)", re.A)
+
+
+def unquote_header_value(value: str) -> str:
+ """Remove double quotes and backslash escapes from a header value.
+
+ This is the reverse of :func:`quote_header_value`.
+
+ :param value: The header value to unquote.
+
+ .. versionchanged:: 3.2
+ Removes escape preceding any character.
+
+ .. versionchanged:: 3.0
+ The ``is_filename`` parameter is removed.
+ """
+ if len(value) >= 2 and value[0] == value[-1] == '"':
+ return _unslash_re.sub(r"\g<1>", value[1:-1])
+
+ return value
+
+
+def dump_options_header(header: str | None, options: t.Mapping[str, t.Any]) -> str:
+ """Produce a header value and ``key=value`` parameters separated by semicolons
+ ``;``. For example, the ``Content-Type`` header.
+
+ .. code-block:: python
+
+ dump_options_header("text/html", {"charset": "UTF-8"})
+ 'text/html; charset=UTF-8'
+
+ This is the reverse of :func:`parse_options_header`.
+
+ If a value contains non-token characters, it will be quoted.
+
+ If a value is ``None``, the parameter is skipped.
+
+ In some keys for some headers, a UTF-8 value can be encoded using a special
+ ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will
+ not produce that format automatically, but if a given key ends with an asterisk
+ ``*``, the value is assumed to have that form and will not be quoted further.
+
+ :param header: The primary header value.
+ :param options: Parameters to encode as ``key=value`` pairs.
+
+ .. versionchanged:: 2.3
+ Keys with ``None`` values are skipped rather than treated as a bare key.
+
+ .. versionchanged:: 2.2.3
+ If a key ends with ``*``, its value will not be quoted.
+ """
+ segments = []
+
+ if header is not None:
+ segments.append(header)
+
+ for key, value in options.items():
+ if value is None:
+ continue
+
+ if key[-1] == "*":
+ segments.append(f"{key}={value}")
+ else:
+ segments.append(f"{key}={quote_header_value(value)}")
+
+ return "; ".join(segments)
+
+
+def dump_header(iterable: dict[str, t.Any] | t.Iterable[t.Any]) -> str:
+ """Produce a header value from a list of items or ``key=value`` pairs, separated by
+ commas ``,``.
+
+ This is the reverse of :func:`parse_list_header`, :func:`parse_dict_header`, and
+ :func:`parse_set_header`.
+
+ If a value contains non-token characters, it will be quoted.
+
+ If a value is ``None``, the key is output alone.
+
+ In some keys for some headers, a UTF-8 value can be encoded using a special
+ ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will
+ not produce that format automatically, but if a given key ends with an asterisk
+ ``*``, the value is assumed to have that form and will not be quoted further.
+
+ .. code-block:: python
+
+ dump_header(["foo", "bar baz"])
+ 'foo, "bar baz"'
+
+ dump_header({"foo": "bar baz"})
+ 'foo="bar baz"'
+
+ :param iterable: The items to create a header from.
+
+ .. versionchanged:: 3.0
+ The ``allow_token`` parameter is removed.
+
+ .. versionchanged:: 2.2.3
+ If a key ends with ``*``, its value will not be quoted.
+ """
+ if isinstance(iterable, dict):
+ items = []
+
+ for key, value in iterable.items():
+ if value is None:
+ items.append(key)
+ elif key[-1] == "*":
+ items.append(f"{key}={value}")
+ else:
+ items.append(f"{key}={quote_header_value(value)}")
+ else:
+ items = [quote_header_value(x) for x in iterable]
+
+ return ", ".join(items)
+
+
+def dump_csp_header(header: ds.ContentSecurityPolicy) -> str:
+ """Dump a Content Security Policy header.
+
+ These are structured into policies such as "default-src 'self';
+ script-src 'self'".
+
+ .. versionadded:: 1.0.0
+ Support for Content Security Policy headers was added.
+
+ """
+ return "; ".join(f"{key} {value}" for key, value in header.items())
+
+
+def parse_list_header(value: str) -> list[str]:
+ """Parse a header value that consists of a list of comma separated items according
+ to `RFC 9110 `__.
+
+ Surrounding quotes are removed from items, but internal quotes are left for
+ future parsing. Empty values are discarded.
+
+ .. code-block:: python
+
+ parse_list_header('token, "quoted value"')
+ ['token', 'quoted value']
+
+ This is the reverse of :func:`dump_header`.
+
+ :param value: The header value to parse.
+
+ .. versionchanged:: 3.2
+ Quotes and escapes are kept if only part of an item is quoted. Empty
+ values are omitted. An empty list is returned if the value contains an
+ unclosed quoted string.
+ """
+ items = []
+ item = ""
+ escape = False
+ quote = False
+
+ for char in value:
+ if escape:
+ escape = False
+ item += char
+ continue
+
+ if quote:
+ if char == "\\":
+ escape = True
+ elif char == '"':
+ quote = False
+
+ item += char
+ continue
+
+ if char == ",":
+ items.append(item)
+ item = ""
+ continue
+
+ if char == '"':
+ quote = True
+
+ item += char
+
+ if quote:
+ # invalid, unclosed quoted string
+ return []
+
+ items.append(item)
+ return [
+ unquote_header_value(item) for item in (item.strip() for item in items) if item
+ ]
+
+
+def parse_dict_header(value: str) -> dict[str, str | None]:
+ """Parse a list header using :func:`parse_list_header`, then parse each item as a
+ ``key=value`` pair.
+
+ .. code-block:: python
+
+ parse_dict_header('a=b, c="d, e", f')
+ {"a": "b", "c": "d, e", "f": None}
+
+ This is the reverse of :func:`dump_header`.
+
+ If a key does not have a value, it is ``None``.
+
+ This handles charsets for values as described in
+ `RFC 2231 `__. Only ASCII, UTF-8,
+ and ISO-8859-1 charsets are accepted, otherwise the value remains quoted.
+
+ :param value: The header value to parse.
+
+ .. versionchanged:: 3.2
+ An empty dict is returned if the value contains an unclosed quoted
+ string.
+
+ .. versionchanged:: 3.0
+ Passing bytes is not supported.
+
+ .. versionchanged:: 3.0
+ The ``cls`` argument is removed.
+
+ .. versionchanged:: 2.3
+ Added support for ``key*=charset''value`` encoded items.
+
+ .. versionchanged:: 0.9
+ The ``cls`` argument was added.
+ """
+ result: dict[str, str | None] = {}
+
+ for item in parse_list_header(value):
+ key, has_value, value = item.partition("=")
+ key = key.strip()
+
+ if not key:
+ # =value is not valid
+ continue
+
+ if not has_value:
+ result[key] = None
+ continue
+
+ value = value.strip()
+ encoding: str | None = None
+
+ if key[-1] == "*":
+ # key*=charset''value becomes key=value, where value is percent encoded
+ # adapted from parse_options_header, without the continuation handling
+ key = key[:-1]
+ match = _charset_value_re.match(value)
+
+ if match:
+ # If there is a charset marker in the value, split it off.
+ encoding, value = match.groups()
+ encoding = encoding.lower()
+
+ # A safe list of encodings. Modern clients should only send ASCII or UTF-8.
+ # This list will not be extended further. An invalid encoding will leave the
+ # value quoted.
+ if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:
+ # invalid bytes are replaced during unquoting
+ value = unquote(value, encoding=encoding)
+
+ result[key] = unquote_header_value(value)
+
+ return result
+
+
+# https://httpwg.org/specs/rfc9110.html#parameter
+_parameter_key_re = re.compile(r"([\w!#$%&'*+\-.^`|~]+)=", flags=re.ASCII)
+_parameter_token_value_re = re.compile(r"[\w!#$%&'*+\-.^`|~]+", flags=re.ASCII)
+# https://www.rfc-editor.org/rfc/rfc2231#section-4
+_charset_value_re = re.compile(
+ r"""
+ ([\w!#$%&*+\-.^`|~]*)' # charset part, could be empty
+ [\w!#$%&*+\-.^`|~]*' # don't care about language part, usually empty
+ ([\w!#$%&'*+\-.^`|~]+) # one or more token chars with percent encoding
+ """,
+ re.ASCII | re.VERBOSE,
+)
+# https://www.rfc-editor.org/rfc/rfc2231#section-3
+_continuation_re = re.compile(r"\*(\d+)$", re.ASCII)
+
+
+def parse_options_header(value: str | None) -> tuple[str, dict[str, str]]:
+ """Parse a header that consists of a value with ``key=value`` parameters separated
+ by semicolons ``;``. For example, the ``Content-Type`` header.
+
+ .. code-block:: python
+
+ parse_options_header("text/html; charset=UTF-8")
+ ('text/html', {'charset': 'UTF-8'})
+
+ parse_options_header("")
+ ("", {})
+
+ This is the reverse of :func:`dump_options_header`.
+
+ This parses valid parameter parts as described in
+ `RFC 9110 `__. Invalid parts are
+ skipped.
+
+ This handles continuations and charsets as described in
+ `RFC 2231 `__, although not as
+ strictly as the RFC. Only ASCII, UTF-8, and ISO-8859-1 charsets are accepted,
+ otherwise the value remains quoted.
+
+ Clients may not be consistent in how they handle a quote character within a quoted
+ value. The `HTML Standard `__
+ replaces it with ``%22`` in multipart form data.
+ `RFC 9110 `__ uses backslash
+ escapes in HTTP headers. Both are decoded to the ``"`` character.
+
+ Clients may not be consistent in how they handle non-ASCII characters. HTML
+ documents must declare ````, otherwise browsers may replace with
+ HTML character references, which can be decoded using :func:`html.unescape`.
+
+ :param value: The header value to parse.
+ :return: ``(value, options)``, where ``options`` is a dict
+
+ .. versionchanged:: 2.3
+ Invalid parts, such as keys with no value, quoted keys, and incorrectly quoted
+ values, are discarded instead of treating as ``None``.
+
+ .. versionchanged:: 2.3
+ Only ASCII, UTF-8, and ISO-8859-1 are accepted for charset values.
+
+ .. versionchanged:: 2.3
+ Escaped quotes in quoted values, like ``%22`` and ``\\"``, are handled.
+
+ .. versionchanged:: 2.2
+ Option names are always converted to lowercase.
+
+ .. versionchanged:: 2.2
+ The ``multiple`` parameter was removed.
+
+ .. versionchanged:: 0.15
+ :rfc:`2231` parameter continuations are handled.
+
+ .. versionadded:: 0.5
+ """
+ if value is None:
+ return "", {}
+
+ value, _, rest = value.partition(";")
+ value = value.strip()
+ rest = rest.strip()
+
+ if not value or not rest:
+ # empty (invalid) value, or value without options
+ return value, {}
+
+ # Collect all valid key=value parts without processing the value.
+ parts: list[tuple[str, str]] = []
+
+ while True:
+ if (m := _parameter_key_re.match(rest)) is not None:
+ pk = m.group(1).lower()
+ rest = rest[m.end() :]
+
+ # Value may be a token.
+ if (m := _parameter_token_value_re.match(rest)) is not None:
+ parts.append((pk, m.group()))
+
+ # Value may be a quoted string, find the closing quote.
+ elif rest[:1] == '"':
+ pos = 1
+ length = len(rest)
+
+ while pos < length:
+ if rest[pos : pos + 2] in {"\\\\", '\\"'}:
+ # Consume escaped slashes and quotes.
+ pos += 2
+ elif rest[pos] == '"':
+ # Stop at an unescaped quote.
+ parts.append((pk, rest[: pos + 1]))
+ rest = rest[pos + 1 :]
+ break
+ else:
+ # Consume any other character.
+ pos += 1
+
+ # Find the next section delimited by `;`, if any.
+ if (end := rest.find(";")) == -1:
+ break
+
+ rest = rest[end + 1 :].lstrip()
+
+ options: dict[str, str] = {}
+ encoding: str | None = None
+ continued_encoding: str | None = None
+
+ # For each collected part, process optional charset and continuation,
+ # unquote quoted values.
+ for pk, pv in parts:
+ if pk[-1] == "*":
+ # key*=charset''value becomes key=value, where value is percent encoded
+ pk = pk[:-1]
+ match = _charset_value_re.match(pv)
+
+ if match:
+ # If there is a valid charset marker in the value, split it off.
+ encoding, pv = match.groups()
+ # This might be the empty string, handled next.
+ encoding = encoding.lower()
+
+ # No charset marker, or marker with empty charset value.
+ if not encoding:
+ encoding = continued_encoding
+
+ # A safe list of encodings. Modern clients should only send ASCII or UTF-8.
+ # This list will not be extended further. An invalid encoding will leave the
+ # value quoted.
+ if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:
+ # Continuation parts don't require their own charset marker. This is
+ # looser than the RFC, it will persist across different keys and allows
+ # changing the charset during a continuation. But this implementation is
+ # much simpler than tracking the full state.
+ continued_encoding = encoding
+ # invalid bytes are replaced during unquoting
+ pv = unquote(pv, encoding=encoding)
+
+ # Remove quotes. At this point the value cannot be empty or a single quote.
+ if pv[0] == pv[-1] == '"':
+ # HTTP headers use slash, multipart form data uses percent
+ pv = pv[1:-1].replace("\\\\", "\\").replace('\\"', '"').replace("%22", '"')
+
+ match = _continuation_re.search(pk)
+
+ if match:
+ # key*0=a; key*1=b becomes key=ab
+ pk = pk[: match.start()]
+ options[pk] = options.get(pk, "") + pv
+ else:
+ options[pk] = pv
+
+ return value, options
+
+
+_q_value_re = re.compile(r"-?\d+(\.\d+)?", re.ASCII)
+_TAnyAccept = t.TypeVar("_TAnyAccept", bound="ds.Accept")
+
+
+@t.overload
+def parse_accept_header(value: str | None) -> ds.Accept: ...
+
+
+@t.overload
+def parse_accept_header(value: str | None, cls: type[_TAnyAccept]) -> _TAnyAccept: ...
+
+
+def parse_accept_header(
+ value: str | None, cls: type[_TAnyAccept] | None = None
+) -> _TAnyAccept:
+ """Parse an ``Accept`` header according to
+ `RFC 9110 `__.
+
+ Returns an :class:`.Accept` instance, which can sort and inspect items based on
+ their quality parameter. When parsing ``Accept-Charset``, ``Accept-Encoding``, or
+ ``Accept-Language``, pass the appropriate :class:`.Accept` subclass.
+
+ :param value: The header value to parse.
+ :param cls: The :class:`.Accept` class to wrap the result in.
+ :return: An instance of ``cls``.
+
+ .. versionchanged:: 2.3
+ Parse according to RFC 9110. Items with invalid ``q`` values are skipped.
+ """
+ if cls is None:
+ cls = t.cast(type[_TAnyAccept], ds.Accept)
+
+ if not value:
+ return cls(None)
+
+ result = []
+
+ for item in parse_list_header(value):
+ item, options = parse_options_header(item)
+
+ if "q" in options:
+ # pop q, remaining options are reconstructed
+ q_str = options.pop("q").strip()
+
+ if _q_value_re.fullmatch(q_str) is None:
+ # ignore an invalid q
+ continue
+
+ q = float(q_str)
+
+ if q < 0 or q > 1:
+ # ignore an invalid q
+ continue
+ else:
+ q = 1
+
+ if options:
+ # reconstruct the media type with any options
+ item = dump_options_header(item, options)
+
+ result.append((item, q))
+
+ return cls(result)
+
+
+_TAnyCC = t.TypeVar("_TAnyCC", bound="ds.cache_control._CacheControl")
+
+
+@t.overload
+def parse_cache_control_header(
+ value: str | None,
+ on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,
+) -> ds.RequestCacheControl: ...
+
+
+@t.overload
+def parse_cache_control_header(
+ value: str | None,
+ on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,
+ cls: type[_TAnyCC] = ...,
+) -> _TAnyCC: ...
+
+
+def parse_cache_control_header(
+ value: str | None,
+ on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None,
+ cls: type[_TAnyCC] | None = None,
+) -> _TAnyCC:
+ """Parse a cache control header. The RFC differs between response and
+ request cache control, this method does not. It's your responsibility
+ to not use the wrong control statements.
+
+ .. versionadded:: 0.5
+ The `cls` was added. If not specified an immutable
+ :class:`~werkzeug.datastructures.RequestCacheControl` is returned.
+
+ :param value: a cache control header to be parsed.
+ :param on_update: an optional callable that is called every time a value
+ on the :class:`~werkzeug.datastructures.CacheControl`
+ object is changed.
+ :param cls: the class for the returned object. By default
+ :class:`~werkzeug.datastructures.RequestCacheControl` is used.
+ :return: a `cls` object.
+ """
+ if cls is None:
+ cls = t.cast("type[_TAnyCC]", ds.RequestCacheControl)
+
+ if not value:
+ return cls((), on_update)
+
+ return cls(parse_dict_header(value), on_update)
+
+
+_TAnyCSP = t.TypeVar("_TAnyCSP", bound="ds.ContentSecurityPolicy")
+
+
+@t.overload
+def parse_csp_header(
+ value: str | None,
+ on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,
+) -> ds.ContentSecurityPolicy: ...
+
+
+@t.overload
+def parse_csp_header(
+ value: str | None,
+ on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,
+ cls: type[_TAnyCSP] = ...,
+) -> _TAnyCSP: ...
+
+
+def parse_csp_header(
+ value: str | None,
+ on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None,
+ cls: type[_TAnyCSP] | None = None,
+) -> _TAnyCSP:
+ """Parse a Content Security Policy header.
+
+ .. versionadded:: 1.0.0
+ Support for Content Security Policy headers was added.
+
+ :param value: a csp header to be parsed.
+ :param on_update: an optional callable that is called every time a value
+ on the object is changed.
+ :param cls: the class for the returned object. By default
+ :class:`~werkzeug.datastructures.ContentSecurityPolicy` is used.
+ :return: a `cls` object.
+ """
+ if cls is None:
+ cls = t.cast("type[_TAnyCSP]", ds.ContentSecurityPolicy)
+
+ if value is None:
+ return cls((), on_update)
+
+ items = []
+
+ for policy in value.split(";"):
+ policy = policy.strip()
+
+ # Ignore badly formatted policies (no space)
+ if " " in policy:
+ directive, value = policy.strip().split(" ", 1)
+ items.append((directive.strip(), value.strip()))
+
+ return cls(items, on_update)
+
+
+def parse_set_header(
+ value: str | None,
+ on_update: t.Callable[[ds.HeaderSet], None] | None = None,
+) -> ds.HeaderSet:
+ """Parse a set-like header and return a
+ :class:`~werkzeug.datastructures.HeaderSet` object:
+
+ >>> hs = parse_set_header('token, "quoted value"')
+
+ The return value is an object that treats the items case-insensitively
+ and keeps the order of the items:
+
+ >>> 'TOKEN' in hs
+ True
+ >>> hs.index('quoted value')
+ 1
+ >>> hs
+ HeaderSet(['token', 'quoted value'])
+
+ To create a header from the :class:`HeaderSet` again, use the
+ :func:`dump_header` function.
+
+ :param value: a set header to be parsed.
+ :param on_update: an optional callable that is called every time a
+ value on the :class:`~werkzeug.datastructures.HeaderSet`
+ object is changed.
+ :return: a :class:`~werkzeug.datastructures.HeaderSet`
+ """
+ if not value:
+ return ds.HeaderSet(None, on_update)
+ return ds.HeaderSet(parse_list_header(value), on_update)
+
+
+def parse_if_range_header(value: str | None) -> ds.IfRange:
+ """Parses an if-range header which can be an etag or a date. Returns
+ a :class:`~werkzeug.datastructures.IfRange` object.
+
+ .. versionchanged:: 2.0
+ If the value represents a datetime, it is timezone-aware.
+
+ .. versionadded:: 0.7
+ """
+ if not value:
+ return ds.IfRange()
+ date = parse_date(value)
+ if date is not None:
+ return ds.IfRange(date=date)
+ # drop weakness information
+ return ds.IfRange(unquote_etag(value)[0])
+
+
+def parse_range_header(
+ value: str | None, make_inclusive: bool = True
+) -> ds.Range | None:
+ """Parses a range header into a :class:`~werkzeug.datastructures.Range`
+ object. If the header is missing or malformed `None` is returned.
+ `ranges` is a list of ``(start, stop)`` tuples where the ranges are
+ non-inclusive.
+
+ .. versionadded:: 0.7
+ """
+ if not value or "=" not in value:
+ return None
+
+ ranges = []
+ last_end = 0
+ units, rng = value.split("=", 1)
+ units = units.strip().lower()
+
+ for item in rng.split(","):
+ item = item.strip()
+ if "-" not in item:
+ return None
+ if item.startswith("-"):
+ if last_end < 0:
+ return None
+ try:
+ begin = _plain_int(item)
+ except ValueError:
+ return None
+ end = None
+ last_end = -1
+ elif "-" in item:
+ begin_str, end_str = item.split("-", 1)
+ begin_str = begin_str.strip()
+ end_str = end_str.strip()
+
+ try:
+ begin = _plain_int(begin_str)
+ except ValueError:
+ return None
+
+ if begin < last_end or last_end < 0:
+ return None
+ if end_str:
+ try:
+ end = _plain_int(end_str) + 1
+ except ValueError:
+ return None
+
+ if begin >= end:
+ return None
+ else:
+ end = None
+ last_end = end if end is not None else -1
+ ranges.append((begin, end))
+
+ return ds.Range(units, ranges)
+
+
+def parse_content_range_header(
+ value: str | None,
+ on_update: t.Callable[[ds.ContentRange], None] | None = None,
+) -> ds.ContentRange | None:
+ """Parses a range header into a
+ :class:`~werkzeug.datastructures.ContentRange` object or `None` if
+ parsing is not possible.
+
+ .. versionadded:: 0.7
+
+ :param value: a content range header to be parsed.
+ :param on_update: an optional callable that is called every time a value
+ on the :class:`~werkzeug.datastructures.ContentRange`
+ object is changed.
+ """
+ if value is None:
+ return None
+ try:
+ units, rangedef = (value or "").strip().split(None, 1)
+ except ValueError:
+ return None
+
+ if "/" not in rangedef:
+ return None
+ rng, length_str = rangedef.split("/", 1)
+ if length_str == "*":
+ length = None
+ else:
+ try:
+ length = _plain_int(length_str)
+ except ValueError:
+ return None
+
+ if rng == "*":
+ if not is_byte_range_valid(None, None, length):
+ return None
+
+ return ds.ContentRange(units, None, None, length, on_update=on_update)
+ elif "-" not in rng:
+ return None
+
+ start_str, stop_str = rng.split("-", 1)
+ try:
+ start = _plain_int(start_str)
+ stop = _plain_int(stop_str) + 1
+ except ValueError:
+ return None
+
+ if is_byte_range_valid(start, stop, length):
+ return ds.ContentRange(units, start, stop, length, on_update=on_update)
+
+ return None
+
+
+def quote_etag(etag: str, weak: bool = False) -> str:
+ """Quote an etag.
+
+ :param etag: the etag to quote.
+ :param weak: set to `True` to tag it "weak".
+ """
+ if '"' in etag:
+ raise ValueError("invalid etag")
+ etag = f'"{etag}"'
+ if weak:
+ etag = f"W/{etag}"
+ return etag
+
+
+@t.overload
+def unquote_etag(etag: str) -> tuple[str, bool]: ...
+@t.overload
+def unquote_etag(etag: None) -> tuple[None, None]: ...
+def unquote_etag(
+ etag: str | None,
+) -> tuple[str, bool] | tuple[None, None]:
+ """Unquote a single etag:
+
+ >>> unquote_etag('W/"bar"')
+ ('bar', True)
+ >>> unquote_etag('"bar"')
+ ('bar', False)
+
+ :param etag: the etag identifier to unquote.
+ :return: a ``(etag, weak)`` tuple.
+ """
+ if not etag:
+ return None, None
+ etag = etag.strip()
+ weak = False
+ if etag.startswith(("W/", "w/")):
+ weak = True
+ etag = etag[2:]
+ if etag[:1] == etag[-1:] == '"':
+ etag = etag[1:-1]
+ return etag, weak
+
+
+def parse_etags(value: str | None) -> ds.ETags:
+ """Parse an etag header.
+
+ :param value: the tag header to parse
+ :return: an :class:`~werkzeug.datastructures.ETags` object.
+ """
+ if not value:
+ return ds.ETags()
+ strong = []
+ weak = []
+ end = len(value)
+ pos = 0
+ while pos < end:
+ match = _etag_re.match(value, pos)
+ if match is None:
+ break
+ is_weak, quoted, raw = match.groups()
+ if raw == "*":
+ return ds.ETags(star_tag=True)
+ elif quoted:
+ raw = quoted
+ if is_weak:
+ weak.append(raw)
+ else:
+ strong.append(raw)
+ pos = match.end()
+ return ds.ETags(strong, weak)
+
+
+def generate_etag(data: bytes) -> str:
+ """Generate an etag for some data.
+
+ .. versionchanged:: 2.0
+ Use SHA-1. MD5 may not be available in some environments.
+ """
+ return sha1(data).hexdigest()
+
+
+def parse_date(value: str | None) -> datetime | None:
+ """Parse an :rfc:`2822` date into a timezone-aware
+ :class:`datetime.datetime` object, or ``None`` if parsing fails.
+
+ This is a wrapper for :func:`email.utils.parsedate_to_datetime`. It
+ returns ``None`` if parsing fails instead of raising an exception,
+ and always returns a timezone-aware datetime object. If the string
+ doesn't have timezone information, it is assumed to be UTC.
+
+ :param value: A string with a supported date format.
+
+ .. versionchanged:: 2.0
+ Return a timezone-aware datetime object. Use
+ ``email.utils.parsedate_to_datetime``.
+ """
+ if value is None:
+ return None
+
+ try:
+ dt = email.utils.parsedate_to_datetime(value)
+ except (TypeError, ValueError):
+ return None
+
+ if dt.tzinfo is None:
+ return dt.replace(tzinfo=timezone.utc)
+
+ return dt
+
+
+def http_date(
+ timestamp: datetime | date | int | float | struct_time | None = None,
+) -> str:
+ """Format a datetime object or timestamp into an :rfc:`2822` date
+ string.
+
+ This is a wrapper for :func:`email.utils.format_datetime`. It
+ assumes naive datetime objects are in UTC instead of raising an
+ exception.
+
+ :param timestamp: The datetime or timestamp to format. Defaults to
+ the current time.
+
+ .. versionchanged:: 2.0
+ Use ``email.utils.format_datetime``. Accept ``date`` objects.
+ """
+ if isinstance(timestamp, date):
+ if not isinstance(timestamp, datetime):
+ # Assume plain date is midnight UTC.
+ timestamp = datetime.combine(timestamp, time(), tzinfo=timezone.utc)
+ else:
+ # Ensure datetime is timezone-aware.
+ timestamp = _dt_as_utc(timestamp)
+
+ return email.utils.format_datetime(timestamp, usegmt=True)
+
+ if isinstance(timestamp, struct_time):
+ timestamp = mktime(timestamp)
+
+ return email.utils.formatdate(timestamp, usegmt=True)
+
+
+def parse_age(value: str | None = None) -> timedelta | None:
+ """Parses a base-10 integer count of seconds into a timedelta.
+
+ If parsing fails, the return value is `None`.
+
+ :param value: a string consisting of an integer represented in base-10
+ :return: a :class:`datetime.timedelta` object or `None`.
+ """
+ if not value:
+ return None
+ try:
+ seconds = int(value)
+ except ValueError:
+ return None
+ if seconds < 0:
+ return None
+ try:
+ return timedelta(seconds=seconds)
+ except OverflowError:
+ return None
+
+
+def dump_age(age: timedelta | int | None = None) -> str | None:
+ """Formats the duration as a base-10 integer.
+
+ :param age: should be an integer number of seconds,
+ a :class:`datetime.timedelta` object, or,
+ if the age is unknown, `None` (default).
+ """
+ if age is None:
+ return None
+ if isinstance(age, timedelta):
+ age = int(age.total_seconds())
+ else:
+ age = int(age)
+
+ if age < 0:
+ raise ValueError("age cannot be negative")
+
+ return str(age)
+
+
+def is_resource_modified(
+ environ: WSGIEnvironment,
+ etag: str | None = None,
+ data: bytes | None = None,
+ last_modified: datetime | str | None = None,
+ ignore_if_range: bool = True,
+) -> bool:
+ """Convenience method for conditional requests.
+
+ :param environ: the WSGI environment of the request to be checked.
+ :param etag: the etag for the response for comparison.
+ :param data: or alternatively the data of the response to automatically
+ generate an etag using :func:`generate_etag`.
+ :param last_modified: an optional date of the last modification.
+ :param ignore_if_range: If `False`, `If-Range` header will be taken into
+ account.
+ :return: `True` if the resource was modified, otherwise `False`.
+
+ .. versionchanged:: 2.0
+ SHA-1 is used to generate an etag value for the data. MD5 may
+ not be available in some environments.
+
+ .. versionchanged:: 1.0.0
+ The check is run for methods other than ``GET`` and ``HEAD``.
+ """
+ return _sansio_http.is_resource_modified(
+ http_range=environ.get("HTTP_RANGE"),
+ http_if_range=environ.get("HTTP_IF_RANGE"),
+ http_if_modified_since=environ.get("HTTP_IF_MODIFIED_SINCE"),
+ http_if_none_match=environ.get("HTTP_IF_NONE_MATCH"),
+ http_if_match=environ.get("HTTP_IF_MATCH"),
+ etag=etag,
+ data=data,
+ last_modified=last_modified,
+ ignore_if_range=ignore_if_range,
+ )
+
+
+def remove_entity_headers(
+ headers: ds.Headers | list[tuple[str, str]],
+ allowed: t.Iterable[str] = ("expires", "content-location"),
+) -> None:
+ """Remove all entity headers from a list or :class:`Headers` object. This
+ operation works in-place. `Expires` and `Content-Location` headers are
+ by default not removed. The reason for this is :rfc:`2616` section
+ 10.3.5 which specifies some entity headers that should be sent.
+
+ .. versionchanged:: 0.5
+ added `allowed` parameter.
+
+ :param headers: a list or :class:`Headers` object.
+ :param allowed: a list of headers that should still be allowed even though
+ they are entity headers.
+ """
+ allowed = {x.lower() for x in allowed}
+ headers[:] = [
+ (key, value)
+ for key, value in headers
+ if not is_entity_header(key) or key.lower() in allowed
+ ]
+
+
+def remove_hop_by_hop_headers(headers: ds.Headers | list[tuple[str, str]]) -> None:
+ """Remove all HTTP/1.1 "Hop-by-Hop" headers from a list or
+ :class:`Headers` object. This operation works in-place.
+
+ .. versionadded:: 0.5
+
+ :param headers: a list or :class:`Headers` object.
+ """
+ headers[:] = [
+ (key, value) for key, value in headers if not is_hop_by_hop_header(key)
+ ]
+
+
+def is_entity_header(header: str) -> bool:
+ """Check if a header is an entity header.
+
+ .. versionadded:: 0.5
+
+ :param header: the header to test.
+ :return: `True` if it's an entity header, `False` otherwise.
+ """
+ return header.lower() in _entity_headers
+
+
+def is_hop_by_hop_header(header: str) -> bool:
+ """Check if a header is an HTTP/1.1 "Hop-by-Hop" header.
+
+ .. versionadded:: 0.5
+
+ :param header: the header to test.
+ :return: `True` if it's an HTTP/1.1 "Hop-by-Hop" header, `False` otherwise.
+ """
+ return header.lower() in _hop_by_hop_headers
+
+
+def parse_cookie(
+ header: WSGIEnvironment | str | None,
+ cls: type[ds.MultiDict[str, str]] | None = None,
+) -> ds.MultiDict[str, str]:
+ """Parse a cookie from a string or WSGI environ.
+
+ The same key can be provided multiple times, the values are stored
+ in-order. The default :class:`MultiDict` will have the first value
+ first, and all values can be retrieved with
+ :meth:`MultiDict.getlist`.
+
+ :param header: The cookie header as a string, or a WSGI environ dict
+ with a ``HTTP_COOKIE`` key.
+ :param cls: A dict-like class to store the parsed cookies in.
+ Defaults to :class:`MultiDict`.
+
+ .. versionchanged:: 3.0
+ Passing bytes, and the ``charset`` and ``errors`` parameters, were removed.
+
+ .. versionchanged:: 1.0
+ Returns a :class:`MultiDict` instead of a ``TypeConversionDict``.
+
+ .. versionchanged:: 0.5
+ Returns a :class:`TypeConversionDict` instead of a regular dict. The ``cls``
+ parameter was added.
+ """
+ if isinstance(header, dict):
+ cookie = header.get("HTTP_COOKIE")
+ else:
+ cookie = header
+
+ if cookie:
+ cookie = cookie.encode("latin1").decode()
+
+ return _sansio_http.parse_cookie(cookie=cookie, cls=cls)
+
+
+_cookie_no_quote_re = re.compile(r"[\w!#$%&'()*+\-./:<=>?@\[\]^`{|}~]*", re.A)
+_cookie_slash_re = re.compile(rb"[\x00-\x19\",;\\\x7f-\xff]", re.A)
+_cookie_slash_map = {b'"': b'\\"', b"\\": b"\\\\"}
+_cookie_slash_map.update(
+ (v.to_bytes(1, "big"), b"\\%03o" % v)
+ for v in [*range(0x20), *b",;", *range(0x7F, 256)]
+)
+
+
+def dump_cookie(
+ key: str,
+ value: str = "",
+ max_age: timedelta | int | None = None,
+ expires: str | datetime | int | float | None = None,
+ path: str | None = "/",
+ domain: str | None = None,
+ secure: bool = False,
+ httponly: bool = False,
+ sync_expires: bool = True,
+ max_size: int = 4093,
+ samesite: str | None = None,
+ partitioned: bool = False,
+) -> str:
+ """Create a Set-Cookie header without the ``Set-Cookie`` prefix.
+
+ The return value is usually restricted to ascii as the vast majority
+ of values are properly escaped, but that is no guarantee. It's
+ tunneled through latin1 as required by :pep:`3333`.
+
+ The return value is not ASCII safe if the key contains unicode
+ characters. This is technically against the specification but
+ happens in the wild. It's strongly recommended to not use
+ non-ASCII values for the keys.
+
+ :param max_age: should be a number of seconds, or `None` (default) if
+ the cookie should last only as long as the client's
+ browser session. Additionally `timedelta` objects
+ are accepted, too.
+ :param expires: should be a `datetime` object or unix timestamp.
+ :param path: limits the cookie to a given path, per default it will
+ span the whole domain.
+ :param domain: Use this if you want to set a cross-domain cookie. For
+ example, ``domain="example.com"`` will set a cookie
+ that is readable by the domain ``www.example.com``,
+ ``foo.example.com`` etc. Otherwise, a cookie will only
+ be readable by the domain that set it.
+ :param secure: The cookie will only be available via HTTPS
+ :param httponly: disallow JavaScript to access the cookie. This is an
+ extension to the cookie standard and probably not
+ supported by all browsers.
+ :param charset: the encoding for string values.
+ :param sync_expires: automatically set expires if max_age is defined
+ but expires not.
+ :param max_size: Warn if the final header value exceeds this size. The
+ default, 4093, should be safely `supported by most browsers
+ `_. Set to 0 to disable this check.
+ :param samesite: Limits the scope of the cookie such that it will
+ only be attached to requests if those requests are same-site.
+ :param partitioned: Opts the cookie into partitioned storage. This
+ will also set secure to True
+
+ .. _`cookie`: http://browsercookielimits.squawky.net/
+
+ .. versionchanged:: 3.1
+ The ``partitioned`` parameter was added.
+
+ .. versionchanged:: 3.0
+ Passing bytes, and the ``charset`` parameter, were removed.
+
+ .. versionchanged:: 2.3.3
+ The ``path`` parameter is ``/`` by default.
+
+ .. versionchanged:: 2.3.1
+ The value allows more characters without quoting.
+
+ .. versionchanged:: 2.3
+ ``localhost`` and other names without a dot are allowed for the domain. A
+ leading dot is ignored.
+
+ .. versionchanged:: 2.3
+ The ``path`` parameter is ``None`` by default.
+
+ .. versionchanged:: 1.0.0
+ The string ``'None'`` is accepted for ``samesite``.
+ """
+ if path is not None:
+ # safe = https://url.spec.whatwg.org/#url-path-segment-string
+ # as well as percent for things that are already quoted
+ # excluding semicolon since it's part of the header syntax
+ path = quote(path, safe="%!$&'()*+,/:=@")
+
+ if domain:
+ domain = domain.partition(":")[0].lstrip(".").encode("idna").decode("ascii")
+
+ if isinstance(max_age, timedelta):
+ max_age = int(max_age.total_seconds())
+
+ if expires is not None:
+ if not isinstance(expires, str):
+ expires = http_date(expires)
+ elif max_age is not None and sync_expires:
+ expires = http_date(datetime.now(tz=timezone.utc).timestamp() + max_age)
+
+ if samesite is not None:
+ samesite = samesite.title()
+
+ if samesite not in {"Strict", "Lax", "None"}:
+ raise ValueError("SameSite must be 'Strict', 'Lax', or 'None'.")
+
+ if partitioned:
+ secure = True
+
+ # Quote value if it contains characters not allowed by RFC 6265. Slash-escape with
+ # three octal digits, which matches http.cookies, although the RFC suggests base64.
+ if not _cookie_no_quote_re.fullmatch(value):
+ # Work with bytes here, since a UTF-8 character could be multiple bytes.
+ value = _cookie_slash_re.sub(
+ lambda m: _cookie_slash_map[m.group()], value.encode()
+ ).decode("ascii")
+ value = f'"{value}"'
+
+ # Send a non-ASCII key as mojibake. Everything else should already be ASCII.
+ # TODO Remove encoding dance, it seems like clients accept UTF-8 keys
+ buf = [f"{key.encode().decode('latin1')}={value}"]
+
+ for k, v in (
+ ("Domain", domain),
+ ("Expires", expires),
+ ("Max-Age", max_age),
+ ("Secure", secure),
+ ("HttpOnly", httponly),
+ ("Path", path),
+ ("SameSite", samesite),
+ ("Partitioned", partitioned),
+ ):
+ if v is None or v is False:
+ continue
+
+ if v is True:
+ buf.append(k)
+ continue
+
+ buf.append(f"{k}={v}")
+
+ rv = "; ".join(buf)
+
+ # Warn if the final value of the cookie is larger than the limit. If the cookie is
+ # too large, then it may be silently ignored by the browser, which can be quite hard
+ # to debug.
+ cookie_size = len(rv)
+
+ if max_size and cookie_size > max_size:
+ value_size = len(value)
+ warnings.warn(
+ f"The '{key}' cookie is too large: the value was {value_size} bytes but the"
+ f" header required {cookie_size - value_size} extra bytes. The final size"
+ f" was {cookie_size} bytes but the limit is {max_size} bytes. Browsers may"
+ " silently ignore cookies larger than this.",
+ stacklevel=2,
+ )
+
+ return rv
+
+
+def is_byte_range_valid(
+ start: int | None, stop: int | None, length: int | None
+) -> bool:
+ """Checks if a given byte content range is valid for the given length.
+
+ .. versionadded:: 0.7
+ """
+ if (start is None) != (stop is None):
+ return False
+ elif start is None:
+ return length is None or length >= 0
+ elif length is None:
+ return 0 <= start < stop # type: ignore
+ elif start >= stop: # type: ignore
+ return False
+ return 0 <= start < length
+
+
+# circular dependencies
+from . import datastructures as ds # noqa: E402
+from .sansio import http as _sansio_http # noqa: E402
diff --git a/venv/Lib/site-packages/werkzeug/local.py b/venv/Lib/site-packages/werkzeug/local.py
new file mode 100644
index 0000000..32c1aca
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/local.py
@@ -0,0 +1,653 @@
+from __future__ import annotations
+
+import copy
+import math
+import operator
+import typing as t
+from contextvars import ContextVar
+from functools import partial
+from functools import update_wrapper
+from operator import attrgetter
+
+from .wsgi import ClosingIterator
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import StartResponse
+ from _typeshed.wsgi import WSGIApplication
+ from _typeshed.wsgi import WSGIEnvironment
+
+T = t.TypeVar("T")
+F = t.TypeVar("F", bound=t.Callable[..., t.Any])
+
+
+def release_local(local: Local | LocalStack[t.Any]) -> None:
+ """Release the data for the current context in a :class:`Local` or
+ :class:`LocalStack` without using a :class:`LocalManager`.
+
+ This should not be needed for modern use cases, and may be removed
+ in the future.
+
+ .. versionadded:: 0.6.1
+ """
+ local.__release_local__()
+
+
+class Local:
+ """Create a namespace of context-local data. This wraps a
+ :class:`ContextVar` containing a :class:`dict` value.
+
+ This may incur a performance penalty compared to using individual
+ context vars, as it has to copy data to avoid mutating the dict
+ between nested contexts.
+
+ :param context_var: The :class:`~contextvars.ContextVar` to use as
+ storage for this local. If not given, one will be created.
+ Context vars not created at the global scope may interfere with
+ garbage collection.
+
+ .. versionchanged:: 2.0
+ Uses ``ContextVar`` instead of a custom storage implementation.
+ """
+
+ __slots__ = ("__storage",)
+
+ def __init__(self, context_var: ContextVar[dict[str, t.Any]] | None = None) -> None:
+ if context_var is None:
+ # A ContextVar not created at global scope interferes with
+ # Python's garbage collection. However, a local only makes
+ # sense defined at the global scope as well, in which case
+ # the GC issue doesn't seem relevant.
+ context_var = ContextVar(f"werkzeug.Local<{id(self)}>.storage")
+
+ object.__setattr__(self, "_Local__storage", context_var)
+
+ def __iter__(self) -> t.Iterator[tuple[str, t.Any]]:
+ return iter(self.__storage.get({}).items())
+
+ def __call__(
+ self, name: str, *, unbound_message: str | None = None
+ ) -> LocalProxy[t.Any]:
+ """Create a :class:`LocalProxy` that access an attribute on this
+ local namespace.
+
+ :param name: Proxy this attribute.
+ :param unbound_message: The error message that the proxy will
+ show if the attribute isn't set.
+ """
+ return LocalProxy(self, name, unbound_message=unbound_message)
+
+ def __release_local__(self) -> None:
+ self.__storage.set({})
+
+ def __getattr__(self, name: str) -> t.Any:
+ values = self.__storage.get({})
+
+ if name in values:
+ return values[name]
+
+ raise AttributeError(name)
+
+ def __setattr__(self, name: str, value: t.Any) -> None:
+ values = self.__storage.get({}).copy()
+ values[name] = value
+ self.__storage.set(values)
+
+ def __delattr__(self, name: str) -> None:
+ values = self.__storage.get({})
+
+ if name in values:
+ values = values.copy()
+ del values[name]
+ self.__storage.set(values)
+ else:
+ raise AttributeError(name)
+
+
+class LocalStack(t.Generic[T]):
+ """Create a stack of context-local data. This wraps a
+ :class:`ContextVar` containing a :class:`list` value.
+
+ This may incur a performance penalty compared to using individual
+ context vars, as it has to copy data to avoid mutating the list
+ between nested contexts.
+
+ :param context_var: The :class:`~contextvars.ContextVar` to use as
+ storage for this local. If not given, one will be created.
+ Context vars not created at the global scope may interfere with
+ garbage collection.
+
+ .. versionchanged:: 2.0
+ Uses ``ContextVar`` instead of a custom storage implementation.
+
+ .. versionadded:: 0.6.1
+ """
+
+ __slots__ = ("_storage",)
+
+ def __init__(self, context_var: ContextVar[list[T]] | None = None) -> None:
+ if context_var is None:
+ # A ContextVar not created at global scope interferes with
+ # Python's garbage collection. However, a local only makes
+ # sense defined at the global scope as well, in which case
+ # the GC issue doesn't seem relevant.
+ context_var = ContextVar(f"werkzeug.LocalStack<{id(self)}>.storage")
+
+ self._storage = context_var
+
+ def __release_local__(self) -> None:
+ self._storage.set([])
+
+ def push(self, obj: T) -> list[T]:
+ """Add a new item to the top of the stack."""
+ stack = self._storage.get([]).copy()
+ stack.append(obj)
+ self._storage.set(stack)
+ return stack
+
+ def pop(self) -> T | None:
+ """Remove the top item from the stack and return it. If the
+ stack is empty, return ``None``.
+ """
+ stack = self._storage.get([])
+
+ if len(stack) == 0:
+ return None
+
+ rv = stack[-1]
+ self._storage.set(stack[:-1])
+ return rv
+
+ @property
+ def top(self) -> T | None:
+ """The topmost item on the stack. If the stack is empty,
+ `None` is returned.
+ """
+ stack = self._storage.get([])
+
+ if len(stack) == 0:
+ return None
+
+ return stack[-1]
+
+ def __call__(
+ self, name: str | None = None, *, unbound_message: str | None = None
+ ) -> LocalProxy[t.Any]:
+ """Create a :class:`LocalProxy` that accesses the top of this
+ local stack.
+
+ :param name: If given, the proxy access this attribute of the
+ top item, rather than the item itself.
+ :param unbound_message: The error message that the proxy will
+ show if the stack is empty.
+ """
+ return LocalProxy(self, name, unbound_message=unbound_message)
+
+
+class LocalManager:
+ """Manage releasing the data for the current context in one or more
+ :class:`Local` and :class:`LocalStack` objects.
+
+ This should not be needed for modern use cases, and may be removed
+ in the future.
+
+ :param locals: A local or list of locals to manage.
+
+ .. versionchanged:: 2.1
+ The ``ident_func`` was removed.
+
+ .. versionchanged:: 0.7
+ The ``ident_func`` parameter was added.
+
+ .. versionchanged:: 0.6.1
+ The :func:`release_local` function can be used instead of a
+ manager.
+ """
+
+ __slots__ = ("locals",)
+
+ def __init__(
+ self,
+ locals: None
+ | (Local | LocalStack[t.Any] | t.Iterable[Local | LocalStack[t.Any]]) = None,
+ ) -> None:
+ if locals is None:
+ self.locals = []
+ elif isinstance(locals, Local):
+ self.locals = [locals]
+ else:
+ self.locals = list(locals) # type: ignore[arg-type]
+
+ def cleanup(self) -> None:
+ """Release the data in the locals for this context. Call this at
+ the end of each request or use :meth:`make_middleware`.
+ """
+ for local in self.locals:
+ release_local(local)
+
+ def make_middleware(self, app: WSGIApplication) -> WSGIApplication:
+ """Wrap a WSGI application so that local data is released
+ automatically after the response has been sent for a request.
+ """
+
+ def application(
+ environ: WSGIEnvironment, start_response: StartResponse
+ ) -> t.Iterable[bytes]:
+ return ClosingIterator(app(environ, start_response), self.cleanup)
+
+ return application
+
+ def middleware(self, func: WSGIApplication) -> WSGIApplication:
+ """Like :meth:`make_middleware` but used as a decorator on the
+ WSGI application function.
+
+ .. code-block:: python
+
+ @manager.middleware
+ def application(environ, start_response):
+ ...
+ """
+ return update_wrapper(self.make_middleware(func), func)
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} storages: {len(self.locals)}>"
+
+
+class _ProxyLookup:
+ """Descriptor that handles proxied attribute lookup for
+ :class:`LocalProxy`.
+
+ :param f: The built-in function this attribute is accessed through.
+ Instead of looking up the special method, the function call
+ is redone on the object.
+ :param fallback: Return this function if the proxy is unbound
+ instead of raising a :exc:`RuntimeError`.
+ :param is_attr: This proxied name is an attribute, not a function.
+ Call the fallback immediately to get the value.
+ :param class_value: Value to return when accessed from the
+ ``LocalProxy`` class directly. Used for ``__doc__`` so building
+ docs still works.
+ """
+
+ __slots__ = ("bind_f", "fallback", "is_attr", "class_value", "name")
+
+ def __init__(
+ self,
+ f: t.Callable[..., t.Any] | None = None,
+ fallback: t.Callable[[LocalProxy[t.Any]], t.Any] | None = None,
+ class_value: t.Any | None = None,
+ is_attr: bool = False,
+ ) -> None:
+ bind_f: t.Callable[[LocalProxy[t.Any], t.Any], t.Callable[..., t.Any]] | None
+
+ if hasattr(f, "__get__"):
+ # A Python function, can be turned into a bound method.
+
+ def bind_f(
+ instance: LocalProxy[t.Any], obj: t.Any
+ ) -> t.Callable[..., t.Any]:
+ return f.__get__(obj, type(obj)) # type: ignore
+
+ elif f is not None:
+ # A C function, use partial to bind the first argument.
+
+ def bind_f(
+ instance: LocalProxy[t.Any], obj: t.Any
+ ) -> t.Callable[..., t.Any]:
+ return partial(f, obj)
+
+ else:
+ # Use getattr, which will produce a bound method.
+ bind_f = None
+
+ self.bind_f = bind_f
+ self.fallback = fallback
+ self.class_value = class_value
+ self.is_attr = is_attr
+
+ def __set_name__(self, owner: LocalProxy[t.Any], name: str) -> None:
+ self.name = name
+
+ def __get__(self, instance: LocalProxy[t.Any], owner: type | None = None) -> t.Any:
+ if instance is None:
+ if self.class_value is not None:
+ return self.class_value
+
+ return self
+
+ try:
+ obj = instance._get_current_object()
+ except RuntimeError:
+ if self.fallback is None:
+ raise
+
+ fallback = self.fallback.__get__(instance, owner)
+
+ if self.is_attr:
+ # __class__ and __doc__ are attributes, not methods.
+ # Call the fallback to get the value.
+ return fallback()
+
+ return fallback
+
+ if self.bind_f is not None:
+ return self.bind_f(instance, obj)
+
+ return getattr(obj, self.name)
+
+ def __repr__(self) -> str:
+ return f"proxy {self.name}"
+
+ def __call__(
+ self, instance: LocalProxy[t.Any], *args: t.Any, **kwargs: t.Any
+ ) -> t.Any:
+ """Support calling unbound methods from the class. For example,
+ this happens with ``copy.copy``, which does
+ ``type(x).__copy__(x)``. ``type(x)`` can't be proxied, so it
+ returns the proxy type and descriptor.
+ """
+ return self.__get__(instance, type(instance))(*args, **kwargs)
+
+
+class _ProxyIOp(_ProxyLookup):
+ """Look up an augmented assignment method on a proxied object. The
+ method is wrapped to return the proxy instead of the object.
+ """
+
+ __slots__ = ()
+
+ def __init__(
+ self,
+ f: t.Callable[..., t.Any] | None = None,
+ fallback: t.Callable[[LocalProxy[t.Any]], t.Any] | None = None,
+ ) -> None:
+ super().__init__(f, fallback)
+
+ def bind_f(instance: LocalProxy[t.Any], obj: t.Any) -> t.Callable[..., t.Any]:
+ def i_op(self: t.Any, other: t.Any) -> LocalProxy[t.Any]:
+ f(self, other) # type: ignore
+ return instance
+
+ return i_op.__get__(obj, type(obj)) # type: ignore
+
+ self.bind_f = bind_f
+
+
+def _l_to_r_op(op: F) -> F:
+ """Swap the argument order to turn an l-op into an r-op."""
+
+ def r_op(obj: t.Any, other: t.Any) -> t.Any:
+ return op(other, obj)
+
+ return t.cast(F, r_op)
+
+
+def _identity(o: T) -> T:
+ return o
+
+
+class LocalProxy(t.Generic[T]):
+ """A proxy to the object bound to a context-local object. All
+ operations on the proxy are forwarded to the bound object. If no
+ object is bound, a ``RuntimeError`` is raised.
+
+ :param local: The context-local object that provides the proxied
+ object.
+ :param name: Proxy this attribute from the proxied object.
+ :param unbound_message: The error message to show if the
+ context-local object is unbound.
+
+ Proxy a :class:`~contextvars.ContextVar` to make it easier to
+ access. Pass a name to proxy that attribute.
+
+ .. code-block:: python
+
+ _request_var = ContextVar("request")
+ request = LocalProxy(_request_var)
+ session = LocalProxy(_request_var, "session")
+
+ Proxy an attribute on a :class:`Local` namespace by calling the
+ local with the attribute name:
+
+ .. code-block:: python
+
+ data = Local()
+ user = data("user")
+
+ Proxy the top item on a :class:`LocalStack` by calling the local.
+ Pass a name to proxy that attribute.
+
+ .. code-block::
+
+ app_stack = LocalStack()
+ current_app = app_stack()
+ g = app_stack("g")
+
+ Pass a function to proxy the return value from that function. This
+ was previously used to access attributes of local objects before
+ that was supported directly.
+
+ .. code-block:: python
+
+ session = LocalProxy(lambda: request.session)
+
+ ``__repr__`` and ``__class__`` are proxied, so ``repr(x)`` and
+ ``isinstance(x, cls)`` will look like the proxied object. Use
+ ``issubclass(type(x), LocalProxy)`` to check if an object is a
+ proxy.
+
+ .. code-block:: python
+
+ repr(user) #
+ isinstance(user, User) # True
+ issubclass(type(user), LocalProxy) # True
+
+ .. versionchanged:: 2.2.2
+ ``__wrapped__`` is set when wrapping an object, not only when
+ wrapping a function, to prevent doctest from failing.
+
+ .. versionchanged:: 2.2
+ Can proxy a ``ContextVar`` or ``LocalStack`` directly.
+
+ .. versionchanged:: 2.2
+ The ``name`` parameter can be used with any proxied object, not
+ only ``Local``.
+
+ .. versionchanged:: 2.2
+ Added the ``unbound_message`` parameter.
+
+ .. versionchanged:: 2.0
+ Updated proxied attributes and methods to reflect the current
+ data model.
+
+ .. versionchanged:: 0.6.1
+ The class can be instantiated with a callable.
+ """
+
+ __slots__ = ("__wrapped", "_get_current_object")
+
+ _get_current_object: t.Callable[[], T]
+ """Return the current object this proxy is bound to. If the proxy is
+ unbound, this raises a ``RuntimeError``.
+
+ This should be used if you need to pass the object to something that
+ doesn't understand the proxy. It can also be useful for performance
+ if you are accessing the object multiple times in a function, rather
+ than going through the proxy multiple times.
+ """
+
+ def __init__(
+ self,
+ local: ContextVar[T] | Local | LocalStack[T] | t.Callable[[], T],
+ name: str | None = None,
+ *,
+ unbound_message: str | None = None,
+ ) -> None:
+ if name is None:
+ get_name = _identity
+ else:
+ get_name = attrgetter(name) # type: ignore[assignment]
+
+ if unbound_message is None:
+ unbound_message = "object is not bound"
+
+ if isinstance(local, Local):
+ if name is None:
+ raise TypeError("'name' is required when proxying a 'Local' object.")
+
+ def _get_current_object() -> T:
+ try:
+ return get_name(local) # type: ignore[return-value]
+ except AttributeError:
+ raise RuntimeError(unbound_message) from None
+
+ elif isinstance(local, LocalStack):
+
+ def _get_current_object() -> T:
+ obj = local.top
+
+ if obj is None:
+ raise RuntimeError(unbound_message)
+
+ return get_name(obj)
+
+ elif isinstance(local, ContextVar):
+
+ def _get_current_object() -> T:
+ try:
+ obj = local.get()
+ except LookupError:
+ raise RuntimeError(unbound_message) from None
+
+ return get_name(obj)
+
+ elif callable(local):
+
+ def _get_current_object() -> T:
+ return get_name(local())
+
+ else:
+ raise TypeError(f"Don't know how to proxy '{type(local)}'.")
+
+ object.__setattr__(self, "_LocalProxy__wrapped", local)
+ object.__setattr__(self, "_get_current_object", _get_current_object)
+
+ __doc__ = _ProxyLookup(
+ class_value=__doc__, fallback=lambda self: type(self).__doc__, is_attr=True
+ )
+ __wrapped__ = _ProxyLookup(
+ fallback=lambda self: self._LocalProxy__wrapped, # type: ignore[attr-defined]
+ is_attr=True,
+ )
+ # __del__ should only delete the proxy
+ __repr__ = _ProxyLookup(
+ repr, fallback=lambda self: f"<{type(self).__name__} unbound>"
+ )
+ __str__ = _ProxyLookup(str)
+ __bytes__ = _ProxyLookup(bytes)
+ __format__ = _ProxyLookup()
+ __lt__ = _ProxyLookup(operator.lt)
+ __le__ = _ProxyLookup(operator.le)
+ __eq__ = _ProxyLookup(operator.eq)
+ __ne__ = _ProxyLookup(operator.ne)
+ __gt__ = _ProxyLookup(operator.gt)
+ __ge__ = _ProxyLookup(operator.ge)
+ __hash__ = _ProxyLookup(hash)
+ __bool__ = _ProxyLookup(bool, fallback=lambda self: False)
+ __getattr__ = _ProxyLookup(getattr)
+ # __getattribute__ triggered through __getattr__
+ __setattr__ = _ProxyLookup(setattr)
+ __delattr__ = _ProxyLookup(delattr)
+ __dir__ = _ProxyLookup(dir, fallback=lambda self: [])
+ # __get__ (proxying descriptor not supported)
+ # __set__ (descriptor)
+ # __delete__ (descriptor)
+ # __set_name__ (descriptor)
+ # __objclass__ (descriptor)
+ # __slots__ used by proxy itself
+ # __dict__ (__getattr__)
+ # __weakref__ (__getattr__)
+ # __init_subclass__ (proxying metaclass not supported)
+ # __prepare__ (metaclass)
+ __class__ = _ProxyLookup(fallback=lambda self: type(self), is_attr=True)
+ __instancecheck__ = _ProxyLookup(lambda self, other: isinstance(other, self))
+ __subclasscheck__ = _ProxyLookup(lambda self, other: issubclass(other, self))
+ # __class_getitem__ triggered through __getitem__
+ __call__ = _ProxyLookup(lambda self, *args, **kwargs: self(*args, **kwargs))
+ __len__ = _ProxyLookup(len)
+ __length_hint__ = _ProxyLookup(operator.length_hint)
+ __getitem__ = _ProxyLookup(operator.getitem)
+ __setitem__ = _ProxyLookup(operator.setitem)
+ __delitem__ = _ProxyLookup(operator.delitem)
+ # __missing__ triggered through __getitem__
+ __iter__ = _ProxyLookup(iter)
+ __next__ = _ProxyLookup(next)
+ __reversed__ = _ProxyLookup(reversed)
+ __contains__ = _ProxyLookup(operator.contains)
+ __add__ = _ProxyLookup(operator.add)
+ __sub__ = _ProxyLookup(operator.sub)
+ __mul__ = _ProxyLookup(operator.mul)
+ __matmul__ = _ProxyLookup(operator.matmul)
+ __truediv__ = _ProxyLookup(operator.truediv)
+ __floordiv__ = _ProxyLookup(operator.floordiv)
+ __mod__ = _ProxyLookup(operator.mod)
+ __divmod__ = _ProxyLookup(divmod)
+ __pow__ = _ProxyLookup(pow)
+ __lshift__ = _ProxyLookup(operator.lshift)
+ __rshift__ = _ProxyLookup(operator.rshift)
+ __and__ = _ProxyLookup(operator.and_)
+ __xor__ = _ProxyLookup(operator.xor)
+ __or__ = _ProxyLookup(operator.or_)
+ __radd__ = _ProxyLookup(_l_to_r_op(operator.add))
+ __rsub__ = _ProxyLookup(_l_to_r_op(operator.sub))
+ __rmul__ = _ProxyLookup(_l_to_r_op(operator.mul))
+ __rmatmul__ = _ProxyLookup(_l_to_r_op(operator.matmul))
+ __rtruediv__ = _ProxyLookup(_l_to_r_op(operator.truediv))
+ __rfloordiv__ = _ProxyLookup(_l_to_r_op(operator.floordiv))
+ __rmod__ = _ProxyLookup(_l_to_r_op(operator.mod))
+ __rdivmod__ = _ProxyLookup(_l_to_r_op(divmod))
+ __rpow__ = _ProxyLookup(_l_to_r_op(pow))
+ __rlshift__ = _ProxyLookup(_l_to_r_op(operator.lshift))
+ __rrshift__ = _ProxyLookup(_l_to_r_op(operator.rshift))
+ __rand__ = _ProxyLookup(_l_to_r_op(operator.and_))
+ __rxor__ = _ProxyLookup(_l_to_r_op(operator.xor))
+ __ror__ = _ProxyLookup(_l_to_r_op(operator.or_))
+ __iadd__ = _ProxyIOp(operator.iadd)
+ __isub__ = _ProxyIOp(operator.isub)
+ __imul__ = _ProxyIOp(operator.imul)
+ __imatmul__ = _ProxyIOp(operator.imatmul)
+ __itruediv__ = _ProxyIOp(operator.itruediv)
+ __ifloordiv__ = _ProxyIOp(operator.ifloordiv)
+ __imod__ = _ProxyIOp(operator.imod)
+ __ipow__ = _ProxyIOp(operator.ipow)
+ __ilshift__ = _ProxyIOp(operator.ilshift)
+ __irshift__ = _ProxyIOp(operator.irshift)
+ __iand__ = _ProxyIOp(operator.iand)
+ __ixor__ = _ProxyIOp(operator.ixor)
+ __ior__ = _ProxyIOp(operator.ior)
+ __neg__ = _ProxyLookup(operator.neg)
+ __pos__ = _ProxyLookup(operator.pos)
+ __abs__ = _ProxyLookup(abs)
+ __invert__ = _ProxyLookup(operator.invert)
+ __complex__ = _ProxyLookup(complex)
+ __int__ = _ProxyLookup(int)
+ __float__ = _ProxyLookup(float)
+ __index__ = _ProxyLookup(operator.index)
+ __round__ = _ProxyLookup(round)
+ __trunc__ = _ProxyLookup(math.trunc)
+ __floor__ = _ProxyLookup(math.floor)
+ __ceil__ = _ProxyLookup(math.ceil)
+ __enter__ = _ProxyLookup()
+ __exit__ = _ProxyLookup()
+ __await__ = _ProxyLookup()
+ __aiter__ = _ProxyLookup()
+ __anext__ = _ProxyLookup()
+ __aenter__ = _ProxyLookup()
+ __aexit__ = _ProxyLookup()
+ __copy__ = _ProxyLookup(copy.copy)
+ __deepcopy__ = _ProxyLookup(copy.deepcopy)
+ # __getnewargs_ex__ (pickle through proxy not supported)
+ # __getnewargs__ (pickle)
+ # __getstate__ (pickle)
+ # __setstate__ (pickle)
+ # __reduce__ (pickle)
+ # __reduce_ex__ (pickle)
diff --git a/venv/Lib/site-packages/werkzeug/middleware/__init__.py b/venv/Lib/site-packages/werkzeug/middleware/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/werkzeug/middleware/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..4e44bd1
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/middleware/__pycache__/dispatcher.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/dispatcher.cpython-310.pyc
new file mode 100644
index 0000000..2962478
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/dispatcher.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/middleware/__pycache__/http_proxy.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/http_proxy.cpython-310.pyc
new file mode 100644
index 0000000..ab96a13
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/http_proxy.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/middleware/__pycache__/lint.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/lint.cpython-310.pyc
new file mode 100644
index 0000000..d399669
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/lint.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/middleware/__pycache__/profiler.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/profiler.cpython-310.pyc
new file mode 100644
index 0000000..483e489
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/profiler.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/middleware/__pycache__/proxy_fix.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/proxy_fix.cpython-310.pyc
new file mode 100644
index 0000000..8af13d9
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/proxy_fix.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/middleware/__pycache__/shared_data.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/shared_data.cpython-310.pyc
new file mode 100644
index 0000000..45e71e8
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/middleware/__pycache__/shared_data.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/middleware/dispatcher.py b/venv/Lib/site-packages/werkzeug/middleware/dispatcher.py
new file mode 100644
index 0000000..e11bacc
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/middleware/dispatcher.py
@@ -0,0 +1,81 @@
+"""
+Application Dispatcher
+======================
+
+This middleware creates a single WSGI application that dispatches to
+multiple other WSGI applications mounted at different URL paths.
+
+A common example is writing a Single Page Application, where you have a
+backend API and a frontend written in JavaScript that does the routing
+in the browser rather than requesting different pages from the server.
+The frontend is a single HTML and JS file that should be served for any
+path besides "/api".
+
+This example dispatches to an API app under "/api", an admin app
+under "/admin", and an app that serves frontend files for all other
+requests::
+
+ app = DispatcherMiddleware(serve_frontend, {
+ '/api': api_app,
+ '/admin': admin_app,
+ })
+
+In production, you might instead handle this at the HTTP server level,
+serving files or proxying to application servers based on location. The
+API and admin apps would each be deployed with a separate WSGI server,
+and the static files would be served directly by the HTTP server.
+
+.. autoclass:: DispatcherMiddleware
+
+:copyright: 2007 Pallets
+:license: BSD-3-Clause
+"""
+
+from __future__ import annotations
+
+import typing as t
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import StartResponse
+ from _typeshed.wsgi import WSGIApplication
+ from _typeshed.wsgi import WSGIEnvironment
+
+
+class DispatcherMiddleware:
+ """Combine multiple applications as a single WSGI application.
+ Requests are dispatched to an application based on the path it is
+ mounted under.
+
+ :param app: The WSGI application to dispatch to if the request
+ doesn't match a mounted path.
+ :param mounts: Maps path prefixes to applications for dispatching.
+ """
+
+ def __init__(
+ self,
+ app: WSGIApplication,
+ mounts: dict[str, WSGIApplication] | None = None,
+ ) -> None:
+ self.app = app
+ self.mounts = mounts or {}
+
+ def __call__(
+ self, environ: WSGIEnvironment, start_response: StartResponse
+ ) -> t.Iterable[bytes]:
+ script = environ.get("PATH_INFO", "")
+ path_info = ""
+
+ while "/" in script:
+ if script in self.mounts:
+ app = self.mounts[script]
+ break
+
+ script, last_item = script.rsplit("/", 1)
+ path_info = f"/{last_item}{path_info}"
+ else:
+ app = self.mounts.get(script, self.app)
+
+ original_script_name = environ.get("SCRIPT_NAME", "")
+ environ["SCRIPT_NAME"] = original_script_name + script
+ environ["PATH_INFO"] = path_info
+ return app(environ, start_response)
diff --git a/venv/Lib/site-packages/werkzeug/middleware/http_proxy.py b/venv/Lib/site-packages/werkzeug/middleware/http_proxy.py
new file mode 100644
index 0000000..5e23915
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/middleware/http_proxy.py
@@ -0,0 +1,236 @@
+"""
+Basic HTTP Proxy
+================
+
+.. autoclass:: ProxyMiddleware
+
+:copyright: 2007 Pallets
+:license: BSD-3-Clause
+"""
+
+from __future__ import annotations
+
+import typing as t
+from http import client
+from urllib.parse import quote
+from urllib.parse import urlsplit
+
+from ..datastructures import EnvironHeaders
+from ..http import is_hop_by_hop_header
+from ..wsgi import get_input_stream
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import StartResponse
+ from _typeshed.wsgi import WSGIApplication
+ from _typeshed.wsgi import WSGIEnvironment
+
+
+class ProxyMiddleware:
+ """Proxy requests under a path to an external server, routing other
+ requests to the app.
+
+ This middleware can only proxy HTTP requests, as HTTP is the only
+ protocol handled by the WSGI server. Other protocols, such as
+ WebSocket requests, cannot be proxied at this layer. This should
+ only be used for development, in production a real proxy server
+ should be used.
+
+ The middleware takes a dict mapping a path prefix to a dict
+ describing the host to be proxied to::
+
+ app = ProxyMiddleware(app, {
+ "/static/": {
+ "target": "http://127.0.0.1:5001/",
+ }
+ })
+
+ Each host has the following options:
+
+ ``target``:
+ The target URL to dispatch to. This is required.
+ ``remove_prefix``:
+ Whether to remove the prefix from the URL before dispatching it
+ to the target. The default is ``False``.
+ ``host``:
+ ``""`` (default):
+ The host header is automatically rewritten to the URL of the
+ target.
+ ``None``:
+ The host header is unmodified from the client request.
+ Any other value:
+ The host header is overwritten with the value.
+ ``headers``:
+ A dictionary of headers to be sent with the request to the
+ target. The default is ``{}``.
+ ``ssl_context``:
+ A :class:`ssl.SSLContext` defining how to verify requests if the
+ target is HTTPS. The default is ``None``.
+
+ In the example above, everything under ``"/static/"`` is proxied to
+ the server on port 5001. The host header is rewritten to the target,
+ and the ``"/static/"`` prefix is removed from the URLs.
+
+ :param app: The WSGI application to wrap.
+ :param targets: Proxy target configurations. See description above.
+ :param chunk_size: Size of chunks to read from input stream and
+ write to target.
+ :param timeout: Seconds before an operation to a target fails.
+
+ .. versionadded:: 0.14
+ """
+
+ def __init__(
+ self,
+ app: WSGIApplication,
+ targets: t.Mapping[str, dict[str, t.Any]],
+ chunk_size: int = 2 << 13,
+ timeout: int = 10,
+ ) -> None:
+ def _set_defaults(opts: dict[str, t.Any]) -> dict[str, t.Any]:
+ opts.setdefault("remove_prefix", False)
+ opts.setdefault("host", "")
+ opts.setdefault("headers", {})
+ opts.setdefault("ssl_context", None)
+ return opts
+
+ self.app = app
+ self.targets = {
+ f"/{k.strip('/')}/": _set_defaults(v) for k, v in targets.items()
+ }
+ self.chunk_size = chunk_size
+ self.timeout = timeout
+
+ def proxy_to(
+ self, opts: dict[str, t.Any], path: str, prefix: str
+ ) -> WSGIApplication:
+ target = urlsplit(opts["target"])
+ # socket can handle unicode host, but header must be ascii
+ host = target.hostname.encode("idna").decode("ascii")
+
+ def application(
+ environ: WSGIEnvironment, start_response: StartResponse
+ ) -> t.Iterable[bytes]:
+ headers = list(EnvironHeaders(environ).items())
+ headers[:] = [
+ (k, v)
+ for k, v in headers
+ if not is_hop_by_hop_header(k)
+ and k.lower() not in ("content-length", "host")
+ ]
+ headers.append(("Connection", "close"))
+
+ if opts["host"] == "":
+ headers.append(("Host", host))
+ elif opts["host"] is None:
+ headers.append(("Host", environ["HTTP_HOST"]))
+ else:
+ headers.append(("Host", opts["host"]))
+
+ headers.extend(opts["headers"].items())
+ remote_path = path
+
+ if opts["remove_prefix"]:
+ remote_path = remote_path[len(prefix) :].lstrip("/")
+ remote_path = f"{target.path.rstrip('/')}/{remote_path}"
+
+ content_length = environ.get("CONTENT_LENGTH")
+ chunked = False
+
+ if content_length not in ("", None):
+ headers.append(("Content-Length", content_length)) # type: ignore
+ elif content_length is not None:
+ headers.append(("Transfer-Encoding", "chunked"))
+ chunked = True
+
+ try:
+ if target.scheme == "http":
+ con = client.HTTPConnection(
+ host, target.port or 80, timeout=self.timeout
+ )
+ elif target.scheme == "https":
+ con = client.HTTPSConnection(
+ host,
+ target.port or 443,
+ timeout=self.timeout,
+ context=opts["ssl_context"],
+ )
+ else:
+ raise RuntimeError(
+ "Target scheme must be 'http' or 'https', got"
+ f" {target.scheme!r}."
+ )
+
+ con.connect()
+ # safe = https://url.spec.whatwg.org/#url-path-segment-string
+ # as well as percent for things that are already quoted
+ remote_url = quote(remote_path, safe="!$&'()*+,/:;=@%")
+ querystring = environ["QUERY_STRING"]
+
+ if querystring:
+ remote_url = f"{remote_url}?{querystring}"
+
+ con.putrequest(environ["REQUEST_METHOD"], remote_url, skip_host=True)
+
+ for k, v in headers:
+ if k.lower() == "connection":
+ v = "close"
+
+ con.putheader(k, v)
+
+ con.endheaders()
+ stream = get_input_stream(environ)
+
+ while True:
+ data = stream.read(self.chunk_size)
+
+ if not data:
+ break
+
+ if chunked:
+ con.send(b"%x\r\n%s\r\n" % (len(data), data))
+ else:
+ con.send(data)
+
+ resp = con.getresponse()
+ except OSError:
+ from ..exceptions import BadGateway
+
+ return BadGateway()(environ, start_response)
+
+ start_response(
+ f"{resp.status} {resp.reason}",
+ [
+ (k.title(), v)
+ for k, v in resp.getheaders()
+ if not is_hop_by_hop_header(k)
+ ],
+ )
+
+ def read() -> t.Iterator[bytes]:
+ while True:
+ try:
+ data = resp.read(self.chunk_size)
+ except OSError:
+ break
+
+ if not data:
+ break
+
+ yield data
+
+ return read()
+
+ return application
+
+ def __call__(
+ self, environ: WSGIEnvironment, start_response: StartResponse
+ ) -> t.Iterable[bytes]:
+ path = environ["PATH_INFO"]
+ app = self.app
+
+ for prefix, opts in self.targets.items():
+ if path.startswith(prefix):
+ app = self.proxy_to(opts, path, prefix)
+ break
+
+ return app(environ, start_response)
diff --git a/venv/Lib/site-packages/werkzeug/middleware/lint.py b/venv/Lib/site-packages/werkzeug/middleware/lint.py
new file mode 100644
index 0000000..3714271
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/middleware/lint.py
@@ -0,0 +1,439 @@
+"""
+WSGI Protocol Linter
+====================
+
+This module provides a middleware that performs sanity checks on the
+behavior of the WSGI server and application. It checks that the
+:pep:`3333` WSGI spec is properly implemented. It also warns on some
+common HTTP errors such as non-empty responses for 304 status codes.
+
+.. autoclass:: LintMiddleware
+
+:copyright: 2007 Pallets
+:license: BSD-3-Clause
+"""
+
+from __future__ import annotations
+
+import typing as t
+from types import TracebackType
+from urllib.parse import urlparse
+from warnings import warn
+
+from ..datastructures import Headers
+from ..http import is_entity_header
+from ..wsgi import FileWrapper
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import StartResponse
+ from _typeshed.wsgi import WSGIApplication
+ from _typeshed.wsgi import WSGIEnvironment
+
+
+class WSGIWarning(Warning):
+ """Warning class for WSGI warnings."""
+
+
+class HTTPWarning(Warning):
+ """Warning class for HTTP warnings."""
+
+
+def check_type(context: str, obj: object, need: type = str) -> None:
+ if type(obj) is not need:
+ warn(
+ f"{context!r} requires {need.__name__!r}, got {type(obj).__name__!r}.",
+ WSGIWarning,
+ stacklevel=3,
+ )
+
+
+class InputStream:
+ def __init__(self, stream: t.IO[bytes]) -> None:
+ self._stream = stream
+
+ def read(self, *args: t.Any) -> bytes:
+ if len(args) == 0:
+ warn(
+ "WSGI does not guarantee an EOF marker on the input stream, thus making"
+ " calls to 'wsgi.input.read()' unsafe. Conforming servers may never"
+ " return from this call.",
+ WSGIWarning,
+ stacklevel=2,
+ )
+ elif len(args) != 1:
+ warn(
+ "Too many parameters passed to 'wsgi.input.read()'.",
+ WSGIWarning,
+ stacklevel=2,
+ )
+ return self._stream.read(*args)
+
+ def readline(self, *args: t.Any) -> bytes:
+ if len(args) == 0:
+ warn(
+ "Calls to 'wsgi.input.readline()' without arguments are unsafe. Use"
+ " 'wsgi.input.read()' instead.",
+ WSGIWarning,
+ stacklevel=2,
+ )
+ elif len(args) == 1:
+ warn(
+ "'wsgi.input.readline()' was called with a size hint. WSGI does not"
+ " support this, although it's available on all major servers.",
+ WSGIWarning,
+ stacklevel=2,
+ )
+ else:
+ raise TypeError("Too many arguments passed to 'wsgi.input.readline()'.")
+ return self._stream.readline(*args)
+
+ def __iter__(self) -> t.Iterator[bytes]:
+ try:
+ return iter(self._stream)
+ except TypeError:
+ warn("'wsgi.input' is not iterable.", WSGIWarning, stacklevel=2)
+ return iter(())
+
+ def close(self) -> None:
+ warn("The application closed the input stream!", WSGIWarning, stacklevel=2)
+ self._stream.close()
+
+
+class ErrorStream:
+ def __init__(self, stream: t.IO[str]) -> None:
+ self._stream = stream
+
+ def write(self, s: str) -> None:
+ check_type("wsgi.error.write()", s, str)
+ self._stream.write(s)
+
+ def flush(self) -> None:
+ self._stream.flush()
+
+ def writelines(self, seq: t.Iterable[str]) -> None:
+ for line in seq:
+ self.write(line)
+
+ def close(self) -> None:
+ warn("The application closed the error stream!", WSGIWarning, stacklevel=2)
+ self._stream.close()
+
+
+class GuardedWrite:
+ def __init__(self, write: t.Callable[[bytes], object], chunks: list[int]) -> None:
+ self._write = write
+ self._chunks = chunks
+
+ def __call__(self, s: bytes) -> None:
+ check_type("write()", s, bytes)
+ self._write(s)
+ self._chunks.append(len(s))
+
+
+class GuardedIterator:
+ def __init__(
+ self,
+ iterator: t.Iterable[bytes],
+ headers_set: tuple[int, Headers],
+ chunks: list[int],
+ ) -> None:
+ self._iterator = iterator
+ self._next = iter(iterator).__next__
+ self.closed = False
+ self.headers_set = headers_set
+ self.chunks = chunks
+
+ def __iter__(self) -> GuardedIterator:
+ return self
+
+ def __next__(self) -> bytes:
+ if self.closed:
+ warn("Iterated over closed 'app_iter'.", WSGIWarning, stacklevel=2)
+
+ rv = self._next()
+
+ if not self.headers_set:
+ warn(
+ "The application returned before it started the response.",
+ WSGIWarning,
+ stacklevel=2,
+ )
+
+ check_type("application iterator items", rv, bytes)
+ self.chunks.append(len(rv))
+ return rv
+
+ def close(self) -> None:
+ self.closed = True
+
+ if hasattr(self._iterator, "close"):
+ self._iterator.close()
+
+ if self.headers_set:
+ status_code, headers = self.headers_set
+ bytes_sent = sum(self.chunks)
+ content_length = headers.get("content-length", type=int)
+
+ if status_code == 304:
+ for key, _value in headers:
+ key = key.lower()
+ if key not in ("expires", "content-location") and is_entity_header(
+ key
+ ):
+ warn(
+ f"Entity header {key!r} found in 304 response.",
+ HTTPWarning,
+ stacklevel=2,
+ )
+ if bytes_sent:
+ warn(
+ "304 responses must not have a body.",
+ HTTPWarning,
+ stacklevel=2,
+ )
+ elif 100 <= status_code < 200 or status_code == 204:
+ if content_length != 0:
+ warn(
+ f"{status_code} responses must have an empty content length.",
+ HTTPWarning,
+ stacklevel=2,
+ )
+ if bytes_sent:
+ warn(
+ f"{status_code} responses must not have a body.",
+ HTTPWarning,
+ stacklevel=2,
+ )
+ elif content_length is not None and content_length != bytes_sent:
+ warn(
+ "Content-Length and the number of bytes sent to the"
+ " client do not match.",
+ WSGIWarning,
+ stacklevel=2,
+ )
+
+ def __del__(self) -> None:
+ if not self.closed:
+ try:
+ warn(
+ "Iterator was garbage collected before it was closed.",
+ WSGIWarning,
+ stacklevel=2,
+ )
+ except Exception:
+ pass
+
+
+class LintMiddleware:
+ """Warns about common errors in the WSGI and HTTP behavior of the
+ server and wrapped application. Some of the issues it checks are:
+
+ - invalid status codes
+ - non-bytes sent to the WSGI server
+ - strings returned from the WSGI application
+ - non-empty conditional responses
+ - unquoted etags
+ - relative URLs in the Location header
+ - unsafe calls to wsgi.input
+ - unclosed iterators
+
+ Error information is emitted using the :mod:`warnings` module.
+
+ :param app: The WSGI application to wrap.
+
+ .. code-block:: python
+
+ from werkzeug.middleware.lint import LintMiddleware
+ app = LintMiddleware(app)
+ """
+
+ def __init__(self, app: WSGIApplication) -> None:
+ self.app = app
+
+ def check_environ(self, environ: WSGIEnvironment) -> None:
+ if type(environ) is not dict: # noqa: E721
+ warn(
+ "WSGI environment is not a standard Python dict.",
+ WSGIWarning,
+ stacklevel=4,
+ )
+ for key in (
+ "REQUEST_METHOD",
+ "SERVER_NAME",
+ "SERVER_PORT",
+ "wsgi.version",
+ "wsgi.input",
+ "wsgi.errors",
+ "wsgi.multithread",
+ "wsgi.multiprocess",
+ "wsgi.run_once",
+ ):
+ if key not in environ:
+ warn(
+ f"Required environment key {key!r} not found",
+ WSGIWarning,
+ stacklevel=3,
+ )
+ if environ["wsgi.version"] != (1, 0):
+ warn("Environ is not a WSGI 1.0 environ.", WSGIWarning, stacklevel=3)
+
+ script_name = environ.get("SCRIPT_NAME", "")
+ path_info = environ.get("PATH_INFO", "")
+
+ if script_name and script_name[0] != "/":
+ warn(
+ f"'SCRIPT_NAME' does not start with a slash: {script_name!r}",
+ WSGIWarning,
+ stacklevel=3,
+ )
+
+ if path_info and path_info[0] != "/":
+ warn(
+ f"'PATH_INFO' does not start with a slash: {path_info!r}",
+ WSGIWarning,
+ stacklevel=3,
+ )
+
+ def check_start_response(
+ self,
+ status: str,
+ headers: list[tuple[str, str]],
+ exc_info: None | (tuple[type[BaseException], BaseException, TracebackType]),
+ ) -> tuple[int, Headers]:
+ check_type("status", status, str)
+ status_code_str = status.split(None, 1)[0]
+
+ if len(status_code_str) != 3 or not status_code_str.isdecimal():
+ warn("Status code must be three digits.", WSGIWarning, stacklevel=3)
+
+ if len(status) < 4 or status[3] != " ":
+ warn(
+ f"Invalid value for status {status!r}. Valid status strings are three"
+ " digits, a space and a status explanation.",
+ WSGIWarning,
+ stacklevel=3,
+ )
+
+ status_code = int(status_code_str)
+
+ if status_code < 100:
+ warn("Status code < 100 detected.", WSGIWarning, stacklevel=3)
+
+ if type(headers) is not list: # noqa: E721
+ warn("Header list is not a list.", WSGIWarning, stacklevel=3)
+
+ for item in headers:
+ if type(item) is not tuple or len(item) != 2:
+ warn("Header items must be 2-item tuples.", WSGIWarning, stacklevel=3)
+ name, value = item
+ if type(name) is not str or type(value) is not str: # noqa: E721
+ warn(
+ "Header keys and values must be strings.", WSGIWarning, stacklevel=3
+ )
+ if name.lower() == "status":
+ warn(
+ "The status header is not supported due to"
+ " conflicts with the CGI spec.",
+ WSGIWarning,
+ stacklevel=3,
+ )
+
+ if exc_info is not None and not isinstance(exc_info, tuple):
+ warn("Invalid value for exc_info.", WSGIWarning, stacklevel=3)
+
+ headers_obj = Headers(headers)
+ self.check_headers(headers_obj)
+
+ return status_code, headers_obj
+
+ def check_headers(self, headers: Headers) -> None:
+ etag = headers.get("etag")
+
+ if etag is not None:
+ if etag.startswith(("W/", "w/")):
+ if etag.startswith("w/"):
+ warn(
+ "Weak etag indicator should be upper case.",
+ HTTPWarning,
+ stacklevel=4,
+ )
+
+ etag = etag[2:]
+
+ if not (etag[:1] == etag[-1:] == '"'):
+ warn("Unquoted etag emitted.", HTTPWarning, stacklevel=4)
+
+ location = headers.get("location")
+
+ if location is not None:
+ if not urlparse(location).netloc:
+ warn(
+ "Absolute URLs required for location header.",
+ HTTPWarning,
+ stacklevel=4,
+ )
+
+ def check_iterator(self, app_iter: t.Iterable[bytes]) -> None:
+ if isinstance(app_iter, str):
+ warn(
+ "The application returned a string. The response will send one"
+ " character at a time to the client, which will kill performance."
+ " Return a list or iterable instead.",
+ WSGIWarning,
+ stacklevel=3,
+ )
+
+ def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Iterable[bytes]:
+ if len(args) != 2:
+ warn("A WSGI app takes two arguments.", WSGIWarning, stacklevel=2)
+
+ if kwargs:
+ warn(
+ "A WSGI app does not take keyword arguments.", WSGIWarning, stacklevel=2
+ )
+
+ environ: WSGIEnvironment = args[0]
+ start_response: StartResponse = args[1]
+
+ self.check_environ(environ)
+ environ["wsgi.input"] = InputStream(environ["wsgi.input"])
+ environ["wsgi.errors"] = ErrorStream(environ["wsgi.errors"])
+
+ # Hook our own file wrapper in so that applications will always
+ # iterate to the end and we can check the content length.
+ environ["wsgi.file_wrapper"] = FileWrapper
+
+ headers_set: list[t.Any] = []
+ chunks: list[int] = []
+
+ def checking_start_response(
+ *args: t.Any, **kwargs: t.Any
+ ) -> t.Callable[[bytes], None]:
+ if len(args) not in {2, 3}:
+ warn(
+ f"Invalid number of arguments: {len(args)}, expected 2 or 3.",
+ WSGIWarning,
+ stacklevel=2,
+ )
+
+ if kwargs:
+ warn(
+ "'start_response' does not take keyword arguments.",
+ WSGIWarning,
+ stacklevel=2,
+ )
+
+ status: str = args[0]
+ headers: list[tuple[str, str]] = args[1]
+ exc_info: (
+ None | (tuple[type[BaseException], BaseException, TracebackType])
+ ) = args[2] if len(args) == 3 else None
+
+ headers_set[:] = self.check_start_response(status, headers, exc_info)
+ return GuardedWrite(start_response(status, headers, exc_info), chunks)
+
+ app_iter = self.app(environ, t.cast("StartResponse", checking_start_response))
+ self.check_iterator(app_iter)
+ return GuardedIterator(
+ app_iter, t.cast(tuple[int, Headers], headers_set), chunks
+ )
diff --git a/venv/Lib/site-packages/werkzeug/middleware/profiler.py b/venv/Lib/site-packages/werkzeug/middleware/profiler.py
new file mode 100644
index 0000000..112b877
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/middleware/profiler.py
@@ -0,0 +1,155 @@
+"""
+Application Profiler
+====================
+
+This module provides a middleware that profiles each request with the
+:mod:`cProfile` module. This can help identify bottlenecks in your code
+that may be slowing down your application.
+
+.. autoclass:: ProfilerMiddleware
+
+:copyright: 2007 Pallets
+:license: BSD-3-Clause
+"""
+
+from __future__ import annotations
+
+import os.path
+import sys
+import time
+import typing as t
+from pstats import Stats
+
+try:
+ from cProfile import Profile
+except ImportError:
+ from profile import Profile # type: ignore
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import StartResponse
+ from _typeshed.wsgi import WSGIApplication
+ from _typeshed.wsgi import WSGIEnvironment
+
+
+class ProfilerMiddleware:
+ """Wrap a WSGI application and profile the execution of each
+ request. Responses are buffered so that timings are more exact.
+
+ If ``stream`` is given, :class:`pstats.Stats` are written to it
+ after each request. If ``profile_dir`` is given, :mod:`cProfile`
+ data files are saved to that directory, one file per request.
+
+ The filename can be customized by passing ``filename_format``. If
+ it is a string, it will be formatted using :meth:`str.format` with
+ the following fields available:
+
+ - ``{method}`` - The request method; GET, POST, etc.
+ - ``{path}`` - The request path or 'root' should one not exist.
+ - ``{elapsed}`` - The elapsed time of the request in milliseconds.
+ - ``{time}`` - The time of the request.
+
+ If it is a callable, it will be called with the WSGI ``environ`` and
+ be expected to return a filename string. The ``environ`` dictionary
+ will also have the ``"werkzeug.profiler"`` key populated with a
+ dictionary containing the following fields (more may be added in the
+ future):
+ - ``{elapsed}`` - The elapsed time of the request in milliseconds.
+ - ``{time}`` - The time of the request.
+
+ :param app: The WSGI application to wrap.
+ :param stream: Write stats to this stream. Disable with ``None``.
+ :param sort_by: A tuple of columns to sort stats by. See
+ :meth:`pstats.Stats.sort_stats`.
+ :param restrictions: A tuple of restrictions to filter stats by. See
+ :meth:`pstats.Stats.print_stats`.
+ :param profile_dir: Save profile data files to this directory.
+ :param filename_format: Format string for profile data file names,
+ or a callable returning a name. See explanation above.
+
+ .. code-block:: python
+
+ from werkzeug.middleware.profiler import ProfilerMiddleware
+ app = ProfilerMiddleware(app)
+
+ .. versionchanged:: 3.0
+ Added the ``"werkzeug.profiler"`` key to the ``filename_format(environ)``
+ parameter with the ``elapsed`` and ``time`` fields.
+
+ .. versionchanged:: 0.15
+ Stats are written even if ``profile_dir`` is given, and can be
+ disable by passing ``stream=None``.
+
+ .. versionadded:: 0.15
+ Added ``filename_format``.
+
+ .. versionadded:: 0.9
+ Added ``restrictions`` and ``profile_dir``.
+ """
+
+ def __init__(
+ self,
+ app: WSGIApplication,
+ stream: t.IO[str] | None = sys.stdout,
+ sort_by: t.Iterable[str] = ("time", "calls"),
+ restrictions: t.Iterable[str | int | float] = (),
+ profile_dir: str | None = None,
+ filename_format: str = "{method}.{path}.{elapsed:.0f}ms.{time:.0f}.prof",
+ ) -> None:
+ self._app = app
+ self._stream = stream
+ self._sort_by = sort_by
+ self._restrictions = restrictions
+ self._profile_dir = profile_dir
+ self._filename_format = filename_format
+
+ def __call__(
+ self, environ: WSGIEnvironment, start_response: StartResponse
+ ) -> t.Iterable[bytes]:
+ response_body: list[bytes] = []
+
+ def catching_start_response(status, headers, exc_info=None): # type: ignore
+ start_response(status, headers, exc_info)
+ return response_body.append
+
+ def runapp() -> None:
+ app_iter = self._app(
+ environ, t.cast("StartResponse", catching_start_response)
+ )
+ response_body.extend(app_iter)
+
+ if hasattr(app_iter, "close"):
+ app_iter.close()
+
+ profile = Profile()
+ start = time.time()
+ profile.runcall(runapp)
+ body = b"".join(response_body)
+ elapsed = time.time() - start
+
+ if self._profile_dir is not None:
+ if callable(self._filename_format):
+ environ["werkzeug.profiler"] = {
+ "elapsed": elapsed * 1000.0,
+ "time": time.time(),
+ }
+ filename = self._filename_format(environ)
+ else:
+ filename = self._filename_format.format(
+ method=environ["REQUEST_METHOD"],
+ path=environ["PATH_INFO"].strip("/").replace("/", ".") or "root",
+ elapsed=elapsed * 1000.0,
+ time=time.time(),
+ )
+ filename = os.path.join(self._profile_dir, filename)
+ profile.dump_stats(filename)
+
+ if self._stream is not None:
+ stats = Stats(profile, stream=self._stream)
+ stats.sort_stats(*self._sort_by)
+ print("-" * 80, file=self._stream)
+ path_info = environ.get("PATH_INFO", "")
+ print(f"PATH: {path_info!r}", file=self._stream)
+ stats.print_stats(*self._restrictions)
+ print(f"{'-' * 80}\n", file=self._stream)
+
+ return [body]
diff --git a/venv/Lib/site-packages/werkzeug/middleware/proxy_fix.py b/venv/Lib/site-packages/werkzeug/middleware/proxy_fix.py
new file mode 100644
index 0000000..cbf4e0b
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/middleware/proxy_fix.py
@@ -0,0 +1,183 @@
+"""
+X-Forwarded-For Proxy Fix
+=========================
+
+This module provides a middleware that adjusts the WSGI environ based on
+``X-Forwarded-`` headers that proxies in front of an application may
+set.
+
+When an application is running behind a proxy server, WSGI may see the
+request as coming from that server rather than the real client. Proxies
+set various headers to track where the request actually came from.
+
+This middleware should only be used if the application is actually
+behind such a proxy, and should be configured with the number of proxies
+that are chained in front of it. Not all proxies set all the headers.
+Since incoming headers can be faked, you must set how many proxies are
+setting each header so the middleware knows what to trust.
+
+.. autoclass:: ProxyFix
+
+:copyright: 2007 Pallets
+:license: BSD-3-Clause
+"""
+
+from __future__ import annotations
+
+import typing as t
+
+from ..http import parse_list_header
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import StartResponse
+ from _typeshed.wsgi import WSGIApplication
+ from _typeshed.wsgi import WSGIEnvironment
+
+
+class ProxyFix:
+ """Adjust the WSGI environ based on ``X-Forwarded-`` that proxies in
+ front of the application may set.
+
+ - ``X-Forwarded-For`` sets ``REMOTE_ADDR``.
+ - ``X-Forwarded-Proto`` sets ``wsgi.url_scheme``.
+ - ``X-Forwarded-Host`` sets ``HTTP_HOST``, ``SERVER_NAME``, and
+ ``SERVER_PORT``.
+ - ``X-Forwarded-Port`` sets ``HTTP_HOST`` and ``SERVER_PORT``.
+ - ``X-Forwarded-Prefix`` sets ``SCRIPT_NAME``.
+
+ You must tell the middleware how many proxies set each header so it
+ knows what values to trust. It is a security issue to trust values
+ that came from the client rather than a proxy.
+
+ The original values of the headers are stored in the WSGI
+ environ as ``werkzeug.proxy_fix.orig``, a dict.
+
+ :param app: The WSGI application to wrap.
+ :param x_for: Number of values to trust for ``X-Forwarded-For``.
+ :param x_proto: Number of values to trust for ``X-Forwarded-Proto``.
+ :param x_host: Number of values to trust for ``X-Forwarded-Host``.
+ :param x_port: Number of values to trust for ``X-Forwarded-Port``.
+ :param x_prefix: Number of values to trust for
+ ``X-Forwarded-Prefix``.
+
+ .. code-block:: python
+
+ from werkzeug.middleware.proxy_fix import ProxyFix
+ # App is behind one proxy that sets the -For and -Host headers.
+ app = ProxyFix(app, x_for=1, x_host=1)
+
+ .. versionchanged:: 1.0
+ The ``num_proxies`` argument and attribute; the ``get_remote_addr`` method; and
+ the environ keys ``orig_remote_addr``, ``orig_wsgi_url_scheme``, and
+ ``orig_http_host`` were removed.
+
+ .. versionchanged:: 0.15
+ All headers support multiple values. Each header is configured with a separate
+ number of trusted proxies.
+
+ .. versionchanged:: 0.15
+ Original WSGI environ values are stored in the ``werkzeug.proxy_fix.orig`` dict.
+
+ .. versionchanged:: 0.15
+ Support ``X-Forwarded-Port`` and ``X-Forwarded-Prefix``.
+
+ .. versionchanged:: 0.15
+ ``X-Forwarded-Host`` and ``X-Forwarded-Port`` modify
+ ``SERVER_NAME`` and ``SERVER_PORT``.
+ """
+
+ def __init__(
+ self,
+ app: WSGIApplication,
+ x_for: int = 1,
+ x_proto: int = 1,
+ x_host: int = 0,
+ x_port: int = 0,
+ x_prefix: int = 0,
+ ) -> None:
+ self.app = app
+ self.x_for = x_for
+ self.x_proto = x_proto
+ self.x_host = x_host
+ self.x_port = x_port
+ self.x_prefix = x_prefix
+
+ def _get_real_value(self, trusted: int, value: str | None) -> str | None:
+ """Get the real value from a list header based on the configured
+ number of trusted proxies.
+
+ :param trusted: Number of values to trust in the header.
+ :param value: Comma separated list header value to parse.
+ :return: The real value, or ``None`` if there are fewer values
+ than the number of trusted proxies.
+
+ .. versionchanged:: 1.0
+ Renamed from ``_get_trusted_comma``.
+
+ .. versionadded:: 0.15
+ """
+ if not (trusted and value):
+ return None
+ values = parse_list_header(value)
+ if len(values) >= trusted:
+ return values[-trusted]
+ return None
+
+ def __call__(
+ self, environ: WSGIEnvironment, start_response: StartResponse
+ ) -> t.Iterable[bytes]:
+ """Modify the WSGI environ based on the various ``Forwarded``
+ headers before calling the wrapped application. Store the
+ original environ values in ``werkzeug.proxy_fix.orig_{key}``.
+ """
+ environ_get = environ.get
+ orig_remote_addr = environ_get("REMOTE_ADDR")
+ orig_wsgi_url_scheme = environ_get("wsgi.url_scheme")
+ orig_http_host = environ_get("HTTP_HOST")
+ environ.update(
+ {
+ "werkzeug.proxy_fix.orig": {
+ "REMOTE_ADDR": orig_remote_addr,
+ "wsgi.url_scheme": orig_wsgi_url_scheme,
+ "HTTP_HOST": orig_http_host,
+ "SERVER_NAME": environ_get("SERVER_NAME"),
+ "SERVER_PORT": environ_get("SERVER_PORT"),
+ "SCRIPT_NAME": environ_get("SCRIPT_NAME"),
+ }
+ }
+ )
+
+ x_for = self._get_real_value(self.x_for, environ_get("HTTP_X_FORWARDED_FOR"))
+ if x_for:
+ environ["REMOTE_ADDR"] = x_for
+
+ x_proto = self._get_real_value(
+ self.x_proto, environ_get("HTTP_X_FORWARDED_PROTO")
+ )
+ if x_proto:
+ environ["wsgi.url_scheme"] = x_proto
+
+ x_host = self._get_real_value(self.x_host, environ_get("HTTP_X_FORWARDED_HOST"))
+ if x_host:
+ environ["HTTP_HOST"] = environ["SERVER_NAME"] = x_host
+ # "]" to check for IPv6 address without port
+ if ":" in x_host and not x_host.endswith("]"):
+ environ["SERVER_NAME"], environ["SERVER_PORT"] = x_host.rsplit(":", 1)
+
+ x_port = self._get_real_value(self.x_port, environ_get("HTTP_X_FORWARDED_PORT"))
+ if x_port:
+ host = environ.get("HTTP_HOST")
+ if host:
+ # "]" to check for IPv6 address without port
+ if ":" in host and not host.endswith("]"):
+ host = host.rsplit(":", 1)[0]
+ environ["HTTP_HOST"] = f"{host}:{x_port}"
+ environ["SERVER_PORT"] = x_port
+
+ x_prefix = self._get_real_value(
+ self.x_prefix, environ_get("HTTP_X_FORWARDED_PREFIX")
+ )
+ if x_prefix:
+ environ["SCRIPT_NAME"] = x_prefix
+
+ return self.app(environ, start_response)
diff --git a/venv/Lib/site-packages/werkzeug/middleware/shared_data.py b/venv/Lib/site-packages/werkzeug/middleware/shared_data.py
new file mode 100644
index 0000000..c7c06df
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/middleware/shared_data.py
@@ -0,0 +1,283 @@
+"""
+Serve Shared Static Files
+=========================
+
+.. autoclass:: SharedDataMiddleware
+ :members: is_allowed
+
+:copyright: 2007 Pallets
+:license: BSD-3-Clause
+"""
+
+from __future__ import annotations
+
+import collections.abc as cabc
+import importlib.util
+import mimetypes
+import os
+import posixpath
+import typing as t
+from datetime import datetime
+from datetime import timezone
+from io import BytesIO
+from time import time
+from zlib import adler32
+
+from ..http import http_date
+from ..http import is_resource_modified
+from ..security import safe_join
+from ..utils import get_content_type
+from ..wsgi import get_path_info
+from ..wsgi import wrap_file
+
+_TOpener = t.Callable[[], tuple[t.IO[bytes], datetime, int]]
+_TLoader = t.Callable[[t.Optional[str]], tuple[t.Optional[str], t.Optional[_TOpener]]]
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import StartResponse
+ from _typeshed.wsgi import WSGIApplication
+ from _typeshed.wsgi import WSGIEnvironment
+
+
+class SharedDataMiddleware:
+ """A WSGI middleware which provides static content for development
+ environments or simple server setups. Its usage is quite simple::
+
+ import os
+ from werkzeug.middleware.shared_data import SharedDataMiddleware
+
+ app = SharedDataMiddleware(app, {
+ '/shared': os.path.join(os.path.dirname(__file__), 'shared')
+ })
+
+ The contents of the folder ``./shared`` will now be available on
+ ``http://example.com/shared/``. This is pretty useful during development
+ because a standalone media server is not required. Files can also be
+ mounted on the root folder and still continue to use the application because
+ the shared data middleware forwards all unhandled requests to the
+ application, even if the requests are below one of the shared folders.
+
+ If `pkg_resources` is available you can also tell the middleware to serve
+ files from package data::
+
+ app = SharedDataMiddleware(app, {
+ '/static': ('myapplication', 'static')
+ })
+
+ This will then serve the ``static`` folder in the `myapplication`
+ Python package.
+
+ The optional `disallow` parameter can be a list of :func:`~fnmatch.fnmatch`
+ rules for files that are not accessible from the web. If `cache` is set to
+ `False` no caching headers are sent.
+
+ Currently the middleware does not support non-ASCII filenames. If the
+ encoding on the file system happens to match the encoding of the URI it may
+ work but this could also be by accident. We strongly suggest using ASCII
+ only file names for static files.
+
+ The middleware will guess the mimetype using the Python `mimetype`
+ module. If it's unable to figure out the charset it will fall back
+ to `fallback_mimetype`.
+
+ :param app: the application to wrap. If you don't want to wrap an
+ application you can pass it :exc:`NotFound`.
+ :param exports: a list or dict of exported files and folders.
+ :param disallow: a list of :func:`~fnmatch.fnmatch` rules.
+ :param cache: enable or disable caching headers.
+ :param cache_timeout: the cache timeout in seconds for the headers.
+ :param fallback_mimetype: The fallback mimetype for unknown files.
+
+ .. versionchanged:: 1.0
+ The default ``fallback_mimetype`` is
+ ``application/octet-stream``. If a filename looks like a text
+ mimetype, the ``utf-8`` charset is added to it.
+
+ .. versionadded:: 0.6
+ Added ``fallback_mimetype``.
+
+ .. versionchanged:: 0.5
+ Added ``cache_timeout``.
+ """
+
+ def __init__(
+ self,
+ app: WSGIApplication,
+ exports: (
+ cabc.Mapping[str, str | tuple[str, str]]
+ | t.Iterable[tuple[str, str | tuple[str, str]]]
+ ),
+ disallow: None = None,
+ cache: bool = True,
+ cache_timeout: int = 60 * 60 * 12,
+ fallback_mimetype: str = "application/octet-stream",
+ ) -> None:
+ self.app = app
+ self.exports: list[tuple[str, _TLoader]] = []
+ self.cache = cache
+ self.cache_timeout = cache_timeout
+
+ if isinstance(exports, cabc.Mapping):
+ exports = exports.items()
+
+ for key, value in exports:
+ if isinstance(value, tuple):
+ loader = self.get_package_loader(*value)
+ elif isinstance(value, str):
+ if os.path.isfile(value):
+ loader = self.get_file_loader(value)
+ else:
+ loader = self.get_directory_loader(value)
+ else:
+ raise TypeError(f"unknown def {value!r}")
+
+ self.exports.append((key, loader))
+
+ if disallow is not None:
+ from fnmatch import fnmatch
+
+ self.is_allowed = lambda x: not fnmatch(x, disallow)
+
+ self.fallback_mimetype = fallback_mimetype
+
+ def is_allowed(self, filename: str) -> bool:
+ """Subclasses can override this method to disallow the access to
+ certain files. However by providing `disallow` in the constructor
+ this method is overwritten.
+ """
+ return True
+
+ def _opener(self, filename: str) -> _TOpener:
+ return lambda: (
+ open(filename, "rb"),
+ datetime.fromtimestamp(os.path.getmtime(filename), tz=timezone.utc),
+ int(os.path.getsize(filename)),
+ )
+
+ def get_file_loader(self, filename: str) -> _TLoader:
+ return lambda x: (os.path.basename(filename), self._opener(filename))
+
+ def get_package_loader(self, package: str, package_path: str) -> _TLoader:
+ load_time = datetime.now(timezone.utc)
+ spec = importlib.util.find_spec(package)
+ reader = spec.loader.get_resource_reader(package) # type: ignore[union-attr]
+
+ def loader(
+ path: str | None,
+ ) -> tuple[str | None, _TOpener | None]:
+ if path is None:
+ return None, None
+
+ path = safe_join(package_path, path)
+
+ if path is None:
+ return None, None
+
+ basename = posixpath.basename(path)
+
+ try:
+ resource = reader.open_resource(path)
+ except OSError:
+ return None, None
+
+ if isinstance(resource, BytesIO):
+ return (
+ basename,
+ lambda: (resource, load_time, len(resource.getvalue())),
+ )
+
+ return (
+ basename,
+ lambda: (
+ resource,
+ datetime.fromtimestamp(
+ os.path.getmtime(resource.name), tz=timezone.utc
+ ),
+ os.path.getsize(resource.name),
+ ),
+ )
+
+ return loader
+
+ def get_directory_loader(self, directory: str) -> _TLoader:
+ def loader(
+ path: str | None,
+ ) -> tuple[str | None, _TOpener | None]:
+ if path is not None:
+ path = safe_join(directory, path)
+
+ if path is None:
+ return None, None
+ else:
+ path = directory
+
+ if os.path.isfile(path):
+ return os.path.basename(path), self._opener(path)
+
+ return None, None
+
+ return loader
+
+ def generate_etag(self, mtime: datetime, file_size: int, real_filename: str) -> str:
+ fn_str = os.fsencode(real_filename)
+ timestamp = mtime.timestamp()
+ checksum = adler32(fn_str) & 0xFFFFFFFF
+ return f"wzsdm-{timestamp}-{file_size}-{checksum}"
+
+ def __call__(
+ self, environ: WSGIEnvironment, start_response: StartResponse
+ ) -> t.Iterable[bytes]:
+ path = get_path_info(environ)
+ file_loader = None
+
+ for search_path, loader in self.exports:
+ if search_path == path:
+ real_filename, file_loader = loader(None)
+
+ if file_loader is not None:
+ break
+
+ if not search_path.endswith("/"):
+ search_path += "/"
+
+ if path.startswith(search_path):
+ real_filename, file_loader = loader(path[len(search_path) :])
+
+ if file_loader is not None:
+ break
+
+ if file_loader is None or not self.is_allowed(real_filename): # type: ignore
+ return self.app(environ, start_response)
+
+ guessed_type = mimetypes.guess_type(real_filename) # type: ignore
+ mime_type = get_content_type(guessed_type[0] or self.fallback_mimetype, "utf-8")
+ f, mtime, file_size = file_loader()
+
+ headers = [("Date", http_date())]
+
+ if self.cache:
+ timeout = self.cache_timeout
+ etag = self.generate_etag(mtime, file_size, real_filename) # type: ignore
+ headers += [
+ ("Etag", f'"{etag}"'),
+ ("Cache-Control", f"max-age={timeout}, public"),
+ ]
+
+ if not is_resource_modified(environ, etag, last_modified=mtime):
+ f.close()
+ start_response("304 Not Modified", headers)
+ return []
+
+ headers.append(("Expires", http_date(time() + timeout)))
+ else:
+ headers.append(("Cache-Control", "public"))
+
+ headers.extend(
+ (
+ ("Content-Type", mime_type),
+ ("Content-Length", str(file_size)),
+ ("Last-Modified", http_date(mtime)),
+ )
+ )
+ start_response("200 OK", headers)
+ return wrap_file(environ, f)
diff --git a/venv/Lib/site-packages/werkzeug/py.typed b/venv/Lib/site-packages/werkzeug/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/werkzeug/routing/__init__.py b/venv/Lib/site-packages/werkzeug/routing/__init__.py
new file mode 100644
index 0000000..62adc48
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/routing/__init__.py
@@ -0,0 +1,134 @@
+"""When it comes to combining multiple controller or view functions
+(however you want to call them) you need a dispatcher. A simple way
+would be applying regular expression tests on the ``PATH_INFO`` and
+calling registered callback functions that return the value then.
+
+This module implements a much more powerful system than simple regular
+expression matching because it can also convert values in the URLs and
+build URLs.
+
+Here a simple example that creates a URL map for an application with
+two subdomains (www and kb) and some URL rules:
+
+.. code-block:: python
+
+ m = Map([
+ # Static URLs
+ Rule('/', endpoint='static/index'),
+ Rule('/about', endpoint='static/about'),
+ Rule('/help', endpoint='static/help'),
+ # Knowledge Base
+ Subdomain('kb', [
+ Rule('/', endpoint='kb/index'),
+ Rule('/browse/', endpoint='kb/browse'),
+ Rule('/browse//', endpoint='kb/browse'),
+ Rule('/browse//', endpoint='kb/browse')
+ ])
+ ], default_subdomain='www')
+
+If the application doesn't use subdomains it's perfectly fine to not set
+the default subdomain and not use the `Subdomain` rule factory. The
+endpoint in the rules can be anything, for example import paths or
+unique identifiers. The WSGI application can use those endpoints to get the
+handler for that URL. It doesn't have to be a string at all but it's
+recommended.
+
+Now it's possible to create a URL adapter for one of the subdomains and
+build URLs:
+
+.. code-block:: python
+
+ c = m.bind('example.com')
+
+ c.build("kb/browse", dict(id=42))
+ 'http://kb.example.com/browse/42/'
+
+ c.build("kb/browse", dict())
+ 'http://kb.example.com/browse/'
+
+ c.build("kb/browse", dict(id=42, page=3))
+ 'http://kb.example.com/browse/42/3'
+
+ c.build("static/about")
+ '/about'
+
+ c.build("static/index", force_external=True)
+ 'http://www.example.com/'
+
+ c = m.bind('example.com', subdomain='kb')
+
+ c.build("static/about")
+ 'http://www.example.com/about'
+
+The first argument to bind is the server name *without* the subdomain.
+Per default it will assume that the script is mounted on the root, but
+often that's not the case so you can provide the real mount point as
+second argument:
+
+.. code-block:: python
+
+ c = m.bind('example.com', '/applications/example')
+
+The third argument can be the subdomain, if not given the default
+subdomain is used. For more details about binding have a look at the
+documentation of the `MapAdapter`.
+
+And here is how you can match URLs:
+
+.. code-block:: python
+
+ c = m.bind('example.com')
+
+ c.match("/")
+ ('static/index', {})
+
+ c.match("/about")
+ ('static/about', {})
+
+ c = m.bind('example.com', '/', 'kb')
+
+ c.match("/")
+ ('kb/index', {})
+
+ c.match("/browse/42/23")
+ ('kb/browse', {'id': 42, 'page': 23})
+
+If matching fails you get a ``NotFound`` exception, if the rule thinks
+it's a good idea to redirect (for example because the URL was defined
+to have a slash at the end but the request was missing that slash) it
+will raise a ``RequestRedirect`` exception. Both are subclasses of
+``HTTPException`` so you can use those errors as responses in the
+application.
+
+If matching succeeded but the URL rule was incompatible to the given
+method (for example there were only rules for ``GET`` and ``HEAD`` but
+routing tried to match a ``POST`` request) a ``MethodNotAllowed``
+exception is raised.
+"""
+
+from .converters import AnyConverter as AnyConverter
+from .converters import BaseConverter as BaseConverter
+from .converters import FloatConverter as FloatConverter
+from .converters import IntegerConverter as IntegerConverter
+from .converters import PathConverter as PathConverter
+from .converters import UnicodeConverter as UnicodeConverter
+from .converters import UUIDConverter as UUIDConverter
+from .converters import ValidationError as ValidationError
+from .exceptions import BuildError as BuildError
+from .exceptions import NoMatch as NoMatch
+from .exceptions import RequestAliasRedirect as RequestAliasRedirect
+from .exceptions import RequestPath as RequestPath
+from .exceptions import RequestRedirect as RequestRedirect
+from .exceptions import RoutingException as RoutingException
+from .exceptions import WebsocketMismatch as WebsocketMismatch
+from .map import Map as Map
+from .map import MapAdapter as MapAdapter
+from .matcher import StateMachineMatcher as StateMachineMatcher
+from .rules import EndpointPrefix as EndpointPrefix
+from .rules import parse_converter_args as parse_converter_args
+from .rules import Rule as Rule
+from .rules import RuleFactory as RuleFactory
+from .rules import RuleTemplate as RuleTemplate
+from .rules import RuleTemplateFactory as RuleTemplateFactory
+from .rules import Subdomain as Subdomain
+from .rules import Submount as Submount
diff --git a/venv/Lib/site-packages/werkzeug/routing/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/routing/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..554c603
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/routing/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/routing/__pycache__/converters.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/routing/__pycache__/converters.cpython-310.pyc
new file mode 100644
index 0000000..540e2ce
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/routing/__pycache__/converters.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/routing/__pycache__/exceptions.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/routing/__pycache__/exceptions.cpython-310.pyc
new file mode 100644
index 0000000..04fffca
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/routing/__pycache__/exceptions.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/routing/__pycache__/map.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/routing/__pycache__/map.cpython-310.pyc
new file mode 100644
index 0000000..1fcd812
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/routing/__pycache__/map.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/routing/__pycache__/matcher.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/routing/__pycache__/matcher.cpython-310.pyc
new file mode 100644
index 0000000..b9bf518
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/routing/__pycache__/matcher.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/routing/__pycache__/rules.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/routing/__pycache__/rules.cpython-310.pyc
new file mode 100644
index 0000000..c2945f6
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/routing/__pycache__/rules.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/routing/converters.py b/venv/Lib/site-packages/werkzeug/routing/converters.py
new file mode 100644
index 0000000..6016a97
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/routing/converters.py
@@ -0,0 +1,261 @@
+from __future__ import annotations
+
+import re
+import typing as t
+import uuid
+from urllib.parse import quote
+
+if t.TYPE_CHECKING:
+ from .map import Map
+
+
+class ValidationError(ValueError):
+ """Validation error. If a rule converter raises this exception the rule
+ does not match the current URL and the next URL is tried.
+ """
+
+
+class BaseConverter:
+ """Base class for all converters.
+
+ .. versionchanged:: 2.3
+ ``part_isolating`` defaults to ``False`` if ``regex`` contains a ``/``.
+ """
+
+ regex = "[^/]+"
+ weight = 100
+ part_isolating = True
+
+ def __init_subclass__(cls, **kwargs: t.Any) -> None:
+ super().__init_subclass__(**kwargs)
+
+ # If the converter isn't inheriting its regex, disable part_isolating by default
+ # if the regex contains a / character.
+ if "regex" in cls.__dict__ and "part_isolating" not in cls.__dict__:
+ cls.part_isolating = "/" not in cls.regex
+
+ def __init__(self, map: Map, *args: t.Any, **kwargs: t.Any) -> None:
+ self.map = map
+
+ def to_python(self, value: str) -> t.Any:
+ return value
+
+ def to_url(self, value: t.Any) -> str:
+ # safe = https://url.spec.whatwg.org/#url-path-segment-string
+ return quote(str(value), safe="!$&'()*+,/:;=@")
+
+
+class UnicodeConverter(BaseConverter):
+ """This converter is the default converter and accepts any string but
+ only one path segment. Thus the string can not include a slash.
+
+ This is the default validator.
+
+ Example::
+
+ Rule('/pages/'),
+ Rule('/')
+
+ :param map: the :class:`Map`.
+ :param minlength: the minimum length of the string. Must be greater
+ or equal 1.
+ :param maxlength: the maximum length of the string.
+ :param length: the exact length of the string.
+ """
+
+ def __init__(
+ self,
+ map: Map,
+ minlength: int = 1,
+ maxlength: int | None = None,
+ length: int | None = None,
+ ) -> None:
+ super().__init__(map)
+ if length is not None:
+ length_regex = f"{{{int(length)}}}"
+ else:
+ if maxlength is None:
+ maxlength_value = ""
+ else:
+ maxlength_value = str(int(maxlength))
+ length_regex = f"{{{int(minlength)},{maxlength_value}}}"
+ self.regex = f"[^/]{length_regex}"
+
+
+class AnyConverter(BaseConverter):
+ """Matches one of the items provided. Items can either be Python
+ identifiers or strings::
+
+ Rule('/')
+
+ :param map: the :class:`Map`.
+ :param items: this function accepts the possible items as positional
+ arguments.
+
+ .. versionchanged:: 2.2
+ Value is validated when building a URL.
+ """
+
+ def __init__(self, map: Map, *items: str) -> None:
+ super().__init__(map)
+ self.items = set(items)
+ self.regex = f"(?:{'|'.join([re.escape(x) for x in items])})"
+
+ def to_url(self, value: t.Any) -> str:
+ if value in self.items:
+ return str(value)
+
+ valid_values = ", ".join(f"'{item}'" for item in sorted(self.items))
+ raise ValueError(f"'{value}' is not one of {valid_values}")
+
+
+class PathConverter(BaseConverter):
+ """Like the default :class:`UnicodeConverter`, but it also matches
+ slashes. This is useful for wikis and similar applications::
+
+ Rule('/')
+ Rule('//edit')
+
+ :param map: the :class:`Map`.
+ """
+
+ part_isolating = False
+ regex = "[^/].*?"
+ weight = 200
+
+
+class NumberConverter(BaseConverter):
+ """Baseclass for `IntegerConverter` and `FloatConverter`.
+
+ :internal:
+ """
+
+ weight = 50
+ num_convert: t.Callable[[t.Any], t.Any] = int
+
+ def __init__(
+ self,
+ map: Map,
+ fixed_digits: int = 0,
+ min: int | None = None,
+ max: int | None = None,
+ signed: bool = False,
+ ) -> None:
+ if signed:
+ self.regex = self.signed_regex
+ super().__init__(map)
+ self.fixed_digits = fixed_digits
+ self.min = min
+ self.max = max
+ self.signed = signed
+
+ def to_python(self, value: str) -> t.Any:
+ if self.fixed_digits and len(value) != self.fixed_digits:
+ raise ValidationError()
+ value_num = self.num_convert(value)
+ if (self.min is not None and value_num < self.min) or (
+ self.max is not None and value_num > self.max
+ ):
+ raise ValidationError()
+ return value_num
+
+ def to_url(self, value: t.Any) -> str:
+ value_str = str(self.num_convert(value))
+ if self.fixed_digits:
+ value_str = value_str.zfill(self.fixed_digits)
+ return value_str
+
+ @property
+ def signed_regex(self) -> str:
+ return f"-?{self.regex}"
+
+
+class IntegerConverter(NumberConverter):
+ """This converter only accepts integer values::
+
+ Rule("/page/")
+
+ By default it only accepts unsigned, positive values. The ``signed``
+ parameter will enable signed, negative values. ::
+
+ Rule("/page/")
+
+ :param map: The :class:`Map`.
+ :param fixed_digits: The number of fixed digits in the URL. If you
+ set this to ``4`` for example, the rule will only match if the
+ URL looks like ``/0001/``. The default is variable length.
+ :param min: The minimal value.
+ :param max: The maximal value.
+ :param signed: Allow signed (negative) values.
+
+ .. versionadded:: 0.15
+ The ``signed`` parameter.
+ """
+
+ regex = r"\d+"
+
+
+class FloatConverter(NumberConverter):
+ """This converter only accepts floating point values::
+
+ Rule("/probability/")
+
+ By default it only accepts unsigned, positive values. The ``signed``
+ parameter will enable signed, negative values. ::
+
+ Rule("/offset/")
+
+ :param map: The :class:`Map`.
+ :param min: The minimal value.
+ :param max: The maximal value.
+ :param signed: Allow signed (negative) values.
+
+ .. versionadded:: 0.15
+ The ``signed`` parameter.
+ """
+
+ regex = r"\d+\.\d+"
+ num_convert = float
+
+ def __init__(
+ self,
+ map: Map,
+ min: float | None = None,
+ max: float | None = None,
+ signed: bool = False,
+ ) -> None:
+ super().__init__(map, min=min, max=max, signed=signed) # type: ignore
+
+
+class UUIDConverter(BaseConverter):
+ """This converter only accepts UUID strings::
+
+ Rule('/object/')
+
+ .. versionadded:: 0.10
+
+ :param map: the :class:`Map`.
+ """
+
+ regex = (
+ r"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-"
+ r"[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}"
+ )
+
+ def to_python(self, value: str) -> uuid.UUID:
+ return uuid.UUID(value)
+
+ def to_url(self, value: uuid.UUID) -> str:
+ return str(value)
+
+
+#: the default converter mapping for the map.
+DEFAULT_CONVERTERS: t.Mapping[str, type[BaseConverter]] = {
+ "default": UnicodeConverter,
+ "string": UnicodeConverter,
+ "any": AnyConverter,
+ "path": PathConverter,
+ "int": IntegerConverter,
+ "float": FloatConverter,
+ "uuid": UUIDConverter,
+}
diff --git a/venv/Lib/site-packages/werkzeug/routing/exceptions.py b/venv/Lib/site-packages/werkzeug/routing/exceptions.py
new file mode 100644
index 0000000..eeabd4e
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/routing/exceptions.py
@@ -0,0 +1,152 @@
+from __future__ import annotations
+
+import difflib
+import typing as t
+
+from ..exceptions import BadRequest
+from ..exceptions import HTTPException
+from ..utils import cached_property
+from ..utils import redirect
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import WSGIEnvironment
+
+ from ..wrappers.request import Request
+ from ..wrappers.response import Response
+ from .map import MapAdapter
+ from .rules import Rule
+
+
+class RoutingException(Exception):
+ """Special exceptions that require the application to redirect, notifying
+ about missing urls, etc.
+
+ :internal:
+ """
+
+
+class RequestRedirect(HTTPException, RoutingException):
+ """Raise if the map requests a redirect. This is for example the case if
+ `strict_slashes` are activated and an url that requires a trailing slash.
+
+ The attribute `new_url` contains the absolute destination url.
+ """
+
+ code = 308
+
+ def __init__(self, new_url: str) -> None:
+ super().__init__(new_url)
+ self.new_url = new_url
+
+ def get_response(
+ self,
+ environ: WSGIEnvironment | Request | None = None,
+ scope: dict[str, t.Any] | None = None,
+ ) -> Response:
+ return redirect(self.new_url, self.code)
+
+
+class RequestPath(RoutingException):
+ """Internal exception."""
+
+ __slots__ = ("path_info",)
+
+ def __init__(self, path_info: str) -> None:
+ super().__init__()
+ self.path_info = path_info
+
+
+class RequestAliasRedirect(RoutingException): # noqa: B903
+ """This rule is an alias and wants to redirect to the canonical URL."""
+
+ def __init__(self, matched_values: t.Mapping[str, t.Any], endpoint: t.Any) -> None:
+ super().__init__()
+ self.matched_values = matched_values
+ self.endpoint = endpoint
+
+
+class BuildError(RoutingException, LookupError):
+ """Raised if the build system cannot find a URL for an endpoint with the
+ values provided.
+ """
+
+ def __init__(
+ self,
+ endpoint: t.Any,
+ values: t.Mapping[str, t.Any],
+ method: str | None,
+ adapter: MapAdapter | None = None,
+ ) -> None:
+ super().__init__(endpoint, values, method)
+ self.endpoint = endpoint
+ self.values = values
+ self.method = method
+ self.adapter = adapter
+
+ @cached_property
+ def suggested(self) -> Rule | None:
+ return self.closest_rule(self.adapter)
+
+ def closest_rule(self, adapter: MapAdapter | None) -> Rule | None:
+ def _score_rule(rule: Rule) -> float:
+ return sum(
+ [
+ 0.98
+ * difflib.SequenceMatcher(
+ # endpoints can be any type, compare as strings
+ None,
+ str(rule.endpoint),
+ str(self.endpoint),
+ ).ratio(),
+ 0.01 * bool(set(self.values or ()).issubset(rule.arguments)),
+ 0.01 * bool(rule.methods and self.method in rule.methods),
+ ]
+ )
+
+ if adapter and adapter.map._rules:
+ return max(adapter.map._rules, key=_score_rule)
+
+ return None
+
+ def __str__(self) -> str:
+ message = [f"Could not build url for endpoint {self.endpoint!r}"]
+ if self.method:
+ message.append(f" ({self.method!r})")
+ if self.values:
+ message.append(f" with values {sorted(self.values)!r}")
+ message.append(".")
+ if self.suggested:
+ if self.endpoint == self.suggested.endpoint:
+ if (
+ self.method
+ and self.suggested.methods is not None
+ and self.method not in self.suggested.methods
+ ):
+ message.append(
+ " Did you mean to use methods"
+ f" {sorted(self.suggested.methods)!r}?"
+ )
+ missing_values = self.suggested.arguments.union(
+ set(self.suggested.defaults or ())
+ ) - set(self.values.keys())
+ if missing_values:
+ message.append(
+ f" Did you forget to specify values {sorted(missing_values)!r}?"
+ )
+ else:
+ message.append(f" Did you mean {self.suggested.endpoint!r} instead?")
+ return "".join(message)
+
+
+class WebsocketMismatch(BadRequest):
+ """The only matched rule is either a WebSocket and the request is
+ HTTP, or the rule is HTTP and the request is a WebSocket.
+ """
+
+
+class NoMatch(Exception):
+ __slots__ = ("have_match_for", "websocket_mismatch")
+
+ def __init__(self, have_match_for: set[str], websocket_mismatch: bool) -> None:
+ self.have_match_for = have_match_for
+ self.websocket_mismatch = websocket_mismatch
diff --git a/venv/Lib/site-packages/werkzeug/routing/map.py b/venv/Lib/site-packages/werkzeug/routing/map.py
new file mode 100644
index 0000000..c86f95a
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/routing/map.py
@@ -0,0 +1,928 @@
+from __future__ import annotations
+
+import typing as t
+import warnings
+from pprint import pformat
+from threading import Lock
+from urllib.parse import quote
+from urllib.parse import urljoin
+from urllib.parse import urlunsplit
+
+from .._internal import _get_environ
+from .._internal import _wsgi_decoding_dance
+from ..datastructures import ImmutableDict
+from ..datastructures import MultiDict
+from ..exceptions import BadHost
+from ..exceptions import HTTPException
+from ..exceptions import MethodNotAllowed
+from ..exceptions import NotFound
+from ..urls import _urlencode
+from ..wsgi import get_host
+from .converters import DEFAULT_CONVERTERS
+from .exceptions import BuildError
+from .exceptions import NoMatch
+from .exceptions import RequestAliasRedirect
+from .exceptions import RequestPath
+from .exceptions import RequestRedirect
+from .exceptions import WebsocketMismatch
+from .matcher import StateMachineMatcher
+from .rules import _simple_rule_re
+from .rules import Rule
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import WSGIApplication
+ from _typeshed.wsgi import WSGIEnvironment
+
+ from ..wrappers.request import Request
+ from .converters import BaseConverter
+ from .rules import RuleFactory
+
+
+class Map:
+ """The map class stores all the URL rules and some configuration
+ parameters. Some of the configuration values are only stored on the
+ `Map` instance since those affect all rules, others are just defaults
+ and can be overridden for each rule. Note that you have to specify all
+ arguments besides the `rules` as keyword arguments!
+
+ :param rules: sequence of url rules for this map.
+ :param default_subdomain: The default subdomain for rules without a
+ subdomain defined.
+ :param strict_slashes: If a rule ends with a slash but the matched
+ URL does not, redirect to the URL with a trailing slash.
+ :param merge_slashes: Merge consecutive slashes when matching or
+ building URLs. Matches will redirect to the normalized URL.
+ Slashes in variable parts are not merged.
+ :param redirect_defaults: This will redirect to the default rule if it
+ wasn't visited that way. This helps creating
+ unique URLs.
+ :param converters: A dict of converters that adds additional converters
+ to the list of converters. If you redefine one
+ converter this will override the original one.
+ :param sort_parameters: If set to `True` the url parameters are sorted.
+ See `url_encode` for more details.
+ :param sort_key: The sort key function for `url_encode`.
+ :param host_matching: if set to `True` it enables the host matching
+ feature and disables the subdomain one. If
+ enabled the `host` parameter to rules is used
+ instead of the `subdomain` one.
+
+ .. versionchanged:: 3.0
+ The ``charset`` and ``encoding_errors`` parameters were removed.
+
+ .. versionchanged:: 1.0
+ If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules will match.
+
+ .. versionchanged:: 1.0
+ The ``merge_slashes`` parameter was added.
+
+ .. versionchanged:: 0.7
+ The ``encoding_errors`` and ``host_matching`` parameters were added.
+
+ .. versionchanged:: 0.5
+ The ``sort_parameters`` and ``sort_key`` parameters were added.
+ """
+
+ #: A dict of default converters to be used.
+ default_converters = ImmutableDict(DEFAULT_CONVERTERS)
+
+ #: The type of lock to use when updating.
+ #:
+ #: .. versionadded:: 1.0
+ lock_class = Lock
+
+ def __init__(
+ self,
+ rules: t.Iterable[RuleFactory] | None = None,
+ default_subdomain: str = "",
+ strict_slashes: bool = True,
+ merge_slashes: bool = True,
+ redirect_defaults: bool = True,
+ converters: t.Mapping[str, type[BaseConverter]] | None = None,
+ sort_parameters: bool = False,
+ sort_key: t.Callable[[t.Any], t.Any] | None = None,
+ host_matching: bool = False,
+ ) -> None:
+ self._matcher = StateMachineMatcher(merge_slashes)
+ self._rules_by_endpoint: dict[t.Any, list[Rule]] = {}
+ self._remap = True
+ self._remap_lock = self.lock_class()
+
+ self.default_subdomain = default_subdomain
+ self.strict_slashes = strict_slashes
+ self.redirect_defaults = redirect_defaults
+ self.host_matching = host_matching
+
+ self.converters = self.default_converters.copy()
+ if converters:
+ self.converters.update(converters)
+
+ self.sort_parameters = sort_parameters
+ self.sort_key = sort_key
+
+ for rulefactory in rules or ():
+ self.add(rulefactory)
+
+ @property
+ def merge_slashes(self) -> bool:
+ return self._matcher.merge_slashes
+
+ @merge_slashes.setter
+ def merge_slashes(self, value: bool) -> None:
+ self._matcher.merge_slashes = value
+
+ def is_endpoint_expecting(self, endpoint: t.Any, *arguments: str) -> bool:
+ """Iterate over all rules and check if the endpoint expects
+ the arguments provided. This is for example useful if you have
+ some URLs that expect a language code and others that do not and
+ you want to wrap the builder a bit so that the current language
+ code is automatically added if not provided but endpoints expect
+ it.
+
+ :param endpoint: the endpoint to check.
+ :param arguments: this function accepts one or more arguments
+ as positional arguments. Each one of them is
+ checked.
+ """
+ self.update()
+ arguments_set = set(arguments)
+ for rule in self._rules_by_endpoint[endpoint]:
+ if arguments_set.issubset(rule.arguments):
+ return True
+ return False
+
+ @property
+ def _rules(self) -> list[Rule]:
+ return [rule for rules in self._rules_by_endpoint.values() for rule in rules]
+
+ def iter_rules(self, endpoint: t.Any | None = None) -> t.Iterator[Rule]:
+ """Iterate over all rules or the rules of an endpoint.
+
+ :param endpoint: if provided only the rules for that endpoint
+ are returned.
+ :return: an iterator
+ """
+ self.update()
+ if endpoint is not None:
+ return iter(self._rules_by_endpoint[endpoint])
+ return iter(self._rules)
+
+ def add(self, rulefactory: RuleFactory) -> None:
+ """Add a new rule or factory to the map and bind it. Requires that the
+ rule is not bound to another map.
+
+ :param rulefactory: a :class:`Rule` or :class:`RuleFactory`
+ """
+ for rule in rulefactory.get_rules(self):
+ rule.bind(self)
+ if not rule.build_only:
+ self._matcher.add(rule)
+ self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
+ self._remap = True
+
+ def bind(
+ self,
+ server_name: str,
+ script_name: str | None = None,
+ subdomain: str | None = None,
+ url_scheme: str = "http",
+ default_method: str = "GET",
+ path_info: str | None = None,
+ query_args: t.Mapping[str, t.Any] | str | None = None,
+ ) -> MapAdapter:
+ """Return a new :class:`MapAdapter` with the details specified to the
+ call. Note that `script_name` will default to ``'/'`` if not further
+ specified or `None`. The `server_name` at least is a requirement
+ because the HTTP RFC requires absolute URLs for redirects and so all
+ redirect exceptions raised by Werkzeug will contain the full canonical
+ URL.
+
+ If no path_info is passed to :meth:`match` it will use the default path
+ info passed to bind. While this doesn't really make sense for
+ manual bind calls, it's useful if you bind a map to a WSGI
+ environment which already contains the path info.
+
+ `subdomain` will default to the `default_subdomain` for this map if
+ no defined. If there is no `default_subdomain` you cannot use the
+ subdomain feature.
+
+ .. versionchanged:: 1.0
+ If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules
+ will match.
+
+ .. versionchanged:: 0.15
+ ``path_info`` defaults to ``'/'`` if ``None``.
+
+ .. versionchanged:: 0.8
+ ``query_args`` can be a string.
+
+ .. versionchanged:: 0.7
+ Added ``query_args``.
+ """
+ server_name = server_name.lower()
+ if self.host_matching:
+ if subdomain is not None:
+ raise RuntimeError("host matching enabled and a subdomain was provided")
+ elif subdomain is None:
+ subdomain = self.default_subdomain
+ if script_name is None:
+ script_name = "/"
+ if path_info is None:
+ path_info = "/"
+
+ # Port isn't part of IDNA, and might push a name over the 63 octet limit.
+ server_name, port_sep, port = server_name.partition(":")
+
+ try:
+ server_name = server_name.encode("idna").decode("ascii")
+ except UnicodeError as e:
+ raise BadHost() from e
+
+ return MapAdapter(
+ self,
+ f"{server_name}{port_sep}{port}",
+ script_name,
+ subdomain,
+ url_scheme,
+ path_info,
+ default_method,
+ query_args,
+ )
+
+ def bind_to_environ(
+ self,
+ environ: WSGIEnvironment | Request,
+ server_name: str | None = None,
+ subdomain: str | None = None,
+ ) -> MapAdapter:
+ """Like :meth:`bind` but you can pass it an WSGI environment and it
+ will fetch the information from that dictionary. Note that because of
+ limitations in the protocol there is no way to get the current
+ subdomain and real `server_name` from the environment. If you don't
+ provide it, Werkzeug will use `SERVER_NAME` and `SERVER_PORT` (or
+ `HTTP_HOST` if provided) as used `server_name` with disabled subdomain
+ feature.
+
+ If `subdomain` is `None` but an environment and a server name is
+ provided it will calculate the current subdomain automatically.
+ Example: `server_name` is ``'example.com'`` and the `SERVER_NAME`
+ in the wsgi `environ` is ``'staging.dev.example.com'`` the calculated
+ subdomain will be ``'staging.dev'``.
+
+ If the object passed as environ has an environ attribute, the value of
+ this attribute is used instead. This allows you to pass request
+ objects. Additionally `PATH_INFO` added as a default of the
+ :class:`MapAdapter` so that you don't have to pass the path info to
+ the match method.
+
+ .. versionchanged:: 1.0.0
+ If the passed server name specifies port 443, it will match
+ if the incoming scheme is ``https`` without a port.
+
+ .. versionchanged:: 1.0.0
+ A warning is shown when the passed server name does not
+ match the incoming WSGI server name.
+
+ .. versionchanged:: 0.8
+ This will no longer raise a ValueError when an unexpected server
+ name was passed.
+
+ .. versionchanged:: 0.5
+ previously this method accepted a bogus `calculate_subdomain`
+ parameter that did not have any effect. It was removed because
+ of that.
+
+ :param environ: a WSGI environment.
+ :param server_name: an optional server name hint (see above).
+ :param subdomain: optionally the current subdomain (see above).
+ """
+ env = _get_environ(environ)
+ wsgi_server_name = get_host(env).lower()
+ scheme = env["wsgi.url_scheme"]
+ upgrade = any(
+ v.strip() == "upgrade"
+ for v in env.get("HTTP_CONNECTION", "").lower().split(",")
+ )
+
+ if upgrade and env.get("HTTP_UPGRADE", "").lower() == "websocket":
+ scheme = "wss" if scheme == "https" else "ws"
+
+ if server_name is None:
+ server_name = wsgi_server_name
+ else:
+ server_name = server_name.lower()
+
+ # strip standard port to match get_host()
+ if scheme in {"http", "ws"} and server_name.endswith(":80"):
+ server_name = server_name[:-3]
+ elif scheme in {"https", "wss"} and server_name.endswith(":443"):
+ server_name = server_name[:-4]
+
+ if subdomain is None and not self.host_matching:
+ cur_server_name = wsgi_server_name.split(".")
+ real_server_name = server_name.split(".")
+ offset = -len(real_server_name)
+
+ if cur_server_name[offset:] != real_server_name:
+ # This can happen even with valid configs if the server was
+ # accessed directly by IP address under some situations.
+ # Instead of raising an exception like in Werkzeug 0.7 or
+ # earlier we go by an invalid subdomain which will result
+ # in a 404 error on matching.
+ warnings.warn(
+ f"Current server name {wsgi_server_name!r} doesn't match configured"
+ f" server name {server_name!r}",
+ stacklevel=2,
+ )
+ subdomain = ""
+ else:
+ subdomain = ".".join(filter(None, cur_server_name[:offset]))
+
+ def _get_wsgi_string(name: str) -> str | None:
+ val = env.get(name)
+ if val is not None:
+ return _wsgi_decoding_dance(val)
+ return None
+
+ script_name = _get_wsgi_string("SCRIPT_NAME")
+ path_info = _get_wsgi_string("PATH_INFO")
+ query_args = _get_wsgi_string("QUERY_STRING")
+ return Map.bind(
+ self,
+ server_name,
+ script_name,
+ subdomain,
+ scheme,
+ env["REQUEST_METHOD"],
+ path_info,
+ query_args=query_args,
+ )
+
+ def update(self) -> None:
+ """Called before matching and building to keep the compiled rules
+ in the correct order after things changed.
+ """
+ if not self._remap:
+ return
+
+ with self._remap_lock:
+ if not self._remap:
+ return
+
+ self._matcher.update()
+ for rules in self._rules_by_endpoint.values():
+ rules.sort(key=lambda x: x.build_compare_key())
+ self._remap = False
+
+ def __repr__(self) -> str:
+ rules = self.iter_rules()
+ return f"{type(self).__name__}({pformat(list(rules))})"
+
+
+class MapAdapter:
+ """Returned by :meth:`Map.bind` or :meth:`Map.bind_to_environ` and does
+ the URL matching and building based on runtime information.
+ """
+
+ def __init__(
+ self,
+ map: Map,
+ server_name: str,
+ script_name: str,
+ subdomain: str | None,
+ url_scheme: str,
+ path_info: str,
+ default_method: str,
+ query_args: t.Mapping[str, t.Any] | str | None = None,
+ ):
+ self.map = map
+ self.server_name = server_name
+
+ if not script_name.endswith("/"):
+ script_name += "/"
+
+ self.script_name = script_name
+ self.subdomain = subdomain
+ self.url_scheme = url_scheme
+ self.path_info = path_info
+ self.default_method = default_method
+ self.query_args = query_args
+ self.websocket = self.url_scheme in {"ws", "wss"}
+
+ def dispatch(
+ self,
+ view_func: t.Callable[[str, t.Mapping[str, t.Any]], WSGIApplication],
+ path_info: str | None = None,
+ method: str | None = None,
+ catch_http_exceptions: bool = False,
+ ) -> WSGIApplication:
+ """Does the complete dispatching process. `view_func` is called with
+ the endpoint and a dict with the values for the view. It should
+ look up the view function, call it, and return a response object
+ or WSGI application. http exceptions are not caught by default
+ so that applications can display nicer error messages by just
+ catching them by hand. If you want to stick with the default
+ error messages you can pass it ``catch_http_exceptions=True`` and
+ it will catch the http exceptions.
+
+ Here a small example for the dispatch usage::
+
+ from werkzeug.wrappers import Request, Response
+ from werkzeug.wsgi import responder
+ from werkzeug.routing import Map, Rule
+
+ def on_index(request):
+ return Response('Hello from the index')
+
+ url_map = Map([Rule('/', endpoint='index')])
+ views = {'index': on_index}
+
+ @responder
+ def application(environ, start_response):
+ request = Request(environ)
+ urls = url_map.bind_to_environ(environ)
+ return urls.dispatch(lambda e, v: views[e](request, **v),
+ catch_http_exceptions=True)
+
+ Keep in mind that this method might return exception objects, too, so
+ use :class:`Response.force_type` to get a response object.
+
+ :param view_func: a function that is called with the endpoint as
+ first argument and the value dict as second. Has
+ to dispatch to the actual view function with this
+ information. (see above)
+ :param path_info: the path info to use for matching. Overrides the
+ path info specified on binding.
+ :param method: the HTTP method used for matching. Overrides the
+ method specified on binding.
+ :param catch_http_exceptions: set to `True` to catch any of the
+ werkzeug :class:`HTTPException`\\s.
+ """
+ try:
+ try:
+ endpoint, args = self.match(path_info, method)
+ except RequestRedirect as e:
+ return e
+ return view_func(endpoint, args)
+ except HTTPException as e:
+ if catch_http_exceptions:
+ return e
+ raise
+
+ @t.overload
+ def match(
+ self,
+ path_info: str | None = None,
+ method: str | None = None,
+ return_rule: t.Literal[False] = False,
+ query_args: t.Mapping[str, t.Any] | str | None = None,
+ websocket: bool | None = None,
+ ) -> tuple[t.Any, t.Mapping[str, t.Any]]: ...
+
+ @t.overload
+ def match(
+ self,
+ path_info: str | None = None,
+ method: str | None = None,
+ return_rule: t.Literal[True] = True,
+ query_args: t.Mapping[str, t.Any] | str | None = None,
+ websocket: bool | None = None,
+ ) -> tuple[Rule, t.Mapping[str, t.Any]]: ...
+
+ def match(
+ self,
+ path_info: str | None = None,
+ method: str | None = None,
+ return_rule: bool = False,
+ query_args: t.Mapping[str, t.Any] | str | None = None,
+ websocket: bool | None = None,
+ ) -> tuple[t.Any | Rule, t.Mapping[str, t.Any]]:
+ """The usage is simple: you just pass the match method the current
+ path info as well as the method (which defaults to `GET`). The
+ following things can then happen:
+
+ - you receive a `NotFound` exception that indicates that no URL is
+ matching. A `NotFound` exception is also a WSGI application you
+ can call to get a default page not found page (happens to be the
+ same object as `werkzeug.exceptions.NotFound`)
+
+ - you receive a `MethodNotAllowed` exception that indicates that there
+ is a match for this URL but not for the current request method.
+ This is useful for RESTful applications.
+
+ - you receive a `RequestRedirect` exception with a `new_url`
+ attribute. This exception is used to notify you about a request
+ Werkzeug requests from your WSGI application. This is for example the
+ case if you request ``/foo`` although the correct URL is ``/foo/``
+ You can use the `RequestRedirect` instance as response-like object
+ similar to all other subclasses of `HTTPException`.
+
+ - you receive a ``WebsocketMismatch`` exception if the only
+ match is a WebSocket rule but the bind is an HTTP request, or
+ if the match is an HTTP rule but the bind is a WebSocket
+ request.
+
+ - you get a tuple in the form ``(endpoint, arguments)`` if there is
+ a match (unless `return_rule` is True, in which case you get a tuple
+ in the form ``(rule, arguments)``)
+
+ If the path info is not passed to the match method the default path
+ info of the map is used (defaults to the root URL if not defined
+ explicitly).
+
+ All of the exceptions raised are subclasses of `HTTPException` so they
+ can be used as WSGI responses. They will all render generic error or
+ redirect pages.
+
+ Here is a small example for matching:
+
+ >>> m = Map([
+ ... Rule('/', endpoint='index'),
+ ... Rule('/downloads/', endpoint='downloads/index'),
+ ... Rule('/downloads/', endpoint='downloads/show')
+ ... ])
+ >>> urls = m.bind("example.com", "/")
+ >>> urls.match("/", "GET")
+ ('index', {})
+ >>> urls.match("/downloads/42")
+ ('downloads/show', {'id': 42})
+
+ And here is what happens on redirect and missing URLs:
+
+ >>> urls.match("/downloads")
+ Traceback (most recent call last):
+ ...
+ RequestRedirect: http://example.com/downloads/
+ >>> urls.match("/missing")
+ Traceback (most recent call last):
+ ...
+ NotFound: 404 Not Found
+
+ :param path_info: the path info to use for matching. Overrides the
+ path info specified on binding.
+ :param method: the HTTP method used for matching. Overrides the
+ method specified on binding.
+ :param return_rule: return the rule that matched instead of just the
+ endpoint (defaults to `False`).
+ :param query_args: optional query arguments that are used for
+ automatic redirects as string or dictionary. It's
+ currently not possible to use the query arguments
+ for URL matching.
+ :param websocket: Match WebSocket instead of HTTP requests. A
+ websocket request has a ``ws`` or ``wss``
+ :attr:`url_scheme`. This overrides that detection.
+
+ .. versionadded:: 1.0
+ Added ``websocket``.
+
+ .. versionchanged:: 0.8
+ ``query_args`` can be a string.
+
+ .. versionadded:: 0.7
+ Added ``query_args``.
+
+ .. versionadded:: 0.6
+ Added ``return_rule``.
+ """
+ self.map.update()
+ if path_info is None:
+ path_info = self.path_info
+ if query_args is None:
+ query_args = self.query_args or {}
+ method = (method or self.default_method).upper()
+
+ if websocket is None:
+ websocket = self.websocket
+
+ domain_part = self.server_name
+
+ if not self.map.host_matching and self.subdomain is not None:
+ domain_part = self.subdomain
+
+ path_part = f"/{path_info.lstrip('/')}" if path_info else ""
+
+ try:
+ result = self.map._matcher.match(domain_part, path_part, method, websocket)
+ except RequestPath as e:
+ # safe = https://url.spec.whatwg.org/#url-path-segment-string
+ new_path = quote(e.path_info, safe="!$&'()*+,/:;=@")
+ raise RequestRedirect(
+ self.make_redirect_url(new_path, query_args)
+ ) from None
+ except RequestAliasRedirect as e:
+ raise RequestRedirect(
+ self.make_alias_redirect_url(
+ f"{domain_part}|{path_part}",
+ e.endpoint,
+ e.matched_values,
+ method,
+ query_args,
+ )
+ ) from None
+ except NoMatch as e:
+ if e.have_match_for:
+ raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None
+
+ if e.websocket_mismatch:
+ raise WebsocketMismatch() from None
+
+ raise NotFound() from None
+ else:
+ rule, rv = result
+
+ if self.map.redirect_defaults:
+ redirect_url = self.get_default_redirect(rule, method, rv, query_args)
+ if redirect_url is not None:
+ raise RequestRedirect(redirect_url)
+
+ if rule.redirect_to is not None:
+ if isinstance(rule.redirect_to, str):
+
+ def _handle_match(match: t.Match[str]) -> str:
+ value = rv[match.group(1)]
+ return rule._converters[match.group(1)].to_url(value)
+
+ redirect_url = _simple_rule_re.sub(_handle_match, rule.redirect_to)
+ else:
+ redirect_url = rule.redirect_to(self, **rv)
+
+ if self.subdomain:
+ netloc = f"{self.subdomain}.{self.server_name}"
+ else:
+ netloc = self.server_name
+
+ raise RequestRedirect(
+ urljoin(
+ f"{self.url_scheme or 'http'}://{netloc}{self.script_name}",
+ redirect_url,
+ )
+ )
+
+ if return_rule:
+ return rule, rv
+ else:
+ return rule.endpoint, rv
+
+ def test(self, path_info: str | None = None, method: str | None = None) -> bool:
+ """Test if a rule would match. Works like `match` but returns `True`
+ if the URL matches, or `False` if it does not exist.
+
+ :param path_info: the path info to use for matching. Overrides the
+ path info specified on binding.
+ :param method: the HTTP method used for matching. Overrides the
+ method specified on binding.
+ """
+ try:
+ self.match(path_info, method)
+ except RequestRedirect:
+ pass
+ except HTTPException:
+ return False
+ return True
+
+ def allowed_methods(self, path_info: str | None = None) -> t.Iterable[str]:
+ """Returns the valid methods that match for a given path.
+
+ .. versionadded:: 0.7
+ """
+ try:
+ self.match(path_info, method="--")
+ except MethodNotAllowed as e:
+ return e.valid_methods # type: ignore
+ except HTTPException:
+ pass
+ return []
+
+ def get_host(self, domain_part: str | None) -> str:
+ """Figures out the full host name for the given domain part. The
+ domain part is a subdomain in case host matching is disabled or
+ a full host name.
+ """
+ if self.map.host_matching:
+ if domain_part is None:
+ return self.server_name
+
+ return domain_part
+
+ if domain_part is None:
+ subdomain = self.subdomain
+ else:
+ subdomain = domain_part
+
+ if subdomain:
+ return f"{subdomain}.{self.server_name}"
+ else:
+ return self.server_name
+
+ def get_default_redirect(
+ self,
+ rule: Rule,
+ method: str,
+ values: t.MutableMapping[str, t.Any],
+ query_args: t.Mapping[str, t.Any] | str,
+ ) -> str | None:
+ """A helper that returns the URL to redirect to if it finds one.
+ This is used for default redirecting only.
+
+ :internal:
+ """
+ assert self.map.redirect_defaults
+ for r in self.map._rules_by_endpoint[rule.endpoint]:
+ # every rule that comes after this one, including ourself
+ # has a lower priority for the defaults. We order the ones
+ # with the highest priority up for building.
+ if r is rule:
+ break
+ if r.provides_defaults_for(rule) and r.suitable_for(values, method):
+ values.update(r.defaults) # type: ignore
+ domain_part, path = r.build(values) # type: ignore
+ return self.make_redirect_url(path, query_args, domain_part=domain_part)
+ return None
+
+ def encode_query_args(self, query_args: t.Mapping[str, t.Any] | str) -> str:
+ if not isinstance(query_args, str):
+ return _urlencode(query_args)
+ return query_args
+
+ def make_redirect_url(
+ self,
+ path_info: str,
+ query_args: t.Mapping[str, t.Any] | str | None = None,
+ domain_part: str | None = None,
+ ) -> str:
+ """Creates a redirect URL.
+
+ :internal:
+ """
+ if query_args is None:
+ query_args = self.query_args
+
+ if query_args:
+ query_str = self.encode_query_args(query_args)
+ else:
+ query_str = None
+
+ scheme = self.url_scheme or "http"
+ host = self.get_host(domain_part)
+ path = "/".join((self.script_name.strip("/"), path_info.lstrip("/")))
+ return urlunsplit((scheme, host, path, query_str, None))
+
+ def make_alias_redirect_url(
+ self,
+ path: str,
+ endpoint: t.Any,
+ values: t.Mapping[str, t.Any],
+ method: str,
+ query_args: t.Mapping[str, t.Any] | str,
+ ) -> str:
+ """Internally called to make an alias redirect URL."""
+ url = self.build(
+ endpoint, values, method, append_unknown=False, force_external=True
+ )
+ if query_args:
+ url += f"?{self.encode_query_args(query_args)}"
+ assert url != path, "detected invalid alias setting. No canonical URL found"
+ return url
+
+ def _partial_build(
+ self,
+ endpoint: t.Any,
+ values: t.Mapping[str, t.Any],
+ method: str | None,
+ append_unknown: bool,
+ ) -> tuple[str, str, bool] | None:
+ """Helper for :meth:`build`. Returns subdomain and path for the
+ rule that accepts this endpoint, values and method.
+
+ :internal:
+ """
+ # in case the method is none, try with the default method first
+ if method is None:
+ rv = self._partial_build(
+ endpoint, values, self.default_method, append_unknown
+ )
+ if rv is not None:
+ return rv
+
+ # Default method did not match or a specific method is passed.
+ # Check all for first match with matching host. If no matching
+ # host is found, go with first result.
+ first_match = None
+
+ for rule in self.map._rules_by_endpoint.get(endpoint, ()):
+ if rule.suitable_for(values, method):
+ build_rv = rule.build(values, append_unknown)
+
+ if build_rv is not None:
+ rv = (build_rv[0], build_rv[1], rule.websocket)
+ if self.map.host_matching:
+ if rv[0] == self.server_name:
+ return rv
+ elif first_match is None:
+ first_match = rv
+ else:
+ return rv
+
+ return first_match
+
+ def build(
+ self,
+ endpoint: t.Any,
+ values: t.Mapping[str, t.Any] | None = None,
+ method: str | None = None,
+ force_external: bool = False,
+ append_unknown: bool = True,
+ url_scheme: str | None = None,
+ ) -> str:
+ """Build a URL by filling in any variable slots in the endpoint's rule
+ with the provided values.
+
+ This produces relative URLs (path only, no host) by default, which is
+ appropriate for most use cases. When using subdomain or host matching,
+ the URL will be absolute if the endpoint is not on the same host as the
+ current request. If you need an absolute URL no matter what, such as
+ when adding a link to an email, pass ``force_external=True``.
+
+ Characters that are not allowed directly in URLs will be percent-encoded
+ using UTF-8.
+
+ Additional values that don't match any rule parts are added to the query
+ (``?key=value``) part of the URL.
+
+ If a rule is not found, an :exc:`.BuildError` is raised. Converters may
+ also raise exceptions when preparing a value.
+
+ Unlike matching, building does not perform comprehensive validation of
+ the values, it's assumed that they are trusted and correct. It's also
+ possible to build a URL for one endpoint that will be matched by
+ another, or will not match. This is very unlikely to happen or to matter
+ in most applications, especially with testing. If you need a
+ stronger guarantee, you can use a helper to verify that
+ ``match(build(endpoint)) == endpoint``.
+
+ :param endpoint: The endpoint for the rule to build.
+ :param values: Values for the variable parts of the rule, which will be
+ converted using the part's converter's
+ :meth:`~.BaseConverter.to_url` method. Keys that don't match
+ variable parts will be converted to a query string unless
+ ``append_unknown`` is disabled.
+ :param method: Further match the rule to build by this HTTP method, for
+ when there are multiple rules for the same endpoint that vary based
+ on method.
+ :param force_external: Always produce an absolute URL, for external use.
+ If ``scheme`` is not set, this produces a protocol-relative URL.
+ :param append_unknown: Convert values that don't match any rule parts
+ to a query string. Disable to ignore such values.
+ :param url_scheme: Scheme to use for absolute URLs. Defaults to
+ :attr:`url_scheme`.
+
+ .. versionchanged:: 2.0
+ Added the ``url_scheme`` parameter.
+
+ .. versionadded:: 0.6
+ Added the ``append_unknown`` parameter.
+ """
+ self.map.update()
+
+ if values:
+ if isinstance(values, MultiDict):
+ values = {
+ k: (v[0] if len(v) == 1 else v)
+ for k, v in dict.items(values)
+ if len(v) != 0
+ }
+ else: # plain dict
+ values = {k: v for k, v in values.items() if v is not None}
+ else:
+ values = {}
+
+ rv = self._partial_build(endpoint, values, method, append_unknown)
+ if rv is None:
+ raise BuildError(endpoint, values, method, self)
+
+ domain_part, path, websocket = rv
+ host = self.get_host(domain_part)
+
+ if url_scheme is None:
+ url_scheme = self.url_scheme
+
+ # Always build WebSocket routes with the scheme (browsers
+ # require full URLs). If bound to a WebSocket, ensure that HTTP
+ # routes are built with an HTTP scheme.
+ secure = url_scheme in {"https", "wss"}
+
+ if websocket:
+ force_external = True
+ url_scheme = "wss" if secure else "ws"
+ elif url_scheme:
+ url_scheme = "https" if secure else "http"
+
+ # shortcut this.
+ if not force_external and (
+ (self.map.host_matching and host == self.server_name)
+ or (not self.map.host_matching and domain_part == self.subdomain)
+ ):
+ return f"{self.script_name.rstrip('/')}/{path.lstrip('/')}"
+
+ scheme = f"{url_scheme}:" if url_scheme else ""
+ return f"{scheme}//{host}{self.script_name[:-1]}/{path.lstrip('/')}"
diff --git a/venv/Lib/site-packages/werkzeug/routing/matcher.py b/venv/Lib/site-packages/werkzeug/routing/matcher.py
new file mode 100644
index 0000000..1f03c46
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/routing/matcher.py
@@ -0,0 +1,202 @@
+from __future__ import annotations
+
+import re
+import typing as t
+from dataclasses import dataclass
+from dataclasses import field
+
+from .converters import ValidationError
+from .exceptions import NoMatch
+from .exceptions import RequestAliasRedirect
+from .exceptions import RequestPath
+from .rules import Rule
+from .rules import RulePart
+
+
+class SlashRequired(Exception):
+ pass
+
+
+@dataclass
+class State:
+ """A representation of a rule state.
+
+ This includes the *rules* that correspond to the state and the
+ possible *static* and *dynamic* transitions to the next state.
+ """
+
+ dynamic: list[tuple[RulePart, State]] = field(default_factory=list)
+ rules: list[Rule] = field(default_factory=list)
+ static: dict[str, State] = field(default_factory=dict)
+
+
+class StateMachineMatcher:
+ def __init__(self, merge_slashes: bool) -> None:
+ self._root = State()
+ self.merge_slashes = merge_slashes
+
+ def add(self, rule: Rule) -> None:
+ state = self._root
+ for part in rule._parts:
+ if part.static:
+ state.static.setdefault(part.content, State())
+ state = state.static[part.content]
+ else:
+ for test_part, new_state in state.dynamic:
+ if test_part == part:
+ state = new_state
+ break
+ else:
+ new_state = State()
+ state.dynamic.append((part, new_state))
+ state = new_state
+ state.rules.append(rule)
+
+ def update(self) -> None:
+ # For every state the dynamic transitions should be sorted by
+ # the weight of the transition
+ state = self._root
+
+ def _update_state(state: State) -> None:
+ state.dynamic.sort(key=lambda entry: entry[0].weight)
+ for new_state in state.static.values():
+ _update_state(new_state)
+ for _, new_state in state.dynamic:
+ _update_state(new_state)
+
+ _update_state(state)
+
+ def match(
+ self, domain: str, path: str, method: str, websocket: bool
+ ) -> tuple[Rule, t.MutableMapping[str, t.Any]]:
+ # To match to a rule we need to start at the root state and
+ # try to follow the transitions until we find a match, or find
+ # there is no transition to follow.
+
+ have_match_for = set()
+ websocket_mismatch = False
+
+ def _match(
+ state: State, parts: list[str], values: list[str]
+ ) -> tuple[Rule, list[str]] | None:
+ # This function is meant to be called recursively, and will attempt
+ # to match the head part to the state's transitions.
+ nonlocal have_match_for, websocket_mismatch
+
+ # The base case is when all parts have been matched via
+ # transitions. Hence if there is a rule with methods &
+ # websocket that work return it and the dynamic values
+ # extracted.
+ if parts == []:
+ for rule in state.rules:
+ if rule.methods is not None and method not in rule.methods:
+ have_match_for.update(rule.methods)
+ elif rule.websocket != websocket:
+ websocket_mismatch = True
+ else:
+ return rule, values
+
+ # Test if there is a match with this path with a
+ # trailing slash, if so raise an exception to report
+ # that matching is possible with an additional slash
+ if "" in state.static:
+ for rule in state.static[""].rules:
+ if websocket == rule.websocket and (
+ rule.methods is None or method in rule.methods
+ ):
+ if rule.strict_slashes:
+ raise SlashRequired()
+ else:
+ return rule, values
+ return None
+
+ part = parts[0]
+ # To match this part try the static transitions first
+ if part in state.static:
+ rv = _match(state.static[part], parts[1:], values)
+ if rv is not None:
+ return rv
+ # No match via the static transitions, so try the dynamic
+ # ones.
+ for test_part, new_state in state.dynamic:
+ target = part
+ remaining = parts[1:]
+ # A final part indicates a transition that always
+ # consumes the remaining parts i.e. transitions to a
+ # final state.
+ if test_part.final:
+ target = "/".join(parts)
+ remaining = []
+ match = re.compile(test_part.content).match(target)
+ if match is not None:
+ if test_part.suffixed:
+ # If a part_isolating=False part has a slash suffix, remove the
+ # suffix from the match and check for the slash redirect next.
+ suffix = match.groups()[-1]
+ if suffix == "/":
+ remaining = [""]
+
+ converter_groups = sorted(
+ match.groupdict().items(), key=lambda entry: entry[0]
+ )
+ groups = [
+ value
+ for key, value in converter_groups
+ if key[:11] == "__werkzeug_"
+ ]
+ rv = _match(new_state, remaining, values + groups)
+ if rv is not None:
+ return rv
+
+ # If there is no match and the only part left is a
+ # trailing slash ("") consider rules that aren't
+ # strict-slashes as these should match if there is a final
+ # slash part.
+ if parts == [""]:
+ for rule in state.rules:
+ if rule.strict_slashes:
+ continue
+ if rule.methods is not None and method not in rule.methods:
+ have_match_for.update(rule.methods)
+ elif rule.websocket != websocket:
+ websocket_mismatch = True
+ else:
+ return rule, values
+
+ return None
+
+ try:
+ rv = _match(self._root, [domain, *path.split("/")], [])
+ except SlashRequired:
+ raise RequestPath(f"{path}/") from None
+
+ if self.merge_slashes and rv is None:
+ # Try to match again, but with slashes merged
+ path = re.sub("/{2,}", "/", path)
+ try:
+ rv = _match(self._root, [domain, *path.split("/")], [])
+ except SlashRequired:
+ raise RequestPath(f"{path}/") from None
+ if rv is None or rv[0].merge_slashes is False:
+ raise NoMatch(have_match_for, websocket_mismatch)
+ else:
+ raise RequestPath(f"{path}")
+ elif rv is not None:
+ rule, values = rv
+
+ result = {}
+ for name, value in zip(rule._converters.keys(), values):
+ try:
+ value = rule._converters[name].to_python(value)
+ except ValidationError:
+ raise NoMatch(have_match_for, websocket_mismatch) from None
+ result[str(name)] = value
+ if rule.defaults:
+ result.update(rule.defaults)
+
+ if rule.alias and rule.map.redirect_defaults:
+ raise RequestAliasRedirect(result, rule.endpoint)
+
+ return rule, result
+
+ raise NoMatch(have_match_for, websocket_mismatch)
diff --git a/venv/Lib/site-packages/werkzeug/routing/rules.py b/venv/Lib/site-packages/werkzeug/routing/rules.py
new file mode 100644
index 0000000..7971660
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/routing/rules.py
@@ -0,0 +1,927 @@
+from __future__ import annotations
+
+import ast
+import re
+import typing as t
+from dataclasses import dataclass
+from string import Template
+from types import CodeType
+from urllib.parse import quote
+
+from ..datastructures import iter_multi_items
+from ..urls import _urlencode
+from .converters import ValidationError
+
+if t.TYPE_CHECKING:
+ from .converters import BaseConverter
+ from .map import Map
+
+
+class Weighting(t.NamedTuple):
+ number_static_weights: int
+ static_weights: list[tuple[int, int]]
+ number_argument_weights: int
+ argument_weights: list[int]
+
+
+@dataclass
+class RulePart:
+ """A part of a rule.
+
+ Rules can be represented by parts as delimited by `/` with
+ instances of this class representing those parts. The *content* is
+ either the raw content if *static* or a regex string to match
+ against. The *weight* can be used to order parts when matching.
+
+ """
+
+ content: str
+ final: bool
+ static: bool
+ suffixed: bool
+ weight: Weighting
+
+
+_part_re = re.compile(
+ r"""
+ (?:
+ (?P/) # a slash
+ |
+ (?P[^]+) # static rule data
+ |
+ (?:
+ <
+ (?:
+ (?P[a-zA-Z_][a-zA-Z0-9_]*) # converter name
+ (?:\((?P.*?)\))? # converter arguments
+ : # variable delimiter
+ )?
+ (?P[a-zA-Z_][a-zA-Z0-9_]*) # variable name
+ >
+ )
+ )
+ """,
+ re.VERBOSE,
+)
+
+_simple_rule_re = re.compile(r"<([^>]+)>")
+_converter_args_re = re.compile(
+ r"""
+ \s*
+ ((?P\w+)\s*=\s*)?
+ (?P
+ True|False|
+ \d+.\d+|
+ \d+.|
+ \d+|
+ [\w\d_.]+|
+ [urUR]?(?P"[^"]*?"|'[^']*')
+ )\s*,
+ """,
+ re.VERBOSE,
+)
+
+_PYTHON_CONSTANTS = {"None": None, "True": True, "False": False}
+
+
+def _find(value: str, target: str, pos: int) -> int:
+ """Find the *target* in *value* after *pos*.
+
+ Returns the *value* length if *target* isn't found.
+ """
+ try:
+ return value.index(target, pos)
+ except ValueError:
+ return len(value)
+
+
+def _pythonize(value: str) -> None | bool | int | float | str:
+ if value in _PYTHON_CONSTANTS:
+ return _PYTHON_CONSTANTS[value]
+ for convert in int, float:
+ try:
+ return convert(value)
+ except ValueError:
+ pass
+ if value[:1] == value[-1:] and value[0] in "\"'":
+ value = value[1:-1]
+ return str(value)
+
+
+def parse_converter_args(argstr: str) -> tuple[tuple[t.Any, ...], dict[str, t.Any]]:
+ argstr += ","
+ args = []
+ kwargs = {}
+ position = 0
+
+ for item in _converter_args_re.finditer(argstr):
+ if item.start() != position:
+ raise ValueError(
+ f"Cannot parse converter argument '{argstr[position : item.start()]}'"
+ )
+
+ value = item.group("stringval")
+ if value is None:
+ value = item.group("value")
+ value = _pythonize(value)
+ if not item.group("name"):
+ args.append(value)
+ else:
+ name = item.group("name")
+ kwargs[name] = value
+ position = item.end()
+
+ return tuple(args), kwargs
+
+
+class RuleFactory:
+ """As soon as you have more complex URL setups it's a good idea to use rule
+ factories to avoid repetitive tasks. Some of them are builtin, others can
+ be added by subclassing `RuleFactory` and overriding `get_rules`.
+ """
+
+ def get_rules(self, map: Map) -> t.Iterable[Rule]:
+ """Subclasses of `RuleFactory` have to override this method and return
+ an iterable of rules."""
+ raise NotImplementedError()
+
+
+class Subdomain(RuleFactory):
+ """All URLs provided by this factory have the subdomain set to a
+ specific domain. For example if you want to use the subdomain for
+ the current language this can be a good setup::
+
+ url_map = Map([
+ Rule('/', endpoint='#select_language'),
+ Subdomain('', [
+ Rule('/', endpoint='index'),
+ Rule('/about', endpoint='about'),
+ Rule('/help', endpoint='help')
+ ])
+ ])
+
+ All the rules except for the ``'#select_language'`` endpoint will now
+ listen on a two letter long subdomain that holds the language code
+ for the current request.
+ """
+
+ def __init__(self, subdomain: str, rules: t.Iterable[RuleFactory]) -> None:
+ self.subdomain = subdomain
+ self.rules = rules
+
+ def get_rules(self, map: Map) -> t.Iterator[Rule]:
+ for rulefactory in self.rules:
+ for rule in rulefactory.get_rules(map):
+ rule = rule.empty()
+ rule.subdomain = self.subdomain
+ yield rule
+
+
+class Submount(RuleFactory):
+ """Like `Subdomain` but prefixes the URL rule with a given string::
+
+ url_map = Map([
+ Rule('/', endpoint='index'),
+ Submount('/blog', [
+ Rule('/', endpoint='blog/index'),
+ Rule('/entry/', endpoint='blog/show')
+ ])
+ ])
+
+ Now the rule ``'blog/show'`` matches ``/blog/entry/``.
+ """
+
+ def __init__(self, path: str, rules: t.Iterable[RuleFactory]) -> None:
+ self.path = path.rstrip("/")
+ self.rules = rules
+
+ def get_rules(self, map: Map) -> t.Iterator[Rule]:
+ for rulefactory in self.rules:
+ for rule in rulefactory.get_rules(map):
+ rule = rule.empty()
+ rule.rule = self.path + rule.rule
+ yield rule
+
+
+class EndpointPrefix(RuleFactory):
+ """Prefixes all endpoints (which must be strings for this factory) with
+ another string. This can be useful for sub applications::
+
+ url_map = Map([
+ Rule('/', endpoint='index'),
+ EndpointPrefix('blog/', [Submount('/blog', [
+ Rule('/', endpoint='index'),
+ Rule('/entry/', endpoint='show')
+ ])])
+ ])
+ """
+
+ def __init__(self, prefix: str, rules: t.Iterable[RuleFactory]) -> None:
+ self.prefix = prefix
+ self.rules = rules
+
+ def get_rules(self, map: Map) -> t.Iterator[Rule]:
+ for rulefactory in self.rules:
+ for rule in rulefactory.get_rules(map):
+ rule = rule.empty()
+ rule.endpoint = self.prefix + rule.endpoint
+ yield rule
+
+
+class RuleTemplate:
+ """Returns copies of the rules wrapped and expands string templates in
+ the endpoint, rule, defaults or subdomain sections.
+
+ Here a small example for such a rule template::
+
+ from werkzeug.routing import Map, Rule, RuleTemplate
+
+ resource = RuleTemplate([
+ Rule('/$name/', endpoint='$name.list'),
+ Rule('/$name/', endpoint='$name.show')
+ ])
+
+ url_map = Map([resource(name='user'), resource(name='page')])
+
+ When a rule template is called the keyword arguments are used to
+ replace the placeholders in all the string parameters.
+ """
+
+ def __init__(self, rules: t.Iterable[Rule]) -> None:
+ self.rules = list(rules)
+
+ def __call__(self, *args: t.Any, **kwargs: t.Any) -> RuleTemplateFactory:
+ return RuleTemplateFactory(self.rules, dict(*args, **kwargs))
+
+
+class RuleTemplateFactory(RuleFactory):
+ """A factory that fills in template variables into rules. Used by
+ `RuleTemplate` internally.
+
+ :internal:
+ """
+
+ def __init__(
+ self, rules: t.Iterable[RuleFactory], context: dict[str, t.Any]
+ ) -> None:
+ self.rules = rules
+ self.context = context
+
+ def get_rules(self, map: Map) -> t.Iterator[Rule]:
+ for rulefactory in self.rules:
+ for rule in rulefactory.get_rules(map):
+ new_defaults = subdomain = None
+ if rule.defaults:
+ new_defaults = {}
+ for key, value in rule.defaults.items():
+ if isinstance(value, str):
+ value = Template(value).substitute(self.context)
+ new_defaults[key] = value
+ if rule.subdomain is not None:
+ subdomain = Template(rule.subdomain).substitute(self.context)
+ new_endpoint = rule.endpoint
+ if isinstance(new_endpoint, str):
+ new_endpoint = Template(new_endpoint).substitute(self.context)
+ yield Rule(
+ Template(rule.rule).substitute(self.context),
+ new_defaults,
+ subdomain,
+ rule.methods,
+ rule.build_only,
+ new_endpoint,
+ rule.strict_slashes,
+ )
+
+
+_ASTT = t.TypeVar("_ASTT", bound=ast.AST)
+
+
+def _prefix_names(src: str, expected_type: type[_ASTT]) -> _ASTT:
+ """ast parse and prefix names with `.` to avoid collision with user vars"""
+ tree: ast.AST = ast.parse(src).body[0]
+ if isinstance(tree, ast.Expr):
+ tree = tree.value
+ if not isinstance(tree, expected_type):
+ raise TypeError(
+ f"AST node is of type {type(tree).__name__}, not {expected_type.__name__}"
+ )
+ for node in ast.walk(tree):
+ if isinstance(node, ast.Name):
+ node.id = f".{node.id}"
+ return tree
+
+
+_CALL_CONVERTER_CODE_FMT = "self._converters[{elem!r}].to_url()"
+_IF_KWARGS_URL_ENCODE_CODE = """\
+if kwargs:
+ params = self._encode_query_vars(kwargs)
+ q = "?" if params else ""
+else:
+ q = params = ""
+"""
+_IF_KWARGS_URL_ENCODE_AST = _prefix_names(_IF_KWARGS_URL_ENCODE_CODE, ast.If)
+_URL_ENCODE_AST_NAMES = (
+ _prefix_names("q", ast.Name),
+ _prefix_names("params", ast.Name),
+)
+
+
+class Rule(RuleFactory):
+ """A Rule represents one URL pattern. There are some options for `Rule`
+ that change the way it behaves and are passed to the `Rule` constructor.
+ Note that besides the rule-string all arguments *must* be keyword arguments
+ in order to not break the application on Werkzeug upgrades.
+
+ `string`
+ Rule strings basically are just normal URL paths with placeholders in
+ the format ```` where the converter and the
+ arguments are optional. If no converter is defined the `default`
+ converter is used which means `string` in the normal configuration.
+
+ URL rules that end with a slash are branch URLs, others are leaves.
+ If you have `strict_slashes` enabled (which is the default), all
+ branch URLs that are matched without a trailing slash will trigger a
+ redirect to the same URL with the missing slash appended.
+
+ The converters are defined on the `Map`.
+
+ `endpoint`
+ The endpoint for this rule. This can be anything. A reference to a
+ function, a string, a number etc. The preferred way is using a string
+ because the endpoint is used for URL generation.
+
+ `defaults`
+ An optional dict with defaults for other rules with the same endpoint.
+ This is a bit tricky but useful if you want to have unique URLs::
+
+ url_map = Map([
+ Rule('/all/', defaults={'page': 1}, endpoint='all_entries'),
+ Rule('/all/page/', endpoint='all_entries')
+ ])
+
+ If a user now visits ``http://example.com/all/page/1`` they will be
+ redirected to ``http://example.com/all/``. If `redirect_defaults` is
+ disabled on the `Map` instance this will only affect the URL
+ generation.
+
+ `subdomain`
+ The subdomain rule string for this rule. If not specified the rule
+ only matches for the `default_subdomain` of the map. If the map is
+ not bound to a subdomain this feature is disabled.
+
+ Can be useful if you want to have user profiles on different subdomains
+ and all subdomains are forwarded to your application::
+
+ url_map = Map([
+ Rule('/', subdomain='', endpoint='user/homepage'),
+ Rule('/stats', subdomain='', endpoint='user/stats')
+ ])
+
+ `methods`
+ A sequence of http methods this rule applies to. If not specified, all
+ methods are allowed. For example this can be useful if you want different
+ endpoints for `POST` and `GET`. If methods are defined and the path
+ matches but the method matched against is not in this list or in the
+ list of another rule for that path the error raised is of the type
+ `MethodNotAllowed` rather than `NotFound`. If `GET` is present in the
+ list of methods and `HEAD` is not, `HEAD` is added automatically.
+
+ `strict_slashes`
+ Override the `Map` setting for `strict_slashes` only for this rule. If
+ not specified the `Map` setting is used.
+
+ `merge_slashes`
+ Override :attr:`Map.merge_slashes` for this rule.
+
+ `build_only`
+ Set this to True and the rule will never match but will create a URL
+ that can be build. This is useful if you have resources on a subdomain
+ or folder that are not handled by the WSGI application (like static data)
+
+ `redirect_to`
+ If given this must be either a string or callable. In case of a
+ callable it's called with the url adapter that triggered the match and
+ the values of the URL as keyword arguments and has to return the target
+ for the redirect, otherwise it has to be a string with placeholders in
+ rule syntax::
+
+ def foo_with_slug(adapter, id):
+ # ask the database for the slug for the old id. this of
+ # course has nothing to do with werkzeug.
+ return f'foo/{Foo.get_slug_for_id(id)}'
+
+ url_map = Map([
+ Rule('/foo/', endpoint='foo'),
+ Rule('/some/old/url/', redirect_to='foo/'),
+ Rule('/other/old/url/', redirect_to=foo_with_slug)
+ ])
+
+ When the rule is matched the routing system will raise a
+ `RequestRedirect` exception with the target for the redirect.
+
+ Keep in mind that the URL will be joined against the URL root of the
+ script so don't use a leading slash on the target URL unless you
+ really mean root of that domain.
+
+ `alias`
+ If enabled this rule serves as an alias for another rule with the same
+ endpoint and arguments.
+
+ `host`
+ If provided and the URL map has host matching enabled this can be
+ used to provide a match rule for the whole host. This also means
+ that the subdomain feature is disabled.
+
+ `websocket`
+ If ``True``, this rule is only matches for WebSocket (``ws://``,
+ ``wss://``) requests. By default, rules will only match for HTTP
+ requests.
+
+ .. versionchanged:: 2.1
+ Percent-encoded newlines (``%0a``), which are decoded by WSGI
+ servers, are considered when routing instead of terminating the
+ match early.
+
+ .. versionadded:: 1.0
+ Added ``websocket``.
+
+ .. versionadded:: 1.0
+ Added ``merge_slashes``.
+
+ .. versionadded:: 0.7
+ Added ``alias`` and ``host``.
+
+ .. versionchanged:: 0.6.1
+ ``HEAD`` is added to ``methods`` if ``GET`` is present.
+ """
+
+ def __init__(
+ self,
+ string: str,
+ defaults: t.Mapping[str, t.Any] | None = None,
+ subdomain: str | None = None,
+ methods: t.Iterable[str] | None = None,
+ build_only: bool = False,
+ endpoint: t.Any | None = None,
+ strict_slashes: bool | None = None,
+ merge_slashes: bool | None = None,
+ redirect_to: str | t.Callable[..., str] | None = None,
+ alias: bool = False,
+ host: str | None = None,
+ websocket: bool = False,
+ ) -> None:
+ if not string.startswith("/"):
+ raise ValueError(f"URL rule '{string}' must start with a slash.")
+
+ self.rule = string
+ self.is_leaf = not string.endswith("/")
+ self.is_branch = string.endswith("/")
+
+ self.map: Map = None # type: ignore
+ self.strict_slashes = strict_slashes
+ self.merge_slashes = merge_slashes
+ self.subdomain = subdomain
+ self.host = host
+ self.defaults = defaults
+ self.build_only = build_only
+ self.alias = alias
+ self.websocket = websocket
+
+ if methods is not None:
+ if isinstance(methods, str):
+ raise TypeError("'methods' should be a list of strings.")
+
+ methods = {x.upper() for x in methods}
+
+ if "HEAD" not in methods and "GET" in methods:
+ methods.add("HEAD")
+
+ if websocket and methods - {"GET", "HEAD", "OPTIONS"}:
+ raise ValueError(
+ "WebSocket rules can only use 'GET', 'HEAD', and 'OPTIONS' methods."
+ )
+
+ self.methods = methods
+ self.endpoint: t.Any = endpoint
+ self.redirect_to = redirect_to
+
+ if defaults:
+ self.arguments = set(map(str, defaults))
+ else:
+ self.arguments = set()
+
+ self._converters: dict[str, BaseConverter] = {}
+ self._trace: list[tuple[bool, str]] = []
+ self._parts: list[RulePart] = []
+
+ def empty(self) -> Rule:
+ """
+ Return an unbound copy of this rule.
+
+ This can be useful if want to reuse an already bound URL for another
+ map. See ``get_empty_kwargs`` to override what keyword arguments are
+ provided to the new copy.
+ """
+ return type(self)(self.rule, **self.get_empty_kwargs())
+
+ def get_empty_kwargs(self) -> t.Mapping[str, t.Any]:
+ """
+ Provides kwargs for instantiating empty copy with empty()
+
+ Use this method to provide custom keyword arguments to the subclass of
+ ``Rule`` when calling ``some_rule.empty()``. Helpful when the subclass
+ has custom keyword arguments that are needed at instantiation.
+
+ Must return a ``dict`` that will be provided as kwargs to the new
+ instance of ``Rule``, following the initial ``self.rule`` value which
+ is always provided as the first, required positional argument.
+ """
+ defaults = None
+ if self.defaults:
+ defaults = dict(self.defaults)
+ return dict(
+ defaults=defaults,
+ subdomain=self.subdomain,
+ methods=self.methods,
+ build_only=self.build_only,
+ endpoint=self.endpoint,
+ strict_slashes=self.strict_slashes,
+ redirect_to=self.redirect_to,
+ alias=self.alias,
+ host=self.host,
+ )
+
+ def get_rules(self, map: Map) -> t.Iterator[Rule]:
+ yield self
+
+ def refresh(self) -> None:
+ """Rebinds and refreshes the URL. Call this if you modified the
+ rule in place.
+
+ :internal:
+ """
+ self.bind(self.map, rebind=True)
+
+ def bind(self, map: Map, rebind: bool = False) -> None:
+ """Bind the url to a map and create a regular expression based on
+ the information from the rule itself and the defaults from the map.
+
+ :internal:
+ """
+ if self.map is not None and not rebind:
+ raise RuntimeError(f"url rule {self!r} already bound to map {self.map!r}")
+ self.map = map
+ if self.strict_slashes is None:
+ self.strict_slashes = map.strict_slashes
+ if self.merge_slashes is None:
+ self.merge_slashes = map.merge_slashes
+ if self.subdomain is None:
+ self.subdomain = map.default_subdomain
+ self.compile()
+
+ def get_converter(
+ self,
+ variable_name: str,
+ converter_name: str,
+ args: tuple[t.Any, ...],
+ kwargs: t.Mapping[str, t.Any],
+ ) -> BaseConverter:
+ """Looks up the converter for the given parameter.
+
+ .. versionadded:: 0.9
+ """
+ if converter_name not in self.map.converters:
+ raise LookupError(f"the converter {converter_name!r} does not exist")
+ return self.map.converters[converter_name](self.map, *args, **kwargs)
+
+ def _encode_query_vars(self, query_vars: t.Mapping[str, t.Any]) -> str:
+ items: t.Iterable[tuple[str, str]] = iter_multi_items(query_vars)
+
+ if self.map.sort_parameters:
+ items = sorted(items, key=self.map.sort_key)
+
+ return _urlencode(items)
+
+ def _parse_rule(self, rule: str) -> t.Iterable[RulePart]:
+ content = ""
+ static = True
+ argument_weights = []
+ static_weights: list[tuple[int, int]] = []
+ final = False
+ convertor_number = 0
+
+ pos = 0
+ while pos < len(rule):
+ match = _part_re.match(rule, pos)
+ if match is None:
+ raise ValueError(f"malformed url rule: {rule!r}")
+
+ data = match.groupdict()
+ if data["static"] is not None:
+ static_weights.append((len(static_weights), -len(data["static"])))
+ self._trace.append((False, data["static"]))
+ content += data["static"] if static else re.escape(data["static"])
+
+ if data["variable"] is not None:
+ if static:
+ # Switching content to represent regex, hence the need to escape
+ content = re.escape(content)
+ static = False
+ c_args, c_kwargs = parse_converter_args(data["arguments"] or "")
+ convobj = self.get_converter(
+ data["variable"], data["converter"] or "default", c_args, c_kwargs
+ )
+ self._converters[data["variable"]] = convobj
+ self.arguments.add(data["variable"])
+ if not convobj.part_isolating:
+ final = True
+ content += f"(?P<__werkzeug_{convertor_number}>{convobj.regex})"
+ convertor_number += 1
+ argument_weights.append(convobj.weight)
+ self._trace.append((True, data["variable"]))
+
+ if data["slash"] is not None:
+ self._trace.append((False, "/"))
+ if final:
+ content += "/"
+ else:
+ if not static:
+ content += r"\Z"
+ weight = Weighting(
+ -len(static_weights),
+ static_weights,
+ -len(argument_weights),
+ argument_weights,
+ )
+ yield RulePart(
+ content=content,
+ final=final,
+ static=static,
+ suffixed=False,
+ weight=weight,
+ )
+ content = ""
+ static = True
+ argument_weights = []
+ static_weights = []
+ final = False
+ convertor_number = 0
+
+ pos = match.end()
+
+ suffixed = False
+ if final and content[-1] == "/":
+ # If a converter is part_isolating=False (matches slashes) and ends with a
+ # slash, augment the regex to support slash redirects.
+ suffixed = True
+ content = content[:-1] + "(? None:
+ """Compiles the regular expression and stores it."""
+ assert self.map is not None, "rule not bound"
+
+ if self.map.host_matching:
+ domain_rule = self.host or ""
+ else:
+ domain_rule = self.subdomain or ""
+ self._parts = []
+ self._trace = []
+ self._converters = {}
+ if domain_rule == "":
+ self._parts = [
+ RulePart(
+ content="",
+ final=False,
+ static=True,
+ suffixed=False,
+ weight=Weighting(0, [], 0, []),
+ )
+ ]
+ else:
+ self._parts.extend(self._parse_rule(domain_rule))
+ self._trace.append((False, "|"))
+ rule = self.rule
+ if self.merge_slashes:
+ rule = re.sub("/{2,}", "/", self.rule)
+ self._parts.extend(self._parse_rule(rule))
+
+ self._build: t.Callable[..., tuple[str, str]]
+ self._build = self._compile_builder(False).__get__(self, None)
+ self._build_unknown: t.Callable[..., tuple[str, str]]
+ self._build_unknown = self._compile_builder(True).__get__(self, None)
+
+ @staticmethod
+ def _get_func_code(code: CodeType, name: str) -> t.Callable[..., tuple[str, str]]:
+ globs: dict[str, t.Any] = {}
+ locs: dict[str, t.Any] = {}
+ exec(code, globs, locs)
+ return locs[name] # type: ignore
+
+ def _compile_builder(
+ self, append_unknown: bool = True
+ ) -> t.Callable[..., tuple[str, str]]:
+ defaults = self.defaults or {}
+ dom_ops: list[tuple[bool, str]] = []
+ url_ops: list[tuple[bool, str]] = []
+
+ opl = dom_ops
+ for is_dynamic, data in self._trace:
+ if data == "|" and opl is dom_ops:
+ opl = url_ops
+ continue
+ # this seems like a silly case to ever come up but:
+ # if a default is given for a value that appears in the rule,
+ # resolve it to a constant ahead of time
+ if is_dynamic and data in defaults:
+ data = self._converters[data].to_url(defaults[data])
+ opl.append((False, data))
+ elif not is_dynamic:
+ # safe = https://url.spec.whatwg.org/#url-path-segment-string
+ opl.append((False, quote(data, safe="!$&'()*+,/:;=@")))
+ else:
+ opl.append((True, data))
+
+ def _convert(elem: str) -> ast.Call:
+ ret = _prefix_names(_CALL_CONVERTER_CODE_FMT.format(elem=elem), ast.Call)
+ ret.args = [ast.Name(elem, ast.Load())]
+ return ret
+
+ def _parts(ops: list[tuple[bool, str]]) -> list[ast.expr]:
+ parts: list[ast.expr] = [
+ _convert(elem) if is_dynamic else ast.Constant(elem)
+ for is_dynamic, elem in ops
+ ]
+ parts = parts or [ast.Constant("")]
+ # constant fold
+ ret = [parts[0]]
+ for p in parts[1:]:
+ if isinstance(p, ast.Constant) and isinstance(ret[-1], ast.Constant):
+ ret[-1] = ast.Constant(ret[-1].value + p.value) # type: ignore[operator]
+ else:
+ ret.append(p)
+ return ret
+
+ dom_parts = _parts(dom_ops)
+ url_parts = _parts(url_ops)
+ body: list[ast.stmt]
+ if not append_unknown:
+ body = []
+ else:
+ body = [_IF_KWARGS_URL_ENCODE_AST]
+ url_parts.extend(_URL_ENCODE_AST_NAMES)
+
+ def _join(parts: list[ast.expr]) -> ast.expr:
+ if len(parts) == 1: # shortcut
+ return parts[0]
+ return ast.JoinedStr(parts)
+
+ body.append(
+ ast.Return(ast.Tuple([_join(dom_parts), _join(url_parts)], ast.Load()))
+ )
+
+ pargs = [
+ elem
+ for is_dynamic, elem in dom_ops + url_ops
+ if is_dynamic and elem not in defaults
+ ]
+ kargs = [str(k) for k in defaults]
+
+ func_ast = _prefix_names("def _(): pass", ast.FunctionDef)
+ func_ast.name = f""
+ func_ast.args.args.append(ast.arg(".self", None))
+ for arg in pargs + kargs:
+ func_ast.args.args.append(ast.arg(arg, None))
+ func_ast.args.kwarg = ast.arg(".kwargs", None)
+ for _ in kargs:
+ func_ast.args.defaults.append(ast.Constant(""))
+ func_ast.body = body
+
+ # Use `ast.parse` instead of `ast.Module` for better portability, since the
+ # signature of `ast.Module` can change.
+ module = ast.parse("")
+ module.body = [func_ast]
+
+ # mark everything as on line 1, offset 0
+ # less error-prone than `ast.fix_missing_locations`
+ # bad line numbers cause an assert to fail in debug builds
+ for node in ast.walk(module):
+ if "lineno" in node._attributes:
+ node.lineno = 1 # type: ignore[attr-defined]
+ if "end_lineno" in node._attributes:
+ node.end_lineno = node.lineno # type: ignore[attr-defined]
+ if "col_offset" in node._attributes:
+ node.col_offset = 0 # type: ignore[attr-defined]
+ if "end_col_offset" in node._attributes:
+ node.end_col_offset = node.col_offset # type: ignore[attr-defined]
+
+ code = compile(module, "", "exec")
+ return self._get_func_code(code, func_ast.name)
+
+ def build(
+ self, values: t.Mapping[str, t.Any], append_unknown: bool = True
+ ) -> tuple[str, str] | None:
+ """Assembles the relative url for that rule and the subdomain.
+ If building doesn't work for some reasons `None` is returned.
+
+ :internal:
+ """
+ try:
+ if append_unknown:
+ return self._build_unknown(**values)
+ else:
+ return self._build(**values)
+ except ValidationError:
+ return None
+
+ def provides_defaults_for(self, rule: Rule) -> bool:
+ """Check if this rule has defaults for a given rule.
+
+ :internal:
+ """
+ return bool(
+ not self.build_only
+ and self.defaults
+ and self.endpoint == rule.endpoint
+ and self != rule
+ and self.arguments == rule.arguments
+ )
+
+ def suitable_for(
+ self, values: t.Mapping[str, t.Any], method: str | None = None
+ ) -> bool:
+ """Check if the dict of values has enough data for url generation.
+
+ :internal:
+ """
+ # if a method was given explicitly and that method is not supported
+ # by this rule, this rule is not suitable.
+ if (
+ method is not None
+ and self.methods is not None
+ and method not in self.methods
+ ):
+ return False
+
+ defaults = self.defaults or ()
+
+ # all arguments required must be either in the defaults dict or
+ # the value dictionary otherwise it's not suitable
+ for key in self.arguments:
+ if key not in defaults and key not in values:
+ return False
+
+ # in case defaults are given we ensure that either the value was
+ # skipped or the value is the same as the default value.
+ if defaults:
+ for key, value in defaults.items():
+ if key in values and value != values[key]:
+ return False
+
+ return True
+
+ def build_compare_key(self) -> tuple[int, int, int]:
+ """The build compare key for sorting.
+
+ :internal:
+ """
+ return (1 if self.alias else 0, -len(self.arguments), -len(self.defaults or ()))
+
+ def __eq__(self, other: object) -> bool:
+ return isinstance(other, type(self)) and self._trace == other._trace
+
+ __hash__ = None # type: ignore
+
+ def __str__(self) -> str:
+ return self.rule
+
+ def __repr__(self) -> str:
+ if self.map is None:
+ return f"<{type(self).__name__} (unbound)>"
+ parts = []
+ for is_dynamic, data in self._trace:
+ if is_dynamic:
+ parts.append(f"<{data}>")
+ else:
+ parts.append(data)
+ parts_str = "".join(parts).lstrip("|")
+ methods = f" ({', '.join(self.methods)})" if self.methods is not None else ""
+ return f"<{type(self).__name__} {parts_str!r}{methods} -> {self.endpoint}>"
diff --git a/venv/Lib/site-packages/werkzeug/sansio/__init__.py b/venv/Lib/site-packages/werkzeug/sansio/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/werkzeug/sansio/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/sansio/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..292dc85
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/sansio/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/sansio/__pycache__/http.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/sansio/__pycache__/http.cpython-310.pyc
new file mode 100644
index 0000000..7101296
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/sansio/__pycache__/http.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/sansio/__pycache__/multipart.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/sansio/__pycache__/multipart.cpython-310.pyc
new file mode 100644
index 0000000..cbc77a0
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/sansio/__pycache__/multipart.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/sansio/__pycache__/request.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/sansio/__pycache__/request.cpython-310.pyc
new file mode 100644
index 0000000..449717a
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/sansio/__pycache__/request.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/sansio/__pycache__/response.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/sansio/__pycache__/response.cpython-310.pyc
new file mode 100644
index 0000000..684960b
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/sansio/__pycache__/response.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/sansio/__pycache__/utils.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/sansio/__pycache__/utils.cpython-310.pyc
new file mode 100644
index 0000000..b33702a
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/sansio/__pycache__/utils.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/sansio/http.py b/venv/Lib/site-packages/werkzeug/sansio/http.py
new file mode 100644
index 0000000..c68a57f
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/sansio/http.py
@@ -0,0 +1,170 @@
+from __future__ import annotations
+
+import re
+import typing as t
+from datetime import datetime
+
+from .._internal import _dt_as_utc
+from ..http import generate_etag
+from ..http import parse_date
+from ..http import parse_etags
+from ..http import parse_if_range_header
+from ..http import unquote_etag
+
+_etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)')
+
+
+def is_resource_modified(
+ http_range: str | None = None,
+ http_if_range: str | None = None,
+ http_if_modified_since: str | None = None,
+ http_if_none_match: str | None = None,
+ http_if_match: str | None = None,
+ etag: str | None = None,
+ data: bytes | None = None,
+ last_modified: datetime | str | None = None,
+ ignore_if_range: bool = True,
+) -> bool:
+ """Convenience method for conditional requests.
+ :param http_range: Range HTTP header
+ :param http_if_range: If-Range HTTP header
+ :param http_if_modified_since: If-Modified-Since HTTP header
+ :param http_if_none_match: If-None-Match HTTP header
+ :param http_if_match: If-Match HTTP header
+ :param etag: the etag for the response for comparison.
+ :param data: or alternatively the data of the response to automatically
+ generate an etag using :func:`generate_etag`.
+ :param last_modified: an optional date of the last modification.
+ :param ignore_if_range: If `False`, `If-Range` header will be taken into
+ account.
+ :return: `True` if the resource was modified, otherwise `False`.
+
+ .. versionadded:: 2.2
+ """
+ if etag is None and data is not None:
+ etag = generate_etag(data)
+ elif data is not None:
+ raise TypeError("both data and etag given")
+
+ unmodified = False
+ if isinstance(last_modified, str):
+ last_modified = parse_date(last_modified)
+
+ # HTTP doesn't use microsecond, remove it to avoid false positive
+ # comparisons. Mark naive datetimes as UTC.
+ if last_modified is not None:
+ last_modified = _dt_as_utc(last_modified.replace(microsecond=0))
+
+ if_range = None
+ if not ignore_if_range and http_range is not None:
+ # https://tools.ietf.org/html/rfc7233#section-3.2
+ # A server MUST ignore an If-Range header field received in a request
+ # that does not contain a Range header field.
+ if_range = parse_if_range_header(http_if_range)
+
+ if if_range is not None and if_range.date is not None:
+ modified_since: datetime | None = if_range.date
+ else:
+ modified_since = parse_date(http_if_modified_since)
+
+ if modified_since and last_modified and last_modified <= modified_since:
+ unmodified = True
+
+ if etag:
+ etag, _ = unquote_etag(etag)
+
+ if if_range is not None and if_range.etag is not None:
+ unmodified = parse_etags(if_range.etag).contains(etag)
+ else:
+ if_none_match = parse_etags(http_if_none_match)
+ if if_none_match:
+ # https://tools.ietf.org/html/rfc7232#section-3.2
+ # "A recipient MUST use the weak comparison function when comparing
+ # entity-tags for If-None-Match"
+ unmodified = if_none_match.contains_weak(etag)
+
+ # https://tools.ietf.org/html/rfc7232#section-3.1
+ # "Origin server MUST use the strong comparison function when
+ # comparing entity-tags for If-Match"
+ if_match = parse_etags(http_if_match)
+ if if_match:
+ unmodified = not if_match.is_strong(etag)
+
+ return not unmodified
+
+
+_cookie_re = re.compile(
+ r"""
+ ([^=;]*)
+ (?:\s*=\s*
+ (
+ "(?:[^\\"]|\\.)*"
+ |
+ .*?
+ )
+ )?
+ \s*;\s*
+ """,
+ flags=re.ASCII | re.VERBOSE,
+)
+_cookie_unslash_re = re.compile(rb"\\([0-3][0-7]{2}|.)")
+
+
+def _cookie_unslash_replace(m: t.Match[bytes]) -> bytes:
+ v = m.group(1)
+
+ if len(v) == 1:
+ return v
+
+ return int(v, 8).to_bytes(1, "big")
+
+
+def parse_cookie(
+ cookie: str | None = None,
+ cls: type[ds.MultiDict[str, str]] | None = None,
+) -> ds.MultiDict[str, str]:
+ """Parse a cookie from a string.
+
+ The same key can be provided multiple times, the values are stored
+ in-order. The default :class:`MultiDict` will have the first value
+ first, and all values can be retrieved with
+ :meth:`MultiDict.getlist`.
+
+ :param cookie: The cookie header as a string.
+ :param cls: A dict-like class to store the parsed cookies in.
+ Defaults to :class:`MultiDict`.
+
+ .. versionchanged:: 3.0
+ Passing bytes, and the ``charset`` and ``errors`` parameters, were removed.
+
+ .. versionadded:: 2.2
+ """
+ if cls is None:
+ cls = t.cast("type[ds.MultiDict[str, str]]", ds.MultiDict)
+
+ if not cookie:
+ return cls()
+
+ cookie = f"{cookie};"
+ out = []
+
+ for ck, cv in _cookie_re.findall(cookie):
+ ck = ck.strip()
+ cv = cv.strip()
+
+ if not ck:
+ continue
+
+ if len(cv) >= 2 and cv[0] == cv[-1] == '"':
+ # Work with bytes here, since a UTF-8 character could be multiple bytes.
+ cv = _cookie_unslash_re.sub(
+ _cookie_unslash_replace, cv[1:-1].encode()
+ ).decode(errors="replace")
+
+ out.append((ck, cv))
+
+ return cls(out)
+
+
+# circular dependencies
+from .. import datastructures as ds # noqa: E402
diff --git a/venv/Lib/site-packages/werkzeug/sansio/multipart.py b/venv/Lib/site-packages/werkzeug/sansio/multipart.py
new file mode 100644
index 0000000..9582aba
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/sansio/multipart.py
@@ -0,0 +1,331 @@
+from __future__ import annotations
+
+import re
+import typing as t
+from dataclasses import dataclass
+from enum import auto
+from enum import Enum
+
+from ..datastructures import Headers
+from ..exceptions import RequestEntityTooLarge
+from ..http import parse_options_header
+
+
+class Event:
+ pass
+
+
+@dataclass(frozen=True)
+class Preamble(Event):
+ data: bytes
+
+
+@dataclass(frozen=True)
+class Field(Event):
+ name: str
+ headers: Headers
+
+
+@dataclass(frozen=True)
+class File(Event):
+ name: str
+ filename: str
+ headers: Headers
+
+
+@dataclass(frozen=True)
+class Data(Event):
+ data: bytes
+ more_data: bool
+
+
+@dataclass(frozen=True)
+class Epilogue(Event):
+ data: bytes
+
+
+class NeedData(Event):
+ pass
+
+
+NEED_DATA = NeedData()
+
+
+class State(Enum):
+ PREAMBLE = auto()
+ PART = auto()
+ DATA = auto()
+ DATA_START = auto()
+ EPILOGUE = auto()
+ COMPLETE = auto()
+
+
+# Multipart line breaks MUST be CRLF (\r\n) by RFC-7578, except that
+# many implementations break this and either use CR or LF alone.
+LINE_BREAK = b"(?:\r\n|\n|\r)"
+BLANK_LINE_RE = re.compile(b"(?:\r\n\r\n|\r\r|\n\n)", re.MULTILINE)
+LINE_BREAK_RE = re.compile(LINE_BREAK, re.MULTILINE)
+# Header values can be continued via a space or tab after the linebreak, as
+# per RFC2231
+HEADER_CONTINUATION_RE = re.compile(b"%s[ \t]" % LINE_BREAK, re.MULTILINE)
+# This must be long enough to contain any line breaks plus any
+# additional boundary markers (--) such that they will be found in a
+# subsequent search
+SEARCH_EXTRA_LENGTH = 8
+
+
+class MultipartDecoder:
+ """Decodes a multipart message as bytes into Python events.
+
+ The part data is returned as available to allow the caller to save
+ the data from memory to disk, if desired.
+
+ .. versionchanged:: 3.1.4
+ Handle chunks that split a``\r\n`` sequence.
+ """
+
+ def __init__(
+ self,
+ boundary: bytes,
+ max_form_memory_size: int | None = None,
+ *,
+ max_parts: int | None = None,
+ ) -> None:
+ self.buffer = bytearray()
+ self.complete = False
+ self.max_form_memory_size = max_form_memory_size
+ self.max_parts = max_parts
+ self.state = State.PREAMBLE
+ self.boundary = boundary
+
+ # Note in the below \h i.e. horizontal whitespace is used
+ # as [^\S\n\r] as \h isn't supported in python.
+
+ # The preamble must end with a boundary where the boundary is
+ # prefixed by a line break, RFC2046. Except that many
+ # implementations including Werkzeug's tests omit the line
+ # break prefix. In addition the first boundary could be the
+ # epilogue boundary (for empty form-data) hence the matching
+ # group to understand if it is an epilogue boundary.
+ self.preamble_re = re.compile(
+ rb"%s?--%s(--[^\S\n\r]*%s?|[^\S\n\r]*%s)"
+ % (LINE_BREAK, re.escape(boundary), LINE_BREAK, LINE_BREAK),
+ re.MULTILINE,
+ )
+ # A boundary must include a line break prefix and suffix, and
+ # may include trailing whitespace. In addition the boundary
+ # could be the epilogue boundary hence the matching group to
+ # understand if it is an epilogue boundary.
+ self.boundary_re = re.compile(
+ rb"%s--%s(--[^\S\n\r]*%s?|[^\S\n\r]*%s)"
+ % (LINE_BREAK, re.escape(boundary), LINE_BREAK, LINE_BREAK),
+ re.MULTILINE,
+ )
+ self._search_position = 0
+ self._parts_decoded = 0
+
+ def receive_data(self, data: bytes | None) -> None:
+ if data is None:
+ self.complete = True
+ elif (
+ self.max_form_memory_size is not None
+ and len(self.buffer) + len(data) > self.max_form_memory_size
+ ):
+ # Ensure that data within single event does not exceed limit.
+ # Also checked across accumulated events in MultiPartParser.
+ raise RequestEntityTooLarge()
+ else:
+ self.buffer.extend(data)
+
+ def next_event(self) -> Event:
+ event: Event = NEED_DATA
+ if self.state == State.PREAMBLE:
+ match = self.preamble_re.search(self.buffer, self._search_position)
+ if match is not None:
+ if match.group(1).startswith(b"--"):
+ self.state = State.EPILOGUE
+ else:
+ self.state = State.PART
+ data = bytes(self.buffer[: match.start()])
+ del self.buffer[: match.end()]
+ event = Preamble(data=data)
+ self._search_position = 0
+ else:
+ # Update the search start position to be equal to the
+ # current buffer length (already searched) minus a
+ # safe buffer for part of the search target.
+ self._search_position = max(
+ 0, len(self.buffer) - len(self.boundary) - SEARCH_EXTRA_LENGTH
+ )
+
+ elif self.state == State.PART:
+ match = BLANK_LINE_RE.search(self.buffer, self._search_position)
+ if match is not None:
+ headers = self._parse_headers(self.buffer[: match.start()])
+ # The final header ends with a single CRLF, however a
+ # blank line indicates the start of the
+ # body. Therefore the end is after the first CRLF.
+ headers_end = (match.start() + match.end()) // 2
+ del self.buffer[:headers_end]
+
+ if "content-disposition" not in headers:
+ raise ValueError("Missing Content-Disposition header")
+
+ disposition, extra = parse_options_header(
+ headers["content-disposition"]
+ )
+ name = t.cast(str, extra.get("name"))
+ filename = extra.get("filename")
+ if filename is not None:
+ event = File(
+ filename=filename,
+ headers=headers,
+ name=name,
+ )
+ else:
+ event = Field(
+ headers=headers,
+ name=name,
+ )
+ self.state = State.DATA_START
+ self._search_position = 0
+ self._parts_decoded += 1
+
+ if self.max_parts is not None and self._parts_decoded > self.max_parts:
+ raise RequestEntityTooLarge()
+ else:
+ # Update the search start position to be equal to the
+ # current buffer length (already searched) minus a
+ # safe buffer for part of the search target.
+ self._search_position = max(0, len(self.buffer) - SEARCH_EXTRA_LENGTH)
+
+ elif self.state == State.DATA_START:
+ data, del_index, more_data = self._parse_data(self.buffer, start=True)
+ del self.buffer[:del_index]
+ event = Data(data=data, more_data=more_data)
+ if more_data:
+ self.state = State.DATA
+
+ elif self.state == State.DATA:
+ data, del_index, more_data = self._parse_data(self.buffer, start=False)
+ del self.buffer[:del_index]
+ if data or not more_data:
+ event = Data(data=data, more_data=more_data)
+
+ elif self.state == State.EPILOGUE and self.complete:
+ event = Epilogue(data=bytes(self.buffer))
+ del self.buffer[:]
+ self.state = State.COMPLETE
+
+ if self.complete and isinstance(event, NeedData):
+ raise ValueError(f"Invalid form-data cannot parse beyond {self.state}")
+
+ return event
+
+ def _parse_headers(self, data: bytes | bytearray) -> Headers:
+ headers: list[tuple[str, str]] = []
+ # Merge the continued headers into one line
+ data = HEADER_CONTINUATION_RE.sub(b" ", data)
+ # Now there is one header per line
+ for line in data.splitlines():
+ line = line.strip()
+
+ if line != b"":
+ name, _, value = line.decode().partition(":")
+ headers.append((name.strip(), value.strip()))
+ return Headers(headers)
+
+ def _parse_data(
+ self, data: bytes | bytearray, *, start: bool
+ ) -> tuple[bytes, int, bool]:
+ # Body parts must start with CRLF (or CR or LF)
+ if start:
+ match = LINE_BREAK_RE.match(data)
+ data_start = t.cast(t.Match[bytes], match).end()
+ else:
+ data_start = 0
+
+ if self.buffer.find(b"--" + self.boundary) == -1:
+ # No complete boundary in the buffer, but there may be
+ # a partial boundary at the end.
+ data_end = del_index = (
+ self._last_partial_boundary_index(data[data_start:]) + data_start
+ )
+ more_data = True
+ else:
+ match = self.boundary_re.search(data)
+ if match is not None:
+ if match.group(1).startswith(b"--"):
+ self.state = State.EPILOGUE
+ else:
+ self.state = State.PART
+ data_end = match.start()
+ del_index = match.end()
+ else:
+ data_end = del_index = (
+ self._last_partial_boundary_index(data[data_start:]) + data_start
+ )
+ more_data = match is None
+ return bytes(data[data_start:data_end]), del_index, more_data
+
+ def _last_partial_boundary_index(self, data: bytes | bytearray) -> int:
+ # Find the last index following which a partial boundary
+ # could be present in the data. This will be the earliest
+ # position of a LF or a CR, unless that position is more
+ # than a complete boundary from the end in which case there
+ # is no partial boundary.
+ complete_boundary_index = len(data) - len(b"\r\n--" + self.boundary)
+ try:
+ last_nl = data.rindex(b"\n")
+ except ValueError:
+ last_nl = len(data)
+ else:
+ if last_nl < complete_boundary_index:
+ last_nl = len(data)
+ try:
+ last_cr = data.rindex(b"\r")
+ except ValueError:
+ last_cr = len(data)
+ else:
+ if last_cr < complete_boundary_index:
+ last_cr = len(data)
+ return min(last_nl, last_cr)
+
+
+class MultipartEncoder:
+ def __init__(self, boundary: bytes) -> None:
+ self.boundary = boundary
+ self.state = State.PREAMBLE
+
+ def send_event(self, event: Event) -> bytes:
+ if isinstance(event, Preamble) and self.state == State.PREAMBLE:
+ self.state = State.PART
+ return event.data
+ elif isinstance(event, (Field, File)) and self.state in {
+ State.PREAMBLE,
+ State.PART,
+ State.DATA,
+ }:
+ data = b"\r\n--" + self.boundary + b"\r\n"
+ data += b'Content-Disposition: form-data; name="%s"' % event.name.encode()
+ if isinstance(event, File):
+ data += b'; filename="%s"' % event.filename.encode()
+ data += b"\r\n"
+ for name, value in t.cast(Field, event).headers:
+ if name.lower() != "content-disposition":
+ data += f"{name}: {value}\r\n".encode()
+ self.state = State.DATA_START
+ return data
+ elif isinstance(event, Data) and self.state == State.DATA_START:
+ self.state = State.DATA
+ if len(event.data) > 0:
+ return b"\r\n" + event.data
+ else:
+ return event.data
+ elif isinstance(event, Data) and self.state == State.DATA:
+ return event.data
+ elif isinstance(event, Epilogue):
+ self.state = State.COMPLETE
+ return b"\r\n--" + self.boundary + b"--\r\n" + event.data
+ else:
+ raise ValueError(f"Cannot generate {event} in state: {self.state}")
diff --git a/venv/Lib/site-packages/werkzeug/sansio/request.py b/venv/Lib/site-packages/werkzeug/sansio/request.py
new file mode 100644
index 0000000..0d72f32
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/sansio/request.py
@@ -0,0 +1,536 @@
+from __future__ import annotations
+
+import typing as t
+from datetime import datetime
+from urllib.parse import parse_qsl
+
+from ..datastructures import Accept
+from ..datastructures import Authorization
+from ..datastructures import CharsetAccept
+from ..datastructures import ETags
+from ..datastructures import Headers
+from ..datastructures import HeaderSet
+from ..datastructures import IfRange
+from ..datastructures import ImmutableList
+from ..datastructures import ImmutableMultiDict
+from ..datastructures import LanguageAccept
+from ..datastructures import MIMEAccept
+from ..datastructures import MultiDict
+from ..datastructures import Range
+from ..datastructures import RequestCacheControl
+from ..http import parse_accept_header
+from ..http import parse_cache_control_header
+from ..http import parse_date
+from ..http import parse_etags
+from ..http import parse_if_range_header
+from ..http import parse_list_header
+from ..http import parse_options_header
+from ..http import parse_range_header
+from ..http import parse_set_header
+from ..user_agent import UserAgent
+from ..utils import cached_property
+from ..utils import header_property
+from .http import parse_cookie
+from .utils import get_content_length
+from .utils import get_current_url
+from .utils import get_host
+
+
+class Request:
+ """Represents the non-IO parts of a HTTP request, including the
+ method, URL info, and headers.
+
+ This class is not meant for general use. It should only be used when
+ implementing WSGI, ASGI, or another HTTP application spec. Werkzeug
+ provides a WSGI implementation at :cls:`werkzeug.wrappers.Request`.
+
+ :param method: The method the request was made with, such as
+ ``GET``.
+ :param scheme: The URL scheme of the protocol the request used, such
+ as ``https`` or ``wss``.
+ :param server: The address of the server. ``(host, port)``,
+ ``(path, None)`` for unix sockets, or ``None`` if not known.
+ :param root_path: The prefix that the application is mounted under.
+ This is prepended to generated URLs, but is not part of route
+ matching.
+ :param path: The path part of the URL after ``root_path``.
+ :param query_string: The part of the URL after the "?".
+ :param headers: The headers received with the request.
+ :param remote_addr: The address of the client sending the request.
+
+ .. versionchanged:: 3.0
+ The ``charset``, ``url_charset``, and ``encoding_errors`` attributes
+ were removed.
+
+ .. versionadded:: 2.0
+ """
+
+ #: the class to use for `args` and `form`. The default is an
+ #: :class:`~werkzeug.datastructures.ImmutableMultiDict` which supports
+ #: multiple values per key. A :class:`~werkzeug.datastructures.ImmutableDict`
+ #: is faster but only remembers the last key. It is also
+ #: possible to use mutable structures, but this is not recommended.
+ #:
+ #: .. versionadded:: 0.6
+ parameter_storage_class: type[MultiDict[str, t.Any]] = ImmutableMultiDict
+
+ #: The type to be used for dict values from the incoming WSGI
+ #: environment. (For example for :attr:`cookies`.) By default an
+ #: :class:`~werkzeug.datastructures.ImmutableMultiDict` is used.
+ #:
+ #: .. versionchanged:: 1.0.0
+ #: Changed to ``ImmutableMultiDict`` to support multiple values.
+ #:
+ #: .. versionadded:: 0.6
+ dict_storage_class: type[MultiDict[str, t.Any]] = ImmutableMultiDict
+
+ #: the type to be used for list values from the incoming WSGI environment.
+ #: By default an :class:`~werkzeug.datastructures.ImmutableList` is used
+ #: (for example for :attr:`access_list`).
+ #:
+ #: .. versionadded:: 0.6
+ list_storage_class: type[list[t.Any]] = ImmutableList
+
+ user_agent_class: type[UserAgent] = UserAgent
+ """The class used and returned by the :attr:`user_agent` property to
+ parse the header. Defaults to
+ :class:`~werkzeug.user_agent.UserAgent`, which does no parsing. An
+ extension can provide a subclass that uses a parser to provide other
+ data.
+
+ .. versionadded:: 2.0
+ """
+
+ #: Valid host names when handling requests. By default all hosts are
+ #: trusted, which means that whatever the client says the host is
+ #: will be accepted.
+ #:
+ #: Because ``Host`` and ``X-Forwarded-Host`` headers can be set to
+ #: any value by a malicious client, it is recommended to either set
+ #: this property or implement similar validation in the proxy (if
+ #: the application is being run behind one).
+ #:
+ #: .. versionadded:: 0.9
+ trusted_hosts: list[str] | None = None
+
+ def __init__(
+ self,
+ method: str,
+ scheme: str,
+ server: tuple[str, int | None] | None,
+ root_path: str,
+ path: str,
+ query_string: bytes,
+ headers: Headers,
+ remote_addr: str | None,
+ ) -> None:
+ #: The method the request was made with, such as ``GET``.
+ self.method = method.upper()
+ #: The URL scheme of the protocol the request used, such as
+ #: ``https`` or ``wss``.
+ self.scheme = scheme
+ #: The address of the server. ``(host, port)``, ``(path, None)``
+ #: for unix sockets, or ``None`` if not known.
+ self.server = server
+ #: The prefix that the application is mounted under, without a
+ #: trailing slash. :attr:`path` comes after this.
+ self.root_path = root_path.rstrip("/")
+ #: The path part of the URL after :attr:`root_path`. This is the
+ #: path used for routing within the application.
+ self.path = "/" + path.lstrip("/")
+ #: The part of the URL after the "?". This is the raw value, use
+ #: :attr:`args` for the parsed values.
+ self.query_string = query_string
+ #: The headers received with the request.
+ self.headers = headers
+ #: The address of the client sending the request.
+ self.remote_addr = remote_addr
+
+ def __repr__(self) -> str:
+ try:
+ url = self.url
+ except Exception as e:
+ url = f"(invalid URL: {e})"
+
+ return f"<{type(self).__name__} {url!r} [{self.method}]>"
+
+ @cached_property
+ def args(self) -> MultiDict[str, str]:
+ """The parsed URL parameters (the part in the URL after the question
+ mark).
+
+ By default an
+ :class:`~werkzeug.datastructures.ImmutableMultiDict`
+ is returned from this function. This can be changed by setting
+ :attr:`parameter_storage_class` to a different type. This might
+ be necessary if the order of the form data is important.
+
+ .. versionchanged:: 2.3
+ Invalid bytes remain percent encoded.
+ """
+ return self.parameter_storage_class(
+ parse_qsl(
+ self.query_string.decode(),
+ keep_blank_values=True,
+ errors="werkzeug.url_quote",
+ )
+ )
+
+ @cached_property
+ def access_route(self) -> list[str]:
+ """If a forwarded header exists this is a list of all ip addresses
+ from the client ip to the last proxy server.
+ """
+ if "X-Forwarded-For" in self.headers:
+ return self.list_storage_class(
+ parse_list_header(self.headers["X-Forwarded-For"])
+ )
+ elif self.remote_addr is not None:
+ return self.list_storage_class([self.remote_addr])
+ return self.list_storage_class()
+
+ @cached_property
+ def full_path(self) -> str:
+ """Requested path, including the query string."""
+ return f"{self.path}?{self.query_string.decode()}"
+
+ @property
+ def is_secure(self) -> bool:
+ """``True`` if the request was made with a secure protocol
+ (HTTPS or WSS).
+ """
+ return self.scheme in {"https", "wss"}
+
+ @cached_property
+ def url(self) -> str:
+ """The full request URL with the scheme, host, root path, path,
+ and query string."""
+ return get_current_url(
+ self.scheme, self.host, self.root_path, self.path, self.query_string
+ )
+
+ @cached_property
+ def base_url(self) -> str:
+ """Like :attr:`url` but without the query string."""
+ return get_current_url(self.scheme, self.host, self.root_path, self.path)
+
+ @cached_property
+ def root_url(self) -> str:
+ """The request URL scheme, host, and root path. This is the root
+ that the application is accessed from.
+ """
+ return get_current_url(self.scheme, self.host, self.root_path)
+
+ @cached_property
+ def host_url(self) -> str:
+ """The request URL scheme and host only."""
+ return get_current_url(self.scheme, self.host)
+
+ @cached_property
+ def host(self) -> str:
+ """The host name the request was made to, including the port if
+ it's non-standard. Validated with :attr:`trusted_hosts`.
+
+ See :func:`.get_host` for a detailed explanation.
+ """
+ return get_host(
+ self.scheme, self.headers.get("host"), self.server, self.trusted_hosts
+ )
+
+ @cached_property
+ def cookies(self) -> ImmutableMultiDict[str, str]:
+ """A :class:`dict` with the contents of all cookies transmitted with
+ the request."""
+ wsgi_combined_cookie = ";".join(self.headers.getlist("Cookie"))
+ return parse_cookie( # type: ignore
+ wsgi_combined_cookie, cls=self.dict_storage_class
+ )
+
+ # Common Descriptors
+
+ content_type = header_property[str](
+ "Content-Type",
+ doc="""The Content-Type entity-header field indicates the media
+ type of the entity-body sent to the recipient or, in the case of
+ the HEAD method, the media type that would have been sent had
+ the request been a GET.""",
+ read_only=True,
+ )
+
+ @cached_property
+ def content_length(self) -> int | None:
+ """The Content-Length entity-header field indicates the size of the
+ entity-body in bytes or, in the case of the HEAD method, the size of
+ the entity-body that would have been sent had the request been a
+ GET.
+ """
+ return get_content_length(
+ http_content_length=self.headers.get("Content-Length"),
+ http_transfer_encoding=self.headers.get("Transfer-Encoding"),
+ )
+
+ content_encoding = header_property[str](
+ "Content-Encoding",
+ doc="""The Content-Encoding entity-header field is used as a
+ modifier to the media-type. When present, its value indicates
+ what additional content codings have been applied to the
+ entity-body, and thus what decoding mechanisms must be applied
+ in order to obtain the media-type referenced by the Content-Type
+ header field.
+
+ .. versionadded:: 0.9""",
+ read_only=True,
+ )
+ content_md5 = header_property[str](
+ "Content-MD5",
+ doc="""The Content-MD5 entity-header field, as defined in
+ RFC 1864, is an MD5 digest of the entity-body for the purpose of
+ providing an end-to-end message integrity check (MIC) of the
+ entity-body. (Note: a MIC is good for detecting accidental
+ modification of the entity-body in transit, but is not proof
+ against malicious attacks.)
+
+ .. versionadded:: 0.9""",
+ read_only=True,
+ )
+ referrer = header_property[str](
+ "Referer",
+ doc="""The Referer[sic] request-header field allows the client
+ to specify, for the server's benefit, the address (URI) of the
+ resource from which the Request-URI was obtained (the
+ "referrer", although the header field is misspelled).""",
+ read_only=True,
+ )
+ date = header_property(
+ "Date",
+ None,
+ parse_date,
+ doc="""The Date general-header field represents the date and
+ time at which the message was originated, having the same
+ semantics as orig-date in RFC 822.
+
+ .. versionchanged:: 2.0
+ The datetime object is timezone-aware.
+ """,
+ read_only=True,
+ )
+ max_forwards = header_property(
+ "Max-Forwards",
+ None,
+ int,
+ doc="""The Max-Forwards request-header field provides a
+ mechanism with the TRACE and OPTIONS methods to limit the number
+ of proxies or gateways that can forward the request to the next
+ inbound server.""",
+ read_only=True,
+ )
+
+ def _parse_content_type(self) -> None:
+ if not hasattr(self, "_parsed_content_type"):
+ self._parsed_content_type = parse_options_header(
+ self.headers.get("Content-Type", "")
+ )
+
+ @property
+ def mimetype(self) -> str:
+ """Like :attr:`content_type`, but without parameters (eg, without
+ charset, type etc.) and always lowercase. For example if the content
+ type is ``text/HTML; charset=utf-8`` the mimetype would be
+ ``'text/html'``.
+ """
+ self._parse_content_type()
+ return self._parsed_content_type[0].lower()
+
+ @property
+ def mimetype_params(self) -> dict[str, str]:
+ """The mimetype parameters as dict. For example if the content
+ type is ``text/html; charset=utf-8`` the params would be
+ ``{'charset': 'utf-8'}``.
+ """
+ self._parse_content_type()
+ return self._parsed_content_type[1]
+
+ @cached_property
+ def pragma(self) -> HeaderSet:
+ """The Pragma general-header field is used to include
+ implementation-specific directives that might apply to any recipient
+ along the request/response chain. All pragma directives specify
+ optional behavior from the viewpoint of the protocol; however, some
+ systems MAY require that behavior be consistent with the directives.
+ """
+ return parse_set_header(self.headers.get("Pragma", ""))
+
+ # Accept
+
+ @cached_property
+ def accept_mimetypes(self) -> MIMEAccept:
+ """List of mimetypes this client supports as
+ :class:`~werkzeug.datastructures.MIMEAccept` object.
+ """
+ return parse_accept_header(self.headers.get("Accept"), MIMEAccept)
+
+ @cached_property
+ def accept_charsets(self) -> CharsetAccept:
+ """List of charsets this client supports as
+ :class:`~werkzeug.datastructures.CharsetAccept` object.
+ """
+ return parse_accept_header(self.headers.get("Accept-Charset"), CharsetAccept)
+
+ @cached_property
+ def accept_encodings(self) -> Accept:
+ """List of encodings this client accepts. Encodings in a HTTP term
+ are compression encodings such as gzip. For charsets have a look at
+ :attr:`accept_charset`.
+ """
+ return parse_accept_header(self.headers.get("Accept-Encoding"))
+
+ @cached_property
+ def accept_languages(self) -> LanguageAccept:
+ """List of languages this client accepts as
+ :class:`~werkzeug.datastructures.LanguageAccept` object.
+
+ .. versionchanged 0.5
+ In previous versions this was a regular
+ :class:`~werkzeug.datastructures.Accept` object.
+ """
+ return parse_accept_header(self.headers.get("Accept-Language"), LanguageAccept)
+
+ # ETag
+
+ @cached_property
+ def cache_control(self) -> RequestCacheControl:
+ """A :class:`~werkzeug.datastructures.RequestCacheControl` object
+ for the incoming cache control headers.
+ """
+ cache_control = self.headers.get("Cache-Control")
+ return parse_cache_control_header(cache_control, None, RequestCacheControl)
+
+ @cached_property
+ def if_match(self) -> ETags:
+ """An object containing all the etags in the `If-Match` header.
+
+ :rtype: :class:`~werkzeug.datastructures.ETags`
+ """
+ return parse_etags(self.headers.get("If-Match"))
+
+ @cached_property
+ def if_none_match(self) -> ETags:
+ """An object containing all the etags in the `If-None-Match` header.
+
+ :rtype: :class:`~werkzeug.datastructures.ETags`
+ """
+ return parse_etags(self.headers.get("If-None-Match"))
+
+ @cached_property
+ def if_modified_since(self) -> datetime | None:
+ """The parsed `If-Modified-Since` header as a datetime object.
+
+ .. versionchanged:: 2.0
+ The datetime object is timezone-aware.
+ """
+ return parse_date(self.headers.get("If-Modified-Since"))
+
+ @cached_property
+ def if_unmodified_since(self) -> datetime | None:
+ """The parsed `If-Unmodified-Since` header as a datetime object.
+
+ .. versionchanged:: 2.0
+ The datetime object is timezone-aware.
+ """
+ return parse_date(self.headers.get("If-Unmodified-Since"))
+
+ @cached_property
+ def if_range(self) -> IfRange:
+ """The parsed ``If-Range`` header.
+
+ .. versionchanged:: 2.0
+ ``IfRange.date`` is timezone-aware.
+
+ .. versionadded:: 0.7
+ """
+ return parse_if_range_header(self.headers.get("If-Range"))
+
+ @cached_property
+ def range(self) -> Range | None:
+ """The parsed `Range` header.
+
+ .. versionadded:: 0.7
+
+ :rtype: :class:`~werkzeug.datastructures.Range`
+ """
+ return parse_range_header(self.headers.get("Range"))
+
+ # User Agent
+
+ @cached_property
+ def user_agent(self) -> UserAgent:
+ """The user agent. Use ``user_agent.string`` to get the header
+ value. Set :attr:`user_agent_class` to a subclass of
+ :class:`~werkzeug.user_agent.UserAgent` to provide parsing for
+ the other properties or other extended data.
+
+ .. versionchanged:: 2.1
+ The built-in parser was removed. Set ``user_agent_class`` to a ``UserAgent``
+ subclass to parse data from the string.
+ """
+ return self.user_agent_class(self.headers.get("User-Agent", ""))
+
+ # Authorization
+
+ @cached_property
+ def authorization(self) -> Authorization | None:
+ """The ``Authorization`` header parsed into an :class:`.Authorization` object.
+ ``None`` if the header is not present.
+
+ .. versionchanged:: 2.3
+ :class:`Authorization` is no longer a ``dict``. The ``token`` attribute
+ was added for auth schemes that use a token instead of parameters.
+ """
+ return Authorization.from_header(self.headers.get("Authorization"))
+
+ # CORS
+
+ origin = header_property[str](
+ "Origin",
+ doc=(
+ "The host that the request originated from. Set"
+ " :attr:`~CORSResponseMixin.access_control_allow_origin` on"
+ " the response to indicate which origins are allowed."
+ ),
+ read_only=True,
+ )
+
+ access_control_request_headers = header_property(
+ "Access-Control-Request-Headers",
+ load_func=parse_set_header,
+ doc=(
+ "Sent with a preflight request to indicate which headers"
+ " will be sent with the cross origin request. Set"
+ " :attr:`~CORSResponseMixin.access_control_allow_headers`"
+ " on the response to indicate which headers are allowed."
+ ),
+ read_only=True,
+ )
+
+ access_control_request_method = header_property[str](
+ "Access-Control-Request-Method",
+ doc=(
+ "Sent with a preflight request to indicate which method"
+ " will be used for the cross origin request. Set"
+ " :attr:`~CORSResponseMixin.access_control_allow_methods`"
+ " on the response to indicate which methods are allowed."
+ ),
+ read_only=True,
+ )
+
+ @property
+ def is_json(self) -> bool:
+ """Check if the mimetype indicates JSON data, either
+ :mimetype:`application/json` or :mimetype:`application/*+json`.
+ """
+ mt = self.mimetype
+ return (
+ mt == "application/json"
+ or mt.startswith("application/")
+ and mt.endswith("+json")
+ )
diff --git a/venv/Lib/site-packages/werkzeug/sansio/response.py b/venv/Lib/site-packages/werkzeug/sansio/response.py
new file mode 100644
index 0000000..a52f963
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/sansio/response.py
@@ -0,0 +1,763 @@
+from __future__ import annotations
+
+import typing as t
+from datetime import datetime
+from datetime import timedelta
+from datetime import timezone
+from http import HTTPStatus
+
+from ..datastructures import CallbackDict
+from ..datastructures import ContentRange
+from ..datastructures import ContentSecurityPolicy
+from ..datastructures import Headers
+from ..datastructures import HeaderSet
+from ..datastructures import ResponseCacheControl
+from ..datastructures import WWWAuthenticate
+from ..http import COEP
+from ..http import COOP
+from ..http import dump_age
+from ..http import dump_cookie
+from ..http import dump_header
+from ..http import dump_options_header
+from ..http import http_date
+from ..http import HTTP_STATUS_CODES
+from ..http import parse_age
+from ..http import parse_cache_control_header
+from ..http import parse_content_range_header
+from ..http import parse_csp_header
+from ..http import parse_date
+from ..http import parse_options_header
+from ..http import parse_set_header
+from ..http import quote_etag
+from ..http import unquote_etag
+from ..utils import get_content_type
+from ..utils import header_property
+
+if t.TYPE_CHECKING:
+ from ..datastructures.cache_control import _CacheControl
+
+
+def _set_property(name: str, doc: str | None = None) -> property:
+ def fget(self: Response) -> HeaderSet:
+ def on_update(header_set: HeaderSet) -> None:
+ if not header_set and name in self.headers:
+ del self.headers[name]
+ elif header_set:
+ self.headers[name] = header_set.to_header()
+
+ return parse_set_header(self.headers.get(name), on_update)
+
+ def fset(
+ self: Response,
+ value: None | (str | dict[str, str | int] | t.Iterable[str]),
+ ) -> None:
+ if not value:
+ del self.headers[name]
+ elif isinstance(value, str):
+ self.headers[name] = value
+ else:
+ self.headers[name] = dump_header(value)
+
+ return property(fget, fset, doc=doc)
+
+
+class Response:
+ """Represents the non-IO parts of an HTTP response, specifically the
+ status and headers but not the body.
+
+ This class is not meant for general use. It should only be used when
+ implementing WSGI, ASGI, or another HTTP application spec. Werkzeug
+ provides a WSGI implementation at :cls:`werkzeug.wrappers.Response`.
+
+ :param status: The status code for the response. Either an int, in
+ which case the default status message is added, or a string in
+ the form ``{code} {message}``, like ``404 Not Found``. Defaults
+ to 200.
+ :param headers: A :class:`~werkzeug.datastructures.Headers` object,
+ or a list of ``(key, value)`` tuples that will be converted to a
+ ``Headers`` object.
+ :param mimetype: The mime type (content type without charset or
+ other parameters) of the response. If the value starts with
+ ``text/`` (or matches some other special cases), the charset
+ will be added to create the ``content_type``.
+ :param content_type: The full content type of the response.
+ Overrides building the value from ``mimetype``.
+
+ .. versionchanged:: 3.0
+ The ``charset`` attribute was removed.
+
+ .. versionadded:: 2.0
+ """
+
+ #: the default status if none is provided.
+ default_status = 200
+
+ #: the default mimetype if none is provided.
+ default_mimetype: str | None = "text/plain"
+
+ #: Warn if a cookie header exceeds this size. The default, 4093, should be
+ #: safely `supported by most browsers `_. A cookie larger than
+ #: this size will still be sent, but it may be ignored or handled
+ #: incorrectly by some browsers. Set to 0 to disable this check.
+ #:
+ #: .. versionadded:: 0.13
+ #:
+ #: .. _`cookie`: http://browsercookielimits.squawky.net/
+ max_cookie_size = 4093
+
+ # A :class:`Headers` object representing the response headers.
+ headers: Headers
+
+ def __init__(
+ self,
+ status: int | str | HTTPStatus | None = None,
+ headers: t.Mapping[str, str | t.Iterable[str]]
+ | t.Iterable[tuple[str, str]]
+ | None = None,
+ mimetype: str | None = None,
+ content_type: str | None = None,
+ ) -> None:
+ if isinstance(headers, Headers):
+ self.headers = headers
+ elif not headers:
+ self.headers = Headers()
+ else:
+ self.headers = Headers(headers)
+
+ if content_type is None:
+ if mimetype is None and "content-type" not in self.headers:
+ mimetype = self.default_mimetype
+ if mimetype is not None:
+ mimetype = get_content_type(mimetype, "utf-8")
+ content_type = mimetype
+ if content_type is not None:
+ self.headers["Content-Type"] = content_type
+ if status is None:
+ status = self.default_status
+ self.status = status
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} [{self.status}]>"
+
+ @property
+ def status_code(self) -> int:
+ """The HTTP status code as a number."""
+ return self._status_code
+
+ @status_code.setter
+ def status_code(self, code: int) -> None:
+ self.status = code
+
+ @property
+ def status(self) -> str:
+ """The HTTP status code as a string."""
+ return self._status
+
+ @status.setter
+ def status(self, value: str | int | HTTPStatus) -> None:
+ self._status, self._status_code = self._clean_status(value)
+
+ def _clean_status(self, value: str | int | HTTPStatus) -> tuple[str, int]:
+ if isinstance(value, (int, HTTPStatus)):
+ status_code = int(value)
+ else:
+ value = value.strip()
+
+ if not value:
+ raise ValueError("Empty status argument")
+
+ code_str, sep, _ = value.partition(" ")
+
+ try:
+ status_code = int(code_str)
+ except ValueError:
+ # only message
+ return f"0 {value}", 0
+
+ if sep:
+ # code and message
+ return value, status_code
+
+ # only code, look up message
+ try:
+ status = f"{status_code} {HTTP_STATUS_CODES[status_code].upper()}"
+ except KeyError:
+ status = f"{status_code} UNKNOWN"
+
+ return status, status_code
+
+ def set_cookie(
+ self,
+ key: str,
+ value: str = "",
+ max_age: timedelta | int | None = None,
+ expires: str | datetime | int | float | None = None,
+ path: str | None = "/",
+ domain: str | None = None,
+ secure: bool = False,
+ httponly: bool = False,
+ samesite: str | None = None,
+ partitioned: bool = False,
+ ) -> None:
+ """Sets a cookie.
+
+ A warning is raised if the size of the cookie header exceeds
+ :attr:`max_cookie_size`, but the header will still be set.
+
+ :param key: the key (name) of the cookie to be set.
+ :param value: the value of the cookie.
+ :param max_age: should be a number of seconds, or `None` (default) if
+ the cookie should last only as long as the client's
+ browser session.
+ :param expires: should be a `datetime` object or UNIX timestamp.
+ :param path: limits the cookie to a given path, per default it will
+ span the whole domain.
+ :param domain: if you want to set a cross-domain cookie. For example,
+ ``domain="example.com"`` will set a cookie that is
+ readable by the domain ``www.example.com``,
+ ``foo.example.com`` etc. Otherwise, a cookie will only
+ be readable by the domain that set it.
+ :param secure: If ``True``, the cookie will only be available
+ via HTTPS.
+ :param httponly: Disallow JavaScript access to the cookie.
+ :param samesite: Limit the scope of the cookie to only be
+ attached to requests that are "same-site".
+ :param partitioned: If ``True``, the cookie will be partitioned.
+
+ .. versionchanged:: 3.1
+ The ``partitioned`` parameter was added.
+ """
+ self.headers.add(
+ "Set-Cookie",
+ dump_cookie(
+ key,
+ value=value,
+ max_age=max_age,
+ expires=expires,
+ path=path,
+ domain=domain,
+ secure=secure,
+ httponly=httponly,
+ max_size=self.max_cookie_size,
+ samesite=samesite,
+ partitioned=partitioned,
+ ),
+ )
+
+ def delete_cookie(
+ self,
+ key: str,
+ path: str | None = "/",
+ domain: str | None = None,
+ secure: bool = False,
+ httponly: bool = False,
+ samesite: str | None = None,
+ partitioned: bool = False,
+ ) -> None:
+ """Delete a cookie. Fails silently if key doesn't exist.
+
+ :param key: the key (name) of the cookie to be deleted.
+ :param path: if the cookie that should be deleted was limited to a
+ path, the path has to be defined here.
+ :param domain: if the cookie that should be deleted was limited to a
+ domain, that domain has to be defined here.
+ :param secure: If ``True``, the cookie will only be available
+ via HTTPS.
+ :param httponly: Disallow JavaScript access to the cookie.
+ :param samesite: Limit the scope of the cookie to only be
+ attached to requests that are "same-site".
+ :param partitioned: If ``True``, the cookie will be partitioned.
+ """
+ self.set_cookie(
+ key,
+ expires=0,
+ max_age=0,
+ path=path,
+ domain=domain,
+ secure=secure,
+ httponly=httponly,
+ samesite=samesite,
+ partitioned=partitioned,
+ )
+
+ @property
+ def is_json(self) -> bool:
+ """Check if the mimetype indicates JSON data, either
+ :mimetype:`application/json` or :mimetype:`application/*+json`.
+ """
+ mt = self.mimetype
+ return mt is not None and (
+ mt == "application/json"
+ or mt.startswith("application/")
+ and mt.endswith("+json")
+ )
+
+ # Common Descriptors
+
+ @property
+ def mimetype(self) -> str | None:
+ """The mimetype (content type without charset etc.)"""
+ ct = self.headers.get("content-type")
+
+ if ct:
+ return ct.split(";")[0].strip()
+ else:
+ return None
+
+ @mimetype.setter
+ def mimetype(self, value: str) -> None:
+ self.headers["Content-Type"] = get_content_type(value, "utf-8")
+
+ @property
+ def mimetype_params(self) -> dict[str, str]:
+ """The mimetype parameters as dict. For example if the
+ content type is ``text/html; charset=utf-8`` the params would be
+ ``{'charset': 'utf-8'}``.
+
+ .. versionadded:: 0.5
+ """
+
+ def on_update(d: CallbackDict[str, str]) -> None:
+ self.headers["Content-Type"] = dump_options_header(self.mimetype, d)
+
+ d = parse_options_header(self.headers.get("content-type", ""))[1]
+ return CallbackDict(d, on_update)
+
+ location = header_property[str](
+ "Location",
+ doc="""The Location response-header field is used to redirect
+ the recipient to a location other than the Request-URI for
+ completion of the request or identification of a new
+ resource.""",
+ )
+ age = header_property(
+ "Age",
+ None,
+ parse_age,
+ dump_age, # type: ignore
+ doc="""The Age response-header field conveys the sender's
+ estimate of the amount of time since the response (or its
+ revalidation) was generated at the origin server.
+
+ Age values are non-negative decimal integers, representing time
+ in seconds.""",
+ )
+ content_type = header_property[str](
+ "Content-Type",
+ doc="""The Content-Type entity-header field indicates the media
+ type of the entity-body sent to the recipient or, in the case of
+ the HEAD method, the media type that would have been sent had
+ the request been a GET.""",
+ )
+ content_length = header_property(
+ "Content-Length",
+ None,
+ int,
+ str,
+ doc="""The Content-Length entity-header field indicates the size
+ of the entity-body, in decimal number of OCTETs, sent to the
+ recipient or, in the case of the HEAD method, the size of the
+ entity-body that would have been sent had the request been a
+ GET.""",
+ )
+ content_location = header_property[str](
+ "Content-Location",
+ doc="""The Content-Location entity-header field MAY be used to
+ supply the resource location for the entity enclosed in the
+ message when that entity is accessible from a location separate
+ from the requested resource's URI.""",
+ )
+ content_encoding = header_property[str](
+ "Content-Encoding",
+ doc="""The Content-Encoding entity-header field is used as a
+ modifier to the media-type. When present, its value indicates
+ what additional content codings have been applied to the
+ entity-body, and thus what decoding mechanisms must be applied
+ in order to obtain the media-type referenced by the Content-Type
+ header field.""",
+ )
+ content_md5 = header_property[str](
+ "Content-MD5",
+ doc="""The Content-MD5 entity-header field, as defined in
+ RFC 1864, is an MD5 digest of the entity-body for the purpose of
+ providing an end-to-end message integrity check (MIC) of the
+ entity-body. (Note: a MIC is good for detecting accidental
+ modification of the entity-body in transit, but is not proof
+ against malicious attacks.)""",
+ )
+ date = header_property(
+ "Date",
+ None,
+ parse_date,
+ http_date,
+ doc="""The Date general-header field represents the date and
+ time at which the message was originated, having the same
+ semantics as orig-date in RFC 822.
+
+ .. versionchanged:: 2.0
+ The datetime object is timezone-aware.
+ """,
+ )
+ expires = header_property(
+ "Expires",
+ None,
+ parse_date,
+ http_date,
+ doc="""The Expires entity-header field gives the date/time after
+ which the response is considered stale. A stale cache entry may
+ not normally be returned by a cache.
+
+ .. versionchanged:: 2.0
+ The datetime object is timezone-aware.
+ """,
+ )
+ last_modified = header_property(
+ "Last-Modified",
+ None,
+ parse_date,
+ http_date,
+ doc="""The Last-Modified entity-header field indicates the date
+ and time at which the origin server believes the variant was
+ last modified.
+
+ .. versionchanged:: 2.0
+ The datetime object is timezone-aware.
+ """,
+ )
+
+ @property
+ def retry_after(self) -> datetime | None:
+ """The Retry-After response-header field can be used with a
+ 503 (Service Unavailable) response to indicate how long the
+ service is expected to be unavailable to the requesting client.
+
+ Time in seconds until expiration or date.
+
+ .. versionchanged:: 2.0
+ The datetime object is timezone-aware.
+ """
+ value = self.headers.get("retry-after")
+ if value is None:
+ return None
+
+ try:
+ seconds = int(value)
+ except ValueError:
+ return parse_date(value)
+
+ return datetime.now(timezone.utc) + timedelta(seconds=seconds)
+
+ @retry_after.setter
+ def retry_after(self, value: datetime | int | str | None) -> None:
+ if value is None:
+ if "retry-after" in self.headers:
+ del self.headers["retry-after"]
+ return
+ elif isinstance(value, datetime):
+ value = http_date(value)
+ else:
+ value = str(value)
+ self.headers["Retry-After"] = value
+
+ vary = _set_property(
+ "Vary",
+ doc="""The Vary field value indicates the set of request-header
+ fields that fully determines, while the response is fresh,
+ whether a cache is permitted to use the response to reply to a
+ subsequent request without revalidation.""",
+ )
+ content_language = _set_property(
+ "Content-Language",
+ doc="""The Content-Language entity-header field describes the
+ natural language(s) of the intended audience for the enclosed
+ entity. Note that this might not be equivalent to all the
+ languages used within the entity-body.""",
+ )
+ allow = _set_property(
+ "Allow",
+ doc="""The Allow entity-header field lists the set of methods
+ supported by the resource identified by the Request-URI. The
+ purpose of this field is strictly to inform the recipient of
+ valid methods associated with the resource. An Allow header
+ field MUST be present in a 405 (Method Not Allowed)
+ response.""",
+ )
+
+ # ETag
+
+ @property
+ def cache_control(self) -> ResponseCacheControl:
+ """The Cache-Control general-header field is used to specify
+ directives that MUST be obeyed by all caching mechanisms along the
+ request/response chain.
+ """
+
+ def on_update(cache_control: _CacheControl) -> None:
+ if not cache_control and "cache-control" in self.headers:
+ del self.headers["cache-control"]
+ elif cache_control:
+ self.headers["Cache-Control"] = cache_control.to_header()
+
+ return parse_cache_control_header(
+ self.headers.get("cache-control"), on_update, ResponseCacheControl
+ )
+
+ def set_etag(self, etag: str, weak: bool = False) -> None:
+ """Set the etag, and override the old one if there was one."""
+ self.headers["ETag"] = quote_etag(etag, weak)
+
+ def get_etag(self) -> tuple[str, bool] | tuple[None, None]:
+ """Return a tuple in the form ``(etag, is_weak)``. If there is no
+ ETag the return value is ``(None, None)``.
+ """
+ return unquote_etag(self.headers.get("ETag"))
+
+ accept_ranges = header_property[str](
+ "Accept-Ranges",
+ doc="""The `Accept-Ranges` header. Even though the name would
+ indicate that multiple values are supported, it must be one
+ string token only.
+
+ The values ``'bytes'`` and ``'none'`` are common.
+
+ .. versionadded:: 0.7""",
+ )
+
+ @property
+ def content_range(self) -> ContentRange:
+ """The ``Content-Range`` header as a
+ :class:`~werkzeug.datastructures.ContentRange` object. Available
+ even if the header is not set.
+
+ .. versionadded:: 0.7
+ """
+
+ def on_update(rng: ContentRange) -> None:
+ if not rng:
+ del self.headers["content-range"]
+ else:
+ self.headers["Content-Range"] = rng.to_header()
+
+ rv = parse_content_range_header(self.headers.get("content-range"), on_update)
+ # always provide a content range object to make the descriptor
+ # more user friendly. It provides an unset() method that can be
+ # used to remove the header quickly.
+ if rv is None:
+ rv = ContentRange(None, None, None, on_update=on_update)
+ return rv
+
+ @content_range.setter
+ def content_range(self, value: ContentRange | str | None) -> None:
+ if not value:
+ del self.headers["content-range"]
+ elif isinstance(value, str):
+ self.headers["Content-Range"] = value
+ else:
+ self.headers["Content-Range"] = value.to_header()
+
+ # Authorization
+
+ @property
+ def www_authenticate(self) -> WWWAuthenticate:
+ """The ``WWW-Authenticate`` header parsed into a :class:`.WWWAuthenticate`
+ object. Modifying the object will modify the header value.
+
+ This header is not set by default. To set this header, assign an instance of
+ :class:`.WWWAuthenticate` to this attribute.
+
+ .. code-block:: python
+
+ response.www_authenticate = WWWAuthenticate(
+ "basic", {"realm": "Authentication Required"}
+ )
+
+ Multiple values for this header can be sent to give the client multiple options.
+ Assign a list to set multiple headers. However, modifying the items in the list
+ will not automatically update the header values, and accessing this attribute
+ will only ever return the first value.
+
+ To unset this header, assign ``None`` or use ``del``.
+
+ .. versionchanged:: 2.3
+ This attribute can be assigned to set the header. A list can be assigned
+ to set multiple header values. Use ``del`` to unset the header.
+
+ .. versionchanged:: 2.3
+ :class:`WWWAuthenticate` is no longer a ``dict``. The ``token`` attribute
+ was added for auth challenges that use a token instead of parameters.
+ """
+ value = WWWAuthenticate.from_header(self.headers.get("WWW-Authenticate"))
+
+ if value is None:
+ value = WWWAuthenticate("basic")
+
+ def on_update(value: WWWAuthenticate) -> None:
+ self.www_authenticate = value
+
+ value._on_update = on_update
+ return value
+
+ @www_authenticate.setter
+ def www_authenticate(
+ self, value: WWWAuthenticate | list[WWWAuthenticate] | None
+ ) -> None:
+ if not value: # None or empty list
+ del self.www_authenticate
+ elif isinstance(value, list):
+ # Clear any existing header by setting the first item.
+ self.headers.set("WWW-Authenticate", value[0].to_header())
+
+ for item in value[1:]:
+ # Add additional header lines for additional items.
+ self.headers.add("WWW-Authenticate", item.to_header())
+ else:
+ self.headers.set("WWW-Authenticate", value.to_header())
+
+ def on_update(value: WWWAuthenticate) -> None:
+ self.www_authenticate = value
+
+ # When setting a single value, allow updating it directly.
+ value._on_update = on_update
+
+ @www_authenticate.deleter
+ def www_authenticate(self) -> None:
+ if "WWW-Authenticate" in self.headers:
+ del self.headers["WWW-Authenticate"]
+
+ # CSP
+
+ @property
+ def content_security_policy(self) -> ContentSecurityPolicy:
+ """The ``Content-Security-Policy`` header as a
+ :class:`~werkzeug.datastructures.ContentSecurityPolicy` object. Available
+ even if the header is not set.
+
+ The Content-Security-Policy header adds an additional layer of
+ security to help detect and mitigate certain types of attacks.
+ """
+
+ def on_update(csp: ContentSecurityPolicy) -> None:
+ if not csp:
+ del self.headers["content-security-policy"]
+ else:
+ self.headers["Content-Security-Policy"] = csp.to_header()
+
+ rv = parse_csp_header(self.headers.get("content-security-policy"), on_update)
+ if rv is None:
+ rv = ContentSecurityPolicy(None, on_update=on_update)
+ return rv
+
+ @content_security_policy.setter
+ def content_security_policy(
+ self, value: ContentSecurityPolicy | str | None
+ ) -> None:
+ if not value:
+ del self.headers["content-security-policy"]
+ elif isinstance(value, str):
+ self.headers["Content-Security-Policy"] = value
+ else:
+ self.headers["Content-Security-Policy"] = value.to_header()
+
+ @property
+ def content_security_policy_report_only(self) -> ContentSecurityPolicy:
+ """The ``Content-Security-policy-report-only`` header as a
+ :class:`~werkzeug.datastructures.ContentSecurityPolicy` object. Available
+ even if the header is not set.
+
+ The Content-Security-Policy-Report-Only header adds a csp policy
+ that is not enforced but is reported thereby helping detect
+ certain types of attacks.
+ """
+
+ def on_update(csp: ContentSecurityPolicy) -> None:
+ if not csp:
+ del self.headers["content-security-policy-report-only"]
+ else:
+ self.headers["Content-Security-policy-report-only"] = csp.to_header()
+
+ rv = parse_csp_header(
+ self.headers.get("content-security-policy-report-only"), on_update
+ )
+ if rv is None:
+ rv = ContentSecurityPolicy(None, on_update=on_update)
+ return rv
+
+ @content_security_policy_report_only.setter
+ def content_security_policy_report_only(
+ self, value: ContentSecurityPolicy | str | None
+ ) -> None:
+ if not value:
+ del self.headers["content-security-policy-report-only"]
+ elif isinstance(value, str):
+ self.headers["Content-Security-policy-report-only"] = value
+ else:
+ self.headers["Content-Security-policy-report-only"] = value.to_header()
+
+ # CORS
+
+ @property
+ def access_control_allow_credentials(self) -> bool:
+ """Whether credentials can be shared by the browser to
+ JavaScript code. As part of the preflight request it indicates
+ whether credentials can be used on the cross origin request.
+ """
+ return "Access-Control-Allow-Credentials" in self.headers
+
+ @access_control_allow_credentials.setter
+ def access_control_allow_credentials(self, value: bool | None) -> None:
+ if value is True:
+ self.headers["Access-Control-Allow-Credentials"] = "true"
+ else:
+ self.headers.pop("Access-Control-Allow-Credentials", None)
+
+ access_control_allow_headers = header_property(
+ "Access-Control-Allow-Headers",
+ load_func=parse_set_header,
+ dump_func=dump_header,
+ doc="Which headers can be sent with the cross origin request.",
+ )
+
+ access_control_allow_methods = header_property(
+ "Access-Control-Allow-Methods",
+ load_func=parse_set_header,
+ dump_func=dump_header,
+ doc="Which methods can be used for the cross origin request.",
+ )
+
+ access_control_allow_origin = header_property[str](
+ "Access-Control-Allow-Origin",
+ doc="The origin or '*' for any origin that may make cross origin requests.",
+ )
+
+ access_control_expose_headers = header_property(
+ "Access-Control-Expose-Headers",
+ load_func=parse_set_header,
+ dump_func=dump_header,
+ doc="Which headers can be shared by the browser to JavaScript code.",
+ )
+
+ access_control_max_age = header_property(
+ "Access-Control-Max-Age",
+ load_func=int,
+ dump_func=str,
+ doc="The maximum age in seconds the access control settings can be cached for.",
+ )
+
+ cross_origin_opener_policy = header_property[COOP](
+ "Cross-Origin-Opener-Policy",
+ load_func=lambda value: COOP(value),
+ dump_func=lambda value: value.value,
+ default=COOP.UNSAFE_NONE,
+ doc="""Allows control over sharing of browsing context group with cross-origin
+ documents. Values must be a member of the :class:`werkzeug.http.COOP` enum.""",
+ )
+
+ cross_origin_embedder_policy = header_property[COEP](
+ "Cross-Origin-Embedder-Policy",
+ load_func=lambda value: COEP(value),
+ dump_func=lambda value: value.value,
+ default=COEP.UNSAFE_NONE,
+ doc="""Prevents a document from loading any cross-origin resources that do not
+ explicitly grant the document permission. Values must be a member of the
+ :class:`werkzeug.http.COEP` enum.""",
+ )
diff --git a/venv/Lib/site-packages/werkzeug/sansio/utils.py b/venv/Lib/site-packages/werkzeug/sansio/utils.py
new file mode 100644
index 0000000..586d169
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/sansio/utils.py
@@ -0,0 +1,224 @@
+from __future__ import annotations
+
+import re
+import typing as t
+from urllib.parse import quote
+
+from .._internal import _plain_int
+from ..exceptions import SecurityError
+from ..http import parse_set_header
+from ..urls import uri_to_iri
+
+_host_re = re.compile(
+ r"""
+ (
+ [a-z0-9.-]+ # domain or ipv4
+ |
+ \[[a-f0-9]*:[a-f0-9.:]+] # ipv6
+ )
+ (?::[0-9]+)? # optional port
+ """,
+ flags=re.ASCII | re.IGNORECASE | re.VERBOSE,
+)
+
+
+def host_is_trusted(
+ hostname: str | None, trusted_list: t.Collection[str] | None = None
+) -> bool:
+ """Perform some checks on a ``Host`` header ``host:port``. The host must be
+ made up of valid characters, but this does not check validity beyond that.
+ If a list of trusted domains is given, the domain must match one.
+
+ :param hostname: The ``Host`` header ``host:port`` to check.
+ :param trusted_list: A list of trusted domains to match. These should
+ already be IDNA encoded, but will be encoded if needed. The port is
+ ignored for this check. If a name starts with a dot it will match as a
+ suffix, accepting all subdomains. If empty or ``None``, all domains are
+ allowed.
+
+ .. versionchanged:: 3.2
+ The value's characters are validated.
+
+ .. versionchanged:: 3.2
+ ``trusted_list`` defaults to ``None``.
+
+ .. versionadded:: 0.9
+ """
+ if not hostname:
+ return False
+
+ if _host_re.fullmatch(hostname) is None:
+ return False
+
+ hostname = hostname.partition(":")[0]
+
+ if not trusted_list:
+ return True
+
+ if isinstance(trusted_list, str):
+ trusted_list = [trusted_list]
+
+ for ref in trusted_list:
+ if ref.startswith("."):
+ ref = ref[1:]
+ suffix_match = True
+ else:
+ suffix_match = False
+
+ try:
+ ref = ref.partition(":")[0].encode("idna").decode("ascii")
+ except UnicodeEncodeError:
+ return False
+
+ if ref == hostname or (suffix_match and hostname.endswith(f".{ref}")):
+ return True
+
+ return False
+
+
+def get_host(
+ scheme: str,
+ host_header: str | None,
+ server: tuple[str, int | None] | None = None,
+ trusted_hosts: t.Collection[str] | None = None,
+) -> str:
+ """Get and validate a request's ``host:port`` based on the given values.
+
+ The ``Host`` header sent by the client is preferred. Otherwise, the server's
+ configured address is used. The port is omitted if it matches the standard
+ HTTP or HTTPS ports.
+
+ The value is passed through :func:`host_is_trusted`. The host must be made
+ up of valid characters, but this does not check validity beyond that. If a
+ list of trusted domains is given, the domain must match one.
+
+ If the host header is not available, such as for HTTP/0.9 and 1.0, or it has
+ invalid characters, the empty string is returned. Subdomain and host
+ routing, and external URL building, will not work in these cases.
+
+ :param scheme: The protocol of the request. Used to omit the standard ports
+ 80 and 443.
+ :param host_header: The ``Host`` header value.
+ :param server: The server's configured address ``(host, port)``. The server
+ may be using a Unix socket and give ``(path, None)``; this is ignored as
+ it would not produce a useful host value.
+ :param trusted_hosts: A list of trusted domains to match. These should
+ already be IDNA encoded, but will be encoded if needed. The port is
+ ignored for this check. If a name starts with a dot it will match as a
+ suffix, accepting all subdomains. If empty or ``None``, all domains are
+ allowed.
+
+ :return: Host, with port if necessary.
+ :raise .SecurityError: If the host is not trusted.
+
+ .. versionchanged:: 3.1.8
+ The empty string is again returned if no host header value is available,
+ or if the characters are invalid.
+
+ .. versionchanged:: 3.1.7
+ The characters of the host value are validated. The empty string is no
+ longer allowed if no header value is available.
+
+ .. versionchanged:: 3.2
+ When using the server address, Unix sockets are ignored.
+
+ .. versionchanged:: 3.1.3
+ If ``SERVER_NAME`` is IPv6, it is wrapped in ``[]``.
+ """
+ if host_header is not None:
+ host = host_header
+ # The port server[1] will be None for a Unix socket. Ignore in that case.
+ elif server is not None and server[1] is not None:
+ host = server[0]
+
+ # If SERVER_NAME is IPv6, wrap it in [] to match Host header.
+ # Check for : because domain or IPv4 can't have that.
+ if ":" in host and host[0] != "[":
+ host = f"[{host}]"
+
+ host = f"{host}:{server[1]}"
+ else:
+ # Pass through empty host from HTTP/0.9 and 1.0.
+ return ""
+
+ if scheme in {"http", "ws"}:
+ host = host.removesuffix(":80")
+ elif scheme in {"https", "wss"}:
+ host = host.removesuffix(":443")
+
+ if not host_is_trusted(host, trusted_hosts):
+ if trusted_hosts:
+ raise SecurityError(f"Host {host!r} is not trusted.")
+
+ # Invalid characters, treat as empty.
+ return ""
+
+ return host
+
+
+def get_current_url(
+ scheme: str,
+ host: str,
+ root_path: str | None = None,
+ path: str | None = None,
+ query_string: bytes | None = None,
+) -> str:
+ """Recreate the URL for a request. If an optional part isn't
+ provided, it and subsequent parts are not included in the URL.
+
+ The URL is an IRI, not a URI, so it may contain Unicode characters.
+ Use :func:`~werkzeug.urls.iri_to_uri` to convert it to ASCII.
+
+ :param scheme: The protocol the request used, like ``"https"``.
+ :param host: The host the request was made to. See :func:`get_host`.
+ :param root_path: Prefix that the application is mounted under. This
+ is prepended to ``path``.
+ :param path: The path part of the URL after ``root_path``.
+ :param query_string: The portion of the URL after the "?".
+ """
+ url = [scheme, "://", host]
+
+ if root_path is None:
+ url.append("/")
+ return uri_to_iri("".join(url))
+
+ # safe = https://url.spec.whatwg.org/#url-path-segment-string
+ # as well as percent for things that are already quoted
+ url.append(quote(root_path.rstrip("/"), safe="!$&'()*+,/:;=@%"))
+ url.append("/")
+
+ if path is None:
+ return uri_to_iri("".join(url))
+
+ url.append(quote(path.lstrip("/"), safe="!$&'()*+,/:;=@%"))
+
+ if query_string:
+ url.append("?")
+ url.append(quote(query_string, safe="!$&'()*+,/:;=?@%"))
+
+ return uri_to_iri("".join(url))
+
+
+def get_content_length(
+ http_content_length: str | None = None,
+ http_transfer_encoding: str | None = None,
+) -> int | None:
+ """Return the ``Content-Length`` header value as an int. If the header is not given
+ or the ``Transfer-Encoding`` header is ``chunked``, ``None`` is returned to indicate
+ a streaming request. If the value is not an integer, or negative, 0 is returned.
+
+ :param http_content_length: The Content-Length HTTP header.
+ :param http_transfer_encoding: The Transfer-Encoding HTTP header.
+
+ .. versionadded:: 2.2
+ """
+ if (
+ http_transfer_encoding is not None
+ and "chunked" in parse_set_header(http_transfer_encoding)
+ ) or http_content_length is None:
+ return None
+
+ try:
+ return max(0, _plain_int(http_content_length))
+ except ValueError:
+ return 0
diff --git a/venv/Lib/site-packages/werkzeug/security.py b/venv/Lib/site-packages/werkzeug/security.py
new file mode 100644
index 0000000..9ae5117
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/security.py
@@ -0,0 +1,201 @@
+from __future__ import annotations
+
+import hashlib
+import hmac
+import os
+import posixpath
+import secrets
+
+SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+DEFAULT_PBKDF2_ITERATIONS = 1_000_000
+
+_os_alt_seps: list[str] = list(
+ sep for sep in [os.sep, os.altsep] if sep is not None and sep != "/"
+)
+# https://chrisdenton.github.io/omnipath/Special%20Dos%20Device%20Names.html
+_windows_device_files = {
+ "AUX",
+ "CON",
+ "CONIN$",
+ "CONOUT$",
+ *(f"COM{c}" for c in "123456789¹²³"),
+ *(f"LPT{c}" for c in "123456789¹²³"),
+ "NUL",
+ "PRN",
+}
+
+
+def gen_salt(length: int) -> str:
+ """Generate a random string of SALT_CHARS with specified ``length``."""
+ if length <= 0:
+ raise ValueError("Salt length must be at least 1.")
+
+ return "".join(secrets.choice(SALT_CHARS) for _ in range(length))
+
+
+def _hash_internal(method: str, salt: str, password: str) -> tuple[str, str]:
+ method, *args = method.split(":")
+ salt_bytes = salt.encode()
+ password_bytes = password.encode()
+
+ if method == "scrypt":
+ if not args:
+ n = 2**15
+ r = 8
+ p = 1
+ else:
+ try:
+ n, r, p = map(int, args)
+ except ValueError:
+ raise ValueError("'scrypt' takes 3 arguments.") from None
+
+ maxmem = 132 * n * r * p # ideally 128, but some extra seems needed
+ return (
+ hashlib.scrypt(
+ password_bytes, salt=salt_bytes, n=n, r=r, p=p, maxmem=maxmem
+ ).hex(),
+ f"scrypt:{n}:{r}:{p}",
+ )
+ elif method == "pbkdf2":
+ len_args = len(args)
+
+ if len_args == 0:
+ hash_name = "sha256"
+ iterations = DEFAULT_PBKDF2_ITERATIONS
+ elif len_args == 1:
+ hash_name = args[0]
+ iterations = DEFAULT_PBKDF2_ITERATIONS
+ elif len_args == 2:
+ hash_name = args[0]
+ iterations = int(args[1])
+ else:
+ raise ValueError("'pbkdf2' takes 2 arguments.")
+
+ return (
+ hashlib.pbkdf2_hmac(
+ hash_name, password_bytes, salt_bytes, iterations
+ ).hex(),
+ f"pbkdf2:{hash_name}:{iterations}",
+ )
+ else:
+ raise ValueError(f"Invalid hash method '{method}'.")
+
+
+def generate_password_hash(
+ password: str, method: str = "scrypt", salt_length: int = 16
+) -> str:
+ """Securely hash a password for storage. A password can be compared to a stored hash
+ using :func:`check_password_hash`.
+
+ The following methods are supported:
+
+ - ``scrypt``, the default. The parameters are ``n``, ``r``, and ``p``, the default
+ is ``scrypt:32768:8:1``. See :func:`hashlib.scrypt`.
+ - ``pbkdf2``, less secure. The parameters are ``hash_method`` and ``iterations``,
+ the default is ``pbkdf2:sha256:600000``. See :func:`hashlib.pbkdf2_hmac`.
+
+ Default parameters may be updated to reflect current guidelines, and methods may be
+ deprecated and removed if they are no longer considered secure. To migrate old
+ hashes, you may generate a new hash when checking an old hash, or you may contact
+ users with a link to reset their password.
+
+ :param password: The plaintext password.
+ :param method: The key derivation function and parameters.
+ :param salt_length: The number of characters to generate for the salt.
+
+ .. versionchanged:: 3.1
+ The default iterations for pbkdf2 was increased to 1,000,000.
+
+ .. versionchanged:: 2.3
+ Scrypt support was added.
+
+ .. versionchanged:: 2.3
+ The default iterations for pbkdf2 was increased to 600,000.
+
+ .. versionchanged:: 2.3
+ All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
+ """
+ salt = gen_salt(salt_length)
+ h, actual_method = _hash_internal(method, salt, password)
+ return f"{actual_method}${salt}${h}"
+
+
+def check_password_hash(pwhash: str, password: str) -> bool:
+ """Securely check that the given stored password hash, previously generated using
+ :func:`generate_password_hash`, matches the given password.
+
+ Methods may be deprecated and removed if they are no longer considered secure. To
+ migrate old hashes, you may generate a new hash when checking an old hash, or you
+ may contact users with a link to reset their password.
+
+ :param pwhash: The hashed password.
+ :param password: The plaintext password.
+
+ .. versionchanged:: 2.3
+ All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
+ """
+ try:
+ method, salt, hashval = pwhash.split("$", 2)
+ except ValueError:
+ return False
+
+ return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval)
+
+
+def safe_join(directory: str, *untrusted: str) -> str | None:
+ """Safely join zero or more untrusted path components to a trusted base
+ directory to avoid escaping the base directory.
+
+ The untrusted path is assumed to be from/for a URL, such as for serving
+ files. Therefore, it should only use the forward slash ``/`` path separator,
+ and will be joined using that separator. On Windows, the backslash ``\\``
+ separator is not allowed.
+
+ :param directory: The trusted base directory.
+ :param untrusted: The untrusted path components relative to the
+ base directory.
+ :return: A safe path, otherwise ``None``.
+
+ .. versionchanged:: 3.1.6
+ Special device names in multi-segment paths are not allowed on Windows.
+
+ .. versionchanged:: 3.1.5
+ More special device names, regardless of extension or trailing spaces,
+ are not allowed on Windows.
+
+ .. versionchanged:: 3.1.4
+ Special device names are not allowed on Windows.
+ """
+ if not directory:
+ # Ensure we end up with ./path if directory="" is given,
+ # otherwise the first untrusted part could become trusted.
+ directory = "."
+
+ parts = [directory]
+
+ for part in untrusted:
+ if not part:
+ continue
+
+ part = posixpath.normpath(part)
+
+ if (
+ os.path.isabs(part)
+ # ntpath.isabs doesn't catch this
+ or part.startswith("/")
+ or part == ".."
+ or part.startswith("../")
+ or any(sep in part for sep in _os_alt_seps)
+ or (
+ os.name == "nt"
+ and any(
+ p.partition(".")[0].strip().upper() in _windows_device_files
+ for p in part.split("/")
+ )
+ )
+ ):
+ return None
+
+ parts.append(part)
+
+ return posixpath.join(*parts)
diff --git a/venv/Lib/site-packages/werkzeug/serving.py b/venv/Lib/site-packages/werkzeug/serving.py
new file mode 100644
index 0000000..37f3a2d
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/serving.py
@@ -0,0 +1,1126 @@
+"""A WSGI and HTTP server for use **during development only**. This
+server is convenient to use, but is not designed to be particularly
+stable, secure, or efficient. Use a dedicate WSGI server and HTTP
+server when deploying to production.
+
+It provides features like interactive debugging and code reloading. Use
+``run_simple`` to start the server. Put this in a ``run.py`` script:
+
+.. code-block:: python
+
+ from myapp import create_app
+ from werkzeug import run_simple
+"""
+
+from __future__ import annotations
+
+import errno
+import io
+import os
+import selectors
+import socket
+import socketserver
+import sys
+import typing as t
+from datetime import datetime as dt
+from datetime import timedelta
+from datetime import timezone
+from http.server import BaseHTTPRequestHandler
+from http.server import HTTPServer
+from urllib.parse import unquote
+from urllib.parse import urlsplit
+
+from ._internal import _log
+from ._internal import _wsgi_encoding_dance
+from .exceptions import InternalServerError
+from .http import parse_set_header
+from .urls import uri_to_iri
+
+try:
+ import ssl
+
+ connection_dropped_errors: tuple[type[Exception], ...] = (
+ ConnectionError,
+ socket.timeout,
+ ssl.SSLEOFError,
+ )
+except ImportError:
+
+ class _SslDummy:
+ def __getattr__(self, name: str) -> t.Any:
+ raise RuntimeError( # noqa: B904
+ "SSL is unavailable because this Python runtime was not"
+ " compiled with SSL/TLS support."
+ )
+
+ ssl = _SslDummy() # type: ignore
+ connection_dropped_errors = (ConnectionError, socket.timeout)
+
+_log_add_style = True
+
+if os.name == "nt":
+ try:
+ __import__("colorama")
+ except ImportError:
+ _log_add_style = False
+
+can_fork = hasattr(os, "fork")
+
+if can_fork:
+ ForkingMixIn = socketserver.ForkingMixIn
+else:
+
+ class ForkingMixIn: # type: ignore
+ pass
+
+
+try:
+ af_unix = socket.AF_UNIX
+except AttributeError:
+ af_unix = None # type: ignore
+
+LISTEN_QUEUE = 128
+
+_TSSLContextArg = t.Optional[
+ t.Union["ssl.SSLContext", tuple[str, t.Optional[str]], t.Literal["adhoc"]]
+]
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import WSGIApplication
+ from _typeshed.wsgi import WSGIEnvironment
+ from cryptography.hazmat.primitives.asymmetric.rsa import (
+ RSAPrivateKeyWithSerialization,
+ )
+ from cryptography.x509 import Certificate
+
+
+class DechunkedInput(io.RawIOBase):
+ """An input stream that handles Transfer-Encoding 'chunked'"""
+
+ def __init__(self, rfile: t.IO[bytes]) -> None:
+ self._rfile = rfile
+ self._done = False
+ self._len = 0
+
+ def readable(self) -> bool:
+ return True
+
+ def read_chunk_len(self) -> int:
+ try:
+ line = self._rfile.readline().decode("latin1")
+ _len = int(line.strip(), 16)
+ except ValueError as e:
+ raise OSError("Invalid chunk header") from e
+ if _len < 0:
+ raise OSError("Negative chunk length not allowed")
+ return _len
+
+ def readinto(self, buf: bytearray) -> int: # type: ignore
+ read = 0
+ while not self._done and read < len(buf):
+ if self._len == 0:
+ # This is the first chunk or we fully consumed the previous
+ # one. Read the next length of the next chunk
+ self._len = self.read_chunk_len()
+
+ if self._len == 0:
+ # Found the final chunk of size 0. The stream is now exhausted,
+ # but there is still a final newline that should be consumed
+ self._done = True
+
+ if self._len > 0:
+ # There is data (left) in this chunk, so append it to the
+ # buffer. If this operation fully consumes the chunk, this will
+ # reset self._len to 0.
+ n = min(len(buf), self._len)
+
+ # If (read + chunk size) becomes more than len(buf), buf will
+ # grow beyond the original size and read more data than
+ # required. So only read as much data as can fit in buf.
+ if read + n > len(buf):
+ buf[read:] = self._rfile.read(len(buf) - read)
+ self._len -= len(buf) - read
+ read = len(buf)
+ else:
+ buf[read : read + n] = self._rfile.read(n)
+ self._len -= n
+ read += n
+
+ if self._len == 0:
+ # Skip the terminating newline of a chunk that has been fully
+ # consumed. This also applies to the 0-sized final chunk
+ terminator = self._rfile.readline()
+ if terminator not in (b"\n", b"\r\n", b"\r"):
+ raise OSError("Missing chunk terminating newline")
+
+ return read
+
+
+class WSGIRequestHandler(BaseHTTPRequestHandler):
+ """A request handler that implements WSGI dispatching."""
+
+ server: BaseWSGIServer
+
+ @property
+ def server_version(self) -> str: # type: ignore
+ return self.server._server_version
+
+ def make_environ(self) -> WSGIEnvironment:
+ request_url = urlsplit(self.path)
+ url_scheme = "http" if self.server.ssl_context is None else "https"
+
+ if not self.client_address:
+ self.client_address = ("", 0)
+ elif isinstance(self.client_address, str):
+ self.client_address = (self.client_address, 0)
+
+ # If there was no scheme but the path started with two slashes,
+ # the first segment may have been incorrectly parsed as the
+ # netloc, prepend it to the path again.
+ if not request_url.scheme and request_url.netloc:
+ path_info = f"/{request_url.netloc}{request_url.path}"
+ else:
+ path_info = request_url.path
+
+ path_info = unquote(path_info)
+
+ environ: WSGIEnvironment = {
+ "wsgi.version": (1, 0),
+ "wsgi.url_scheme": url_scheme,
+ "wsgi.input": self.rfile,
+ "wsgi.errors": sys.stderr,
+ "wsgi.multithread": self.server.multithread,
+ "wsgi.multiprocess": self.server.multiprocess,
+ "wsgi.run_once": False,
+ "werkzeug.socket": self.connection,
+ "SERVER_SOFTWARE": self.server_version,
+ "REQUEST_METHOD": self.command,
+ "SCRIPT_NAME": "",
+ "PATH_INFO": _wsgi_encoding_dance(path_info),
+ "QUERY_STRING": _wsgi_encoding_dance(request_url.query),
+ # Non-standard, added by mod_wsgi, uWSGI
+ "REQUEST_URI": _wsgi_encoding_dance(self.path),
+ # Non-standard, added by gunicorn
+ "RAW_URI": _wsgi_encoding_dance(self.path),
+ "REMOTE_ADDR": self.address_string(),
+ "REMOTE_PORT": self.port_integer(),
+ "SERVER_NAME": self.server.server_address[0],
+ "SERVER_PORT": str(self.server.server_address[1]),
+ "SERVER_PROTOCOL": self.request_version,
+ }
+
+ for key, value in self.headers.items():
+ if "_" in key:
+ continue
+
+ key = key.upper().replace("-", "_")
+ value = value.replace("\r\n", "")
+ if key not in ("CONTENT_TYPE", "CONTENT_LENGTH"):
+ key = f"HTTP_{key}"
+ if key in environ:
+ value = f"{environ[key]},{value}"
+ environ[key] = value
+
+ if "chunked" in parse_set_header(environ.get("HTTP_TRANSFER_ENCODING")):
+ environ["wsgi.input_terminated"] = True
+ environ["wsgi.input"] = DechunkedInput(environ["wsgi.input"])
+
+ # Per RFC 2616, if the URL is absolute, use that as the host.
+ # We're using "has a scheme" to indicate an absolute URL.
+ if request_url.scheme and request_url.netloc:
+ environ["HTTP_HOST"] = request_url.netloc
+
+ try:
+ # binary_form=False gives nicer information, but wouldn't be compatible with
+ # what Nginx or Apache could return.
+ peer_cert = self.connection.getpeercert(binary_form=True)
+ if peer_cert is not None:
+ # Nginx and Apache use PEM format.
+ environ["SSL_CLIENT_CERT"] = ssl.DER_cert_to_PEM_cert(peer_cert)
+ except ValueError:
+ # SSL handshake hasn't finished.
+ self.server.log("error", "Cannot fetch SSL peer certificate info")
+ except AttributeError:
+ # Not using TLS, the socket will not have getpeercert().
+ pass
+
+ return environ
+
+ def run_wsgi(self) -> None:
+ if self.headers.get("Expect", "").lower().strip() == "100-continue":
+ self.wfile.write(b"HTTP/1.1 100 Continue\r\n\r\n")
+
+ self.environ = environ = self.make_environ()
+ status_set: str | None = None
+ headers_set: list[tuple[str, str]] | None = None
+ status_sent: str | None = None
+ headers_sent: list[tuple[str, str]] | None = None
+ chunk_response: bool = False
+
+ def write(data: bytes) -> None:
+ nonlocal status_sent, headers_sent, chunk_response
+ assert status_set is not None, "write() before start_response"
+ assert headers_set is not None, "write() before start_response"
+ if status_sent is None:
+ status_sent = status_set
+ headers_sent = headers_set
+ try:
+ code_str, msg = status_sent.split(None, 1)
+ except ValueError:
+ code_str, msg = status_sent, ""
+ code = int(code_str)
+ self.send_response(code, msg)
+ header_keys = set()
+ for key, value in headers_sent:
+ self.send_header(key, value)
+ header_keys.add(key.lower())
+
+ # Use chunked transfer encoding if there is no content
+ # length. Do not use for 1xx and 204 responses. 304
+ # responses and HEAD requests are also excluded, which
+ # is the more conservative behavior and matches other
+ # parts of the code.
+ # https://httpwg.org/specs/rfc7230.html#rfc.section.3.3.1
+ if (
+ not (
+ "content-length" in header_keys
+ or environ["REQUEST_METHOD"] == "HEAD"
+ or (100 <= code < 200)
+ or code in {204, 304}
+ )
+ and self.protocol_version >= "HTTP/1.1"
+ ):
+ chunk_response = True
+ self.send_header("Transfer-Encoding", "chunked")
+
+ # Always close the connection. This disables HTTP/1.1
+ # keep-alive connections. They aren't handled well by
+ # Python's http.server because it doesn't know how to
+ # drain the stream before the next request line.
+ self.send_header("Connection", "close")
+ self.end_headers()
+
+ assert isinstance(data, bytes), "applications must write bytes"
+
+ if data:
+ if chunk_response:
+ self.wfile.write(hex(len(data))[2:].encode())
+ self.wfile.write(b"\r\n")
+
+ self.wfile.write(data)
+
+ if chunk_response:
+ self.wfile.write(b"\r\n")
+
+ self.wfile.flush()
+
+ def start_response(status, headers, exc_info=None): # type: ignore
+ nonlocal status_set, headers_set
+ if exc_info:
+ try:
+ if headers_sent:
+ raise exc_info[1].with_traceback(exc_info[2])
+ finally:
+ exc_info = None
+ elif headers_set:
+ raise AssertionError("Headers already set")
+ status_set = status
+ headers_set = headers
+ return write
+
+ def execute(app: WSGIApplication) -> None:
+ application_iter = app(environ, start_response)
+ try:
+ for data in application_iter:
+ write(data)
+ if not headers_sent:
+ write(b"")
+ if chunk_response:
+ self.wfile.write(b"0\r\n\r\n")
+ finally:
+ # Check for any remaining data in the read socket, and discard it. This
+ # will read past request.max_content_length, but lets the client see a
+ # 413 response instead of a connection reset failure. If we supported
+ # keep-alive connections, this naive approach would break by reading the
+ # next request line. Since we know that write (above) closes every
+ # connection we can read everything.
+ selector = selectors.DefaultSelector()
+ selector.register(self.connection, selectors.EVENT_READ)
+ total_size = 0
+ total_reads = 0
+
+ # A timeout of 0 tends to fail because a client needs a small amount of
+ # time to continue sending its data.
+ while selector.select(timeout=0.01):
+ # Only read 10MB into memory at a time.
+ data = self.rfile.read(10_000_000)
+ total_size += len(data)
+ total_reads += 1
+
+ # Stop reading on no data, >=10GB, or 1000 reads. If a client sends
+ # more than that, they'll get a connection reset failure.
+ if not data or total_size >= 10_000_000_000 or total_reads > 1000:
+ break
+
+ selector.close()
+
+ if hasattr(application_iter, "close"):
+ application_iter.close()
+
+ try:
+ execute(self.server.app)
+ except connection_dropped_errors as e:
+ self.connection_dropped(e, environ)
+ except Exception as e:
+ if self.server.passthrough_errors:
+ raise
+
+ if status_sent is not None and chunk_response:
+ self.close_connection = True
+
+ try:
+ # if we haven't yet sent the headers but they are set
+ # we roll back to be able to set them again.
+ if status_sent is None:
+ status_set = None
+ headers_set = None
+ execute(InternalServerError())
+ except Exception:
+ pass
+
+ from .debug.tbtools import DebugTraceback
+
+ msg = DebugTraceback(e).render_traceback_text()
+ self.server.log("error", f"Error on request:\n{msg}")
+
+ def handle(self) -> None:
+ """Handles a request ignoring dropped connections."""
+ try:
+ super().handle()
+ except (ConnectionError, socket.timeout) as e:
+ self.connection_dropped(e)
+ except Exception as e:
+ if self.server.ssl_context is not None and is_ssl_error(e):
+ self.log_error("SSL error occurred: %s", e)
+ else:
+ raise
+
+ def connection_dropped(
+ self, error: BaseException, environ: WSGIEnvironment | None = None
+ ) -> None:
+ """Called if the connection was closed by the client. By default
+ nothing happens.
+ """
+
+ def __getattr__(self, name: str) -> t.Any:
+ # All HTTP methods are handled by run_wsgi.
+ if name.startswith("do_"):
+ return self.run_wsgi
+
+ # All other attributes are forwarded to the base class.
+ return getattr(super(), name)
+
+ def address_string(self) -> str:
+ if getattr(self, "environ", None):
+ return self.environ["REMOTE_ADDR"] # type: ignore
+
+ if not self.client_address:
+ return ""
+
+ return self.client_address[0]
+
+ def port_integer(self) -> int:
+ return self.client_address[1]
+
+ # Escape control characters. This is defined (but private) in Python 3.12.
+ _control_char_table = str.maketrans(
+ {c: rf"\x{c:02x}" for c in [*range(0x20), *range(0x7F, 0xA0)]}
+ )
+ _control_char_table[ord("\\")] = r"\\"
+
+ def log_request(self, code: int | str = "-", size: int | str = "-") -> None:
+ try:
+ path = uri_to_iri(self.path)
+ msg = f"{self.command} {path} {self.request_version}"
+ except AttributeError:
+ # path isn't set if the requestline was bad
+ msg = self.requestline
+
+ # Escape control characters that may be in the decoded path.
+ msg = msg.translate(self._control_char_table)
+ code = str(code)
+
+ if code[0] == "1": # 1xx - Informational
+ msg = _ansi_style(msg, "bold")
+ elif code == "200": # 2xx - Success
+ pass
+ elif code == "304": # 304 - Resource Not Modified
+ msg = _ansi_style(msg, "cyan")
+ elif code[0] == "3": # 3xx - Redirection
+ msg = _ansi_style(msg, "green")
+ elif code == "404": # 404 - Resource Not Found
+ msg = _ansi_style(msg, "yellow")
+ elif code[0] == "4": # 4xx - Client Error
+ msg = _ansi_style(msg, "bold", "red")
+ else: # 5xx, or any other response
+ msg = _ansi_style(msg, "bold", "magenta")
+
+ self.log("info", '"%s" %s %s', msg, code, size)
+
+ def log_error(self, format: str, *args: t.Any) -> None:
+ self.log("error", format, *args)
+
+ def log_message(self, format: str, *args: t.Any) -> None:
+ self.log("info", format, *args)
+
+ def log(self, type: str, message: str, *args: t.Any) -> None:
+ # an IPv6 scoped address contains "%" which breaks logging
+ address_string = self.address_string().replace("%", "%%")
+ _log(
+ type,
+ f"{address_string} - - [{self.log_date_time_string()}] {message}\n",
+ *args,
+ )
+
+
+def _ansi_style(value: str, *styles: str) -> str:
+ if not _log_add_style:
+ return value
+
+ codes = {
+ "bold": 1,
+ "red": 31,
+ "green": 32,
+ "yellow": 33,
+ "magenta": 35,
+ "cyan": 36,
+ }
+
+ for style in styles:
+ value = f"\x1b[{codes[style]}m{value}"
+
+ return f"{value}\x1b[0m"
+
+
+def generate_adhoc_ssl_pair(
+ cn: str | None = None,
+) -> tuple[Certificate, RSAPrivateKeyWithSerialization]:
+ try:
+ from cryptography import x509
+ from cryptography.hazmat.backends import default_backend
+ from cryptography.hazmat.primitives import hashes
+ from cryptography.hazmat.primitives.asymmetric import rsa
+ from cryptography.x509.oid import NameOID
+ except ImportError:
+ raise TypeError(
+ "Using ad-hoc certificates requires the cryptography library."
+ ) from None
+
+ backend = default_backend()
+ pkey = rsa.generate_private_key(
+ public_exponent=65537, key_size=2048, backend=backend
+ )
+
+ # pretty damn sure that this is not actually accepted by anyone
+ if cn is None:
+ cn = "*"
+
+ subject = x509.Name(
+ [
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Dummy Certificate"),
+ x509.NameAttribute(NameOID.COMMON_NAME, cn),
+ ]
+ )
+
+ backend = default_backend()
+ cert = (
+ x509.CertificateBuilder()
+ .subject_name(subject)
+ .issuer_name(subject)
+ .public_key(pkey.public_key())
+ .serial_number(x509.random_serial_number())
+ .not_valid_before(dt.now(timezone.utc))
+ .not_valid_after(dt.now(timezone.utc) + timedelta(days=365))
+ .add_extension(x509.ExtendedKeyUsage([x509.OID_SERVER_AUTH]), critical=False)
+ .add_extension(
+ x509.SubjectAlternativeName([x509.DNSName(cn), x509.DNSName(f"*.{cn}")]),
+ critical=False,
+ )
+ .sign(pkey, hashes.SHA256(), backend)
+ )
+ return cert, pkey
+
+
+def make_ssl_devcert(
+ base_path: str, host: str | None = None, cn: str | None = None
+) -> tuple[str, str]:
+ """Creates an SSL key for development. This should be used instead of
+ the ``'adhoc'`` key which generates a new cert on each server start.
+ It accepts a path for where it should store the key and cert and
+ either a host or CN. If a host is given it will use the CN
+ ``*.host/CN=host``.
+
+ For more information see :func:`run_simple`.
+
+ .. versionadded:: 0.9
+
+ :param base_path: the path to the certificate and key. The extension
+ ``.crt`` is added for the certificate, ``.key`` is
+ added for the key.
+ :param host: the name of the host. This can be used as an alternative
+ for the `cn`.
+ :param cn: the `CN` to use.
+ """
+
+ if host is not None:
+ cn = host
+ cert, pkey = generate_adhoc_ssl_pair(cn=cn)
+
+ from cryptography.hazmat.primitives import serialization
+
+ cert_file = f"{base_path}.crt"
+ pkey_file = f"{base_path}.key"
+
+ with open(cert_file, "wb") as f:
+ f.write(cert.public_bytes(serialization.Encoding.PEM))
+ with open(pkey_file, "wb") as f:
+ f.write(
+ pkey.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption(),
+ )
+ )
+
+ return cert_file, pkey_file
+
+
+def generate_adhoc_ssl_context() -> ssl.SSLContext:
+ """Generates an adhoc SSL context for the development server."""
+ import atexit
+ import tempfile
+
+ cert, pkey = generate_adhoc_ssl_pair()
+
+ from cryptography.hazmat.primitives import serialization
+
+ cert_handle, cert_file = tempfile.mkstemp()
+ pkey_handle, pkey_file = tempfile.mkstemp()
+ atexit.register(os.remove, pkey_file)
+ atexit.register(os.remove, cert_file)
+
+ os.write(cert_handle, cert.public_bytes(serialization.Encoding.PEM))
+ os.write(
+ pkey_handle,
+ pkey.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption(),
+ ),
+ )
+
+ os.close(cert_handle)
+ os.close(pkey_handle)
+ ctx = load_ssl_context(cert_file, pkey_file)
+ return ctx
+
+
+def load_ssl_context(
+ cert_file: str, pkey_file: str | None = None, protocol: int | None = None
+) -> ssl.SSLContext:
+ """Loads SSL context from cert/private key files and optional protocol.
+ Many parameters are directly taken from the API of
+ :py:class:`ssl.SSLContext`.
+
+ :param cert_file: Path of the certificate to use.
+ :param pkey_file: Path of the private key to use. If not given, the key
+ will be obtained from the certificate file.
+ :param protocol: A ``PROTOCOL`` constant from the :mod:`ssl` module.
+ Defaults to :data:`ssl.PROTOCOL_TLS_SERVER`.
+ """
+ if protocol is None:
+ protocol = ssl.PROTOCOL_TLS_SERVER
+
+ ctx = ssl.SSLContext(protocol)
+ ctx.load_cert_chain(cert_file, pkey_file)
+ return ctx
+
+
+def is_ssl_error(error: Exception | None = None) -> bool:
+ """Checks if the given error (or the current one) is an SSL error."""
+ if error is None:
+ error = t.cast(Exception, sys.exc_info()[1])
+ return isinstance(error, ssl.SSLError)
+
+
+def select_address_family(host: str, port: int) -> socket.AddressFamily:
+ """Return ``AF_INET4``, ``AF_INET6``, or ``AF_UNIX`` depending on
+ the host and port."""
+ if host.startswith("unix://"):
+ return socket.AF_UNIX
+ elif ":" in host and hasattr(socket, "AF_INET6"):
+ return socket.AF_INET6
+ return socket.AF_INET
+
+
+def get_sockaddr(
+ host: str, port: int, family: socket.AddressFamily
+) -> tuple[str, int] | str:
+ """Return a fully qualified socket address that can be passed to
+ :func:`socket.bind`."""
+ if family == af_unix:
+ # Absolute path avoids IDNA encoding error when path starts with dot.
+ return os.path.abspath(host.partition("://")[2])
+ try:
+ res = socket.getaddrinfo(
+ host, port, family, socket.SOCK_STREAM, socket.IPPROTO_TCP
+ )
+ except socket.gaierror:
+ return host, port
+ return res[0][4] # type: ignore
+
+
+def get_interface_ip(family: socket.AddressFamily) -> str:
+ """Get the IP address of an external interface. Used when binding to
+ 0.0.0.0 or ::1 to show a more useful URL.
+
+ :meta private:
+ """
+ # arbitrary private address
+ host = "fd31:f903:5ab5:1::1" if family == socket.AF_INET6 else "10.253.155.219"
+
+ with socket.socket(family, socket.SOCK_DGRAM) as s:
+ try:
+ s.connect((host, 58162))
+ except OSError:
+ return "::1" if family == socket.AF_INET6 else "127.0.0.1"
+
+ return s.getsockname()[0] # type: ignore
+
+
+class BaseWSGIServer(HTTPServer):
+ """A WSGI server that that handles one request at a time.
+
+ Use :func:`make_server` to create a server instance.
+ """
+
+ multithread = False
+ multiprocess = False
+ request_queue_size = LISTEN_QUEUE
+ allow_reuse_address = True
+
+ def __init__(
+ self,
+ host: str,
+ port: int,
+ app: WSGIApplication,
+ handler: type[WSGIRequestHandler] | None = None,
+ passthrough_errors: bool = False,
+ ssl_context: _TSSLContextArg | None = None,
+ fd: int | None = None,
+ ) -> None:
+ if handler is None:
+ handler = WSGIRequestHandler
+
+ # If the handler doesn't directly set a protocol version and
+ # thread or process workers are used, then allow chunked
+ # responses and keep-alive connections by enabling HTTP/1.1.
+ if "protocol_version" not in vars(handler) and (
+ self.multithread or self.multiprocess
+ ):
+ handler.protocol_version = "HTTP/1.1"
+
+ self.host = host
+ self.port = port
+ self.app = app
+ self.passthrough_errors = passthrough_errors
+
+ self.address_family = address_family = select_address_family(host, port)
+ server_address = get_sockaddr(host, int(port), address_family)
+
+ # Remove a leftover Unix socket file from a previous run. Don't
+ # remove a file that was set up by run_simple.
+ if address_family == af_unix and fd is None:
+ server_address = t.cast(str, server_address)
+
+ if os.path.exists(server_address):
+ os.unlink(server_address)
+
+ # Bind and activate will be handled manually, it should only
+ # happen if we're not using a socket that was already set up.
+ super().__init__(
+ server_address, # type: ignore[arg-type]
+ handler,
+ bind_and_activate=False,
+ )
+
+ if fd is None:
+ # No existing socket descriptor, do bind_and_activate=True.
+ try:
+ self.server_bind()
+ self.server_activate()
+ except OSError as e:
+ # Catch connection issues and show them without the traceback. Show
+ # extra instructions for address not found, and for macOS.
+ self.server_close()
+ print(e.strerror, file=sys.stderr)
+
+ if e.errno == errno.EADDRINUSE:
+ print(
+ f"Port {port} is in use by another program. Either identify and"
+ " stop that program, or start the server with a different"
+ " port.",
+ file=sys.stderr,
+ )
+
+ if sys.platform == "darwin" and port == 5000:
+ print(
+ "On macOS, try searching for and disabling"
+ " 'AirPlay Receiver' in System Settings.",
+ file=sys.stderr,
+ )
+
+ sys.exit(1)
+ except BaseException:
+ self.server_close()
+ raise
+ else:
+ # TCPServer automatically opens a socket even if bind_and_activate is False.
+ # Close it to silence a ResourceWarning.
+ self.server_close()
+
+ # Use the passed in socket directly.
+ self.socket = socket.fromfd(fd, address_family, socket.SOCK_STREAM)
+ self.server_address = self.socket.getsockname()
+
+ if address_family != af_unix:
+ # If port was 0, this will record the bound port.
+ self.port = self.server_address[1]
+
+ if ssl_context is not None:
+ if isinstance(ssl_context, tuple):
+ ssl_context = load_ssl_context(*ssl_context)
+ elif ssl_context == "adhoc":
+ ssl_context = generate_adhoc_ssl_context()
+
+ self.socket = ssl_context.wrap_socket(self.socket, server_side=True)
+ self.ssl_context: ssl.SSLContext | None = ssl_context
+ else:
+ self.ssl_context = None
+
+ import importlib.metadata
+
+ self._server_version = f"Werkzeug/{importlib.metadata.version('werkzeug')}"
+
+ def log(self, type: str, message: str, *args: t.Any) -> None:
+ _log(type, message, *args)
+
+ def serve_forever(self, poll_interval: float = 0.5) -> None:
+ try:
+ super().serve_forever(poll_interval=poll_interval)
+ except KeyboardInterrupt:
+ pass
+ finally:
+ self.server_close()
+
+ def handle_error(
+ self, request: t.Any, client_address: tuple[str, int] | str
+ ) -> None:
+ if self.passthrough_errors:
+ raise
+
+ return super().handle_error(request, client_address)
+
+ def log_startup(self) -> None:
+ """Show information about the address when starting the server."""
+ dev_warning = (
+ "WARNING: This is a development server. Do not use it in a production"
+ " deployment. Use a production WSGI server instead."
+ )
+ dev_warning = _ansi_style(dev_warning, "bold", "red")
+ messages = [dev_warning]
+
+ if self.address_family == af_unix:
+ messages.append(f" * Running on {self.host}")
+ else:
+ scheme = "http" if self.ssl_context is None else "https"
+ display_hostname = self.host
+
+ if self.host in {"0.0.0.0", "::"}:
+ messages.append(f" * Running on all addresses ({self.host})")
+
+ if self.host == "0.0.0.0":
+ localhost = "127.0.0.1"
+ display_hostname = get_interface_ip(socket.AF_INET)
+ else:
+ localhost = "[::1]"
+ display_hostname = get_interface_ip(socket.AF_INET6)
+
+ messages.append(f" * Running on {scheme}://{localhost}:{self.port}")
+
+ if ":" in display_hostname:
+ display_hostname = f"[{display_hostname}]"
+
+ messages.append(f" * Running on {scheme}://{display_hostname}:{self.port}")
+
+ _log("info", "\n".join(messages))
+
+
+class ThreadedWSGIServer(socketserver.ThreadingMixIn, BaseWSGIServer):
+ """A WSGI server that handles concurrent requests in separate
+ threads.
+
+ Use :func:`make_server` to create a server instance.
+ """
+
+ multithread = True
+ daemon_threads = True
+
+
+class ForkingWSGIServer(ForkingMixIn, BaseWSGIServer):
+ """A WSGI server that handles concurrent requests in separate forked
+ processes.
+
+ Use :func:`make_server` to create a server instance.
+ """
+
+ multiprocess = True
+
+ def __init__(
+ self,
+ host: str,
+ port: int,
+ app: WSGIApplication,
+ processes: int = 40,
+ handler: type[WSGIRequestHandler] | None = None,
+ passthrough_errors: bool = False,
+ ssl_context: _TSSLContextArg | None = None,
+ fd: int | None = None,
+ ) -> None:
+ if not can_fork:
+ raise ValueError("Your platform does not support forking.")
+
+ super().__init__(host, port, app, handler, passthrough_errors, ssl_context, fd)
+ self.max_children = processes
+
+
+def make_server(
+ host: str,
+ port: int,
+ app: WSGIApplication,
+ threaded: bool = False,
+ processes: int = 1,
+ request_handler: type[WSGIRequestHandler] | None = None,
+ passthrough_errors: bool = False,
+ ssl_context: _TSSLContextArg | None = None,
+ fd: int | None = None,
+) -> BaseWSGIServer:
+ """Create an appropriate WSGI server instance based on the value of
+ ``threaded`` and ``processes``.
+
+ This is called from :func:`run_simple`, but can be used separately
+ to have access to the server object, such as to run it in a separate
+ thread.
+
+ See :func:`run_simple` for parameter docs.
+ """
+ if threaded and processes > 1:
+ raise ValueError("Cannot have a multi-thread and multi-process server.")
+
+ if threaded:
+ return ThreadedWSGIServer(
+ host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd
+ )
+
+ if processes > 1:
+ return ForkingWSGIServer(
+ host,
+ port,
+ app,
+ processes,
+ request_handler,
+ passthrough_errors,
+ ssl_context,
+ fd=fd,
+ )
+
+ return BaseWSGIServer(
+ host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd
+ )
+
+
+def is_running_from_reloader() -> bool:
+ """Check if the server is running as a subprocess within the
+ Werkzeug reloader.
+
+ .. versionadded:: 0.10
+ """
+ return os.environ.get("WERKZEUG_RUN_MAIN") == "true"
+
+
+def run_simple(
+ hostname: str,
+ port: int,
+ application: WSGIApplication,
+ use_reloader: bool = False,
+ use_debugger: bool = False,
+ use_evalex: bool = True,
+ extra_files: t.Iterable[str] | None = None,
+ exclude_patterns: t.Iterable[str] | None = None,
+ reloader_interval: int = 1,
+ reloader_type: str = "auto",
+ threaded: bool = False,
+ processes: int = 1,
+ request_handler: type[WSGIRequestHandler] | None = None,
+ static_files: dict[str, str | tuple[str, str]] | None = None,
+ passthrough_errors: bool = False,
+ ssl_context: _TSSLContextArg | None = None,
+) -> None:
+ """Start a development server for a WSGI application. Various
+ optional features can be enabled.
+
+ .. warning::
+
+ Do not use the development server when deploying to production.
+ It is intended for use only during local development. It is not
+ designed to be particularly efficient, stable, or secure.
+
+ :param hostname: The host to bind to, for example ``'localhost'``.
+ Can be a domain, IPv4 or IPv6 address, or file path starting
+ with ``unix://`` for a Unix socket.
+ :param port: The port to bind to, for example ``8080``. Using ``0``
+ tells the OS to pick a random free port.
+ :param application: The WSGI application to run.
+ :param use_reloader: Use a reloader process to restart the server
+ process when files are changed.
+ :param use_debugger: Use Werkzeug's debugger, which will show
+ formatted tracebacks on unhandled exceptions.
+ :param use_evalex: Make the debugger interactive. A Python terminal
+ can be opened for any frame in the traceback. Some protection is
+ provided by requiring a PIN, but this should never be enabled
+ on a publicly visible server.
+ :param extra_files: The reloader will watch these files for changes
+ in addition to Python modules. For example, watch a
+ configuration file.
+ :param exclude_patterns: The reloader will ignore changes to any
+ files matching these :mod:`fnmatch` patterns. For example,
+ ignore cache files.
+ :param reloader_interval: How often the reloader tries to check for
+ changes.
+ :param reloader_type: The reloader to use. The ``'stat'`` reloader
+ is built in, but may require significant CPU to watch files. The
+ ``'watchdog'`` reloader is much more efficient but requires
+ installing the ``watchdog`` package first.
+ :param threaded: Handle concurrent requests using threads. Cannot be
+ used with ``processes``.
+ :param processes: Handle concurrent requests using up to this number
+ of processes. Cannot be used with ``threaded``.
+ :param request_handler: Use a different
+ :class:`~BaseHTTPServer.BaseHTTPRequestHandler` subclass to
+ handle requests.
+ :param static_files: A dict mapping URL prefixes to directories to
+ serve static files from using
+ :class:`~werkzeug.middleware.SharedDataMiddleware`.
+ :param passthrough_errors: Don't catch unhandled exceptions at the
+ server level, let the server crash instead. If ``use_debugger``
+ is enabled, the debugger will still catch such errors.
+ :param ssl_context: Configure TLS to serve over HTTPS. Can be an
+ :class:`ssl.SSLContext` object, a ``(cert_file, key_file)``
+ tuple to create a typical context, or the string ``'adhoc'`` to
+ generate a temporary self-signed certificate.
+
+ .. versionchanged:: 2.1
+ Instructions are shown for dealing with an "address already in
+ use" error.
+
+ .. versionchanged:: 2.1
+ Running on ``0.0.0.0`` or ``::`` shows the loopback IP in
+ addition to a real IP.
+
+ .. versionchanged:: 2.1
+ The command-line interface was removed.
+
+ .. versionchanged:: 2.0
+ Running on ``0.0.0.0`` or ``::`` shows a real IP address that
+ was bound as well as a warning not to run the development server
+ in production.
+
+ .. versionchanged:: 2.0
+ The ``exclude_patterns`` parameter was added.
+
+ .. versionchanged:: 0.15
+ Bind to a Unix socket by passing a ``hostname`` that starts with
+ ``unix://``.
+
+ .. versionchanged:: 0.10
+ Improved the reloader and added support for changing the backend
+ through the ``reloader_type`` parameter.
+
+ .. versionchanged:: 0.9
+ A command-line interface was added.
+
+ .. versionchanged:: 0.8
+ ``ssl_context`` can be a tuple of paths to the certificate and
+ private key files.
+
+ .. versionchanged:: 0.6
+ The ``ssl_context`` parameter was added.
+
+ .. versionchanged:: 0.5
+ The ``static_files`` and ``passthrough_errors`` parameters were
+ added.
+ """
+ if not isinstance(port, int):
+ raise TypeError("port must be an integer")
+
+ if static_files:
+ from .middleware.shared_data import SharedDataMiddleware
+
+ application = SharedDataMiddleware(application, static_files)
+
+ if use_debugger:
+ from .debug import DebuggedApplication
+
+ application = DebuggedApplication(application, evalex=use_evalex)
+ # Allow the specified hostname to use the debugger, in addition to
+ # localhost domains.
+ application.trusted_hosts.append(hostname)
+
+ if not is_running_from_reloader():
+ fd = None
+ else:
+ fd = int(os.environ["WERKZEUG_SERVER_FD"])
+
+ srv = make_server(
+ hostname,
+ port,
+ application,
+ threaded,
+ processes,
+ request_handler,
+ passthrough_errors,
+ ssl_context,
+ fd=fd,
+ )
+ srv.socket.set_inheritable(True)
+ os.environ["WERKZEUG_SERVER_FD"] = str(srv.fileno())
+
+ if not is_running_from_reloader():
+ srv.log_startup()
+ _log("info", _ansi_style("Press CTRL+C to quit", "yellow"))
+
+ if use_reloader:
+ from ._reloader import run_with_reloader
+
+ try:
+ run_with_reloader(
+ srv.serve_forever,
+ extra_files=extra_files,
+ exclude_patterns=exclude_patterns,
+ interval=reloader_interval,
+ reloader_type=reloader_type,
+ )
+ finally:
+ srv.server_close()
+ else:
+ srv.serve_forever()
diff --git a/venv/Lib/site-packages/werkzeug/test.py b/venv/Lib/site-packages/werkzeug/test.py
new file mode 100644
index 0000000..58b4ce7
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/test.py
@@ -0,0 +1,1464 @@
+from __future__ import annotations
+
+import dataclasses
+import mimetypes
+import sys
+import typing as t
+from collections import defaultdict
+from datetime import datetime
+from io import BytesIO
+from itertools import chain
+from random import random
+from tempfile import TemporaryFile
+from time import time
+from urllib.parse import unquote
+from urllib.parse import urlsplit
+from urllib.parse import urlunsplit
+
+from ._internal import _get_environ
+from ._internal import _wsgi_decoding_dance
+from ._internal import _wsgi_encoding_dance
+from .datastructures import Authorization
+from .datastructures import CallbackDict
+from .datastructures import CombinedMultiDict
+from .datastructures import EnvironHeaders
+from .datastructures import FileMultiDict
+from .datastructures import Headers
+from .datastructures import MultiDict
+from .http import dump_cookie
+from .http import dump_options_header
+from .http import parse_cookie
+from .http import parse_date
+from .http import parse_options_header
+from .sansio.multipart import Data
+from .sansio.multipart import Epilogue
+from .sansio.multipart import Field
+from .sansio.multipart import File
+from .sansio.multipart import MultipartEncoder
+from .sansio.multipart import Preamble
+from .urls import _urlencode
+from .urls import iri_to_uri
+from .utils import cached_property
+from .utils import get_content_type
+from .wrappers.request import Request
+from .wrappers.response import Response
+from .wsgi import ClosingIterator
+from .wsgi import get_current_url
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+ from _typeshed.wsgi import WSGIApplication
+ from _typeshed.wsgi import WSGIEnvironment
+
+
+def stream_encode_multipart(
+ data: t.Mapping[str, t.Any],
+ use_tempfile: bool = True,
+ threshold: int = 1024 * 500,
+ boundary: str | None = None,
+) -> tuple[t.IO[bytes], int, str]:
+ """Encode a dict of values (either strings or file descriptors or
+ :class:`FileStorage` objects.) into a multipart encoded string stored
+ in a file descriptor.
+
+ .. versionchanged:: 3.0
+ The ``charset`` parameter was removed.
+ """
+ if boundary is None:
+ boundary = f"---------------WerkzeugFormPart_{time()}{random()}"
+
+ stream: t.IO[bytes] = BytesIO()
+ total_length = 0
+ on_disk = False
+ write_binary: t.Callable[[bytes], int]
+
+ if use_tempfile:
+
+ def write_binary(s: bytes) -> int:
+ nonlocal stream, total_length, on_disk
+
+ if on_disk:
+ return stream.write(s)
+ else:
+ length = len(s)
+
+ if length + total_length <= threshold:
+ stream.write(s)
+ else:
+ new_stream = t.cast(t.IO[bytes], TemporaryFile("wb+"))
+ new_stream.write(stream.getvalue()) # type: ignore
+ new_stream.write(s)
+ stream = new_stream
+ on_disk = True
+
+ total_length += length
+ return length
+
+ else:
+ write_binary = stream.write
+
+ encoder = MultipartEncoder(boundary.encode())
+ write_binary(encoder.send_event(Preamble(data=b"")))
+ for key, value in _iter_data(data):
+ reader = getattr(value, "read", None)
+ if reader is not None:
+ filename = getattr(value, "filename", getattr(value, "name", None))
+ content_type = getattr(value, "content_type", None)
+ if content_type is None:
+ content_type = (
+ filename
+ and mimetypes.guess_type(filename)[0]
+ or "application/octet-stream"
+ )
+ headers = value.headers
+ headers.update([("Content-Type", content_type)])
+ if filename is None:
+ write_binary(encoder.send_event(Field(name=key, headers=headers)))
+ else:
+ write_binary(
+ encoder.send_event(
+ File(name=key, filename=filename, headers=headers)
+ )
+ )
+ while True:
+ chunk = reader(16384)
+
+ if not chunk:
+ write_binary(encoder.send_event(Data(data=chunk, more_data=False)))
+ break
+
+ write_binary(encoder.send_event(Data(data=chunk, more_data=True)))
+ else:
+ if not isinstance(value, str):
+ value = str(value)
+ write_binary(encoder.send_event(Field(name=key, headers=Headers())))
+ write_binary(encoder.send_event(Data(data=value.encode(), more_data=False)))
+
+ write_binary(encoder.send_event(Epilogue(data=b"")))
+
+ length = stream.tell()
+ stream.seek(0)
+ return stream, length, boundary
+
+
+def encode_multipart(
+ values: t.Mapping[str, t.Any], boundary: str | None = None
+) -> tuple[str, bytes]:
+ """Like `stream_encode_multipart` but returns a tuple in the form
+ (``boundary``, ``data``) where data is bytes.
+
+ .. versionchanged:: 3.0
+ The ``charset`` parameter was removed.
+ """
+ stream, length, boundary = stream_encode_multipart(
+ values, use_tempfile=False, boundary=boundary
+ )
+ return boundary, stream.read()
+
+
+def _iter_data(data: t.Mapping[str, t.Any]) -> t.Iterator[tuple[str, t.Any]]:
+ """Iterate over a mapping that might have a list of values, yielding
+ all key, value pairs. Almost like iter_multi_items but only allows
+ lists, not tuples, of values so tuples can be used for files.
+ """
+ if isinstance(data, MultiDict):
+ yield from data.items(multi=True)
+ else:
+ for key, value in data.items():
+ if isinstance(value, list):
+ for v in value:
+ yield key, v
+ else:
+ yield key, value
+
+
+_TAnyMultiDict = t.TypeVar("_TAnyMultiDict", bound="MultiDict[t.Any, t.Any]")
+
+
+class EnvironBuilder:
+ """This class can be used to conveniently create a WSGI environment
+ for testing purposes. It can be used to quickly create WSGI environments
+ or request objects from arbitrary data.
+
+ The signature of this class is also used in some other places as of
+ Werkzeug 0.5 (:func:`create_environ`, :meth:`Response.from_values`,
+ :meth:`Client.open`). Because of this most of the functionality is
+ available through the constructor alone.
+
+ Files and regular form data can be manipulated independently of each
+ other with the :attr:`form` and :attr:`files` attributes, but are
+ passed with the same argument to the constructor: `data`.
+
+ `data` can be any of these values:
+
+ - a `str` or `bytes` object: The object is converted into an
+ :attr:`input_stream`, the :attr:`content_length` is set and you have to
+ provide a :attr:`content_type`.
+ - a `dict` or :class:`MultiDict`: The keys have to be strings. The values
+ have to be either any of the following objects, or a list of any of the
+ following objects:
+
+ - a :class:`file`-like object: These are converted into
+ :class:`FileStorage` objects automatically.
+ - a `tuple`: The :meth:`~FileMultiDict.add_file` method is called
+ with the key and the unpacked `tuple` items as positional
+ arguments.
+ - a `str`: The string is set as form data for the associated key.
+ - a file-like object: The object content is loaded in memory and then
+ handled like a regular `str` or a `bytes`.
+
+ :param path: the path of the request. In the WSGI environment this will
+ end up as `PATH_INFO`. If the `query_string` is not defined
+ and there is a question mark in the `path` everything after
+ it is used as query string.
+ :param base_url: the base URL is a URL that is used to extract the WSGI
+ URL scheme, host (server name + server port) and the
+ script root (`SCRIPT_NAME`).
+ :param query_string: an optional string or dict with URL parameters.
+ :param method: the HTTP method to use, defaults to `GET`.
+ :param input_stream: an optional input stream. Do not specify this and
+ `data`. As soon as an input stream is set you can't
+ modify :attr:`args` and :attr:`files` unless you
+ set the :attr:`input_stream` to `None` again.
+ :param content_type: The content type for the request. As of 0.5 you
+ don't have to provide this when specifying files
+ and form data via `data`.
+ :param content_length: The content length for the request. You don't
+ have to specify this when providing data via
+ `data`.
+ :param errors_stream: an optional error stream that is used for
+ `wsgi.errors`. Defaults to :data:`stderr`.
+ :param multithread: controls `wsgi.multithread`. Defaults to `False`.
+ :param multiprocess: controls `wsgi.multiprocess`. Defaults to `False`.
+ :param run_once: controls `wsgi.run_once`. Defaults to `False`.
+ :param headers: an optional list or :class:`Headers` object of headers.
+ :param data: a string or dict of form data or a file-object.
+ See explanation above.
+ :param json: An object to be serialized and assigned to ``data``.
+ Defaults the content type to ``"application/json"``.
+ Serialized with the function assigned to :attr:`json_dumps`.
+ :param environ_base: an optional dict of environment defaults.
+ :param environ_overrides: an optional dict of environment overrides.
+ :param auth: An authorization object to use for the
+ ``Authorization`` header value. A ``(username, password)`` tuple
+ is a shortcut for ``Basic`` authorization.
+
+ .. versionchanged:: 3.0
+ The ``charset`` parameter was removed.
+
+ .. versionchanged:: 2.1
+ ``CONTENT_TYPE`` and ``CONTENT_LENGTH`` are not duplicated as
+ header keys in the environ.
+
+ .. versionchanged:: 2.0
+ ``REQUEST_URI`` and ``RAW_URI`` is the full raw URI including
+ the query string, not only the path.
+
+ .. versionchanged:: 2.0
+ The default :attr:`request_class` is ``Request`` instead of
+ ``BaseRequest``.
+
+ .. versionadded:: 2.0
+ Added the ``auth`` parameter.
+
+ .. versionadded:: 0.15
+ The ``json`` param and :meth:`json_dumps` method.
+
+ .. versionadded:: 0.15
+ The environ has keys ``REQUEST_URI`` and ``RAW_URI`` containing
+ the path before percent-decoding. This is not part of the WSGI
+ PEP, but many WSGI servers include it.
+
+ .. versionchanged:: 0.6
+ ``path`` and ``base_url`` can now be unicode strings that are
+ encoded with :func:`iri_to_uri`.
+ """
+
+ #: the server protocol to use. defaults to HTTP/1.1
+ server_protocol = "HTTP/1.1"
+
+ #: the wsgi version to use. defaults to (1, 0)
+ wsgi_version = (1, 0)
+
+ #: The default request class used by :meth:`get_request`.
+ request_class = Request
+
+ import json
+
+ #: The serialization function used when ``json`` is passed.
+ json_dumps = staticmethod(json.dumps)
+ del json
+
+ _args: MultiDict[str, str] | None
+ _query_string: str | None
+ _input_stream: t.IO[bytes] | None
+ _form: MultiDict[str, str] | None
+ _files: FileMultiDict | None
+
+ def __init__(
+ self,
+ path: str = "/",
+ base_url: str | None = None,
+ query_string: t.Mapping[str, str] | str | None = None,
+ method: str = "GET",
+ input_stream: t.IO[bytes] | None = None,
+ content_type: str | None = None,
+ content_length: int | None = None,
+ errors_stream: t.IO[str] | None = None,
+ multithread: bool = False,
+ multiprocess: bool = False,
+ run_once: bool = False,
+ headers: Headers | t.Iterable[tuple[str, str]] | None = None,
+ data: None | (t.IO[bytes] | str | bytes | t.Mapping[str, t.Any]) = None,
+ environ_base: t.Mapping[str, t.Any] | None = None,
+ environ_overrides: t.Mapping[str, t.Any] | None = None,
+ mimetype: str | None = None,
+ json: t.Mapping[str, t.Any] | None = None,
+ auth: Authorization | tuple[str, str] | None = None,
+ ) -> None:
+ if query_string is not None and "?" in path:
+ raise ValueError("Query string is defined in the path and as an argument")
+ request_uri = urlsplit(path)
+ if query_string is None and "?" in path:
+ query_string = request_uri.query
+
+ self.path = iri_to_uri(request_uri.path)
+ self.request_uri = path
+ if base_url is not None:
+ base_url = iri_to_uri(base_url)
+ self.base_url = base_url
+ if isinstance(query_string, str):
+ self.query_string = query_string
+ else:
+ if query_string is None:
+ query_string = MultiDict()
+ elif not isinstance(query_string, MultiDict):
+ query_string = MultiDict(query_string)
+ self.args = query_string
+ self.method = method
+ if headers is None:
+ headers = Headers()
+ elif not isinstance(headers, Headers):
+ headers = Headers(headers)
+ self.headers = headers
+ if content_type is not None:
+ self.content_type = content_type
+ if errors_stream is None:
+ errors_stream = sys.stderr
+ self.errors_stream = errors_stream
+ self.multithread = multithread
+ self.multiprocess = multiprocess
+ self.run_once = run_once
+ self.environ_base = environ_base
+ self.environ_overrides = environ_overrides
+ self.input_stream = input_stream
+ self.content_length = content_length
+ self.closed = False
+
+ if auth is not None:
+ if isinstance(auth, tuple):
+ auth = Authorization(
+ "basic", {"username": auth[0], "password": auth[1]}
+ )
+
+ self.headers.set("Authorization", auth.to_header())
+
+ if json is not None:
+ if data is not None:
+ raise TypeError("can't provide both json and data")
+
+ data = self.json_dumps(json)
+
+ if self.content_type is None:
+ self.content_type = "application/json"
+
+ if data:
+ if input_stream is not None:
+ raise TypeError("can't provide input stream and data")
+ if hasattr(data, "read"):
+ data = data.read()
+ if isinstance(data, str):
+ data = data.encode()
+ if isinstance(data, bytes):
+ self.input_stream = BytesIO(data)
+ if self.content_length is None:
+ self.content_length = len(data)
+ else:
+ for key, value in _iter_data(data):
+ if isinstance(value, (tuple, dict)) or hasattr(value, "read"):
+ self._add_file_from_data(key, value)
+ else:
+ self.form.setlistdefault(key).append(value)
+
+ if mimetype is not None:
+ self.mimetype = mimetype
+
+ @classmethod
+ def from_environ(cls, environ: WSGIEnvironment, **kwargs: t.Any) -> EnvironBuilder:
+ """Turn an environ dict back into a builder. Any extra kwargs
+ override the args extracted from the environ.
+
+ .. versionchanged:: 2.0
+ Path and query values are passed through the WSGI decoding
+ dance to avoid double encoding.
+
+ .. versionadded:: 0.15
+ """
+ headers = Headers(EnvironHeaders(environ))
+ out = {
+ "path": _wsgi_decoding_dance(environ["PATH_INFO"]),
+ "base_url": cls._make_base_url(
+ environ["wsgi.url_scheme"],
+ headers.pop("Host"),
+ _wsgi_decoding_dance(environ["SCRIPT_NAME"]),
+ ),
+ "query_string": _wsgi_decoding_dance(environ["QUERY_STRING"]),
+ "method": environ["REQUEST_METHOD"],
+ "input_stream": environ["wsgi.input"],
+ "content_type": headers.pop("Content-Type", None),
+ "content_length": headers.pop("Content-Length", None),
+ "errors_stream": environ["wsgi.errors"],
+ "multithread": environ["wsgi.multithread"],
+ "multiprocess": environ["wsgi.multiprocess"],
+ "run_once": environ["wsgi.run_once"],
+ "headers": headers,
+ }
+ out.update(kwargs)
+ return cls(**out)
+
+ def _add_file_from_data(
+ self,
+ key: str,
+ value: (t.IO[bytes] | tuple[t.IO[bytes], str] | tuple[t.IO[bytes], str, str]),
+ ) -> None:
+ """Called in the EnvironBuilder to add files from the data dict."""
+ if isinstance(value, tuple):
+ self.files.add_file(key, *value)
+ else:
+ self.files.add_file(key, value)
+
+ @staticmethod
+ def _make_base_url(scheme: str, host: str, script_root: str) -> str:
+ return urlunsplit((scheme, host, script_root, "", "")).rstrip("/") + "/"
+
+ @property
+ def base_url(self) -> str:
+ """The base URL is used to extract the URL scheme, host name,
+ port, and root path.
+ """
+ return self._make_base_url(self.url_scheme, self.host, self.script_root)
+
+ @base_url.setter
+ def base_url(self, value: str | None) -> None:
+ if value is None:
+ scheme = "http"
+ netloc = "localhost"
+ script_root = ""
+ else:
+ scheme, netloc, script_root, qs, anchor = urlsplit(value)
+ if qs or anchor:
+ raise ValueError("base url must not contain a query string or fragment")
+ self.script_root = script_root.rstrip("/")
+ self.host = netloc
+ self.url_scheme = scheme
+
+ @property
+ def content_type(self) -> str | None:
+ """The content type for the request. Reflected from and to
+ the :attr:`headers`. Do not set if you set :attr:`files` or
+ :attr:`form` for auto detection.
+ """
+ ct = self.headers.get("Content-Type")
+ if ct is None and not self._input_stream:
+ if self._files:
+ return "multipart/form-data"
+ if self._form:
+ return "application/x-www-form-urlencoded"
+ return None
+ return ct
+
+ @content_type.setter
+ def content_type(self, value: str | None) -> None:
+ if value is None:
+ self.headers.pop("Content-Type", None)
+ else:
+ self.headers["Content-Type"] = value
+
+ @property
+ def mimetype(self) -> str | None:
+ """The mimetype (content type without charset etc.)
+
+ .. versionadded:: 0.14
+ """
+ ct = self.content_type
+ return ct.split(";")[0].strip() if ct else None
+
+ @mimetype.setter
+ def mimetype(self, value: str) -> None:
+ self.content_type = get_content_type(value, "utf-8")
+
+ @property
+ def mimetype_params(self) -> t.Mapping[str, str]:
+ """The mimetype parameters as dict. For example if the
+ content type is ``text/html; charset=utf-8`` the params would be
+ ``{'charset': 'utf-8'}``.
+
+ .. versionadded:: 0.14
+ """
+
+ def on_update(d: CallbackDict[str, str]) -> None:
+ self.headers["Content-Type"] = dump_options_header(self.mimetype, d)
+
+ d = parse_options_header(self.headers.get("content-type", ""))[1]
+ return CallbackDict(d, on_update)
+
+ @property
+ def content_length(self) -> int | None:
+ """The content length as integer. Reflected from and to the
+ :attr:`headers`. Do not set if you set :attr:`files` or
+ :attr:`form` for auto detection.
+ """
+ return self.headers.get("Content-Length", type=int)
+
+ @content_length.setter
+ def content_length(self, value: int | None) -> None:
+ if value is None:
+ self.headers.pop("Content-Length", None)
+ else:
+ self.headers["Content-Length"] = str(value)
+
+ def _get_form(self, name: str, storage: type[_TAnyMultiDict]) -> _TAnyMultiDict:
+ """Common behavior for getting the :attr:`form` and
+ :attr:`files` properties.
+
+ :param name: Name of the internal cached attribute.
+ :param storage: Storage class used for the data.
+ """
+ if self.input_stream is not None:
+ raise AttributeError("an input stream is defined")
+
+ rv = getattr(self, name)
+
+ if rv is None:
+ rv = storage()
+ setattr(self, name, rv)
+
+ return rv # type: ignore
+
+ def _set_form(self, name: str, value: MultiDict[str, t.Any]) -> None:
+ """Common behavior for setting the :attr:`form` and
+ :attr:`files` properties.
+
+ :param name: Name of the internal cached attribute.
+ :param value: Value to assign to the attribute.
+ """
+ self._input_stream = None
+ setattr(self, name, value)
+
+ @property
+ def form(self) -> MultiDict[str, str]:
+ """A :class:`MultiDict` of form values."""
+ return self._get_form("_form", MultiDict)
+
+ @form.setter
+ def form(self, value: MultiDict[str, str]) -> None:
+ self._set_form("_form", value)
+
+ @property
+ def files(self) -> FileMultiDict:
+ """A :class:`FileMultiDict` of uploaded files. Use
+ :meth:`~FileMultiDict.add_file` to add new files.
+ """
+ return self._get_form("_files", FileMultiDict)
+
+ @files.setter
+ def files(self, value: FileMultiDict) -> None:
+ self._set_form("_files", value)
+
+ @property
+ def input_stream(self) -> t.IO[bytes] | None:
+ """An optional input stream. This is mutually exclusive with
+ setting :attr:`form` and :attr:`files`, setting it will clear
+ those. Do not provide this if the method is not ``POST`` or
+ another method that has a body.
+ """
+ return self._input_stream
+
+ @input_stream.setter
+ def input_stream(self, value: t.IO[bytes] | None) -> None:
+ self._input_stream = value
+ self._form = None
+ self._files = None
+
+ @property
+ def query_string(self) -> str:
+ """The query string. If you set this to a string
+ :attr:`args` will no longer be available.
+ """
+ if self._query_string is None:
+ if self._args is not None:
+ return _urlencode(self._args)
+ return ""
+ return self._query_string
+
+ @query_string.setter
+ def query_string(self, value: str | None) -> None:
+ self._query_string = value
+ self._args = None
+
+ @property
+ def args(self) -> MultiDict[str, str]:
+ """The URL arguments as :class:`MultiDict`."""
+ if self._query_string is not None:
+ raise AttributeError("a query string is defined")
+ if self._args is None:
+ self._args = MultiDict()
+ return self._args
+
+ @args.setter
+ def args(self, value: MultiDict[str, str] | None) -> None:
+ self._query_string = None
+ self._args = value
+
+ @property
+ def server_name(self) -> str:
+ """The server name (read-only, use :attr:`host` to set)"""
+ return self.host.split(":", 1)[0]
+
+ @property
+ def server_port(self) -> int:
+ """The server port as integer (read-only, use :attr:`host` to set)"""
+ pieces = self.host.split(":", 1)
+
+ if len(pieces) == 2:
+ try:
+ return int(pieces[1])
+ except ValueError:
+ pass
+
+ if self.url_scheme == "https":
+ return 443
+ return 80
+
+ def __del__(self) -> None:
+ try:
+ self.close()
+ except Exception:
+ pass
+
+ def close(self) -> None:
+ """Closes all files. If you put real :class:`file` objects into the
+ :attr:`files` dict you can call this method to automatically close
+ them all in one go.
+ """
+ if self.closed:
+ return
+ try:
+ files = self.files.values()
+ except AttributeError:
+ files = ()
+ for f in files:
+ try:
+ f.close()
+ except Exception:
+ pass
+ self.closed = True
+
+ def get_environ(self) -> WSGIEnvironment:
+ """Return the built environ.
+
+ .. versionchanged:: 0.15
+ The content type and length headers are set based on
+ input stream detection. Previously this only set the WSGI
+ keys.
+ """
+ input_stream = self.input_stream
+ content_length = self.content_length
+
+ mimetype = self.mimetype
+ content_type = self.content_type
+
+ if input_stream is not None:
+ start_pos = input_stream.tell()
+ input_stream.seek(0, 2)
+ end_pos = input_stream.tell()
+ input_stream.seek(start_pos)
+ content_length = end_pos - start_pos
+ elif mimetype == "multipart/form-data":
+ input_stream, content_length, boundary = stream_encode_multipart(
+ CombinedMultiDict([self.form, self.files])
+ )
+ content_type = f'{mimetype}; boundary="{boundary}"'
+ elif mimetype == "application/x-www-form-urlencoded":
+ form_encoded = _urlencode(self.form).encode("ascii")
+ content_length = len(form_encoded)
+ input_stream = BytesIO(form_encoded)
+ else:
+ input_stream = BytesIO()
+
+ result: WSGIEnvironment = {}
+ if self.environ_base:
+ result.update(self.environ_base)
+
+ def _path_encode(x: str) -> str:
+ return _wsgi_encoding_dance(unquote(x))
+
+ raw_uri = _wsgi_encoding_dance(self.request_uri)
+ result.update(
+ {
+ "REQUEST_METHOD": self.method,
+ "SCRIPT_NAME": _path_encode(self.script_root),
+ "PATH_INFO": _path_encode(self.path),
+ "QUERY_STRING": _wsgi_encoding_dance(self.query_string),
+ # Non-standard, added by mod_wsgi, uWSGI
+ "REQUEST_URI": raw_uri,
+ # Non-standard, added by gunicorn
+ "RAW_URI": raw_uri,
+ "SERVER_NAME": self.server_name,
+ "SERVER_PORT": str(self.server_port),
+ "HTTP_HOST": self.host,
+ "SERVER_PROTOCOL": self.server_protocol,
+ "wsgi.version": self.wsgi_version,
+ "wsgi.url_scheme": self.url_scheme,
+ "wsgi.input": input_stream,
+ "wsgi.errors": self.errors_stream,
+ "wsgi.multithread": self.multithread,
+ "wsgi.multiprocess": self.multiprocess,
+ "wsgi.run_once": self.run_once,
+ }
+ )
+
+ headers = self.headers.copy()
+ # Don't send these as headers, they're part of the environ.
+ headers.remove("Content-Type")
+ headers.remove("Content-Length")
+
+ if content_type is not None:
+ result["CONTENT_TYPE"] = content_type
+
+ if content_length is not None:
+ result["CONTENT_LENGTH"] = str(content_length)
+
+ combined_headers = defaultdict(list)
+
+ for key, value in headers.to_wsgi_list():
+ combined_headers[f"HTTP_{key.upper().replace('-', '_')}"].append(value)
+
+ for key, values in combined_headers.items():
+ result[key] = ", ".join(values)
+
+ if self.environ_overrides:
+ result.update(self.environ_overrides)
+
+ return result
+
+ def get_request(self, cls: type[Request] | None = None) -> Request:
+ """Returns a request with the data. If the request class is not
+ specified :attr:`request_class` is used.
+
+ :param cls: The request wrapper to use.
+ """
+ if cls is None:
+ cls = self.request_class
+
+ return cls(self.get_environ())
+
+
+class ClientRedirectError(Exception):
+ """If a redirect loop is detected when using follow_redirects=True with
+ the :cls:`Client`, then this exception is raised.
+ """
+
+
+class Client:
+ """Simulate sending requests to a WSGI application without running a WSGI or HTTP
+ server.
+
+ :param application: The WSGI application to make requests to.
+ :param response_wrapper: A :class:`.Response` class to wrap response data with.
+ Defaults to :class:`.TestResponse`. If it's not a subclass of ``TestResponse``,
+ one will be created.
+ :param use_cookies: Persist cookies from ``Set-Cookie`` response headers to the
+ ``Cookie`` header in subsequent requests. Domain and path matching is supported,
+ but other cookie parameters are ignored.
+ :param allow_subdomain_redirects: Allow requests to follow redirects to subdomains.
+ Enable this if the application handles subdomains and redirects between them.
+
+ .. versionchanged:: 2.3
+ Simplify cookie implementation, support domain and path matching.
+
+ .. versionchanged:: 2.1
+ All data is available as properties on the returned response object. The
+ response cannot be returned as a tuple.
+
+ .. versionchanged:: 2.0
+ ``response_wrapper`` is always a subclass of :class:``TestResponse``.
+
+ .. versionchanged:: 0.5
+ Added the ``use_cookies`` parameter.
+ """
+
+ def __init__(
+ self,
+ application: WSGIApplication,
+ response_wrapper: type[Response] | None = None,
+ use_cookies: bool = True,
+ allow_subdomain_redirects: bool = False,
+ ) -> None:
+ self.application = application
+
+ if response_wrapper in {None, Response}:
+ response_wrapper = TestResponse
+ elif response_wrapper is not None and not issubclass(
+ response_wrapper, TestResponse
+ ):
+ response_wrapper = type(
+ "WrapperTestResponse",
+ (TestResponse, response_wrapper),
+ {},
+ )
+
+ self.response_wrapper = t.cast(type["TestResponse"], response_wrapper)
+
+ if use_cookies:
+ self._cookies: dict[tuple[str, str, str], Cookie] | None = {}
+ else:
+ self._cookies = None
+
+ self.allow_subdomain_redirects = allow_subdomain_redirects
+
+ def get_cookie(
+ self, key: str, domain: str = "localhost", path: str = "/"
+ ) -> Cookie | None:
+ """Return a :class:`.Cookie` if it exists. Cookies are uniquely identified by
+ ``(domain, path, key)``.
+
+ :param key: The decoded form of the key for the cookie.
+ :param domain: The domain the cookie was set for.
+ :param path: The path the cookie was set for.
+
+ .. versionadded:: 2.3
+ """
+ if self._cookies is None:
+ raise TypeError(
+ "Cookies are disabled. Create a client with 'use_cookies=True'."
+ )
+
+ return self._cookies.get((domain, path, key))
+
+ def set_cookie(
+ self,
+ key: str,
+ value: str = "",
+ *,
+ domain: str = "localhost",
+ origin_only: bool = True,
+ path: str = "/",
+ **kwargs: t.Any,
+ ) -> None:
+ """Set a cookie to be sent in subsequent requests.
+
+ This is a convenience to skip making a test request to a route that would set
+ the cookie. To test the cookie, make a test request to a route that uses the
+ cookie value.
+
+ The client uses ``domain``, ``origin_only``, and ``path`` to determine which
+ cookies to send with a request. It does not use other cookie parameters that
+ browsers use, since they're not applicable in tests.
+
+ :param key: The key part of the cookie.
+ :param value: The value part of the cookie.
+ :param domain: Send this cookie with requests that match this domain. If
+ ``origin_only`` is true, it must be an exact match, otherwise it may be a
+ suffix match.
+ :param origin_only: Whether the domain must be an exact match to the request.
+ :param path: Send this cookie with requests that match this path either exactly
+ or as a prefix.
+ :param kwargs: Passed to :func:`.dump_cookie`.
+
+ .. versionchanged:: 3.0
+ The parameter ``server_name`` is removed. The first parameter is
+ ``key``. Use the ``domain`` and ``origin_only`` parameters instead.
+
+ .. versionchanged:: 2.3
+ The ``origin_only`` parameter was added.
+
+ .. versionchanged:: 2.3
+ The ``domain`` parameter defaults to ``localhost``.
+ """
+ if self._cookies is None:
+ raise TypeError(
+ "Cookies are disabled. Create a client with 'use_cookies=True'."
+ )
+
+ cookie = Cookie._from_response_header(
+ domain, "/", dump_cookie(key, value, domain=domain, path=path, **kwargs)
+ )
+ cookie.origin_only = origin_only
+
+ if cookie._should_delete:
+ self._cookies.pop(cookie._storage_key, None)
+ else:
+ self._cookies[cookie._storage_key] = cookie
+
+ def delete_cookie(
+ self,
+ key: str,
+ *,
+ domain: str = "localhost",
+ path: str = "/",
+ ) -> None:
+ """Delete a cookie if it exists. Cookies are uniquely identified by
+ ``(domain, path, key)``.
+
+ :param key: The decoded form of the key for the cookie.
+ :param domain: The domain the cookie was set for.
+ :param path: The path the cookie was set for.
+
+ .. versionchanged:: 3.0
+ The ``server_name`` parameter is removed. The first parameter is
+ ``key``. Use the ``domain`` parameter instead.
+
+ .. versionchanged:: 3.0
+ The ``secure``, ``httponly`` and ``samesite`` parameters are removed.
+
+ .. versionchanged:: 2.3
+ The ``domain`` parameter defaults to ``localhost``.
+ """
+ if self._cookies is None:
+ raise TypeError(
+ "Cookies are disabled. Create a client with 'use_cookies=True'."
+ )
+
+ self._cookies.pop((domain, path, key), None)
+
+ def _add_cookies_to_wsgi(self, environ: WSGIEnvironment) -> None:
+ """If cookies are enabled, set the ``Cookie`` header in the environ to the
+ cookies that are applicable to the request host and path.
+
+ :meta private:
+
+ .. versionadded:: 2.3
+ """
+ if self._cookies is None:
+ return
+
+ url = urlsplit(get_current_url(environ))
+ server_name = url.hostname or "localhost"
+ value = "; ".join(
+ c._to_request_header()
+ for c in self._cookies.values()
+ if c._matches_request(server_name, url.path)
+ )
+
+ if value:
+ environ["HTTP_COOKIE"] = value
+ else:
+ environ.pop("HTTP_COOKIE", None)
+
+ def _update_cookies_from_response(
+ self, server_name: str, path: str, headers: list[str]
+ ) -> None:
+ """If cookies are enabled, update the stored cookies from any ``Set-Cookie``
+ headers in the response.
+
+ :meta private:
+
+ .. versionadded:: 2.3
+ """
+ if self._cookies is None:
+ return
+
+ for header in headers:
+ cookie = Cookie._from_response_header(server_name, path, header)
+
+ if cookie._should_delete:
+ self._cookies.pop(cookie._storage_key, None)
+ else:
+ self._cookies[cookie._storage_key] = cookie
+
+ def run_wsgi_app(
+ self, environ: WSGIEnvironment, buffered: bool = False
+ ) -> tuple[t.Iterable[bytes], str, Headers]:
+ """Runs the wrapped WSGI app with the given environment.
+
+ :meta private:
+ """
+ self._add_cookies_to_wsgi(environ)
+ rv = run_wsgi_app(self.application, environ, buffered=buffered)
+ url = urlsplit(get_current_url(environ))
+ self._update_cookies_from_response(
+ url.hostname or "localhost", url.path, rv[2].getlist("Set-Cookie")
+ )
+ return rv
+
+ def resolve_redirect(
+ self, response: TestResponse, buffered: bool = False
+ ) -> TestResponse:
+ """Perform a new request to the location given by the redirect
+ response to the previous request.
+
+ :meta private:
+ """
+ scheme, netloc, path, qs, anchor = urlsplit(response.location)
+ builder = EnvironBuilder.from_environ(
+ response.request.environ, path=path, query_string=qs
+ )
+
+ to_name_parts = netloc.split(":", 1)[0].split(".")
+ from_name_parts = builder.server_name.split(".")
+
+ if to_name_parts != [""]:
+ # The new location has a host, use it for the base URL.
+ builder.url_scheme = scheme
+ builder.host = netloc
+ else:
+ # A local redirect with autocorrect_location_header=False
+ # doesn't have a host, so use the request's host.
+ to_name_parts = from_name_parts
+
+ # Explain why a redirect to a different server name won't be followed.
+ if to_name_parts != from_name_parts:
+ if to_name_parts[-len(from_name_parts) :] == from_name_parts:
+ if not self.allow_subdomain_redirects:
+ raise RuntimeError("Following subdomain redirects is not enabled.")
+ else:
+ raise RuntimeError("Following external redirects is not supported.")
+
+ path_parts = path.split("/")
+ root_parts = builder.script_root.split("/")
+
+ if path_parts[: len(root_parts)] == root_parts:
+ # Strip the script root from the path.
+ builder.path = path[len(builder.script_root) :]
+ else:
+ # The new location is not under the script root, so use the
+ # whole path and clear the previous root.
+ builder.path = path
+ builder.script_root = ""
+
+ # Only 307 and 308 preserve all of the original request.
+ if response.status_code not in {307, 308}:
+ # HEAD is preserved, everything else becomes GET.
+ if builder.method != "HEAD":
+ builder.method = "GET"
+
+ # Clear the body and the headers that describe it.
+
+ if builder.input_stream is not None:
+ builder.input_stream.close()
+ builder.input_stream = None
+
+ builder.content_type = None
+ builder.content_length = None
+ builder.headers.pop("Transfer-Encoding", None)
+
+ return self.open(builder, buffered=buffered)
+
+ def open(
+ self,
+ *args: t.Any,
+ buffered: bool = False,
+ follow_redirects: bool = False,
+ **kwargs: t.Any,
+ ) -> TestResponse:
+ """Generate an environ dict from the given arguments, make a
+ request to the application using it, and return the response.
+
+ :param args: Passed to :class:`EnvironBuilder` to create the
+ environ for the request. If a single arg is passed, it can
+ be an existing :class:`EnvironBuilder` or an environ dict.
+ :param buffered: Convert the iterator returned by the app into
+ a list. If the iterator has a ``close()`` method, it is
+ called automatically.
+ :param follow_redirects: Make additional requests to follow HTTP
+ redirects until a non-redirect status is returned.
+ :attr:`TestResponse.history` lists the intermediate
+ responses.
+
+ .. versionchanged:: 2.1
+ Removed the ``as_tuple`` parameter.
+
+ .. versionchanged:: 2.0
+ The request input stream is closed when calling
+ ``response.close()``. Input streams for redirects are
+ automatically closed.
+
+ .. versionchanged:: 0.5
+ If a dict is provided as file in the dict for the ``data``
+ parameter the content type has to be called ``content_type``
+ instead of ``mimetype``. This change was made for
+ consistency with :class:`werkzeug.FileWrapper`.
+
+ .. versionchanged:: 0.5
+ Added the ``follow_redirects`` parameter.
+ """
+ request: Request | None = None
+
+ if not kwargs and len(args) == 1:
+ arg = args[0]
+
+ if isinstance(arg, EnvironBuilder):
+ request = arg.get_request()
+ elif isinstance(arg, dict):
+ request = EnvironBuilder.from_environ(arg).get_request()
+ elif isinstance(arg, Request):
+ request = arg
+
+ if request is None:
+ builder = EnvironBuilder(*args, **kwargs)
+
+ try:
+ request = builder.get_request()
+ finally:
+ builder.close()
+
+ response_parts = self.run_wsgi_app(request.environ, buffered=buffered)
+ response = self.response_wrapper(*response_parts, request=request)
+
+ redirects = set()
+ history: list[TestResponse] = []
+
+ if not follow_redirects:
+ return response
+
+ while response.status_code in {
+ 301,
+ 302,
+ 303,
+ 305,
+ 307,
+ 308,
+ }:
+ # Exhaust intermediate response bodies to ensure middleware
+ # that returns an iterator runs any cleanup code.
+ if not buffered:
+ response.make_sequence()
+ response.close()
+
+ new_redirect_entry = (response.location, response.status_code)
+
+ if new_redirect_entry in redirects:
+ raise ClientRedirectError(
+ f"Loop detected: A {response.status_code} redirect"
+ f" to {response.location} was already made."
+ )
+
+ redirects.add(new_redirect_entry)
+ response.history = tuple(history)
+ history.append(response)
+ response = self.resolve_redirect(response, buffered=buffered)
+ else:
+ # This is the final request after redirects.
+ response.history = tuple(history)
+ # Close the input stream when closing the response, in case
+ # the input is an open temporary file.
+ response.call_on_close(request.input_stream.close)
+ return response
+
+ def get(self, *args: t.Any, **kw: t.Any) -> TestResponse:
+ """Call :meth:`open` with ``method`` set to ``GET``."""
+ kw["method"] = "GET"
+ return self.open(*args, **kw)
+
+ def post(self, *args: t.Any, **kw: t.Any) -> TestResponse:
+ """Call :meth:`open` with ``method`` set to ``POST``."""
+ kw["method"] = "POST"
+ return self.open(*args, **kw)
+
+ def put(self, *args: t.Any, **kw: t.Any) -> TestResponse:
+ """Call :meth:`open` with ``method`` set to ``PUT``."""
+ kw["method"] = "PUT"
+ return self.open(*args, **kw)
+
+ def delete(self, *args: t.Any, **kw: t.Any) -> TestResponse:
+ """Call :meth:`open` with ``method`` set to ``DELETE``."""
+ kw["method"] = "DELETE"
+ return self.open(*args, **kw)
+
+ def patch(self, *args: t.Any, **kw: t.Any) -> TestResponse:
+ """Call :meth:`open` with ``method`` set to ``PATCH``."""
+ kw["method"] = "PATCH"
+ return self.open(*args, **kw)
+
+ def options(self, *args: t.Any, **kw: t.Any) -> TestResponse:
+ """Call :meth:`open` with ``method`` set to ``OPTIONS``."""
+ kw["method"] = "OPTIONS"
+ return self.open(*args, **kw)
+
+ def head(self, *args: t.Any, **kw: t.Any) -> TestResponse:
+ """Call :meth:`open` with ``method`` set to ``HEAD``."""
+ kw["method"] = "HEAD"
+ return self.open(*args, **kw)
+
+ def trace(self, *args: t.Any, **kw: t.Any) -> TestResponse:
+ """Call :meth:`open` with ``method`` set to ``TRACE``."""
+ kw["method"] = "TRACE"
+ return self.open(*args, **kw)
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} {self.application!r}>"
+
+
+def create_environ(*args: t.Any, **kwargs: t.Any) -> WSGIEnvironment:
+ """Create a new WSGI environ dict based on the values passed. The first
+ parameter should be the path of the request which defaults to '/'. The
+ second one can either be an absolute path (in that case the host is
+ localhost:80) or a full path to the request with scheme, netloc port and
+ the path to the script.
+
+ This accepts the same arguments as the :class:`EnvironBuilder`
+ constructor.
+
+ .. versionchanged:: 0.5
+ This function is now a thin wrapper over :class:`EnvironBuilder` which
+ was added in 0.5. The `headers`, `environ_base`, `environ_overrides`
+ and `charset` parameters were added.
+ """
+ builder = EnvironBuilder(*args, **kwargs)
+
+ try:
+ return builder.get_environ()
+ finally:
+ builder.close()
+
+
+def run_wsgi_app(
+ app: WSGIApplication, environ: WSGIEnvironment, buffered: bool = False
+) -> tuple[t.Iterable[bytes], str, Headers]:
+ """Return a tuple in the form (app_iter, status, headers) of the
+ application output. This works best if you pass it an application that
+ returns an iterator all the time.
+
+ Sometimes applications may use the `write()` callable returned
+ by the `start_response` function. This tries to resolve such edge
+ cases automatically. But if you don't get the expected output you
+ should set `buffered` to `True` which enforces buffering.
+
+ If passed an invalid WSGI application the behavior of this function is
+ undefined. Never pass non-conforming WSGI applications to this function.
+
+ :param app: the application to execute.
+ :param buffered: set to `True` to enforce buffering.
+ :return: tuple in the form ``(app_iter, status, headers)``
+ """
+ # Copy environ to ensure any mutations by the app (ProxyFix, for
+ # example) don't affect subsequent requests (such as redirects).
+ environ = _get_environ(environ).copy()
+ status: str
+ response: tuple[str, list[tuple[str, str]]] | None = None
+ buffer: list[bytes] = []
+
+ def start_response(status, headers, exc_info=None): # type: ignore
+ nonlocal response
+
+ if exc_info:
+ try:
+ raise exc_info[1].with_traceback(exc_info[2])
+ finally:
+ exc_info = None
+
+ response = (status, headers)
+ return buffer.append
+
+ app_rv = app(environ, start_response)
+ close_func = getattr(app_rv, "close", None)
+ app_iter: t.Iterable[bytes] = iter(app_rv)
+
+ # when buffering we emit the close call early and convert the
+ # application iterator into a regular list
+ if buffered:
+ try:
+ app_iter = list(app_iter)
+ finally:
+ if close_func is not None:
+ close_func()
+
+ # otherwise we iterate the application iter until we have a response, chain
+ # the already received data with the already collected data and wrap it in
+ # a new `ClosingIterator` if we need to restore a `close` callable from the
+ # original return value.
+ else:
+ for item in app_iter:
+ buffer.append(item)
+
+ if response is not None:
+ break
+
+ if buffer:
+ app_iter = chain(buffer, app_iter)
+
+ if close_func is not None and app_iter is not app_rv:
+ app_iter = ClosingIterator(app_iter, close_func)
+
+ status, headers = response # type: ignore
+ return app_iter, status, Headers(headers)
+
+
+class TestResponse(Response):
+ """:class:`~werkzeug.wrappers.Response` subclass that provides extra
+ information about requests made with the test :class:`Client`.
+
+ Test client requests will always return an instance of this class.
+ If a custom response class is passed to the client, it is
+ subclassed along with this to support test information.
+
+ If the test request included large files, or if the application is
+ serving a file, call :meth:`close` to close any open files and
+ prevent Python showing a ``ResourceWarning``.
+
+ .. versionchanged:: 2.2
+ Set the ``default_mimetype`` to None to prevent a mimetype being
+ assumed if missing.
+
+ .. versionchanged:: 2.1
+ Response instances cannot be treated as tuples.
+
+ .. versionadded:: 2.0
+ Test client methods always return instances of this class.
+ """
+
+ default_mimetype = None
+ # Don't assume a mimetype, instead use whatever the response provides
+
+ request: Request
+ """A request object with the environ used to make the request that
+ resulted in this response.
+ """
+
+ history: tuple[TestResponse, ...]
+ """A list of intermediate responses. Populated when the test request
+ is made with ``follow_redirects`` enabled.
+ """
+
+ # Tell Pytest to ignore this, it's not a test class.
+ __test__ = False
+
+ def __init__(
+ self,
+ response: t.Iterable[bytes],
+ status: str,
+ headers: Headers,
+ request: Request,
+ history: tuple[TestResponse] = (), # type: ignore
+ **kwargs: t.Any,
+ ) -> None:
+ super().__init__(response, status, headers, **kwargs)
+ self.request = request
+ self.history = history
+ self._compat_tuple = response, status, headers
+
+ @cached_property
+ def text(self) -> str:
+ """The response data as text. A shortcut for
+ ``response.get_data(as_text=True)``.
+
+ .. versionadded:: 2.1
+ """
+ return self.get_data(as_text=True)
+
+
+@dataclasses.dataclass
+class Cookie:
+ """A cookie key, value, and parameters.
+
+ The class itself is not a public API. Its attributes are documented for inspection
+ with :meth:`.Client.get_cookie` only.
+
+ .. versionadded:: 2.3
+ """
+
+ key: str
+ """The cookie key, encoded as a client would see it."""
+
+ value: str
+ """The cookie key, encoded as a client would see it."""
+
+ decoded_key: str
+ """The cookie key, decoded as the application would set and see it."""
+
+ decoded_value: str
+ """The cookie value, decoded as the application would set and see it."""
+
+ expires: datetime | None
+ """The time at which the cookie is no longer valid."""
+
+ max_age: int | None
+ """The number of seconds from when the cookie was set at which it is
+ no longer valid.
+ """
+
+ domain: str
+ """The domain that the cookie was set for, or the request domain if not set."""
+
+ origin_only: bool
+ """Whether the cookie will be sent for exact domain matches only. This is ``True``
+ if the ``Domain`` parameter was not present.
+ """
+
+ path: str
+ """The path that the cookie was set for."""
+
+ secure: bool | None
+ """The ``Secure`` parameter."""
+
+ http_only: bool | None
+ """The ``HttpOnly`` parameter."""
+
+ same_site: str | None
+ """The ``SameSite`` parameter."""
+
+ def _matches_request(self, server_name: str, path: str) -> bool:
+ return (
+ server_name == self.domain
+ or (
+ not self.origin_only
+ and server_name.endswith(self.domain)
+ and server_name[: -len(self.domain)].endswith(".")
+ )
+ ) and (
+ path == self.path
+ or (
+ path.startswith(self.path)
+ and path[len(self.path) - self.path.endswith("/") :].startswith("/")
+ )
+ )
+
+ def _to_request_header(self) -> str:
+ return f"{self.key}={self.value}"
+
+ @classmethod
+ def _from_response_header(cls, server_name: str, path: str, header: str) -> te.Self:
+ header, _, parameters_str = header.partition(";")
+ key, _, value = header.partition("=")
+ decoded_key, decoded_value = next(parse_cookie(header).items()) # type: ignore[call-overload]
+ params = {}
+
+ for item in parameters_str.split(";"):
+ k, sep, v = item.partition("=")
+ params[k.strip().lower()] = v.strip() if sep else None
+
+ return cls(
+ key=key.strip(),
+ value=value.strip(),
+ decoded_key=decoded_key,
+ decoded_value=decoded_value,
+ expires=parse_date(params.get("expires")),
+ max_age=int(params["max-age"] or 0) if "max-age" in params else None,
+ domain=params.get("domain") or server_name,
+ origin_only="domain" not in params,
+ path=params.get("path") or path.rpartition("/")[0] or "/",
+ secure="secure" in params,
+ http_only="httponly" in params,
+ same_site=params.get("samesite"),
+ )
+
+ @property
+ def _storage_key(self) -> tuple[str, str, str]:
+ return self.domain, self.path, self.decoded_key
+
+ @property
+ def _should_delete(self) -> bool:
+ return self.max_age == 0 or (
+ self.expires is not None and self.expires.timestamp() == 0
+ )
diff --git a/venv/Lib/site-packages/werkzeug/testapp.py b/venv/Lib/site-packages/werkzeug/testapp.py
new file mode 100644
index 0000000..cdf7fac
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/testapp.py
@@ -0,0 +1,194 @@
+"""A small application that can be used to test a WSGI server and check
+it for WSGI compliance.
+"""
+
+from __future__ import annotations
+
+import importlib.metadata
+import os
+import sys
+import typing as t
+from textwrap import wrap
+
+from markupsafe import escape
+
+from .wrappers.request import Request
+from .wrappers.response import Response
+
+TEMPLATE = """\
+
+
+WSGI Information
+
+
+
WSGI Information
+
+ This page displays all available information about the WSGI server and
+ the underlying Python interpreter.
+
Python Interpreter
+
+
+
Python Version
+
%(python_version)s
+
+
Platform
+
%(platform)s [%(os)s]
+
+
API Version
+
%(api_version)s
+
+
Byteorder
+
%(byteorder)s
+
+
Werkzeug Version
+
%(werkzeug_version)s
+
+
WSGI Environment
+
%(wsgi_env)s
+
Installed Eggs
+
+ The following python packages were installed on the system as
+ Python eggs:
+
%(python_eggs)s
+
System Path
+
+ The following paths are the current contents of the load path. The
+ following entries are looked up for Python packages. Note that not
+ all items in this path are folders. Gray and underlined items are
+ entries pointing to invalid resources or used by custom import hooks
+ such as the zip importer.
+
+ Items with a bright background were expanded for display from a relative
+ path. If you encounter such paths in the output you might want to check
+ your setup as relative paths are usually problematic in multithreaded
+ environments.
+
%(sys_path)s
+
+"""
+
+
+def iter_sys_path() -> t.Iterator[tuple[str, bool, bool]]:
+ if os.name == "posix":
+
+ def strip(x: str) -> str:
+ prefix = os.path.expanduser("~")
+ if x.startswith(prefix):
+ x = f"~{x[len(prefix) :]}"
+ return x
+
+ else:
+
+ def strip(x: str) -> str:
+ return x
+
+ cwd = os.path.abspath(os.getcwd())
+ for item in sys.path:
+ path = os.path.join(cwd, item or os.path.curdir)
+ yield strip(os.path.normpath(path)), not os.path.isdir(path), path != item
+
+
+@Request.application
+def test_app(req: Request) -> Response:
+ """Simple test application that dumps the environment. You can use
+ it to check if Werkzeug is working properly:
+
+ .. sourcecode:: pycon
+
+ >>> from werkzeug.serving import run_simple
+ >>> from werkzeug.testapp import test_app
+ >>> run_simple('localhost', 3000, test_app)
+ * Running on http://localhost:3000/
+
+ The application displays important information from the WSGI environment,
+ the Python interpreter and the installed libraries.
+ """
+ try:
+ import pkg_resources
+ except ImportError:
+ eggs: t.Iterable[t.Any] = ()
+ else:
+ eggs = sorted(
+ pkg_resources.working_set,
+ key=lambda x: x.project_name.lower(),
+ )
+ python_eggs = []
+ for egg in eggs:
+ try:
+ version = egg.version
+ except (ValueError, AttributeError):
+ version = "unknown"
+ python_eggs.append(
+ f"
{escape(egg.project_name)} [{escape(version)}]"
+ )
+
+ wsgi_env = []
+ sorted_environ = sorted(req.environ.items(), key=lambda x: repr(x[0]).lower())
+ for key, value in sorted_environ:
+ value = "".join(wrap(str(escape(repr(value)))))
+ wsgi_env.append(f"
{escape(key)}
{value}")
+
+ sys_path = []
+ for item, virtual, expanded in iter_sys_path():
+ css = []
+ if virtual:
+ css.append("virtual")
+ if expanded:
+ css.append("exp")
+ class_str = f' class="{" ".join(css)}"' if css else ""
+ sys_path.append(f"
{escape(item)}")
+
+ context = {
+ "python_version": " ".join(escape(sys.version).splitlines()),
+ "platform": escape(sys.platform),
+ "os": escape(os.name),
+ "api_version": sys.api_version,
+ "byteorder": sys.byteorder,
+ "werkzeug_version": _get_werkzeug_version(),
+ "python_eggs": "\n".join(python_eggs),
+ "wsgi_env": "\n".join(wsgi_env),
+ "sys_path": "\n".join(sys_path),
+ }
+ return Response(TEMPLATE % context, mimetype="text/html")
+
+
+_werkzeug_version = ""
+
+
+def _get_werkzeug_version() -> str:
+ global _werkzeug_version
+
+ if not _werkzeug_version:
+ _werkzeug_version = importlib.metadata.version("werkzeug")
+
+ return _werkzeug_version
+
+
+if __name__ == "__main__":
+ from .serving import run_simple
+
+ run_simple("localhost", 5000, test_app, use_reloader=True)
diff --git a/venv/Lib/site-packages/werkzeug/urls.py b/venv/Lib/site-packages/werkzeug/urls.py
new file mode 100644
index 0000000..5bffe39
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/urls.py
@@ -0,0 +1,203 @@
+from __future__ import annotations
+
+import codecs
+import re
+import typing as t
+import urllib.parse
+from urllib.parse import quote
+from urllib.parse import unquote
+from urllib.parse import urlencode
+from urllib.parse import urlsplit
+from urllib.parse import urlunsplit
+
+from .datastructures import iter_multi_items
+
+
+def _codec_error_url_quote(e: UnicodeError) -> tuple[str, int]:
+ """Used in :func:`uri_to_iri` after unquoting to re-quote any
+ invalid bytes.
+ """
+ # the docs state that UnicodeError does have these attributes,
+ # but mypy isn't picking them up
+ out = quote(e.object[e.start : e.end], safe="") # type: ignore
+ return out, e.end # type: ignore
+
+
+codecs.register_error("werkzeug.url_quote", _codec_error_url_quote)
+
+
+def _make_unquote_part(name: str, chars: str) -> t.Callable[[str], str]:
+ """Create a function that unquotes all percent encoded characters except those
+ given. This allows working with unquoted characters if possible while not changing
+ the meaning of a given part of a URL.
+ """
+ choices = "|".join(f"{ord(c):02X}" for c in sorted(chars))
+ pattern = re.compile(f"((?:%(?:{choices}))+)", re.I)
+
+ def _unquote_partial(value: str) -> str:
+ parts = iter(pattern.split(value))
+ out = []
+
+ for part in parts:
+ out.append(unquote(part, "utf-8", "werkzeug.url_quote"))
+ out.append(next(parts, ""))
+
+ return "".join(out)
+
+ _unquote_partial.__name__ = f"_unquote_{name}"
+ return _unquote_partial
+
+
+# characters that should remain quoted in URL parts
+# based on https://url.spec.whatwg.org/#percent-encoded-bytes
+# always keep all controls, space, and % quoted
+_always_unsafe = bytes((*range(0x21), 0x25, 0x7F)).decode()
+_unquote_fragment = _make_unquote_part("fragment", _always_unsafe)
+_unquote_query = _make_unquote_part("query", _always_unsafe + "&=+#")
+_unquote_path = _make_unquote_part("path", _always_unsafe + "/?#")
+_unquote_user = _make_unquote_part("user", _always_unsafe + ":@/?#")
+
+
+def uri_to_iri(uri: str) -> str:
+ """Convert a URI to an IRI. All valid UTF-8 characters are unquoted,
+ leaving all reserved and invalid characters quoted. If the URL has
+ a domain, it is decoded from Punycode.
+
+ >>> uri_to_iri("http://xn--n3h.net/p%C3%A5th?q=%C3%A8ry%DF")
+ 'http://\\u2603.net/p\\xe5th?q=\\xe8ry%DF'
+
+ :param uri: The URI to convert.
+
+ .. versionchanged:: 3.0
+ Passing a tuple or bytes, and the ``charset`` and ``errors`` parameters,
+ are removed.
+
+ .. versionchanged:: 2.3
+ Which characters remain quoted is specific to each part of the URL.
+
+ .. versionchanged:: 0.15
+ All reserved and invalid characters remain quoted. Previously,
+ only some reserved characters were preserved, and invalid bytes
+ were replaced instead of left quoted.
+
+ .. versionadded:: 0.6
+ """
+ parts = urlsplit(uri)
+ path = _unquote_path(parts.path)
+ query = _unquote_query(parts.query)
+ fragment = _unquote_fragment(parts.fragment)
+
+ if parts.hostname:
+ netloc = _decode_idna(parts.hostname)
+ else:
+ netloc = ""
+
+ if ":" in netloc:
+ netloc = f"[{netloc}]"
+
+ if parts.port:
+ netloc = f"{netloc}:{parts.port}"
+
+ if parts.username:
+ auth = _unquote_user(parts.username)
+
+ if parts.password:
+ password = _unquote_user(parts.password)
+ auth = f"{auth}:{password}"
+
+ netloc = f"{auth}@{netloc}"
+
+ return urlunsplit((parts.scheme, netloc, path, query, fragment))
+
+
+def iri_to_uri(iri: str) -> str:
+ """Convert an IRI to a URI. All non-ASCII and unsafe characters are
+ quoted. If the URL has a domain, it is encoded to Punycode.
+
+ >>> iri_to_uri('http://\\u2603.net/p\\xe5th?q=\\xe8ry%DF')
+ 'http://xn--n3h.net/p%C3%A5th?q=%C3%A8ry%DF'
+
+ :param iri: The IRI to convert.
+
+ .. versionchanged:: 3.0
+ Passing a tuple or bytes, the ``charset`` and ``errors`` parameters,
+ and the ``safe_conversion`` parameter, are removed.
+
+ .. versionchanged:: 2.3
+ Which characters remain unquoted is specific to each part of the URL.
+
+ .. versionchanged:: 0.15
+ All reserved characters remain unquoted. Previously, only some reserved
+ characters were left unquoted.
+
+ .. versionchanged:: 0.9.6
+ The ``safe_conversion`` parameter was added.
+
+ .. versionadded:: 0.6
+ """
+ parts = urlsplit(iri)
+ # safe = https://url.spec.whatwg.org/#url-path-segment-string
+ # as well as percent for things that are already quoted
+ path = quote(parts.path, safe="%!$&'()*+,/:;=@")
+ query = quote(parts.query, safe="%!$&'()*+,/:;=?@")
+ fragment = quote(parts.fragment, safe="%!#$&'()*+,/:;=?@")
+
+ if parts.hostname:
+ netloc = parts.hostname.encode("idna").decode("ascii")
+ else:
+ netloc = ""
+
+ if ":" in netloc:
+ netloc = f"[{netloc}]"
+
+ if parts.port:
+ netloc = f"{netloc}:{parts.port}"
+
+ if parts.username:
+ auth = quote(parts.username, safe="%!$&'()*+,;=")
+
+ if parts.password:
+ password = quote(parts.password, safe="%!$&'()*+,;=")
+ auth = f"{auth}:{password}"
+
+ netloc = f"{auth}@{netloc}"
+
+ return urlunsplit((parts.scheme, netloc, path, query, fragment))
+
+
+# Python < 3.12
+# itms-services was worked around in previous iri_to_uri implementations, but
+# we can tell Python directly that it needs to preserve the //.
+if "itms-services" not in urllib.parse.uses_netloc:
+ urllib.parse.uses_netloc.append("itms-services")
+
+
+def _decode_idna(domain: str) -> str:
+ try:
+ data = domain.encode("ascii")
+ except UnicodeEncodeError:
+ # If the domain is not ASCII, it's decoded already.
+ return domain
+
+ try:
+ # Try decoding in one shot.
+ return data.decode("idna")
+ except UnicodeDecodeError:
+ pass
+
+ # Decode each part separately, leaving invalid parts as punycode.
+ parts = []
+
+ for part in data.split(b"."):
+ try:
+ parts.append(part.decode("idna"))
+ except UnicodeDecodeError:
+ parts.append(part.decode("ascii"))
+
+ return ".".join(parts)
+
+
+def _urlencode(query: t.Mapping[str, str] | t.Iterable[tuple[str, str]]) -> str:
+ items = [x for x in iter_multi_items(query) if x[1] is not None]
+ # safe = https://url.spec.whatwg.org/#percent-encoded-bytes
+ return urlencode(items, safe="!$'()*,/:;?@")
diff --git a/venv/Lib/site-packages/werkzeug/user_agent.py b/venv/Lib/site-packages/werkzeug/user_agent.py
new file mode 100644
index 0000000..17e5d3f
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/user_agent.py
@@ -0,0 +1,47 @@
+from __future__ import annotations
+
+
+class UserAgent:
+ """Represents a parsed user agent header value.
+
+ The default implementation does no parsing, only the :attr:`string`
+ attribute is set. A subclass may parse the string to set the
+ common attributes or expose other information. Set
+ :attr:`werkzeug.wrappers.Request.user_agent_class` to use a
+ subclass.
+
+ :param string: The header value to parse.
+
+ .. versionadded:: 2.0
+ This replaces the previous ``useragents`` module, but does not
+ provide a built-in parser.
+ """
+
+ platform: str | None = None
+ """The OS name, if it could be parsed from the string."""
+
+ browser: str | None = None
+ """The browser name, if it could be parsed from the string."""
+
+ version: str | None = None
+ """The browser version, if it could be parsed from the string."""
+
+ language: str | None = None
+ """The browser language, if it could be parsed from the string."""
+
+ def __init__(self, string: str) -> None:
+ self.string: str = string
+ """The original header value."""
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} {self.browser}/{self.version}>"
+
+ def __str__(self) -> str:
+ return self.string
+
+ def __bool__(self) -> bool:
+ return bool(self.browser)
+
+ def to_header(self) -> str:
+ """Convert to a header value."""
+ return self.string
diff --git a/venv/Lib/site-packages/werkzeug/utils.py b/venv/Lib/site-packages/werkzeug/utils.py
new file mode 100644
index 0000000..d023f98
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/utils.py
@@ -0,0 +1,684 @@
+from __future__ import annotations
+
+import io
+import mimetypes
+import os
+import pkgutil
+import re
+import sys
+import typing as t
+import unicodedata
+from datetime import datetime
+from time import time
+from urllib.parse import quote
+from zlib import adler32
+
+from markupsafe import escape
+
+from ._internal import _DictAccessorProperty
+from ._internal import _missing
+from ._internal import _TAccessorValue
+from .datastructures import Headers
+from .exceptions import NotFound
+from .exceptions import RequestedRangeNotSatisfiable
+from .security import _windows_device_files
+from .security import safe_join
+from .wsgi import wrap_file
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import WSGIEnvironment
+
+ from .wrappers.request import Request
+ from .wrappers.response import Response
+
+_T = t.TypeVar("_T")
+
+_entity_re = re.compile(r"&([^;]+);")
+_filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9_.-]")
+
+
+class cached_property(property, t.Generic[_T]):
+ """A :func:`property` that is only evaluated once. Subsequent access
+ returns the cached value. Setting the property sets the cached
+ value. Deleting the property clears the cached value, accessing it
+ again will evaluate it again.
+
+ .. code-block:: python
+
+ class Example:
+ @cached_property
+ def value(self):
+ # calculate something important here
+ return 42
+
+ e = Example()
+ e.value # evaluates
+ e.value # uses cache
+ e.value = 16 # sets cache
+ del e.value # clears cache
+
+ If the class defines ``__slots__``, it must add ``_cache_{name}`` as
+ a slot. Alternatively, it can add ``__dict__``, but that's usually
+ not desirable.
+
+ .. versionchanged:: 2.1
+ Works with ``__slots__``.
+
+ .. versionchanged:: 2.0
+ ``del obj.name`` clears the cached value.
+ """
+
+ def __init__(
+ self,
+ fget: t.Callable[[t.Any], _T],
+ name: str | None = None,
+ doc: str | None = None,
+ ) -> None:
+ super().__init__(fget, doc=doc)
+ self.__name__ = name or fget.__name__
+ self.slot_name = f"_cache_{self.__name__}"
+ self.__module__ = fget.__module__
+
+ def __set__(self, obj: object, value: _T) -> None:
+ if hasattr(obj, "__dict__"):
+ obj.__dict__[self.__name__] = value
+ else:
+ setattr(obj, self.slot_name, value)
+
+ def __get__(self, obj: object, type: type = None) -> _T: # type: ignore
+ if obj is None:
+ return self # type: ignore
+
+ obj_dict = getattr(obj, "__dict__", None)
+
+ if obj_dict is not None:
+ value: _T = obj_dict.get(self.__name__, _missing)
+ else:
+ value = getattr(obj, self.slot_name, _missing) # type: ignore[arg-type]
+
+ if value is _missing:
+ value = self.fget(obj) # type: ignore
+
+ if obj_dict is not None:
+ obj.__dict__[self.__name__] = value
+ else:
+ setattr(obj, self.slot_name, value)
+
+ return value
+
+ def __delete__(self, obj: object) -> None:
+ if hasattr(obj, "__dict__"):
+ del obj.__dict__[self.__name__]
+ else:
+ setattr(obj, self.slot_name, _missing)
+
+
+class environ_property(_DictAccessorProperty[_TAccessorValue]):
+ """Maps request attributes to environment variables. This works not only
+ for the Werkzeug request object, but also any other class with an
+ environ attribute:
+
+ >>> class Test(object):
+ ... environ = {'key': 'value'}
+ ... test = environ_property('key')
+ >>> var = Test()
+ >>> var.test
+ 'value'
+
+ If you pass it a second value it's used as default if the key does not
+ exist, the third one can be a converter that takes a value and converts
+ it. If it raises :exc:`ValueError` or :exc:`TypeError` the default value
+ is used. If no default value is provided `None` is used.
+
+ Per default the property is read only. You have to explicitly enable it
+ by passing ``read_only=False`` to the constructor.
+ """
+
+ read_only = True
+
+ def lookup(self, obj: Request) -> WSGIEnvironment:
+ return obj.environ
+
+
+class header_property(_DictAccessorProperty[_TAccessorValue]):
+ """Like `environ_property` but for headers."""
+
+ def lookup(self, obj: Request | Response) -> Headers: # type: ignore[override]
+ return obj.headers
+
+
+# https://cgit.freedesktop.org/xdg/shared-mime-info/tree/freedesktop.org.xml.in
+# https://www.iana.org/assignments/media-types/media-types.xhtml
+# Types listed in the XDG mime info that have a charset in the IANA registration.
+_charset_mimetypes = {
+ "application/ecmascript",
+ "application/javascript",
+ "application/sql",
+ "application/xml",
+ "application/xml-dtd",
+ "application/xml-external-parsed-entity",
+}
+
+
+def get_content_type(mimetype: str, charset: str) -> str:
+ """Returns the full content type string with charset for a mimetype.
+
+ If the mimetype represents text, the charset parameter will be
+ appended, otherwise the mimetype is returned unchanged.
+
+ :param mimetype: The mimetype to be used as content type.
+ :param charset: The charset to be appended for text mimetypes.
+ :return: The content type.
+
+ .. versionchanged:: 0.15
+ Any type that ends with ``+xml`` gets a charset, not just those
+ that start with ``application/``. Known text types such as
+ ``application/javascript`` are also given charsets.
+ """
+ if (
+ mimetype.startswith("text/")
+ or mimetype in _charset_mimetypes
+ or mimetype.endswith("+xml")
+ ):
+ mimetype += f"; charset={charset}"
+
+ return mimetype
+
+
+def secure_filename(filename: str) -> str:
+ r"""Pass it a filename and it will return a secure version of it. This
+ filename can then safely be stored on a regular file system and passed
+ to :func:`os.path.join`. The filename returned is an ASCII only string
+ for maximum portability.
+
+ On windows systems the function also makes sure that the file is not
+ named after one of the special device files.
+
+ >>> secure_filename("My cool movie.mov")
+ 'My_cool_movie.mov'
+ >>> secure_filename("../../../etc/passwd")
+ 'etc_passwd'
+ >>> secure_filename('i contain cool \xfcml\xe4uts.txt')
+ 'i_contain_cool_umlauts.txt'
+
+ The function might return an empty filename. It's your responsibility
+ to ensure that the filename is unique and that you abort or
+ generate a random filename if the function returned an empty one.
+
+ .. versionadded:: 0.5
+
+ :param filename: the filename to secure
+ """
+ filename = unicodedata.normalize("NFKD", filename)
+ filename = filename.encode("ascii", "ignore").decode("ascii")
+
+ for sep in os.sep, os.path.altsep:
+ if sep:
+ filename = filename.replace(sep, " ")
+ filename = str(_filename_ascii_strip_re.sub("", "_".join(filename.split()))).strip(
+ "._"
+ )
+
+ # on nt a couple of special files are present in each folder. We
+ # have to ensure that the target file is not such a filename. In
+ # this case we prepend an underline
+ if (
+ os.name == "nt"
+ and filename
+ and filename.split(".")[0].upper() in _windows_device_files
+ ):
+ filename = f"_{filename}"
+
+ return filename
+
+
+def redirect(
+ location: str, code: int = 302, Response: type[Response] | None = None
+) -> Response:
+ """Returns a response object (a WSGI application) that, if called,
+ redirects the client to the target location. Supported codes are
+ 301, 302, 303, 305, 307, and 308. 300 is not supported because
+ it's not a real redirect and 304 because it's the answer for a
+ request with a request with defined If-Modified-Since headers.
+
+ .. versionadded:: 0.6
+ The location can now be a unicode string that is encoded using
+ the :func:`iri_to_uri` function.
+
+ .. versionadded:: 0.10
+ The class used for the Response object can now be passed in.
+
+ :param location: the location the response should redirect to.
+ :param code: the redirect status code. defaults to 302.
+ :param class Response: a Response class to use when instantiating a
+ response. The default is :class:`werkzeug.wrappers.Response` if
+ unspecified.
+ """
+ if Response is None:
+ from .wrappers import Response
+
+ html_location = escape(location)
+ response = Response( # type: ignore[misc]
+ "\n"
+ "\n"
+ "Redirecting...\n"
+ "
Redirecting...
\n"
+ "
You should be redirected automatically to the target URL: "
+ f'{html_location}. If not, click the link.\n',
+ code,
+ mimetype="text/html",
+ )
+ response.headers["Location"] = location
+ return response
+
+
+def append_slash_redirect(environ: WSGIEnvironment, code: int = 308) -> Response:
+ """Redirect to the current URL with a slash appended.
+
+ If the current URL is ``/user/42``, the redirect URL will be
+ ``42/``. When joined to the current URL during response
+ processing or by the browser, this will produce ``/user/42/``.
+
+ The behavior is undefined if the path ends with a slash already. If
+ called unconditionally on a URL, it may produce a redirect loop.
+
+ :param environ: Use the path and query from this WSGI environment
+ to produce the redirect URL.
+ :param code: the status code for the redirect.
+
+ .. versionchanged:: 2.1
+ Produce a relative URL that only modifies the last segment.
+ Relevant when the current path has multiple segments.
+
+ .. versionchanged:: 2.1
+ The default status code is 308 instead of 301. This preserves
+ the request method and body.
+ """
+ tail = environ["PATH_INFO"].rpartition("/")[2]
+
+ if not tail:
+ new_path = "./"
+ else:
+ new_path = f"{tail}/"
+
+ query_string = environ.get("QUERY_STRING")
+
+ if query_string:
+ new_path = f"{new_path}?{query_string}"
+
+ return redirect(new_path, code)
+
+
+def send_file(
+ path_or_file: os.PathLike[str] | str | t.IO[bytes],
+ environ: WSGIEnvironment,
+ mimetype: str | None = None,
+ as_attachment: bool = False,
+ download_name: str | None = None,
+ conditional: bool = True,
+ etag: bool | str = True,
+ last_modified: datetime | int | float | None = None,
+ max_age: None | (int | t.Callable[[str | None], int | None]) = None,
+ use_x_sendfile: bool = False,
+ response_class: type[Response] | None = None,
+ _root_path: os.PathLike[str] | str | None = None,
+) -> Response:
+ """Send the contents of a file to the client.
+
+ The first argument can be a file path or a file-like object. Paths
+ are preferred in most cases because Werkzeug can manage the file and
+ get extra information from the path. Passing a file-like object
+ requires that the file is opened in binary mode, and is mostly
+ useful when building a file in memory with :class:`io.BytesIO`.
+
+ Never pass file paths provided by a user. The path is assumed to be
+ trusted, so a user could craft a path to access a file you didn't
+ intend. Use :func:`send_from_directory` to safely serve user-provided paths.
+
+ If the WSGI server sets a ``file_wrapper`` in ``environ``, it is
+ used, otherwise Werkzeug's built-in wrapper is used. Alternatively,
+ if the HTTP server supports ``X-Sendfile``, ``use_x_sendfile=True``
+ will tell the server to send the given path, which is much more
+ efficient than reading it in Python.
+
+ :param path_or_file: The path to the file to send, relative to the
+ current working directory if a relative path is given.
+ Alternatively, a file-like object opened in binary mode. Make
+ sure the file pointer is seeked to the start of the data.
+ :param environ: The WSGI environ for the current request.
+ :param mimetype: The MIME type to send for the file. If not
+ provided, it will try to detect it from the file name.
+ :param as_attachment: Indicate to a browser that it should offer to
+ save the file instead of displaying it.
+ :param download_name: The default name browsers will use when saving
+ the file. Defaults to the passed file name.
+ :param conditional: Enable conditional and range responses based on
+ request headers. Requires passing a file path and ``environ``.
+ :param etag: Calculate an ETag for the file, which requires passing
+ a file path. Can also be a string to use instead.
+ :param last_modified: The last modified time to send for the file,
+ in seconds. If not provided, it will try to detect it from the
+ file path.
+ :param max_age: How long the client should cache the file, in
+ seconds. If set, ``Cache-Control`` will be ``public``, otherwise
+ it will be ``no-cache`` to prefer conditional caching.
+ :param use_x_sendfile: Set the ``X-Sendfile`` header to let the
+ server to efficiently send the file. Requires support from the
+ HTTP server. Requires passing a file path.
+ :param response_class: Build the response using this class. Defaults
+ to :class:`~werkzeug.wrappers.Response`.
+ :param _root_path: Do not use. For internal use only. Use
+ :func:`send_from_directory` to safely send files under a path.
+
+ .. versionchanged:: 2.0.2
+ ``send_file`` only sets a detected ``Content-Encoding`` if
+ ``as_attachment`` is disabled.
+
+ .. versionadded:: 2.0
+ Adapted from Flask's implementation.
+
+ .. versionchanged:: 2.0
+ ``download_name`` replaces Flask's ``attachment_filename``
+ parameter. If ``as_attachment=False``, it is passed with
+ ``Content-Disposition: inline`` instead.
+
+ .. versionchanged:: 2.0
+ ``max_age`` replaces Flask's ``cache_timeout`` parameter.
+ ``conditional`` is enabled and ``max_age`` is not set by
+ default.
+
+ .. versionchanged:: 2.0
+ ``etag`` replaces Flask's ``add_etags`` parameter. It can be a
+ string to use instead of generating one.
+
+ .. versionchanged:: 2.0
+ If an encoding is returned when guessing ``mimetype`` from
+ ``download_name``, set the ``Content-Encoding`` header.
+ """
+ if response_class is None:
+ from .wrappers import Response
+
+ response_class = Response
+
+ path: str | None = None
+ file: t.IO[bytes] | None = None
+ size: int | None = None
+ mtime: float | None = None
+ headers = Headers()
+
+ if isinstance(path_or_file, (os.PathLike, str)) or hasattr(
+ path_or_file, "__fspath__"
+ ):
+ path_or_file = t.cast("os.PathLike[str] | str", path_or_file)
+
+ # Flask will pass app.root_path, allowing its send_file wrapper
+ # to not have to deal with paths.
+ if _root_path is not None:
+ path = os.path.join(_root_path, path_or_file)
+ else:
+ path = os.path.abspath(path_or_file)
+
+ stat = os.stat(path)
+ size = stat.st_size
+ mtime = stat.st_mtime
+ else:
+ file = path_or_file
+
+ if download_name is None and path is not None:
+ download_name = os.path.basename(path)
+
+ if mimetype is None:
+ if download_name is None:
+ raise TypeError(
+ "Unable to detect the MIME type because a file name is"
+ " not available. Either set 'download_name', pass a"
+ " path instead of a file, or set 'mimetype'."
+ )
+
+ mimetype, encoding = mimetypes.guess_type(download_name)
+
+ if mimetype is None:
+ mimetype = "application/octet-stream"
+
+ # Don't send encoding for attachments, it causes browsers to
+ # save decompress tar.gz files.
+ if encoding is not None and not as_attachment:
+ headers.set("Content-Encoding", encoding)
+
+ if download_name is not None:
+ try:
+ download_name.encode("ascii")
+ except UnicodeEncodeError:
+ simple = unicodedata.normalize("NFKD", download_name)
+ simple = simple.encode("ascii", "ignore").decode("ascii")
+ # safe = RFC 5987 attr-char
+ quoted = quote(download_name, safe="!#$&+-.^_`|~")
+ names = {"filename": simple, "filename*": f"UTF-8''{quoted}"}
+ else:
+ names = {"filename": download_name}
+
+ value = "attachment" if as_attachment else "inline"
+ headers.set("Content-Disposition", value, **names)
+ elif as_attachment:
+ raise TypeError(
+ "No name provided for attachment. Either set"
+ " 'download_name' or pass a path instead of a file."
+ )
+
+ if use_x_sendfile and path is not None:
+ headers["X-Sendfile"] = path
+ data = None
+ else:
+ if file is None:
+ file = open(path, "rb") # type: ignore
+ elif isinstance(file, io.BytesIO):
+ size = file.getbuffer().nbytes
+ elif isinstance(file, io.TextIOBase):
+ raise ValueError("Files must be opened in binary mode or use BytesIO.")
+
+ data = wrap_file(environ, file)
+
+ rv = response_class(
+ data, mimetype=mimetype, headers=headers, direct_passthrough=True
+ )
+
+ if size is not None:
+ rv.content_length = size
+
+ if last_modified is not None:
+ rv.last_modified = last_modified # type: ignore
+ elif mtime is not None:
+ rv.last_modified = mtime # type: ignore
+
+ rv.cache_control.no_cache = True
+
+ # Flask will pass app.get_send_file_max_age, allowing its send_file
+ # wrapper to not have to deal with paths.
+ if callable(max_age):
+ max_age = max_age(path)
+
+ if max_age is not None:
+ if max_age > 0:
+ rv.cache_control.no_cache = None
+ rv.cache_control.public = True
+
+ rv.cache_control.max_age = max_age
+ rv.expires = int(time() + max_age) # type: ignore
+
+ if isinstance(etag, str):
+ rv.set_etag(etag)
+ elif etag and path is not None:
+ check = adler32(path.encode()) & 0xFFFFFFFF
+ rv.set_etag(f"{mtime}-{size}-{check}")
+
+ if conditional:
+ try:
+ rv = rv.make_conditional(environ, accept_ranges=True, complete_length=size)
+ except RequestedRangeNotSatisfiable:
+ if file is not None:
+ file.close()
+
+ raise
+
+ # Some x-sendfile implementations incorrectly ignore the 304
+ # status code and send the file anyway.
+ if rv.status_code == 304:
+ rv.headers.pop("x-sendfile", None)
+
+ return rv
+
+
+def send_from_directory(
+ directory: os.PathLike[str] | str,
+ path: os.PathLike[str] | str,
+ environ: WSGIEnvironment,
+ **kwargs: t.Any,
+) -> Response:
+ """Send a file from within a directory using :func:`send_file`.
+
+ This is a secure way to serve files from a folder, such as static
+ files or uploads. Uses :func:`~werkzeug.security.safe_join` to
+ ensure the path coming from the client is not maliciously crafted to
+ point outside the specified directory.
+
+ If the final path does not point to an existing regular file,
+ returns a 404 :exc:`~werkzeug.exceptions.NotFound` error.
+
+ :param directory: The directory that ``path`` must be located under. This *must not*
+ be a value provided by the client, otherwise it becomes insecure.
+ :param path: The path to the file to send, relative to ``directory``. This is the
+ part of the path provided by the client, which is checked for security.
+ :param environ: The WSGI environ for the current request.
+ :param kwargs: Arguments to pass to :func:`send_file`.
+
+ .. versionadded:: 2.0
+ Adapted from Flask's implementation.
+ """
+ path_str = safe_join(os.fspath(directory), os.fspath(path))
+
+ if path_str is None:
+ raise NotFound()
+
+ # Flask will pass app.root_path, allowing its send_from_directory
+ # wrapper to not have to deal with paths.
+ if "_root_path" in kwargs:
+ path_str = os.path.join(kwargs["_root_path"], path_str)
+
+ if not os.path.isfile(path_str):
+ raise NotFound()
+
+ return send_file(path_str, environ, **kwargs)
+
+
+def import_string(import_name: str, silent: bool = False) -> t.Any:
+ """Imports an object based on a string. This is useful if you want to
+ use import paths as endpoints or something similar. An import path can
+ be specified either in dotted notation (``xml.sax.saxutils.escape``)
+ or with a colon as object delimiter (``xml.sax.saxutils:escape``).
+
+ If `silent` is True the return value will be `None` if the import fails.
+
+ :param import_name: the dotted name for the object to import.
+ :param silent: if set to `True` import errors are ignored and
+ `None` is returned instead.
+ :return: imported object
+ """
+ import_name = import_name.replace(":", ".")
+ try:
+ try:
+ __import__(import_name)
+ except ImportError:
+ if "." not in import_name:
+ raise
+ else:
+ return sys.modules[import_name]
+
+ module_name, obj_name = import_name.rsplit(".", 1)
+ module = __import__(module_name, globals(), locals(), [obj_name])
+ try:
+ return getattr(module, obj_name)
+ except AttributeError as e:
+ raise ImportError(e) from None
+
+ except ImportError as e:
+ if not silent:
+ raise ImportStringError(import_name, e).with_traceback(
+ sys.exc_info()[2]
+ ) from None
+
+ return None
+
+
+def find_modules(
+ import_path: str, include_packages: bool = False, recursive: bool = False
+) -> t.Iterator[str]:
+ """Finds all the modules below a package. This can be useful to
+ automatically import all views / controllers so that their metaclasses /
+ function decorators have a chance to register themselves on the
+ application.
+
+ Packages are not returned unless `include_packages` is `True`. This can
+ also recursively list modules but in that case it will import all the
+ packages to get the correct load path of that module.
+
+ :param import_path: the dotted name for the package to find child modules.
+ :param include_packages: set to `True` if packages should be returned, too.
+ :param recursive: set to `True` if recursion should happen.
+ :return: generator
+ """
+ module = import_string(import_path)
+ path = getattr(module, "__path__", None)
+ if path is None:
+ raise ValueError(f"{import_path!r} is not a package")
+ basename = f"{module.__name__}."
+ for _importer, modname, ispkg in pkgutil.iter_modules(path):
+ modname = basename + modname
+ if ispkg:
+ if include_packages:
+ yield modname
+ if recursive:
+ yield from find_modules(modname, include_packages, True)
+ else:
+ yield modname
+
+
+class ImportStringError(ImportError):
+ """Provides information about a failed :func:`import_string` attempt."""
+
+ #: String in dotted notation that failed to be imported.
+ import_name: str
+ #: Wrapped exception.
+ exception: BaseException
+
+ def __init__(self, import_name: str, exception: BaseException) -> None:
+ self.import_name = import_name
+ self.exception = exception
+ msg = import_name
+ name = ""
+ tracked = []
+ for part in import_name.replace(":", ".").split("."):
+ name = f"{name}.{part}" if name else part
+ imported = import_string(name, silent=True)
+ if imported:
+ tracked.append((name, getattr(imported, "__file__", None)))
+ else:
+ track = [f"- {n!r} found in {i!r}." for n, i in tracked]
+ track.append(f"- {name!r} not found.")
+ track_str = "\n".join(track)
+ msg = (
+ f"import_string() failed for {import_name!r}. Possible reasons"
+ f" are:\n\n"
+ "- missing __init__.py in a package;\n"
+ "- package or module path not included in sys.path;\n"
+ "- duplicated package or module name taking precedence in"
+ " sys.path;\n"
+ "- missing module, class, function or variable;\n\n"
+ f"Debugged import:\n\n{track_str}\n\n"
+ f"Original exception:\n\n{type(exception).__name__}: {exception}"
+ )
+ break
+
+ super().__init__(msg)
+
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__}({self.import_name!r}, {self.exception!r})>"
diff --git a/venv/Lib/site-packages/werkzeug/wrappers/__init__.py b/venv/Lib/site-packages/werkzeug/wrappers/__init__.py
new file mode 100644
index 0000000..b36f228
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/wrappers/__init__.py
@@ -0,0 +1,3 @@
+from .request import Request as Request
+from .response import Response as Response
+from .response import ResponseStream as ResponseStream
diff --git a/venv/Lib/site-packages/werkzeug/wrappers/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/wrappers/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..e0e2640
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/wrappers/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/wrappers/__pycache__/request.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/wrappers/__pycache__/request.cpython-310.pyc
new file mode 100644
index 0000000..db946e6
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/wrappers/__pycache__/request.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/wrappers/__pycache__/response.cpython-310.pyc b/venv/Lib/site-packages/werkzeug/wrappers/__pycache__/response.cpython-310.pyc
new file mode 100644
index 0000000..56c6af5
Binary files /dev/null and b/venv/Lib/site-packages/werkzeug/wrappers/__pycache__/response.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/werkzeug/wrappers/request.py b/venv/Lib/site-packages/werkzeug/wrappers/request.py
new file mode 100644
index 0000000..9f1eee1
--- /dev/null
+++ b/venv/Lib/site-packages/werkzeug/wrappers/request.py
@@ -0,0 +1,650 @@
+from __future__ import annotations
+
+import collections.abc as cabc
+import functools
+import json
+import typing as t
+from io import BytesIO
+
+from .._internal import _wsgi_decoding_dance
+from ..datastructures import CombinedMultiDict
+from ..datastructures import EnvironHeaders
+from ..datastructures import FileStorage
+from ..datastructures import ImmutableMultiDict
+from ..datastructures import iter_multi_items
+from ..datastructures import MultiDict
+from ..exceptions import BadRequest
+from ..exceptions import UnsupportedMediaType
+from ..formparser import default_stream_factory
+from ..formparser import FormDataParser
+from ..sansio.request import Request as _SansIORequest
+from ..utils import cached_property
+from ..utils import environ_property
+from ..wsgi import _get_server
+from ..wsgi import get_input_stream
+
+if t.TYPE_CHECKING:
+ from _typeshed.wsgi import WSGIApplication
+ from _typeshed.wsgi import WSGIEnvironment
+
+
+class Request(_SansIORequest):
+ """Represents an incoming WSGI HTTP request, with headers and body
+ taken from the WSGI environment. Has properties and methods for
+ using the functionality defined by various HTTP specs. The data in
+ requests object is read-only.
+
+ Text data is assumed to use UTF-8 encoding, which should be true for
+ the vast majority of modern clients. Using an encoding set by the
+ client is unsafe in Python due to extra encodings it provides, such
+ as ``zip``. To change the assumed encoding, subclass and replace
+ :attr:`charset`.
+
+ :param environ: The WSGI environ is generated by the WSGI server and
+ contains information about the server configuration and client
+ request.
+ :param populate_request: Add this request object to the WSGI environ
+ as ``environ['werkzeug.request']``. Can be useful when
+ debugging.
+ :param shallow: Makes reading from :attr:`stream` (and any method
+ that would read from it) raise a :exc:`RuntimeError`. Useful to
+ prevent consuming the form data in middleware, which would make
+ it unavailable to the final application.
+
+ .. versionchanged:: 3.0
+ The ``charset``, ``url_charset``, and ``encoding_errors`` parameters
+ were removed.
+
+ .. versionchanged:: 2.1
+ Old ``BaseRequest`` and mixin classes were removed.
+
+ .. versionchanged:: 2.1
+ Remove the ``disable_data_descriptor`` attribute.
+
+ .. versionchanged:: 2.0
+ Combine ``BaseRequest`` and mixins into a single ``Request``
+ class.
+
+ .. versionchanged:: 0.5
+ Read-only mode is enforced with immutable classes for all data.
+ """
+
+ #: the maximum content length. This is forwarded to the form data
+ #: parsing function (:func:`parse_form_data`). When set and the
+ #: :attr:`form` or :attr:`files` attribute is accessed and the
+ #: parsing fails because more than the specified value is transmitted
+ #: a :exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception is raised.
+ #:
+ #: .. versionadded:: 0.5
+ max_content_length: int | None = None
+
+ #: the maximum form field size. This is forwarded to the form data
+ #: parsing function (:func:`parse_form_data`). When set and the
+ #: :attr:`form` or :attr:`files` attribute is accessed and the
+ #: data in memory for post data is longer than the specified value a
+ #: :exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception is raised.
+ #:
+ #: .. versionchanged:: 3.1
+ #: Defaults to 500kB instead of unlimited.
+ #:
+ #: .. versionadded:: 0.5
+ max_form_memory_size: int | None = 500_000
+
+ #: The maximum number of multipart parts to parse, passed to
+ #: :attr:`form_data_parser_class`. Parsing form data with more than this
+ #: many parts will raise :exc:`~.RequestEntityTooLarge`.
+ #:
+ #: .. versionadded:: 2.2.3
+ max_form_parts = 1000
+
+ #: The form data parser that should be used. Can be replaced to customize
+ #: the form date parsing.
+ form_data_parser_class: type[FormDataParser] = FormDataParser
+
+ #: The WSGI environment containing HTTP headers and information from
+ #: the WSGI server.
+ environ: WSGIEnvironment
+
+ #: Set when creating the request object. If ``True``, reading from
+ #: the request body will cause a ``RuntimeException``. Useful to
+ #: prevent modifying the stream from middleware.
+ shallow: bool
+
+ def __init__(
+ self,
+ environ: WSGIEnvironment,
+ populate_request: bool = True,
+ shallow: bool = False,
+ ) -> None:
+ super().__init__(
+ method=environ.get("REQUEST_METHOD", "GET"),
+ scheme=environ.get("wsgi.url_scheme", "http"),
+ server=_get_server(environ),
+ root_path=_wsgi_decoding_dance(environ.get("SCRIPT_NAME") or ""),
+ path=_wsgi_decoding_dance(environ.get("PATH_INFO") or ""),
+ query_string=environ.get("QUERY_STRING", "").encode("latin1"),
+ headers=EnvironHeaders(environ),
+ remote_addr=environ.get("REMOTE_ADDR"),
+ )
+ self.environ = environ
+ self.shallow = shallow
+
+ if populate_request and not shallow:
+ self.environ["werkzeug.request"] = self
+
+ @classmethod
+ def from_values(cls, *args: t.Any, **kwargs: t.Any) -> Request:
+ """Create a new request object based on the values provided. If
+ environ is given missing values are filled from there. This method is
+ useful for small scripts when you need to simulate a request from an URL.
+ Do not use this method for unittesting, there is a full featured client
+ object (:class:`Client`) that allows to create multipart requests,
+ support for cookies etc.
+
+ This accepts the same options as the
+ :class:`~werkzeug.test.EnvironBuilder`.
+
+ .. versionchanged:: 0.5
+ This method now accepts the same arguments as
+ :class:`~werkzeug.test.EnvironBuilder`. Because of this the
+ `environ` parameter is now called `environ_overrides`.
+
+ :return: request object
+ """
+ from ..test import EnvironBuilder
+
+ builder = EnvironBuilder(*args, **kwargs)
+ try:
+ return builder.get_request(cls)
+ finally:
+ builder.close()
+
+ @classmethod
+ def application(cls, f: t.Callable[[Request], WSGIApplication]) -> WSGIApplication:
+ """Decorate a function as responder that accepts the request as
+ the last argument. This works like the :func:`responder`
+ decorator but the function is passed the request object as the
+ last argument and the request object will be closed
+ automatically::
+
+ @Request.application
+ def my_wsgi_app(request):
+ return Response('Hello World!')
+
+ As of Werkzeug 0.14 HTTP exceptions are automatically caught and
+ converted to responses instead of failing.
+
+ :param f: the WSGI callable to decorate
+ :return: a new WSGI callable
+ """
+ #: return a callable that wraps the -2nd argument with the request
+ #: and calls the function with all the arguments up to that one and
+ #: the request. The return value is then called with the latest
+ #: two arguments. This makes it possible to use this decorator for
+ #: both standalone WSGI functions as well as bound methods and
+ #: partially applied functions.
+ from ..exceptions import HTTPException
+
+ @functools.wraps(f)
+ def application(*args: t.Any) -> cabc.Iterable[bytes]:
+ request = cls(args[-2])
+ with request:
+ try:
+ resp = f(*args[:-2] + (request,))
+ except HTTPException as e:
+ resp = t.cast("WSGIApplication", e.get_response(args[-2]))
+ return resp(*args[-2:])
+
+ return t.cast("WSGIApplication", application)
+
+ def _get_file_stream(
+ self,
+ total_content_length: int | None,
+ content_type: str | None,
+ filename: str | None = None,
+ content_length: int | None = None,
+ ) -> t.IO[bytes]:
+ """Called to get a stream for the file upload.
+
+ This must provide a file-like class with `read()`, `readline()`
+ and `seek()` methods that is both writeable and readable.
+
+ The default implementation returns a temporary file if the total
+ content length is higher than 500KB. Because many browsers do not
+ provide a content length for the files only the total content
+ length matters.
+
+ :param total_content_length: the total content length of all the
+ data in the request combined. This value
+ is guaranteed to be there.
+ :param content_type: the mimetype of the uploaded file.
+ :param filename: the filename of the uploaded file. May be `None`.
+ :param content_length: the length of this file. This value is usually
+ not provided because webbrowsers do not provide
+ this value.
+ """
+ return default_stream_factory(
+ total_content_length=total_content_length,
+ filename=filename,
+ content_type=content_type,
+ content_length=content_length,
+ )
+
+ @property
+ def want_form_data_parsed(self) -> bool:
+ """``True`` if the request method carries content. By default
+ this is true if a ``Content-Type`` is sent.
+
+ .. versionadded:: 0.8
+ """
+ return bool(self.environ.get("CONTENT_TYPE"))
+
+ def make_form_data_parser(self) -> FormDataParser:
+ """Creates the form data parser. Instantiates the
+ :attr:`form_data_parser_class` with some parameters.
+
+ .. versionadded:: 0.8
+ """
+ return self.form_data_parser_class(
+ stream_factory=self._get_file_stream,
+ max_form_memory_size=self.max_form_memory_size,
+ max_content_length=self.max_content_length,
+ max_form_parts=self.max_form_parts,
+ cls=self.parameter_storage_class,
+ )
+
+ def _load_form_data(self) -> None:
+ """Method used internally to retrieve submitted data. After calling
+ this sets `form` and `files` on the request object to multi dicts
+ filled with the incoming form data. As a matter of fact the input
+ stream will be empty afterwards. You can also call this method to
+ force the parsing of the form data.
+
+ .. versionadded:: 0.8
+ """
+ # abort early if we have already consumed the stream
+ if "form" in self.__dict__:
+ return
+
+ if self.want_form_data_parsed:
+ parser = self.make_form_data_parser()
+ data = parser.parse(
+ self._get_stream_for_parsing(),
+ self.mimetype,
+ self.content_length,
+ self.mimetype_params,
+ )
+ else:
+ data = (
+ self.stream,
+ self.parameter_storage_class(),
+ self.parameter_storage_class(),
+ )
+
+ # inject the values into the instance dict so that we bypass
+ # our cached_property non-data descriptor.
+ d = self.__dict__
+ d["stream"], d["form"], d["files"] = data
+
+ def _get_stream_for_parsing(self) -> t.IO[bytes]:
+ """This is the same as accessing :attr:`stream` with the difference
+ that if it finds cached data from calling :meth:`get_data` first it
+ will create a new stream out of the cached data.
+
+ .. versionadded:: 0.9.3
+ """
+ cached_data = getattr(self, "_cached_data", None)
+ if cached_data is not None:
+ return BytesIO(cached_data)
+ return self.stream
+
+ def close(self) -> None:
+ """Closes associated resources of this request object. This
+ closes all file handles explicitly. You can also use the request
+ object in a with statement which will automatically close it.
+
+ .. versionadded:: 0.9
+ """
+ files = self.__dict__.get("files")
+ for _key, value in iter_multi_items(files or ()):
+ value.close()
+
+ def __enter__(self) -> Request:
+ return self
+
+ def __exit__(self, exc_type, exc_value, tb) -> None: # type: ignore
+ self.close()
+
+ @cached_property
+ def stream(self) -> t.IO[bytes]:
+ """The WSGI input stream, with safety checks. This stream can only be consumed
+ once.
+
+ Use :meth:`get_data` to get the full data as bytes or text. The :attr:`data`
+ attribute will contain the full bytes only if they do not represent form data.
+ The :attr:`form` attribute will contain the parsed form data in that case.
+
+ Unlike :attr:`input_stream`, this stream guards against infinite streams or
+ reading past :attr:`content_length` or :attr:`max_content_length`.
+
+ If ``max_content_length`` is set, it can be enforced on streams if
+ ``wsgi.input_terminated`` is set. Otherwise, an empty stream is returned.
+
+ If the limit is reached before the underlying stream is exhausted (such as a
+ file that is too large, or an infinite stream), the remaining contents of the
+ stream cannot be read safely. Depending on how the server handles this, clients
+ may show a "connection reset" failure instead of seeing the 413 response.
+
+ .. versionchanged:: 2.3
+ Check ``max_content_length`` preemptively and while reading.
+
+ .. versionchanged:: 0.9
+ The stream is always set (but may be consumed) even if form parsing was
+ accessed first.
+ """
+ if self.shallow:
+ raise RuntimeError(
+ "This request was created with 'shallow=True', reading"
+ " from the input stream is disabled."
+ )
+
+ return get_input_stream(
+ self.environ, max_content_length=self.max_content_length
+ )
+
+ input_stream = environ_property[t.IO[bytes]](
+ "wsgi.input",
+ doc="""The raw WSGI input stream, without any safety checks.
+
+ This is dangerous to use. It does not guard against infinite streams or reading
+ past :attr:`content_length` or :attr:`max_content_length`.
+
+ Use :attr:`stream` instead.
+ """,
+ )
+
+ @cached_property
+ def data(self) -> bytes:
+ """The raw data read from :attr:`stream`. Will be empty if the request
+ represents form data.
+
+ To get the raw data even if it represents form data, use :meth:`get_data`.
+ """
+ return self.get_data(parse_form_data=True)
+
+ @t.overload
+ def get_data(
+ self,
+ cache: bool = True,
+ as_text: t.Literal[False] = False,
+ parse_form_data: bool = False,
+ ) -> bytes: ...
+
+ @t.overload
+ def get_data(
+ self,
+ cache: bool = True,
+ as_text: t.Literal[True] = ...,
+ parse_form_data: bool = False,
+ ) -> str: ...
+
+ def get_data(
+ self, cache: bool = True, as_text: bool = False, parse_form_data: bool = False
+ ) -> bytes | str:
+ """This reads the buffered incoming data from the client into one
+ bytes object. By default this is cached but that behavior can be
+ changed by setting `cache` to `False`.
+
+ Usually it's a bad idea to call this method without checking the
+ content length first as a client could send dozens of megabytes or more
+ to cause memory problems on the server.
+
+ Note that if the form data was already parsed this method will not
+ return anything as form data parsing does not cache the data like
+ this method does. To implicitly invoke form data parsing function
+ set `parse_form_data` to `True`. When this is done the return value
+ of this method will be an empty string if the form parser handles
+ the data. This generally is not necessary as if the whole data is
+ cached (which is the default) the form parser will used the cached
+ data to parse the form data. Please be generally aware of checking
+ the content length first in any case before calling this method
+ to avoid exhausting server memory.
+
+ If `as_text` is set to `True` the return value will be a decoded
+ string.
+
+ .. versionadded:: 0.9
+ """
+ rv = getattr(self, "_cached_data", None)
+ if rv is None:
+ if parse_form_data:
+ self._load_form_data()
+ rv = self.stream.read()
+ if cache:
+ self._cached_data = rv
+ if as_text:
+ rv = rv.decode(errors="replace")
+ return rv
+
+ @cached_property
+ def form(self) -> ImmutableMultiDict[str, str]:
+ """The form parameters. By default an
+ :class:`~werkzeug.datastructures.ImmutableMultiDict`
+ is returned from this function. This can be changed by setting
+ :attr:`parameter_storage_class` to a different type. This might
+ be necessary if the order of the form data is important.
+
+ Please keep in mind that file uploads will not end up here, but instead
+ in the :attr:`files` attribute.
+
+ .. versionchanged:: 0.9
+
+ Previous to Werkzeug 0.9 this would only contain form data for POST
+ and PUT requests.
+ """
+ self._load_form_data()
+ return self.form
+
+ @cached_property
+ def values(self) -> CombinedMultiDict[str, str]:
+ """A :class:`werkzeug.datastructures.CombinedMultiDict` that
+ combines :attr:`args` and :attr:`form`.
+
+ For GET requests, only ``args`` are present, not ``form``.
+
+ .. versionchanged:: 2.0
+ For GET requests, only ``args`` are present, not ``form``.
+ """
+ sources = [self.args]
+
+ if self.method != "GET":
+ # GET requests can have a body, and some caching proxies
+ # might not treat that differently than a normal GET
+ # request, allowing form data to "invisibly" affect the
+ # cache without indication in the query string / URL.
+ sources.append(self.form)
+
+ args = []
+
+ for d in sources:
+ if not isinstance(d, MultiDict):
+ d = MultiDict(d)
+
+ args.append(d)
+
+ return CombinedMultiDict(args)
+
+ @cached_property
+ def files(self) -> ImmutableMultiDict[str, FileStorage]:
+ """:class:`~werkzeug.datastructures.MultiDict` object containing
+ all uploaded files. Each key in :attr:`files` is the name from the
+ ````. Each value in :attr:`files` is a
+ Werkzeug :class:`~werkzeug.datastructures.FileStorage` object.
+
+ It basically behaves like a standard file object you know from Python,
+ with the difference that it also has a
+ :meth:`~werkzeug.datastructures.FileStorage.save` function that can
+ store the file on the filesystem.
+
+ Note that :attr:`files` will only contain data if the request method was
+ POST, PUT or PATCH and the ``