Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
accept.py351 linesDownload Raw Back to datastructures
1from __future__ import annotations2 3import codecs4import collections.abc as cabc5import re6import typing as t7 8from .structures import ImmutableList9 10 11class Accept(ImmutableList[tuple[str, float]]):12    """An :class:`Accept` object is just a list subclass for lists of13    ``(value, quality)`` tuples.  It is automatically sorted by specificity14    and quality.15 16    All :class:`Accept` objects work similar to a list but provide extra17    functionality for working with the data.  Containment checks are18    normalized to the rules of that header:19 20    >>> a = CharsetAccept([('ISO-8859-1', 1), ('utf-8', 0.7)])21    >>> a.best22    'ISO-8859-1'23    >>> 'iso-8859-1' in a24    True25    >>> 'UTF8' in a26    True27    >>> 'utf7' in a28    False29 30    To get the quality for an item you can use normal item lookup:31 32    >>> print a['utf-8']33    0.734    >>> a['utf7']35    036 37    .. versionchanged:: 0.538       :class:`Accept` objects are forced immutable now.39 40    .. versionchanged:: 1.0.041       :class:`Accept` internal values are no longer ordered42       alphabetically for equal quality tags. Instead the initial43       order is preserved.44 45    """46 47    def __init__(48        self, values: Accept | cabc.Iterable[tuple[str, float]] | None = ()49    ) -> None:50        if values is None:51            super().__init__()52            self.provided = False53        elif isinstance(values, Accept):54            self.provided = values.provided55            super().__init__(values)56        else:57            self.provided = True58            values = sorted(59                values, key=lambda x: (self._specificity(x[0]), x[1]), reverse=True60            )61            super().__init__(values)62 63    def _specificity(self, value: str) -> tuple[bool, ...]:64        """Returns a tuple describing the value's specificity."""65        return (value != "*",)66 67    def _value_matches(self, value: str, item: str) -> bool:68        """Check if a value matches a given accept item."""69        return item == "*" or item.lower() == value.lower()70 71    @t.overload72    def __getitem__(self, key: str) -> float: ...73    @t.overload74    def __getitem__(self, key: t.SupportsIndex) -> tuple[str, float]: ...75    @t.overload76    def __getitem__(self, key: slice) -> list[tuple[str, float]]: ...77    def __getitem__(78        self, key: str | t.SupportsIndex | slice79    ) -> float | tuple[str, float] | list[tuple[str, float]]:80        """Besides index lookup (getting item n) you can also pass it a string81        to get the quality for the item.  If the item is not in the list, the82        returned quality is ``0``.83        """84        if isinstance(key, str):85            return self.quality(key)86        return list.__getitem__(self, key)87 88    def quality(self, key: str) -> float:89        """Returns the quality of the key.90 91        .. versionadded:: 0.692           In previous versions you had to use the item-lookup syntax93           (eg: ``obj[key]`` instead of ``obj.quality(key)``)94        """95        for item, quality in self:96            if self._value_matches(key, item):97                return quality98        return 099 100    def __contains__(self, value: str) -> bool:  # type: ignore[override]101        for item, _quality in self:102            if self._value_matches(value, item):103                return True104        return False105 106    def __repr__(self) -> str:107        pairs_str = ", ".join(f"({x!r}, {y})" for x, y in self)108        return f"{type(self).__name__}([{pairs_str}])"109 110    def index(self, key: str | tuple[str, float]) -> int:  # type: ignore[override]111        """Get the position of an entry or raise :exc:`ValueError`.112 113        :param key: The key to be looked up.114 115        .. versionchanged:: 0.5116           This used to raise :exc:`IndexError`, which was inconsistent117           with the list API.118        """119        if isinstance(key, str):120            for idx, (item, _quality) in enumerate(self):121                if self._value_matches(key, item):122                    return idx123            raise ValueError(key)124        return list.index(self, key)125 126    def find(self, key: str | tuple[str, float]) -> int:127        """Get the position of an entry or return -1.128 129        :param key: The key to be looked up.130        """131        try:132            return self.index(key)133        except ValueError:134            return -1135 136    def values(self) -> cabc.Iterator[str]:137        """Iterate over all values."""138        for item in self:139            yield item[0]140 141    def to_header(self) -> str:142        """Convert the header set into an HTTP header string."""143        result = []144        for value, quality in self:145            if quality != 1:146                value = f"{value};q={quality}"147            result.append(value)148        return ",".join(result)149 150    def __str__(self) -> str:151        return self.to_header()152 153    def _best_single_match(self, match: str) -> tuple[str, float] | None:154        for client_item, quality in self:155            if self._value_matches(match, client_item):156                # self is sorted by specificity descending, we can exit157                return client_item, quality158        return None159 160    @t.overload161    def best_match(self, matches: cabc.Iterable[str]) -> str | None: ...162    @t.overload163    def best_match(self, matches: cabc.Iterable[str], default: str = ...) -> str: ...164    def best_match(165        self, matches: cabc.Iterable[str], default: str | None = None166    ) -> str | None:167        """Returns the best match from a list of possible matches based168        on the specificity and quality of the client. If two items have the169        same quality and specificity, the one is returned that comes first.170 171        :param matches: a list of matches to check for172        :param default: the value that is returned if none match173        """174        result = default175        best_quality: float = -1176        best_specificity: tuple[float, ...] = (-1,)177        for server_item in matches:178            match = self._best_single_match(server_item)179            if not match:180                continue181            client_item, quality = match182            specificity = self._specificity(client_item)183            if quality <= 0 or quality < best_quality:184                continue185            # better quality or same quality but more specific => better match186            if quality > best_quality or specificity > best_specificity:187                result = server_item188                best_quality = quality189                best_specificity = specificity190        return result191 192    @property193    def best(self) -> str | None:194        """The best match as value."""195        if self:196            return self[0][0]197 198        return None199 200 201_mime_split_re = re.compile(r"/|(?:\s*;\s*)")202 203 204def _normalize_mime(value: str) -> list[str]:205    return _mime_split_re.split(value.lower())206 207 208class MIMEAccept(Accept):209    """Like :class:`Accept` but with special methods and behavior for210    mimetypes.211    """212 213    def _specificity(self, value: str) -> tuple[bool, ...]:214        return tuple(x != "*" for x in _mime_split_re.split(value))215 216    def _value_matches(self, value: str, item: str) -> bool:217        # item comes from the client, can't match if it's invalid.218        if "/" not in item:219            return False220 221        # value comes from the application, tell the developer when it222        # doesn't look valid.223        if "/" not in value:224            raise ValueError(f"invalid mimetype {value!r}")225 226        # Split the match value into type, subtype, and a sorted list of parameters.227        normalized_value = _normalize_mime(value)228        value_type, value_subtype = normalized_value[:2]229        value_params = sorted(normalized_value[2:])230 231        # "*/*" is the only valid value that can start with "*".232        if value_type == "*" and value_subtype != "*":233            raise ValueError(f"invalid mimetype {value!r}")234 235        # Split the accept item into type, subtype, and parameters.236        normalized_item = _normalize_mime(item)237        item_type, item_subtype = normalized_item[:2]238        item_params = sorted(normalized_item[2:])239 240        # "*/not-*" from the client is invalid, can't match.241        if item_type == "*" and item_subtype != "*":242            return False243 244        return (245            (item_type == "*" and item_subtype == "*")246            or (value_type == "*" and value_subtype == "*")247        ) or (248            item_type == value_type249            and (250                item_subtype == "*"251                or value_subtype == "*"252                or (item_subtype == value_subtype and item_params == value_params)253            )254        )255 256    @property257    def accept_html(self) -> bool:258        """True if this object accepts HTML."""259        return "text/html" in self or self.accept_xhtml  # type: ignore[comparison-overlap]260 261    @property262    def accept_xhtml(self) -> bool:263        """True if this object accepts XHTML."""264        return "application/xhtml+xml" in self or "application/xml" in self  # type: ignore[comparison-overlap]265 266    @property267    def accept_json(self) -> bool:268        """True if this object accepts JSON."""269        return "application/json" in self  # type: ignore[comparison-overlap]270 271 272_locale_delim_re = re.compile(r"[_-]")273 274 275def _normalize_lang(value: str) -> list[str]:276    """Process a language tag for matching."""277    return _locale_delim_re.split(value.lower())278 279 280class LanguageAccept(Accept):281    """Like :class:`Accept` but with normalization for language tags."""282 283    def _value_matches(self, value: str, item: str) -> bool:284        return item == "*" or _normalize_lang(value) == _normalize_lang(item)285 286    @t.overload287    def best_match(self, matches: cabc.Iterable[str]) -> str | None: ...288    @t.overload289    def best_match(self, matches: cabc.Iterable[str], default: str = ...) -> str: ...290    def best_match(291        self, matches: cabc.Iterable[str], default: str | None = None292    ) -> str | None:293        """Given a list of supported values, finds the best match from294        the list of accepted values.295 296        Language tags are normalized for the purpose of matching, but297        are returned unchanged.298 299        If no exact match is found, this will fall back to matching300        the first subtag (primary language only), first with the301        accepted values then with the match values. This partial is not302        applied to any other language subtags.303 304        The default is returned if no exact or fallback match is found.305 306        :param matches: A list of supported languages to find a match.307        :param default: The value that is returned if none match.308        """309        # Look for an exact match first. If a client accepts "en-US",310        # "en-US" is a valid match at this point.311        result = super().best_match(matches)312 313        if result is not None:314            return result315 316        # Fall back to accepting primary tags. If a client accepts317        # "en-US", "en" is a valid match at this point. Need to use318        # re.split to account for 2 or 3 letter codes.319        fallback = Accept(320            [(_locale_delim_re.split(item[0], 1)[0], item[1]) for item in self]321        )322        result = fallback.best_match(matches)323 324        if result is not None:325            return result326 327        # Fall back to matching primary tags. If the client accepts328        # "en", "en-US" is a valid match at this point.329        fallback_matches = [_locale_delim_re.split(item, 1)[0] for item in matches]330        result = super().best_match(fallback_matches)331 332        # Return a value from the original match list. Find the first333        # original value that starts with the matched primary tag.334        if result is not None:335            return next(item for item in matches if item.startswith(result))336 337        return default338 339 340class CharsetAccept(Accept):341    """Like :class:`Accept` but with normalization for charsets."""342 343    def _value_matches(self, value: str, item: str) -> bool:344        def _normalize(name: str) -> str:345            try:346                return codecs.lookup(name).name347            except LookupError:348                return name.lower()349 350        return item == "*" or _normalize(value) == _normalize(item)351 
codekingpro/portable-devtools · Team Ai