From 646d743dce4a3704febfa28be3abce405823baef Mon Sep 17 00:00:00 2001 From: alyx Date: Sat, 25 Nov 2023 13:25:26 -0500 Subject: Modelling, guts basically done. --- src/bbss/__init__.py | 1 + src/bbss/buttons.py | 28 +++++++++++++------- src/bbss/friends.py | 44 +++++++++++++++++++++++++++++++ src/bbss/lists.py | 73 +++++++++++++++++++++++++++++----------------------- src/bbss/sizes.py | 38 +++++++++++++++++++++++++++ 5 files changed, 142 insertions(+), 42 deletions(-) create mode 100644 src/bbss/friends.py create mode 100644 src/bbss/sizes.py (limited to 'src/bbss') diff --git a/src/bbss/__init__.py b/src/bbss/__init__.py index e69de29..6edc60d 100644 --- a/src/bbss/__init__.py +++ b/src/bbss/__init__.py @@ -0,0 +1 @@ +__all__ = ["lists", "buttons", "friends"] diff --git a/src/bbss/buttons.py b/src/bbss/buttons.py index 633dc0b..77ddcf4 100644 --- a/src/bbss/buttons.py +++ b/src/bbss/buttons.py @@ -1,6 +1,7 @@ -from .lists import ListFile, ListFileEntry, ListFileIterator +from .lists import BaseListFile, ListFileEntry, parse_listfile from dataclasses import dataclass -from typing import cast +from typing import Optional, Self +from collections.abc import Sequence import requests @dataclass(frozen=True) @@ -13,17 +14,24 @@ class ButtonListFileEntry(ListFileEntry): def exists(self) -> bool: return requests.head(self.url()).ok -class ButtonListFileIterator(ListFileIterator): - def __next__(self) -> ButtonListFileEntry: - return cast(ButtonListFileEntry, super().__next__()) + def get(self) -> requests.Response: + return requests.get(self.url()) -@dataclass(frozen=True) -class ButtonListFile(ListFile): +class ButtonListFile(BaseListFile[ButtonListFileEntry]): def __init__(self, contents: str, root: str): super().__init__(contents) - self.root = root + self._root = root + + def construct_entry(entry: str, comment: Optional[str]): + return ButtonListFileEntry(entry, comment, root) + self._entries = parse_listfile(contents, construct_entry) @classmethod - def from_url(cls, url: str): + def from_url(cls, url: str) -> Optional[Self]: (root, _, _) = url.rpartition('/') - return cls(requests.get(url, stream = False).text, root) + req = requests.get(url, stream = False) + if not req.ok: + return None + return cls(req.text, root) + +__all__ = ["ButtonListFileEntry", "ButtonListFile"] diff --git a/src/bbss/friends.py b/src/bbss/friends.py new file mode 100644 index 0000000..cd5e7cf --- /dev/null +++ b/src/bbss/friends.py @@ -0,0 +1,44 @@ +from .lists import BaseListFile, ListFileEntry, parse_listfile +from dataclasses import dataclass +from typing import cast, Optional +from collections.abc import Sequence +import requests +import re + +FRIEND_REGEX = re.compile(r"""^ + (?:(?P[a-z]+)://)? # match the URL scheme, e.x. `https://` + (?P(?:[a-z0-9\-]+\.)*(?:[a-z])+) # match the actual domain + (?: + | # match no path + (?P(?:/[^/\s#?]+)*/?) # match a path + ) + $""", re.X) + +@dataclass(frozen=True) +class FriendListFileEntry(ListFileEntry): + url: str + domain: str + scheme: str + path: str + + def __init__(self, entry: str, comment: Optional[str]): + super().__init__(entry, comment) + m = FRIEND_REGEX.match(entry) + if m is None: return + self.__setattr__('scheme', m.group('scheme') or None) + self.__setattr__('domain', m.group('domain')) + self.__setattr__('path', m.group('path').removesuffix('/')) + self.__setattr__('url', (self.scheme if self.scheme else 'https') + '://' + self.domain + (self.path if self.path else '/BBSS') + '/') + + def exists(self) -> bool: + return requests.head(self.url).ok + + def get(self) -> requests.Response: + return requests.get(self.url) + +class FriendListFile(BaseListFile[FriendListFileEntry]): + def __init__(self, contents: str): + super().__init__(contents) + self._entries = parse_listfile(contents, FriendListFileEntry) + +__all__ = ["FriendListFileEntry", "FriendListFile"] diff --git a/src/bbss/lists.py b/src/bbss/lists.py index 234d0f8..ba6955a 100644 --- a/src/bbss/lists.py +++ b/src/bbss/lists.py @@ -1,45 +1,54 @@ from dataclasses import dataclass -from collections.abc import Iterable, Iterator -from typing import Optional +from collections.abc import Iterable, Iterator, Sequence +from typing import Optional, overload, List, Callable, TypeVar, Self import requests +T = TypeVar('T') +def parse_listfile(contents: str, ctor: Callable[[str, Optional[str]], T]) -> List[T]: + acc = [] + comment_acc = "" + for line in contents.splitlines(): + if line.startswith("#"): + continue + elif line.startswith("##"): + comment_acc += line[2:].strip() + else: + acc.append(ctor(line, comment_acc if comment_acc else None)) + return acc + @dataclass(frozen=True) class ListFileEntry: entry: str comment: Optional[str] -class ListFileIterator(Iterator[ListFileEntry]): - def __init__(self, entries: list[ListFileEntry]): - self._entries = entries - self.index = -1 - def __next__(self) -> ListFileEntry: - if self.index == len(self._entries): - raise StopIteration - self.index += 1 - return self._entries[self.index] - -@dataclass(frozen=True) -class ListFile(Iterable[ListFileEntry]): +L = TypeVar('L', bound=ListFileEntry) +class BaseListFile(Sequence[L]): def __init__(self, contents: str): - self._contents = contents - acc = [] - comment_acc = "" - for line in contents.splitlines(): - if line.startswith("#"): - continue - elif line.startswith("##"): - comment_acc += line[2:].strip() - else: - acc.append(ListFileEntry(comment = comment_acc if comment_acc else None, entry = line)) - - self._entries = acc + self._contents: str = contents + self._entries: List[L] = [] - def __iter__(self) -> ListFileIterator: - return ListFileIterator(self._entries) + @overload + def __getitem__(self, key: int) -> L: + pass + @overload + def __getitem__(self, key: slice) -> Sequence[L]: + pass + def __getitem__(self, key): + return self._entries[key] - def entries(self) -> list[ListFileEntry]: - return self._entries + def __len__(self) -> int: + return self._entries.__len__() @classmethod - def from_url(cls, url: str): - return cls(requests.get(url, stream = False).text) + def from_url(cls, url: str) -> Optional[Self]: + req = requests.get(url, stream = False) + if not req.ok: + return None + return cls(req.text) + +class ListFile(BaseListFile[ListFileEntry]): + def __init__(self, contents: str): + super().__init__(contents) + self._entries = parse_listfile(contents, ListFileEntry) + +__all__ = ["parse_listfile", "ListFileEntry", "BaseListFile", "ListFIle"] diff --git a/src/bbss/sizes.py b/src/bbss/sizes.py new file mode 100644 index 0000000..26818e9 --- /dev/null +++ b/src/bbss/sizes.py @@ -0,0 +1,38 @@ +from .lists import BaseListFile, ListFileEntry, parse_listfile +from .buttons import ButtonListFile +from dataclasses import dataclass +from typing import Optional, Self +from collections.abc import Sequence +import requests + +@dataclass(frozen=True) +class SizeListFileEntry(ListFileEntry): + root: str + + def url(self) -> str: + return self.root + "/" + self.entry + "/list.txt" + + def exists(self) -> bool: + return requests.head(self.url()).ok + + def get(self) -> Optional[ButtonListFile]: + return ButtonListFile.from_url(self.url()) + +class SizeListFile(BaseListFile[SizeListFileEntry]): + def __init__(self, contents: str, root: str): + super().__init__(contents) + self._root = root + + def construct_entry(entry: str, comment: Optional[str]): + return SizeListFileEntry(entry, comment, root) + self._entries = parse_listfile(contents, construct_entry) + + @classmethod + def from_url(cls, url: str) -> Optional[Self]: + (root, _, _) = url.rpartition('/') + req = requests.get(url, stream = False) + if not req.ok: + return None + return cls(req.text, root) + +__all__ = ["SizeListFileEntry", "SizeListFile"] -- cgit v1.2.3-54-g00ecf