cli2 5.0.3__tar.gz → 5.1.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.
Files changed (54) hide show
  1. {cli2-5.0.3/cli2.egg-info → cli2-5.1.0}/PKG-INFO +1 -1
  2. {cli2-5.0.3 → cli2-5.1.0}/cli2/__init__.py +1 -1
  3. {cli2-5.0.3 → cli2-5.1.0}/cli2/log.py +78 -14
  4. {cli2-5.0.3 → cli2-5.1.0/cli2.egg-info}/PKG-INFO +1 -1
  5. {cli2-5.0.3 → cli2-5.1.0}/cli2.egg-info/SOURCES.txt +2 -0
  6. {cli2-5.0.3 → cli2-5.1.0}/setup.py +1 -1
  7. {cli2-5.0.3 → cli2-5.1.0}/tests/test_client.py +104 -22
  8. cli2-5.1.0/tests/test_client_test.py +17 -0
  9. cli2-5.1.0/tests/test_log.py +80 -0
  10. {cli2-5.0.3 → cli2-5.1.0}/MANIFEST.in +0 -0
  11. {cli2-5.0.3 → cli2-5.1.0}/README.rst +0 -0
  12. {cli2-5.0.3 → cli2-5.1.0}/classifiers.txt +0 -0
  13. {cli2-5.0.3 → cli2-5.1.0}/cli2/asyncio.py +0 -0
  14. {cli2-5.0.3 → cli2-5.1.0}/cli2/cli.py +0 -0
  15. {cli2-5.0.3 → cli2-5.1.0}/cli2/cli2.py +0 -0
  16. {cli2-5.0.3 → cli2-5.1.0}/cli2/colors.py +0 -0
  17. {cli2-5.0.3 → cli2-5.1.0}/cli2/configuration.py +0 -0
  18. {cli2-5.0.3 → cli2-5.1.0}/cli2/decorators.py +0 -0
  19. {cli2-5.0.3 → cli2-5.1.0}/cli2/display.py +0 -0
  20. {cli2-5.0.3 → cli2-5.1.0}/cli2/examples/__init__.py +0 -0
  21. {cli2-5.0.3 → cli2-5.1.0}/cli2/examples/conf.py +0 -0
  22. {cli2-5.0.3 → cli2-5.1.0}/cli2/examples/example.py +0 -0
  23. {cli2-5.0.3 → cli2-5.1.0}/cli2/examples/example_obj.py +0 -0
  24. {cli2-5.0.3 → cli2-5.1.0}/cli2/examples/nesting.py +0 -0
  25. {cli2-5.0.3 → cli2-5.1.0}/cli2/examples/obj.py +0 -0
  26. {cli2-5.0.3 → cli2-5.1.0}/cli2/examples/obj2.py +0 -0
  27. {cli2-5.0.3 → cli2-5.1.0}/cli2/examples/test.py +0 -0
  28. {cli2-5.0.3 → cli2-5.1.0}/cli2/lock.py +0 -0
  29. {cli2-5.0.3 → cli2-5.1.0}/cli2/mask.py +0 -0
  30. {cli2-5.0.3 → cli2-5.1.0}/cli2/node.py +0 -0
  31. {cli2-5.0.3 → cli2-5.1.0}/cli2/sphinx.py +0 -0
  32. {cli2-5.0.3 → cli2-5.1.0}/cli2/table.py +0 -0
  33. {cli2-5.0.3 → cli2-5.1.0}/cli2/test.py +0 -0
  34. {cli2-5.0.3 → cli2-5.1.0}/cli2.egg-info/dependency_links.txt +0 -0
  35. {cli2-5.0.3 → cli2-5.1.0}/cli2.egg-info/entry_points.txt +0 -0
  36. {cli2-5.0.3 → cli2-5.1.0}/cli2.egg-info/requires.txt +0 -0
  37. {cli2-5.0.3 → cli2-5.1.0}/cli2.egg-info/top_level.txt +0 -0
  38. {cli2-5.0.3 → cli2-5.1.0}/setup.cfg +0 -0
  39. {cli2-5.0.3 → cli2-5.1.0}/tests/test_ansible.py +0 -0
  40. {cli2-5.0.3 → cli2-5.1.0}/tests/test_ansible_variables.py +0 -0
  41. {cli2-5.0.3 → cli2-5.1.0}/tests/test_asyncio.py +0 -0
  42. {cli2-5.0.3 → cli2-5.1.0}/tests/test_cli.py +0 -0
  43. {cli2-5.0.3 → cli2-5.1.0}/tests/test_command.py +0 -0
  44. {cli2-5.0.3 → cli2-5.1.0}/tests/test_configuration.py +0 -0
  45. {cli2-5.0.3 → cli2-5.1.0}/tests/test_decorators.py +0 -0
  46. {cli2-5.0.3 → cli2-5.1.0}/tests/test_display.py +0 -0
  47. {cli2-5.0.3 → cli2-5.1.0}/tests/test_entry_point.py +0 -0
  48. {cli2-5.0.3 → cli2-5.1.0}/tests/test_group.py +0 -0
  49. {cli2-5.0.3 → cli2-5.1.0}/tests/test_inject.py +0 -0
  50. {cli2-5.0.3 → cli2-5.1.0}/tests/test_lock.py +0 -0
  51. {cli2-5.0.3 → cli2-5.1.0}/tests/test_mask.py +0 -0
  52. {cli2-5.0.3 → cli2-5.1.0}/tests/test_node.py +0 -0
  53. {cli2-5.0.3 → cli2-5.1.0}/tests/test_restful.py +0 -0
  54. {cli2-5.0.3 → cli2-5.1.0}/tests/test_table.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cli2
3
- Version: 5.0.3
3
+ Version: 5.1.0
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
 
@@ -37,6 +37,16 @@ variables.
37
37
 
38
38
  Setting this to ``INFO``, ``DEBUG``, or any other log level is safe.
39
39
 
40
+ .. envvar:: LOG_FILE
41
+
42
+ Path to log file to use, with a couple of special values:
43
+
44
+ - if ``LOG_FILE=auto``, then a path will be calculated in
45
+ ``~/.local/cli2/log``,
46
+ - if ``LOG_FILE=none``, then there will be no file logging.
47
+
48
+ Default: ``auto``
49
+
40
50
  .. envvar:: DEBUG
41
51
 
42
52
  Setting this will set :envvar:`LOG_LEVEL` to `DEBUG`, but also activate
@@ -54,6 +64,7 @@ import os
54
64
  import re
55
65
  import sys
56
66
  import structlog
67
+ import yaml
57
68
  from pathlib import Path
58
69
 
59
70
  import cli2.display
@@ -70,8 +81,15 @@ class YAMLFormatter:
70
81
  return '\n' + value
71
82
 
72
83
 
73
- def configure():
84
+ def configure(log_file=None):
85
+ """
86
+ Configure logging.
87
+
88
+ :param log_file: override for :envvar:`LOG_FILE`.
89
+ """
74
90
  LOG_LEVEL = os.getenv('LOG_LEVEL', 'WARNING').upper()
91
+ if log_file is None:
92
+ log_file = os.getenv('LOG_FILE', 'auto')
75
93
 
76
94
  if os.getenv('DEBUG'):
77
95
  LOG_LEVEL = 'DEBUG'
@@ -92,11 +110,21 @@ def configure():
92
110
  for arg in sys.argv
93
111
  ])[:155]
94
112
 
95
- log_dir = Path(os.getenv("HOME")) / '.local/cli2/log'
96
- log_dir.mkdir(parents=True, exist_ok=True)
97
- ts = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
98
- file_name = f'{sys.argv[0].split("/")[-1]}-{ts}-{cmd}.log'
99
- file_path = log_dir / file_name
113
+ if log_file == 'auto':
114
+ log_dir = Path(os.getenv("HOME")) / '.local/cli2/log'
115
+ log_dir.mkdir(parents=True, exist_ok=True)
116
+ ts = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
117
+ file_name = f'{sys.argv[0].split("/")[-1]}-{ts}-{cmd}.log'
118
+ log_file = log_dir / file_name
119
+ elif log_file == 'none' or not log_file:
120
+ log_file = None
121
+ else:
122
+ log_file = Path(log_file)
123
+
124
+ handlers = ['default']
125
+ if log_file:
126
+ handlers.append('file')
127
+
100
128
  LOGGING = {
101
129
  'version': 1,
102
130
  'disable_existing_loggers': True,
@@ -156,16 +184,10 @@ def configure():
156
184
  'class': 'logging.StreamHandler',
157
185
  'formatter': 'colored',
158
186
  },
159
- 'file': {
160
- 'level': 'DEBUG',
161
- 'class': 'logging.handlers.WatchedFileHandler',
162
- 'formatter': 'plain',
163
- 'filename': str(file_path),
164
- },
165
187
  },
166
188
  'loggers': {
167
189
  'cli2': {
168
- 'handlers': ['default', 'file'],
190
+ 'handlers': handlers,
169
191
  'level': 'DEBUG',
170
192
  'propagate': True,
171
193
  }
@@ -175,12 +197,20 @@ def configure():
175
197
  if os.getenv('HTTP_DEBUG'):
176
198
  LOGGING['loggers'].update({
177
199
  key: {
178
- 'handlers': ['default', 'file'],
200
+ 'handlers': handlers,
179
201
  'level': 'DEBUG',
180
202
  'propagate': True,
181
203
  } for key in ('httpx', 'httpcore')
182
204
  })
183
205
 
206
+ if log_file:
207
+ LOGGING['handlers']['file'] = {
208
+ 'level': 'DEBUG',
209
+ 'class': 'logging.handlers.WatchedFileHandler',
210
+ 'formatter': 'plain',
211
+ 'filename': str(log_file),
212
+ }
213
+
184
214
  logging.config.dictConfig(LOGGING)
185
215
 
186
216
  structlog.configure(
@@ -198,5 +228,39 @@ def configure():
198
228
  )
199
229
 
200
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
+
201
265
  configure()
202
266
  log = structlog.get_logger('cli2')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cli2
3
- Version: 5.0.3
3
+ Version: 5.1.0
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.0.3',
47
+ version='5.1.0',
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 = [
@@ -645,10 +675,11 @@ def test_descriptor(client_class):
645
675
  assert model.data['undeclared']['foo'] == 3
646
676
 
647
677
  model = Model()
648
- assert model.foo == ''
678
+ assert model.foo is None
679
+ assert 'foo' not in model._data
649
680
  assert not model.changed_fields
650
681
  model.foo = 'bar'
651
- assert model.changed_fields == dict(foo='')
682
+ assert model.changed_fields == dict(foo=None)
652
683
  model = Model(foo='bar')
653
684
  model.foo = 'foo'
654
685
  assert model.changed_fields == dict(foo='bar')
@@ -1246,3 +1277,54 @@ def test_client_command(client_class, httpx_mock):
1246
1277
  cmd = Client.cli['request']
1247
1278
  cmd()
1248
1279
  assert not Client.post_call_called
1280
+
1281
+
1282
+ def test_field_callback(client_class):
1283
+ class TestModel(client_class.Model):
1284
+ foo = chttpx.Field()
1285
+ bar = chttpx.Field()
1286
+ other = chttpx.Field()
1287
+
1288
+ @other.factory
1289
+ def other_factory(self):
1290
+ return 'nice'
1291
+
1292
+ @bar.factory(foo)
1293
+ def bar_factory(self):
1294
+ return f'bar{self.foo}'
1295
+
1296
+ model = client_class().TestModel(foo='test')
1297
+ assert model.bar == 'bartest'
1298
+ assert model.other == 'nice'
1299
+
1300
+ model = client_class().TestModel(foo='test')
1301
+ assert model.data['bar'] == 'bartest'
1302
+ assert model.data['other'] == 'nice'
1303
+
1304
+ model = client_class().TestModel()
1305
+ assert model.data == dict(other='nice')
1306
+
1307
+ model = client_class().TestModel(bar='lol')
1308
+ assert model.bar == 'lol'
1309
+ assert model.data['bar'] == 'lol'
1310
+
1311
+
1312
+ def test_field_callback_recursion(client_class):
1313
+ class TestModel(client_class.Model):
1314
+ final = chttpx.Field()
1315
+ req2 = chttpx.Field()
1316
+ req1 = chttpx.Field()
1317
+
1318
+ @final.factory(req2)
1319
+ def final_factory(self):
1320
+ return f'{self.req2}val3'
1321
+
1322
+ @req2.factory(req1)
1323
+ def req2_factory(self):
1324
+ return f'{self.req1}val2'
1325
+
1326
+ model = client_class().TestModel(req1='lol')
1327
+ assert model.data['final'] == 'lolval2val3'
1328
+
1329
+ model = client_class().TestModel(req1='lol')
1330
+ assert model.final == 'lolval2val3'
@@ -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
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