Skip to content

Commit

Permalink
Merge pull request #12 from cloudblue/support_nested_namespaces
Browse files Browse the repository at this point in the history
Add support for nested namespaces
  • Loading branch information
marcserrat authored Dec 24, 2020
2 parents 987baa0 + da914d4 commit feab003
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 8 deletions.
17 changes: 12 additions & 5 deletions cnct/client/help_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,25 @@ def format_client(self):
return render('\n'.join(lines))

def format_ns(self, ns):
namespaces = self._specs.get_nested_namespaces(ns.path)
collections = self._specs.get_namespaced_collections(ns.path)
if not collections:

if not (collections or namespaces):
return render(f'~~{ns.path}~~ **does not exists.**')

lines = [
f'# {ns.path.title()} namespace',
f'**path: /{ns.path}**',
'## Available collections',
]

for collection in collections:
lines.append(f'* {collection}')
if namespaces:
lines.append('## Available namespaces')
for namespace in namespaces:
lines.append(f'* {namespace}')

if collections:
lines.append('## Available collections')
for collection in collections:
lines.append(f'* {collection}')

return render('\n'.join(lines))

Expand Down
25 changes: 25 additions & 0 deletions cnct/client/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ def __getattr__(self, name):
def __iter__(self):
raise TypeError('A Namespace object is not iterable.')

def __call__(self, name):
return self.ns(name)

def collection(self, name):
"""
Returns the collection called ``name``.
Expand All @@ -63,6 +66,28 @@ def collection(self, name):
f'{self._path}/{name}',
)

def ns(self, name):
"""
Returns the namespace called ``name``.
:param name: The name of the namespace.
:type name: str
:raises TypeError: if the ``name`` is not a string.
:raises ValueError: if the ``name`` is blank.
: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._client,
f'{self._path}/{name}',
)

def help(self):
"""
Output the namespace documentation to the console.
Expand Down
40 changes: 38 additions & 2 deletions cnct/client/openapi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from io import StringIO
from functools import partial

import requests
import yaml
Expand Down Expand Up @@ -60,8 +61,9 @@ def get_namespaced_collections(self, path):
nested = filter(lambda x: x[1:].startswith(path), self._specs['paths'].keys())
collections = set()
for p in nested:
splitted = p[1:].split('/', 2)
collections.add(splitted[1])
splitted = p[len(path) + 1:].split('/', 2)
if self._is_collection(p) and len(splitted) == 2:
collections.add(splitted[1])
return list(sorted(collections))

def get_collection(self, path):
Expand Down Expand Up @@ -94,6 +96,28 @@ def get_actions(self, path):
for name in sorted(actions)
]

def get_nested_namespaces(self, path):
def _is_nested_namespace(base_path, path):
if path[1:].startswith(base_path):
comp = path[1:].split('/')
return (
len(comp) > 1
and not comp[-1].startswith('{')
)
return False

nested = filter(
partial(_is_nested_namespace, path),
self._specs['paths'].keys(),
)
current_level = len(path[1:].split('/'))
nested_namespaces = []
for ns in nested:
name = ns[1:].split('/')[current_level]
if not self._is_collection(f'/{path}/{name}'):
nested_namespaces.append(name)
return nested_namespaces

def get_nested_collections(self, path):
p = self._get_path(path)
nested = filter(
Expand Down Expand Up @@ -162,3 +186,15 @@ def _get_info(self, path):
def _is_action(self, operation_id):
op_id_cmps = operation_id.rsplit('_', 2)
return op_id_cmps[-2] not in ('list', 'retrieve')

def _is_collection(self, path):
path_length = len(path[1:].split('/'))
for p in self._specs['paths'].keys():
comp = p[1:].split('/')
if not p.startswith(path):
continue
if p == path:
return True
if len(comp) > path_length and comp[path_length].startswith('{'):
return True
return False
41 changes: 40 additions & 1 deletion tests/client/test_models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,50 @@
import pytest

from cnct.client.exceptions import ClientError
from cnct.client.models import Action, Collection, Resource, ResourceSet
from cnct.client.models import Action, Collection, NS, Resource, ResourceSet
from cnct.client.utils import ContentRange
from cnct.rql import R


def test_ns_ns_invalid_type(ns_factory):
ns = ns_factory()
with pytest.raises(TypeError) as cv:
ns.ns(None)

assert str(cv.value) == '`name` must be a string.'

with pytest.raises(TypeError) as cv:
ns.ns(3)

assert str(cv.value) == '`name` must be a string.'


def test_ns_ns_invalid_value(ns_factory):
ns = ns_factory()
with pytest.raises(ValueError) as cv:
ns.ns('')

assert str(cv.value) == '`name` must not be blank.'


def test_ns_ns(ns_factory):
ns = ns_factory()
ns2 = ns.ns('ns2')

assert isinstance(ns2, NS)
assert ns2._client == ns._client
assert ns2.path == f'{ns.path}/ns2'


def test_ns_ns_call(ns_factory):
ns = ns_factory()
ns2 = ns('ns2')

assert isinstance(ns2, NS)
assert ns2._client == ns._client
assert ns2.path == f'{ns.path}/ns2'


def test_ns_collection_invalid_type(ns_factory):
ns = ns_factory()
with pytest.raises(TypeError) as cv:
Expand Down

0 comments on commit feab003

Please sign in to comment.