def test_format_action(openapi_specs, action_factory): formatter = DefaultFormatter(openapi_specs) action = action_factory(path='products/PRD-000/endsale') formatted = formatter.format(action) assert 'Endsale action' in formatted assert 'path: /products/PRD-000/endsale' in formatted action = action_factory(path='does-not-exists') formatted = formatter.format(action) assert 'does not exist' in formatted
def test_format_resource(openapi_specs, res_factory): formatter = DefaultFormatter(openapi_specs) res = res_factory(path='products/PRD-000') formatted = formatter.format(res) assert 'Product resource' in formatted assert 'path: /products/PRD-000' in formatted res = res_factory(path='subscriptions/assets/AS-0000') formatted = formatter.format(res) assert 'Asset resource' in formatted assert 'path: /subscriptions/assets/AS-0000' in formatted res = res_factory(path='does-not-exists') formatted = formatter.format(res) assert 'does not exist' in formatted
def test_format_rs(openapi_specs, rs_factory): formatter = DefaultFormatter(openapi_specs) rs = rs_factory(path='products') formatted = formatter.format(rs) assert 'Search the Products collection' in formatted assert 'path: /products' in formatted assert 'Available filters' in formatted rs = rs_factory(path='products/PRD-000/items') formatted = formatter.format(rs) assert 'Search the Items collection' in formatted assert 'path: /products/PRD-000/items' in formatted assert 'Available filters' in formatted rs = rs_factory(path='does-not-exists') formatted = formatter.format(rs) assert 'does not exist' in formatted
def __init__( self, api_key, endpoint=None, use_specs=True, specs_location=None, validate_using_specs=True, default_headers=None, default_limit=100, max_retries=0, ): """ Create a new instance of the ConnectClient. :param api_key: The API key used for authentication. :type api_key: str :param endpoint: The API endpoint, defaults to CONNECT_ENDPOINT_URL :type endpoint: str, optional :param specs_location: The Connect OpenAPI specification local path or URL, defaults to CONNECT_SPECS_URL :type specs_location: str, optional :param default_headers: Http headers to apply to each request, defaults to {} :type default_headers: dict, optional """ if default_headers and 'Authorization' in default_headers: raise ValueError( '`default_headers` cannot contains `Authorization`') self.endpoint = endpoint or CONNECT_ENDPOINT_URL self.api_key = api_key self.default_headers = default_headers or {} self.default_limit = default_limit self.max_retries = max_retries self._use_specs = use_specs self._validate_using_specs = validate_using_specs self.specs_location = specs_location or CONNECT_SPECS_URL self.specs = None if self._use_specs: self.specs = OpenAPISpecs(self.specs_location) self.response = None self._help_formatter = DefaultFormatter(self.specs)
class ConnectClient(threading.local): """ Connect ReST API client. """ def __init__( self, api_key, endpoint=None, use_specs=True, specs_location=None, validate_using_specs=True, default_headers=None, default_limit=100, max_retries=0, ): """ Create a new instance of the ConnectClient. :param api_key: The API key used for authentication. :type api_key: str :param endpoint: The API endpoint, defaults to CONNECT_ENDPOINT_URL :type endpoint: str, optional :param specs_location: The Connect OpenAPI specification local path or URL, defaults to CONNECT_SPECS_URL :type specs_location: str, optional :param default_headers: Http headers to apply to each request, defaults to {} :type default_headers: dict, optional """ if default_headers and 'Authorization' in default_headers: raise ValueError( '`default_headers` cannot contains `Authorization`') self.endpoint = endpoint or CONNECT_ENDPOINT_URL self.api_key = api_key self.default_headers = default_headers or {} self.default_limit = default_limit self.max_retries = max_retries self._use_specs = use_specs self._validate_using_specs = validate_using_specs self.specs_location = specs_location or CONNECT_SPECS_URL self.specs = None if self._use_specs: self.specs = OpenAPISpecs(self.specs_location) self.response = None self._help_formatter = DefaultFormatter(self.specs) def __getattr__(self, name): """ Returns a collection object called ``name``. :param name: The name of the collection to retrieve. :type name: str :return: a collection called ``name``. :rtype: Collection """ if '_' in name: name = name.replace('_', '-') return self.collection(name) def __call__(self, name): return self.ns(name) def ns(self, name): """ Returns the namespace called ``name``. :param name: The name of the namespace to access. :type name: str :return: The namespace called ``name``. :rtype: NS """ if not isinstance(name, str): raise TypeError('`name` must be a string.') if not name: raise ValueError('`name` must not be blank.') return NS(self, name) def collection(self, name): """ Returns the collection called ``name``. :param name: The name of the collection to access. :type name: str :return: The collection called ``name``. :rtype: Collection """ if not isinstance(name, str): raise TypeError('`name` must be a string.') if not name: raise ValueError('`name` must not be blank.') return Collection( self, name, ) def get(self, url, **kwargs): return self.execute('get', url, **kwargs) def create(self, url, payload=None, **kwargs): kwargs = kwargs or {} if payload: kwargs['json'] = payload return self.execute('post', url, **kwargs) def update(self, url, payload=None, **kwargs): kwargs = kwargs or {} if payload: kwargs['json'] = payload return self.execute('put', url, **kwargs) def delete(self, url, **kwargs): return self.execute('delete', url, **kwargs) def execute(self, method, path, **kwargs): if (self._use_specs and self._validate_using_specs and not self.specs.exists(method, path)): # TODO more info, specs version, method etc raise ClientError(f'The path `{path}` does not exist.') url = f'{self.endpoint}/{path}' kwargs = self._prepare_call_kwargs(kwargs) self.response = None try: self._execute_http_call(method, url, kwargs) if self.response.status_code == 204: return None if self.response.headers['Content-Type'] == 'application/json': return self.response.json() else: return self.response.content except RequestException as re: api_error = self._get_api_error_details() or {} status_code = self.response.status_code if self.response is not None else None raise ClientError(status_code=status_code, **api_error) from re def print_help(self, obj): print() print(self._help_formatter.format(obj)) def help(self): self.print_help(None) return self def _prepare_call_kwargs(self, kwargs): kwargs = kwargs or {} if 'headers' in kwargs: kwargs['headers'].update(get_headers(self.api_key)) else: kwargs['headers'] = get_headers(self.api_key) if self.default_headers: kwargs['headers'].update(self.default_headers) return kwargs def _execute_http_call(self, method, url, kwargs): retry_count = 0 while True: self.response = requests.request(method, url, **kwargs) if ( # pragma: no branch self.response.status_code == 502 and retry_count < self.max_retries): retry_count += 1 time.sleep(1) continue break # pragma: no cover if self.response.status_code >= 400: self.response.raise_for_status() def _get_api_error_details(self): if self.response is not None: try: error = self.response.json() if 'error_code' in error and 'errors' in error: return error except JSONDecodeError: pass
def test_no_specs(): formatter = DefaultFormatter(None) formatted = formatter.format(None) assert 'No OpenAPI specs available.' in formatted
def test_format_collection_not_exists(openapi_specs, col_factory): formatter = DefaultFormatter(openapi_specs) col = col_factory(path='does-not-exists') formatted = formatter.format(col) assert 'does not exist' in formatted
def test_format_collection(openapi_specs, col_factory, title, path): formatter = DefaultFormatter(openapi_specs) col = col_factory(path=path) formatted = formatter.format(col) assert f'{title} collection' in formatted assert f'path: /{path}' in formatted
def test_format_ns_not_exist(openapi_specs, ns_factory): formatter = DefaultFormatter(openapi_specs) ns = ns_factory(path='does-not-exists') formatted = formatter.format(ns) assert 'does not exist' in formatted
def test_format_ns(openapi_specs, ns_factory, title, path): formatter = DefaultFormatter(openapi_specs) ns = ns_factory(path=path) formatted = formatter.format(ns) assert f'{title} namespace' in formatted assert f'path: /{path}' in formatted
def test_format_client(openapi_specs): formatter = DefaultFormatter(openapi_specs) formatted = formatter.format(None) assert openapi_specs.title in formatted assert openapi_specs.version in formatted
class _ConnectClientBase(threading.local): """ Connect ReST API client. """ def __init__( self, api_key, endpoint=None, use_specs=True, specs_location=None, validate_using_specs=True, default_headers=None, default_limit=100, max_retries=0, ): """ Create a new instance of the ConnectClient. :param api_key: The API key used for authentication. :type api_key: str :param endpoint: The API endpoint, defaults to CONNECT_ENDPOINT_URL :type endpoint: str, optional :param specs_location: The Connect OpenAPI specification local path or URL, defaults to CONNECT_SPECS_URL :type specs_location: str, optional :param default_headers: Http headers to apply to each request, defaults to {} :type default_headers: dict, optional """ if default_headers and 'Authorization' in default_headers: raise ValueError('`default_headers` cannot contains `Authorization`') self.endpoint = endpoint or CONNECT_ENDPOINT_URL self.api_key = api_key self.default_headers = default_headers or {} self.default_limit = default_limit self.max_retries = max_retries self._use_specs = use_specs self._validate_using_specs = validate_using_specs self.specs_location = specs_location or CONNECT_SPECS_URL self.specs = None if self._use_specs: self.specs = OpenAPISpecs(self.specs_location) self.response = None self._help_formatter = DefaultFormatter(self.specs) def __getattr__(self, name): """ Returns a collection object called ``name``. :param name: The name of the collection to retrieve. :type name: str :return: a collection called ``name``. :rtype: Collection """ if '_' in name: name = name.replace('_', '-') return self.collection(name) def __call__(self, name): return self.ns(name) def ns(self, name): """ Returns the namespace called ``name``. :param name: The name of the namespace to access. :type name: str :return: The namespace called ``name``. :rtype: NS """ if not isinstance(name, str): raise TypeError('`name` must be a string.') if not name: raise ValueError('`name` must not be blank.') return self._get_namespace_class()(self, name) def collection(self, name): """ Returns the collection called ``name``. :param name: The name of the collection to access. :type name: str :return: The collection called ``name``. :rtype: Collection """ if not isinstance(name, str): raise TypeError('`name` must be a string.') if not name: raise ValueError('`name` must not be blank.') return self._get_collection_class()( self, name, ) def print_help(self, obj): print() print(self._help_formatter.format(obj)) def help(self): self.print_help(None) return self def _get_collection_class(self): raise NotImplementedError() def _get_namespace_class(self): raise NotImplementedError() def _prepare_call_kwargs(self, kwargs): kwargs = kwargs or {} if 'headers' in kwargs: kwargs['headers'].update(get_headers(self.api_key)) else: kwargs['headers'] = get_headers(self.api_key) if self.default_headers: kwargs['headers'].update(self.default_headers) return kwargs def _get_api_error_details(self): if self.response is not None: try: error = self.response.json() if 'error_code' in error and 'errors' in error: return error except JSONDecodeError: pass