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+
231class 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 ()
0 commit comments