208 lines
5.9 KiB
Python
208 lines
5.9 KiB
Python
""" """
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import importlib.resources
|
|
import re
|
|
import sys
|
|
from collections import UserDict
|
|
from types import ModuleType
|
|
from typing import TYPE_CHECKING
|
|
|
|
from packaging.version import Version
|
|
|
|
from limits.typing import NamedTuple
|
|
|
|
from .errors import ConfigurationError
|
|
from .limits import GRANULARITIES, RateLimitItem
|
|
|
|
SEPARATORS = re.compile(r"[,;|]{1}")
|
|
SINGLE_EXPR = re.compile(
|
|
r"""
|
|
\s*([0-9]+)
|
|
\s*(/|\s*per\s*)
|
|
\s*([0-9]+)
|
|
*\s*(hour|minute|second|day|month|year)s?\s*""",
|
|
re.IGNORECASE | re.VERBOSE,
|
|
)
|
|
EXPR = re.compile(
|
|
rf"^{SINGLE_EXPR.pattern}(:?{SEPARATORS.pattern}{SINGLE_EXPR.pattern})*$",
|
|
re.IGNORECASE | re.VERBOSE,
|
|
)
|
|
|
|
|
|
class WindowStats(NamedTuple):
|
|
"""
|
|
tuple to describe a rate limited window
|
|
"""
|
|
|
|
#: Time as seconds since the Epoch when this window will be reset
|
|
reset_time: float
|
|
#: Quantity remaining in this window
|
|
remaining: int
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class Dependency:
|
|
name: str
|
|
version_required: Version | None
|
|
version_found: Version | None
|
|
module: ModuleType
|
|
|
|
|
|
MissingModule = ModuleType("Missing")
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
_UserDict = UserDict[str, Dependency]
|
|
else:
|
|
_UserDict = UserDict
|
|
|
|
|
|
class DependencyDict(_UserDict):
|
|
def __getitem__(self, key: str) -> Dependency:
|
|
dependency = super().__getitem__(key)
|
|
|
|
if dependency.module is MissingModule:
|
|
message = f"'{dependency.name}' prerequisite not available."
|
|
if dependency.version_required:
|
|
message += (
|
|
f" A minimum version of {dependency.version_required} is required."
|
|
if dependency.version_required
|
|
else ""
|
|
)
|
|
message += (
|
|
" See https://limits.readthedocs.io/en/stable/storage.html#supported-versions"
|
|
" for more details."
|
|
)
|
|
raise ConfigurationError(message)
|
|
elif dependency.version_required and (
|
|
not dependency.version_found
|
|
or dependency.version_found < dependency.version_required
|
|
):
|
|
raise ConfigurationError(
|
|
f"The minimum version of {dependency.version_required}"
|
|
f" for '{dependency.name}' could not be found. Found version: {dependency.version_found}"
|
|
)
|
|
|
|
return dependency
|
|
|
|
|
|
class LazyDependency:
|
|
"""
|
|
Simple utility that provides an :attr:`dependency`
|
|
to the child class to fetch any dependencies
|
|
without having to import them explicitly.
|
|
"""
|
|
|
|
DEPENDENCIES: dict[str, Version | None] | list[str] = []
|
|
"""
|
|
The python modules this class has a dependency on.
|
|
Used to lazily populate the :attr:`dependencies`
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._dependencies: DependencyDict = DependencyDict()
|
|
|
|
@property
|
|
def dependencies(self) -> DependencyDict:
|
|
"""
|
|
Cached mapping of the modules this storage depends on.
|
|
This is done so that the module is only imported lazily
|
|
when the storage is instantiated.
|
|
|
|
:meta private:
|
|
"""
|
|
|
|
if not getattr(self, "_dependencies", None):
|
|
dependencies = DependencyDict()
|
|
mapping: dict[str, Version | None]
|
|
|
|
if isinstance(self.DEPENDENCIES, list):
|
|
mapping = {dependency: None for dependency in self.DEPENDENCIES}
|
|
else:
|
|
mapping = self.DEPENDENCIES
|
|
|
|
for name, minimum_version in mapping.items():
|
|
dependency, version = get_dependency(name)
|
|
|
|
dependencies[name] = Dependency(
|
|
name, minimum_version, version, dependency
|
|
)
|
|
self._dependencies = dependencies
|
|
|
|
return self._dependencies
|
|
|
|
|
|
def get_dependency(module_path: str) -> tuple[ModuleType, Version | None]:
|
|
"""
|
|
safe function to import a module at runtime
|
|
"""
|
|
try:
|
|
if module_path not in sys.modules:
|
|
__import__(module_path)
|
|
root = module_path.split(".")[0]
|
|
version = getattr(sys.modules[root], "__version__", "0.0.0")
|
|
|
|
return sys.modules[module_path], Version(version)
|
|
except ImportError: # pragma: no cover
|
|
return MissingModule, None
|
|
|
|
|
|
def get_package_data(path: str) -> bytes:
|
|
return importlib.resources.files("limits").joinpath(path).read_bytes()
|
|
|
|
|
|
def parse_many(limit_string: str) -> list[RateLimitItem]:
|
|
"""
|
|
parses rate limits in string notation containing multiple rate limits
|
|
(e.g. ``1/second; 5/minute``)
|
|
|
|
:param limit_string: rate limit string using :ref:`ratelimit-string`
|
|
:raise ValueError: if the string notation is invalid.
|
|
|
|
"""
|
|
|
|
if not (isinstance(limit_string, str) and EXPR.match(limit_string)):
|
|
raise ValueError(f"couldn't parse rate limit string '{limit_string}'")
|
|
limits = []
|
|
|
|
for limit in SEPARATORS.split(limit_string):
|
|
match = SINGLE_EXPR.match(limit)
|
|
|
|
if match:
|
|
amount, _, multiples, granularity_string = match.groups()
|
|
granularity = granularity_from_string(granularity_string)
|
|
limits.append(
|
|
granularity(int(amount), multiples and int(multiples) or None)
|
|
)
|
|
|
|
return limits
|
|
|
|
|
|
def parse(limit_string: str) -> RateLimitItem:
|
|
"""
|
|
parses a single rate limit in string notation
|
|
(e.g. ``1/second`` or ``1 per second``)
|
|
|
|
:param limit_string: rate limit string using :ref:`ratelimit-string`
|
|
:raise ValueError: if the string notation is invalid.
|
|
|
|
"""
|
|
|
|
return list(parse_many(limit_string))[0]
|
|
|
|
|
|
def granularity_from_string(granularity_string: str) -> type[RateLimitItem]:
|
|
"""
|
|
|
|
:param granularity_string:
|
|
:raise ValueError:
|
|
"""
|
|
|
|
for granularity in GRANULARITIES.values():
|
|
if granularity.check_granularity_string(granularity_string):
|
|
return granularity
|
|
raise ValueError(f"no granularity matched for {granularity_string}")
|