Skip to content

Commit

Permalink
Update iCal format (#9)
Browse files Browse the repository at this point in the history
* Fix location parsing and display handling

* Bump timetable version
  • Loading branch information
novanai authored Sep 15, 2024
1 parent be355f6 commit 025fd36
Show file tree
Hide file tree
Showing 6 changed files with 42 additions and 22 deletions.
2 changes: 1 addition & 1 deletion backend/api_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
building="Q",
floor="G",
room="15",
error=False,
original=None,
),
],
description="Lecture",
Expand Down
2 changes: 1 addition & 1 deletion timetable/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
datefmt="%Y-%m-%d %H:%M:%S",
)

__version__ = "2.2.1"
__version__ = "2.3.0"
15 changes: 9 additions & 6 deletions timetable/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from timetable import cache as cache_
from timetable import models, utils

from timetable import __version__

logger = logging.getLogger(__name__)

BASE_URL = "https://scientia-eu-v4-api-d1-03.azurewebsites.net/api/Public"
Expand Down Expand Up @@ -63,20 +65,21 @@ async def fetch_data(
headers={
"Authorization": "Anonymous",
"Content-type": "application/json",
"User-Agent": f"TimetableSync/{__version__} (https://timetable.redbrick.dcu.ie)"
},
json=json_data,
) as res:
if not res.ok:
if retries == 3:
logger.error(
f"API Error: {res.status} {res.content_type} {(await res.read()).decode()}"
)
res.raise_for_status()

retries += 1
await asyncio.sleep(5)
continue

if retries == 3:
logger.error(
f"API Error: {res.status} {res.content_type} {(await res.read()).decode()}"
)
res.raise_for_status()

data = await res.json(loads=orjson.loads)
return data

Expand Down
9 changes: 3 additions & 6 deletions timetable/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,8 +458,8 @@ class Location(ModelBase):
"""
room: str
"""The room code. Not guaranteed to be just a number."""
error: bool = False
"""`True` if this location was not parsed correctly, otherwise `False`"""
original: str | None = None
"""The original location code. If `None`, the location was parsed correctly."""

@classmethod
def from_payload(cls, payload: dict[str, typing.Any]) -> typing.Self:
Expand Down Expand Up @@ -496,10 +496,7 @@ def from_payloads(cls, payload: dict[str, typing.Any]) -> list[Location]:

logger.warning(f"Failed to parse location: '{location}'")

# fallback
campus, loc = location.split(".")

return [cls(campus=campus, building="", floor="", room=loc, error=True)]
return [cls(campus="", building="", floor="", room="", original=location)]

def __str__(self) -> str:
return f"{self.campus}.{self.building}{self.floor}{self.room}"
Expand Down
5 changes: 5 additions & 0 deletions timetable/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import typing

def is_str_list(val: list[typing.Any]) -> typing.TypeGuard[list[str]]:
"""Determines whether all objects in the list are strings"""
return all(isinstance(x, str) for x in val)
31 changes: 23 additions & 8 deletions timetable/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import icalendar
import orjson
from timetable.types import is_str_list

from timetable import models

Expand Down Expand Up @@ -152,30 +153,44 @@ def from_event(cls, event: models.Event) -> typing.Self:
(name + (f" {summary_long}" if summary_long else "")).strip()
)
summary_short = title_case(name)
if event.group_name:
summary_short = f"{summary_short} (Group {event.group_name})".strip()

# LOCATIONS

if event.locations:
# dict[(campus, building)] = [locations]
locations: dict[tuple[str, str], list[models.Location]] = (
locations: dict[tuple[str, str] | None, list[models.Location]] = (
collections.defaultdict(list)
)

for loc in event.locations:
locations[(loc.campus, loc.building)].append(loc)
if loc.original is not None:
locations[None].append(loc)
else:
locations[(loc.campus, loc.building)].append(loc)

locations_long: list[str] = []
locations_short: list[str] = []
for (campus, building), locs_ in locations.items():
for main, locs in locations.items():
if main is None:
locs_ = [loc.original for loc in locs]
assert is_str_list(locs_)
loc_string = ", ".join(locs_)
locations_long.append(loc_string)
locations_short.append(loc_string)
continue

campus, building = main
building = models.BUILDINGS[campus][building]
campus = models.CAMPUSES[campus]
locs_ = sorted(locs_, key=lambda r: r.room)
locs_ = sorted(locs_, key=lambda r: ORDER.index(r.floor))
locs = sorted(locs, key=lambda r: r.room)
locs = sorted(locs, key=lambda r: ORDER.index(r.floor))
locations_long.append(
f"{', '.join((f"{loc.building}{loc.floor}{loc.room}" for loc in locs_))} ({building}, {campus})"
f"{', '.join((f"{loc.building}{loc.floor}{loc.room}" for loc in locs))} ({building}, {campus})"
)
locations_short.append(
f"{', '.join((f"{loc.building}{loc.floor}{loc.room}" for loc in locs_))}"
f"{', '.join((f"{loc.building}{loc.floor}{loc.room}" for loc in locs))}"
)

location_long = ", ".join(locations_long)
Expand Down Expand Up @@ -229,7 +244,7 @@ def generate_ical_file(events: list[models.Event]) -> bytes:
event.add("DTSTART", item.original_event.start)
event.add("DTEND", item.original_event.end - datetime.timedelta(seconds=1))
event.add("SUMMARY", item.summary_long)
event.add("DESCRIPTION", item.description)
event.add("DESCRIPTION", f"Details: {item.description}\nStaff: {item.original_event.staff_member}")
event.add("LOCATION", item.location_long)
event.add("CLASS", "PUBLIC")
calendar.add_component(event)
Expand Down

0 comments on commit 025fd36

Please sign in to comment.