from __future__ import annotations __all__ = [ "DeviceDict", "MatchersData", "OSDict", "UserAgentDict", "load_builtins", "load_data", "load_json", "load_lazy", "load_lazy_builtins", "load_yaml", ] import io import json import os from typing import ( TYPE_CHECKING, Callable, List, Literal, Optional, Protocol, Tuple, Type, TypedDict, Union, cast, ) from . import lazy, matchers from .core import Matchers if TYPE_CHECKING: PathOrFile = Union[str, os.PathLike[str], io.IOBase] SafeLoader: Optional[Type[object]] try: from yaml import CSafeLoader as SafeLoader, load except ImportError: try: from yaml import SafeLoader, load except ImportError: load = SafeLoader = None # type: ignore def load_builtins() -> Matchers: """Loads the pre-compiled matcher data into eager matchers. The matchers data is imported lazily, and cached after imports, further imports simply reference the existing datas. """ from ua_parser_builtins.matchers import MATCHERS # typing and mypy don't have safe upcast (#5756) and mypy is # unhappy about returning concrete matchers for a mixed type return cast(Matchers, MATCHERS) def load_lazy_builtins() -> Matchers: """Loads the pre-compiled matcher data into lazy matchers. The matchers data is imported lazily, and cached after imports, further imports simply reference the existing datas. """ from ua_parser_builtins.lazy import MATCHERS return cast(Matchers, MATCHERS) # superclass needed to mix required & optional typed dict entries # before 3.11 (and Required/NotRequired) class _RegexDict(TypedDict): regex: str class UserAgentDict(_RegexDict, total=False): family_replacement: str v1_replacement: str v2_replacement: str v3_replacement: str v4_replacement: str class OSDict(_RegexDict, total=False): os_replacement: str os_v1_replacement: str os_v2_replacement: str os_v3_replacement: str os_v4_replacement: str class DeviceDict(_RegexDict, total=False): regex_flag: Literal["i"] device_replacement: str brand_replacement: str model_replacement: str MatchersData = Tuple[List[UserAgentDict], List[OSDict], List[DeviceDict]] DataLoader = Callable[[MatchersData], Matchers] def load_data(d: MatchersData) -> Matchers: """Loads the input data set into eager matchers.""" return ( [ matchers.UserAgentMatcher( p["regex"], p.get("family_replacement"), p.get("v1_replacement"), p.get("v2_replacement"), p.get("v3_replacement"), p.get("v4_replacement"), ) for p in d[0] ], [ matchers.OSMatcher( p["regex"], p.get("os_replacement"), p.get("os_v1_replacement"), p.get("os_v2_replacement"), p.get("os_v3_replacement"), p.get("os_v4_replacement"), ) for p in d[1] ], [ matchers.DeviceMatcher( p["regex"], p.get("regex_flag"), p.get("device_replacement"), p.get("brand_replacement"), p.get("model_replacement"), ) for p in d[2] ], ) def load_lazy(d: MatchersData) -> Matchers: """Loads the input data set into lazy matchers.""" return ( [ lazy.UserAgentMatcher( p["regex"], p.get("family_replacement"), p.get("v1_replacement"), p.get("v2_replacement"), p.get("v3_replacement"), p.get("v4_replacement"), ) for p in d[0] ], [ lazy.OSMatcher( p["regex"], p.get("os_replacement"), p.get("os_v1_replacement"), p.get("os_v2_replacement"), p.get("os_v3_replacement"), p.get("os_v4_replacement"), ) for p in d[1] ], [ lazy.DeviceMatcher( p["regex"], p.get("regex_flag"), p.get("device_replacement"), p.get("brand_replacement"), p.get("model_replacement"), ) for p in d[2] ], ) class FileLoader(Protocol): def __call__( self, path: PathOrFile, loader: DataLoader = load_data ) -> Matchers: ... def load_json(f: PathOrFile, loader: DataLoader = load_data) -> Matchers: """Loads JSON data following the ``regexes.yaml`` structure. The ``loader`` parameter customises which matcher variant is generated, by default :func:`load_data` is used to generate eager matchers, :func:`load_lazy` can be used to generate lazy matchers instead. """ if isinstance(f, (str, os.PathLike)): with open(f, "rb") as fp: regexes = json.load(fp) else: regexes = json.load(f) return loader( ( regexes["user_agent_parsers"], regexes["os_parsers"], regexes["device_parsers"], ) ) load_yaml: Optional[FileLoader] if load is None: load_yaml = None else: def load_yaml(path: PathOrFile, loader: DataLoader = load_data) -> Matchers: """Loads YAML data following the ``regexes.yaml`` structure. The ``loader`` parameter customises which matcher variant is generated, by default :func:`load_data` is used to generate eager matchers, :func:`load_lazy` cab be used to generate lazy matchers instead. """ if isinstance(path, (str, os.PathLike)): with open(path, "rb") as fp: regexes = load(fp, Loader=SafeLoader) # type: ignore else: regexes = load(path, Loader=SafeLoader) # type: ignore return load_data( ( regexes["user_agent_parsers"], regexes["os_parsers"], regexes["device_parsers"], ) )