aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bbss/__init__.py1
-rw-r--r--src/bbss/buttons.py28
-rw-r--r--src/bbss/friends.py44
-rw-r--r--src/bbss/lists.py73
-rw-r--r--src/bbss/sizes.py38
5 files changed, 142 insertions, 42 deletions
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<scheme>[a-z]+)://)? # match the URL scheme, e.x. `https://`
+ (?P<domain>(?:[a-z0-9\-]+\.)*(?:[a-z])+) # match the actual domain
+ (?:
+ | # match no path
+ (?P<path>(?:/[^/\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"]