You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					183 lines
				
				5.3 KiB
			
		
		
			
		
	
	
					183 lines
				
				5.3 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								"""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								"""
							 | 
						||
| 
								 | 
							
								import dataclasses
							 | 
						||
| 
								 | 
							
								import re
							 | 
						||
| 
								 | 
							
								import sys
							 | 
						||
| 
								 | 
							
								from collections import UserDict
							 | 
						||
| 
								 | 
							
								from types import ModuleType
							 | 
						||
| 
								 | 
							
								from typing import TYPE_CHECKING
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import pkg_resources
							 | 
						||
| 
								 | 
							
								from packaging.version import Version
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								from limits.typing import Dict, List, Optional, Tuple, Type, Union
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								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(
							 | 
						||
| 
								 | 
							
								    r"^{SINGLE}(:?{SEPARATORS}{SINGLE})*$".format(
							 | 
						||
| 
								 | 
							
								        SINGLE=SINGLE_EXPR.pattern, SEPARATORS=SEPARATORS.pattern
							 | 
						||
| 
								 | 
							
								    ),
							 | 
						||
| 
								 | 
							
								    re.IGNORECASE | re.VERBOSE,
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								@dataclasses.dataclass
							 | 
						||
| 
								 | 
							
								class Dependency:
							 | 
						||
| 
								 | 
							
								    name: str
							 | 
						||
| 
								 | 
							
								    version_required: Optional[Version]
							 | 
						||
| 
								 | 
							
								    version_found: Optional[Version]
							 | 
						||
| 
								 | 
							
								    module: ModuleType
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								if TYPE_CHECKING:
							 | 
						||
| 
								 | 
							
								    _UserDict = UserDict[str, Dependency]
							 | 
						||
| 
								 | 
							
								else:
							 | 
						||
| 
								 | 
							
								    _UserDict = UserDict
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class DependencyDict(_UserDict):
							 | 
						||
| 
								 | 
							
								    Missing = Dependency("Missing", None, None, ModuleType("Missing"))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def __getitem__(self, key: str) -> Dependency:
							 | 
						||
| 
								 | 
							
								        dependency = super().__getitem__(key)
							 | 
						||
| 
								 | 
							
								        if dependency == DependencyDict.Missing:
							 | 
						||
| 
								 | 
							
								            raise ConfigurationError(f"{key} prerequisite not available")
							 | 
						||
| 
								 | 
							
								        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" of {dependency.name} could not be 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: Union[Dict[str, Optional[Version]], 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, Optional[Version]]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            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)
							 | 
						||
| 
								 | 
							
								                if not dependency:
							 | 
						||
| 
								 | 
							
								                    dependencies[name] = DependencyDict.Missing
							 | 
						||
| 
								 | 
							
								                else:
							 | 
						||
| 
								 | 
							
								                    dependencies[name] = Dependency(
							 | 
						||
| 
								 | 
							
								                        name, minimum_version, version, dependency
							 | 
						||
| 
								 | 
							
								                    )
							 | 
						||
| 
								 | 
							
								            self._dependencies = dependencies
							 | 
						||
| 
								 | 
							
								        return self._dependencies
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_dependency(module_path: str) -> Tuple[Optional[ModuleType], Optional[Version]]:
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    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 None, None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_package_data(path: str) -> bytes:
							 | 
						||
| 
								 | 
							
								    return pkg_resources.resource_string(__name__, path)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								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("couldn't parse rate limit string '%s'" % 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("no granularity matched for %s" % granularity_string)
							 |