diff --git a/src/garth/__init__.py b/src/garth/__init__.py index 4245dc6b..03c4998a 100644 --- a/src/garth/__init__.py +++ b/src/garth/__init__.py @@ -1,5 +1,6 @@ from .data import ( Activity, + Badge, BodyBatteryData, DailyBodyBatteryStress, DailyHeartRate, @@ -34,6 +35,7 @@ __all__ = [ "Activity", + "Badge", "BodyBatteryData", "Client", "DailyBodyBatteryStress", diff --git a/src/garth/data/__init__.py b/src/garth/data/__init__.py index 49e222c7..f4c827b2 100644 --- a/src/garth/data/__init__.py +++ b/src/garth/data/__init__.py @@ -1,5 +1,6 @@ __all__ = [ "Activity", + "Badge", "BodyBatteryData", "BodyBatteryEvent", "BodyBatteryReading", @@ -18,6 +19,7 @@ ] from .activity import Activity +from .badge import Badge from .body_battery import ( BodyBatteryData, BodyBatteryEvent, diff --git a/src/garth/data/badge.py b/src/garth/data/badge.py new file mode 100644 index 00000000..a42601e6 --- /dev/null +++ b/src/garth/data/badge.py @@ -0,0 +1,191 @@ +from datetime import datetime + +from pydantic.dataclasses import dataclass +from typing_extensions import Self + +from .. import http +from ..utils import camel_to_snake_dict + + +@dataclass(frozen=True) +class Badge: + """Garmin Connect badges data. + + Retrieve badges by ID or full list. + + Example: + >>> badge = Badge.get(55, client=authed_client) + >>> badge.badge_name + 'Strong Start' + >>> badge.earned_by_me + True + """ + + badge_id: int + badge_key: str + badge_name: str + badge_category_id: int + badge_difficulty_id: int + badge_points: int + badge_type_ids: tuple[int] + premium: bool + earned_by_me: bool + badge_assoc_type_id: int + badge_assoc_type: str + user_profile_id: int | None = None + full_name: str | None = None + display_name: str | None = None + badge_is_viewed: bool | None = None + badge_uuid: str | None = None + badge_series_id: int | None = None + badge_start_date: datetime | None = None + badge_end_date: datetime | None = None + badge_earned_date: datetime | None = None + badge_earned_number: int | None = None + badge_limit_count: int | None = None + badge_progress_value: int | float | None = None + badge_target_value: int | float | None = None + badge_unit_id: int | None = None + badge_assoc_data_id: str | None = None + badge_assoc_data_name: str | None = None + create_date: datetime | None = None + + CATEGORY_ACTIVITIES = 1 + CATEGORY_RUNNING = 2 + CATEGORY_CYCLING = 3 + CATEGORY_CHALLENGES = 4 + CATEGORY_STEPS = 5 + CATEGORY_CONNECT_FEATURES = 6 + CATEGORY_HEALTH = 7 + CATEGORY_TACX_MULTI_STAGE = 8 + CATEGORY_DIVING = 9 + CATEGORY_GOLF = 10 + + TYPE_ONE_TIME = 1 + TYPE_TRAINING_CLASS = 2 + TYPE_REPEATABLE = 3 + TYPE_CUMULATIVE = 4 + TYPE_LIMITED_ANNUAL = 5 + TYPE_LIMITED_SINGLE = 6 + TYPE_SERIES_EVENTS = 7 + + DIFFICULTY_EASY = 1 + DIFFICULTY_MEDIUM = 2 + DIFFICULTY_HARD = 3 + DIFFICULTY_ELITE = 4 + + ASSOC_TYPE_ACTIVITY = 1 + ASSOC_TYPE_GROUP_CHALLENGE = 2 + ASSOC_TYPE_ADHOC_CHALLENGE = 3 + ASSOC_TYPE_DAY = 4 + ASSOC_TYPE_NO_LINK = 5 + ASSOC_TYPE_ACTIVITY_DAY = 6 + ASSOC_TYPE_VIVOFITJR_CHALLENGE = 7 + ASSOC_TYPE_VIVOFITJR_TEAM_CHALLENGE = 8 + ASSOC_TYPE_BADGE_CHALLENGE = 9 + ASSOC_TYPE_EVENT = 10 + ASSOC_TYPE_SCORECARD = 11 + + UNIT_MI_KM = 1 + UNIT_FT_M = 2 + UNIT_ACTIVITIES = 3 + UNIT_DAYS = 4 + UNIT_STEPS = 5 + UNIT_MI = 6 + UNIT_SECONDS = 7 + UNIT_CHALLENGES = 8 + UNIT_KILOCALORIES = 9 + UNIT_WEEKS = 10 + UNIT_LIKES = 11 + + @property + def limited_time(self) -> bool: + return Badge.TYPE_LIMITED_SINGLE in self.badge_type_ids + + @property + def annual(self) -> bool: + return Badge.TYPE_LIMITED_ANNUAL in self.badge_type_ids + + @property + def repeatable(self) -> bool: + return Badge.TYPE_REPEATABLE in self.badge_type_ids + + @property + def cumulative(self) -> bool: + return Badge.TYPE_CUMULATIVE in self.badge_type_ids + + @property + def month_challenge(self) -> bool: + return ( + self.badge_category_id == Badge.CATEGORY_CHALLENGES + and self.limited_time + ) + + @property + def expedition(self) -> bool: + return self.badge_assoc_type_id == Badge.ASSOC_TYPE_BADGE_CHALLENGE + + def reload(self, client: http.Client | None = None) -> Self: + """Get actual data for Badge + Useful to retrieve actual information for repeatable badges from list response + """ + return type(self).get(self.badge_id, client or http.client) + + @classmethod + def get(cls, badge_id: int, client: http.Client | None = None) -> Self: + """Get badge by ID. + + Args: + badge_id: The Garmin badge ID + client: Optional HTTP client (uses default if not provided) + + Returns: + Badge instance with full details + """ + client = client or http.client + path = f"/badge-service/badge/detail/v2/{badge_id}" + data = client.connectapi(path) + if data is None: + raise ValueError(f"No data returned from {path}") + if not isinstance(data, dict): + raise TypeError( + f"Expected dict from {path}, got {type(data).__name__}" + ) + + data = camel_to_snake_dict(data) + return cls(**data) + + @classmethod + def list( + cls, + client: http.Client | None = None, + ) -> list[Self]: + """List of badges, combines earned and available lists. + Earned and repeatable badges contain data for the first receiving + For actual progress they should be loaded directly by get or reload methods + + Returns: + List of Badge instances + """ + client = client or http.client + + path = "/badge-service/badge/earned" + earned = client.connectapi(path) + cls._require_type(earned, list, path) + + path = "/badge-service/badge/available?showExclusiveBadge=true" + available = client.connectapi(path) + cls._require_type(available, list, path) + + data = earned + available + if not all(isinstance(item, dict) for item in data): + raise TypeError("Badge list payload contains non-dict entries") + + return [cls(**camel_to_snake_dict(item)) for item in data] + + @staticmethod + def _require_type(payload: object, expected: type, path: str) -> None: + if not isinstance(payload, expected): + raise TypeError( + f"Expected {expected.__name__} from {path}, got {type(payload).__name__}" + ) diff --git a/tests/data/cassettes/test_badge_get.yaml b/tests/data/cassettes/test_badge_get.yaml new file mode 100644 index 00000000..4121135a --- /dev/null +++ b/tests/data/cassettes/test_badge_get.yaml @@ -0,0 +1,188 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer SANITIZED + Connection: + - keep-alive + User-Agent: + - GCM-iOS-5.7.2.1 + method: GET + uri: https://connectapi.garmin.com/badge-service/badge/detail/v2/55 + response: + body: + string: '{ + "badgeId": 55, + "badgeKey": "activity_new_year", + "badgeName": "Strong Start", + "badgeUuid": null, + "badgeCategoryId": 1, + "badgeDifficultyId": 1, + "badgePoints": 1, + "badgeTypeIds": [ + 3, + 5 + ], + "badgeSeriesId": 3, + "badgeStartDate": "2018-01-01T00:00:00.0", + "badgeEndDate": "2027-01-01T23:59:59.0", + "userProfileId": 126523240, + "fullName": "Garmin", + "displayName": "user-guid", + "badgeEarnedDate": "2026-01-01T13:58:28.0", + "badgeEarnedNumber": 1, + "premium": false, + "badgeLimitCount": 250, + "badgeIsViewed": true, + "badgeProgressValue": 0.0, + "badgeTargetValue": null, + "badgeUnitId": null, + "badgeAssocTypeId": 1, + "badgeAssocType": "activityId", + "badgeAssocDataId": "21411202296", + "badgeAssocDataName": null, + "earnedByMe": true, + "currentPlayerType": null, + "userJoined": null, + "badgeChallengeStatusId": null, + "badgePromotionCodeTypeList": [ + null + ], + "promotionCodeStatus": null, + "createDate": "2026-01-01T14:04:22.60", + "relatedBadges": null + }' + headers: + Content-Type: + - application/json;charset=UTF-8 + Date: + - Thu, 18 Feb 2026 12:00:00 GMT + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer SANITIZED + Connection: + - keep-alive + User-Agent: + - GCM-iOS-5.7.2.1 + method: GET + uri: https://connectapi.garmin.com/badge-service/badge/detail/v2/2139 + response: + body: + string: '{ + "badgeId": 2139, + "badgeKey": "virtual_hike_via_transilvanica", + "badgeName": "Via Transilvanica", + "badgeUuid": "3054CA5CB9F24491A6287AE6281D03E3", + "badgeCategoryId": 4, + "badgeDifficultyId": 3, + "badgePoints": 4, + "badgeTypeIds": [ + 4 + ], + "badgeSeriesId": 86, + "badgeStartDate": "2023-01-01T00:00:00.0", + "badgeEndDate": null, + "userProfileId": 126523240, + "fullName": "Garmin", + "displayName": "user-guid", + "badgeEarnedDate": "2026-02-10T12:29:06.777", + "badgeEarnedNumber": 1, + "premium": false, + "badgeLimitCount": null, + "badgeIsViewed": true, + "badgeProgressValue": 1400000.0, + "badgeTargetValue": 1400000.0, + "badgeUnitId": 1, + "badgeAssocTypeId": 9, + "badgeAssocType": "none", + "badgeAssocDataId": "3054CA5CB9F24491A6287AE6281D03E3", + "badgeAssocDataName": null, + "earnedByMe": true, + "currentPlayerType": null, + "userJoined": true, + "badgeChallengeStatusId": null, + "badgePromotionCodeTypeList": [ + null + ], + "promotionCodeStatus": null, + "createDate": "2025-09-27T17:36:01.822", + "relatedBadges": [ + { + "badgeId": 1603, + "badgeKey": "virtual_hike_camino_de_santiago", + "badgeUuid": "34D4BEAEBAD9427C9DF73672B1F91DBD", + "badgeName": "Camino de Santiago", + "badgeDifficultyId": 3, + "badgePoints": 4, + "badgeTypeIds": [ + 4 + ], + "earnedByMe": true, + "badgeCategoryId": 4 + }, + { + "badgeId": 1619, + "badgeKey": "virtual_hike_mont_blanc_circular", + "badgeUuid": "B8416056067147AA8EE10ED46AFB3EFB", + "badgeName": "Mont Blanc Circular", + "badgeDifficultyId": 2, + "badgePoints": 2, + "badgeTypeIds": [ + 4 + ], + "earnedByMe": true, + "badgeCategoryId": 4 + }, + { + "badgeId": 1625, + "badgeKey": "virtual_hike_rheinsteig_trail", + "badgeUuid": "63584C38779C4977A76CE71FD13BDA3D", + "badgeName": "Rheinsteig Trail", + "badgeDifficultyId": 3, + "badgePoints": 4, + "badgeTypeIds": [ + 4 + ], + "earnedByMe": true, + "badgeCategoryId": 4 + }, + { + "badgeId": 1627, + "badgeKey": "virtual_climb_west_highland_way", + "badgeUuid": "BC90E4D52430458C8A11EB95D169E692", + "badgeName": "West Highland Way", + "badgeDifficultyId": 2, + "badgePoints": 2, + "badgeTypeIds": [ + 4 + ], + "earnedByMe": true, + "badgeCategoryId": 4 + } + ], + "connectionNumber": 1, + "connections": [] + }' + headers: + Content-Type: + - application/json;charset=UTF-8 + Date: + - Thu, 18 Feb 2026 12:00:00 GMT + status: + code: 200 + message: OK +version: 1 diff --git a/tests/data/cassettes/test_badge_list.yaml b/tests/data/cassettes/test_badge_list.yaml new file mode 100644 index 00000000..07205400 --- /dev/null +++ b/tests/data/cassettes/test_badge_list.yaml @@ -0,0 +1,373 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer SANITIZED + Connection: + - keep-alive + User-Agent: + - GCM-iOS-5.7.2.1 + method: GET + uri: https://connectapi.garmin.com/badge-service/badge/earned + response: + body: + string: '[ + { + "badgeId": 1166, + "badgeKey": "breath_50_times", + "badgeName": "Deep Breathing 4", + "badgeUuid": null, + "badgeCategoryId": 1, + "badgeDifficultyId": 3, + "badgePoints": 4, + "badgeTypeIds": [ + 4 + ], + "badgeSeriesId": 59, + "badgeStartDate": "2020-04-28T00:00:00.0", + "badgeEndDate": null, + "userProfileId": 42, + "fullName": "Garmin", + "displayName": "user-guid", + "badgeEarnedDate": "2026-02-18T06:05:49.0", + "badgeEarnedNumber": 1, + "premium": false, + "badgeLimitCount": null, + "badgeIsViewed": false, + "badgeProgressValue": 50.0, + "badgeTargetValue": 50.0, + "badgeUnitId": 3, + "badgeAssocTypeId": 5, + "badgeAssocType": "none", + "badgeAssocDataId": null, + "badgeAssocDataName": null, + "earnedByMe": true, + "currentPlayerType": null, + "userJoined": null, + "badgeChallengeStatusId": null, + "badgePromotionCodeTypeList": [ + null + ], + "promotionCodeStatus": null, + "createDate": "2024-12-23T15:30:55.66", + "relatedBadges": null + }, + { + "badgeId": 2886, + "badgeKey": "activity_chinese_horse_year_2026", + "badgeName": "Year of the Horse 2026", + "badgeUuid": "A7210D7B58D14501BB209993911F18E1", + "badgeCategoryId": 1, + "badgeDifficultyId": 1, + "badgePoints": 1, + "badgeTypeIds": [ + 1, + 6 + ], + "badgeSeriesId": null, + "badgeStartDate": "2026-02-17T00:00:00.0", + "badgeEndDate": "2026-02-17T23:59:59.0", + "userProfileId": 42, + "fullName": "Garmin", + "displayName": "user-guid", + "badgeEarnedDate": "2026-02-17T04:20:56.0", + "badgeEarnedNumber": 1, + "premium": false, + "badgeLimitCount": null, + "badgeIsViewed": false, + "badgeProgressValue": 0.0, + "badgeTargetValue": null, + "badgeUnitId": null, + "badgeAssocTypeId": 1, + "badgeAssocType": "activityId", + "badgeAssocDataId": "21892246130", + "badgeAssocDataName": null, + "earnedByMe": true, + "currentPlayerType": null, + "userJoined": null, + "badgeChallengeStatusId": null, + "badgePromotionCodeTypeList": [ + null + ], + "promotionCodeStatus": null, + "createDate": "2026-02-17T05:22:59.247", + "relatedBadges": null + }, + { + "badgeId": 1136, + "badgeKey": "steps_30day_10000", + "badgeName": "10K a Day Challenge", + "badgeUuid": null, + "badgeCategoryId": 5, + "badgeDifficultyId": 3, + "badgePoints": 4, + "badgeTypeIds": [ + 3, + 4 + ], + "badgeSeriesId": null, + "badgeStartDate": "2020-04-01T00:00:00.0", + "badgeEndDate": null, + "userProfileId": 42, + "fullName": "Garmin", + "displayName": "user-guid", + "badgeEarnedDate": "2026-02-15T18:03:34.11", + "badgeEarnedNumber": 11, + "premium": false, + "badgeLimitCount": 250, + "badgeIsViewed": true, + "badgeProgressValue": 30.0, + "badgeTargetValue": 30.0, + "badgeUnitId": 4, + "badgeAssocTypeId": 5, + "badgeAssocType": "none", + "badgeAssocDataId": null, + "badgeAssocDataName": null, + "earnedByMe": true, + "currentPlayerType": null, + "userJoined": null, + "badgeChallengeStatusId": null, + "badgePromotionCodeTypeList": [ + null + ], + "promotionCodeStatus": null, + "createDate": "2026-01-01T22:54:12.827", + "relatedBadges": null + } + ]' + headers: + Content-Type: + - application/json;charset=UTF-8 + Date: + - Thu, 18 Feb 2026 12:00:00 GMT + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer SANITIZED + Connection: + - keep-alive + User-Agent: + - GCM-iOS-5.7.2.1 + method: GET + uri: https://connectapi.garmin.com/badge-service/badge/available?showExclusiveBadge=true + response: + body: + string: '[ + { + "badgeId": 2918, + "badgeKey": "challenge_run_10k_2026_02", + "badgeName": "February Weekend 10K", + "badgeUuid": "DF7FEB14CBD54D81A3A2388ECB888DC8", + "badgeCategoryId": 4, + "badgeDifficultyId": 2, + "badgePoints": 2, + "badgeTypeIds": [ + 1, + 6 + ], + "badgeSeriesId": null, + "badgeStartDate": "2026-02-20T00:00:00.0", + "badgeEndDate": "2026-02-22T23:59:59.0", + "userProfileId": null, + "fullName": null, + "displayName": null, + "badgeEarnedDate": null, + "badgeEarnedNumber": null, + "premium": false, + "badgeLimitCount": null, + "badgeIsViewed": null, + "badgeProgressValue": null, + "badgeTargetValue": null, + "badgeUnitId": null, + "badgeAssocTypeId": 9, + "badgeAssocType": "activityId", + "badgeAssocDataId": null, + "badgeAssocDataName": null, + "earnedByMe": false, + "currentPlayerType": null, + "userJoined": null, + "badgeChallengeStatusId": null, + "badgePromotionCodeTypeList": [], + "promotionCodeStatus": null, + "createDate": "2026-02-08T13:48:59.727", + "relatedBadges": null + }, + { + "badgeId": 77, + "badgeKey": "run_marathon", + "badgeName": "Marathon", + "badgeUuid": null, + "badgeCategoryId": 2, + "badgeDifficultyId": 4, + "badgePoints": 8, + "badgeTypeIds": [ + 3 + ], + "badgeSeriesId": 20, + "badgeStartDate": "2018-04-03T00:00:00.0", + "badgeEndDate": null, + "userProfileId": null, + "fullName": null, + "displayName": null, + "badgeEarnedDate": null, + "badgeEarnedNumber": null, + "premium": false, + "badgeLimitCount": 250, + "badgeIsViewed": null, + "badgeProgressValue": null, + "badgeTargetValue": null, + "badgeUnitId": null, + "badgeAssocTypeId": 1, + "badgeAssocType": "activityId", + "badgeAssocDataId": null, + "badgeAssocDataName": null, + "earnedByMe": false, + "currentPlayerType": null, + "userJoined": null, + "badgeChallengeStatusId": null, + "badgePromotionCodeTypeList": [], + "promotionCodeStatus": null, + "createDate": null, + "relatedBadges": null + }, + { + "badgeId": 2733, + "badgeKey": "nutrition_day_log_weight", + "badgeName": "Daily Balance", + "badgeUuid": "EB9322A42AC34F46A224D861905BC01C", + "badgeCategoryId": 7, + "badgeDifficultyId": 2, + "badgePoints": 2, + "badgeTypeIds": [ + 3, + 4 + ], + "badgeSeriesId": 125, + "badgeStartDate": "2026-01-05T00:00:00.0", + "badgeEndDate": null, + "userProfileId": 42, + "fullName": "Garmin", + "displayName": "user-guid", + "badgeEarnedDate": null, + "badgeEarnedNumber": 1, + "premium": false, + "badgeLimitCount": 250, + "badgeIsViewed": false, + "badgeProgressValue": 24.0, + "badgeTargetValue": 30.0, + "badgeUnitId": 4, + "badgeAssocTypeId": 5, + "badgeAssocType": "none", + "badgeAssocDataId": null, + "badgeAssocDataName": null, + "earnedByMe": false, + "currentPlayerType": null, + "userJoined": null, + "badgeChallengeStatusId": null, + "badgePromotionCodeTypeList": [ + null + ], + "promotionCodeStatus": null, + "createDate": "2026-01-14T13:20:46.629", + "relatedBadges": null + }]' + headers: + Content-Type: + - application/json;charset=UTF-8 + Date: + - Thu, 18 Feb 2026 12:00:00 GMT + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer SANITIZED + Connection: + - keep-alive + User-Agent: + - GCM-iOS-5.7.2.1 + method: GET + uri: https://connectapi.garmin.com/badge-service/badge/detail/v2/1136 + response: + body: + string: '{ + "badgeId": 1136, + "badgeKey": "steps_30day_10000", + "badgeName": "10K a Day Challenge", + "badgeUuid": null, + "badgeCategoryId": 5, + "badgeDifficultyId": 3, + "badgePoints": 4, + "badgeTypeIds": [ + 3, + 4 + ], + "badgeSeriesId": null, + "badgeStartDate": "2020-04-01T00:00:00.0", + "badgeEndDate": null, + "userProfileId": 42, + "fullName": "Garmin", + "displayName": "user-guid", + "badgeEarnedDate": "2026-02-15T18:03:34.11", + "badgeEarnedNumber": 11, + "premium": false, + "badgeLimitCount": 250, + "badgeIsViewed": true, + "badgeProgressValue": 5.0, + "badgeTargetValue": 30.0, + "badgeUnitId": 4, + "badgeAssocTypeId": 5, + "badgeAssocType": "none", + "badgeAssocDataId": null, + "badgeAssocDataName": null, + "earnedByMe": true, + "currentPlayerType": null, + "userJoined": true, + "badgeChallengeStatusId": null, + "badgePromotionCodeTypeList": [ + null + ], + "promotionCodeStatus": null, + "createDate": "2026-01-01T22:54:12.827", + "relatedBadges": [], + "connectionNumber": 1, + "connections": [ + { + "userProfileId": 44, + "fullName": "Garmin1", + "displayName": "guid", + "profileImageUrlMedium": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/guid-prfr.png", + "profileImageUrlSmall": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/guid-prth.png", + "userLevel": 6, + "badgeEarnedDate": "2023-04-26T17:34:10.293" + } + ] + }' + headers: + Content-Type: + - application/json;charset=UTF-8 + Date: + - Thu, 18 Feb 2026 12:00:00 GMT + status: + code: 200 + message: OK +version: 1 diff --git a/tests/data/test_badge.py b/tests/data/test_badge.py new file mode 100644 index 00000000..3313bdd7 --- /dev/null +++ b/tests/data/test_badge.py @@ -0,0 +1,41 @@ +import pytest + +from garth import Badge +from garth.http import Client + + +@pytest.mark.vcr +def test_badge_list(authed_client: Client): + badges = Badge.list(client=authed_client) + assert len(badges) == 6 + assert len([b for b in badges if b.earned_by_me]) == 3 + for badge in badges: + assert badge.badge_id + assert badge.badge_name + assert badge.badge_category_id + assert badge.badge_type_ids + assert badge.badge_assoc_type_id + + earned_badge = next(b for b in badges if b.badge_id == 1136) + assert earned_badge.repeatable + assert earned_badge.badge_target_value == 30 + assert ( + earned_badge.badge_progress_value == 30 + ) # in list it's always equal target for earned + assert earned_badge.reload(client=authed_client).badge_progress_value == 5 + + limited_time_badge = next(b for b in badges if b.badge_id == 2886) + assert limited_time_badge.limited_time + + cumulative_badge = next(b for b in badges if b.badge_id == 2733) + assert cumulative_badge.cumulative + + month_challenge_badge = next(b for b in badges if b.badge_id == 2918) + assert month_challenge_badge.month_challenge + + +@pytest.mark.vcr +def test_badge_get(authed_client: Client): + assert Badge.get(55, client=authed_client).annual + + assert Badge.get(2139, client=authed_client).expedition