"""Asynchronous Python client for LaMetric TIME devices.""" from __future__ import annotations import asyncio import socket from dataclasses import dataclass from typing import Any, Self import aiohttp import backoff from aiohttp import hdrs from yarl import URL from .exceptions import ( LaMetricConnectionError, LaMetricConnectionTimeoutError, LaMetricError, ) from .models import CloudDevice, User @dataclass class LaMetricCloud: """Main class for handling connections with the LaMetric cloud.""" token: str request_timeout: float = 8.0 session: aiohttp.client.ClientSession | None = None _close_session: bool = False @backoff.on_exception( backoff.expo, LaMetricConnectionError, max_tries=3, logger=None, ) async def _request( self, uri: str = "", ) -> Any: """Handle a request to the LaMetric cloud. A generic method for sending/handling HTTP requests done gainst the LaMetric cloud. Args: ---- uri: Request URI, for example `/api/v2/users/me`. Returns: ------- A Python dictionary (JSON decoded) with the response from the LaMetric device. Raises: ------ LaMetricConnectionError: An error occurred while communication with the LaMetric device. LaMetricConnectionTimeoutError: A timeout occurred while communicating with the LaMetric device. LaMetricError: Received an unexpected response from the LaMetric device. """ url = URL.build(scheme="https", host="developer.lametric.com", path=uri) if self.session is None: self.session = aiohttp.ClientSession() self._close_session = True headers = { "Authorization": f"Bearer {self.token}", "Accept": "application/json", } try: async with asyncio.timeout(self.request_timeout): response = await self.session.request( hdrs.METH_GET, url, headers=headers, raise_for_status=True, ) content_type = response.headers.get("Content-Type", "") if "application/json" not in content_type: raise LaMetricError( response.status, {"message": await response.text()}, ) return await response.json() except asyncio.TimeoutError as exception: msg = "Timeout occurred while connecting to the LaMetric cloud" raise LaMetricConnectionTimeoutError(msg) from exception except (aiohttp.ClientError, socket.gaierror) as exception: msg = "Error occurred while communicating with the LaMetric cloud" raise LaMetricConnectionError(msg) from exception async def current_user(self) -> User: """Get LaMetric user information. Returns ------- A User object, with information about the current user. """ response = await self._request("/api/v2/me") return User.from_dict(response) async def devices(self) -> list[CloudDevice]: """Get LaMetric devices from the cloud. Returns ------- A list of CloudDevices. """ response = await self._request("/api/v2/users/me/devices") return [CloudDevice.from_dict(cloud_device) for cloud_device in response] async def device(self, device_id: int) -> CloudDevice: """Get a LaMetric device from the cloud. Args: ---- device_id: The ID of the device to get information for. Returns: ------- A CloudDevice object, with information about the request device. """ response = await self._request(f"/api/v2/users/me/devices/{device_id}") return CloudDevice.from_dict(response) async def close(self) -> None: """Close open client session.""" if self.session and self._close_session: await self.session.close() async def __aenter__(self) -> Self: """Async enter. Returns ------- The LaMetricCloud object. """ return self async def __aexit__(self, *_exc_info: object) -> None: """Async exit. Args: ---- _exc_info: Exec type. """ await self.close()