Skip to content

Commit 49048d1

Browse files
authored
Add trackable support (#1)
* Add trackable support * Added comments * Logger not constructed.. * CR: - Adding some newlines - Removed double import
1 parent 3d6aae5 commit 49048d1

10 files changed

Lines changed: 161 additions & 40 deletions

File tree

geocachingapi/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
"""Python client for connecting to the Geocaching API"""
2-
from .geocachingapi import GeocachingApi, GeocachingStatus
2+
from .geocachingapi import GeocachingApi
3+
from .models import GeocachingSettings, GeocachingStatus

geocachingapi/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""Python client for accessing the Geocaching API"""
2-
__version__ = "0.0.1"
2+
__version__ = "0.0.7"

geocachingapi/const.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,11 @@
33
GEOCACHING_API_HOST = "staging.api.groundspeak.com"
44
GEOCACHING_API_PORT = 443
55
GEOCACHING_API_BASE_PATH = "/"
6-
GEOCACHING_API_VERSION = "v1"
6+
GEOCACHING_API_VERSION = "v1"
7+
8+
MEMBERSHIP_LEVELS = {
9+
0: "Unknown",
10+
1: "Basic",
11+
2: "Charter",
12+
3: "Premium"
13+
}

geocachingapi/exceptions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ class GeocachingApiConnectionTimeoutError(GeocachingApiConnectionError):
1313

1414

1515
class GeocachingApiRateLimitError(GeocachingApiConnectionError):
16-
"""GeocachingApi Rate Limit exception."""
16+
"""GeocachingApi Rate Limit exception."""
17+

geocachingapi/geocachingapi.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33

44
import asyncio
55
import json
6+
import logging
67
import socket
78
import async_timeout
89
import backoff
10+
911
from yarl import URL
1012
from aiohttp import ClientResponse, ClientSession, ClientError
1113

@@ -26,21 +28,29 @@
2628

2729
from .models import (
2830
GeocachingStatus,
31+
GeocachingSettings
2932
)
3033

31-
class GeocachingApi:
34+
_LOGGER = logging.getLogger(__name__)
3235

36+
class GeocachingApi:
37+
""" Main class to control the Geocaching API"""
3338
_close_session: bool = False
34-
_status: GeocachingStatus = GeocachingStatus()
39+
_status: GeocachingStatus = None
40+
_settings: GeocachingSettings = None
3541
def __init__(
3642
self,
3743
*,
3844
token: str,
45+
settings: GeocachingSettings = None,
3946
request_timeout: int = 8,
4047
session: Optional[ClientSession] = None,
41-
token_refresh_method: Optional[Callable[[], Awaitable[str]]] = None,
48+
token_refresh_method: Optional[Callable[[], Awaitable[str]]] = None
49+
4250
) -> None:
4351
"""Initialize connection with the Geocaching API."""
52+
self._status = GeocachingStatus()
53+
self._settings = settings or GeocachingSettings(False)
4454
self._session = session
4555
self.request_timeout = request_timeout
4656
self.token = token
@@ -54,13 +64,14 @@ async def _request(self, method, uri, **kwargs) -> ClientResponse:
5464
"""Make a request."""
5565
if self.token_refresh_method is not None:
5666
self.token = await self.token_refresh_method()
57-
67+
_LOGGER.debug(f'Received API request with token {self.token}')
5868
url = URL.build(
5969
scheme=GEOCACHING_API_SCHEME,
6070
host=GEOCACHING_API_HOST,
6171
port=GEOCACHING_API_PORT,
6272
path=GEOCACHING_API_BASE_PATH,
6373
).join(URL(uri))
74+
_LOGGER.debug(f'URL: {url}')
6475
headers = kwargs.get("headers")
6576

6677
if headers is None:
@@ -109,13 +120,19 @@ async def _request(self, method, uri, **kwargs) -> ClientResponse:
109120
# Handle empty response
110121
if response.status == 204:
111122
return
112-
123+
113124
if "application/json" in content_type:
114-
return await response.json()
115-
return await response.text()
125+
result = await response.json()
126+
_LOGGER.debug(f'response: {str(result)}')
127+
return result
128+
result = await response.text()
129+
_LOGGER.debug(f'response: {str(result)}')
130+
return result
116131

117132
async def update(self) -> GeocachingStatus:
118133
await self._update_user(None)
134+
if self._settings.fetch_trackables:
135+
await self._update_trackables()
119136
return self._status
120137

121138
async def _update_user(self, data: Dict[str, Any] = None) -> None:
@@ -128,11 +145,27 @@ async def _update_user(self, data: Dict[str, Any] = None) -> None:
128145
"hideCount",
129146
"favoritePoints",
130147
"souvenirCount",
131-
"awardedFavoritePoints"
148+
"awardedFavoritePoints",
149+
"membershipLevelId"
132150
])
133151
data = await self._request("GET", f"/{GEOCACHING_API_VERSION}/users/me?fields={fields}")
134-
self._status.user.update_from_dict(data)
152+
self._status.update_user_from_dict(data)
135153

154+
async def _update_trackables(self, data: Dict[str, Any] = None) -> None:
155+
assert self._status
156+
if data is None:
157+
fields = ",".join([
158+
"referenceCode",
159+
"name",
160+
"holder",
161+
"trackingNumber",
162+
"kilometersTraveled",
163+
"currentGeocacheCode",
164+
"currentGeocacheName"
165+
])
166+
data = await self._request("GET", f"/{GEOCACHING_API_VERSION}/trackables?fields={fields}&type=3")
167+
self._status.update_trackables_from_dict(data)
168+
136169
async def close(self) -> None:
137170
"""Close open client session."""
138171
if self._session and self._close_session:
@@ -144,4 +177,5 @@ async def __aenter__(self) -> GeocachingApi:
144177

145178
async def __aexit__(self, *exc_info) -> None:
146179
"""Async exit."""
147-
await self.close()
180+
await self.close()
181+

geocachingapi/models.py

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
from __future__ import annotations
2-
from typing import Any, Callable, Dict, Optional
2+
from typing import Any, Dict, Optional
33

44
from dataclasses import dataclass
5+
from datetime import datetime
6+
from .utils import try_get_from_dict
7+
8+
9+
class GeocachingSettings:
10+
"""Class to hold the Geocaching Api settings"""
11+
fetch_trackables: bool = False
12+
def __init__(self, fetch_trackables:bool = False) -> None:
13+
"""Initialize settings"""
14+
self.fetch_trackables = fetch_trackables
15+
16+
17+
518

619
@dataclass
720
class GeocachingUser:
21+
"""Class to hold the Geocaching user information"""
822
reference_code: Optional[str] = None
923
username: Optional[str] = None
1024
find_count: Optional[int] = None
@@ -14,27 +28,63 @@ class GeocachingUser:
1428
awarded_favorite_points: Optional[int] = None
1529

1630
def update_from_dict(self, data: Dict[str, Any]) -> None:
17-
if "username" in data:
18-
self.username = data["username"]
19-
if "referenceCode" in data:
20-
self.reference_code = data["referenceCode"]
21-
if "findCount" in data:
22-
self.find_count = data["findCount"]
23-
if "hideCount" in data:
24-
self.hide_count = data["hideCount"]
25-
if "favoritePoints" in data:
26-
self.favorite_points = data["favoritePoints"]
27-
if "souvenirCount" in data:
28-
self.souvenir_count = data["souvenirCount"]
29-
if "awardedFavoritePoints" in data:
30-
self.awarded_favorite_points = data["awardedFavoritePoints"]
31-
32-
pass
31+
"""Update user from the API result"""
32+
self.reference_code = try_get_from_dict(data, "referenceCode", self.reference_code)
33+
self.username = try_get_from_dict(data, "username", self.username)
34+
self.find_count = try_get_from_dict(data, "findCount", self.find_count)
35+
self.hide_count = try_get_from_dict(data, "hideCount", self.hide_count)
36+
self.favorite_points = try_get_from_dict(data, "favoritePoints", self.favorite_points)
37+
self.souvenir_count = try_get_from_dict(data, "souvenirCount", self.souvenir_count)
38+
self.awarded_favorite_points = try_get_from_dict(data, "awardedFavoritePoints", self.awarded_favorite_points)
39+
40+
@dataclass
41+
class GeocachingTrackable:
42+
"""Class to hold the Geocaching trackable information"""
43+
reference_code: Optional[str] = None
44+
name: Optional[str] = None
45+
holder: GeocachingUser = None
46+
tracking_number: Optional[str] = None
47+
kilometers_traveled: Optional[datetime] = None
48+
current_geocache_code: Optional[str] = None
49+
current_geocache_name: Optional[str] = None
50+
51+
def update_from_dict(self, data: Dict[str, Any]) -> None:
52+
"""Update trackble from the API"""
53+
self.reference_code = try_get_from_dict(data, "referenceCode", self.reference_code)
54+
self.name = try_get_from_dict(data, "name", self.name)
55+
if data["holder"] is not None:
56+
if self.holder is None :
57+
holder = GeocachingUser()
58+
holder.update_from_dict(data["holder"])
59+
else:
60+
holder = None
61+
62+
self.tracking_number = try_get_from_dict(data, "trackingNumber", self.tracking_number)
63+
self.kilometers_traveled = try_get_from_dict(data, "kilometersTraveled", self.kilometers_traveled)
64+
self.current_geocache_code = try_get_from_dict(data, "currectGeocacheCode", self.current_geocache_code)
65+
self.current_geocache_name = try_get_from_dict(data, "currentGeocacheName", self.current_geocache_name)
3366

3467
class GeocachingStatus:
35-
user: GeocachingUser = GeocachingUser()
36-
def __init__(self) -> None:
37-
pass
68+
"""Class to hold all account status information"""
69+
user: GeocachingUser = None
70+
trackables: Dict[str, GeocachingTrackable] = None
71+
72+
def __init__(self):
73+
"""Initialize GeocachingStatus"""
74+
self.user = GeocachingUser()
75+
self.trackables = {}
3876

39-
def update_from_dict(self, data: Dict[str, Any]) -> GeocachingStatus:
40-
return self
77+
def update_user_from_dict(self, data: Dict[str, Any]) -> None:
78+
"""Update user from the API result"""
79+
self.user.update_from_dict(data)
80+
81+
def update_trackables_from_dict(self, data: Any) -> None:
82+
"""Update trackables from the API result"""
83+
if not any(data):
84+
pass
85+
for trackable in data:
86+
reference_code = trackable["referenceCode"]
87+
if not reference_code in self.trackables.keys():
88+
self.trackables[reference_code] = GeocachingTrackable()
89+
self.trackables[reference_code].update_from_dict(trackable)
90+

geocachingapi/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Utils for consuming the API"""
2+
from typing import Dict, Any, Callable, Optional
3+
4+
def try_get_from_dict(data: Dict[str, Any], key: str, original_value: Any, conversion: Optional[Callable[[Any], Any]] = None) -> Any:
5+
"""Try to get value from dict, otherwise set default value"""
6+
if not key in data:
7+
return None
8+
9+
value = data[key]
10+
if value is None:
11+
return original_value
12+
if conversion is None:
13+
return value
14+
return conversion(value)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def read(*parts):
4444
"Programming Language :: Python :: 3",
4545
"Topic :: Software Development :: Libraries :: Python Modules",
4646
],
47-
python_requires='>=3.9',
47+
python_requires='>=3.8',
4848
zip_safe=False,
4949
include_package_data=True,
5050
)

tests/pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
[pytest]
2+
log_cli = true
3+
log_cli_level = 0
24
log_format = %(asctime)s %(levelname)s %(message)s
35
log_date_format = %Y-%m-%d %H:%M:%S

tests/test.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
"""Test for Geocaching Api integration."""
2-
from geocachingapi import GeocachingApi, GeocachingStatus
2+
from geocachingapi import GeocachingApi, GeocachingStatus, GeocachingSettings
33
#For adding your token add a file call token.py to current folder and add TOKEN = "<your token here>"
44
from .token import TOKEN
55
import asyncio
66
import pytest
77

8+
# @pytest.mark.asyncio
9+
import logging
10+
logging.basicConfig(level=logging.DEBUG)
11+
12+
mylogger = logging.getLogger()
13+
814
@pytest.mark.asyncio
915
async def test():
1016
status:GeocachingStatus = None
1117
api = GeocachingApi(token=TOKEN)
1218
status = await api.update()
19+
print(status.user.reference_code)
20+
assert(status.user.reference_code is not None)
21+
assert(status.user.find_count is not None)
22+
await api.close()
23+
assert(False)
24+
25+
26+
1327

14-
assert(status.user.username is not None)
15-
assert(status.user.find_count is not None)

0 commit comments

Comments
 (0)