-
Notifications
You must be signed in to change notification settings - Fork 154
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
194 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,185 @@ | ||
import abc | ||
from collections.abc import Iterable | ||
from dataclasses import dataclass | ||
from typing import * | ||
from typing import IO | ||
|
||
VERSION = (0, 15, 0) | ||
|
||
|
||
@dataclass | ||
class UserAgent: | ||
family: str | ||
major: Optional[str] = None | ||
minor: Optional[str] = None | ||
patch: Optional[str] = None | ||
|
||
def __str__(self): | ||
"""Replaces PrettyUserAgent""" | ||
... | ||
|
||
|
||
# or Pattern? | ||
class RegexUserAgentMatcher: | ||
def __call__(self, uas: str) -> Optional[UserAgent]: | ||
... | ||
|
||
|
||
@dataclass | ||
class OS: | ||
family: str | ||
major: Optional[str] = None | ||
minor: Optional[str] = None | ||
patch: Optional[str] = None | ||
patch_minor: Optional[str] = None | ||
|
||
def __str__(self): | ||
"""Replaces PrettyOS""" | ||
... | ||
|
||
|
||
class RegexOsMatcher: | ||
def __call__(self, uas: str) -> Optional[OS]: | ||
... | ||
|
||
|
||
@dataclass | ||
class Device: | ||
name: str | ||
brand: Optional[str] = None | ||
model: Optional[str] = None | ||
|
||
def __str__(self): | ||
"""Would replace PrettyDevice if that was a thing""" | ||
... | ||
|
||
|
||
class RegexDeviceMatcher: | ||
def __call__(self, uas: str) -> Optional[Device]: | ||
... | ||
|
||
|
||
T = TypeVar("T") | ||
# TODO: check that this mess actually does something because I wouldn't bet on it | ||
Matcher = Callable[[], Optional[T]] | ||
UserAgentMatchers = TypeVar("UserAgentMatchers", bound=Iterable[Matcher[UserAgent]]) | ||
OSMatchers = TypeVar("OSMatchers", bound=Iterable[Matcher[OS]]) | ||
DeviceMatchers = TypeVar("DeviceMatchers", bound=Iterable[Matcher[Device]]) | ||
Matchers = Tuple[UserAgentMatchers, OSMatchers, DeviceMatchers] | ||
|
||
|
||
@dataclass | ||
class ParseResult: | ||
user_agent: UserAgent | ||
os: OS | ||
device: Device | ||
string: str | ||
|
||
|
||
@dataclass | ||
class CacheEntry: | ||
string: str | ||
user_agent: Optional[UserAgent] = None | ||
os: Optional[OS] = None | ||
device: Optional[Device] = None | ||
|
||
def to_result(self) -> Optional[ParseResult]: | ||
if self.user_agent and self.os and self.device: | ||
return ParseResult(self.user_agent, self.os, self.device, self.string) | ||
return None | ||
|
||
|
||
class Cache(abc.ABC): | ||
@abc.abstractmethod | ||
def get(self, key) -> Optional[CacheEntry]: | ||
... | ||
|
||
@abc.abstractmethod | ||
def store(self, key, CacheEntry) -> CacheEntry: | ||
... | ||
|
||
@abc.abstractmethod | ||
def make_room(self): | ||
... | ||
|
||
def check_space(self): | ||
if len(self) >= self._limit: | ||
self.make_room() | ||
|
||
def key(self, ua, args): # TODO: keys are internal cache stuff, how can they type? | ||
return (ua, tuple(sorted(args.items()))) | ||
|
||
def lookup(self, ua, args) -> CacheEntry: | ||
key = self.key(ua, args) | ||
entry = self.get(key) | ||
if entry: | ||
return entry | ||
|
||
self.check_space() | ||
return self.store(key, CacheEntry(string=ua)) | ||
|
||
|
||
class Parser: | ||
def __init__( | ||
self, | ||
*matchers: Tuple[Union[Matchers, str, IO]], | ||
cache: Cache = None, | ||
): | ||
ms: Matchers | ||
if not matchers: | ||
from . import _regexes | ||
|
||
ms = ( | ||
_regexes.USER_AGENT_PARSERS, | ||
_regexes.OS_PARSERS, | ||
_regexes.DEVICE_PARSERS, | ||
) | ||
elif not isinstance(matchers[0], tuple): | ||
ms = Parser.yaml_to_matchers(matchers) | ||
else: | ||
ms = cast(Matchers, matchers[0]) | ||
|
||
self._ua_matchers, self._os_matchers, self._device_matchers = ms | ||
self.cache = cache | ||
|
||
@staticmethod | ||
def yaml_to_matchers(yaml_file: Union[str, IO]) -> "Matchers": | ||
... | ||
|
||
def __call__(self, uas: str) -> ParseResult: | ||
... | ||
|
||
def parse_user_agent(self, uas: str) -> UserAgent: | ||
... | ||
|
||
def parse_os(self, uas: str) -> OS: | ||
... | ||
|
||
def parse_device(self, uas: str) -> Device: | ||
... | ||
|
||
|
||
_parser: Optional[Parser] = None | ||
# can be used to force the init of the default parser eagerly | ||
def get_parser() -> Parser: | ||
global _parser | ||
if _parser is None: | ||
_parser = Parser() | ||
|
||
return _parser | ||
|
||
|
||
def parse(uas: str) -> ParseResult: | ||
return get_parser()(uas) | ||
|
||
|
||
def parse_user_agent(uas: str) -> UserAgent: | ||
return get_parser().parse_user_agent(uas) | ||
|
||
|
||
def parse_os(uas: str) -> OS: | ||
return get_parser().parse_os(uas) | ||
|
||
|
||
def parse_device(uas: str) -> Device: | ||
return get_parser().parse_device(uas) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters