async def get_token(self) -> str | None: """Get or refresh the authentication token.""" authentication_data: dict[str, str | None] = dict( email_address=self.email, password=self.password, device_id=self._device_id ) token: str | None = None session = self._session if self._session else aiohttp.ClientSession() try: raw_response: aiohttp.ClientResponse = await session.post( url=AUTH_RESOURCE, data=authentication_data, headers=self._generate_headers() ) if raw_response.status == HTTPStatus.OK: response: dict[str, Any] = await raw_response.json() if "data" in response and "token" in response["data"]: token = self._auth_token = response["data"]["token"] elif raw_response.status == HTTPStatus.NOT_MODIFIED: # Etag header matched, no new data available pass elif raw_response.status == HTTPStatus.UNAUTHORIZED: self._auth_token = None raise SurePetcareAuthenticationError() else: logger.debug("Response from %s: %s", AUTH_RESOURCE, raw_response) raise SurePetcareAPIError( resource=AUTH_RESOURCE, response=raw_response, message="unknown response from sure petcare api", ) return token except asyncio.TimeoutError as error: logger.debug("Timeout while calling %s: %s", AUTH_RESOURCE, error) raise SurePetcareConnectionError() except (aiohttp.ClientError, AttributeError) as error: logger.debug("Failed to fetch %s: %s", AUTH_RESOURCE, error) raise SurePetcareError() finally: if not self._session: await session.close()
def get_token(self) -> Optional[str]: """Get or refresh the authentication token.""" authentication_data: Dict[str, Optional[str]] = dict( email_address=self.email, password=self.password, device_id=self._device_id ) token: Optional[str] = None try: raw_response: requests.Response = requests.post( url=AUTH_RESOURCE, data=authentication_data, headers=self._generate_headers() ) if raw_response.status_code == HTTPStatus.OK: response: Dict[str, Any] = raw_response.json() if "data" in response and "token" in response["data"]: token = self._auth_token = response["data"]["token"] elif raw_response.status_code == HTTPStatus.NOT_MODIFIED: # Etag header matched, no new data available pass elif raw_response.status_code == HTTPStatus.UNAUTHORIZED: self._auth_token = None raise SurePetcareAuthenticationError() else: logger.debug("Response from %s: %s", AUTH_RESOURCE, raw_response) raise SurePetcareError() return token except asyncio.TimeoutError as error: logger.debug("Timeout while calling %s: %s", AUTH_RESOURCE, error) raise SurePetcareConnectionError() except (aiohttp.ClientError, AttributeError) as error: logger.debug("Failed to fetch %s: %s", AUTH_RESOURCE, error) raise SurePetcareError()
async def call( self, method: str, resource: str, data: dict[str, Any] | None = None, second_try: bool = False, **_: Any, ) -> dict[str, Any] | None: """Retrieve the flap data/state.""" logger.debug("self._auth_token: %s", self._auth_token) if not self._auth_token: self._auth_token = await self.get_token() if method not in ["GET", "PUT", "POST"]: raise HTTPException("unknown http method: %d", str(method)) response_data = None session = self._session if self._session else aiohttp.ClientSession() try: with async_timeout.timeout(self._api_timeout): headers = self._generate_headers() # use etag if available if resource in self._etags: headers[ETAG] = str(self._etags.get(resource)) logger.debug("using available etag '%s' in headers: %s", ETAG, headers) await session.options(resource, headers=headers) response: aiohttp.ClientResponse = await session.request( method, resource, headers=headers, data=data) if response.status == HTTPStatus.OK or response.status == HTTPStatus.CREATED: self.resources[ resource] = response_data = await response.json() if ETAG in response.headers: self._etags[resource] = response.headers[ETAG].strip( '"') elif response.status == HTTPStatus.NOT_MODIFIED: # Etag header matched, no new data available pass elif response.status == HTTPStatus.UNAUTHORIZED: logger.debug("AuthenticationError! Try: %s: %s", second_try, response) self._auth_token = None if not second_try: token_refreshed = self.get_token() if token_refreshed: await self.call(method="GET", resource=resource, second_try=True) raise SurePetcareAuthenticationError() else: logger.info(f"Response from {resource}:\n{response}") return response_data except (asyncio.TimeoutError, aiohttp.ClientError): logger.error("Can not load data from %s", resource) raise SurePetcareConnectionError() finally: if not self._session: await session.close()
self._device_id: str = str(uuid1()) # connection settings self._api_timeout: int = api_timeout self._surepy_version: str = surepy_version # api token management self._auth_token: str | None = None if auth_token and token_seems_valid(auth_token): self._auth_token = auth_token elif token := find_token(): self._auth_token = token else: # no valid credentials/token SurePetcareAuthenticationError( "sorry 🐾 no valid credentials/token found ¯\\_(ツ)_/¯") # storage for received api data self.resources: dict[str, Any] = {} # storage for etags self._etags: dict[str, str] = {} logger.debug("initialization completed | vars(): %s", vars()) def _generate_headers(self) -> dict[str, str]: """Build a HTTP header accepted by the API""" user_agent = (SUREPY_USER_AGENT.format( version=self._surepy_version) if self._surepy_version else None) return { HOST: "app.api.surehub.io",