cli2 3.3.45__tar.gz → 3.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cli2-3.3.45/cli2.egg-info → cli2-3.4.0}/PKG-INFO +1 -1
- {cli2-3.3.45 → cli2-3.4.0}/cli2/__init__.py +1 -1
- cli2-3.4.0/cli2/ansible.py +273 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/client.py +76 -8
- {cli2-3.3.45 → cli2-3.4.0}/cli2/display.py +14 -2
- {cli2-3.3.45 → cli2-3.4.0}/cli2/example_client.py +11 -3
- cli2-3.4.0/cli2/locker.py +62 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/logging.py +3 -3
- cli2-3.4.0/cli2/pytest_ansible.py +221 -0
- cli2-3.4.0/cli2/test_ansible.py +113 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/test_client.py +135 -122
- cli2-3.4.0/cli2/test_restful.py +92 -0
- {cli2-3.3.45 → cli2-3.4.0/cli2.egg-info}/PKG-INFO +1 -1
- {cli2-3.3.45 → cli2-3.4.0}/cli2.egg-info/SOURCES.txt +5 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2.egg-info/entry_points.txt +3 -0
- {cli2-3.3.45 → cli2-3.4.0}/setup.py +4 -1
- {cli2-3.3.45 → cli2-3.4.0}/MANIFEST.in +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/README.rst +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/classifiers.txt +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/argument.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/asyncio.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/cli.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/colors.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/command.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/configuration.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/decorators.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/entry_point.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/example_client_complex.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/example_nesting.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/example_obj.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/group.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/node.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/overrides.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/sphinx.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/table.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/test.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/test_cli.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/test_command.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/test_configuration.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/test_decorators.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/test_display.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/test_entry_point.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/test_group.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/test_inject.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/test_node.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2/test_table.py +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2.egg-info/requires.txt +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/cli2.egg-info/top_level.txt +0 -0
- {cli2-3.3.45 → cli2-3.4.0}/setup.cfg +0 -0
|
@@ -28,7 +28,7 @@ except ImportError:
|
|
|
28
28
|
# httpx not installed
|
|
29
29
|
pass
|
|
30
30
|
from .decorators import arg, cmd
|
|
31
|
-
from .display import diff, print, highlight
|
|
31
|
+
from .display import diff, diff_data, render, print, highlight
|
|
32
32
|
from .group import Group
|
|
33
33
|
from .node import Node
|
|
34
34
|
from .table import Table
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Experimental: my base class for Ansible actions.
|
|
3
|
+
|
|
4
|
+
TODO: Turn this into a cool framework.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import cli2
|
|
9
|
+
import copy
|
|
10
|
+
import mock
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import traceback
|
|
14
|
+
|
|
15
|
+
from cli2 import logging
|
|
16
|
+
|
|
17
|
+
from ansible.plugins.action import ActionBase
|
|
18
|
+
|
|
19
|
+
# colors:
|
|
20
|
+
# black
|
|
21
|
+
# bright gray
|
|
22
|
+
# blue
|
|
23
|
+
# white
|
|
24
|
+
# green
|
|
25
|
+
# cyan
|
|
26
|
+
# bright green
|
|
27
|
+
# red
|
|
28
|
+
# bright cyan
|
|
29
|
+
# purple
|
|
30
|
+
# bright red
|
|
31
|
+
# yellow
|
|
32
|
+
# bright purple
|
|
33
|
+
# dark gray
|
|
34
|
+
# magenta
|
|
35
|
+
# bright magenta
|
|
36
|
+
# normal
|
|
37
|
+
|
|
38
|
+
# 7-bit C1 ANSI sequences
|
|
39
|
+
ansi_escape = re.compile(r'''
|
|
40
|
+
\x1B # ESC
|
|
41
|
+
(?: # 7-bit C1 Fe (except CSI)
|
|
42
|
+
[@-Z\\-_]
|
|
43
|
+
| # or [ for CSI, followed by a control sequence
|
|
44
|
+
\[
|
|
45
|
+
[0-?]* # Parameter bytes
|
|
46
|
+
[ -/]* # Intermediate bytes
|
|
47
|
+
[@-~] # Final byte
|
|
48
|
+
)
|
|
49
|
+
''', re.VERBOSE)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
UNSET_DEFAULT = '__UNSET__DEFAULT__'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Option:
|
|
56
|
+
"""
|
|
57
|
+
Ansible Option descriptor.
|
|
58
|
+
|
|
59
|
+
.. py:attribute:: arg
|
|
60
|
+
|
|
61
|
+
Name of the task argument to get this option value from
|
|
62
|
+
|
|
63
|
+
.. py:attribute:: fact
|
|
64
|
+
|
|
65
|
+
Name of the fact, if any, to get a value for this option if no task arg
|
|
66
|
+
is provided
|
|
67
|
+
|
|
68
|
+
.. py:attribute:: default
|
|
69
|
+
|
|
70
|
+
Default value, if any, in case neither of arg and fact were defined.
|
|
71
|
+
"""
|
|
72
|
+
UNSET_DEFAULT = UNSET_DEFAULT
|
|
73
|
+
|
|
74
|
+
def __init__(self, arg=None, fact=None, default=UNSET_DEFAULT):
|
|
75
|
+
self.arg = arg
|
|
76
|
+
self.fact = fact
|
|
77
|
+
self.default = default
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def kwargs(self):
|
|
81
|
+
kwargs = dict(default=self.default)
|
|
82
|
+
if self.arg:
|
|
83
|
+
kwargs['arg_name'] = self.arg
|
|
84
|
+
if self.fact:
|
|
85
|
+
kwargs['fact_name'] = self.fact
|
|
86
|
+
return kwargs
|
|
87
|
+
|
|
88
|
+
def __get__(self, obj, objtype=None):
|
|
89
|
+
if obj is None:
|
|
90
|
+
return self
|
|
91
|
+
try:
|
|
92
|
+
return obj.get(**self.kwargs)
|
|
93
|
+
except AttributeError:
|
|
94
|
+
raise AnsibleOptionError(self)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AnsibleError(Exception):
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AnsibleOptionError(AnsibleError):
|
|
102
|
+
def __init__(self, option):
|
|
103
|
+
self.option = option
|
|
104
|
+
super().__init__(option.kwargs)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def message(self):
|
|
108
|
+
message = ['Missing']
|
|
109
|
+
if self.option.arg:
|
|
110
|
+
message.append(f'arg `{self.option.arg}`')
|
|
111
|
+
if self.option.fact:
|
|
112
|
+
message.append('or')
|
|
113
|
+
if self.option.fact:
|
|
114
|
+
message.append(f'fact `{self.option.fact}`')
|
|
115
|
+
return ' '.join(message)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ActionBase(ActionBase):
|
|
119
|
+
"""
|
|
120
|
+
Base action class
|
|
121
|
+
|
|
122
|
+
.. py:attribute:: result
|
|
123
|
+
|
|
124
|
+
Result dict that will be returned to Ansible
|
|
125
|
+
|
|
126
|
+
.. py:attribute:: task_vars
|
|
127
|
+
|
|
128
|
+
The task_vars that the module was called with
|
|
129
|
+
|
|
130
|
+
.. py:attribute:: client
|
|
131
|
+
|
|
132
|
+
The client object generated by :py:meth:`client_factory` if you
|
|
133
|
+
implement it.
|
|
134
|
+
"""
|
|
135
|
+
def get(self, arg_name, fact_name=None, default=UNSET_DEFAULT):
|
|
136
|
+
if arg_name in self._task.args:
|
|
137
|
+
return self._task.args[arg_name]
|
|
138
|
+
if fact_name and fact_name in self.task_vars:
|
|
139
|
+
return self.task_vars[fact_name]
|
|
140
|
+
if default != UNSET_DEFAULT:
|
|
141
|
+
return default
|
|
142
|
+
if fact_name:
|
|
143
|
+
raise AttributeError(f'Undefined {arg_name} or {fact_name}')
|
|
144
|
+
else:
|
|
145
|
+
raise AttributeError(f'Undefined arg {arg_name}')
|
|
146
|
+
|
|
147
|
+
def run(self, tmp=None, task_vars=None):
|
|
148
|
+
self.tmp = tmp
|
|
149
|
+
self.task_vars = task_vars
|
|
150
|
+
self.result = super().run(tmp, task_vars)
|
|
151
|
+
asyncio.run(self.run_wrapped_async())
|
|
152
|
+
return self.result
|
|
153
|
+
|
|
154
|
+
async def run_wrapped_async(self):
|
|
155
|
+
self.verbosity = self.task_vars.get('ansible_verbosity', 0)
|
|
156
|
+
|
|
157
|
+
if 'LOG_LEVEL' in os.environ or 'DEBUG' in os.environ:
|
|
158
|
+
logging.configure()
|
|
159
|
+
else:
|
|
160
|
+
if self.verbosity == 1:
|
|
161
|
+
os.environ['LOG_LEVEL'] = 'INFO'
|
|
162
|
+
elif self.verbosity >= 2:
|
|
163
|
+
os.environ['LOG_LEVEL'] = 'DEBUG'
|
|
164
|
+
logging.configure()
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
try:
|
|
168
|
+
self.client = await self.client_factory()
|
|
169
|
+
except NotImplementedError:
|
|
170
|
+
self.client = None
|
|
171
|
+
await self.run_async()
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
self.result['failed'] = True
|
|
174
|
+
|
|
175
|
+
if isinstance(exc, AnsibleError):
|
|
176
|
+
self.result['error'] = exc.message
|
|
177
|
+
elif self.verbosity:
|
|
178
|
+
traceback.print_exc()
|
|
179
|
+
|
|
180
|
+
# for pytest to raise
|
|
181
|
+
self.exc = exc
|
|
182
|
+
finally:
|
|
183
|
+
if (
|
|
184
|
+
self._before_data != UNSET_DEFAULT
|
|
185
|
+
and self._after_data != UNSET_DEFAULT
|
|
186
|
+
):
|
|
187
|
+
diff = cli2.diff_data(
|
|
188
|
+
self._before_data,
|
|
189
|
+
self._after_data,
|
|
190
|
+
self._before_label,
|
|
191
|
+
self._after_label,
|
|
192
|
+
)
|
|
193
|
+
if self.client and self.client.mask:
|
|
194
|
+
output = '\n'.join([
|
|
195
|
+
line.rstrip() for line in diff if line.strip()
|
|
196
|
+
])
|
|
197
|
+
output = re.sub(
|
|
198
|
+
f'({"|".join(self.client.mask)}): (.*)',
|
|
199
|
+
'\\1: ***MASKED***',
|
|
200
|
+
''.join(output),
|
|
201
|
+
)
|
|
202
|
+
print(cli2.highlight(output, 'Diff'))
|
|
203
|
+
else:
|
|
204
|
+
cli2.diff(diff)
|
|
205
|
+
|
|
206
|
+
async def run_async(self):
|
|
207
|
+
"""
|
|
208
|
+
The method you are supposed to implement.
|
|
209
|
+
|
|
210
|
+
It should:
|
|
211
|
+
|
|
212
|
+
- provision the :py:attr:`result` dict
|
|
213
|
+
- find task_vars in :py:attr:`task_vars`
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
async def client_factory(self):
|
|
217
|
+
"""
|
|
218
|
+
Return a client instance.
|
|
219
|
+
|
|
220
|
+
:raise NotImplementedError: By default
|
|
221
|
+
"""
|
|
222
|
+
raise NotImplementedError()
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
async def run_test_async(cls, args=None, facts=None, client=None,
|
|
226
|
+
fail=False):
|
|
227
|
+
"""
|
|
228
|
+
Test run the module in a mocked context.
|
|
229
|
+
|
|
230
|
+
:param args: Dict of task arguments
|
|
231
|
+
:param facts: Dict of play facts
|
|
232
|
+
:param client: Client instance, overrides the factory
|
|
233
|
+
:param fail: Allow this test to fail without exception
|
|
234
|
+
"""
|
|
235
|
+
obj = cls(*[mock.Mock()] * 6)
|
|
236
|
+
obj.tmp = None
|
|
237
|
+
obj.task_vars = mock.Mock()
|
|
238
|
+
obj.result = dict()
|
|
239
|
+
obj._task = mock.Mock()
|
|
240
|
+
obj._task.args = args or {}
|
|
241
|
+
obj.task_vars = facts or {}
|
|
242
|
+
obj.task_vars['ansible_verbosity'] = 1
|
|
243
|
+
obj.exc = False
|
|
244
|
+
if client:
|
|
245
|
+
async def _factory():
|
|
246
|
+
return client
|
|
247
|
+
obj.client_factory = _factory
|
|
248
|
+
old = obj.client_factory
|
|
249
|
+
|
|
250
|
+
async def set_tries():
|
|
251
|
+
client = await old()
|
|
252
|
+
client.handler.tries = 0
|
|
253
|
+
return client
|
|
254
|
+
obj.client_factory = set_tries
|
|
255
|
+
await obj.run_wrapped_async()
|
|
256
|
+
if obj.exc and not fail:
|
|
257
|
+
raise obj.exc
|
|
258
|
+
if obj.result.get('failed', False) and not fail:
|
|
259
|
+
raise Exception('Module failed, and fail is not True {obj.result}')
|
|
260
|
+
return obj
|
|
261
|
+
|
|
262
|
+
def __init__(self, *args, **kwargs):
|
|
263
|
+
super().__init__(*args, **kwargs)
|
|
264
|
+
self._before_data = UNSET_DEFAULT
|
|
265
|
+
self._after_data = UNSET_DEFAULT
|
|
266
|
+
|
|
267
|
+
def before_set(self, data, label='before'):
|
|
268
|
+
self._before_data = copy.deepcopy(data)
|
|
269
|
+
self._before_label = label
|
|
270
|
+
|
|
271
|
+
def after_set(self, data, label='after'):
|
|
272
|
+
self._after_data = copy.deepcopy(data)
|
|
273
|
+
self._after_label = label
|
|
@@ -181,14 +181,12 @@ class Paginator:
|
|
|
181
181
|
.. code-block:: python
|
|
182
182
|
|
|
183
183
|
def pagination_parameters(self, params, page_number):
|
|
184
|
-
# this is the default implementation
|
|
185
184
|
params['page'] = page_number
|
|
186
185
|
|
|
187
186
|
:param params: Dict of base GET parameters
|
|
188
187
|
:param page_number: Page number to get
|
|
189
188
|
"""
|
|
190
|
-
|
|
191
|
-
params['page'] = page_number
|
|
189
|
+
raise NotImplementedError("pagination_parameters not implemented")
|
|
192
190
|
|
|
193
191
|
def response_items(self, response):
|
|
194
192
|
"""
|
|
@@ -231,7 +229,11 @@ class Paginator:
|
|
|
231
229
|
|
|
232
230
|
:param page_number: Page number to get the items from
|
|
233
231
|
"""
|
|
234
|
-
|
|
232
|
+
try:
|
|
233
|
+
return self.response_items(await self.page_response(page_number))
|
|
234
|
+
except NotImplementedError:
|
|
235
|
+
# pagination_parameters not implemented, can't paginate
|
|
236
|
+
return []
|
|
235
237
|
|
|
236
238
|
async def page_response(self, page_number):
|
|
237
239
|
"""
|
|
@@ -240,7 +242,11 @@ class Paginator:
|
|
|
240
242
|
:param page_number: Page number to get the items from
|
|
241
243
|
"""
|
|
242
244
|
params = self.params.copy()
|
|
243
|
-
|
|
245
|
+
try:
|
|
246
|
+
self.pagination_parameters(params, page_number)
|
|
247
|
+
except NotImplementedError:
|
|
248
|
+
if page_number > 1:
|
|
249
|
+
raise
|
|
244
250
|
for expression in self.expressions:
|
|
245
251
|
if expression.parameterable:
|
|
246
252
|
expression.params(params)
|
|
@@ -365,6 +371,12 @@ class Field:
|
|
|
365
371
|
- Use :py:meth:`internal_set` to actually set the internal
|
|
366
372
|
:py:attr:`Model.data`
|
|
367
373
|
"""
|
|
374
|
+
try:
|
|
375
|
+
old_value = getattr(obj, self.name)
|
|
376
|
+
if self.name not in obj.changed_fields and value != old_value:
|
|
377
|
+
obj.changed_fields[self.name] = old_value
|
|
378
|
+
except FieldExternalizeError:
|
|
379
|
+
obj.changed_fields[self.name] = None
|
|
368
380
|
value = self.internalize(obj, value)
|
|
369
381
|
self.internal_set(obj, value)
|
|
370
382
|
|
|
@@ -527,6 +539,8 @@ class JSONStringField(MutableField):
|
|
|
527
539
|
return json.dumps(data, **self.options)
|
|
528
540
|
|
|
529
541
|
def externalize(self, obj, value):
|
|
542
|
+
if value == '':
|
|
543
|
+
return value
|
|
530
544
|
return json.loads(value)
|
|
531
545
|
|
|
532
546
|
|
|
@@ -676,6 +690,8 @@ class ModelGroup(Group):
|
|
|
676
690
|
|
|
677
691
|
class ModelMetaclass(type):
|
|
678
692
|
def __new__(cls, name, bases, attributes):
|
|
693
|
+
if 'Paginator' in attributes:
|
|
694
|
+
attributes['paginator'] = attributes['Paginator']
|
|
679
695
|
cls = super().__new__(cls, name, bases, attributes)
|
|
680
696
|
client = getattr(cls, 'client', None)
|
|
681
697
|
if client:
|
|
@@ -767,9 +783,13 @@ class Model(metaclass=ModelMetaclass):
|
|
|
767
783
|
self._dirty_fields = []
|
|
768
784
|
self._field_cache = dict()
|
|
769
785
|
|
|
786
|
+
self.changed_fields = dict()
|
|
770
787
|
for key, value in values.items():
|
|
771
788
|
setattr(self, key, value)
|
|
772
789
|
|
|
790
|
+
# actually reset that
|
|
791
|
+
self.changed_fields = dict()
|
|
792
|
+
|
|
773
793
|
@property
|
|
774
794
|
def data(self):
|
|
775
795
|
"""
|
|
@@ -787,6 +807,10 @@ class Model(metaclass=ModelMetaclass):
|
|
|
787
807
|
def data(self, value):
|
|
788
808
|
self._data = value
|
|
789
809
|
|
|
810
|
+
@property
|
|
811
|
+
def data_masked(self):
|
|
812
|
+
return self.client.mask_data(self.data)
|
|
813
|
+
|
|
790
814
|
@classmethod
|
|
791
815
|
@hide('expressions')
|
|
792
816
|
@cmd(color='green', condition=lambda cls: cls.url_list)
|
|
@@ -864,6 +888,7 @@ class Model(metaclass=ModelMetaclass):
|
|
|
864
888
|
"""
|
|
865
889
|
response = await self.client.get(self.url)
|
|
866
890
|
self.data.update(response.json())
|
|
891
|
+
self.changed_fields = dict()
|
|
867
892
|
|
|
868
893
|
async def save(self):
|
|
869
894
|
"""
|
|
@@ -917,6 +942,8 @@ class Model(metaclass=ModelMetaclass):
|
|
|
917
942
|
|
|
918
943
|
class ClientMetaclass(type):
|
|
919
944
|
def __new__(cls, name, bases, attributes):
|
|
945
|
+
if 'Paginator' in attributes:
|
|
946
|
+
attributes['paginator'] = attributes['Paginator']
|
|
920
947
|
cls = super().__new__(cls, name, bases, attributes)
|
|
921
948
|
|
|
922
949
|
# bind ourself as _client_class to any inherited model
|
|
@@ -1044,13 +1071,52 @@ class ClientError(Exception):
|
|
|
1044
1071
|
|
|
1045
1072
|
|
|
1046
1073
|
class ResponseError(ClientError):
|
|
1074
|
+
"""
|
|
1075
|
+
Beautiful Response Error class.
|
|
1076
|
+
|
|
1077
|
+
.. py:attribute:: response
|
|
1078
|
+
|
|
1079
|
+
httpx Response object
|
|
1080
|
+
|
|
1081
|
+
.. py:attribute:: request
|
|
1082
|
+
|
|
1083
|
+
httpx Request object
|
|
1084
|
+
|
|
1085
|
+
.. py:attribute:: status_code
|
|
1086
|
+
|
|
1087
|
+
Response status code
|
|
1088
|
+
|
|
1089
|
+
.. py:attribute:: url
|
|
1090
|
+
|
|
1091
|
+
Request url
|
|
1092
|
+
|
|
1093
|
+
.. py:attribute:: method
|
|
1094
|
+
|
|
1095
|
+
Request method
|
|
1096
|
+
"""
|
|
1047
1097
|
def __init__(self, client, response, tries, mask, msg=None):
|
|
1048
1098
|
self.client = client
|
|
1049
1099
|
self.response = response
|
|
1050
1100
|
self.tries = tries
|
|
1051
1101
|
self.mask = mask
|
|
1052
|
-
msg = msg or getattr(self, 'msg', '').format(self=self)
|
|
1053
|
-
super().__init__(self.enhance(msg))
|
|
1102
|
+
self.msg = msg or getattr(self, 'msg', '').format(self=self)
|
|
1103
|
+
super().__init__(self.enhance(self.msg))
|
|
1104
|
+
|
|
1105
|
+
@property
|
|
1106
|
+
def request(self):
|
|
1107
|
+
return self.response.request
|
|
1108
|
+
|
|
1109
|
+
@property
|
|
1110
|
+
def method(self):
|
|
1111
|
+
return str(self.request.method)
|
|
1112
|
+
|
|
1113
|
+
@property
|
|
1114
|
+
def url(self):
|
|
1115
|
+
return str(self.request.url)
|
|
1116
|
+
|
|
1117
|
+
@property
|
|
1118
|
+
def status_code(self):
|
|
1119
|
+
return str(self.response.status_code)
|
|
1054
1120
|
|
|
1055
1121
|
def enhance(self, msg):
|
|
1056
1122
|
"""
|
|
@@ -1442,7 +1508,9 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1442
1508
|
|
|
1443
1509
|
return response
|
|
1444
1510
|
|
|
1445
|
-
def response_log_data(self, response, mask):
|
|
1511
|
+
def response_log_data(self, response, mask=None):
|
|
1512
|
+
if mask is None:
|
|
1513
|
+
mask = self.mask
|
|
1446
1514
|
try:
|
|
1447
1515
|
data = response.json()
|
|
1448
1516
|
except json.JSONDecodeError:
|
|
@@ -4,8 +4,10 @@ Generic pretty display utils.
|
|
|
4
4
|
This module defines a print function that's supposed to be able to pretty-print
|
|
5
5
|
anything, as well as a pretty diff printer.
|
|
6
6
|
"""
|
|
7
|
+
import difflib
|
|
7
8
|
import os
|
|
8
9
|
import sys
|
|
10
|
+
import yaml
|
|
9
11
|
|
|
10
12
|
try:
|
|
11
13
|
import jsonlight as json
|
|
@@ -34,7 +36,6 @@ def highlight(string, lexer):
|
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
def yaml_dump(data):
|
|
37
|
-
import yaml
|
|
38
39
|
if isinstance(data, dict):
|
|
39
40
|
# ensure that objects inheriting from dict render nicely
|
|
40
41
|
data = dict(data)
|
|
@@ -45,7 +46,7 @@ def yaml_highlight(yaml_string):
|
|
|
45
46
|
return highlight(yaml_string, 'Yaml')
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
def render(arg):
|
|
49
|
+
def render(arg, highlight=True):
|
|
49
50
|
"""
|
|
50
51
|
Try to render arg as yaml.
|
|
51
52
|
|
|
@@ -78,6 +79,8 @@ def render(arg):
|
|
|
78
79
|
pass
|
|
79
80
|
|
|
80
81
|
string = arg if isinstance(arg, str) else yaml_dump(arg)
|
|
82
|
+
if not highlight:
|
|
83
|
+
return string
|
|
81
84
|
return yaml_highlight(string)
|
|
82
85
|
|
|
83
86
|
|
|
@@ -111,3 +114,12 @@ def diff(diff, **kwargs):
|
|
|
111
114
|
cli2.diff(difflib.unified_diff(old, new))
|
|
112
115
|
"""
|
|
113
116
|
_print(diff_highlight(diff), **kwargs)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def diff_data(before, after, before_label='before', after_label='after'):
|
|
120
|
+
return difflib.unified_diff(
|
|
121
|
+
yaml.dump(before).splitlines(),
|
|
122
|
+
yaml.dump(after).splitlines(),
|
|
123
|
+
before_label,
|
|
124
|
+
after_label,
|
|
125
|
+
)
|
|
@@ -4,9 +4,17 @@ import cli2
|
|
|
4
4
|
class APIClient(cli2.Client):
|
|
5
5
|
"""
|
|
6
6
|
Client for restful-api.dev
|
|
7
|
+
|
|
8
|
+
Prior to using this, run at the root of this repository:
|
|
9
|
+
|
|
10
|
+
pip install django djangorestframework
|
|
11
|
+
./manage.py migrate
|
|
12
|
+
./manage.py runserver
|
|
7
13
|
"""
|
|
14
|
+
mask = ["Capacity"]
|
|
15
|
+
|
|
8
16
|
def __init__(self, *args, **kwargs):
|
|
9
|
-
kwargs.setdefault('base_url', '
|
|
17
|
+
kwargs.setdefault('base_url', 'http://localhost:8000')
|
|
10
18
|
super().__init__(*args, **kwargs)
|
|
11
19
|
|
|
12
20
|
@cli2.cmd
|
|
@@ -23,8 +31,8 @@ class Object(APIClient.Model):
|
|
|
23
31
|
|
|
24
32
|
cli2-example-client object create name=cli2 capacity=2TB
|
|
25
33
|
"""
|
|
26
|
-
url_list = '/objects'
|
|
27
|
-
url_detail = '/objects/{self.id}'
|
|
34
|
+
url_list = '/objects/'
|
|
35
|
+
url_detail = '/objects/{self.id}/'
|
|
28
36
|
|
|
29
37
|
id = cli2.Field()
|
|
30
38
|
name = cli2.Field()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fcntl
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Locker:
|
|
6
|
+
def __init__(self, lock_path, blocking=True, prefix=None):
|
|
7
|
+
self.lock_path = lock_path
|
|
8
|
+
self.blocking = blocking
|
|
9
|
+
self.acquired = True
|
|
10
|
+
self.prefix = prefix or ''
|
|
11
|
+
|
|
12
|
+
def display(self, msg):
|
|
13
|
+
_ = []
|
|
14
|
+
if self.blocking:
|
|
15
|
+
_.append('[BLOCKING LOCK]')
|
|
16
|
+
else:
|
|
17
|
+
_.append('[NON BLOCKING LOCK]')
|
|
18
|
+
if self.prefix:
|
|
19
|
+
_.append(f'[{self.prefix}]')
|
|
20
|
+
_.append(f'[{self.lock_path}]')
|
|
21
|
+
_.append(f' {msg}')
|
|
22
|
+
self.print(''.join(_))
|
|
23
|
+
|
|
24
|
+
def print(self, msg):
|
|
25
|
+
print(msg)
|
|
26
|
+
|
|
27
|
+
def __enter__(self):
|
|
28
|
+
self.display('Waiting')
|
|
29
|
+
self.lock_path.parent.mkdir(parents=True, exists_ok=True)
|
|
30
|
+
self.fp = self.lock_path.open('w+')
|
|
31
|
+
|
|
32
|
+
if self.blocking:
|
|
33
|
+
fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX)
|
|
34
|
+
self.display('Acquired')
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
fcntl.flock(
|
|
39
|
+
self.fp.fileno(),
|
|
40
|
+
fcntl.LOCK_EX | fcntl.LOCK_NB,
|
|
41
|
+
)
|
|
42
|
+
except BlockingIOError:
|
|
43
|
+
self.display('Not acquired but non blocking: proceeding')
|
|
44
|
+
self.acquired = False
|
|
45
|
+
else:
|
|
46
|
+
self.display('Acquired')
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
def __exit__(self, _type, value, tb):
|
|
50
|
+
self.display('Releasing')
|
|
51
|
+
if self.acquired or not self.blocking:
|
|
52
|
+
fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN)
|
|
53
|
+
self.fp.close()
|
|
54
|
+
self.display('Released')
|
|
55
|
+
|
|
56
|
+
if self.acquired:
|
|
57
|
+
try:
|
|
58
|
+
os.unlink(self.lock_path)
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
pass # already deleted, or locked by another process
|
|
61
|
+
else:
|
|
62
|
+
self.display('Deleted')
|
|
@@ -15,9 +15,9 @@ class YAMLFormatter:
|
|
|
15
15
|
|
|
16
16
|
def __call__(self, key, value):
|
|
17
17
|
value = cli2.display.yaml_dump(value)
|
|
18
|
-
if
|
|
19
|
-
|
|
20
|
-
return
|
|
18
|
+
if self.colors:
|
|
19
|
+
value = cli2.display.yaml_highlight(value)
|
|
20
|
+
return '\n' + value
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def configure():
|