class WebsocketClient: """ Websocket client class. """ def __init__(self, token: Optional[str] = None, username: Optional[str] = None, password: Optional[str] = None, url: Optional[str] = None, verify: bool = True): self._token = token self._username = username self._password = password self._url = url self._verify = verify self._websocket = None self._cm = ConfigManager() async def start(self): """ Initializes the websocket and starts the event loop. """ if self._url.startswith('wss:'): ssl_context = ssl.SSLContext() if self._verify: ssl_context.load_verify_locations(self._cm.getCaBundle()) else: ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE else: ssl_context = None async with websockets.connect(self._url, ssl=ssl_context) as ws: await self.send_recieve(ws) async def send_recieve(self, ws: websockets.WebSocketClientProtocol): """ The main loop that sends/receives data. :param ws: the web socket client """ while True: msg = await ws.recv() data = json.loads(msg) pretty_print(data) if data['type'] == 'message': if data['name'] == 'authentication-required': await self.send_auth(ws) if data['name'] == 'authentication-succeeded': await self.send_subscribe(ws) async def send_auth(self, ws: websockets.WebSocketClientProtocol): """ Sends an authentication request. :param ws: the web socket client """ if self._token: data = { 'action': 'authenticate', 'method': 'jwt', 'data': { 'token': self._token } } else: data = { 'action': 'authenticate', 'method': 'password', 'data': { 'username': self._username, 'password': self._password } } await ws.send(json.dumps(data)) async def send_subscribe(self, ws: websockets.WebSocketClientProtocol): """ Sends a subscription request. :param ws: the web socket client """ data = {'action': 'subscribe'} await ws.send(json.dumps(data))
class RestApiClient: """ A generic REST API Client class. """ def __init__(self, username: Optional[str] = None, password: Optional[str] = None, baseurl: Optional[str] = None, verify: bool = True): if baseurl.endswith('/'): baseurl = baseurl[:-1] self.baseurl = baseurl self.username = username self.password = password self.verify = verify self._requests_kwargs = None self._logger = logging.getLogger(WEBSERVICE_CLIENT_NAMESPACE) if not verify: self._logger.warning('SSL verification turned off') self._cm = ConfigManager() def get_requests_kwargs(self) -> dict: # # Cache the base kwargs # if self._requests_kwargs is None: # # Authentication # self._requests_kwargs = {'auth': (self.username, self.password)} # # SSL cert verification # if self.verify: self._requests_kwargs['verify'] = self._cm.getCaBundle() self._logger.debug('Using CA path: {}'.format( self._cm.getCaBundle())) else: self._requests_kwargs['verify'] = False return self._requests_kwargs def build_url(self, path: str) -> str: """ Given a path, returns a fully qualified URL. :param str path: the path for which to build the URL :return str: the full URL """ if not path.startswith('/'): path = '/{}'.format(path) return '{}{}'.format(self.baseurl, path) def process_response( self, response: requests.Response) -> Optional[Union[list, dict]]: """ Process the response, parsing out the data and handling errors/exceptions as required. :param requests.Response response: the response from the request :return Optional[Union[list, dict]]: the response :raises RequestError: if the a non 2xx status code is returned """ # # Check for 2xx status code. If it isn't a 2xx status code, then # treat it as an error, and handle appropriately # if round(response.status_code / 100) != 2: self.process_error_response(response) # # Attempt to get JSON from the response, otherwise None # data = None try: data = response.json() except Exception: pass self._logger.debug('Response Payload: {}'.format(json.dumps(data))) return data def process_error_response(self, error_response: requests.Response): """ Process the response as an error. :param requests.Response error_response: the response from the request :raises RequestError: if the a non 2xx status code is returned """ self._logger.debug('ERROR Code: {}'.format(error_response.status_code)) # # Attempt to get JSON data from the error response # data = None try: data = error_response.json() except Exception: pass self._logger.debug('ERROR Payload: {}'.format(json.dumps(data))) raise RequestError("ERROR: API Request Error {}".format( error_response.status_code), status_code=error_response.status_code, data=data) def get(self, path: str) -> Union[list, dict]: """ Performs a GET request on the specified path. It is assumed the result is JSON, and it is decoded as such. :param str path: the API path to get from :return Union[list, dict]: the response, JSON decoded """ url = self.build_url(path) self._logger.debug('GET: {}'.format(url)) result = requests.get(url, **self.get_requests_kwargs()) return self.process_response(result) def post(self, path: str, data: Optional[dict] = None) -> Optional[dict]: """ Post data to a specified path (API endpoint). Data will automatically be encoded as JSON. :param str path: the API path to post to :param dict data: the data to post :return dict: the response of the request """ url = self.build_url(path) self._logger.debug('POST: {}'.format(url)) result = requests.post(url, json=data, **self.get_requests_kwargs()) return self.process_response(result) def put(self, path: str, data: Optional[dict] = None) -> Optional[dict]: """ Put data to a specified path (API endpoint). Data will automatically be encoded as JSON. :param str path: the API path to put to :param dict data: the data to post :return dict: the response of the request """ url = self.build_url(path) self._logger.debug('PUT: {}'.format(url)) result = requests.put(url, json=data, **self.get_requests_kwargs()) return self.process_response(result) def delete(self, path: str) -> Optional[dict]: """ Delete from the specified path. :param str path: the path to delete :return dict : the result of the delete """ url = self.build_url(path) self._logger.debug('DELETE: {}'.format(url)) result = requests.delete(url, **self.get_requests_kwargs()) return self.process_response(result) def patch(self, path: str, data: Optional[dict] = None) -> Optional[dict]: """ Patch data to a specified path (API endpoint). Data will automatically be encoded as JSON. :param str path: the API path to put to :param dict data: the data to post :return dict: the response of the request """ url = self.build_url(path) self._logger.debug('PATCH: {}'.format(url)) result = requests.patch(url, json=data, **self.get_requests_kwargs()) return self.process_response(result)