Added github integration
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
"""Flask-Limiter extension for rate limiting."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import _version
|
||||
from ._extension import Limiter, RequestLimit
|
||||
from ._limits import (
|
||||
ApplicationLimit,
|
||||
Limit,
|
||||
MetaLimit,
|
||||
RouteLimit,
|
||||
)
|
||||
from .constants import ExemptionScope, HeaderNames
|
||||
from .errors import RateLimitExceeded
|
||||
|
||||
__all__ = [
|
||||
"ExemptionScope",
|
||||
"HeaderNames",
|
||||
"Limiter",
|
||||
"Limit",
|
||||
"RouteLimit",
|
||||
"ApplicationLimit",
|
||||
"MetaLimit",
|
||||
"RateLimitExceeded",
|
||||
"RequestLimit",
|
||||
]
|
||||
|
||||
__version__ = _version.__version__
|
||||
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import flask
|
||||
from flask.ctx import RequestContext
|
||||
|
||||
# flask.globals.request_ctx is only available in Flask >= 2.2.0
|
||||
try:
|
||||
from flask.globals import request_ctx
|
||||
except ImportError:
|
||||
request_ctx = None
|
||||
|
||||
|
||||
def request_context() -> RequestContext:
|
||||
if request_ctx is None:
|
||||
return flask._request_ctx_stack.top
|
||||
return request_ctx
|
||||
1172
buffteks/lib/python3.11/site-packages/flask_limiter/_extension.py
Normal file
1172
buffteks/lib/python3.11/site-packages/flask_limiter/_extension.py
Normal file
File diff suppressed because it is too large
Load Diff
406
buffteks/lib/python3.11/site-packages/flask_limiter/_limits.py
Normal file
406
buffteks/lib/python3.11/site-packages/flask_limiter/_limits.py
Normal file
@@ -0,0 +1,406 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import itertools
|
||||
import traceback
|
||||
import weakref
|
||||
from functools import wraps
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, cast, overload
|
||||
|
||||
import flask
|
||||
from flask import request
|
||||
from flask.wrappers import Response
|
||||
from limits import RateLimitItem, parse_many
|
||||
|
||||
from ._typing import Callable, Iterable, Iterator, P, R, Self, Sequence
|
||||
from .util import get_qualified_name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from flask_limiter import Limiter, RequestLimit
|
||||
|
||||
|
||||
@dataclasses.dataclass(eq=True, unsafe_hash=True)
|
||||
class RuntimeLimit:
|
||||
"""
|
||||
Final representation of a rate limit before it is triggered during a request
|
||||
"""
|
||||
|
||||
limit: RateLimitItem
|
||||
key_func: Callable[[], str]
|
||||
scope: str | Callable[[str], str] | None
|
||||
per_method: bool = False
|
||||
methods: Sequence[str] | None = None
|
||||
error_message: str | Callable[[], str] | None = None
|
||||
exempt_when: Callable[[], bool] | None = None
|
||||
override_defaults: bool | None = False
|
||||
deduct_when: Callable[[Response], bool] | None = None
|
||||
on_breach: Callable[[RequestLimit], Response | None] | None = None
|
||||
cost: Callable[[], int] | int = 1
|
||||
shared: bool = False
|
||||
meta_limits: tuple[RuntimeLimit, ...] | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.methods:
|
||||
self.methods = tuple([k.lower() for k in self.methods])
|
||||
|
||||
@property
|
||||
def is_exempt(self) -> bool:
|
||||
"""Check if the limit is exempt."""
|
||||
|
||||
if self.exempt_when:
|
||||
return self.exempt_when()
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def deduction_amount(self) -> int:
|
||||
"""How much to deduct from the limit"""
|
||||
|
||||
return self.cost() if callable(self.cost) else self.cost
|
||||
|
||||
@property
|
||||
def method_exempt(self) -> bool:
|
||||
"""Check if the limit is not applicable for this method"""
|
||||
|
||||
return self.methods is not None and request.method.lower() not in self.methods
|
||||
|
||||
def scope_for(self, endpoint: str, method: str | None) -> str:
|
||||
"""
|
||||
Derive final bucket (scope) for this limit given the endpoint and request method.
|
||||
If the limit is shared between multiple routes, the scope does not include the endpoint.
|
||||
"""
|
||||
limit_scope = self.scope(request.endpoint or "") if callable(self.scope) else self.scope
|
||||
|
||||
if limit_scope:
|
||||
if self.shared:
|
||||
scope = limit_scope
|
||||
else:
|
||||
scope = f"{endpoint}:{limit_scope}"
|
||||
else:
|
||||
scope = endpoint
|
||||
|
||||
if self.per_method:
|
||||
assert method
|
||||
scope += f":{method.upper()}"
|
||||
|
||||
return scope
|
||||
|
||||
|
||||
@dataclasses.dataclass(eq=True, unsafe_hash=True)
|
||||
class Limit:
|
||||
"""
|
||||
The definition of a rate limit to be used by the extension as a default limit::
|
||||
|
||||
|
||||
def default_key_function():
|
||||
return request.remote_addr
|
||||
|
||||
def username_key_function():
|
||||
return request.headers.get("username", "guest")
|
||||
|
||||
limiter = flask_limiter.Limiter(
|
||||
default_key_function,
|
||||
default_limits = [
|
||||
# 10/second by username
|
||||
flask_limiter.Limit("10/second", key_function=username_key_function),
|
||||
# 100/second by ip (i.e. default_key_function)
|
||||
flask_limiter.Limit("100/second),
|
||||
|
||||
]
|
||||
)
|
||||
limit.init_app(app)
|
||||
|
||||
- For application wide limits see :class:`ApplicationLimit`
|
||||
- For meta limits see :class:`MetaLimit`
|
||||
"""
|
||||
|
||||
#: Rate limit string or a callable that returns a string.
|
||||
#: :ref:`ratelimit-string` for more details.
|
||||
limit_provider: Callable[[], str] | str
|
||||
#: Callable to extract the unique identifier for the rate limit.
|
||||
#: If not provided the key_function will default to the key function
|
||||
#: that the :class:`Limiter` was initialized with (:paramref:`Limiter.key_func`)
|
||||
key_function: Callable[[], str] | None = None
|
||||
#: A string or callable that returns a unique scope for the rate limit.
|
||||
#: The scope is combined with current endpoint of the request if
|
||||
#: :paramref:`shared` is ``False``
|
||||
scope: str | Callable[[str], str] | None = None
|
||||
#: The cost of a hit or a function that
|
||||
#: takes no parameters and returns the cost as an integer (Default: ``1``).
|
||||
cost: Callable[[], int] | int | None = None
|
||||
#: If this a shared limit (i.e. to be used by different endpoints)
|
||||
shared: bool = False
|
||||
#: If specified, only the methods in this list will
|
||||
#: be rate limited.
|
||||
methods: Sequence[str] | None = None
|
||||
#: Whether the limit is sub categorized into the
|
||||
#: http method of the request.
|
||||
per_method: bool = False
|
||||
#: String (or callable that returns one) to override
|
||||
#: the error message used in the response.
|
||||
error_message: str | Callable[[], str] | None = None
|
||||
#: Meta limits to trigger everytime this rate limit definition is exceeded
|
||||
meta_limits: Iterable[Callable[[], str] | str | MetaLimit] | None = None
|
||||
#: Callable used to decide if the rate
|
||||
#: limit should skipped.
|
||||
exempt_when: Callable[[], bool] | None = None
|
||||
#: A function that receives the current
|
||||
#: :class:`flask.Response` object and returns True/False to decide if a
|
||||
#: deduction should be done from the rate limit
|
||||
deduct_when: Callable[[Response], bool] | None = None
|
||||
#: A function that will be called when this limit
|
||||
#: is breached. If the function returns an instance of :class:`flask.Response`
|
||||
#: that will be the response embedded into the :exc:`RateLimitExceeded` exception
|
||||
#: raised.
|
||||
on_breach: Callable[[RequestLimit], Response | None] | None = None
|
||||
#: Whether the decorated limit overrides
|
||||
#: the default limits (Default: ``True``).
|
||||
#:
|
||||
#: .. note:: When used with a :class:`~flask.Blueprint` the meaning
|
||||
#: of the parameter extends to any parents the blueprint instance is
|
||||
#: registered under. For more details see :ref:`recipes:nested blueprints`
|
||||
#:
|
||||
#: :meta private:
|
||||
override_defaults: bool | None = dataclasses.field(default=False, init=False)
|
||||
#: Weak reference to the limiter that this limit definition is bound to
|
||||
#:
|
||||
#: :meta private:
|
||||
limiter: weakref.ProxyType[Limiter] = dataclasses.field(
|
||||
init=False, hash=False, kw_only=True, repr=False
|
||||
)
|
||||
#: :meta private:
|
||||
finalized: bool = dataclasses.field(default=True)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.methods:
|
||||
self.methods = tuple([k.lower() for k in self.methods])
|
||||
|
||||
if self.meta_limits:
|
||||
self.meta_limits = tuple(self.meta_limits)
|
||||
|
||||
def __iter__(self) -> Iterator[RuntimeLimit]:
|
||||
limit_str = self.limit_provider() if callable(self.limit_provider) else self.limit_provider
|
||||
limit_items = parse_many(limit_str) if limit_str else []
|
||||
meta_limits: tuple[RuntimeLimit, ...] = ()
|
||||
|
||||
if self.meta_limits:
|
||||
meta_limits = tuple(
|
||||
itertools.chain(
|
||||
*[
|
||||
list(
|
||||
MetaLimit(meta_limit).bind_parent(self)
|
||||
if not isinstance(meta_limit, MetaLimit)
|
||||
else meta_limit
|
||||
)
|
||||
for meta_limit in self.meta_limits
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
for limit in limit_items:
|
||||
yield RuntimeLimit(
|
||||
limit,
|
||||
self.limit_by,
|
||||
scope=self.scope,
|
||||
per_method=self.per_method,
|
||||
methods=self.methods,
|
||||
error_message=self.error_message,
|
||||
exempt_when=self.exempt_when,
|
||||
deduct_when=self.deduct_when,
|
||||
override_defaults=self.override_defaults,
|
||||
on_breach=self.on_breach,
|
||||
cost=self.cost or 1,
|
||||
shared=self.shared,
|
||||
meta_limits=meta_limits,
|
||||
)
|
||||
|
||||
@property
|
||||
def limit_by(self) -> Callable[[], str]:
|
||||
return self.key_function or self.limiter._key_func
|
||||
|
||||
def bind(self: Self, limiter: Limiter) -> Self:
|
||||
"""
|
||||
Returns an instance of the limit definition that binds to a weak reference of an instance
|
||||
of :class:`Limiter`.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
self.limiter = weakref.proxy(limiter)
|
||||
[
|
||||
meta_limit.bind(limiter)
|
||||
for meta_limit in self.meta_limits or ()
|
||||
if isinstance(meta_limit, MetaLimit)
|
||||
]
|
||||
|
||||
return self
|
||||
|
||||
|
||||
@dataclasses.dataclass(unsafe_hash=True, kw_only=True)
|
||||
class RouteLimit(Limit):
|
||||
"""
|
||||
A variant of :class:`Limit` that can be used to to decorate a flask route or blueprint directly
|
||||
instead of by using :meth:`Limiter.limit` or :meth:`Limiter.shared_limit`.
|
||||
|
||||
Decorating individual routes::
|
||||
|
||||
limiter = flask_limiter.Limiter(.....)
|
||||
limiter.init_app(app)
|
||||
|
||||
@app.route("/")
|
||||
@flask_limiter.RouteLimit("2/second", limiter=limiter)
|
||||
def view_function():
|
||||
...
|
||||
|
||||
"""
|
||||
|
||||
#: Whether the decorated limit overrides
|
||||
#: the default limits (Default: ``True``).
|
||||
#:
|
||||
#: .. note:: When used with a :class:`~flask.Blueprint` the meaning
|
||||
#: of the parameter extends to any parents the blueprint instance is
|
||||
#: registered under. For more details see :ref:`recipes:nested blueprints`
|
||||
override_defaults: bool | None = False
|
||||
|
||||
limiter: dataclasses.InitVar[Limiter] = dataclasses.field(hash=False)
|
||||
|
||||
def __post_init__(self, limiter: Limiter) -> None:
|
||||
self.bind(limiter)
|
||||
super().__post_init__()
|
||||
|
||||
def __enter__(self) -> None:
|
||||
tb = traceback.extract_stack(limit=2)
|
||||
qualified_location = f"{tb[0].filename}:{tb[0].name}:{tb[0].lineno}"
|
||||
|
||||
# TODO: if use as a context manager becomes interesting/valuable
|
||||
# a less hacky approach than using the traceback and piggy backing
|
||||
# on the limit manager's knowledge of decorated limits might be worth it.
|
||||
self.limiter.limit_manager.add_decorated_limit(qualified_location, self, override=True)
|
||||
|
||||
self.limiter.limit_manager.add_endpoint_hint(
|
||||
self.limiter.identify_request(), qualified_location
|
||||
)
|
||||
|
||||
self.limiter._check_request_limit(in_middleware=False, callable_name=qualified_location)
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __call__(self, obj: Callable[P, R]) -> Callable[P, R]: ...
|
||||
|
||||
@overload
|
||||
def __call__(self, obj: flask.Blueprint) -> None: ...
|
||||
|
||||
def __call__(self, obj: Callable[P, R] | flask.Blueprint) -> Callable[P, R] | None:
|
||||
if isinstance(obj, flask.Blueprint):
|
||||
name = obj.name
|
||||
else:
|
||||
name = get_qualified_name(obj)
|
||||
|
||||
if isinstance(obj, flask.Blueprint):
|
||||
self.limiter.limit_manager.add_blueprint_limit(name, self)
|
||||
|
||||
return None
|
||||
else:
|
||||
self.limiter._marked_for_limiting.add(name)
|
||||
self.limiter.limit_manager.add_decorated_limit(name, self)
|
||||
|
||||
@wraps(obj)
|
||||
def __inner(*a: P.args, **k: P.kwargs) -> R:
|
||||
if not getattr(obj, "__wrapper-limiter-instance", None) == self.limiter:
|
||||
identity = self.limiter.identify_request()
|
||||
|
||||
if identity:
|
||||
view_func = flask.current_app.view_functions.get(identity, None)
|
||||
|
||||
if view_func and not get_qualified_name(view_func) == name:
|
||||
self.limiter.limit_manager.add_endpoint_hint(identity, name)
|
||||
|
||||
self.limiter._check_request_limit(in_middleware=False, callable_name=name)
|
||||
|
||||
return cast(R, flask.current_app.ensure_sync(obj)(*a, **k))
|
||||
|
||||
# mark this wrapper as wrapped by a decorator from the limiter
|
||||
# from which the decorator was created. This ensures that stacked
|
||||
# decorations only trigger rate limiting from the inner most
|
||||
# decorator from each limiter instance (the weird need for
|
||||
# keeping track of the instance is to handle cases where multiple
|
||||
# limiter extensions are registered on the same application).
|
||||
setattr(__inner, "__wrapper-limiter-instance", self.limiter)
|
||||
|
||||
return __inner
|
||||
|
||||
|
||||
@dataclasses.dataclass(kw_only=True, unsafe_hash=True)
|
||||
class ApplicationLimit(Limit):
|
||||
"""
|
||||
Variant of :class:`Limit` to be used for declaring an application wide limit that can be passed
|
||||
to :class:`Limiter` as one of the members of :paramref:`Limiter.application_limits`
|
||||
"""
|
||||
|
||||
#: The scope to use for the application wide limit
|
||||
scope: str | Callable[[str], str] | None = dataclasses.field(default="global")
|
||||
#: Application limits are always "shared"
|
||||
#:
|
||||
#: :meta private:
|
||||
shared: bool = dataclasses.field(init=False, default=True)
|
||||
|
||||
|
||||
@dataclasses.dataclass(kw_only=True, unsafe_hash=True)
|
||||
class MetaLimit(Limit):
|
||||
"""
|
||||
Variant of :class:`Limit` to be used for declaring a meta limit that can be passed to
|
||||
either :class:`Limiter` as one of the members of :paramref:`Limiter.meta_limits` or to another
|
||||
instance of :class:`Limit` as a member of :paramref:`Limit.meta_limits`
|
||||
"""
|
||||
|
||||
#: The scope to use for the meta limit
|
||||
scope: str | Callable[[str], str] | None = dataclasses.field(default="meta")
|
||||
#: meta limits can't have meta limits - at least here :)
|
||||
#:
|
||||
#: :meta private:
|
||||
meta_limits: Sequence[Callable[[], str] | str | MetaLimit] | None = dataclasses.field(
|
||||
init=False, default=None
|
||||
)
|
||||
#: The rate limit this meta limit is limiting.
|
||||
#:
|
||||
# :meta private:
|
||||
parent_limit: Limit | None = dataclasses.field(init=False, default=None)
|
||||
#: Meta limits are always "shared"
|
||||
#:
|
||||
#: :meta private:
|
||||
shared: bool = dataclasses.field(init=False, default=True)
|
||||
#: Meta limits can't have conditional deductions
|
||||
#:
|
||||
#: :meta private:
|
||||
deduct_when: Callable[[Response], bool] | None = dataclasses.field(init=False, default=None)
|
||||
#: Callable to extract the unique identifier for the rate limit.
|
||||
#: If not provided the key_function will fallback to:
|
||||
#:
|
||||
#: - the key function of the parent limit this meta limit is declared for
|
||||
#: - the key function for the :class:`Limiter` instance this meta limit
|
||||
#: is eventually used with.
|
||||
key_function: Callable[[], str] | None = None
|
||||
|
||||
@property
|
||||
def limit_by(self) -> Callable[[], str]:
|
||||
return (
|
||||
self.key_function
|
||||
or self.parent_limit
|
||||
and self.parent_limit.key_function
|
||||
or self.limiter._key_func
|
||||
)
|
||||
|
||||
def bind_parent(self: Self, parent: Limit) -> Self:
|
||||
"""
|
||||
Binds this meta limit to be associated as a child of the ``parent`` limit.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
self.parent_limit = parent
|
||||
return self
|
||||
243
buffteks/lib/python3.11/site-packages/flask_limiter/_manager.py
Normal file
243
buffteks/lib/python3.11/site-packages/flask_limiter/_manager.py
Normal file
@@ -0,0 +1,243 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import flask
|
||||
from ordered_set import OrderedSet
|
||||
|
||||
from ._limits import ApplicationLimit, RuntimeLimit
|
||||
from .constants import ExemptionScope
|
||||
from .util import get_qualified_name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Limit
|
||||
|
||||
|
||||
class LimitManager:
|
||||
def __init__(
|
||||
self,
|
||||
application_limits: list[ApplicationLimit],
|
||||
default_limits: list[Limit],
|
||||
decorated_limits: dict[str, OrderedSet[Limit]],
|
||||
blueprint_limits: dict[str, OrderedSet[Limit]],
|
||||
route_exemptions: dict[str, ExemptionScope],
|
||||
blueprint_exemptions: dict[str, ExemptionScope],
|
||||
) -> None:
|
||||
self._application_limits = application_limits
|
||||
self._default_limits = default_limits
|
||||
self._decorated_limits = decorated_limits
|
||||
self._blueprint_limits = blueprint_limits
|
||||
self._route_exemptions = route_exemptions
|
||||
self._blueprint_exemptions = blueprint_exemptions
|
||||
self._endpoint_hints: dict[str, OrderedSet[str]] = {}
|
||||
self._logger = logging.getLogger("flask-limiter")
|
||||
|
||||
@property
|
||||
def application_limits(self) -> list[RuntimeLimit]:
|
||||
return list(itertools.chain(*self._application_limits))
|
||||
|
||||
@property
|
||||
def default_limits(self) -> list[RuntimeLimit]:
|
||||
return list(itertools.chain(*self._default_limits))
|
||||
|
||||
def set_application_limits(self, limits: list[ApplicationLimit]) -> None:
|
||||
self._application_limits = limits
|
||||
|
||||
def set_default_limits(self, limits: list[Limit]) -> None:
|
||||
self._default_limits = limits
|
||||
|
||||
def add_decorated_limit(self, route: str, limit: Limit | None, override: bool = False) -> None:
|
||||
if limit:
|
||||
if not override:
|
||||
self._decorated_limits.setdefault(route, OrderedSet()).add(limit)
|
||||
else:
|
||||
self._decorated_limits[route] = OrderedSet([limit])
|
||||
|
||||
def add_blueprint_limit(self, blueprint: str, limit: Limit | None) -> None:
|
||||
if limit:
|
||||
self._blueprint_limits.setdefault(blueprint, OrderedSet()).add(limit)
|
||||
|
||||
def add_route_exemption(self, route: str, scope: ExemptionScope) -> None:
|
||||
self._route_exemptions[route] = scope
|
||||
|
||||
def add_blueprint_exemption(self, blueprint: str, scope: ExemptionScope) -> None:
|
||||
self._blueprint_exemptions[blueprint] = scope
|
||||
|
||||
def add_endpoint_hint(self, endpoint: str, callable: str) -> None:
|
||||
self._endpoint_hints.setdefault(endpoint, OrderedSet()).add(callable)
|
||||
|
||||
def has_hints(self, endpoint: str) -> bool:
|
||||
return bool(self._endpoint_hints.get(endpoint))
|
||||
|
||||
def resolve_limits(
|
||||
self,
|
||||
app: flask.Flask,
|
||||
endpoint: str | None = None,
|
||||
blueprint: str | None = None,
|
||||
callable_name: str | None = None,
|
||||
in_middleware: bool = False,
|
||||
marked_for_limiting: bool = False,
|
||||
) -> tuple[list[RuntimeLimit], ...]:
|
||||
before_request_context = in_middleware and marked_for_limiting
|
||||
decorated_limits = []
|
||||
hinted_limits = []
|
||||
if endpoint:
|
||||
if not in_middleware:
|
||||
if not callable_name:
|
||||
view_func = app.view_functions.get(endpoint, None)
|
||||
name = get_qualified_name(view_func) if view_func else ""
|
||||
else:
|
||||
name = callable_name
|
||||
decorated_limits.extend(self.decorated_limits(name))
|
||||
|
||||
for hint in self._endpoint_hints.get(endpoint, OrderedSet()):
|
||||
hinted_limits.extend(self.decorated_limits(hint))
|
||||
|
||||
if blueprint:
|
||||
if not before_request_context and (
|
||||
not decorated_limits
|
||||
or all(not limit.override_defaults for limit in decorated_limits)
|
||||
):
|
||||
decorated_limits.extend(self.blueprint_limits(app, blueprint))
|
||||
exemption_scope = self.exemption_scope(app, endpoint, blueprint)
|
||||
|
||||
all_limits = (
|
||||
self.application_limits
|
||||
if in_middleware and not (exemption_scope & ExemptionScope.APPLICATION)
|
||||
else []
|
||||
)
|
||||
# all_limits += decorated_limits
|
||||
explicit_limits_exempt = all(limit.method_exempt for limit in decorated_limits)
|
||||
|
||||
# all the decorated limits explicitly declared
|
||||
# that they don't override the defaults - so, they should
|
||||
# be included.
|
||||
combined_defaults = all(not limit.override_defaults for limit in decorated_limits)
|
||||
# previous requests to this endpoint have exercised decorated
|
||||
# rate limits on callables that are not view functions. check
|
||||
# if all of them declared that they don't override defaults
|
||||
# and if so include the default limits.
|
||||
hinted_limits_request_defaults = (
|
||||
all(not limit.override_defaults for limit in hinted_limits) if hinted_limits else False
|
||||
)
|
||||
if (
|
||||
(explicit_limits_exempt or combined_defaults)
|
||||
and (not (before_request_context or exemption_scope & ExemptionScope.DEFAULT))
|
||||
) or hinted_limits_request_defaults:
|
||||
all_limits += self.default_limits
|
||||
return all_limits, decorated_limits
|
||||
|
||||
def exemption_scope(
|
||||
self, app: flask.Flask, endpoint: str | None, blueprint: str | None
|
||||
) -> ExemptionScope:
|
||||
view_func = app.view_functions.get(endpoint or "", None)
|
||||
name = get_qualified_name(view_func) if view_func else ""
|
||||
route_exemption_scope = self._route_exemptions.get(name, ExemptionScope.NONE)
|
||||
blueprint_instance = app.blueprints.get(blueprint) if blueprint else None
|
||||
|
||||
if not blueprint_instance:
|
||||
return route_exemption_scope
|
||||
else:
|
||||
assert blueprint
|
||||
(
|
||||
blueprint_exemption_scope,
|
||||
ancestor_exemption_scopes,
|
||||
) = self._blueprint_exemption_scope(app, blueprint)
|
||||
if (
|
||||
blueprint_exemption_scope & ~(ExemptionScope.DEFAULT | ExemptionScope.APPLICATION)
|
||||
or ancestor_exemption_scopes
|
||||
):
|
||||
for exemption in ancestor_exemption_scopes.values():
|
||||
blueprint_exemption_scope |= exemption
|
||||
return route_exemption_scope | blueprint_exemption_scope
|
||||
|
||||
def decorated_limits(self, callable_name: str) -> list[RuntimeLimit]:
|
||||
limits = []
|
||||
if not self._route_exemptions.get(callable_name, ExemptionScope.NONE):
|
||||
if callable_name in self._decorated_limits:
|
||||
for group in self._decorated_limits[callable_name]:
|
||||
try:
|
||||
for limit in group:
|
||||
limits.append(limit)
|
||||
except ValueError as e:
|
||||
self._logger.error(
|
||||
f"failed to load ratelimit for function {callable_name}: {e}",
|
||||
)
|
||||
return limits
|
||||
|
||||
def blueprint_limits(self, app: flask.Flask, blueprint: str) -> list[RuntimeLimit]:
|
||||
limits: list[RuntimeLimit] = []
|
||||
|
||||
blueprint_instance = app.blueprints.get(blueprint) if blueprint else None
|
||||
if blueprint_instance:
|
||||
blueprint_name = blueprint_instance.name
|
||||
blueprint_ancestory = set(blueprint.split(".") if blueprint else [])
|
||||
|
||||
self_exemption, ancestor_exemptions = self._blueprint_exemption_scope(app, blueprint)
|
||||
|
||||
if not (self_exemption & ~(ExemptionScope.DEFAULT | ExemptionScope.APPLICATION)):
|
||||
blueprint_self_limits = self._blueprint_limits.get(blueprint_name, OrderedSet())
|
||||
blueprint_limits: Iterable[Limit] = (
|
||||
itertools.chain(
|
||||
*(
|
||||
self._blueprint_limits.get(member, [])
|
||||
for member in blueprint_ancestory.intersection(
|
||||
self._blueprint_limits
|
||||
).difference(ancestor_exemptions)
|
||||
)
|
||||
)
|
||||
if not (
|
||||
blueprint_self_limits
|
||||
and all(limit.override_defaults for limit in blueprint_self_limits)
|
||||
)
|
||||
and not self._blueprint_exemptions.get(blueprint_name, ExemptionScope.NONE)
|
||||
& ExemptionScope.ANCESTORS
|
||||
else blueprint_self_limits
|
||||
)
|
||||
if blueprint_limits:
|
||||
for limit_group in blueprint_limits:
|
||||
try:
|
||||
limits.extend(
|
||||
[
|
||||
RuntimeLimit(
|
||||
limit.limit,
|
||||
limit.key_func,
|
||||
limit.scope,
|
||||
limit.per_method,
|
||||
limit.methods,
|
||||
limit.error_message,
|
||||
limit.exempt_when,
|
||||
limit.override_defaults,
|
||||
limit.deduct_when,
|
||||
limit.on_breach,
|
||||
limit.cost,
|
||||
limit.shared,
|
||||
)
|
||||
for limit in limit_group
|
||||
]
|
||||
)
|
||||
except ValueError as e:
|
||||
self._logger.error(
|
||||
f"failed to load ratelimit for blueprint {blueprint_name}: {e}",
|
||||
)
|
||||
return limits
|
||||
|
||||
def _blueprint_exemption_scope(
|
||||
self, app: flask.Flask, blueprint_name: str
|
||||
) -> tuple[ExemptionScope, dict[str, ExemptionScope]]:
|
||||
name = app.blueprints[blueprint_name].name
|
||||
exemption = self._blueprint_exemptions.get(name, ExemptionScope.NONE) & ~(
|
||||
ExemptionScope.ANCESTORS
|
||||
)
|
||||
|
||||
ancestory = set(blueprint_name.split("."))
|
||||
ancestor_exemption = {
|
||||
k for k, f in self._blueprint_exemptions.items() if f & ExemptionScope.DESCENDENTS
|
||||
}.intersection(ancestory)
|
||||
|
||||
return exemption, {
|
||||
k: self._blueprint_exemptions.get(k, ExemptionScope.NONE) for k in ancestor_exemption
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Generator, Iterable, Iterator, Sequence
|
||||
from typing import (
|
||||
ParamSpec,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
R = TypeVar("R")
|
||||
P = ParamSpec("P")
|
||||
|
||||
__all__ = [
|
||||
"Callable",
|
||||
"Generator",
|
||||
"Iterable",
|
||||
"Iterator",
|
||||
"P",
|
||||
"R",
|
||||
"Sequence",
|
||||
"Self",
|
||||
"TypeVar",
|
||||
"cast",
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
# file generated by setuptools-scm
|
||||
# don't change, don't track in version control
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"__version_tuple__",
|
||||
"version",
|
||||
"version_tuple",
|
||||
"__commit_id__",
|
||||
"commit_id",
|
||||
]
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
||||
COMMIT_ID = Union[str, None]
|
||||
else:
|
||||
VERSION_TUPLE = object
|
||||
COMMIT_ID = object
|
||||
|
||||
version: str
|
||||
__version__: str
|
||||
__version_tuple__: VERSION_TUPLE
|
||||
version_tuple: VERSION_TUPLE
|
||||
commit_id: COMMIT_ID
|
||||
__commit_id__: COMMIT_ID
|
||||
|
||||
__version__ = version = '4.0.0'
|
||||
__version_tuple__ = version_tuple = (4, 0, 0)
|
||||
|
||||
__commit_id__ = commit_id = None
|
||||
@@ -0,0 +1 @@
|
||||
__version__: str
|
||||
563
buffteks/lib/python3.11/site-packages/flask_limiter/commands.py
Normal file
563
buffteks/lib/python3.11/site-packages/flask_limiter/commands.py
Normal file
@@ -0,0 +1,563 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import time
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import click
|
||||
from flask import Flask, current_app
|
||||
from flask.cli import with_appcontext
|
||||
from limits.strategies import RateLimiter
|
||||
from rich.console import Console, group
|
||||
from rich.live import Live
|
||||
from rich.pretty import Pretty
|
||||
from rich.prompt import Confirm
|
||||
from rich.table import Table
|
||||
from rich.theme import Theme
|
||||
from rich.tree import Tree
|
||||
from typing_extensions import TypedDict
|
||||
from werkzeug.exceptions import MethodNotAllowed, NotFound
|
||||
from werkzeug.routing import Rule
|
||||
|
||||
from ._extension import Limiter
|
||||
from ._limits import RuntimeLimit
|
||||
from ._typing import Callable, Generator, cast
|
||||
from .constants import ConfigVars, ExemptionScope, HeaderNames
|
||||
from .util import get_qualified_name
|
||||
|
||||
limiter_theme = Theme(
|
||||
{
|
||||
"success": "bold green",
|
||||
"danger": "bold red",
|
||||
"error": "bold red",
|
||||
"blueprint": "bold red",
|
||||
"default": "magenta",
|
||||
"callable": "cyan",
|
||||
"entity": "magenta",
|
||||
"exempt": "bold red",
|
||||
"route": "yellow",
|
||||
"http": "bold green",
|
||||
"option": "bold yellow",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def render_func(func: Any) -> str | Pretty:
|
||||
if callable(func):
|
||||
if func.__name__ == "<lambda>":
|
||||
return f"[callable]<lambda>({func.__module__})[/callable]"
|
||||
return f"[callable]{func.__module__}.{func.__name__}()[/callable]"
|
||||
return Pretty(func)
|
||||
|
||||
|
||||
def render_storage(ext: Limiter) -> Tree:
|
||||
render = Tree(ext._storage_uri or "N/A")
|
||||
if ext.storage:
|
||||
render.add(f"[entity]{ext.storage.__class__.__name__}[/entity]")
|
||||
render.add(f"[entity]{ext.storage.storage}[/entity]") # type: ignore
|
||||
render.add(Pretty(ext._storage_options or {}))
|
||||
health = ext.storage.check()
|
||||
if health:
|
||||
render.add("[success]OK[/success]")
|
||||
else:
|
||||
render.add("[error]Error[/error]")
|
||||
return render
|
||||
|
||||
|
||||
def render_strategy(strategy: RateLimiter) -> str:
|
||||
return f"[entity]{strategy.__class__.__name__}[/entity]"
|
||||
|
||||
|
||||
def render_limit_state(
|
||||
limiter: Limiter, endpoint: str, limit: RuntimeLimit, key: str, method: str
|
||||
) -> str:
|
||||
args = [key, limit.scope_for(endpoint, method)]
|
||||
if not limiter.storage or (limiter.storage and not limiter.storage.check()):
|
||||
return ": [error]Storage not available[/error]"
|
||||
test = limiter.limiter.test(limit.limit, *args)
|
||||
stats = limiter.limiter.get_window_stats(limit.limit, *args)
|
||||
if not test:
|
||||
return f": [error]Fail[/error] ({stats[1]} out of {limit.limit.amount} remaining)"
|
||||
else:
|
||||
return f": [success]Pass[/success] ({stats[1]} out of {limit.limit.amount} remaining)"
|
||||
|
||||
|
||||
def render_limit(limit: RuntimeLimit, simple: bool = True) -> str:
|
||||
render = str(limit.limit)
|
||||
if simple:
|
||||
return render
|
||||
options = []
|
||||
if limit.deduct_when:
|
||||
options.append(f"deduct_when: {render_func(limit.deduct_when)}")
|
||||
if limit.exempt_when:
|
||||
options.append(f"exempt_when: {render_func(limit.exempt_when)}")
|
||||
if options:
|
||||
render = f"{render} [option]{{{', '.join(options)}}}[/option]"
|
||||
return render
|
||||
|
||||
|
||||
def render_limits(
|
||||
app: Flask,
|
||||
limiter: Limiter,
|
||||
limits: tuple[list[RuntimeLimit], ...],
|
||||
endpoint: str | None = None,
|
||||
blueprint: str | None = None,
|
||||
rule: Rule | None = None,
|
||||
exemption_scope: ExemptionScope = ExemptionScope.NONE,
|
||||
test: str | None = None,
|
||||
method: str = "GET",
|
||||
label: str | None = "",
|
||||
) -> Tree:
|
||||
_label = None
|
||||
if rule and endpoint:
|
||||
_label = f"{endpoint}: {rule}"
|
||||
label = _label or label or ""
|
||||
|
||||
renderable = Tree(label)
|
||||
entries = []
|
||||
|
||||
for limit in limits[0] + limits[1]:
|
||||
if endpoint:
|
||||
view_func = app.view_functions.get(endpoint, None)
|
||||
source = (
|
||||
"blueprint"
|
||||
if blueprint and limit in limiter.limit_manager.blueprint_limits(app, blueprint)
|
||||
else (
|
||||
"route"
|
||||
if limit
|
||||
in limiter.limit_manager.decorated_limits(
|
||||
get_qualified_name(view_func) if view_func else ""
|
||||
)
|
||||
else "default"
|
||||
)
|
||||
)
|
||||
else:
|
||||
source = "default"
|
||||
if limit.per_method and rule and rule.methods:
|
||||
for method in rule.methods:
|
||||
rendered = render_limit(limit, False)
|
||||
entry = f"[{source}]{rendered} [http]({method})[/http][/{source}]"
|
||||
if test:
|
||||
entry += render_limit_state(limiter, endpoint or "", limit, test, method)
|
||||
entries.append(entry)
|
||||
else:
|
||||
rendered = render_limit(limit, False)
|
||||
entry = f"[{source}]{rendered}[/{source}]"
|
||||
if test:
|
||||
entry += render_limit_state(limiter, endpoint or "", limit, test, method)
|
||||
entries.append(entry)
|
||||
if not entries and exemption_scope:
|
||||
renderable.add("[exempt]Exempt[/exempt]")
|
||||
else:
|
||||
[renderable.add(entry) for entry in entries]
|
||||
return renderable
|
||||
|
||||
|
||||
def get_filtered_endpoint(
|
||||
app: Flask,
|
||||
console: Console,
|
||||
endpoint: str | None,
|
||||
path: str | None,
|
||||
method: str | None = None,
|
||||
) -> str | None:
|
||||
if not (endpoint or path):
|
||||
return None
|
||||
if endpoint:
|
||||
if endpoint in current_app.view_functions:
|
||||
return endpoint
|
||||
else:
|
||||
console.print(f"[red]Error: {endpoint} not found")
|
||||
elif path:
|
||||
adapter = app.url_map.bind("dev.null")
|
||||
parsed = urlparse(path)
|
||||
try:
|
||||
filter_endpoint, _ = adapter.match(parsed.path, method=method, query_args=parsed.query)
|
||||
return cast(str, filter_endpoint)
|
||||
except NotFound:
|
||||
console.print(f"[error]Error: {path} could not be matched to an endpoint[/error]")
|
||||
except MethodNotAllowed:
|
||||
assert method
|
||||
console.print(
|
||||
f"[error]Error: {method.upper()}: {path}"
|
||||
" could not be matched to an endpoint[/error]"
|
||||
)
|
||||
raise SystemExit
|
||||
|
||||
|
||||
@click.group(help="Flask-Limiter maintenance & utility commmands")
|
||||
def cli() -> None:
|
||||
pass
|
||||
|
||||
|
||||
@cli.command(help="View the extension configuration")
|
||||
@with_appcontext
|
||||
def config() -> None:
|
||||
with current_app.test_request_context():
|
||||
console = Console(theme=limiter_theme)
|
||||
limiters = list(current_app.extensions.get("limiter", set()))
|
||||
limiter = limiters and list(limiters)[0]
|
||||
if limiter:
|
||||
extension_details = Table(title="Flask-Limiter Config")
|
||||
extension_details.add_column("Notes")
|
||||
extension_details.add_column("Configuration")
|
||||
extension_details.add_column("Value")
|
||||
extension_details.add_row("Enabled", ConfigVars.ENABLED, Pretty(limiter.enabled))
|
||||
extension_details.add_row(
|
||||
"Key Function", ConfigVars.KEY_FUNC, render_func(limiter._key_func)
|
||||
)
|
||||
extension_details.add_row(
|
||||
"Key Prefix", ConfigVars.KEY_PREFIX, Pretty(limiter._key_prefix)
|
||||
)
|
||||
limiter_config = Tree(ConfigVars.STRATEGY)
|
||||
limiter_config_values = Tree(render_strategy(limiter.limiter))
|
||||
node = limiter_config.add(ConfigVars.STORAGE_URI)
|
||||
node.add("Instance")
|
||||
node.add("Backend")
|
||||
limiter_config.add(ConfigVars.STORAGE_OPTIONS)
|
||||
limiter_config.add("Status")
|
||||
limiter_config_values.add(render_storage(limiter))
|
||||
extension_details.add_row("Rate Limiting Config", limiter_config, limiter_config_values)
|
||||
if limiter.limit_manager.application_limits:
|
||||
extension_details.add_row(
|
||||
"Application Limits",
|
||||
ConfigVars.APPLICATION_LIMITS,
|
||||
Pretty(
|
||||
[render_limit(limit) for limit in limiter.limit_manager.application_limits]
|
||||
),
|
||||
)
|
||||
extension_details.add_row(
|
||||
None,
|
||||
ConfigVars.APPLICATION_LIMITS_PER_METHOD,
|
||||
Pretty(limiter._application_limits_per_method),
|
||||
)
|
||||
extension_details.add_row(
|
||||
None,
|
||||
ConfigVars.APPLICATION_LIMITS_EXEMPT_WHEN,
|
||||
render_func(limiter._application_limits_exempt_when),
|
||||
)
|
||||
extension_details.add_row(
|
||||
None,
|
||||
ConfigVars.APPLICATION_LIMITS_DEDUCT_WHEN,
|
||||
render_func(limiter._application_limits_deduct_when),
|
||||
)
|
||||
extension_details.add_row(
|
||||
None,
|
||||
ConfigVars.APPLICATION_LIMITS_COST,
|
||||
Pretty(limiter._application_limits_cost),
|
||||
)
|
||||
else:
|
||||
extension_details.add_row(
|
||||
"ApplicationLimits Limits",
|
||||
ConfigVars.APPLICATION_LIMITS,
|
||||
Pretty([]),
|
||||
)
|
||||
if limiter.limit_manager.default_limits:
|
||||
extension_details.add_row(
|
||||
"Default Limits",
|
||||
ConfigVars.DEFAULT_LIMITS,
|
||||
Pretty([render_limit(limit) for limit in limiter.limit_manager.default_limits]),
|
||||
)
|
||||
extension_details.add_row(
|
||||
None,
|
||||
ConfigVars.DEFAULT_LIMITS_PER_METHOD,
|
||||
Pretty(limiter._default_limits_per_method),
|
||||
)
|
||||
extension_details.add_row(
|
||||
None,
|
||||
ConfigVars.DEFAULT_LIMITS_EXEMPT_WHEN,
|
||||
render_func(limiter._default_limits_exempt_when),
|
||||
)
|
||||
extension_details.add_row(
|
||||
None,
|
||||
ConfigVars.DEFAULT_LIMITS_DEDUCT_WHEN,
|
||||
render_func(limiter._default_limits_deduct_when),
|
||||
)
|
||||
extension_details.add_row(
|
||||
None,
|
||||
ConfigVars.DEFAULT_LIMITS_COST,
|
||||
render_func(limiter._default_limits_cost),
|
||||
)
|
||||
else:
|
||||
extension_details.add_row("Default Limits", ConfigVars.DEFAULT_LIMITS, Pretty([]))
|
||||
|
||||
if limiter._meta_limits:
|
||||
extension_details.add_row(
|
||||
"Meta Limits",
|
||||
ConfigVars.META_LIMITS,
|
||||
Pretty(
|
||||
[render_limit(limit) for limit in itertools.chain(*limiter._meta_limits)]
|
||||
),
|
||||
)
|
||||
if limiter._headers_enabled:
|
||||
header_configs = Tree(ConfigVars.HEADERS_ENABLED)
|
||||
header_configs.add(ConfigVars.HEADER_RESET)
|
||||
header_configs.add(ConfigVars.HEADER_REMAINING)
|
||||
header_configs.add(ConfigVars.HEADER_RETRY_AFTER)
|
||||
header_configs.add(ConfigVars.HEADER_RETRY_AFTER_VALUE)
|
||||
|
||||
header_values = Tree(Pretty(limiter._headers_enabled))
|
||||
header_values.add(Pretty(limiter._header_mapping[HeaderNames.RESET]))
|
||||
header_values.add(Pretty(limiter._header_mapping[HeaderNames.REMAINING]))
|
||||
header_values.add(Pretty(limiter._header_mapping[HeaderNames.RETRY_AFTER]))
|
||||
header_values.add(Pretty(limiter._retry_after))
|
||||
extension_details.add_row(
|
||||
"Header configuration",
|
||||
header_configs,
|
||||
header_values,
|
||||
)
|
||||
else:
|
||||
extension_details.add_row(
|
||||
"Header configuration", ConfigVars.HEADERS_ENABLED, Pretty(False)
|
||||
)
|
||||
|
||||
extension_details.add_row(
|
||||
"Fail on first breach",
|
||||
ConfigVars.FAIL_ON_FIRST_BREACH,
|
||||
Pretty(limiter._fail_on_first_breach),
|
||||
)
|
||||
extension_details.add_row(
|
||||
"On breach callback",
|
||||
ConfigVars.ON_BREACH,
|
||||
render_func(limiter._on_breach),
|
||||
)
|
||||
|
||||
console.print(extension_details)
|
||||
else:
|
||||
console.print(
|
||||
f"No Flask-Limiter extension installed on {current_app}",
|
||||
style="bold red",
|
||||
)
|
||||
|
||||
|
||||
@cli.command(help="Enumerate details about all routes with rate limits")
|
||||
@click.option("--endpoint", default=None, help="Endpoint to filter by")
|
||||
@click.option("--path", default=None, help="Path to filter by")
|
||||
@click.option("--method", default=None, help="HTTP Method to filter by")
|
||||
@click.option("--key", default=None, help="Test the limit")
|
||||
@click.option("--watch/--no-watch", default=False, help="Create a live dashboard")
|
||||
@with_appcontext
|
||||
def limits(
|
||||
endpoint: str | None = None,
|
||||
path: str | None = None,
|
||||
method: str = "GET",
|
||||
key: str | None = None,
|
||||
watch: bool = False,
|
||||
) -> None:
|
||||
with current_app.test_request_context():
|
||||
limiters: set[Limiter] = current_app.extensions.get("limiter", set())
|
||||
limiter: Limiter | None = list(limiters)[0] if limiters else None
|
||||
console = Console(theme=limiter_theme)
|
||||
if limiter:
|
||||
manager = limiter.limit_manager
|
||||
groups: dict[str, list[Callable[..., Tree]]] = {}
|
||||
|
||||
filter_endpoint = get_filtered_endpoint(current_app, console, endpoint, path, method)
|
||||
for rule in sorted(
|
||||
current_app.url_map.iter_rules(filter_endpoint), key=lambda r: str(r)
|
||||
):
|
||||
rule_endpoint = rule.endpoint
|
||||
if rule_endpoint == "static":
|
||||
continue
|
||||
if len(rule_endpoint.split(".")) > 1:
|
||||
bp_fullname = ".".join(rule_endpoint.split(".")[:-1])
|
||||
groups.setdefault(bp_fullname, []).append(
|
||||
partial(
|
||||
render_limits,
|
||||
current_app,
|
||||
limiter,
|
||||
manager.resolve_limits(current_app, rule_endpoint, bp_fullname),
|
||||
rule_endpoint,
|
||||
bp_fullname,
|
||||
rule,
|
||||
exemption_scope=manager.exemption_scope(
|
||||
current_app, rule_endpoint, bp_fullname
|
||||
),
|
||||
method=method,
|
||||
test=key,
|
||||
)
|
||||
)
|
||||
else:
|
||||
groups.setdefault("root", []).append(
|
||||
partial(
|
||||
render_limits,
|
||||
current_app,
|
||||
limiter,
|
||||
manager.resolve_limits(current_app, rule_endpoint, ""),
|
||||
rule_endpoint,
|
||||
None,
|
||||
rule,
|
||||
exemption_scope=manager.exemption_scope(
|
||||
current_app, rule_endpoint, None
|
||||
),
|
||||
method=method,
|
||||
test=key,
|
||||
)
|
||||
)
|
||||
|
||||
@group()
|
||||
def console_renderable() -> Generator: # type: ignore
|
||||
if limiter and limiter.limit_manager.application_limits and not (endpoint or path):
|
||||
yield render_limits(
|
||||
current_app,
|
||||
limiter,
|
||||
(list(itertools.chain(*limiter._meta_limits)), []),
|
||||
test=key,
|
||||
method=method,
|
||||
label="[gold3]Meta Limits[/gold3]",
|
||||
)
|
||||
yield render_limits(
|
||||
current_app,
|
||||
limiter,
|
||||
(limiter.limit_manager.application_limits, []),
|
||||
test=key,
|
||||
method=method,
|
||||
label="[gold3]Application Limits[/gold3]",
|
||||
)
|
||||
for name in groups:
|
||||
if name == "root":
|
||||
group_tree = Tree(f"[gold3]{current_app.name}[/gold3]")
|
||||
else:
|
||||
group_tree = Tree(f"[blue]{name}[/blue]")
|
||||
[group_tree.add(renderable()) for renderable in groups[name]]
|
||||
yield group_tree
|
||||
|
||||
if not watch:
|
||||
console.print(console_renderable())
|
||||
else: # noqa
|
||||
with Live(
|
||||
console_renderable(),
|
||||
console=console,
|
||||
refresh_per_second=0.4,
|
||||
screen=True,
|
||||
) as live:
|
||||
while True:
|
||||
try:
|
||||
live.update(console_renderable())
|
||||
time.sleep(0.4)
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
else:
|
||||
console.print(
|
||||
f"No Flask-Limiter extension installed on {current_app}",
|
||||
style="bold red",
|
||||
)
|
||||
|
||||
|
||||
@cli.command(help="Clear limits for a specific key")
|
||||
@click.option("--endpoint", default=None, help="Endpoint to filter by")
|
||||
@click.option("--path", default=None, help="Path to filter by")
|
||||
@click.option("--method", default=None, help="HTTP Method to filter by")
|
||||
@click.option("--key", default=None, required=True, help="Key to reset the limits for")
|
||||
@click.option("-y", is_flag=True, help="Skip prompt for confirmation")
|
||||
@with_appcontext
|
||||
def clear(
|
||||
key: str,
|
||||
endpoint: str | None = None,
|
||||
path: str | None = None,
|
||||
method: str = "GET",
|
||||
y: bool = False,
|
||||
) -> None:
|
||||
with current_app.test_request_context():
|
||||
limiters = list(current_app.extensions.get("limiter", set()))
|
||||
limiter: Limiter | None = limiters[0] if limiters else None
|
||||
console = Console(theme=limiter_theme)
|
||||
if limiter:
|
||||
manager = limiter.limit_manager
|
||||
filter_endpoint = get_filtered_endpoint(current_app, console, endpoint, path, method)
|
||||
|
||||
class Details(TypedDict):
|
||||
rule: Rule
|
||||
limits: tuple[list[RuntimeLimit], ...]
|
||||
|
||||
rule_limits: dict[str, Details] = {}
|
||||
for rule in sorted(
|
||||
current_app.url_map.iter_rules(filter_endpoint), key=lambda r: str(r)
|
||||
):
|
||||
rule_endpoint = rule.endpoint
|
||||
if rule_endpoint == "static":
|
||||
continue
|
||||
if len(rule_endpoint.split(".")) > 1:
|
||||
bp_fullname = ".".join(rule_endpoint.split(".")[:-1])
|
||||
rule_limits[rule_endpoint] = Details(
|
||||
rule=rule,
|
||||
limits=manager.resolve_limits(current_app, rule_endpoint, bp_fullname),
|
||||
)
|
||||
else:
|
||||
rule_limits[rule_endpoint] = Details(
|
||||
rule=rule,
|
||||
limits=manager.resolve_limits(current_app, rule_endpoint, ""),
|
||||
)
|
||||
application_limits = None
|
||||
if not filter_endpoint:
|
||||
application_limits = limiter.limit_manager.application_limits
|
||||
|
||||
if not y: # noqa
|
||||
if application_limits:
|
||||
console.print(
|
||||
render_limits(
|
||||
current_app,
|
||||
limiter,
|
||||
(application_limits, []),
|
||||
label="Application Limits",
|
||||
test=key,
|
||||
)
|
||||
)
|
||||
for endpoint, details in rule_limits.items():
|
||||
if details["limits"]:
|
||||
console.print(
|
||||
render_limits(
|
||||
current_app,
|
||||
limiter,
|
||||
details["limits"],
|
||||
endpoint,
|
||||
rule=details["rule"],
|
||||
test=key,
|
||||
)
|
||||
)
|
||||
if y or Confirm.ask(f"Proceed with resetting limits for key: [danger]{key}[/danger]?"):
|
||||
if application_limits:
|
||||
node = Tree("Application Limits")
|
||||
for limit in application_limits:
|
||||
limiter.limiter.clear(
|
||||
limit.limit,
|
||||
key,
|
||||
limit.scope_for("", method),
|
||||
)
|
||||
node.add(f"{render_limit(limit)}: [success]Cleared[/success]")
|
||||
console.print(node)
|
||||
for endpoint, details in rule_limits.items():
|
||||
if details["limits"]:
|
||||
node = Tree(endpoint)
|
||||
default, decorated = details["limits"]
|
||||
for limit in default + decorated:
|
||||
if (
|
||||
limit.per_method
|
||||
and details["rule"]
|
||||
and details["rule"].methods
|
||||
and not method
|
||||
):
|
||||
for rule_method in details["rule"].methods:
|
||||
limiter.limiter.clear(
|
||||
limit.limit,
|
||||
key,
|
||||
limit.scope_for(endpoint, rule_method),
|
||||
)
|
||||
else:
|
||||
limiter.limiter.clear(
|
||||
limit.limit,
|
||||
key,
|
||||
limit.scope_for(endpoint, method),
|
||||
)
|
||||
node.add(f"{render_limit(limit)}: [success]Cleared[/success]")
|
||||
console.print(node)
|
||||
else:
|
||||
console.print(
|
||||
f"No Flask-Limiter extension installed on {current_app}",
|
||||
style="bold red",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__": # noqa
|
||||
cli()
|
||||
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class ConfigVars:
|
||||
ENABLED = "RATELIMIT_ENABLED"
|
||||
KEY_FUNC = "RATELIMIT_KEY_FUNC"
|
||||
KEY_PREFIX = "RATELIMIT_KEY_PREFIX"
|
||||
FAIL_ON_FIRST_BREACH = "RATELIMIT_FAIL_ON_FIRST_BREACH"
|
||||
ON_BREACH = "RATELIMIT_ON_BREACH_CALLBACK"
|
||||
SWALLOW_ERRORS = "RATELIMIT_SWALLOW_ERRORS"
|
||||
APPLICATION_LIMITS = "RATELIMIT_APPLICATION"
|
||||
APPLICATION_LIMITS_PER_METHOD = "RATELIMIT_APPLICATION_PER_METHOD"
|
||||
APPLICATION_LIMITS_EXEMPT_WHEN = "RATELIMIT_APPLICATION_EXEMPT_WHEN"
|
||||
APPLICATION_LIMITS_DEDUCT_WHEN = "RATELIMIT_APPLICATION_DEDUCT_WHEN"
|
||||
APPLICATION_LIMITS_COST = "RATELIMIT_APPLICATION_COST"
|
||||
DEFAULT_LIMITS = "RATELIMIT_DEFAULT"
|
||||
DEFAULT_LIMITS_PER_METHOD = "RATELIMIT_DEFAULTS_PER_METHOD"
|
||||
DEFAULT_LIMITS_EXEMPT_WHEN = "RATELIMIT_DEFAULTS_EXEMPT_WHEN"
|
||||
DEFAULT_LIMITS_DEDUCT_WHEN = "RATELIMIT_DEFAULTS_DEDUCT_WHEN"
|
||||
DEFAULT_LIMITS_COST = "RATELIMIT_DEFAULTS_COST"
|
||||
REQUEST_IDENTIFIER = "RATELIMIT_REQUEST_IDENTIFIER"
|
||||
STRATEGY = "RATELIMIT_STRATEGY"
|
||||
STORAGE_URI = "RATELIMIT_STORAGE_URI"
|
||||
STORAGE_OPTIONS = "RATELIMIT_STORAGE_OPTIONS"
|
||||
HEADERS_ENABLED = "RATELIMIT_HEADERS_ENABLED"
|
||||
HEADER_LIMIT = "RATELIMIT_HEADER_LIMIT"
|
||||
HEADER_REMAINING = "RATELIMIT_HEADER_REMAINING"
|
||||
HEADER_RESET = "RATELIMIT_HEADER_RESET"
|
||||
HEADER_RETRY_AFTER = "RATELIMIT_HEADER_RETRY_AFTER"
|
||||
HEADER_RETRY_AFTER_VALUE = "RATELIMIT_HEADER_RETRY_AFTER_VALUE"
|
||||
IN_MEMORY_FALLBACK = "RATELIMIT_IN_MEMORY_FALLBACK"
|
||||
IN_MEMORY_FALLBACK_ENABLED = "RATELIMIT_IN_MEMORY_FALLBACK_ENABLED"
|
||||
META_LIMITS = "RATELIMIT_META"
|
||||
ON_META_BREACH = "RATELIMIT_ON_META_BREACH_CALLBACK"
|
||||
|
||||
|
||||
class HeaderNames(enum.Enum):
|
||||
"""
|
||||
Enumeration of supported rate limit related headers to
|
||||
be used when configuring via :paramref:`~flask_limiter.Limiter.header_name_mapping`
|
||||
"""
|
||||
|
||||
#: Timestamp at which this rate limit will be reset
|
||||
RESET = "X-RateLimit-Reset"
|
||||
#: Remaining number of requests within the current window
|
||||
REMAINING = "X-RateLimit-Remaining"
|
||||
#: Total number of allowed requests within a window
|
||||
LIMIT = "X-RateLimit-Limit"
|
||||
#: Number of seconds to retry after at
|
||||
RETRY_AFTER = "Retry-After"
|
||||
|
||||
|
||||
class ExemptionScope(enum.Flag):
|
||||
"""
|
||||
Flags used to configure the scope of exemption when used
|
||||
in conjunction with :meth:`~flask_limiter.Limiter.exempt`.
|
||||
"""
|
||||
|
||||
NONE = 0
|
||||
|
||||
#: Exempt from application wide "global" limits
|
||||
APPLICATION = enum.auto()
|
||||
#: Exempts from meta limits
|
||||
META = enum.auto()
|
||||
#: Exempt from default limits configured on the extension
|
||||
DEFAULT = enum.auto()
|
||||
#: Exempts any nested blueprints. See :ref:`recipes:nested blueprints`
|
||||
DESCENDENTS = enum.auto()
|
||||
#: Exempt from any rate limits inherited from ancestor blueprints.
|
||||
#: See :ref:`recipes:nested blueprints`
|
||||
ANCESTORS = enum.auto()
|
||||
|
||||
|
||||
MAX_BACKEND_CHECKS = 5
|
||||
@@ -0,0 +1 @@
|
||||
"""Contributed 'recipes'"""
|
||||
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import request
|
||||
|
||||
|
||||
def get_remote_address_cloudflare() -> str:
|
||||
"""
|
||||
:return: the ip address for the current request from the CF-Connecting-IP header
|
||||
(or 127.0.0.1 if none found)
|
||||
|
||||
"""
|
||||
return request.headers["CF-Connecting-IP"] or "127.0.0.1"
|
||||
@@ -0,0 +1,29 @@
|
||||
"""errors and exceptions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from flask.wrappers import Response
|
||||
from werkzeug import exceptions
|
||||
|
||||
from ._limits import RuntimeLimit
|
||||
|
||||
|
||||
class RateLimitExceeded(exceptions.TooManyRequests):
|
||||
"""Exception raised when a rate limit is hit."""
|
||||
|
||||
def __init__(self, limit: RuntimeLimit, response: Response | None = None) -> None:
|
||||
"""
|
||||
:param limit: The actual rate limit that was hit. This is used to construct the default
|
||||
response message
|
||||
:param response: Optional pre constructed response. If provided it will be rendered by
|
||||
flask instead of the default error response of :class:`~werkzeug.exceptions.HTTPException`
|
||||
"""
|
||||
self.limit = limit
|
||||
self.response = response
|
||||
if limit.error_message:
|
||||
description = (
|
||||
limit.error_message if not callable(limit.error_message) else limit.error_message()
|
||||
)
|
||||
else:
|
||||
description = str(limit.limit)
|
||||
super().__init__(description=description, response=response)
|
||||
31
buffteks/lib/python3.11/site-packages/flask_limiter/util.py
Normal file
31
buffteks/lib/python3.11/site-packages/flask_limiter/util.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from flask import request
|
||||
|
||||
|
||||
def get_remote_address() -> str:
|
||||
"""
|
||||
:return: the ip address for the current request (or 127.0.0.1 if none found)
|
||||
|
||||
"""
|
||||
return request.remote_addr or "127.0.0.1"
|
||||
|
||||
|
||||
def get_qualified_name(callable: Callable[..., Any]) -> str:
|
||||
"""
|
||||
Generate the fully qualified name of a callable for use in storing mappings of decorated
|
||||
functions to rate limits
|
||||
|
||||
The __qualname__ of the callable is appended in case there is a name clash in a module due to
|
||||
locally scoped functions that are decorated.
|
||||
|
||||
TODO: Ideally __qualname__ should be enough, however view functions generated by class based
|
||||
views do not update that and therefore would not be uniquely identifiable unless
|
||||
__module__ & __name__ are inspected.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return f"{callable.__module__}.{callable.__name__}.{callable.__qualname__}"
|
||||
Reference in New Issue
Block a user