238 lines
6.1 KiB
Python
238 lines
6.1 KiB
Python
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"],
|
|
)
|
|
)
|