cli2 3.3.46__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.46/cli2.egg-info → cli2-3.4.0}/PKG-INFO +1 -1
- {cli2-3.3.46 → cli2-3.4.0}/cli2/__init__.py +1 -1
- cli2-3.4.0/cli2/ansible.py +273 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/client.py +74 -8
- {cli2-3.3.46 → cli2-3.4.0}/cli2/display.py +14 -2
- {cli2-3.3.46 → cli2-3.4.0}/cli2/example_client.py +11 -3
- cli2-3.4.0/cli2/locker.py +62 -0
- {cli2-3.3.46 → 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.46 → cli2-3.4.0}/cli2/test_client.py +132 -122
- cli2-3.4.0/cli2/test_restful.py +92 -0
- {cli2-3.3.46 → cli2-3.4.0/cli2.egg-info}/PKG-INFO +1 -1
- {cli2-3.3.46 → cli2-3.4.0}/cli2.egg-info/SOURCES.txt +5 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2.egg-info/entry_points.txt +3 -0
- {cli2-3.3.46 → cli2-3.4.0}/setup.py +4 -1
- {cli2-3.3.46 → cli2-3.4.0}/MANIFEST.in +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/README.rst +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/classifiers.txt +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/argument.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/asyncio.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/cli.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/colors.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/command.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/configuration.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/decorators.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/entry_point.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/example_client_complex.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/example_nesting.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/example_obj.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/group.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/node.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/overrides.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/sphinx.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/table.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/test.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/test_cli.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/test_command.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/test_configuration.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/test_decorators.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/test_display.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/test_entry_point.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/test_group.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/test_inject.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/test_node.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2/test_table.py +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2.egg-info/requires.txt +0 -0
- {cli2-3.3.46 → cli2-3.4.0}/cli2.egg-info/top_level.txt +0 -0
- {cli2-3.3.46 → 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
|
|
|
@@ -678,6 +690,8 @@ class ModelGroup(Group):
|
|
|
678
690
|
|
|
679
691
|
class ModelMetaclass(type):
|
|
680
692
|
def __new__(cls, name, bases, attributes):
|
|
693
|
+
if 'Paginator' in attributes:
|
|
694
|
+
attributes['paginator'] = attributes['Paginator']
|
|
681
695
|
cls = super().__new__(cls, name, bases, attributes)
|
|
682
696
|
client = getattr(cls, 'client', None)
|
|
683
697
|
if client:
|
|
@@ -769,9 +783,13 @@ class Model(metaclass=ModelMetaclass):
|
|
|
769
783
|
self._dirty_fields = []
|
|
770
784
|
self._field_cache = dict()
|
|
771
785
|
|
|
786
|
+
self.changed_fields = dict()
|
|
772
787
|
for key, value in values.items():
|
|
773
788
|
setattr(self, key, value)
|
|
774
789
|
|
|
790
|
+
# actually reset that
|
|
791
|
+
self.changed_fields = dict()
|
|
792
|
+
|
|
775
793
|
@property
|
|
776
794
|
def data(self):
|
|
777
795
|
"""
|
|
@@ -789,6 +807,10 @@ class Model(metaclass=ModelMetaclass):
|
|
|
789
807
|
def data(self, value):
|
|
790
808
|
self._data = value
|
|
791
809
|
|
|
810
|
+
@property
|
|
811
|
+
def data_masked(self):
|
|
812
|
+
return self.client.mask_data(self.data)
|
|
813
|
+
|
|
792
814
|
@classmethod
|
|
793
815
|
@hide('expressions')
|
|
794
816
|
@cmd(color='green', condition=lambda cls: cls.url_list)
|
|
@@ -866,6 +888,7 @@ class Model(metaclass=ModelMetaclass):
|
|
|
866
888
|
"""
|
|
867
889
|
response = await self.client.get(self.url)
|
|
868
890
|
self.data.update(response.json())
|
|
891
|
+
self.changed_fields = dict()
|
|
869
892
|
|
|
870
893
|
async def save(self):
|
|
871
894
|
"""
|
|
@@ -919,6 +942,8 @@ class Model(metaclass=ModelMetaclass):
|
|
|
919
942
|
|
|
920
943
|
class ClientMetaclass(type):
|
|
921
944
|
def __new__(cls, name, bases, attributes):
|
|
945
|
+
if 'Paginator' in attributes:
|
|
946
|
+
attributes['paginator'] = attributes['Paginator']
|
|
922
947
|
cls = super().__new__(cls, name, bases, attributes)
|
|
923
948
|
|
|
924
949
|
# bind ourself as _client_class to any inherited model
|
|
@@ -1046,13 +1071,52 @@ class ClientError(Exception):
|
|
|
1046
1071
|
|
|
1047
1072
|
|
|
1048
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
|
+
"""
|
|
1049
1097
|
def __init__(self, client, response, tries, mask, msg=None):
|
|
1050
1098
|
self.client = client
|
|
1051
1099
|
self.response = response
|
|
1052
1100
|
self.tries = tries
|
|
1053
1101
|
self.mask = mask
|
|
1054
|
-
msg = msg or getattr(self, 'msg', '').format(self=self)
|
|
1055
|
-
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)
|
|
1056
1120
|
|
|
1057
1121
|
def enhance(self, msg):
|
|
1058
1122
|
"""
|
|
@@ -1444,7 +1508,9 @@ class Client(metaclass=ClientMetaclass):
|
|
|
1444
1508
|
|
|
1445
1509
|
return response
|
|
1446
1510
|
|
|
1447
|
-
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
|
|
1448
1514
|
try:
|
|
1449
1515
|
data = response.json()
|
|
1450
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():
|