csu 2.3.6__tar.gz → 2.4.1__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.1}/.bumpversion.cfg +1 -1
  2. {csu-2.3.6 → csu-2.4.1}/.cookiecutterrc +1 -1
  3. {csu-2.3.6 → csu-2.4.1}/PKG-INFO +3 -3
  4. {csu-2.3.6 → csu-2.4.1}/README.rst +2 -2
  5. {csu-2.3.6 → csu-2.4.1}/pyproject.toml +1 -1
  6. {csu-2.3.6 → csu-2.4.1}/src/csu/__init__.py +1 -1
  7. {csu-2.3.6 → csu-2.4.1}/src/csu/service.py +47 -40
  8. {csu-2.3.6 → csu-2.4.1}/src/csu.egg-info/PKG-INFO +3 -3
  9. {csu-2.3.6 → csu-2.4.1}/src/csu.egg-info/SOURCES.txt +2 -0
  10. csu-2.4.1/tests/cassettes/test_service_auth/test_async_auth.yaml +110 -0
  11. csu-2.4.1/tests/cassettes/test_service_auth/test_sync_auth.yaml +146 -0
  12. {csu-2.3.6 → csu-2.4.1}/tests/test_service.py +18 -41
  13. {csu-2.3.6 → csu-2.4.1}/tests/test_service_auth.py +102 -0
  14. {csu-2.3.6 → csu-2.4.1}/.coveragerc +0 -0
  15. {csu-2.3.6 → csu-2.4.1}/.editorconfig +0 -0
  16. {csu-2.3.6 → csu-2.4.1}/.github/workflows/github-actions.yml +0 -0
  17. {csu-2.3.6 → csu-2.4.1}/.pre-commit-config.yaml +0 -0
  18. {csu-2.3.6 → csu-2.4.1}/.tombi.toml +0 -0
  19. {csu-2.3.6 → csu-2.4.1}/AUTHORS.rst +0 -0
  20. {csu-2.3.6 → csu-2.4.1}/CHANGELOG.rst +0 -0
  21. {csu-2.3.6 → csu-2.4.1}/CONTRIBUTING.rst +0 -0
  22. {csu-2.3.6 → csu-2.4.1}/LICENSE +0 -0
  23. {csu-2.3.6 → csu-2.4.1}/MANIFEST.in +0 -0
  24. {csu-2.3.6 → csu-2.4.1}/ci/bootstrap.py +0 -0
  25. {csu-2.3.6 → csu-2.4.1}/ci/requirements.txt +0 -0
  26. {csu-2.3.6 → csu-2.4.1}/ci/templates/.github/workflows/github-actions.yml +0 -0
  27. {csu-2.3.6 → csu-2.4.1}/pytest.ini +0 -0
  28. {csu-2.3.6 → csu-2.4.1}/setup.cfg +0 -0
  29. {csu-2.3.6 → csu-2.4.1}/src/csu/admin.py +0 -0
  30. {csu-2.3.6 → csu-2.4.1}/src/csu/conf.py +0 -0
  31. {csu-2.3.6 → csu-2.4.1}/src/csu/consts.py +0 -0
  32. {csu-2.3.6 → csu-2.4.1}/src/csu/drf/__init__.py +0 -0
  33. {csu-2.3.6 → csu-2.4.1}/src/csu/drf/auth.py +0 -0
  34. {csu-2.3.6 → csu-2.4.1}/src/csu/drf/fields.py +0 -0
  35. {csu-2.3.6 → csu-2.4.1}/src/csu/drf/forms.py +0 -0
  36. {csu-2.3.6 → csu-2.4.1}/src/csu/drf/perms.py +0 -0
  37. {csu-2.3.6 → csu-2.4.1}/src/csu/drf/phonenumber.py +0 -0
  38. {csu-2.3.6 → csu-2.4.1}/src/csu/drf/serializers.py +0 -0
  39. {csu-2.3.6 → csu-2.4.1}/src/csu/drf/test_auth.py +0 -0
  40. {csu-2.3.6 → csu-2.4.1}/src/csu/drf/test_fields.py +0 -0
  41. {csu-2.3.6 → csu-2.4.1}/src/csu/drf/test_forms.py +0 -0
  42. {csu-2.3.6 → csu-2.4.1}/src/csu/drf/test_phonenumber.py +0 -0
  43. {csu-2.3.6 → csu-2.4.1}/src/csu/drf/test_serializers.py +0 -0
  44. {csu-2.3.6 → csu-2.4.1}/src/csu/drf/views.py +0 -0
  45. {csu-2.3.6 → csu-2.4.1}/src/csu/enums.py +0 -0
  46. {csu-2.3.6 → csu-2.4.1}/src/csu/env.py +0 -0
  47. {csu-2.3.6 → csu-2.4.1}/src/csu/exceptions.py +0 -0
  48. {csu-2.3.6 → csu-2.4.1}/src/csu/export.py +0 -0
  49. {csu-2.3.6 → csu-2.4.1}/src/csu/fixups.py +0 -0
  50. {csu-2.3.6 → csu-2.4.1}/src/csu/forms/__init__.py +0 -0
  51. {csu-2.3.6 → csu-2.4.1}/src/csu/forms/crispy.py +0 -0
  52. {csu-2.3.6 → csu-2.4.1}/src/csu/forms/fields.py +0 -0
  53. {csu-2.3.6 → csu-2.4.1}/src/csu/gettext.py +0 -0
  54. {csu-2.3.6 → csu-2.4.1}/src/csu/gettext_lazy.py +0 -0
  55. {csu-2.3.6 → csu-2.4.1}/src/csu/locale/ro/LC_MESSAGES/django.mo +0 -0
  56. {csu-2.3.6 → csu-2.4.1}/src/csu/locale/ro/LC_MESSAGES/django.po +0 -0
  57. {csu-2.3.6 → csu-2.4.1}/src/csu/logging.py +0 -0
  58. {csu-2.3.6 → csu-2.4.1}/src/csu/management.py +0 -0
  59. {csu-2.3.6 → csu-2.4.1}/src/csu/models.py +0 -0
  60. {csu-2.3.6 → csu-2.4.1}/src/csu/query.py +0 -0
  61. {csu-2.3.6 → csu-2.4.1}/src/csu/routers.py +0 -0
  62. {csu-2.3.6 → csu-2.4.1}/src/csu/templates/api_exception.html +0 -0
  63. {csu-2.3.6 → csu-2.4.1}/src/csu/templates/forms/widgets/opaquewidget.html +0 -0
  64. {csu-2.3.6 → csu-2.4.1}/src/csu/test_consts.py +0 -0
  65. {csu-2.3.6 → csu-2.4.1}/src/csu/test_env.py +0 -0
  66. {csu-2.3.6 → csu-2.4.1}/src/csu/test_exceptions.py +0 -0
  67. {csu-2.3.6 → csu-2.4.1}/src/csu/test_logging.py +0 -0
  68. {csu-2.3.6 → csu-2.4.1}/src/csu/test_management.py +0 -0
  69. {csu-2.3.6 → csu-2.4.1}/src/csu/test_service.py +0 -0
  70. {csu-2.3.6 → csu-2.4.1}/src/csu/test_timezones.py +0 -0
  71. {csu-2.3.6 → csu-2.4.1}/src/csu/test_utils.py +0 -0
  72. {csu-2.3.6 → csu-2.4.1}/src/csu/test_xml.py +0 -0
  73. {csu-2.3.6 → csu-2.4.1}/src/csu/timezones.py +0 -0
  74. {csu-2.3.6 → csu-2.4.1}/src/csu/utils.py +0 -0
  75. {csu-2.3.6 → csu-2.4.1}/src/csu/views.py +0 -0
  76. {csu-2.3.6 → csu-2.4.1}/src/csu/worker/__init__.py +0 -0
  77. {csu-2.3.6 → csu-2.4.1}/src/csu/worker/admin.py +0 -0
  78. {csu-2.3.6 → csu-2.4.1}/src/csu/worker/asgi.py +0 -0
  79. {csu-2.3.6 → csu-2.4.1}/src/csu/worker/engine.py +0 -0
  80. {csu-2.3.6 → csu-2.4.1}/src/csu/worker/enums.py +0 -0
  81. {csu-2.3.6 → csu-2.4.1}/src/csu/worker/job.py +0 -0
  82. {csu-2.3.6 → csu-2.4.1}/src/csu/worker/models.py +0 -0
  83. {csu-2.3.6 → csu-2.4.1}/src/csu/worker/registry.py +0 -0
  84. {csu-2.3.6 → csu-2.4.1}/src/csu/wsgi.py +0 -0
  85. {csu-2.3.6 → csu-2.4.1}/src/csu/xml.py +0 -0
  86. {csu-2.3.6 → csu-2.4.1}/src/csu.egg-info/dependency_links.txt +0 -0
  87. {csu-2.3.6 → csu-2.4.1}/src/csu.egg-info/requires.txt +0 -0
  88. {csu-2.3.6 → csu-2.4.1}/src/csu.egg-info/top_level.txt +0 -0
  89. {csu-2.3.6 → csu-2.4.1}/tests/cassettes/test_service/test_custom_context.yaml +0 -0
  90. {csu-2.3.6 → csu-2.4.1}/tests/cassettes/test_service/test_custom_context_0_retries.yaml +0 -0
  91. {csu-2.3.6 → csu-2.4.1}/tests/cassettes/test_service/test_decoding.yaml +0 -0
  92. {csu-2.3.6 → csu-2.4.1}/tests/cassettes/test_service/test_error.yaml +0 -0
  93. {csu-2.3.6 → csu-2.4.1}/tests/cassettes/test_service/test_logging.yaml +0 -0
  94. {csu-2.3.6 → csu-2.4.1}/tests/cassettes/test_service/test_redirects.yaml +0 -0
  95. {csu-2.3.6 → csu-2.4.1}/tests/cassettes/test_service_auth/test_async.yaml +0 -0
  96. {csu-2.3.6 → csu-2.4.1}/tests/cassettes/test_service_auth/test_sync.yaml +0 -0
  97. {csu-2.3.6 → csu-2.4.1}/tests/conftest.py +0 -0
  98. {csu-2.3.6 → csu-2.4.1}/tests/exampleworker.py +0 -0
  99. {csu-2.3.6 → csu-2.4.1}/tests/test_exampleworker.py +0 -0
  100. {csu-2.3.6 → csu-2.4.1}/tests/test_models.py +0 -0
  101. {csu-2.3.6 → csu-2.4.1}/tests/test_views.py +0 -0
  102. {csu-2.3.6 → csu-2.4.1}/tests/test_worker.py +0 -0
  103. {csu-2.3.6 → csu-2.4.1}/tests/testproject/__init__.py +0 -0
  104. {csu-2.3.6 → csu-2.4.1}/tests/testproject/fixtures/testuser.json +0 -0
  105. {csu-2.3.6 → csu-2.4.1}/tests/testproject/models.py +0 -0
  106. {csu-2.3.6 → csu-2.4.1}/tests/testproject/settings.py +0 -0
  107. {csu-2.3.6 → csu-2.4.1}/tests/testproject/urls.py +0 -0
  108. {csu-2.3.6 → csu-2.4.1}/tox.ini +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 2.3.6
2
+ current_version = 2.4.1
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.1
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.1
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.1.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.1...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.1.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.1...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.1"
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.1"
@@ -16,9 +16,9 @@ from niquests import Response
16
16
  from niquests import Session
17
17
  from niquests.auth import AsyncAuthBase
18
18
  from niquests.auth import AuthBase
19
+ from niquests.exceptions import ConnectionError
19
20
  from niquests.typing import TLSClientCertType
20
21
  from niquests.typing import TLSVerifyType
21
- from urllib3_future.exceptions import RequestError
22
22
 
23
23
  from .conf import SERVICE_REQUEST_CONTENT_LOG_LIMIT
24
24
  from .conf import SERVICE_REQUEST_SHOW_HEADERS
@@ -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,
@@ -270,6 +265,7 @@ class HTTPServiceContext[RTYPE = HTTPServiceResponse]:
270
265
  expect_json: bool = True,
271
266
  follow_redirects: bool = False,
272
267
  retry_statuses: Iterable[int] = (500, 502, 503, 504),
268
+ retry_exceptions: Iterable[type[Exception]] = (ConnectionError, RetryableError),
273
269
  retry: bool = True,
274
270
  proxy: str | None = None,
275
271
  stream: bool | None = None,
@@ -300,15 +296,12 @@ class HTTPServiceContext[RTYPE = HTTPServiceResponse]:
300
296
  try:
301
297
  self.log_request(prepared_request)
302
298
  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)
311
- except (RequestError, RetryableError) as exc:
299
+ if response.status_code in retry_statuses:
300
+ raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
301
+ elif response.status_code not in accept_statuses:
302
+ raise UnexpectedStatusError(response, accept_statuses, event_id=self.event_id)
303
+ return self.handle_process_response(response, expect_json=expect_json)
304
+ except retry_exceptions as exc:
312
305
  retry_failures.append(exc)
313
306
  self.handle_retriable_failure(method, prepared_request.url, exc, retries_left, retry_budget)
314
307
  except (InternalServiceError, OpenServiceError) as exc:
@@ -321,7 +314,7 @@ class HTTPServiceContext[RTYPE = HTTPServiceResponse]:
321
314
  raise ExhaustedRetriesError(retry_budget, retry_failures, event_id=self.event_id)
322
315
 
323
316
 
324
- class AsyncHTTPServiceContext[RT = HTTPServiceResponse](HTTPServiceContext[RT]):
317
+ class AsyncHTTPServiceContext[RTYPE = HTTPServiceResponse](HTTPServiceContext[RTYPE]):
325
318
  auth_class = AsyncHTTPServiceAuth
326
319
  client_class = AsyncSession
327
320
  client: AsyncSession
@@ -345,6 +338,24 @@ class AsyncHTTPServiceContext[RT = HTTPServiceResponse](HTTPServiceContext[RT]):
345
338
  await self.client.__aexit__(exc_type, exc_val, exc_tb)
346
339
  self.handle_exit(exc_type, exc_val, exc_tb)
347
340
 
341
+ async def handle_process_response(self, response: Response, *, expect_json: bool) -> RTYPE:
342
+ if expect_json:
343
+ try:
344
+ if isinstance(response, AsyncResponse):
345
+ json = await response.json(parse_float=Decimal)
346
+ else:
347
+ json = response.json(parse_float=Decimal)
348
+ except Exception as exc:
349
+ raise DecodeError(response, exc, event_id=self.event_id) from exc
350
+ else:
351
+ json = None
352
+ return HTTPServiceResponse(
353
+ status=response.status_code,
354
+ http_response=response,
355
+ json=json,
356
+ event_id=self.event_id,
357
+ )
358
+
348
359
  async def request(
349
360
  self,
350
361
  method,
@@ -354,13 +365,14 @@ class AsyncHTTPServiceContext[RT = HTTPServiceResponse](HTTPServiceContext[RT]):
354
365
  expect_json: bool = True,
355
366
  follow_redirects: bool = False,
356
367
  retry_statuses: Iterable[int] = (500, 502, 503, 504),
368
+ retry_exceptions: Iterable[type[Exception]] = (ConnectionError, RetryableError),
357
369
  retry: bool = True,
358
370
  proxy: str | None = None,
359
371
  stream: bool | None = None,
360
372
  verify: TLSVerifyType | None = None,
361
373
  cert: TLSClientCertType | None = None,
362
374
  **kwargs,
363
- ) -> RT:
375
+ ) -> RTYPE:
364
376
  retry_statuses = set(retry_statuses)
365
377
  accept_statuses = set(accept_statuses)
366
378
  assert not (common := sorted(retry_statuses & accept_statuses)), f"{common} is in both retry_statuses and accept_statuses!"
@@ -379,7 +391,7 @@ class AsyncHTTPServiceContext[RT = HTTPServiceResponse](HTTPServiceContext[RT]):
379
391
  **kwargs,
380
392
  )
381
393
  if prepared_request._asynchronous_auth:
382
- # needs a solution with less internals coupling
394
+ # needs a solution without internals coupling
383
395
  r = await prepared_request._pending_async_auth(prepared_request)
384
396
  prepared_request.__dict__.update(r.__dict__)
385
397
  prepared_request.prepare_content_length(prepared_request.body)
@@ -390,18 +402,12 @@ class AsyncHTTPServiceContext[RT = HTTPServiceResponse](HTTPServiceContext[RT]):
390
402
  try:
391
403
  await self.log_request(prepared_request)
392
404
  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)
404
- except (RequestError, RetryableError) as exc:
405
+ if response.status_code in retry_statuses:
406
+ raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
407
+ elif response.status_code not in accept_statuses:
408
+ raise UnexpectedStatusError(response, accept_statuses, event_id=self.event_id)
409
+ return await self.handle_process_response(response, expect_json=expect_json)
410
+ except retry_exceptions as exc:
405
411
  retry_failures.append(exc)
406
412
  self.handle_retriable_failure(method, prepared_request.url, exc, retries_left, retry_budget)
407
413
  except (InternalServiceError, OpenServiceError) as exc:
@@ -416,6 +422,7 @@ class AsyncHTTPServiceContext[RT = HTTPServiceResponse](HTTPServiceContext[RT]):
416
422
 
417
423
  class HTTPService[CT = HTTPServiceContext]:
418
424
  context_class: type[CT] = HTTPServiceContext
425
+ context_defaults: dict = {}
419
426
  logger_name: str = "service"
420
427
  logger: Logger
421
428
  base_url: str
@@ -442,7 +449,7 @@ class HTTPService[CT = HTTPServiceContext]:
442
449
  )
443
450
 
444
451
  def context(self, **kwargs) -> CT:
445
- return self.context_class(self, **kwargs)
452
+ return self.context_class(self, **self.context_defaults, **kwargs)
446
453
 
447
454
  def auth(self, request: PreparedRequest, context: CT) -> PreparedRequest:
448
455
  return request
@@ -452,7 +459,7 @@ class AsyncHTTPService[ACT = AsyncHTTPServiceContext](HTTPService[ACT]):
452
459
  context_class: type[AsyncHTTPServiceContext] = AsyncHTTPServiceContext
453
460
 
454
461
  def context(self, **kwargs) -> ACT:
455
- return self.context_class(self, **kwargs)
462
+ return self.context_class(self, **self.context_defaults, **kwargs)
456
463
 
457
464
  async def auth(self, request: PreparedRequest, context: ACT) -> PreparedRequest:
458
465
  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.1
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.1.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.1...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
@@ -154,7 +154,6 @@ def test_error(fake_accident_id, caplogs, httpbin):
154
154
  bingo.error()
155
155
  assert exc.value.message == "It's ok."
156
156
  api_exc = exc.value.as_api_service_error()
157
- exc.value.details[0]
158
157
  assert api_exc.status_code == 200
159
158
  assert api_exc.detail == {"accident_id": "241212:01:000000.00", "code": "ok", "detail": "It's ok."}
160
159
 
@@ -261,7 +260,7 @@ json.decoder.JSONDecodeError: Expecting value: line 2 column 5 (char 5)
261
260
  During handling of the above exception, another exception occurred:
262
261
 
263
262
  Traceback (most recent call last):
264
- File "[&SERVICE_PATH]", line 305, in request
263
+ File "[&SERVICE_PATH]", line 221, in handle_process_response
265
264
  json = response.json(parse_float=Decimal)
266
265
  File "[&MODELS_PATH]", line 1529, in json
267
266
  raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
@@ -270,13 +269,16 @@ niquests.exceptions.JSONDecodeError: Expecting value: line 2 column 5 (char 5)
270
269
  The above exception was the direct cause of the following exception:
271
270
 
272
271
  Traceback (most recent call last):
273
- File "[&TEST_PATH]", line 203, in test_decoding
272
+ File "[&TEST_PATH]", line 202, in test_decoding
274
273
  bingo.json()
275
274
  ~~~~~~~~~~^^
276
275
  File "[&TEST_PATH]", line 78, in json
277
276
  return ctx.request("GET", "status/418", accept_statuses=[418]).json
278
277
  ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
279
- File "[&SERVICE_PATH]", line 307, in request
278
+ File "[&SERVICE_PATH]", line 303, in request
279
+ return self.handle_process_response(response, expect_json=expect_json)
280
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
281
+ File "[&SERVICE_PATH]", line 223, in handle_process_response
280
282
  raise DecodeError(response, exc, event_id=self.event_id) from exc
281
283
  csu.exceptions.DecodeError: DecodeError(JSONDecodeError('Expecting value: line 2 column 5 (char 5)'), response=<Response Dummy [418]>)
282
284
  (418, b'\\n -=[ teapot ]=-\\n\\n _...._\\n .\\' _ _ `.\\n | ."` ^ `". _,\\n \\\\_;`"---"`|//\\n | ;/\\n \\\\_ _/\\n `"""`\\n')
@@ -293,7 +295,6 @@ def test_logging(fake_accident_id, caplogs, httpbin):
293
295
  bingo = BingoService(httpbin.url)
294
296
  with pytest.raises(OkError) as exc:
295
297
  bingo.error()
296
- exc.value.details[0]
297
298
  exception_handler(exc.value, {})
298
299
  assert len(fake_accident_id.calls) == 1
299
300
  assert fake_accident_id.calls[0].startswith("service.py:")
@@ -339,10 +340,7 @@ Traceback (most recent call last):
339
340
  File "[&TEST_PATH]", line 47, in error
340
341
  return ctx.request("GET", "status/599", expect_json=False)
341
342
  ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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
343
+ File "[&SERVICE_PATH]", line 302, in request
346
344
  raise UnexpectedStatusError(response, accept_statuses, event_id=self.event_id)
347
345
  csu.exceptions.UnexpectedStatusError: UnexpectedStatusError(<Response Dummy [599]>, accept_statuses=[200])
348
346
  (599, b'')
@@ -350,7 +348,7 @@ csu.exceptions.UnexpectedStatusError: UnexpectedStatusError(<Response Dummy [599
350
348
  The above exception was the direct cause of the following exception:
351
349
 
352
350
  Traceback (most recent call last):
353
- File "[&TEST_PATH]", line 295, in test_logging
351
+ File "[&TEST_PATH]", line 297, in test_logging
354
352
  bingo.error()
355
353
  ~~~~~~~~~~~^^
356
354
  File "[&TEST_PATH]", line 50, in error
@@ -542,10 +540,7 @@ INFO | [01:000000.00] POST [&HTTPBIN]/anything?foo=bar&accept_statuses=%7B999
542
540
  """\
543
541
  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
542
  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
543
+ File "[&SERVICE_PATH]", line 300, in request
549
544
  raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
550
545
  csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
551
546
  (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 +574,7 @@ INFO | [01:000000.00] POST [&HTTPBIN]/anything?foo=bar&accept_statuses=%7B999
579
574
  """\
580
575
  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
576
  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
577
+ File "[&SERVICE_PATH]", line 300, in request
586
578
  raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
587
579
  csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
588
580
  (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 +608,7 @@ INFO | [01:000000.00] POST [&HTTPBIN]/anything?foo=bar&accept_statuses=%7B999
616
608
  """\
617
609
  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
610
  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
611
+ File "[&SERVICE_PATH]", line 300, in request
623
612
  raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
624
613
  csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
625
614
  (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')
@@ -658,52 +647,40 @@ ERROR | (renderer: NoneType)
658
647
  {}
659
648
  ---------------------------------------------------------------- traceback -----------------------------------------------------------------
660
649
  + Exception Group Traceback (most recent call last):
661
- | File "[&TEST_PATH]", line 437, in test_custom_context
650
+ | File "[&TEST_PATH]", line 435, in test_custom_context
662
651
  | custom.run()
663
652
  | ~~~~~~~~~~^^
664
- | File "[&TEST_PATH]", line 420, in run
653
+ | File "[&TEST_PATH]", line 418, in run
665
654
  | resp = ctx.request(
666
655
  | "POST",
667
656
  | ...<6 lines>...
668
657
  | accept_statuses=[999],
669
658
  | )
670
- | File "[&SERVICE_PATH]", line 321, in request
659
+ | File "[&SERVICE_PATH]", line 314, in request
671
660
  | raise ExhaustedRetriesError(retry_budget, retry_failures, event_id=self.event_id)
672
661
  | csu.exceptions.ExhaustedRetriesError: ExhaustedRetriesError(3)
673
662
  | ([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
663
  +-+---------------- 1 ----------------
675
664
  | 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
665
+ | File "[&SERVICE_PATH]", line 300, in request
680
666
  | raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
681
667
  | csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
682
668
  | (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
669
  +---------------- 2 ----------------
684
670
  | 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
671
+ | File "[&SERVICE_PATH]", line 300, in request
689
672
  | raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
690
673
  | csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
691
674
  | (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
675
  +---------------- 3 ----------------
693
676
  | 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
677
+ | File "[&SERVICE_PATH]", line 300, in request
698
678
  | raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
699
679
  | csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
700
680
  | (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
681
  +---------------- 4 ----------------
702
682
  | 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
683
+ | File "[&SERVICE_PATH]", line 300, in request
707
684
  | raise RetryableStatusError(response, accept_statuses, retry_statuses, event_id=self.event_id)
708
685
  | csu.exceptions.RetryableStatusError: RetryableStatusError(<Response Dummy [200]>, accept_statuses=[999], retry_statuses=[200])
709
686
  | (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