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
[docs]
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)
[docs]
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]
[docs]
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]
],
)
[docs]
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: ...
[docs]
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:
[docs]
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"],
)
)