forked from ua-parser/uap-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path__init__.py
More file actions
199 lines (153 loc) · 5.85 KB
/
__init__.py
File metadata and controls
199 lines (153 loc) · 5.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
"""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, BasicResolver))
)
VERSION = (1, 0, 2)
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(CachingResolver(BestAvailableResolver(m), Cache(2000)))
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