From 1daaf9a3bf61b34e4cec5b1edecc3cf51d3ad8c7 Mon Sep 17 00:00:00 2001 From: Thibault Date: Sat, 25 May 2024 15:58:40 +0200 Subject: [PATCH] refactor: add better api types --- calendar_connector/consts.py | 3 +- calendar_connector/event_convertor.py | 10 +- calendar_connector/event_utils/date.py | 10 +- calendar_connector/event_utils/description.py | 20 +-- calendar_connector/event_utils/location.py | 13 +- calendar_connector/event_utils/score.py | 22 ++- calendar_connector/event_utils/summary.py | 12 +- calendar_connector/sporteasy_connector.py | 18 +- calendar_connector/types/__init__.py | 0 calendar_connector/types/event_type.py | 86 +++++++++ calendar_connector/types/me_type.py | 31 ++++ calendar_connector/types/request_type.py | 13 ++ calendar_connector/types/season_type.py | 10 ++ calendar_connector/types/shared_type.py | 30 ++++ calendar_connector/types/team_type.py | 165 ++++++++++++++++++ 15 files changed, 387 insertions(+), 56 deletions(-) create mode 100644 calendar_connector/types/__init__.py create mode 100644 calendar_connector/types/event_type.py create mode 100644 calendar_connector/types/me_type.py create mode 100644 calendar_connector/types/request_type.py create mode 100644 calendar_connector/types/season_type.py create mode 100644 calendar_connector/types/shared_type.py create mode 100644 calendar_connector/types/team_type.py diff --git a/calendar_connector/consts.py b/calendar_connector/consts.py index 6dcbceb..00dd6db 100644 --- a/calendar_connector/consts.py +++ b/calendar_connector/consts.py @@ -1,4 +1,3 @@ -from typing import Any from collections import namedtuple import pytz @@ -18,7 +17,7 @@ route_change_presence = "/api/change_my_presence" PLAYED_WORDS = "played", "present", "available" -EVENT_TYPE = dict[str, int | str | Any | list[Any] | dict[str, Any]] + TIMEZONE = pytz.timezone("UTC") ORDER_PRESENT = { diff --git a/calendar_connector/event_convertor.py b/calendar_connector/event_convertor.py index 71ec847..c375d47 100644 --- a/calendar_connector/event_convertor.py +++ b/calendar_connector/event_convertor.py @@ -1,9 +1,8 @@ from datetime import datetime -from typing import Any, cast, Optional +from typing import Optional 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 ( @@ -13,12 +12,13 @@ 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 +from calendar_connector.types.event_type import EventType def event_to_calendar_event( team_id: int, team_name: str, - event_data: EVENT_TYPE, + event_data: EventType, links_data: Optional[GenerateLinksData], team_web_url: str, ) -> Event: @@ -38,8 +38,6 @@ def event_to_calendar_event( 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"] - ) + category_name = normalize(event_data["category"]["localized_name"]) return event diff --git a/calendar_connector/event_utils/date.py b/calendar_connector/event_utils/date.py index 9c88861..d52e7a7 100644 --- a/calendar_connector/event_utils/date.py +++ b/calendar_connector/event_utils/date.py @@ -1,15 +1,15 @@ from datetime import datetime, timedelta -from typing import cast from icalendar import Event -from calendar_connector.consts import EVENT_TYPE, TIMEZONE +from calendar_connector.consts import TIMEZONE +from calendar_connector.types.event_type import EventType -def extract_event_dates(event_data: EVENT_TYPE, event: Event) -> None: - start_date = datetime.fromisoformat(cast(str, event_data["start_at"])) +def extract_event_dates(event_data: EventType, event: Event) -> None: + start_date = datetime.fromisoformat(event_data["start_at"]) start_date = start_date.astimezone(TIMEZONE) - end_date_str = cast(str | None, event_data["end_at"]) + end_date_str = event_data["end_at"] if end_date_str is not None: end_date = datetime.fromisoformat(end_date_str) else: diff --git a/calendar_connector/event_utils/description.py b/calendar_connector/event_utils/description.py index 9244d6a..cae7e3d 100644 --- a/calendar_connector/event_utils/description.py +++ b/calendar_connector/event_utils/description.py @@ -4,7 +4,6 @@ from icalendar import Event from calendar_connector.consts import ( - EVENT_TYPE, ORDER_PRESENT, route_change_presence, PRESENCE, @@ -12,6 +11,7 @@ from calendar_connector.datetime_utils import get_formated_current_time from calendar_connector.event_utils.score import extract_scores from calendar_connector.cryptography import generate_hash +from calendar_connector.types.event_type import EventType @dataclasses.dataclass @@ -23,20 +23,18 @@ class GenerateLinksData: url_root: str -def _extract_attendee_description(event_data: EVENT_TYPE) -> str: - attendance_groups = cast( - list[dict[str, int | str]] | None, event_data["attendance_groups"] - ) +def _extract_attendee_description(event_data: EventType) -> str: + attendance_groups = 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) + slug_sort_value = ORDER_PRESENT.get(ppc["slug_name"], 0) attendance_group_list.append( ( slug_sort_value, - cast(str, ppc["localized_name"]), - cast(int, ppc["count"]), + ppc["localized_name"], + ppc["count"], ) ) attendance_group_list.sort(reverse=True) @@ -84,14 +82,14 @@ def _generate_response_links( return f"{present} | {absent}" -def _generate_link_to_sporteasy(team_web_url: str, event_data: EVENT_TYPE) -> str: +def _generate_link_to_sporteasy(team_web_url: str, event_data: EventType) -> str: id_ = event_data["id"] return f'SportEasy event' def extract_event_description( team_id: int, - event_data: EVENT_TYPE, + event_data: EventType, event: Event, links_data: Optional[GenerateLinksData], team_web_url: str, @@ -104,7 +102,7 @@ def extract_event_description( description += f"{attendee}\n" if links_data: - event_id = cast(int, event_data["id"]) + event_id = event_data["id"] response_links = _generate_response_links(team_id, event_id, links_data) description += f"{response_links}\n" diff --git a/calendar_connector/event_utils/location.py b/calendar_connector/event_utils/location.py index 9b7a51f..e53af91 100644 --- a/calendar_connector/event_utils/location.py +++ b/calendar_connector/event_utils/location.py @@ -1,19 +1,14 @@ -from typing import cast, Any +from typing import Optional from icalendar import Event -from calendar_connector.consts import EVENT_TYPE from calendar_connector.normalize import normalize +from calendar_connector.types.event_type import EventType -def extract_event_location(event_data: EVENT_TYPE, event: Event) -> None: +def extract_event_location(event_data: Optional[EventType], 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"] - ) + location = normalize(event_data["location"]["formatted_address"]) event.add("location", location) diff --git a/calendar_connector/event_utils/score.py b/calendar_connector/event_utils/score.py index d3690eb..5213297 100644 --- a/calendar_connector/event_utils/score.py +++ b/calendar_connector/event_utils/score.py @@ -1,24 +1,28 @@ -from typing import Literal, cast, Any +from typing import Literal -from calendar_connector.consts import EVENT_TYPE +from calendar_connector.types.event_type import EventType def _extract_scores_for_opponent( - event_data: EVENT_TYPE, left_or_right: Literal["left", "right"] + event_data: EventType, left_or_right: Literal["left", "right"] ) -> tuple[str, int] | None: - opponent = event_data.get(f"opponent_{left_or_right}") + if left_or_right == "left": + opponent = event_data.get("opponent_left") + else: + opponent = event_data.get("opponent_right") + if opponent is None: return None - opponent = cast(dict[str, Any], opponent) - score_str = opponent.get("score") + + score_str = opponent["score"] if score_str is None: return None - name = cast(str, opponent.get("short_name")) - score = int(cast(str, score_str)) + name = opponent["short_name"] + score = int(score_str) return name, score -def extract_scores(event_data: EVENT_TYPE) -> str | None: +def extract_scores(event_data: EventType) -> str | None: left = _extract_scores_for_opponent(event_data, "left") right = _extract_scores_for_opponent(event_data, "right") diff --git a/calendar_connector/event_utils/summary.py b/calendar_connector/event_utils/summary.py index 4f0b758..86855db 100644 --- a/calendar_connector/event_utils/summary.py +++ b/calendar_connector/event_utils/summary.py @@ -1,10 +1,9 @@ -from typing import cast, Any - from icalendar import Event -from calendar_connector.consts import EVENT_TYPE, MY_PRESENCE +from calendar_connector.consts import MY_PRESENCE from calendar_connector.custom_exceptions import AttributeNotFoundException from calendar_connector.normalize import normalize +from calendar_connector.types.event_type import EventType def _status_to_ics_status(sp_status: str) -> str: @@ -18,8 +17,8 @@ def _status_to_ics_status(sp_status: str) -> str: return sp_status -def extract_event_summary(event_data: EVENT_TYPE, event: Event, team_name: str) -> None: - name = normalize(cast(str, event_data["name"])) +def extract_event_summary(event_data: EventType, event: Event, team_name: str) -> None: + name = normalize(event_data["name"]) if team_name not in name: summary = f"{team_name} - {name}" else: @@ -37,9 +36,8 @@ def extract_event_summary(event_data: EVENT_TYPE, event: Event, team_name: str) # summary += f" - contre {opponent_name}" me_object = event_data.get("me", {}) - me_object = cast(dict[str, Any], me_object) if me_object is not None and me_object.get("group") is not None: - group = cast(dict[str, str], me_object.get("group", {})) + group = me_object.get("group", {}) localized_name_presence = group.get("localized_name") if localized_name_presence is not None: summary += f" - {normalize(localized_name_presence)}" diff --git a/calendar_connector/sporteasy_connector.py b/calendar_connector/sporteasy_connector.py index 372dbba..0666628 100644 --- a/calendar_connector/sporteasy_connector.py +++ b/calendar_connector/sporteasy_connector.py @@ -1,4 +1,3 @@ -from typing import cast import collections import requests @@ -6,13 +5,16 @@ from calendar_connector.consts import ( url_authenticate, url_list_teams, - EVENT_TYPE, url_list_events, url_put_event_presence, url_me, url_csrf, ) from calendar_connector.normalize import normalize +from calendar_connector.types.event_type import EventType +from calendar_connector.types.me_type import MeType +from calendar_connector.types.request_type import RequestType, CsrfType +from calendar_connector.types.team_type import TeamType team_namedtuple = collections.namedtuple("team_namedtuple", ["id", "name", "web_url"]) @@ -37,28 +39,30 @@ def login(self, username: str, password: str) -> str: def list_teams(self) -> list[team_namedtuple]: response = self.session_requests.get(url_list_teams) - data = response.json() + data: RequestType[TeamType] = response.json() return [ team_namedtuple(d["id"], normalize(d["name"]), d["web_url"]) for d in data["results"] ] - def list_events(self, team_id: int) -> list[EVENT_TYPE]: + def list_events(self, team_id: int) -> list[EventType]: 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"] + data: list[EventType] = response.json()["results"] return data def get_profile_id(self) -> int: response = self.session_requests.get(url_me) - profile_id = cast(int, response.json()["id"]) + data: MeType = response.json() + profile_id = data["id"] return profile_id def get_csrf_token(self, team_id: int) -> tuple[str, str]: response = self.session_requests.get(url_csrf) - csrf = response.json()["csrf_token"] + data: CsrfType = response.json() + csrf = data["csrf_token"] teams = self.session_requests.get(url_list_teams).json()["results"] current_team = [t for t in teams if t["id"] == team_id][0] diff --git a/calendar_connector/types/__init__.py b/calendar_connector/types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/calendar_connector/types/event_type.py b/calendar_connector/types/event_type.py new file mode 100644 index 0000000..17d2262 --- /dev/null +++ b/calendar_connector/types/event_type.py @@ -0,0 +1,86 @@ +from typing import TypedDict, Any, Optional + +from calendar_connector.types.season_type import SeasonType +from calendar_connector.types.shared_type import CountryType + + +class LocationType(TypedDict): + id: int + is_stadium: bool + lat: float + lng: float + name: str + url: str + formatted_address: str + country: CountryType + + +class GroupType(TypedDict): + attendance_status: str + importance_level: int + localized_name: str + slug_name: str + target_groups: list[str] + + +class OpponentType(TypedDict): + id: str + full_name: str + is_current_team: bool + is_home: bool + jersey_color: Optional[Any] + match_outcome: str + score: int + short_name: str + + +class AttendanceGroupType(TypedDict): + attendance_status: str + count: int + importance_level: int + localized_name: str + slug_name: str + + +class CategoryType(TypedDict): + championship_day: int + id: int + localized_name: str + slug_name: str + type: str + + +class RegistrationParametersType(TypedDict): + hide_attendance: bool + + +class MeType(TypedDict): + group: GroupType + opponent: str + url: str + + +class EventType(TypedDict): + id: int + name: str + start_at: str + end_at: str + url: str + thread_id: int + location: LocationType + available_threshold_reached: bool + can_see_attendance: bool + is_cancelled: bool + is_date_unknown: bool + is_past: bool + is_recurring: bool + is_sportive: bool + me: MeType + opponent_left: Optional[OpponentType] + opponent_right: Optional[OpponentType] + registration_open_at: str + step: str + attendance_groups: list[AttendanceGroupType] + category: CategoryType + registration_parameters: RegistrationParametersType + season: SeasonType diff --git a/calendar_connector/types/me_type.py b/calendar_connector/types/me_type.py new file mode 100644 index 0000000..272721a --- /dev/null +++ b/calendar_connector/types/me_type.py @@ -0,0 +1,31 @@ +from typing import TypedDict, Any, Optional + +from calendar_connector.types.shared_type import AvatarType + + +class LinkType(TypedDict): + method: str + url: str + + +class MeType(TypedDict): + activated_at: str + analytics_id: str + avatar: AvatarType + date_of_birth: Optional[str] + default_language: str + email: str + email_verification_deferment: bool + first_name: str + full_name: str + gender: Optional[str] + height: Optional[int] + id: int + is_verified: bool + last_name: str + phone_number: Optional[str] + profile_display_good_deals: bool + profile_display_origin_survey: bool + profile_display_purpose_survey: bool + weight: Optional[int] + _links: dict[str, LinkType] diff --git a/calendar_connector/types/request_type.py b/calendar_connector/types/request_type.py new file mode 100644 index 0000000..1fc0cdf --- /dev/null +++ b/calendar_connector/types/request_type.py @@ -0,0 +1,13 @@ +from typing import TypedDict, TypeVar, Generic + +T = TypeVar("T") + + +class RequestType(TypedDict, Generic[T]): + count: int + links: dict[str, str] + results: list[T] + + +class CsrfType(TypedDict): + csrf_token: str diff --git a/calendar_connector/types/season_type.py b/calendar_connector/types/season_type.py new file mode 100644 index 0000000..789374a --- /dev/null +++ b/calendar_connector/types/season_type.py @@ -0,0 +1,10 @@ +from typing import TypedDict + + +class SeasonType(TypedDict): + archived: bool + current: bool + start_date: str + end_date: str + name: str + slug_name: str diff --git a/calendar_connector/types/shared_type.py b/calendar_connector/types/shared_type.py new file mode 100644 index 0000000..c08e70b --- /dev/null +++ b/calendar_connector/types/shared_type.py @@ -0,0 +1,30 @@ +from typing import TypedDict + + +class CountryType(TypedDict): + iso2: str + name: str + + +class AvatarType(TypedDict): + _120x120: str + medium: str + field: str + small: str + + +class LogoType(TypedDict): + _168x168: str + _98x70: str + _54x54: str + + +class CoverType(TypedDict): + _640x414: str + _170x110: str + + +class SiteType(TypedDict): + url: str + color_primary: str + color_secondary: str diff --git a/calendar_connector/types/team_type.py b/calendar_connector/types/team_type.py new file mode 100644 index 0000000..be2617a --- /dev/null +++ b/calendar_connector/types/team_type.py @@ -0,0 +1,165 @@ +from typing import TypedDict, Any, Optional + +from calendar_connector.types.shared_type import ( + CountryType, + LogoType, + SiteType, + CoverType, + AvatarType, +) + + +class AgeCategoryType(TypedDict): + age_range: str + is_youth: bool + localized_age_range: str + + +class RoleType(TypedDict): + slug_name: str + localized_name: str + + +class MemberRoleType(TypedDict): + count: int + role: RoleType + + +class SportType(TypedDict): + slug_name: str + localized_name: str + icon_url: str + is_generic: bool + + +class FormatType(TypedDict): + nb_players: int + sport_slug_name: str + localized_name: str + + +class ClubType(TypedDict): + id: int + name: str + url: str + + +class DefaultStadiumType(TypedDict): + id: int + name: str + formatted_address: str + lat: float + lng: float + country: CountryType + url: str + is_stadium: bool + + +class ProfileType(TypedDict): + id: int + first_name: str + last_name: str + full_name: str + avatar: AvatarType + email: str + phone_number: str + activated_at: str + date_of_birth: Optional[str] + weight: Optional[Any] + height: Optional[Any] + default_language: str + is_active: bool + nickname: Optional[str] + invited_at: Optional[str] + + +class MeType(TypedDict): + is_admin: bool + licence_number: str + year_of_arrival: Optional[str] + profile: ProfileType + role: RoleType + usual_tactic_position: Optional[Any] + jersey_number: Optional[Any] + is_unavailable: bool + _links: dict + + +class PlanType(TypedDict): + slug_name: str + name: str + max_number_of_member: Optional[Any] + price: str + has_advertising: bool + is_premium: bool + + +class CurrentSubscriptionType(TypedDict): + id: int + plan: PlanType + start_at: str + end_at: str + is_auto_renew: bool + blocking_countdown: Optional[Any] + members_count: Optional[Any] + balance: Optional[Any] + pending_balance: Optional[Any] + next: Optional[Any] + profile: Optional[Any] + + +class CurrentSeasonType(TypedDict): + id: int + name: str + current: Optional[Any] + + +class MobilePlansType(TypedDict): + ios: list[str] + android: list[str] + + +class TeamType(TypedDict): + age_category: AgeCategoryType + age_range: str + email: str + full_name: str + gender: str + id: int + is_impersonated: bool + motto: str + name: str + practice_level: str + short_name: str + slug_name: str + team_group_type: str + timezone: str + url: str + url_categories: str + url_events: str + url_opponents: str + url_seasons: str + url_stadiums: str + web_url: str + + location: Optional[Any] + onboarding_info: Optional[Any] + phone_number: Optional[Any] + sponsor: Optional[Any] + sponsor_information: Optional[Any] + year_of_creation: Optional[Any] + + country: CountryType + logo: LogoType + site: SiteType + member_roles: list[MemberRoleType] + sport: SportType + format: FormatType + cover: CoverType + club: ClubType + default_stadium: DefaultStadiumType + me: MeType + current_subscription: CurrentSubscriptionType + current_season: CurrentSeasonType + mobile_plans: MobilePlansType + managers: list[Any]