Skip to content

Commit

Permalink
refactor: reorganise code and split it
Browse files Browse the repository at this point in the history
  • Loading branch information
tbmc committed May 15, 2024
1 parent 1e697b4 commit ac7c6cd
Show file tree
Hide file tree
Showing 16 changed files with 278 additions and 222 deletions.
9 changes: 5 additions & 4 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from flask import send_from_directory
from calendar_connector.calendar_converter import CalendarConverter
from calendar_connector.data_decoder import decode_data
from calendar_connector.sporteasy_connector import SporteasyConnector

logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
Expand All @@ -21,9 +22,9 @@

def _list_teams_response() -> str:
username, password, _ = decode_data()
calendar_converter = CalendarConverter()
calendar_converter.login(username, password)
teams = calendar_converter.list_teams()
connector = SporteasyConnector()
connector.login(username, password)
teams = connector.list_teams()
return json.dumps(teams)


Expand Down Expand Up @@ -69,7 +70,7 @@ def serve_static_index() -> flask.Response:
# Redirect user to /api if data is passed to keep compatibility
redirect_route = flask.url_for("main_request_api_handler")
redirect_url = f"{redirect_route}?data={data}"
return flask.redirect(redirect_url)
return flask.redirect(redirect_url) # type: ignore

return send_from_directory("web-app/build", "index.html")

Expand Down
215 changes: 7 additions & 208 deletions calendar_connector/calendar_converter.py
Original file line number Diff line number Diff line change
@@ -1,225 +1,24 @@
from datetime import datetime, timedelta
from typing import Any, cast, Literal

import requests
from icalendar import Calendar, Event, vText

from calendar_connector.consts import (
EVENT_TYPE,
TIMEZONE,
MY_PRESENCE,
ORDER_PRESENT,
url_authenticate,
url_list_teams,
url_list_events,
)
from icalendar import Calendar, vText

from calendar_connector.datetime_utils import (
get_current_timestamp,
get_current_datetime,
)
from calendar_connector.env import load_env_data
from calendar_connector.normalize import normalize


def _extract_event_dates(event_data: EVENT_TYPE, event: Event) -> None:
start_date = datetime.fromisoformat(cast(str, event_data["start_at"]))
start_date = start_date.astimezone(TIMEZONE)
end_date_str = cast(str | None, event_data["end_at"])
if end_date_str is not None:
end_date = datetime.fromisoformat(end_date_str)
else:
end_date = start_date + timedelta(hours=2)
end_date = end_date.astimezone(TIMEZONE)

event.add("dtstart", start_date)
event.add("dtstamp", start_date)
event.add("dtend", end_date)


def _extract_event_location(event_data: EVENT_TYPE, event: Event) -> None:
if event_data is None or event_data["location"] is None:
return

location = normalize(
cast(
dict[str, str | Any],
event_data["location"],
)["formatted_address"]
)
event.add("location", location)


def _status_to_ics_status(sp_status: str) -> str:
sp_status = sp_status.upper()
if sp_status == "CONFIRMED":
return "CONFIRMED"
if sp_status == "CANCELLED":
return "CANCELLED"
if sp_status == "NEEDS-ACTION":
return "TENTATIVE"
return sp_status


def _extract_event_summary(
event_data: EVENT_TYPE, event: Event, team_name: str
) -> None:
name = normalize(cast(str, event_data["name"]))
if team_name not in name:
summary = f"{team_name} - {name}"
else:
summary = name
if "is_cancelled" in event_data and event_data["is_cancelled"]:
summary = f"CANCELLED | {summary}"

# opponent_right = cast(dict[str, int | str | None] | None, event_data["opponent_right"])
# opponent_left = cast(dict[str, int | str | None] | None, event_data["opponent_left"])
# if opponent_left is not None and opponent_right is not None:
# if opponent_left["id"] == team_id:
# opponent_name = opponent_right["short_name"]
# else:
# opponent_name = opponent_left["short_name"]
# summary += f" - contre {opponent_name}"

me_object = event_data.get("me", {})
if me_object is not None and me_object.get("group") is not None:
group = cast(dict[str, str], me_object.get("group", {}))
localized_name_presence = group.get("localized_name")
if localized_name_presence is not None:
summary += f" - {normalize(localized_name_presence)}"

slug_presence = group.get("slug_name")
presence = MY_PRESENCE.get(slug_presence)
event.add("status", _status_to_ics_status(presence))

event.add("summary", summary)


def _extract_attendee_description(event_data: EVENT_TYPE) -> str:
attendance_groups = cast(
list[dict[str, int | str]] | None, event_data["attendance_groups"]
)
attendee = ""
if attendance_groups is not None:
attendance_group_list: list[tuple[int, str, int]] = []
for ppc in attendance_groups:
slug_sort_value = ORDER_PRESENT.get(cast(str, ppc["slug_name"]), 0)
attendance_group_list.append(
(
slug_sort_value,
cast(str, ppc["localized_name"]),
cast(int, ppc["count"]),
)
)
attendance_group_list.sort(reverse=True)
from calendar_connector.event_convertor import event_to_calendar_event
from calendar_connector.sporteasy_connector import SporteasyConnector

attendee = ", ".join(
[
f"{localized_name}: {count}"
for _, localized_name, count in attendance_group_list
]
)

return attendee


def _extract_scores_for_opponent(
event_data: EVENT_TYPE, left_or_right: Literal["left", "right"]
) -> tuple[str, int] | None:
opponent = event_data.get(f"opponent_{left_or_right}")
if opponent is None:
return None
score_str = opponent.get("score")
if score_str is None:
return None
name = cast(str, opponent.get("short_name"))
score = int(cast(str, score_str))
return name, score


def _extract_scores(event_data: EVENT_TYPE) -> str | None:
left = _extract_scores_for_opponent(event_data, "left")
right = _extract_scores_for_opponent(event_data, "right")

if left is None or right is None:
return None

left_name, left_score = left
right_name, right_score = right
return f"{left_name} {left_score} - {right_score} {right_name}"


def _extract_event_description(event_data: EVENT_TYPE, event: Event) -> None:
description = ""
score = _extract_scores(event_data)
if score is not None:
description += f"{score}\n"
attendee = _extract_attendee_description(event_data)
description += attendee

event.add("description", description.strip())


def event_to_calendar_event(team_name: str, event_data: EVENT_TYPE) -> Event:
event = Event()
event.add("uid", str(event_data["id"]) + f"@sporteasy.net")
_extract_event_location(event_data, event)
_extract_event_dates(event_data, event)
_extract_event_summary(event_data, event, team_name)
_extract_event_description(event_data, event)

event.add("class", "PUBLIC")
current_timestamp = get_current_timestamp()
event.add("sequence", current_timestamp)
event.add("transp", "OPAQUE")

# todo: change this if possible
event.add("created", datetime(2020, 1, 1, 1, 1, 1))
event.add("last-modified", datetime(2020, 1, 1, 1, 1, 1))

category_name = normalize(
cast(dict[str, str | Any], event_data["category"])["localized_name"]
)

return event


class CalendarConverter:
def __init__(self) -> None:
self.session_requests = requests.Session()

def login(self, username: str, password: str) -> str:
authenticate_response = self.session_requests.post(
url_authenticate,
{
"username": username,
"password": password,
},
)
if authenticate_response.status_code != 200:
raise Exception("Authentication error")
token: str = authenticate_response.cookies.get("sporteasy")
# csrf = authenticate_response.cookies.get(csrf_name)
return token

def list_teams(self) -> list[tuple[int, str]]:
response = self.session_requests.get(url_list_teams)
data = response.json()
return [(d["id"], normalize(d["name"])) for d in data["results"]]

def list_events(self, team_id: int) -> list[EVENT_TYPE]:
response = self.session_requests.get(
url_list_events.format(team_id=team_id),
headers={"Accept-Language": "fr-FR"},
)
data: list[EVENT_TYPE] = response.json()["results"]
return data
self.connector = SporteasyConnector()

def get_calendar_text(
self, username: str, password: str, team_id: str | None = None
) -> str:
self.login(username, password)
teams = self.list_teams()
self.connector.login(username, password)
teams = self.connector.list_teams()

cal = Calendar()
cal.add("summary", vText("SportEasyCalendar"))
Expand All @@ -244,7 +43,7 @@ def get_calendar_text(
and int(team_id) != current_team_id
):
continue
events = self.list_events(current_team_id)
events = self.connector.list_events(current_team_id)
for event in events:
cal.add_component(event_to_calendar_event(team_name, event))

Expand Down
4 changes: 3 additions & 1 deletion calendar_connector/env.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import Optional

from dotenv import dotenv_values

env = dotenv_values(".env")


def load_env_data() -> tuple[str, str, str]:
def load_env_data() -> tuple[str, str, Optional[str]]:
username = env.get("username")
password = env.get("password")
team_id = env.get("team_id")
Expand Down
36 changes: 36 additions & 0 deletions calendar_connector/event_convertor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from datetime import datetime
from typing import Any, cast

from icalendar import Event

from calendar_connector.consts import EVENT_TYPE
from calendar_connector.datetime_utils import get_current_timestamp
from calendar_connector.event_utils.date import extract_event_dates
from calendar_connector.event_utils.description import extract_event_description
from calendar_connector.event_utils.location import extract_event_location
from calendar_connector.event_utils.summary import extract_event_summary
from calendar_connector.normalize import normalize


def event_to_calendar_event(team_name: str, event_data: EVENT_TYPE) -> Event:
event = Event()
event.add("uid", str(event_data["id"]) + f"@sporteasy.net")
extract_event_location(event_data, event)
extract_event_dates(event_data, event)
extract_event_summary(event_data, event, team_name)
extract_event_description(event_data, event)

event.add("class", "PUBLIC")
current_timestamp = get_current_timestamp()
event.add("sequence", current_timestamp)
event.add("transp", "OPAQUE")

# todo: change this if possible
event.add("created", datetime(2020, 1, 1, 1, 1, 1))
event.add("last-modified", datetime(2020, 1, 1, 1, 1, 1))

category_name = normalize(
cast(dict[str, str | Any], event_data["category"])["localized_name"]
)

return event
21 changes: 21 additions & 0 deletions calendar_connector/event_utils/date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from datetime import datetime, timedelta
from typing import cast

from icalendar import Event

from calendar_connector.consts import EVENT_TYPE, TIMEZONE


def extract_event_dates(event_data: EVENT_TYPE, event: Event) -> None:
start_date = datetime.fromisoformat(cast(str, event_data["start_at"]))
start_date = start_date.astimezone(TIMEZONE)
end_date_str = cast(str | None, event_data["end_at"])
if end_date_str is not None:
end_date = datetime.fromisoformat(end_date_str)
else:
end_date = start_date + timedelta(hours=2)
end_date = end_date.astimezone(TIMEZONE)

event.add("dtstart", start_date)
event.add("dtstamp", start_date)
event.add("dtend", end_date)
45 changes: 45 additions & 0 deletions calendar_connector/event_utils/description.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from typing import cast

from icalendar import Event

from calendar_connector.consts import EVENT_TYPE, ORDER_PRESENT
from calendar_connector.event_utils.score import extract_scores


def _extract_attendee_description(event_data: EVENT_TYPE) -> str:
attendance_groups = cast(
list[dict[str, int | str]] | None, event_data["attendance_groups"]
)
attendee = ""
if attendance_groups is not None:
attendance_group_list: list[tuple[int, str, int]] = []
for ppc in attendance_groups:
slug_sort_value = ORDER_PRESENT.get(cast(str, ppc["slug_name"]), 0)
attendance_group_list.append(
(
slug_sort_value,
cast(str, ppc["localized_name"]),
cast(int, ppc["count"]),
)
)
attendance_group_list.sort(reverse=True)

attendee = ", ".join(
[
f"{localized_name}: {count}"
for _, localized_name, count in attendance_group_list
]
)

return attendee


def extract_event_description(event_data: EVENT_TYPE, event: Event) -> None:
description = ""
score = extract_scores(event_data)
if score is not None:
description += f"{score}\n"
attendee = _extract_attendee_description(event_data)
description += attendee

event.add("description", description.strip())
Loading

0 comments on commit ac7c6cd

Please sign in to comment.