diff options
| author | 2021-03-09 22:41:25 +0100 | |
|---|---|---|
| committer | 2021-03-13 12:39:42 +0100 | |
| commit | cb3b80788bde2aba280de5370ee78abcaa39f613 (patch) | |
| tree | da531b3b683c2c3d4ee5fbd270f0f49a77b262db | |
| parent | Branding: add HTTP fetch helper methods (diff) | |
Branding: define event construction methodology
| -rw-r--r-- | bot/errors.py | 6 | ||||
| -rw-r--r-- | bot/exts/backend/branding/_repository.py | 84 |
2 files changed, 90 insertions, 0 deletions
diff --git a/bot/errors.py b/bot/errors.py index ab0adcd42..3544c6320 100644 --- a/bot/errors.py +++ b/bot/errors.py @@ -35,3 +35,9 @@ class InvalidInfractedUser(Exception): self.reason = reason super().__init__(reason) + + +class BrandingMisconfiguration(RuntimeError): + """Raised by the Branding cog when a misconfigured event is encountered.""" + + pass diff --git a/bot/exts/backend/branding/_repository.py b/bot/exts/backend/branding/_repository.py index bf38fccad..9d32fdfb1 100644 --- a/bot/exts/backend/branding/_repository.py +++ b/bot/exts/backend/branding/_repository.py @@ -1,8 +1,12 @@ import logging import typing as t +from datetime import date, datetime + +import frontmatter from bot.bot import Bot from bot.constants import Keys +from bot.errors import BrandingMisconfiguration # Base URL for requests into the branding repository BRANDING_URL = "https://api.github.com/repos/kwzrd/pydis-branding/contents" @@ -14,6 +18,13 @@ HEADERS = {"Accept": "application/vnd.github.v3+json"} # Ensure we use API v3 if Keys.github: HEADERS["Authorization"] = f"token {Keys.github}" +# Since event periods are year-agnostic, we parse them into `datetime` objects with a manually inserted year +# Please note that this is intentionally a leap year in order to allow Feb 29 to be valid +ARBITRARY_YEAR = 2020 + +# Format used to parse date strings after we inject `ARBITRARY_YEAR` at the end +DATE_FMT = "%B %d %Y" # Ex: July 10 2020 + log = logging.getLogger(__name__) @@ -35,6 +46,23 @@ class RemoteObject: setattr(self, annotation, dictionary[annotation]) +class MetaFile(t.NamedTuple): + """Composition of attributes defined in a 'meta.md' file.""" + + is_fallback: bool + start_date: t.Optional[date] + end_date: t.Optional[date] + description: str # Markdown event description + + +class Event(t.NamedTuple): + """Represent an event defined in the branding repository.""" + + banner: RemoteObject + icons: t.List[RemoteObject] + meta: MetaFile + + class BrandingRepository: """Abstraction exposing the branding repository via convenient methods.""" @@ -75,3 +103,59 @@ class BrandingRepository: return await response.read() else: log.warning(f"Received non-200 response status: {response.status}") + + async def parse_meta_file(self, raw_file: bytes) -> MetaFile: + """ + Parse a 'meta.md' file from raw bytes. + + The caller is responsible for handling errors caused by misconfiguration. + """ + attrs, description = frontmatter.parse(raw_file) # Library automatically decodes using UTF-8 + + if not description: + raise BrandingMisconfiguration("No description found in 'meta.md'!") + + if attrs.get("fallback", False): + return MetaFile(is_fallback=True, start_date=None, end_date=None, description=description) + + start_date_raw = attrs.get("start_date") + end_date_raw = attrs.get("end_date") + + if None in (start_date_raw, end_date_raw): + raise BrandingMisconfiguration("Non-fallback event doesn't have start and end dates defined!") + + # We extend the configured month & day with an arbitrary leap year to allow a `datetime` repr to exist + # This may raise errors if configured in a wrong format ~ we let the caller handle such cases + start_date = datetime.strptime(f"{start_date_raw} {ARBITRARY_YEAR}", DATE_FMT).date() + end_date = datetime.strptime(f"{end_date_raw} {ARBITRARY_YEAR}", DATE_FMT).date() + + return MetaFile(is_fallback=False, start_date=start_date, end_date=end_date, description=description) + + async def construct_event(self, directory: RemoteObject) -> Event: + """ + Construct an `Event` instance from an event `directory`. + + The caller is responsible for handling errors caused by misconfiguration. + """ + contents = await self.fetch_directory(directory.path) + + missing_assets = {"meta.md", "banner.png", "server_icons"} - contents.keys() + + if missing_assets: + raise BrandingMisconfiguration(f"Directory is missing following assets: {missing_assets}") + + server_icons = await self.fetch_directory(contents["server_icons"].path, types=("file",)) + + if server_icons is None: + raise BrandingMisconfiguration("Failed to fetch server icons!") + if len(server_icons) == 0: + raise BrandingMisconfiguration("Found no server icons!") + + meta_bytes = await self.fetch_file(contents["meta.md"]) + + if meta_bytes is None: + raise BrandingMisconfiguration("Failed to fetch 'meta.md' file!") + + meta_file = await self.parse_meta_file(meta_bytes) + + return Event(contents["banner.png"], list(server_icons.values()), meta_file) |