csu 2.3.6__tar.gz → 2.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.
Files changed (108) hide show
  1. {csu-2.3.6 → csu-2.4.0}/.bumpversion.cfg +1 -1
  2. {csu-2.3.6 → csu-2.4.0}/.cookiecutterrc +1 -1
  3. {csu-2.3.6 → csu-2.4.0}/PKG-INFO +3 -3
  4. {csu-2.3.6 → csu-2.4.0}/README.rst +2 -2
  5. {csu-2.3.6 → csu-2.4.0}/pyproject.toml +1 -1
  6. {csu-2.3.6 → csu-2.4.0}/src/csu/__init__.py +1 -1
  7. {csu-2.3.6 → csu-2.4.0}/src/csu/service.py +42 -37
  8. {csu-2.3.6 → csu-2.4.0}/src/csu.egg-info/PKG-INFO +3 -3
  9. {csu-2.3.6 → csu-2.4.0}/src/csu.egg-info/SOURCES.txt +2 -0
  10. csu-2.4.0/tests/cassettes/test_service_auth/test_async_auth.yaml +110 -0
  11. csu-2.4.0/tests/cassettes/test_service_auth/test_sync_auth.yaml +146 -0
  12. {csu-2.3.6 → csu-2.4.0}/tests/test_service.py +15 -36
  13. {csu-2.3.6 → csu-2.4.0}/tests/test_service_auth.py +102 -0
  14. {csu-2.3.6 → csu-2.4.0}/.coveragerc +0 -0
  15. {csu-2.3.6 → csu-2.4.0}/.editorconfig +0 -0
  16. {csu-2.3.6 → csu-2.4.0}/.github/workflows/github-actions.yml +0 -0
  17. {csu-2.3.6 → csu-2.4.0}/.pre-commit-config.yaml +0 -0
  18. {csu-2.3.6 → csu-2.4.0}/.tombi.toml +0 -0
  19. {csu-2.3.6 → csu-2.4.0}/AUTHORS.rst +0 -0
  20. {csu-2.3.6 → csu-2.4.0}/CHANGELOG.rst +0 -0
  21. {csu-2.3.6 → csu-2.4.0}/CONTRIBUTING.rst +0 -0
  22. {csu-2.3.6 → csu-2.4.0}/LICENSE +0 -0
  23. {csu-2.3.6 → csu-2.4.0}/MANIFEST.in +0 -0
  24. {csu-2.3.6 → csu-2.4.0}/ci/bootstrap.py +0 -0
  25. {csu-2.3.6 → csu-2.4.0}/ci/requirements.txt +0 -0
  26. {csu-2.3.6 → csu-2.4.0}/ci/templates/.github/workflows/github-actions.yml +0 -0
  27. {csu-2.3.6 → csu-2.4.0}/pytest.ini +0 -0
  28. {csu-2.3.6 → csu-2.4.0}/setup.cfg +0 -0
  29. {csu-2.3.6 → csu-2.4.0}/src/csu/admin.py +0 -0
  30. {csu-2.3.6 → csu-2.4.0}/src/csu/conf.py +0 -0
  31. {csu-2.3.6 → csu-2.4.0}/src/csu/consts.py +0 -0
  32. {csu-2.3.6 → csu-2.4.0}/src/csu/drf/__init__.py +0 -0
  33. {csu-2.3.6 → csu-2.4.0}/src/csu/drf/auth.py +0 -0
  34. {csu-2.3.6 → csu-2.4.0}/src/csu/drf/fields.py +0 -0
  35. {csu-2.3.6 → csu-2.4.0}/src/csu/drf/forms.py +0 -0
  36. {csu-2.3.6 → csu-2.4.0}/src/csu/drf/perms.py +0 -0
  37. {csu-2.3.6 → csu-2.4.0}/src/csu/drf/phonenumber.py +0 -0
  38. {csu-2.3.6 → csu-2.4.0}/src/csu/drf/serializers.py +0 -0
  39. {csu-2.3.6 → csu-2.4.0}/src/csu/drf/test_auth.py +0 -0
  40. {csu-2.3.6 → csu-2.4.0}/src/csu/drf/test_fields.py +0 -0
  41. {csu-2.3.6 → csu-2.4.0}/src/csu/drf/test_forms.py +0 -0
  42. {csu-2.3.6 → csu-2.4.0}/src/csu/drf/test_phonenumber.py +0 -0
  43. {csu-2.3.6 → csu-2.4.0}/src/csu/drf/test_serializers.py +0 -0
  44. {csu-2.3.6 → csu-2.4.0}/src/csu/drf/views.py +0 -0
  45. {csu-2.3.6 → csu-2.4.0}/src/csu/enums.py +0 -0
  46. {csu-2.3.6 → csu-2.4.0}/src/csu/env.py +0 -0
  47. {csu-2.3.6 → csu-2.4.0}/src/csu/exceptions.py +0 -0
  48. {csu-2.3.6 → csu-2.4.0}/src/csu/export.py +0 -0
  49. {csu-2.3.6 → csu-2.4.0}/src/csu/fixups.py +0 -0
  50. {csu-2.3.6 → csu-2.4.0}/src/csu/forms/__init__.py +0 -0
  51. {csu-2.3.6 → csu-2.4.0}/src/csu/forms/crispy.py +0 -0
  52. {csu-2.3.6 → csu-2.4.0}/src/csu/forms/fields.py +0 -0
  53. {csu-2.3.6 → csu-2.4.0}/src/csu/gettext.py +0 -0
  54. {csu-2.3.6 → csu-2.4.0}/src/csu/gettext_lazy.py +0 -0
  55. {csu-2.3.6 → csu-2.4.0}/src/csu/locale/ro/LC_MESSAGES/django.mo +0 -0
  56. {csu-2.3.6 → csu-2.4.0}/src/csu/locale/ro/LC_MESSAGES/django.po +0 -0
  57. {csu-2.3.6 → csu-2.4.0}/src/csu/logging.py +0 -0
  58. {csu-2.3.6 → csu-2.4.0}/src/csu/management.py +0 -0
  59. {csu-2.3.6 → csu-2.4.0}/src/csu/models.py +0 -0
  60. {csu-2.3.6 → csu-2.4.0}/src/csu/query.py +0 -0
  61. {csu-2.3.6 → csu-2.4.0}/src/csu/routers.py +0 -0
  62. {csu-2.3.6 → csu-2.4.0}/src/csu/templates/api_exception.html +0 -0
  63. {csu-2.3.6 → csu-2.4.0}/src/csu/templates/forms/widgets/opaquewidget.html +0 -0
  64. {csu-2.3.6 → csu-2.4.0}/src/csu/test_consts.py +0 -0
  65. {csu-2.3.6 → csu-2.4.0}/src/csu/test_env.py +0 -0
  66. {csu-2.3.6 → csu-2.4.0}/src/csu/test_exceptions.py +0 -0
  67. {csu-2.3.6 → csu-2.4.0}/src/csu/test_logging.py +0 -0
  68. {csu-2.3.6 → csu-2.4.0}/src/csu/test_management.py +0 -0
  69. {csu-2.3.6 → csu-2.4.0}/src/csu/test_service.py +0 -0
  70. {csu-2.3.6 → csu-2.4.0}/src/csu/test_timezones.py +0 -0
  71. {csu-2.3.6 → csu-2.4.0}/src/csu/test_utils.py +0 -0
  72. {csu-2.3.6 → csu-2.4.0}/src/csu/test_xml.py +0 -0
  73. {csu-2.3.6 → csu-2.4.0}/src/csu/timezones.py +0 -0
  74. {csu-2.3.6 → csu-2.4.0}/src/csu/utils.py +0 -0
  75. {csu-2.3.6 → csu-2.4.0}/src/csu/views.py +0 -0
  76. {csu-2.3.6 → csu-2.4.0}/src/csu/worker/__init__.py +0 -0
  77. {csu-2.3.6 → csu-2.4.0}/src/csu/worker/admin.py +0 -0
  78. {csu-2.3.6 → csu-2.4.0}/src/csu/worker/asgi.py +0 -0
  79. {csu-2.3.6 → csu-2.4.0}/src/csu/worker/engine.py +0 -0
  80. {csu-2.3.6 → csu-2.4.0}/src/csu/worker/enums.py +0 -0
  81. {csu-2.3.6 → csu-2.4.0}/src/csu/worker/job.py +0 -0
  82. {csu-2.3.6 → csu-2.4.0}/src/csu/worker/models.py +0 -0
  83. {csu-2.3.6 → csu-2.4.0}/src/csu/worker/registry.py +0 -0
  84. {csu-2.3.6 → csu-2.4.0}/src/csu/wsgi.py +0 -0
  85. {csu-2.3.6 → csu-2.4.0}/src/csu/xml.py +0 -0
  86. {csu-2.3.6 → csu-2.4.0}/src/csu.egg-info/dependency_links.txt +0 -0
  87. {csu-2.3.6 → csu-2.4.0}/src/csu.egg-info/requires.txt +0 -0
  88. {csu-2.3.6 → csu-2.4.0}/src/csu.egg-info/top_level.txt +0 -0
  89. {csu-2.3.6 → csu-2.4.0}/tests/cassettes/test_service/test_custom_context.yaml +0 -0
  90. {csu-2.3.6 → csu-2.4.0}/tests/cassettes/test_service/test_custom_context_0_retries.yaml +0 -0
  91. {csu-2.3.6 → csu-2.4.0}/tests/cassettes/test_service/test_decoding.yaml +0 -0
  92. {csu-2.3.6 → csu-2.4.0}/tests/cassettes/test_service/test_error.yaml +0 -0
  93. {csu-2.3.6 → csu-2.4.0}/tests/cassettes/test_service/test_logging.yaml +0 -0
  94. {csu-2.3.6 → csu-2.4.0}/tests/cassettes/test_service/test_redirects.yaml +0 -0
  95. {csu-2.3.6 → csu-2.4.0}/tests/cassettes/test_service_auth/test_async.yaml +0 -0
  96. {csu-2.3.6 → csu-2.4.0}/tests/cassettes/test_service_auth/test_sync.yaml +0 -0
  97. {csu-2.3.6 → csu-2.4.0}/tests/conftest.py +0 -0
  98. {csu-2.3.6 → csu-2.4.0}/tests/exampleworker.py +0 -0
  99. {csu-2.3.6 → csu-2.4.0}/tests/test_exampleworker.py +0 -0
  100. {csu-2.3.6 → csu-2.4.0}/tests/test_models.py +0 -0
  101. {csu-2.3.6 → csu-2.4.0}/tests/test_views.py +0 -0
  102. {csu-2.3.6 → csu-2.4.0}/tests/test_worker.py +0 -0
  103. {csu-2.3.6 → csu-2.4.0}/tests/testproject/__init__.py +0 -0
  104. {csu-2.3.6 → csu-2.4.0}/tests/testproject/fixtures/testuser.json +0 -0
  105. {csu-2.3.6 → csu-2.4.0}/tests/testproject/models.py +0 -0
  106. {csu-2.3.6 → csu-2.4.0}/tests/testproject/settings.py +0 -0
  107. {csu-2.3.6 → csu-2.4.0}/tests/testproject/urls.py +0 -0
  108. {csu-2.3.6 → csu-2.4.0}/tox.ini +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 2.3.6
2
+ current_version = 2.4.0
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -41,7 +41,7 @@ default_context:
41
41
  sphinx_theme: furo
42
42
  test_matrix_separate_coverage: false
43
43
  tests_inside_package: true
44
- version: 2.3.6
44
+ version: 2.4.0
45
45
  version_manager: bump2version
46
46
  website: https://blog.ionelmc.ro
47
47
  year_from: '2024'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csu
3
- Version: 2.3.6
3
+ Version: 2.4.0
4
4
  Summary: Clean Slate Utils - bunch of utility code, mostly Django/DRF specific.
5
5
  Author-email: Ionel Cristian Mărieș <contact@ionelmc.ro>
6
6
  License-Expression: BSD-2-Clause
@@ -69,9 +69,9 @@ Overview
69
69
  .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/csu.svg
70
70
  :alt: Supported implementations
71
71
  :target: https://pypi.org/project/csu
72
- .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.3.6.svg
72
+ .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.4.0.svg
73
73
  :alt: Commits since latest release
74
- :target: https://github.com/ionelmc/python-csu/compare/v2.3.6...main
74
+ :target: https://github.com/ionelmc/python-csu/compare/v2.4.0...main
75
75
  .. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/ionelmc/python-csu/main.svg
76
76
  :alt: Scrutinizer Status
77
77
  :target: https://scrutinizer-ci.com/g/ionelmc/python-csu/
@@ -34,9 +34,9 @@ Overview
34
34
  .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/csu.svg
35
35
  :alt: Supported implementations
36
36
  :target: https://pypi.org/project/csu
37
- .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.3.6.svg
37
+ .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.4.0.svg
38
38
  :alt: Commits since latest release
39
- :target: https://github.com/ionelmc/python-csu/compare/v2.3.6...main
39
+ :target: https://github.com/ionelmc/python-csu/compare/v2.4.0...main
40
40
  .. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/ionelmc/python-csu/main.svg
41
41
  :alt: Scrutinizer Status
42
42
  :target: https://scrutinizer-ci.com/g/ionelmc/python-csu/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "csu"
3
- version = "2.3.6"
3
+ version = "2.4.0"
4
4
  description = "Clean Slate Utils - bunch of utility code, mostly Django/DRF specific."
5
5
  requires-python = ">=3.14"
6
6
  license = "BSD-2-Clause"
@@ -2,4 +2,4 @@
2
2
  a.k.a Clean Slate Utils
3
3
  """
4
4
 
5
- __version__ = "2.3.6"
5
+ __version__ = "2.4.0"
@@ -215,19 +215,14 @@ class HTTPServiceContext[RTYPE = HTTPServiceResponse]:
215
215
  else:
216
216
  self.logger.warning("[%s] __exit__ !> %r", self.short_id, exc_val)
217
217
 
218
- def handle_process_response(
219
- self,
220
- response: Response,
221
- *,
222
- retry_statuses: set[int],
223
- accept_statuses: set[int],
224
- json,
225
- ) -> RTYPE:
226
- if response.status_code in retry_statuses:
227
- raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
228
- elif response.status_code not in accept_statuses:
229
- raise UnexpectedStatusError(response, accept_statuses, event_id=self.event_id)
230
-
218
+ def handle_process_response(self, response: Response, *, expect_json: bool) -> RTYPE:
219
+ if expect_json:
220
+ try:
221
+ json = response.json(parse_float=Decimal)
222
+ except Exception as exc:
223
+ raise DecodeError(response, exc, event_id=self.event_id) from exc
224
+ else:
225
+ json = None
231
226
  return HTTPServiceResponse(
232
227
  status=response.status_code,
233
228
  http_response=response,
@@ -300,14 +295,11 @@ class HTTPServiceContext[RTYPE = HTTPServiceResponse]:
300
295
  try:
301
296
  self.log_request(prepared_request)
302
297
  response = self.client.send(prepared_request, timeout=self.client.timeout, allow_redirects=follow_redirects, **settings)
303
- if expect_json:
304
- try:
305
- json = response.json(parse_float=Decimal)
306
- except Exception as exc:
307
- raise DecodeError(response, exc, event_id=self.event_id) from exc
308
- else:
309
- json = None
310
- return self.handle_process_response(response, retry_statuses=retry_statuses, accept_statuses=accept_statuses, json=json)
298
+ if response.status_code in retry_statuses:
299
+ raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
300
+ elif response.status_code not in accept_statuses:
301
+ raise UnexpectedStatusError(response, accept_statuses, event_id=self.event_id)
302
+ return self.handle_process_response(response, expect_json=expect_json)
311
303
  except (RequestError, RetryableError) as exc:
312
304
  retry_failures.append(exc)
313
305
  self.handle_retriable_failure(method, prepared_request.url, exc, retries_left, retry_budget)
@@ -321,7 +313,7 @@ class HTTPServiceContext[RTYPE = HTTPServiceResponse]:
321
313
  raise ExhaustedRetriesError(retry_budget, retry_failures, event_id=self.event_id)
322
314
 
323
315
 
324
- class AsyncHTTPServiceContext[RT = HTTPServiceResponse](HTTPServiceContext[RT]):
316
+ class AsyncHTTPServiceContext[RTYPE = HTTPServiceResponse](HTTPServiceContext[RTYPE]):
325
317
  auth_class = AsyncHTTPServiceAuth
326
318
  client_class = AsyncSession
327
319
  client: AsyncSession
@@ -345,6 +337,24 @@ class AsyncHTTPServiceContext[RT = HTTPServiceResponse](HTTPServiceContext[RT]):
345
337
  await self.client.__aexit__(exc_type, exc_val, exc_tb)
346
338
  self.handle_exit(exc_type, exc_val, exc_tb)
347
339
 
340
+ async def handle_process_response(self, response: Response, *, expect_json: bool) -> RTYPE:
341
+ if expect_json:
342
+ try:
343
+ if isinstance(response, AsyncResponse):
344
+ json = await response.json(parse_float=Decimal)
345
+ else:
346
+ json = response.json(parse_float=Decimal)
347
+ except Exception as exc:
348
+ raise DecodeError(response, exc, event_id=self.event_id) from exc
349
+ else:
350
+ json = None
351
+ return HTTPServiceResponse(
352
+ status=response.status_code,
353
+ http_response=response,
354
+ json=json,
355
+ event_id=self.event_id,
356
+ )
357
+
348
358
  async def request(
349
359
  self,
350
360
  method,
@@ -360,7 +370,7 @@ class AsyncHTTPServiceContext[RT = HTTPServiceResponse](HTTPServiceContext[RT]):
360
370
  verify: TLSVerifyType | None = None,
361
371
  cert: TLSClientCertType | None = None,
362
372
  **kwargs,
363
- ) -> RT:
373
+ ) -> RTYPE:
364
374
  retry_statuses = set(retry_statuses)
365
375
  accept_statuses = set(accept_statuses)
366
376
  assert not (common := sorted(retry_statuses & accept_statuses)), f"{common} is in both retry_statuses and accept_statuses!"
@@ -379,7 +389,7 @@ class AsyncHTTPServiceContext[RT = HTTPServiceResponse](HTTPServiceContext[RT]):
379
389
  **kwargs,
380
390
  )
381
391
  if prepared_request._asynchronous_auth:
382
- # needs a solution with less internals coupling
392
+ # needs a solution without internals coupling
383
393
  r = await prepared_request._pending_async_auth(prepared_request)
384
394
  prepared_request.__dict__.update(r.__dict__)
385
395
  prepared_request.prepare_content_length(prepared_request.body)
@@ -390,17 +400,11 @@ class AsyncHTTPServiceContext[RT = HTTPServiceResponse](HTTPServiceContext[RT]):
390
400
  try:
391
401
  await self.log_request(prepared_request)
392
402
  response = await self.client.send(prepared_request, timeout=self.client.timeout, allow_redirects=follow_redirects, **settings)
393
- if expect_json:
394
- try:
395
- if isinstance(response, AsyncResponse):
396
- json = await response.json(parse_float=Decimal)
397
- else:
398
- json = response.json(parse_float=Decimal)
399
- except Exception as exc:
400
- raise DecodeError(response, exc, event_id=self.event_id) from exc
401
- else:
402
- json = None
403
- return self.handle_process_response(response, retry_statuses=retry_statuses, accept_statuses=accept_statuses, json=json)
403
+ if response.status_code in retry_statuses:
404
+ raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
405
+ elif response.status_code not in accept_statuses:
406
+ raise UnexpectedStatusError(response, accept_statuses, event_id=self.event_id)
407
+ return await self.handle_process_response(response, expect_json=expect_json)
404
408
  except (RequestError, RetryableError) as exc:
405
409
  retry_failures.append(exc)
406
410
  self.handle_retriable_failure(method, prepared_request.url, exc, retries_left, retry_budget)
@@ -416,6 +420,7 @@ class AsyncHTTPServiceContext[RT = HTTPServiceResponse](HTTPServiceContext[RT]):
416
420
 
417
421
  class HTTPService[CT = HTTPServiceContext]:
418
422
  context_class: type[CT] = HTTPServiceContext
423
+ context_defaults: dict = {}
419
424
  logger_name: str = "service"
420
425
  logger: Logger
421
426
  base_url: str
@@ -442,7 +447,7 @@ class HTTPService[CT = HTTPServiceContext]:
442
447
  )
443
448
 
444
449
  def context(self, **kwargs) -> CT:
445
- return self.context_class(self, **kwargs)
450
+ return self.context_class(self, **self.context_defaults, **kwargs)
446
451
 
447
452
  def auth(self, request: PreparedRequest, context: CT) -> PreparedRequest:
448
453
  return request
@@ -452,7 +457,7 @@ class AsyncHTTPService[ACT = AsyncHTTPServiceContext](HTTPService[ACT]):
452
457
  context_class: type[AsyncHTTPServiceContext] = AsyncHTTPServiceContext
453
458
 
454
459
  def context(self, **kwargs) -> ACT:
455
- return self.context_class(self, **kwargs)
460
+ return self.context_class(self, **self.context_defaults, **kwargs)
456
461
 
457
462
  async def auth(self, request: PreparedRequest, context: ACT) -> PreparedRequest:
458
463
  return request
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csu
3
- Version: 2.3.6
3
+ Version: 2.4.0
4
4
  Summary: Clean Slate Utils - bunch of utility code, mostly Django/DRF specific.
5
5
  Author-email: Ionel Cristian Mărieș <contact@ionelmc.ro>
6
6
  License-Expression: BSD-2-Clause
@@ -69,9 +69,9 @@ Overview
69
69
  .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/csu.svg
70
70
  :alt: Supported implementations
71
71
  :target: https://pypi.org/project/csu
72
- .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.3.6.svg
72
+ .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.4.0.svg
73
73
  :alt: Commits since latest release
74
- :target: https://github.com/ionelmc/python-csu/compare/v2.3.6...main
74
+ :target: https://github.com/ionelmc/python-csu/compare/v2.4.0...main
75
75
  .. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/ionelmc/python-csu/main.svg
76
76
  :alt: Scrutinizer Status
77
77
  :target: https://scrutinizer-ci.com/g/ionelmc/python-csu/
@@ -96,7 +96,9 @@ tests/cassettes/test_service/test_error.yaml
96
96
  tests/cassettes/test_service/test_logging.yaml
97
97
  tests/cassettes/test_service/test_redirects.yaml
98
98
  tests/cassettes/test_service_auth/test_async.yaml
99
+ tests/cassettes/test_service_auth/test_async_auth.yaml
99
100
  tests/cassettes/test_service_auth/test_sync.yaml
101
+ tests/cassettes/test_service_auth/test_sync_auth.yaml
100
102
  tests/testproject/__init__.py
101
103
  tests/testproject/models.py
102
104
  tests/testproject/settings.py
@@ -0,0 +1,110 @@
1
+ interactions:
2
+ - request:
3
+ body: null
4
+ headers:
5
+ Accept:
6
+ - '*/*'
7
+ Accept-Encoding:
8
+ - gzip, deflate, br
9
+ Authorization:
10
+ - Basic dXNyOnB3ZA==
11
+ Connection:
12
+ - keep-alive
13
+ User-Agent:
14
+ - niquests/3.18.6
15
+ method: GET
16
+ uri: http://127.0.0.1:37027/basic-auth/usr/pwd
17
+ response:
18
+ body:
19
+ string: "{\n \"authenticated\": true,\n \"user\": \"usr\"\n}\n"
20
+ headers:
21
+ Access-Control-Allow-Credentials:
22
+ - 'true'
23
+ Access-Control-Allow-Origin:
24
+ - '*'
25
+ Connection:
26
+ - Close
27
+ Content-Length:
28
+ - '45'
29
+ Content-Type:
30
+ - application/json
31
+ Date:
32
+ - Wed, 17 Jun 2026 16:14:27 GMT
33
+ Server:
34
+ - Pytest-HTTPBIN/0.1.0
35
+ status:
36
+ code: 200
37
+ message: OK
38
+ - request:
39
+ body: null
40
+ headers:
41
+ Accept:
42
+ - '*/*'
43
+ Accept-Encoding:
44
+ - gzip, deflate, br
45
+ Authorization:
46
+ - Basic dXNyOnB3ZA==
47
+ Connection:
48
+ - keep-alive
49
+ User-Agent:
50
+ - niquests/3.18.6
51
+ method: GET
52
+ uri: http://127.0.0.1:40371/basic-auth/usr/pwd
53
+ response:
54
+ body:
55
+ string: "{\n \"authenticated\": true,\n \"user\": \"usr\"\n}\n"
56
+ headers:
57
+ Access-Control-Allow-Credentials:
58
+ - 'true'
59
+ Access-Control-Allow-Origin:
60
+ - '*'
61
+ Connection:
62
+ - Close
63
+ Content-Length:
64
+ - '45'
65
+ Content-Type:
66
+ - application/json
67
+ Date:
68
+ - Wed, 17 Jun 2026 16:25:16 GMT
69
+ Server:
70
+ - Pytest-HTTPBIN/0.1.0
71
+ status:
72
+ code: 200
73
+ message: OK
74
+ - request:
75
+ body: null
76
+ headers:
77
+ Accept:
78
+ - '*/*'
79
+ Accept-Encoding:
80
+ - gzip, deflate, br
81
+ Authorization:
82
+ - Basic dXNyOnB3ZA==
83
+ Connection:
84
+ - keep-alive
85
+ User-Agent:
86
+ - niquests/3.18.6
87
+ method: GET
88
+ uri: http://127.0.0.1:38445/basic-auth/quite/bad
89
+ response:
90
+ body:
91
+ string: ''
92
+ headers:
93
+ Access-Control-Allow-Credentials:
94
+ - 'true'
95
+ Access-Control-Allow-Origin:
96
+ - '*'
97
+ Connection:
98
+ - Close
99
+ Content-Length:
100
+ - '0'
101
+ Date:
102
+ - Wed, 17 Jun 2026 16:25:39 GMT
103
+ Server:
104
+ - Pytest-HTTPBIN/0.1.0
105
+ WWW-Authenticate:
106
+ - Basic realm="Fake Realm"
107
+ status:
108
+ code: 401
109
+ message: Unauthorized
110
+ version: 1
@@ -0,0 +1,146 @@
1
+ interactions:
2
+ - request:
3
+ body: null
4
+ headers:
5
+ Accept:
6
+ - '*/*'
7
+ Accept-Encoding:
8
+ - gzip, deflate, br
9
+ Authorization:
10
+ - Basic dXNyOnB3ZA==
11
+ Connection:
12
+ - keep-alive
13
+ User-Agent:
14
+ - niquests/3.18.6
15
+ method: GET
16
+ uri: http://127.0.0.1:37027/basic-auth/usr/pwd
17
+ response:
18
+ body:
19
+ string: "{\n \"authenticated\": true,\n \"user\": \"usr\"\n}\n"
20
+ headers:
21
+ Access-Control-Allow-Credentials:
22
+ - 'true'
23
+ Access-Control-Allow-Origin:
24
+ - '*'
25
+ Connection:
26
+ - Close
27
+ Content-Length:
28
+ - '45'
29
+ Content-Type:
30
+ - application/json
31
+ Date:
32
+ - Wed, 17 Jun 2026 16:14:27 GMT
33
+ Server:
34
+ - Pytest-HTTPBIN/0.1.0
35
+ status:
36
+ code: 200
37
+ message: OK
38
+ - request:
39
+ body: null
40
+ headers:
41
+ Accept:
42
+ - '*/*'
43
+ Accept-Encoding:
44
+ - gzip, deflate, br
45
+ Authorization:
46
+ - Basic dXNyOnB3ZA==
47
+ Connection:
48
+ - keep-alive
49
+ User-Agent:
50
+ - niquests/3.18.6
51
+ method: GET
52
+ uri: http://127.0.0.1:37027/basic-auth/quite/bad
53
+ response:
54
+ body:
55
+ string: ''
56
+ headers:
57
+ Access-Control-Allow-Credentials:
58
+ - 'true'
59
+ Access-Control-Allow-Origin:
60
+ - '*'
61
+ Connection:
62
+ - Close
63
+ Content-Length:
64
+ - '0'
65
+ Date:
66
+ - Wed, 17 Jun 2026 16:14:27 GMT
67
+ Server:
68
+ - Pytest-HTTPBIN/0.1.0
69
+ WWW-Authenticate:
70
+ - Basic realm="Fake Realm"
71
+ status:
72
+ code: 401
73
+ message: Unauthorized
74
+ - request:
75
+ body: null
76
+ headers:
77
+ Accept:
78
+ - '*/*'
79
+ Accept-Encoding:
80
+ - gzip, deflate, br
81
+ Authorization:
82
+ - Basic dXNyOnB3ZA==
83
+ Connection:
84
+ - keep-alive
85
+ User-Agent:
86
+ - niquests/3.18.6
87
+ method: GET
88
+ uri: http://127.0.0.1:40371/basic-auth/usr/pwd
89
+ response:
90
+ body:
91
+ string: "{\n \"authenticated\": true,\n \"user\": \"usr\"\n}\n"
92
+ headers:
93
+ Access-Control-Allow-Credentials:
94
+ - 'true'
95
+ Access-Control-Allow-Origin:
96
+ - '*'
97
+ Connection:
98
+ - Close
99
+ Content-Length:
100
+ - '45'
101
+ Content-Type:
102
+ - application/json
103
+ Date:
104
+ - Wed, 17 Jun 2026 16:25:16 GMT
105
+ Server:
106
+ - Pytest-HTTPBIN/0.1.0
107
+ status:
108
+ code: 200
109
+ message: OK
110
+ - request:
111
+ body: null
112
+ headers:
113
+ Accept:
114
+ - '*/*'
115
+ Accept-Encoding:
116
+ - gzip, deflate, br
117
+ Authorization:
118
+ - Basic dXNyOnB3ZA==
119
+ Connection:
120
+ - keep-alive
121
+ User-Agent:
122
+ - niquests/3.18.6
123
+ method: GET
124
+ uri: http://127.0.0.1:40371/basic-auth/quite/bad
125
+ response:
126
+ body:
127
+ string: ''
128
+ headers:
129
+ Access-Control-Allow-Credentials:
130
+ - 'true'
131
+ Access-Control-Allow-Origin:
132
+ - '*'
133
+ Connection:
134
+ - Close
135
+ Content-Length:
136
+ - '0'
137
+ Date:
138
+ - Wed, 17 Jun 2026 16:25:16 GMT
139
+ Server:
140
+ - Pytest-HTTPBIN/0.1.0
141
+ WWW-Authenticate:
142
+ - Basic realm="Fake Realm"
143
+ status:
144
+ code: 401
145
+ message: Unauthorized
146
+ version: 1
@@ -261,7 +261,7 @@ json.decoder.JSONDecodeError: Expecting value: line 2 column 5 (char 5)
261
261
  During handling of the above exception, another exception occurred:
262
262
 
263
263
  Traceback (most recent call last):
264
- File "[&SERVICE_PATH]", line 305, in request
264
+ File "[&SERVICE_PATH]", line 221, in handle_process_response
265
265
  json = response.json(parse_float=Decimal)
266
266
  File "[&MODELS_PATH]", line 1529, in json
267
267
  raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
@@ -276,7 +276,10 @@ Traceback (most recent call last):
276
276
  File "[&TEST_PATH]", line 78, in json
277
277
  return ctx.request("GET", "status/418", accept_statuses=[418]).json
278
278
  ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
279
- File "[&SERVICE_PATH]", line 307, in request
279
+ File "[&SERVICE_PATH]", line 302, in request
280
+ return self.handle_process_response(response, expect_json=expect_json)
281
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
282
+ File "[&SERVICE_PATH]", line 223, in handle_process_response
280
283
  raise DecodeError(response, exc, event_id=self.event_id) from exc
281
284
  csu.exceptions.DecodeError: DecodeError(JSONDecodeError('Expecting value: line 2 column 5 (char 5)'), response=<Response Dummy [418]>)
282
285
  (418, b'\\n -=[ teapot ]=-\\n\\n _...._\\n .\\' _ _ `.\\n | ."` ^ `". _,\\n \\\\_;`"---"`|//\\n | ;/\\n \\\\_ _/\\n `"""`\\n')
@@ -339,10 +342,7 @@ Traceback (most recent call last):
339
342
  File "[&TEST_PATH]", line 47, in error
340
343
  return ctx.request("GET", "status/599", expect_json=False)
341
344
  ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
342
- File "[&SERVICE_PATH]", line 310, in request
343
- return self.handle_process_response(response, retry_statuses=retry_statuses, accept_statuses=accept_statuses, json=json)
344
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
345
- File "[&SERVICE_PATH]", line 229, in handle_process_response
345
+ File "[&SERVICE_PATH]", line 301, in request
346
346
  raise UnexpectedStatusError(response, accept_statuses, event_id=self.event_id)
347
347
  csu.exceptions.UnexpectedStatusError: UnexpectedStatusError(<Response Dummy [599]>, accept_statuses=[200])
348
348
  (599, b'')
@@ -350,7 +350,7 @@ csu.exceptions.UnexpectedStatusError: UnexpectedStatusError(<Response Dummy [599
350
350
  The above exception was the direct cause of the following exception:
351
351
 
352
352
  Traceback (most recent call last):
353
- File "[&TEST_PATH]", line 295, in test_logging
353
+ File "[&TEST_PATH]", line 298, in test_logging
354
354
  bingo.error()
355
355
  ~~~~~~~~~~~^^
356
356
  File "[&TEST_PATH]", line 50, in error
@@ -542,10 +542,7 @@ INFO | [01:000000.00] POST [&HTTPBIN]/anything?foo=bar&accept_statuses=%7B999
542
542
  """\
543
543
  ERROR | [01:000000.00] POST [&HTTPBIN]/anything?foo=bar&accept_statuses=%7B999%7D&expect_json=true&follow_redirects=false&retries_left=3&retry_count=0&retry_statuses=%7B200%7D !> RetryableStatusError('200 not in [999] but in [200]', 200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"3","retry_count":"0","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=3&retry_count=0&retry_statuses={200}"}\\n', event_id=241212:01:000000.00) (will retry 3 more times)
544
544
  Traceback (most recent call last):
545
- File "[&SERVICE_PATH]", line 310, in request
546
- return self.handle_process_response(response, retry_statuses=retry_statuses, accept_statuses=accept_statuses, json=json)
547
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
548
- File "[&SERVICE_PATH]", line 227, in handle_process_response
545
+ File "[&SERVICE_PATH]", line 299, in request
549
546
  raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
550
547
  csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
551
548
  (200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"3","retry_count":"0","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=3&retry_count=0&retry_statuses={200}"}\\n')
@@ -579,10 +576,7 @@ INFO | [01:000000.00] POST [&HTTPBIN]/anything?foo=bar&accept_statuses=%7B999
579
576
  """\
580
577
  ERROR | [01:000000.00] POST [&HTTPBIN]/anything?foo=bar&accept_statuses=%7B999%7D&expect_json=true&follow_redirects=false&retries_left=2&retry_count=1&retry_statuses=%7B200%7D !> RetryableStatusError('200 not in [999] but in [200]', 200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"2","retry_count":"1","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=2&retry_count=1&retry_statuses={200}"}\\n', event_id=241212:01:000000.00) (will retry 2 more times)
581
578
  Traceback (most recent call last):
582
- File "[&SERVICE_PATH]", line 310, in request
583
- return self.handle_process_response(response, retry_statuses=retry_statuses, accept_statuses=accept_statuses, json=json)
584
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
585
- File "[&SERVICE_PATH]", line 227, in handle_process_response
579
+ File "[&SERVICE_PATH]", line 299, in request
586
580
  raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
587
581
  csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
588
582
  (200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"2","retry_count":"1","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=2&retry_count=1&retry_statuses={200}"}\\n')
@@ -616,10 +610,7 @@ INFO | [01:000000.00] POST [&HTTPBIN]/anything?foo=bar&accept_statuses=%7B999
616
610
  """\
617
611
  ERROR | [01:000000.00] POST [&HTTPBIN]/anything?foo=bar&accept_statuses=%7B999%7D&expect_json=true&follow_redirects=false&retries_left=1&retry_count=2&retry_statuses=%7B200%7D !> RetryableStatusError('200 not in [999] but in [200]', 200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"1","retry_count":"2","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=1&retry_count=2&retry_statuses={200}"}\\n', event_id=241212:01:000000.00) (will retry 1 more times)
618
612
  Traceback (most recent call last):
619
- File "[&SERVICE_PATH]", line 310, in request
620
- return self.handle_process_response(response, retry_statuses=retry_statuses, accept_statuses=accept_statuses, json=json)
621
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
622
- File "[&SERVICE_PATH]", line 227, in handle_process_response
613
+ File "[&SERVICE_PATH]", line 299, in request
623
614
  raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
624
615
  csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
625
616
  (200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"1","retry_count":"2","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=1&retry_count=2&retry_statuses={200}"}\\n')
@@ -667,43 +658,31 @@ ERROR | (renderer: NoneType)
667
658
  | ...<6 lines>...
668
659
  | accept_statuses=[999],
669
660
  | )
670
- | File "[&SERVICE_PATH]", line 321, in request
661
+ | File "[&SERVICE_PATH]", line 313, in request
671
662
  | raise ExhaustedRetriesError(retry_budget, retry_failures, event_id=self.event_id)
672
663
  | csu.exceptions.ExhaustedRetriesError: ExhaustedRetriesError(3)
673
664
  | ([RetryableStatusError('200 not in [999] but in [200]', 200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"3","retry_count":"0","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=3&retry_count=0&retry_statuses={200}"}\\n', event_id=241212:01:000000.00), RetryableStatusError('200 not in [999] but in [200]', 200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"2","retry_count":"1","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=2&retry_count=1&retry_statuses={200}"}\\n', event_id=241212:01:000000.00), RetryableStatusError('200 not in [999] but in [200]', 200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"1","retry_count":"2","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=1&retry_count=2&retry_statuses={200}"}\\n', event_id=241212:01:000000.00), RetryableStatusError('200 not in [999] but in [200]', 200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"0","retry_count":"3","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=0&retry_count=3&retry_statuses={200}"}\\n', event_id=241212:01:000000.00)],)
674
665
  +-+---------------- 1 ----------------
675
666
  | Traceback (most recent call last):
676
- | File "[&SERVICE_PATH]", line 310, in request
677
- | return self.handle_process_response(response, retry_statuses=retry_statuses, accept_statuses=accept_statuses, json=json)
678
- | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
679
- | File "[&SERVICE_PATH]", line 227, in handle_process_response
667
+ | File "[&SERVICE_PATH]", line 299, in request
680
668
  | raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
681
669
  | csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
682
670
  | (200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"3","retry_count":"0","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=3&retry_count=0&retry_statuses={200}"}\\n')
683
671
  +---------------- 2 ----------------
684
672
  | Traceback (most recent call last):
685
- | File "[&SERVICE_PATH]", line 310, in request
686
- | return self.handle_process_response(response, retry_statuses=retry_statuses, accept_statuses=accept_statuses, json=json)
687
- | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
688
- | File "[&SERVICE_PATH]", line 227, in handle_process_response
673
+ | File "[&SERVICE_PATH]", line 299, in request
689
674
  | raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
690
675
  | csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
691
676
  | (200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"2","retry_count":"1","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=2&retry_count=1&retry_statuses={200}"}\\n')
692
677
  +---------------- 3 ----------------
693
678
  | Traceback (most recent call last):
694
- | File "[&SERVICE_PATH]", line 310, in request
695
- | return self.handle_process_response(response, retry_statuses=retry_statuses, accept_statuses=accept_statuses, json=json)
696
- | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
697
- | File "[&SERVICE_PATH]", line 227, in handle_process_response
679
+ | File "[&SERVICE_PATH]", line 299, in request
698
680
  | raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
699
681
  | csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
700
682
  | (200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"1","retry_count":"2","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=1&retry_count=2&retry_statuses={200}"}\\n')
701
683
  +---------------- 4 ----------------
702
684
  | Traceback (most recent call last):
703
- | File "[&SERVICE_PATH]", line 310, in request
704
- | return self.handle_process_response(response, retry_statuses=retry_statuses, accept_statuses=accept_statuses, json=json)
705
- | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
706
- | File "[&SERVICE_PATH]", line 227, in handle_process_response
685
+ | File "[&SERVICE_PATH]", line 299, in request
707
686
  | raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
708
687
  | csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
709
688
  | (200, b'{"args":{"accept_statuses":"{999}","expect_json":"true","follow_redirects":"false","foo":"bar","retries_left":"0","retry_count":"3","retry_statuses":"{200}"},"data":"config={\\'custom\\': \\'config\\'}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive","Content-Length":"27","Host":"127.0.0.1:12345","User-Agent":"python-httpx/0.X.X"},"json":null,"method":"POST","origin":"127.0.0.1","url":"http://127.0.0.1:12345/anything?foo=bar&accept_statuses={999}&expect_json=true&follow_redirects=false&retries_left=0&retry_count=3&retry_statuses={200}"}\\n')
@@ -3,7 +3,9 @@ from os import getpid
3
3
  import pytest
4
4
  from inline_snapshot import snapshot
5
5
  from niquests import PreparedRequest
6
+ from niquests.auth import HTTPBasicAuth
6
7
 
8
+ from csu.exceptions import UnexpectedStatusError
7
9
  from csu.service import AsyncHTTPService
8
10
  from csu.service import HTTPService
9
11
  from csu.service import HTTPServiceContext
@@ -41,6 +43,106 @@ class AsyncAuthedService(AsyncHTTPService):
41
43
  await ctx.request("GET", "json")
42
44
 
43
45
 
46
+ class VanillaAuthedService(HTTPService):
47
+ def __init__(self, url, user, password):
48
+ super().__init__(config={"BASE_URL": url})
49
+ self.context_defaults = {"auth": HTTPBasicAuth(user, password)}
50
+
51
+ def test(self, path):
52
+ with self.context() as ctx:
53
+ ctx.request("GET", path)
54
+
55
+
56
+ class VanillaAsyncAuthedService(AsyncHTTPService):
57
+ def __init__(self, url, user, password):
58
+ super().__init__(config={"BASE_URL": url})
59
+ self.context_defaults = {"auth": HTTPBasicAuth(user, password)}
60
+
61
+ async def test(self, path):
62
+ async with self.context() as ctx:
63
+ await ctx.request("GET", path)
64
+
65
+
66
+ @pytest.mark.vcr
67
+ def test_sync_auth(httpbin, fake_accident_id, caplogs):
68
+ srv = VanillaAuthedService(httpbin.url, "usr", "pwd")
69
+ srv.test("basic-auth/usr/pwd")
70
+ assert caplogs.get(
71
+ HTTPBIN=f"{httpbin.host}:{httpbin.port}",
72
+ PID=f"[pid:{getpid()}]",
73
+ ) == snapshot(
74
+ [
75
+ "INFO | [&PID] Setting up VanillaAuthedService with BASE_URL='http://[&HTTPBIN]' TIMEOUT=60 RETRIES=3 VERIFY=True",
76
+ """\
77
+ INFO | [01:000000.00] GET http://[&HTTPBIN]/basic-auth/usr/pwd
78
+ ------------------------------------------------------------- request headers --------------------------------------------------------------
79
+ Accept: '*/*'
80
+ Accept-Encoding: 'gzip, deflate, br'
81
+ Authorization: 'Basic dXNyOnB3ZA=='
82
+ Connection: 'keep-alive'
83
+ User-Agent: 'niquests/3.18.6'\
84
+ """,
85
+ """\
86
+ INFO | [01:000000.00] GET http://[&HTTPBIN]/basic-auth/usr/pwd => 200 in 0.0000s (HNone)
87
+ ------------------------------------------------------------- response headers -------------------------------------------------------------
88
+ Access-Control-Allow-Credentials: 'true'
89
+ Access-Control-Allow-Origin: '*'
90
+ Connection: 'Close'
91
+ Content-Length: '45'
92
+ Content-Type: 'application/json'
93
+ Date: 'Wed, 17 Jun 2026 16:14:27 GMT'
94
+ Server: 'Pytest-HTTPBIN/0.1.0'
95
+ ---------------------------------------------- response body ('application/json' - 45 bytes) -----------------------------------------------
96
+ b'{\\n "authenticated": true,\\n "user": "usr"\\n}\\n'
97
+ ============================================================================================================================================\
98
+ """,
99
+ ]
100
+ )
101
+ with pytest.raises(UnexpectedStatusError):
102
+ srv.test("basic-auth/quite/bad")
103
+
104
+
105
+ @pytest.mark.vcr
106
+ @pytest.mark.asyncio
107
+ async def test_async_auth(httpbin, fake_accident_id, caplogs):
108
+ srv = VanillaAsyncAuthedService(httpbin.url, "usr", "pwd")
109
+ await srv.test("basic-auth/usr/pwd")
110
+ assert caplogs.get(
111
+ HTTPBIN=f"{httpbin.host}:{httpbin.port}",
112
+ PID=f"[pid:{getpid()}]",
113
+ ) == snapshot(
114
+ [
115
+ "INFO | [&PID] Setting up VanillaAsyncAuthedService with BASE_URL='http://[&HTTPBIN]' TIMEOUT=60 RETRIES=3 VERIFY=True",
116
+ """\
117
+ INFO | [01:000000.00] GET http://[&HTTPBIN]/basic-auth/usr/pwd
118
+ ------------------------------------------------------------- request headers --------------------------------------------------------------
119
+ Accept: '*/*'
120
+ Accept-Encoding: 'gzip, deflate, br'
121
+ Authorization: 'Basic dXNyOnB3ZA=='
122
+ Connection: 'keep-alive'
123
+ User-Agent: 'niquests/3.18.6'\
124
+ """,
125
+ "INFO | Playing response for <Request (GET) http://[&HTTPBIN]/basic-auth/usr/pwd> from cassette",
126
+ """\
127
+ INFO | [01:000000.00] GET http://[&HTTPBIN]/basic-auth/usr/pwd => 200 in 0.0000s (H0)
128
+ ------------------------------------------------------------- response headers -------------------------------------------------------------
129
+ Access-Control-Allow-Credentials: 'true'
130
+ Access-Control-Allow-Origin: '*'
131
+ Connection: 'Close'
132
+ Content-Length: '45'
133
+ Content-Type: 'application/json'
134
+ Date: 'Wed, 17 Jun 2026 16:14:27 GMT'
135
+ Server: 'Pytest-HTTPBIN/0.1.0'
136
+ ---------------------------------------------- response body ('application/json' - 45 bytes) -----------------------------------------------
137
+ b'{\\n "authenticated": true,\\n "user": "usr"\\n}\\n'
138
+ ============================================================================================================================================\
139
+ """,
140
+ ]
141
+ )
142
+ with pytest.raises(UnexpectedStatusError):
143
+ await srv.test("basic-auth/quite/bad")
144
+
145
+
44
146
  @pytest.mark.vcr
45
147
  def test_sync(httpbin, fake_accident_id, caplogs):
46
148
  srv = AuthedService(httpbin.url)
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
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