Skip to content

Commit

Permalink
new api first draft
Browse files Browse the repository at this point in the history
  • Loading branch information
masklinn committed May 2, 2022
1 parent 7a73e3e commit e747b6a
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 3 deletions.
9 changes: 8 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py27, py36, py37, py38, py39, py310, docs, flake8, black
envlist = py27, py36, py37, py38, py39, py310, docs, flake8, black, typecheck
skipsdist = True

[testenv]
Expand Down Expand Up @@ -29,6 +29,13 @@ skip_install = True
deps = black
commands = black --check --diff .

[testenv:typecheck]
skip_install = True
deps =
types-PyYaml
mypy
commands = mypy ua_parser

[flake8]
max_line_length = 88
filename = ua_parser/
184 changes: 184 additions & 0 deletions ua_parser/__init__.py
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)
2 changes: 1 addition & 1 deletion ua_parser/user_agent_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ def GetFilters(
# pyyaml doesn't do it by default (yaml/pyyaml#436)
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from yaml import SafeLoader # type: ignore

with open(UA_PARSER_YAML, "rb") as fp:
regexes = yaml.load(fp, Loader=SafeLoader)
Expand Down
2 changes: 1 addition & 1 deletion ua_parser/user_agent_parser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
# Try and use libyaml bindings if available since faster
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from yaml import SafeLoader # type: ignore

from ua_parser import user_agent_parser

Expand Down

0 comments on commit e747b6a

Please sign in to comment.