Skip to content

Commit e56218e

Browse files
author
Francesco Faraone
committed
Use specs just to validate path before call (if validation enabled)
Change way specs are managed (remove specs models) Reorganize help management and improve formatter
1 parent b17985a commit e56218e

28 files changed

+876
-1150
lines changed

cnct/client/fluent.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
from cnct.client.exceptions import ClientError
88
from cnct.client.models import Collection, NS
99
from cnct.client.utils import get_headers
10-
# from cnct.help import DefaultFormatter
11-
from cnct.openapi import OpenAPISpecs
12-
# from cnct.specs.parser import parse
10+
from cnct.client.help_formatter import DefaultFormatter
11+
from cnct.client.openapi import OpenAPISpecs
1312

1413

1514
class ConnectClient:
@@ -22,7 +21,7 @@ def __init__(
2221
endpoint=None,
2322
use_specs=True,
2423
specs_location=None,
25-
validate_calls=True,
24+
validate_using_specs=True,
2625
default_headers=None,
2726
default_limit=100,
2827
):
@@ -45,13 +44,13 @@ def __init__(
4544
self.api_key = api_key
4645
self.default_headers = default_headers or {}
4746
self._use_specs = use_specs
48-
self._validate_calls = validate_calls
47+
self._validate_using_specs = validate_using_specs
4948
self.specs_location = specs_location or CONNECT_SPECS_URL
5049
self.specs = None
5150
if self._use_specs:
5251
self.specs = OpenAPISpecs(self.specs_location)
5352
self.response = None
54-
# self._help_formatter = help_formatter
53+
self._help_formatter = DefaultFormatter(self.specs)
5554

5655
def __getattr__(self, name):
5756
"""
@@ -131,7 +130,7 @@ def delete(self, url, **kwargs):
131130
def execute(self, method, path, **kwargs):
132131
if (
133132
self._use_specs
134-
and self._validate_calls
133+
and self._validate_using_specs
135134
and not self.specs.exists(method, path)
136135
):
137136
# TODO more info, specs version, method etc
@@ -164,8 +163,12 @@ def execute(self, method, path, **kwargs):
164163
status_code = self.response.status_code if self.response is not None else None
165164
raise ClientError(status_code=status_code, **api_error) from re
166165

166+
def print_help(self, obj):
167+
print()
168+
print(self._help_formatter.format(obj))
169+
167170
def help(self):
168-
self._help_formatter.print_help(self.specs)
171+
self.print_help(None)
169172
return self
170173

171174
def _execute_http_call(self, method, url, kwargs):

cnct/client/help_formatter.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import inflect
2+
3+
from cmr import render
4+
from cnct.client.models import Action, Collection, NS, Resource, ResourceSet
5+
6+
7+
_COL_HTTP_METHOD_TO_METHOD = {
8+
'get': '.all(), .filter(), .first(), .last()',
9+
'post': '.create()',
10+
}
11+
12+
13+
class DefaultFormatter:
14+
15+
def __init__(self, specs):
16+
self._specs = specs
17+
self._p = inflect.engine()
18+
19+
def format_client(self):
20+
lines = [
21+
f'# Welcome to {self._specs.title} {self._specs.version}',
22+
'## Introduction'
23+
] + self._specs.description.splitlines()
24+
25+
lines += [
26+
'',
27+
'## Namespaces'
28+
]
29+
for ns in self._specs.get_namespaces():
30+
lines.append(f'* {ns}')
31+
32+
lines += [
33+
'',
34+
'## Collections'
35+
]
36+
for col in self._specs.get_collections():
37+
lines.append(f'* {col}')
38+
39+
return render('\n'.join(lines))
40+
41+
def format_ns(self, ns):
42+
collections = self._specs.get_namespaced_collections(ns.path)
43+
if not collections:
44+
return render(f'~~{ns.path}~~ **does not exists.**')
45+
46+
lines = [
47+
f'# {ns.path.title()} namespace',
48+
f'**path: /{ns.path}**',
49+
'## Available collections',
50+
]
51+
52+
for collection in collections:
53+
lines.append(f'* {collection}')
54+
55+
return render('\n'.join(lines))
56+
57+
def format_collection(self, collection):
58+
col_info = self._specs.get_collection(collection.path)
59+
if not col_info:
60+
return render(f'~~{collection.path}~~ **does not exists.**')
61+
62+
if '/' in collection.path:
63+
_, collection_name = collection.path.rsplit('/', 1)
64+
else:
65+
collection_name = collection.path
66+
67+
lines = [
68+
f'# {collection_name.title()} collection',
69+
f'**path: /{collection.path}**',
70+
]
71+
72+
lines.extend(self._format_heading(col_info))
73+
lines.append('### Available operations')
74+
if 'get' in col_info:
75+
lines.append(f'* GET: {_COL_HTTP_METHOD_TO_METHOD["get"]}')
76+
77+
if 'post' in col_info:
78+
lines.append(f'* POST: {_COL_HTTP_METHOD_TO_METHOD["post"]}')
79+
80+
return render('\n'.join(lines))
81+
82+
def format_resource(self, resource):
83+
res_info = self._specs.get_resource(resource.path)
84+
if not res_info:
85+
return render(f'~~{resource.path}~~ **does not exists.**')
86+
87+
resource_name = resource.path.split('/')[-2]
88+
resource_name = self._p.singular_noun(resource_name)
89+
lines = [
90+
f'# {resource_name.title()} resource',
91+
f'**path: /{resource.path}**',
92+
]
93+
94+
lines.extend(self._format_heading(res_info))
95+
96+
actions = self._specs.get_actions(resource.path)
97+
if actions:
98+
lines.append('### Available actions')
99+
for name, summary in actions:
100+
if summary:
101+
lines.append(f'* {name} - {summary}')
102+
else:
103+
lines.append(f'* {name}')
104+
lines.append('')
105+
106+
nested = self._specs.get_nested_collections(resource.path)
107+
if nested:
108+
lines.append('### Available nested collections')
109+
for name, summary in nested:
110+
if summary:
111+
lines.append(f'* {name} - {summary}')
112+
else:
113+
lines.append(f'* {name}')
114+
115+
return render('\n'.join(lines))
116+
117+
def format_action(self, action):
118+
action_info = self._specs.get_action(action.path)
119+
if not action_info:
120+
return render(f'~~{action.path}~~ **does not exists.**')
121+
122+
_, action_name = action.path.rsplit('/', 1)
123+
124+
lines = [
125+
f'# {action_name.title()} action',
126+
f'**path: /{action.path}**',
127+
]
128+
lines.extend(self._format_heading(action_info))
129+
lines.append('## Available methods')
130+
for method in ('get', 'post', 'put', 'delete'):
131+
if method in action_info:
132+
lines.append(f'* {method.upper()}: .{method}()')
133+
134+
return render('\n'.join(lines))
135+
136+
def format_resource_set(self, rs):
137+
col_info = self._specs.get_collection(rs.path)
138+
if not col_info:
139+
return render(f'~~{rs.path}~~ **does not exists.**')
140+
141+
if '/' in rs.path:
142+
_, collection_name = rs.path.rsplit('/', 1)
143+
else:
144+
collection_name = rs.path
145+
146+
lines = [
147+
f'# Search the {collection_name.title()} collection',
148+
f'**path: /{rs.path}**',
149+
]
150+
lines.extend(self._format_heading(col_info))
151+
get_info = col_info['get']
152+
params = get_info.get('parameters')
153+
if not params:
154+
return render('\n'.join(lines))
155+
filters = list(sorted(filter(lambda x: '$ref' not in x, params), key=lambda x: x['name']))
156+
lines.append(f'Support pagination: *{len(params) > len(filters)}*')
157+
if not filters:
158+
return render('\n'.join(lines))
159+
160+
lines.append('## Available filters')
161+
for filter_ in filters:
162+
lines.append(f'*{filter_["name"]}*')
163+
description = filter_['description'].splitlines()
164+
for line in description:
165+
line = line.strip()
166+
if line:
167+
lines.append(f' {line}')
168+
lines.append('')
169+
170+
return render('\n'.join(lines))
171+
172+
def format(self, obj):
173+
if not self._specs:
174+
return render('**No OpenAPI specs available.**')
175+
176+
if isinstance(obj, NS):
177+
return self.format_ns(obj)
178+
179+
if isinstance(obj, Collection):
180+
return self.format_collection(obj)
181+
182+
if isinstance(obj, Resource):
183+
return self.format_resource(obj)
184+
185+
if isinstance(obj, Action):
186+
return self.format_action(obj)
187+
188+
if isinstance(obj, ResourceSet):
189+
return self.format_resource_set(obj)
190+
191+
return self.format_client()
192+
193+
def _format_heading(self, info):
194+
lines = []
195+
for section in ('summary', 'description'):
196+
if section in info:
197+
lines.append(f'### {section.title()}')
198+
lines.append(info[section])
199+
return lines

cnct/client/models/base.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from cnct.client.exceptions import ClientError
12
from cnct.client.models.resourceset import ResourceSet
23
from cnct.client.utils import resolve_attribute
34
from cnct.rql import R
@@ -69,7 +70,7 @@ def help(self):
6970
:return: self
7071
:rtype: NS
7172
"""
72-
# self._client._help_formatter.print_help(self._specs)
73+
self._client.print_help(self)
7374
return self
7475

7576

@@ -220,7 +221,7 @@ def help(self):
220221
:return: self
221222
:rtype: Collection
222223
"""
223-
# self._client._help_formatter.print_help(self._specs)
224+
self._client.print_help(self)
224225
return self
225226

226227

@@ -306,6 +307,15 @@ def action(self, name):
306307
f'{self._path}/{name}',
307308
)
308309

310+
def exists(self):
311+
try:
312+
self.get()
313+
return True
314+
except ClientError as ce:
315+
if ce.status_code and ce.status_code == 404:
316+
return False
317+
raise
318+
309319
def get(self, **kwargs):
310320
"""
311321
Execute a http GET to retrieve this resource.
@@ -375,7 +385,7 @@ def help(self):
375385
:return: self
376386
:rtype: Resource
377387
"""
378-
# self._client._help_formatter.print_help(self._specs)
388+
self._client.print_help(self)
379389
return self
380390

381391

@@ -430,7 +440,6 @@ def post(self, payload=None, **kwargs):
430440
return self._client.execute(
431441
'post',
432442
self._path,
433-
200,
434443
**kwargs,
435444
)
436445

@@ -451,7 +460,6 @@ def put(self, payload=None, **kwargs):
451460
return self._client.execute(
452461
'put',
453462
self._path,
454-
200,
455463
**kwargs,
456464
)
457465

@@ -474,5 +482,5 @@ def help(self):
474482
:return: self
475483
:rtype: Action
476484
"""
477-
# self._client._help_formatter.print_help(self._specs)
485+
self._client.print_help(self)
478486
return self

cnct/client/models/resourceset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,5 +428,5 @@ def help(self):
428428
:return: self
429429
:rtype: ResourceSet
430430
"""
431-
# self._client._help_formatter.print_help(self._specs)
431+
self._client.print_help(self)
432432
return self

0 commit comments

Comments
 (0)