from contextlib import suppress from typing import Dict, Tuple, Union from flask import json from flask.testing import FlaskClient from werkzeug.wrappers import Response from ereuse_devicehub.ereuse_utils.session import ANY, AUTH, BASIC, DevicehubClient, JSON, Query, Status ANY = ANY AUTH = AUTH BASIC = BASIC Res = Tuple[Union[Dict[str, object], str], Response] class Client(FlaskClient): """ A client for the REST servers of DeviceHub and WorkbenchServer. - JSON first. By default it sends and expects receiving JSON files. - Assert regular status responses, like 200 for GET. - Auto-parses a nested dictionary of URL query params to the URL version with nested properties to JSON. - Meaningful headers format: a dictionary of name-values. """ def open(self, uri: str, status: Status = 200, query: Query = tuple(), accept=JSON, content_type=JSON, item=None, headers: dict = None, **kw) -> Res: """ :param uri: The URI without basename and query. :param status: Assert the response for specified status. Set None to avoid. :param query: The query of the URL in the form of [(key1, value1), (key2, value2), (key1, value3)]. If value is a list or a dict, they will be converted to JSON. Please, see :class:`boltons.urlutils`. QueryParamDict` for more info. :param accept: The Accept header. If 'application/json' (default) then it will parse incoming JSON. :param item: The last part of the path. Useful to do something like ``get('db/accounts', item='24')``. If you use ``item``, you can't set a final backslash into ``uri`` (or the parse will fail). :param headers: A dictionary of headers, where keys are header names and values their values. Ex: {'Accept', 'application/json'}. :param kw: Kwargs passed into parent ``open``. :return: A tuple with: 1. response data, as a string or JSON depending of Accept, and 2. the Response object. """ j_encoder = self.application.json_encoder headers = headers or {} headers['Accept'] = accept headers['Content-Type'] = content_type headers = [(k, v) for k, v in headers.items()] if 'data' in kw and content_type == JSON: kw['data'] = json.dumps(kw['data'], cls=j_encoder) if item: uri = DevicehubClient.parse_uri(uri, item) if query: uri = DevicehubClient.parse_query(uri, query) response = super().open(uri, headers=headers, **kw) if status: _status = getattr(status, 'code', status) assert response.status_code == _status, \ 'Expected status code {} but got {}. Returned data is:\n' \ '{}'.format(_status, response.status_code, response.get_data().decode()) data = response.get_data() with suppress(UnicodeDecodeError): data = data.decode() if accept == JSON: data = json.loads(data) if data else {} return data, response def get(self, uri: str, query: Query = tuple(), item: str = None, status: Status = 200, accept: str = JSON, headers: dict = None, **kw) -> Res: """ Performs a GET. See the parameters in :meth:`ereuse_utils.test.Client.open`. Moreover: :param query: A dictionary of query params. If a parameter is a dict or a list, it will be parsed to JSON, then all params are encoded with ``urlencode``. :param kw: Kwargs passed into parent ``open``. """ return super().get(uri, item=item, status=status, accept=accept, headers=headers, query=query, **kw) def post(self, uri: str, data: str or dict, query: Query = tuple(), status: Status = 201, content_type: str = JSON, accept: str = JSON, headers: dict = None, **kw) -> Res: """ Performs a POST. See the parameters in :meth:`ereuse_utils.test.Client.open`. """ return super().post(uri, data=data, status=status, content_type=content_type, accept=accept, headers=headers, query=query, **kw) def patch(self, uri: str, data: str or dict, query: Query = tuple(), status: Status = 200, content_type: str = JSON, item: str = None, accept: str = JSON, headers: dict = None, **kw) -> Res: """ Performs a PATCH. See the parameters in :meth:`ereuse_utils.test.Client.open`. """ return super().patch(uri, item=item, data=data, status=status, content_type=content_type, accept=accept, headers=headers, query=query, **kw) def put(self, uri: str, data: str or dict, query: Query = tuple(), status: Status = 201, content_type: str = JSON, item: str = None, accept: str = JSON, headers: dict = None, **kw) -> Res: return super().put(uri, item=item, data=data, status=status, content_type=content_type, accept=accept, headers=headers, query=query, **kw) def delete(self, uri: str, query: Query = tuple(), item: str = None, status: Status = 204, accept: str = JSON, headers: dict = None, **kw) -> Res: return super().delete(uri, query=query, item=item, status=status, accept=accept, headers=headers, **kw)