Skip to content

Commit feab003

Browse files
authored
Merge pull request #12 from cloudblue/support_nested_namespaces
Add support for nested namespaces
2 parents 987baa0 + da914d4 commit feab003

File tree

4 files changed

+115
-8
lines changed

4 files changed

+115
-8
lines changed

cnct/client/help_formatter.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,25 @@ def format_client(self):
3939
return render('\n'.join(lines))
4040

4141
def format_ns(self, ns):
42+
namespaces = self._specs.get_nested_namespaces(ns.path)
4243
collections = self._specs.get_namespaced_collections(ns.path)
43-
if not collections:
44+
45+
if not (collections or namespaces):
4446
return render(f'~~{ns.path}~~ **does not exists.**')
4547

4648
lines = [
4749
f'# {ns.path.title()} namespace',
4850
f'**path: /{ns.path}**',
49-
'## Available collections',
5051
]
51-
52-
for collection in collections:
53-
lines.append(f'* {collection}')
52+
if namespaces:
53+
lines.append('## Available namespaces')
54+
for namespace in namespaces:
55+
lines.append(f'* {namespace}')
56+
57+
if collections:
58+
lines.append('## Available collections')
59+
for collection in collections:
60+
lines.append(f'* {collection}')
5461

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

cnct/client/models/base.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ def __getattr__(self, name):
4040
def __iter__(self):
4141
raise TypeError('A Namespace object is not iterable.')
4242

43+
def __call__(self, name):
44+
return self.ns(name)
45+
4346
def collection(self, name):
4447
"""
4548
Returns the collection called ``name``.
@@ -63,6 +66,28 @@ def collection(self, name):
6366
f'{self._path}/{name}',
6467
)
6568

69+
def ns(self, name):
70+
"""
71+
Returns the namespace called ``name``.
72+
73+
:param name: The name of the namespace.
74+
:type name: str
75+
:raises TypeError: if the ``name`` is not a string.
76+
:raises ValueError: if the ``name`` is blank.
77+
:return: The namespace called ``name``.
78+
:rtype: NS
79+
"""
80+
if not isinstance(name, str):
81+
raise TypeError('`name` must be a string.')
82+
83+
if not name:
84+
raise ValueError('`name` must not be blank.')
85+
86+
return NS(
87+
self._client,
88+
f'{self._path}/{name}',
89+
)
90+
6691
def help(self):
6792
"""
6893
Output the namespace documentation to the console.

cnct/client/openapi.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from io import StringIO
2+
from functools import partial
23

34
import requests
45
import yaml
@@ -60,8 +61,9 @@ def get_namespaced_collections(self, path):
6061
nested = filter(lambda x: x[1:].startswith(path), self._specs['paths'].keys())
6162
collections = set()
6263
for p in nested:
63-
splitted = p[1:].split('/', 2)
64-
collections.add(splitted[1])
64+
splitted = p[len(path) + 1:].split('/', 2)
65+
if self._is_collection(p) and len(splitted) == 2:
66+
collections.add(splitted[1])
6567
return list(sorted(collections))
6668

6769
def get_collection(self, path):
@@ -94,6 +96,28 @@ def get_actions(self, path):
9496
for name in sorted(actions)
9597
]
9698

99+
def get_nested_namespaces(self, path):
100+
def _is_nested_namespace(base_path, path):
101+
if path[1:].startswith(base_path):
102+
comp = path[1:].split('/')
103+
return (
104+
len(comp) > 1
105+
and not comp[-1].startswith('{')
106+
)
107+
return False
108+
109+
nested = filter(
110+
partial(_is_nested_namespace, path),
111+
self._specs['paths'].keys(),
112+
)
113+
current_level = len(path[1:].split('/'))
114+
nested_namespaces = []
115+
for ns in nested:
116+
name = ns[1:].split('/')[current_level]
117+
if not self._is_collection(f'/{path}/{name}'):
118+
nested_namespaces.append(name)
119+
return nested_namespaces
120+
97121
def get_nested_collections(self, path):
98122
p = self._get_path(path)
99123
nested = filter(
@@ -162,3 +186,15 @@ def _get_info(self, path):
162186
def _is_action(self, operation_id):
163187
op_id_cmps = operation_id.rsplit('_', 2)
164188
return op_id_cmps[-2] not in ('list', 'retrieve')
189+
190+
def _is_collection(self, path):
191+
path_length = len(path[1:].split('/'))
192+
for p in self._specs['paths'].keys():
193+
comp = p[1:].split('/')
194+
if not p.startswith(path):
195+
continue
196+
if p == path:
197+
return True
198+
if len(comp) > path_length and comp[path_length].startswith('{'):
199+
return True
200+
return False

tests/client/test_models.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,50 @@
11
import pytest
22

33
from cnct.client.exceptions import ClientError
4-
from cnct.client.models import Action, Collection, Resource, ResourceSet
4+
from cnct.client.models import Action, Collection, NS, Resource, ResourceSet
55
from cnct.client.utils import ContentRange
66
from cnct.rql import R
77

88

9+
def test_ns_ns_invalid_type(ns_factory):
10+
ns = ns_factory()
11+
with pytest.raises(TypeError) as cv:
12+
ns.ns(None)
13+
14+
assert str(cv.value) == '`name` must be a string.'
15+
16+
with pytest.raises(TypeError) as cv:
17+
ns.ns(3)
18+
19+
assert str(cv.value) == '`name` must be a string.'
20+
21+
22+
def test_ns_ns_invalid_value(ns_factory):
23+
ns = ns_factory()
24+
with pytest.raises(ValueError) as cv:
25+
ns.ns('')
26+
27+
assert str(cv.value) == '`name` must not be blank.'
28+
29+
30+
def test_ns_ns(ns_factory):
31+
ns = ns_factory()
32+
ns2 = ns.ns('ns2')
33+
34+
assert isinstance(ns2, NS)
35+
assert ns2._client == ns._client
36+
assert ns2.path == f'{ns.path}/ns2'
37+
38+
39+
def test_ns_ns_call(ns_factory):
40+
ns = ns_factory()
41+
ns2 = ns('ns2')
42+
43+
assert isinstance(ns2, NS)
44+
assert ns2._client == ns._client
45+
assert ns2.path == f'{ns.path}/ns2'
46+
47+
948
def test_ns_collection_invalid_type(ns_factory):
1049
ns = ns_factory()
1150
with pytest.raises(TypeError) as cv:

0 commit comments

Comments
 (0)