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.
207 lines
5.9 KiB
207 lines
5.9 KiB
"""The package provides top-level helpers which use a lazily initialised
|
|
default parser. These are convenience functions, for more control it
|
|
is perfectly acceptable to instantiate and call parsers directly.
|
|
|
|
The default parser does use a cache keyed on the user-agent string,
|
|
but its exact behaviour is unspecified, if you require a consistent
|
|
behaviour or specific algorithm, set up your own parser (global or
|
|
not).
|
|
|
|
For convenience, direct aliases are also provided for:
|
|
|
|
- :mod:`core types <.types>`
|
|
- :mod:`caching utilities <.caching>`
|
|
- :mod:`ua_parser.basic.Parser` as :class:`BasicParser`
|
|
|
|
This way importing anything but the top-level package should not be
|
|
necessary unless you want to *implement* a parser.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
__all__ = [
|
|
"OS",
|
|
"BasicResolver",
|
|
"Cache",
|
|
"CachingResolver",
|
|
"DefaultedResult",
|
|
"Device",
|
|
"Domain",
|
|
"Matchers",
|
|
"PartialResult",
|
|
"Resolver",
|
|
"Result",
|
|
"UserAgent",
|
|
"load_builtins",
|
|
"load_lazy_builtins",
|
|
"parse",
|
|
"parse_device",
|
|
"parse_os",
|
|
"parse_user_agent",
|
|
]
|
|
|
|
import importlib.util
|
|
import threading
|
|
from typing import Callable, Optional, cast
|
|
|
|
from .basic import Resolver as BasicResolver
|
|
from .caching import CachingResolver, S3Fifo as Cache
|
|
from .core import (
|
|
DefaultedResult,
|
|
Device,
|
|
Domain,
|
|
Matchers,
|
|
OS,
|
|
PartialResult,
|
|
Resolver,
|
|
Result,
|
|
UserAgent,
|
|
)
|
|
from .loaders import load_builtins, load_lazy_builtins
|
|
from .utils import IS_GRAAL
|
|
|
|
_ResolverCtor = Callable[[Matchers], Resolver]
|
|
Re2Resolver: Optional[_ResolverCtor] = None
|
|
if importlib.util.find_spec("re2"):
|
|
from .re2 import Resolver as Re2Resolver
|
|
RegexResolver: Optional[_ResolverCtor] = None
|
|
if importlib.util.find_spec("ua_parser_rs"):
|
|
from .regex import Resolver as RegexResolver
|
|
BestAvailableResolver: _ResolverCtor = next(
|
|
filter(
|
|
None,
|
|
(
|
|
RegexResolver,
|
|
Re2Resolver,
|
|
lambda m: CachingResolver(BasicResolver(m), Cache(2000)),
|
|
),
|
|
)
|
|
)
|
|
|
|
|
|
VERSION = (1, 0, 1)
|
|
|
|
|
|
class Parser:
|
|
"""Wrapper object, provides convenience methods around an
|
|
underlying :class:`Resolver`.
|
|
|
|
"""
|
|
|
|
@classmethod
|
|
def from_matchers(cls, m: Matchers, /) -> Parser:
|
|
"""from_matchers(Matchers) -> Parser
|
|
|
|
Instantiates a parser from the provided
|
|
:class:`~ua_parser.core.Matchers` using the default resolver
|
|
stack.
|
|
|
|
"""
|
|
return cls(BestAvailableResolver(m))
|
|
|
|
def __init__(self, resolver: Resolver) -> None:
|
|
self.resolver = resolver
|
|
|
|
def __call__(self, ua: str, domains: Domain, /) -> PartialResult:
|
|
"""Parses the ``ua`` string, returning a parse result with *at least*
|
|
the requested :class:`domains <Domain>` resolved (whether to success or
|
|
failure).
|
|
"""
|
|
return self.resolver(ua, domains)
|
|
|
|
def parse(self: Resolver, ua: str) -> Result:
|
|
"""Convenience method for parsing all domains."""
|
|
return self(ua, Domain.ALL).complete()
|
|
|
|
def parse_user_agent(self: Resolver, ua: str) -> Optional[UserAgent]:
|
|
"""Convenience method for parsing the :class:`UserAgent` domain."""
|
|
return self(ua, Domain.USER_AGENT).user_agent
|
|
|
|
def parse_os(self: Resolver, ua: str) -> Optional[OS]:
|
|
"""Convenience method for parsing the :class:`OS` domain."""
|
|
return self(ua, Domain.OS).os
|
|
|
|
def parse_device(self: Resolver, ua: str) -> Optional[Device]:
|
|
"""Convenience method for parsing the :class:`Device` domain."""
|
|
return self(ua, Domain.DEVICE).device
|
|
|
|
|
|
parser: Parser
|
|
"""Global :class:`Parser`, lazy-initialised on first access, used by
|
|
the global helper functions.
|
|
|
|
Can be *set* to configure a customised global parser.
|
|
|
|
Accessing the parser explicitely can be used eagerly force its
|
|
initialisation, rather than pay for it at first call.
|
|
"""
|
|
|
|
_lazy_globals_lock = threading.Lock()
|
|
|
|
|
|
def __getattr__(name: str) -> Parser:
|
|
global parser
|
|
with _lazy_globals_lock:
|
|
if name == "parser":
|
|
# if two threads access `ua_parser.parser` before it's
|
|
# initialised, the second one will wait until the first
|
|
# one's finished by which time the parser global should be
|
|
# set and can be returned with no extra work
|
|
if p := globals().get("parser"):
|
|
return cast(Parser, p)
|
|
|
|
if RegexResolver or Re2Resolver or IS_GRAAL:
|
|
matchers = load_lazy_builtins()
|
|
else:
|
|
matchers = load_builtins()
|
|
parser = Parser.from_matchers(matchers)
|
|
return parser
|
|
|
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
|
|
|
|
def parse(ua: str) -> Result:
|
|
"""Parses the :class:`.UserAgent`, :class:`.OS`, and :class:`.Device`
|
|
information using the :data:`global parser <parser>`.
|
|
|
|
Equivalent to calling each of :func:`parse_user_agent`,
|
|
:func:`parse_os`, and :func:`parse_device` but *may* be more
|
|
efficient than calling them separately depending on the underlying
|
|
parser.
|
|
|
|
Even in the best case, prefer the domain-specific helpers if
|
|
you're not going to use *all* of them.
|
|
"""
|
|
# import required to trigger __getattr__ and initialise the
|
|
# parser, a `global` access fails to and we get a NameError
|
|
from . import parser
|
|
|
|
return parser(ua, Domain.ALL).complete()
|
|
|
|
|
|
def parse_user_agent(ua: str) -> Optional[UserAgent]:
|
|
"""Parses the :class:`browser <.UserAgent>` information using the
|
|
:data:`global parser <parser>`.
|
|
"""
|
|
from . import parser
|
|
|
|
return parser(ua, Domain.USER_AGENT).user_agent
|
|
|
|
|
|
def parse_os(ua: str) -> Optional[OS]:
|
|
"""Parses the :class:`.OS` information using the :data:`global parser
|
|
<parser>`.
|
|
"""
|
|
from . import parser
|
|
|
|
return parser(ua, Domain.OS).os
|
|
|
|
|
|
def parse_device(ua: str) -> Optional[Device]:
|
|
"""Parses the :class:`.Device` information using the :data:`global
|
|
parser <parser>`.
|
|
"""
|
|
from . import parser
|
|
|
|
return parser(ua, Domain.DEVICE).device
|