Skip to content

Commit 530722c

Browse files
committed
- Initial setup GeocachingAPi class
- Added some models. - Added a test
1 parent a87adf5 commit 530722c

7 files changed

Lines changed: 197 additions & 8 deletions

File tree

geocachingapi/__init__.py

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

geocachingapi/const.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
"""Geocaching Api Constants."""
1+
"""Geocaching Api Constants."""
2+
GEOCACHING_API_SCHEME = "https"
3+
GEOCACHING_API_HOST = "staging.api.groundspeak.com"
4+
GEOCACHING_API_PORT = 443
5+
GEOCACHING_API_BASE_PATH = "/"
6+
GEOCACHING_API_VERSION = "v1"

geocachingapi/exceptions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Exceptions for the Gecaching API."""
2+
3+
class GeocachingApiError(Exception):
4+
"""Generic GeocachingApi exception."""
5+
6+
7+
class GeocachingApiConnectionError(GeocachingApiError):
8+
"""GeocachingApi connection exception."""
9+
10+
11+
class GeocachingApiConnectionTimeoutError(GeocachingApiConnectionError):
12+
"""GeocachingApi connection timeout exception."""
13+
14+
15+
class GeocachingApiRateLimitError(GeocachingApiConnectionError):
16+
"""GeocachingApi Rate Limit exception."""

geocachingapi/geocachingapi.py

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,138 @@
11
"""Class for managing one Geocaching API integration."""
2+
from __future__ import annotations
3+
4+
import asyncio
5+
import json
6+
import socket
7+
import async_timeout
8+
import backoff
9+
from yarl import URL
10+
from aiohttp import ClientResponse, ClientSession, ClientError
11+
12+
from typing import Any, Awaitable, Callable, Dict, List, Optional
13+
from .const import (
14+
GEOCACHING_API_BASE_PATH,
15+
GEOCACHING_API_HOST,
16+
GEOCACHING_API_PORT,
17+
GEOCACHING_API_SCHEME,
18+
GEOCACHING_API_VERSION,
19+
)
20+
from .exceptions import (
21+
GeocachingApiConnectionError,
22+
GeocachingApiConnectionTimeoutError,
23+
GeocachingApiError,
24+
GeocachingApiRateLimitError,
25+
)
26+
27+
from .models import (
28+
GeocachingStatus,
29+
)
30+
231
class GeocachingApi:
3-
def __init__(self) -> None:
4-
pass
32+
33+
_close_session: bool = False
34+
_status: GeocachingStatus = GeocachingStatus()
35+
def __init__(
36+
self,
37+
*,
38+
request_timeout: int = 8,
39+
session: Optional[ClientSession] = None,
40+
token_refresh_method: Optional[Callable[[], Awaitable[str]]] = None,
41+
token: str,
42+
) -> None:
43+
"""Initialize connection with the Geocaching API."""
44+
self._session = session
45+
self.request_timeout = request_timeout
46+
self.token = token
47+
self.token_refresh_method = token_refresh_method
48+
49+
@backoff.on_exception(backoff.expo, GeocachingApiConnectionError, max_tries=3, logger=None)
50+
@backoff.on_exception(
51+
backoff.expo, GeocachingApiRateLimitError, base=60, max_tries=6, logger=None
52+
)
53+
async def _request(self, method, uri, **kwargs) -> ClientResponse:
54+
"""Make a request."""
55+
if self.token_refresh_method is not None:
56+
self.token = await self.token_refresh_method()
57+
58+
url = URL.build(
59+
scheme=GEOCACHING_API_SCHEME,
60+
host=GEOCACHING_API_HOST,
61+
port=GEOCACHING_API_PORT,
62+
path=GEOCACHING_API_BASE_PATH,
63+
).join(URL(uri))
64+
headers = kwargs.get("headers")
65+
66+
if headers is None:
67+
headers = {}
68+
else:
69+
headers = dict(headers)
70+
71+
headers["Authorization"] = f"Bearer {self.token}"
72+
73+
if self._session is None:
74+
self._session = ClientSession()
75+
self._close_session = True
76+
77+
try:
78+
with async_timeout.timeout(self.request_timeout):
79+
response = await self._session.request(
80+
method,
81+
f"{url}",
82+
**kwargs,
83+
headers=headers,
84+
)
85+
except asyncio.TimeoutError as exception:
86+
raise GeocachingApiConnectionTimeoutError(
87+
"Timeout occurred while connecting to the Geocaching API"
88+
) from exception
89+
except (ClientError, socket.gaierror) as exception:
90+
raise GeocachingApiConnectionError(
91+
"Error occurred while communicating with the Geocaching API"
92+
) from exception
93+
94+
content_type = response.headers.get("Content-Type", "")
95+
# Error handling
96+
if (response.status // 100) in [4, 5]:
97+
contents = await response.read()
98+
response.close()
99+
100+
if response.status == 429:
101+
raise GeocachingApiRateLimitError(
102+
"Rate limit error has occurred with the Geocaching API"
103+
)
104+
105+
if content_type == "application/json":
106+
raise GeocachingApiError(response.status, json.loads(contents.decode("utf8")))
107+
raise GeocachingApiError(response.status, {"message": contents.decode("utf8")})
108+
109+
# Handle empty response
110+
if response.status == 204:
111+
return
112+
113+
if "application/json" in content_type:
114+
return await response.json()
115+
return await response.text()
116+
117+
async def update(self) -> GeocachingStatus:
118+
await self._update_user(None)
119+
return self._status
120+
121+
async def _update_user(self, data: Dict[str, Any] = None) -> None:
122+
assert self._status
123+
if data is None:
124+
data = await self._request("GET", f"/{GEOCACHING_API_VERSION}/users/me?fields=username,referenceCode")
125+
self._status.user.update_from_dict(data)
126+
127+
async def close(self) -> None:
128+
"""Close open client session."""
129+
if self._session and self._close_session:
130+
await self._session.close()
131+
132+
async def __aenter__(self) -> GeocachingApi:
133+
"""Async enter."""
134+
return self
135+
136+
async def __aexit__(self, *exc_info) -> None:
137+
"""Async exit."""
138+
await self.close()

geocachingapi/models.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
from typing import Any, Callable, Dict, Optional
3+
4+
from dataclasses import dataclass
5+
6+
@dataclass
7+
class GeocachingUser:
8+
username: Optional[str] = None
9+
reference_code: Optional[str] = None
10+
11+
def update_from_dict(self, data: Dict[str, Any]) -> None:
12+
if "username" in data:
13+
self.username = data["username"]
14+
if "reference" in data:
15+
self.reference_code = data["referenceCode"]
16+
17+
pass
18+
19+
class GeocachingStatus:
20+
user: GeocachingUser = GeocachingUser()
21+
def __init__(self) -> None:
22+
pass
23+
24+
def update_from_dict(self, data: Dict[str, Any]) -> GeocachingStatus:
25+
return self

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def read(*parts):
3030
url="https://github.com/Sholofly/geocachingapi-python",
3131
packages=setuptools.find_packages(include=["geocachingapi"]),
3232
license="MIT license",
33-
install_requires=[],
33+
install_requires=["aiohttp>=3.0.0", "backoff>=1.9.0", "yarl"],
3434
keywords=["geocaching", "api"],
3535
classifiers=[
3636
"Development Status :: 3 - Alpha",

tests/test.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
"""Test for Geocaching Api integration."""
2-
from geocachingapi import GeocachingApi
3-
def test():
4-
assert(True)
2+
from geocachingapi import GeocachingApi, GeocachingStatus
3+
import asyncio
4+
import pytest
5+
6+
@pytest.mark.asyncio
7+
async def test():
8+
status:GeocachingStatus = None
9+
token = "<insert token here>"
10+
api = GeocachingApi(token=token)
11+
status = await api.update()
12+
print(status.user.username)
13+
assert(status.user.username is not None)

0 commit comments

Comments
 (0)