# -*- coding: utf-8 -*- """ mygeotab.py3.api_async ~~~~~~~~~~~~~~~~~~~~~~ Async/Await-able (Python 3.5+) public objects and methods wrapping the MyGeotab API. """ import sys if sys.version_info < (3, 5): raise Exception("Python 3.5+ is required to use the async API") import ssl from concurrent.futures import TimeoutError import aiohttp from mygeotab import api from mygeotab.api import DEFAULT_TIMEOUT, get_headers from mygeotab.exceptions import MyGeotabException, TimeoutException, AuthenticationException from mygeotab.serializers import json_serialize, json_deserialize class API(api.API): """A simple, asynchronous, and Pythonic wrapper for the MyGeotab API. """ def __init__( self, username, password=None, database=None, session_id=None, server="my.geotab.com", timeout=DEFAULT_TIMEOUT ): """ Initialize the asynchronous MyGeotab API object with credentials. :param username: The username used for MyGeotab servers. Usually an email address. :param password: The password associated with the username. Optional if `session_id` is provided. :param database: The database or company name. Optional as this usually gets resolved upon authentication. :param session_id: A session ID, assigned by the server. :param server: The server ie. my23.geotab.com. Optional as this usually gets resolved upon authentication. :param timeout: The timeout to make the call, in seconds. By default, this is 300 seconds (or 5 minutes). :raise Exception: Raises an Exception if a username, or one of the session_id or password is not provided. """ super().__init__(username, password, database, session_id, server, timeout) async def call_async(self, method, **parameters): """Makes an async call to the API. :param method: The method name. :param params: Additional parameters to send (for example, search=dict(id='b123') ) :return: The JSON result (decoded into a dict) from the server.abs :raise MyGeotabException: Raises when an exception occurs on the MyGeotab server. :raise TimeoutException: Raises when the request does not respond after some time. """ if method is None: raise Exception("A method name must be specified") params = api.process_parameters(parameters) if self.credentials and not self.credentials.session_id: self.authenticate() if "credentials" not in params and self.credentials.session_id: params["credentials"] = self.credentials.get_param() try: result = await _query(self._server, method, params, verify_ssl=self._is_verify_ssl) if result is not None: self.__reauthorize_count = 0 return result except MyGeotabException as exception: if exception.name == "InvalidUserException": if self.__reauthorize_count == 0 and self.credentials.password: self.__reauthorize_count += 1 self.authenticate() return await self.call_async(method, **parameters) else: raise AuthenticationException( self.credentials.username, self.credentials.database, self.credentials.server ) raise async def multi_call_async(self, calls): """Performs an async multi-call to the API :param calls: A list of call 2-tuples with method name and params (for example, ('Get', dict(typeName='Trip')) ) :return: The JSON result (decoded into a dict) from the server :raise MyGeotabException: Raises when an exception occurs on the MyGeotab server :raise TimeoutException: Raises when the request does not respond after some time. """ formatted_calls = [dict(method=call[0], params=call[1] if len(call) > 1 else {}) for call in calls] return await self.call_async("ExecuteMultiCall", calls=formatted_calls) async def get_async(self, type_name, **parameters): """Gets entities asynchronously using the API. Shortcut for using async_call() with the 'Get' method. :param type_name: The type of entity. :param parameters: Additional parameters to send. :return: The JSON result (decoded into a dict) from the server. :raise MyGeotabException: Raises when an exception occurs on the MyGeotab server. :raise TimeoutException: Raises when the request does not respond after some time. """ if parameters: results_limit = parameters.get("resultsLimit", None) if results_limit is not None: del parameters["resultsLimit"] if "search" in parameters: parameters.update(parameters["search"]) parameters = dict(search=parameters, resultsLimit=results_limit) return await self.call_async("Get", type_name=type_name, **parameters) async def add_async(self, type_name, entity): """ Adds an entity asynchronously using the API. Shortcut for using async_call() with the 'Add' method. :param type_name: The type of entity. :param entity: The entity to add. :return: The id of the object added. :raise MyGeotabException: Raises when an exception occurs on the MyGeotab server. :raise TimeoutException: Raises when the request does not respond after some time. """ return await self.call_async("Add", type_name=type_name, entity=entity) async def set_async(self, type_name, entity): """Sets an entity asynchronously using the API. Shortcut for using async_call() with the 'Set' method. :param type_name: The type of entity :param entity: The entity to set :raise MyGeotabException: Raises when an exception occurs on the MyGeotab server """ return await self.call_async("Set", type_name=type_name, entity=entity) async def remove_async(self, type_name, entity): """Removes an entity asynchronously using the API. Shortcut for using async_call() with the 'Remove' method. :param type_name: The type of entity. :param entity: The entity to remove. :raise MyGeotabException: Raises when an exception occurs on the MyGeotab server. :raise TimeoutException: Raises when the request does not respond after some time. """ return await self.call_async("Remove", type_name=type_name, entity=entity) @staticmethod def from_credentials(credentials): """Returns a new async API object from an existing Credentials object. :param credentials: The existing saved credentials. :return: A new API object populated with MyGeotab credentials. """ return API( username=credentials.username, password=credentials.password, database=credentials.database, session_id=credentials.session_id, server=credentials.server, ) async def server_call_async(method, server, timeout=DEFAULT_TIMEOUT, verify_ssl=True, **parameters): """Makes an asynchronous call to an un-authenticated method on a server. :param method: The method name. :param server: The MyGeotab server. :param timeout: The timeout to make the call, in seconds. By default, this is 300 seconds (or 5 minutes). :param verify_ssl: If True, verify the SSL certificate. It's recommended not to modify this. :param parameters: Additional parameters to send (for example, search=dict(id='b123') ). :return: The JSON result (decoded into a dict) from the server. :raise MyGeotabException: Raises when an exception occurs on the MyGeotab server. :raise TimeoutException: Raises when the request does not respond after some time. """ if method is None: raise Exception("A method name must be specified") if server is None: raise Exception("A server (eg. my3.geotab.com) must be specified") parameters = api.process_parameters(parameters) return await _query(server, method, parameters, timeout=timeout, verify_ssl=verify_ssl) async def _query(server, method, parameters, timeout=DEFAULT_TIMEOUT, verify_ssl=True): """Formats and performs the asynchronous query against the API :param server: The server to query. :param method: The method name. :param parameters: A dict of parameters to send :param timeout: The timeout to make the call, in seconds. By default, this is 300 seconds (or 5 minutes). :param verify_ssl: Whether or not to verify SSL connections :return: The JSON-decoded result from the server :raise MyGeotabException: Raises when an exception occurs on the MyGeotab server :raise TimeoutException: Raises when the request does not respond after some time. :raise aiohttp.ClientResponseError: Raises when there is an HTTP status code that indicates failure. """ api_endpoint = api.get_api_url(server) params = dict(id=-1, method=method, params=parameters) headers = get_headers() conn = aiohttp.TCPConnector(ssl=ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) if verify_ssl else False) try: async with aiohttp.ClientSession(connector=conn) as session: response = await session.post( api_endpoint, data=json_serialize(params), headers=headers, timeout=timeout, allow_redirects=True ) response.raise_for_status() content_type = response.headers.get("Content-Type") body = await response.text() except TimeoutError: raise TimeoutException(server) if content_type and "application/json" not in content_type.lower(): return body return api._process(json_deserialize(body))