cli2 5.1.0rc1__tar.gz → 5.1.0rc2__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.
Files changed (54) hide show
  1. {cli2-5.1.0rc1/cli2.egg-info → cli2-5.1.0rc2}/PKG-INFO +1 -1
  2. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/__init__.py +1 -1
  3. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/log.py +51 -10
  4. {cli2-5.1.0rc1 → cli2-5.1.0rc2/cli2.egg-info}/PKG-INFO +1 -1
  5. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2.egg-info/SOURCES.txt +2 -0
  6. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/setup.py +1 -1
  7. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_client.py +50 -20
  8. cli2-5.1.0rc2/tests/test_client_test.py +17 -0
  9. cli2-5.1.0rc2/tests/test_log.py +80 -0
  10. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/MANIFEST.in +0 -0
  11. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/README.rst +0 -0
  12. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/classifiers.txt +0 -0
  13. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/asyncio.py +0 -0
  14. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/cli.py +0 -0
  15. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/cli2.py +0 -0
  16. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/colors.py +0 -0
  17. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/configuration.py +0 -0
  18. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/decorators.py +0 -0
  19. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/display.py +0 -0
  20. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/examples/__init__.py +0 -0
  21. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/examples/conf.py +0 -0
  22. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/examples/example.py +0 -0
  23. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/examples/example_obj.py +0 -0
  24. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/examples/nesting.py +0 -0
  25. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/examples/obj.py +0 -0
  26. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/examples/obj2.py +0 -0
  27. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/examples/test.py +0 -0
  28. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/lock.py +0 -0
  29. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/mask.py +0 -0
  30. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/node.py +0 -0
  31. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/sphinx.py +0 -0
  32. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/table.py +0 -0
  33. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2/test.py +0 -0
  34. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2.egg-info/dependency_links.txt +0 -0
  35. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2.egg-info/entry_points.txt +0 -0
  36. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2.egg-info/requires.txt +0 -0
  37. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/cli2.egg-info/top_level.txt +0 -0
  38. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/setup.cfg +0 -0
  39. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_ansible.py +0 -0
  40. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_ansible_variables.py +0 -0
  41. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_asyncio.py +0 -0
  42. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_cli.py +0 -0
  43. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_command.py +0 -0
  44. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_configuration.py +0 -0
  45. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_decorators.py +0 -0
  46. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_display.py +0 -0
  47. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_entry_point.py +0 -0
  48. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_group.py +0 -0
  49. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_inject.py +0 -0
  50. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_lock.py +0 -0
  51. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_mask.py +0 -0
  52. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_node.py +0 -0
  53. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_restful.py +0 -0
  54. {cli2-5.1.0rc1 → cli2-5.1.0rc2}/tests/test_table.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cli2
3
- Version: 5.1.0rc1
3
+ Version: 5.1.0rc2
4
4
  Summary: image:: https://yourlabs.io/oss/cli2/badges/master/pipeline.svg
5
5
  Home-page: https://yourlabs.io/oss/cli2
6
6
  Author: James Pic
@@ -20,7 +20,7 @@ except ImportError:
20
20
  """ windows """
21
21
  else:
22
22
  from .lock import Lock
23
- from .log import configure, log
23
+ from .log import configure, log, parse
24
24
  from .mask import Mask
25
25
  from .table import Table
26
26
 
@@ -64,6 +64,7 @@ import os
64
64
  import re
65
65
  import sys
66
66
  import structlog
67
+ import yaml
67
68
  from pathlib import Path
68
69
 
69
70
  import cli2.display
@@ -80,9 +81,15 @@ class YAMLFormatter:
80
81
  return '\n' + value
81
82
 
82
83
 
83
- def configure():
84
+ def configure(log_file=None):
85
+ """
86
+ Configure logging.
87
+
88
+ :param log_file: override for :envvar:`LOG_FILE`.
89
+ """
84
90
  LOG_LEVEL = os.getenv('LOG_LEVEL', 'WARNING').upper()
85
- LOG_FILE = os.getenv('LOG_FILE', 'auto').upper()
91
+ if log_file is None:
92
+ log_file = os.getenv('LOG_FILE', 'auto')
86
93
 
87
94
  if os.getenv('DEBUG'):
88
95
  LOG_LEVEL = 'DEBUG'
@@ -103,19 +110,19 @@ def configure():
103
110
  for arg in sys.argv
104
111
  ])[:155]
105
112
 
106
- if LOG_FILE == 'auto':
113
+ if log_file == 'auto':
107
114
  log_dir = Path(os.getenv("HOME")) / '.local/cli2/log'
108
115
  log_dir.mkdir(parents=True, exist_ok=True)
109
116
  ts = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
110
117
  file_name = f'{sys.argv[0].split("/")[-1]}-{ts}-{cmd}.log'
111
- LOG_FILE = log_dir / file_name
112
- elif LOG_FILE == 'none' or not LOG_FILE:
113
- LOG_FILE = None
118
+ log_file = log_dir / file_name
119
+ elif log_file == 'none' or not log_file:
120
+ log_file = None
114
121
  else:
115
- LOG_FILE = Path(LOG_FILE)
122
+ log_file = Path(log_file)
116
123
 
117
124
  handlers = ['default']
118
- if LOG_FILE:
125
+ if log_file:
119
126
  handlers.append('file')
120
127
 
121
128
  LOGGING = {
@@ -196,12 +203,12 @@ def configure():
196
203
  } for key in ('httpx', 'httpcore')
197
204
  })
198
205
 
199
- if LOG_FILE:
206
+ if log_file:
200
207
  LOGGING['handlers']['file'] = {
201
208
  'level': 'DEBUG',
202
209
  'class': 'logging.handlers.WatchedFileHandler',
203
210
  'formatter': 'plain',
204
- 'filename': str(LOG_FILE),
211
+ 'filename': str(log_file),
205
212
  }
206
213
 
207
214
  logging.config.dictConfig(LOGGING)
@@ -221,5 +228,39 @@ def configure():
221
228
  )
222
229
 
223
230
 
231
+ def parse(data):
232
+ """
233
+ Parse log file data into a list of entries.
234
+
235
+ :param data: Contents of a log file.
236
+ """
237
+ yaml_lines = []
238
+ entries = []
239
+ for line in data.split('\n'):
240
+ if 'event=' in line:
241
+ data = {}
242
+ for token in line.strip().split():
243
+ if match := re.match('^(\\w+)=(.*)', token):
244
+ key = match.group(1)
245
+ data[key] = match.group(2)
246
+ else:
247
+ data[key] += ' ' + token
248
+
249
+ if yaml_lines:
250
+ data['json'] = yaml.safe_load('\n'.join(yaml_lines))
251
+
252
+ if data['event'] == 'request':
253
+ entries.append(dict(request=data))
254
+ elif data['event'] == 'response':
255
+ entries[-1]['response'] = data
256
+ else:
257
+ entries.append(data)
258
+
259
+ yaml_lines = []
260
+ else:
261
+ yaml_lines.append(line)
262
+ return entries
263
+
264
+
224
265
  configure()
225
266
  log = structlog.get_logger('cli2')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cli2
3
- Version: 5.1.0rc1
3
+ Version: 5.1.0rc2
4
4
  Summary: image:: https://yourlabs.io/oss/cli2/badges/master/pipeline.svg
5
5
  Home-page: https://yourlabs.io/oss/cli2
6
6
  Author: James Pic
@@ -36,6 +36,7 @@ tests/test_ansible_variables.py
36
36
  tests/test_asyncio.py
37
37
  tests/test_cli.py
38
38
  tests/test_client.py
39
+ tests/test_client_test.py
39
40
  tests/test_command.py
40
41
  tests/test_configuration.py
41
42
  tests/test_decorators.py
@@ -44,6 +45,7 @@ tests/test_entry_point.py
44
45
  tests/test_group.py
45
46
  tests/test_inject.py
46
47
  tests/test_lock.py
48
+ tests/test_log.py
47
49
  tests/test_mask.py
48
50
  tests/test_node.py
49
51
  tests/test_restful.py
@@ -44,7 +44,7 @@ from setuptools import setup
44
44
 
45
45
  setup(
46
46
  name='cli2',
47
- version='5.1.0rc1',
47
+ version='5.1.0rc2',
48
48
  setup_requires='setupmeta',
49
49
  packages=['cli2'],
50
50
  install_requires=[
@@ -7,8 +7,10 @@ import mock
7
7
  import pytest
8
8
 
9
9
 
10
- async def _response(**kwargs):
11
- return httpx.Response(**kwargs)
10
+ def _response(**kwargs):
11
+ response = httpx.Response(**kwargs)
12
+ response.request = httpx.Request('POST', '/', json=[1])
13
+ return response
12
14
 
13
15
 
14
16
  class HandlerSentinel(chttpx.Handler):
@@ -17,7 +19,11 @@ class HandlerSentinel(chttpx.Handler):
17
19
  self.calls = []
18
20
 
19
21
  async def __call__(self, client, response, tries, log):
20
- self.calls.append((client, response.status_code, tries))
22
+ self.calls.append((
23
+ client,
24
+ getattr(response, 'status_code', response),
25
+ tries,
26
+ ))
21
27
  return await super().__call__(client, response, tries, log)
22
28
 
23
29
 
@@ -191,6 +197,7 @@ async def test_error_remote(httpx_mock, client_class):
191
197
  return 'token'
192
198
 
193
199
  client = TokenClient()
200
+ client.handler.tries = 2
194
201
  httpx_mock.add_response(url='http://lol', json=[1])
195
202
 
196
203
  async def raises(*a, **k):
@@ -230,10 +237,11 @@ async def test_client_handler(httpx_mock, client_class):
230
237
  class Client(client_class):
231
238
  def client_factory(self):
232
239
  client = super().client_factory()
233
- client.send = mock.Mock()
240
+ client.send = mock.AsyncMock()
234
241
  return client
235
242
 
236
243
  client = Client(handler=HandlerSentinel())
244
+ client.handler.tries = 3
237
245
 
238
246
  # test response retry
239
247
  client.client.send.side_effect = [
@@ -247,16 +255,33 @@ async def test_client_handler(httpx_mock, client_class):
247
255
  (client, 200, 1),
248
256
  ]
249
257
 
250
- # test TransportError retry
251
- client.client.send.side_effect = [
252
- httpx.TransportError("foo"),
253
- _response(status_code=200),
254
- ]
258
+
259
+ @pytest.mark.asyncio
260
+ async def test_client_error_retry(httpx_mock, client_class):
261
+ class Client(client_class):
262
+ calls = 0
263
+
264
+ def client_factory(self):
265
+ self.calls += 1
266
+
267
+ client = super().client_factory()
268
+ client.send = mock.AsyncMock()
269
+
270
+ if self.calls == 1:
271
+ client.send.side_effect = httpx.TransportError('foo')
272
+ elif self.calls == 2:
273
+ client.send.return_value = _response(status_code=200)
274
+
275
+ return client
276
+
277
+ client = Client(handler=HandlerSentinel())
278
+ client.handler.tries = 3
279
+
280
+ response = await client.request('GET', '/')
255
281
  assert response.status_code == 200
256
- assert client.handler.calls == [
257
- (client, 500, 0),
258
- (client, 200, 1),
259
- ]
282
+ assert isinstance(client.handler.calls[0][1], httpx.TransportError)
283
+ assert client.handler.calls[1][1] == 200
284
+ assert len(client.handler.calls) == 2
260
285
 
261
286
 
262
287
  @pytest.mark.asyncio
@@ -265,13 +290,19 @@ async def test_handler(client_class):
265
290
  client = client_class()
266
291
  client.client_reset = mock.AsyncMock()
267
292
  client.token_reset = mock.AsyncMock()
268
- handler = chttpx.Handler(accepts=[201], refuses=[218], retokens=[418])
293
+ handler = chttpx.Handler(
294
+ accepts=[201],
295
+ refuses=[218],
296
+ retokens=[418],
297
+ tries=3,
298
+ )
269
299
 
270
300
  response = httpx.Response(status_code=201)
271
301
  result = await handler(client, response, 0, log)
272
302
  assert result == response
273
303
 
274
304
  response = httpx.Response(status_code=200)
305
+ response.request = httpx.Request('POST', '/', json=[1])
275
306
  result = await handler(client, response, 0, log)
276
307
  log.info.assert_called_once_with(
277
308
  'retry', status_code=200, tries=0, sleep=.0
@@ -286,7 +317,7 @@ async def test_handler(client_class):
286
317
  'retry', status_code=200, tries=0, sleep=.0
287
318
  )
288
319
 
289
- msg = 'Unacceptable response <Response [200 OK]> after 31 tries\n\x1b[0m\x1b[1mPOST /\x1b[0m\n-\x1b[37m \x1b[39;49;00m1\x1b[37m\x1b[39;49;00m\n\n\x1b[1mHTTP 200\x1b[0m\n-\x1b[37m \x1b[39;49;00m2\x1b[37m\x1b[39;49;00m\n' # noqa
320
+ msg = 'Unacceptable response <Response [200 OK]> after 4 tries\n\x1b[0m\x1b[1mPOST /\x1b[0m\n-\x1b[37m \x1b[39;49;00m1\x1b[37m\x1b[39;49;00m\n\n\x1b[1mHTTP 200\x1b[0m\n-\x1b[37m \x1b[39;49;00m2\x1b[37m\x1b[39;49;00m\n' # noqa
290
321
  assert str(exc.value) == msg
291
322
 
292
323
  response = httpx.Response(status_code=200)
@@ -294,7 +325,7 @@ async def test_handler(client_class):
294
325
  with pytest.raises(chttpx.RetriesExceededError) as exc:
295
326
  await handler(client, response, handler.tries + 1, log)
296
327
 
297
- msg = 'Unacceptable response <Response [200 OK]> after 31 tries\n\x1b[0m\x1b[1mGET /\x1b[0m\n\x1b[1mHTTP 200\x1b[0m' # noqa
328
+ msg = 'Unacceptable response <Response [200 OK]> after 4 tries\n\x1b[0m\x1b[1mGET /\x1b[0m\n\x1b[1mHTTP 200\x1b[0m' # noqa
298
329
  assert str(exc.value) == msg
299
330
 
300
331
  response = httpx.Response(status_code=218)
@@ -335,9 +366,7 @@ async def test_handler(client_class):
335
366
  assert not client.token_reset.await_count
336
367
  log.warn.reset_mock()
337
368
  result = await handler(client, response, 0, log)
338
- log.warn.assert_called_once_with(
339
- 'retoken', status_code=418, tries=0, sleep=.0
340
- )
369
+ log.warn.assert_called_once_with('retoken')
341
370
  assert not result
342
371
  assert client.token_reset.await_count == 1
343
372
 
@@ -358,10 +387,11 @@ async def test_retry(httpx_mock, client_class):
358
387
 
359
388
  def client_factory(self):
360
389
  client = super().client_factory()
361
- client.send = mock.Mock()
390
+ client.send = mock.AsyncMock()
362
391
  return client
363
392
 
364
393
  client = Client()
394
+ client.handler.tries = 3
365
395
 
366
396
  current_client = client.client
367
397
  client.client.send.side_effect = [
@@ -0,0 +1,17 @@
1
+ import chttpx
2
+ import cli2
3
+ import pytest
4
+ from chttpx.example import APIClient
5
+
6
+
7
+ @pytest.mark.chttpx_mock
8
+ def test_object_story():
9
+ test_name = 'test33312'
10
+ obj = APIClient.cli['object']['create'](f'name={test_name}')
11
+ assert obj.name == test_name
12
+
13
+ cli2.log.info('bogus')
14
+
15
+ with pytest.raises(chttpx.RefusedResponseError):
16
+ APIClient.cli['object']['create'](f'name={test_name}')
17
+ result = APIClient.cli['object']['delete'](f'{obj.id}')
@@ -0,0 +1,80 @@
1
+ import cli2
2
+
3
+
4
+ def test_log_parse():
5
+ logs = '''
6
+
7
+ name: tes2980898zzzyzy7
8
+ method=POST url=http://localhost:8000/objects/ event=request level=debug timestamp=2025-03-21 10:09:40
9
+
10
+ data: {}
11
+ id: 103
12
+ name: tes2980898zzzyzy7
13
+ method=POST url=http://localhost:8000/objects/ status_code=201 event=response level=info timestamp=2025-03-21 10:09:40
14
+ event=bogus level=info
15
+
16
+ name: tes2980898zzzyzy7
17
+ method=POST url=http://localhost:8000/objects/ event=request level=debug timestamp=2025-03-21 10:09:40
18
+
19
+ name:
20
+ - object with this name already exists.
21
+ method=POST url=http://localhost:8000/objects/ status_code=400 event=response level=info timestamp=2025-03-21 10:09:40
22
+ '''
23
+
24
+ result = cli2.parse(logs)
25
+ assert result == [
26
+ {
27
+ 'request': {
28
+ 'event': 'request',
29
+ 'json': {
30
+ 'name': 'tes2980898zzzyzy7',
31
+ },
32
+ 'level': 'debug',
33
+ 'method': 'POST',
34
+ 'timestamp': '2025-03-21 10:09:40',
35
+ 'url': 'http://localhost:8000/objects/',
36
+ },
37
+ 'response': {
38
+ 'event': 'response',
39
+ 'json': {
40
+ 'data': {},
41
+ 'id': 103,
42
+ 'name': 'tes2980898zzzyzy7',
43
+ },
44
+ 'level': 'info',
45
+ 'method': 'POST',
46
+ 'status_code': '201',
47
+ 'timestamp': '2025-03-21 10:09:40',
48
+ 'url': 'http://localhost:8000/objects/',
49
+ },
50
+ },
51
+ {
52
+ 'event': 'bogus',
53
+ 'level': 'info',
54
+ },
55
+ {
56
+ 'request': {
57
+ 'event': 'request',
58
+ 'json': {
59
+ 'name': 'tes2980898zzzyzy7',
60
+ },
61
+ 'level': 'debug',
62
+ 'method': 'POST',
63
+ 'timestamp': '2025-03-21 10:09:40',
64
+ 'url': 'http://localhost:8000/objects/',
65
+ },
66
+ 'response': {
67
+ 'event': 'response',
68
+ 'json': {
69
+ 'name': [
70
+ 'object with this name already exists.',
71
+ ],
72
+ },
73
+ 'level': 'info',
74
+ 'method': 'POST',
75
+ 'status_code': '400',
76
+ 'timestamp': '2025-03-21 10:09:40',
77
+ 'url': 'http://localhost:8000/objects/',
78
+ },
79
+ },
80
+ ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes