lib-shopware6-api-base 3.0.2__tar.gz → 3.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/CHANGELOG.md +11 -0
  2. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/PKG-INFO +4 -6
  3. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/README.md +1 -2
  4. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/pyproject.toml +3 -4
  5. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/dependencies.py +4 -4
  6. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/refractor.md +1 -1
  7. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/refractor.py +1 -1
  8. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/__init__conf__.py +1 -1
  9. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/_http_common.py +6 -6
  10. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/lib_shopware6_admin_client.py +87 -72
  11. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/lib_shopware6_storefront_client.py +9 -9
  12. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/conftest.py +3 -3
  13. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/.devcontainer/devcontainer.json +0 -0
  14. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/.devcontainer/settings.json +0 -0
  15. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/.github/actions/extract-metadata/action.yml +0 -0
  16. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/.github/dependabot.yml +0 -0
  17. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/.github/workflows/cicd_docker.yml +0 -0
  18. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/.github/workflows/codeql-analysis.yml +0 -0
  19. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/.github/workflows/default_release_public.yml +0 -0
  20. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/.gitignore +0 -0
  21. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/CODE_OF_CONDUCT.md +0 -0
  22. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/CONTRIBUTING.md +0 -0
  23. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/INSTALL.md +0 -0
  24. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/ISSUE_TEMPLATE.md +0 -0
  25. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/LICENSE +0 -0
  26. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/PULL_REQUEST_TEMPLATE.md +0 -0
  27. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/example.env +0 -0
  28. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/mk +0 -0
  29. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/mk.py +0 -0
  30. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/notebooks/Quickstart.ipynb +0 -0
  31. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/__init__.py +0 -0
  32. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/__main__.py +0 -0
  33. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/_git.py +0 -0
  34. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/_metadata.py +0 -0
  35. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/_pip.py +0 -0
  36. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/_subprocess.py +0 -0
  37. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/_test_audit.py +0 -0
  38. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/_test_coverage.py +0 -0
  39. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/_test_steps.py +0 -0
  40. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/_utils.py +0 -0
  41. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/build.py +0 -0
  42. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/bump.py +0 -0
  43. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/bump_major.py +0 -0
  44. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/bump_minor.py +0 -0
  45. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/bump_patch.py +0 -0
  46. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/bump_version.py +0 -0
  47. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/clean.py +0 -0
  48. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/cli.py +0 -0
  49. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/dev.py +0 -0
  50. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/help.py +0 -0
  51. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/install.py +0 -0
  52. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/menu.py +0 -0
  53. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/push.py +0 -0
  54. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/release.py +0 -0
  55. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/run_cli.py +0 -0
  56. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/target_metadata.py +0 -0
  57. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/test.py +0 -0
  58. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/toml_config.py +0 -0
  59. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/scripts/version_current.py +0 -0
  60. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/.gitignore +0 -0
  61. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/__init__.py +0 -0
  62. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/__main__.py +0 -0
  63. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/_compat.py +0 -0
  64. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/conf_shopware6_api_base_classes.py +0 -0
  65. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/exit_codes.py +0 -0
  66. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/lib_shopware6_api_base.py +0 -0
  67. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/lib_shopware6_api_base_cli.py +0 -0
  68. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/lib_shopware6_api_base_criteria.py +0 -0
  69. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/lib_shopware6_api_base_criteria_aggregation.py +0 -0
  70. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/lib_shopware6_api_base_criteria_filter.py +0 -0
  71. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/lib_shopware6_api_base_criteria_sorting.py +0 -0
  72. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/lib_shopware6_api_base_helpers.py +0 -0
  73. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/src/lib_shopware6_api_base/py.typed +0 -0
  74. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/conf_test_docker.py +0 -0
  75. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/docker.env +0 -0
  76. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/local_testscripts/lib_bash_functions.sh +0 -0
  77. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/local_testscripts/run_clean.sh +0 -0
  78. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/local_testscripts/run_pytest.sh +0 -0
  79. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/local_testscripts/run_testloop.sh +0 -0
  80. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/local_testscripts/run_testloop_pytest_and_mypy_only_no_setup.sh +0 -0
  81. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/local_testscripts/run_testloop_windows.cmd +0 -0
  82. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/local_testscripts/shellcheck.sh +0 -0
  83. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/local_testscripts/testing_tools.py +0 -0
  84. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/test_api_base.py +0 -0
  85. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/test_cli.py +0 -0
  86. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/test_conf_classes.py +0 -0
  87. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/test_config.py +0 -0
  88. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/test_criteria.py +0 -0
  89. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/test_criteria_aggregation.py +0 -0
  90. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/test_criteria_filter.py +0 -0
  91. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/test_criteria_sorting.py +0 -0
  92. {lib_shopware6_api_base-3.0.2 → lib_shopware6_api_base-3.1.0}/tests/test_helpers.py +0 -0
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.1.0] - 2026-06-01
9
+
10
+ ### Changed
11
+
12
+ - **HTTP client migrated from `httpx` to `httpx2`** (the Pydantic-stewarded successor); minimum `httpx2>=2.2.0`. Storefront client and shared HTTP helpers swapped over directly (API-compatible surface). Dev tooling (`scripts/dependencies.py`) migrated too.
13
+ - Admin API OAuth2 (token fetch, refresh-token flow, Bearer auth, expiry handling) reimplemented directly on `httpx2`, replacing Authlib's `OAuth2Client`. The `self.token` shape is unchanged (`access_token`, `expires_in`, `expires_at`, and `refresh_token` for the password grant); public client APIs are unchanged.
14
+
15
+ ### Removed
16
+
17
+ - Dropped the `authlib` dependency — OAuth2 token handling is now performed in-library. As a side effect, `insecure_transport` is no longer required to talk to plain-HTTP endpoints (the previous `OAUTHLIB_INSECURE_TRANSPORT` guard is gone); the config field is retained for backward compatibility but is now a no-op.
18
+
8
19
  ## [3.0.2] - 2026-05-08
9
20
 
10
21
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lib_shopware6_api_base
3
- Version: 3.0.2
3
+ Version: 3.1.0
4
4
  Summary: python3 base API client for shopware6
5
5
  Project-URL: Homepage, https://github.com/bitranox/lib_shopware6_api_base
6
6
  Project-URL: Documentation, https://github.com/bitranox/lib_shopware6_api_base/blob/master/README.md
@@ -22,8 +22,7 @@ Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Programming Language :: Python :: 3.14
23
23
  Classifier: Typing :: Typed
24
24
  Requires-Python: >=3.10
25
- Requires-Dist: authlib>=1.6.11
26
- Requires-Dist: httpx>=0.28.1
25
+ Requires-Dist: httpx2>=2.2.0
27
26
  Requires-Dist: lib-cli-exit-tools>=2.2.4
28
27
  Requires-Dist: orjson
29
28
  Requires-Dist: pydantic-settings>=2.12.0
@@ -33,7 +32,7 @@ Provides-Extra: dev
33
32
  Requires-Dist: bandit>=1.9.3; extra == 'dev'
34
33
  Requires-Dist: build>=1.4.0; extra == 'dev'
35
34
  Requires-Dist: codecov-cli>=11.2.6; extra == 'dev'
36
- Requires-Dist: httpx>=0.28.1; extra == 'dev'
35
+ Requires-Dist: httpx2>=2.2.0; extra == 'dev'
37
36
  Requires-Dist: hypothesis>=6.151.4; extra == 'dev'
38
37
  Requires-Dist: import-linter>=2.9; extra == 'dev'
39
38
  Requires-Dist: jaraco-context>=6.1.0; extra == 'dev'
@@ -494,8 +493,7 @@ Automatically installed dependencies:
494
493
 
495
494
  - `pydantic>=2.0.0` - Data validation
496
495
  - `pydantic-settings>=2.0.0` - Settings management
497
- - `httpx>=0.28` - HTTP client
498
- - `authlib>=1.6` - OAuth2 authentication
496
+ - `httpx2>=2.2.0` - HTTP client
499
497
  - `rich-click` - CLI formatting
500
498
  - `orjson` - Fast JSON serialization
501
499
  - `lib_cli_exit_tools>=2.2.4` - CLI utilities
@@ -440,8 +440,7 @@ Automatically installed dependencies:
440
440
 
441
441
  - `pydantic>=2.0.0` - Data validation
442
442
  - `pydantic-settings>=2.0.0` - Settings management
443
- - `httpx>=0.28` - HTTP client
444
- - `authlib>=1.6` - OAuth2 authentication
443
+ - `httpx2>=2.2.0` - HTTP client
445
444
  - `rich-click` - CLI formatting
446
445
  - `orjson` - Fast JSON serialization
447
446
  - `lib_cli_exit_tools>=2.2.4` - CLI utilities
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lib_shopware6_api_base"
3
- version = "3.0.2"
3
+ version = "3.1.0"
4
4
  description = "python3 base API client for shopware6"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -10,8 +10,7 @@ dependencies = [
10
10
  "lib_cli_exit_tools>=2.2.4",
11
11
  "rich-click",
12
12
  "orjson",
13
- "httpx>=0.28.1",
14
- "authlib>=1.6.11",
13
+ "httpx2>=2.2.0",
15
14
  ]
16
15
  license = {text = "MIT"}
17
16
  authors = [{ name = "bitranox", email = "bitranox@gmail.com" }]
@@ -50,7 +49,7 @@ dev = [
50
49
  "textual>=7.5.0",
51
50
  "import-linter>=2.9",
52
51
  "rtoml>=0.13.0",
53
- "httpx>=0.28.1",
52
+ "httpx2>=2.2.0",
54
53
  # Transitive CVE pins
55
54
  "jaraco-context>=6.1.0", # CVE-2025-23949
56
55
  "python-multipart>=0.0.27", # CVE-2025-24486, CVE-2026-40347, CVE-2026-42561
@@ -28,7 +28,7 @@ from dataclasses import dataclass
28
28
  from pathlib import Path
29
29
  from typing import Any, cast
30
30
 
31
- import httpx
31
+ import httpx2
32
32
  import orjson
33
33
 
34
34
  from .toml_config import PyprojectConfig, load_pyproject_config
@@ -131,14 +131,14 @@ def _fetch_pypi_data(package_name: str) -> dict[str, Any] | None:
131
131
  url = f"https://pypi.org/pypi/{normalized}/json"
132
132
 
133
133
  try:
134
- response = httpx.get(url, timeout=10.0)
134
+ response = httpx2.get(url, timeout=10.0)
135
135
  response.raise_for_status()
136
136
  return orjson.loads(response.content) # type: ignore[no-any-return]
137
- except httpx.HTTPStatusError as exc:
137
+ except httpx2.HTTPStatusError as exc:
138
138
  if exc.response.status_code == 404:
139
139
  return None
140
140
  return None
141
- except (httpx.ConnectError, httpx.TimeoutException, orjson.JSONDecodeError):
141
+ except (httpx2.ConnectError, httpx2.TimeoutException, orjson.JSONDecodeError):
142
142
  return None
143
143
 
144
144
 
@@ -48,7 +48,7 @@ After the roast loop completes (only MINOR issues remain), the pipeline runs the
48
48
  | 1 | Data Architecture Enforcement | Run `/bx_data_architecture_enforcement` |
49
49
  | 2 | Architecture Review | Review using skill `bx-python-clean-architecture` |
50
50
  | 3 | LRU Cache Opportunities | Find caching opportunities, precompile regex |
51
- | 4 | Proper Usage of Libraries | Ensure orjson, rtoml, httpx usage |
51
+ | 4 | Proper Usage of Libraries | Ensure orjson, rtoml, httpx2 usage |
52
52
  | 5 | Edge Cases & Security | Review for edge cases, race conditions, security |
53
53
  | 6 | Test Refractor | Run `/bx_refactor_tests_python` |
54
54
  | 7 | Code Simplification | Simplify code, remove dead code, modernize |
@@ -679,7 +679,7 @@ Continue numbering for all issues found. Each issue MUST have a specific, action
679
679
  (
680
680
  "Proper Usage of Libraries",
681
681
  "make sure that we use orjson, rtoml instead of stdlib. "
682
- "make sure that we use httpx if applicable. "
682
+ "make sure that we use httpx2 if applicable. "
683
683
  "do not create compatibility shims, we dont care for backward compatibility. "
684
684
  "write tests for any new code you create - aim for good test coverage. "
685
685
  "dont ask back - decide yourself given all the systemprompts in claude.md. "
@@ -26,7 +26,7 @@ name = "lib_shopware6_api_base"
26
26
  #: Human-readable summary shown in CLI help output.
27
27
  title = "python3 base API client for shopware6"
28
28
  #: Current release version pulled from ``pyproject.toml`` by automation.
29
- version = "3.0.2"
29
+ version = "3.1.0"
30
30
  #: Repository homepage presented to users.
31
31
  homepage = "https://github.com/bitranox/lib_shopware6_api_base"
32
32
  #: Author attribution surfaced in CLI output.
@@ -6,7 +6,7 @@ from typing import Any
6
6
  from urllib.parse import urljoin
7
7
 
8
8
  # EXT
9
- import httpx
9
+ import httpx2
10
10
 
11
11
  from .conf_shopware6_api_base_classes import ShopwareAPIError
12
12
  from .lib_shopware6_api_base_criteria import Criteria
@@ -46,7 +46,7 @@ __all__ = [
46
46
  ]
47
47
 
48
48
  # Module-level constants for HTTP settings
49
- DEFAULT_REQUEST_TIMEOUT = httpx.Timeout(30.0, connect=10.0)
49
+ DEFAULT_REQUEST_TIMEOUT = httpx2.Timeout(30.0, connect=10.0)
50
50
  MAX_RETRY_ATTEMPTS = 2
51
51
  CONTENT_TYPE_JSON = "json"
52
52
  HEADER_SW_ACCESS_KEY = "sw-access-key"
@@ -94,7 +94,7 @@ def get_payload_dict(payload: PayLoad) -> dict[str, Any]:
94
94
  return payload # type: ignore
95
95
 
96
96
 
97
- def handle_json_decode_error(response: httpx.Response) -> dict[str, Any]:
97
+ def handle_json_decode_error(response: httpx2.Response) -> dict[str, Any]:
98
98
  """Safely decode JSON response, returning empty dict on failure."""
99
99
  try:
100
100
  return dict(response.json())
@@ -103,18 +103,18 @@ def handle_json_decode_error(response: httpx.Response) -> dict[str, Any]:
103
103
  return {}
104
104
 
105
105
 
106
- def handle_http_error(exc: httpx.HTTPStatusError) -> None:
106
+ def handle_http_error(exc: httpx2.HTTPStatusError) -> None:
107
107
  """Convert HTTP status errors to ShopwareAPIError with detailed message."""
108
108
  detailed_error = f" : {exc.response.text}"
109
109
  raise ShopwareAPIError(f"{exc}{detailed_error}") from exc
110
110
 
111
111
 
112
- def log_request(request: httpx.Request) -> None:
112
+ def log_request(request: httpx2.Request) -> None:
113
113
  """Log outgoing HTTP request for debugging."""
114
114
  logger.debug("Request: %s %s", request.method, request.url)
115
115
 
116
116
 
117
- def log_response(response: httpx.Response) -> None:
117
+ def log_response(response: httpx2.Response) -> None:
118
118
  """Log incoming HTTP response for debugging."""
119
119
  logger.debug("Response: %s %s", response.status_code, response.url)
120
120
 
@@ -5,13 +5,12 @@ import time
5
5
  from typing import Any
6
6
 
7
7
  # EXT
8
- import httpx
8
+ import httpx2
9
9
  import orjson
10
- from authlib.integrations.base_client import TokenExpiredError
11
- from authlib.integrations.httpx_client import OAuth2Client
12
10
 
13
11
  from ._http_common import (
14
12
  CONTENT_TYPE_JSON,
13
+ DEFAULT_REQUEST_TIMEOUT,
15
14
  MAX_RETRY_ATTEMPTS,
16
15
  PayLoad,
17
16
  build_api_url,
@@ -53,7 +52,7 @@ class Shopware6AdminAPIClientBase:
53
52
  """
54
53
  self.config = config
55
54
  self.token: dict[str, Any] = {}
56
- self.session: OAuth2Client | httpx.Client = httpx.Client()
55
+ self.session: httpx2.Client = httpx2.Client()
57
56
 
58
57
  def request_get(
59
58
  self, request_url: str, payload: PayLoad = None, update_header_fields: dict[str, str] | None = None
@@ -522,34 +521,16 @@ class Shopware6AdminAPIClientBase:
522
521
  update_header_fields=update_header_fields,
523
522
  )
524
523
  retry = 0
525
- except TokenExpiredError:
526
- if self._is_refreshable_token(): # pragma: no cover
527
- # this actually should never happen - just in case.
528
- # Authlib handles token refresh via callback automatically
529
- logger.warning(
530
- "something went wrong - the token should have been automatically refreshed. getting a new token"
531
- ) # pragma: no cover
532
- self._get_access_token_by_user_credentials() # pragma: no cover
533
- else:
534
- self._get_access_token_by_resource_owner()
535
- self._get_session()
536
- response = self._request(
537
- http_method=http_method,
538
- request_url=request_url,
539
- payload=payload,
540
- content_type=content_type,
541
- additional_query_params=additional_query_params,
542
- update_header_fields=update_header_fields,
543
- )
544
- retry = 0
545
524
  except ShopwareAPIError as exc:
546
525
  # retry: how often to retry - sometimes we get error code:9, status:401,
547
526
  # The resource owner or authorization server denied the request,
548
527
  # detail: Access token could not be verified.
549
- # But it works if You try again, it seems to be an error in shopware API or race condition
528
+ # But it works if You try again, it seems to be an error in shopware API or race condition.
529
+ # Drop the token so the next iteration re-authenticates from scratch.
550
530
  retry = retry - 1
551
531
  if not retry:
552
532
  raise exc
533
+ self.token = {}
553
534
 
554
535
  if not retry:
555
536
  break
@@ -570,7 +551,7 @@ class Shopware6AdminAPIClientBase:
570
551
  content_type: str = CONTENT_TYPE_JSON,
571
552
  additional_query_params: dict[str, Any] | None = None,
572
553
  update_header_fields: dict[str, str] | None = None,
573
- ) -> httpx.Response:
554
+ ) -> httpx2.Response:
574
555
  """
575
556
  makes a request, needs a "self.session" to be set up and authenticated
576
557
 
@@ -585,7 +566,7 @@ class Shopware6AdminAPIClientBase:
585
566
  returns
586
567
  response_dict: dictionary with the response as dict
587
568
 
588
- see : https://www.python-httpx.org/quickstart/
569
+ see : https://www.encode.io/httpx2/
589
570
 
590
571
  """
591
572
  request_data: str
@@ -602,8 +583,9 @@ class Shopware6AdminAPIClientBase:
602
583
  if not additional_query_params:
603
584
  additional_query_params = {}
604
585
 
605
- response: httpx.Response
586
+ response: httpx2.Response
606
587
  headers = self._get_headers(content_type=content_type, update_header_fields=update_header_fields)
588
+ headers["Authorization"] = f"Bearer {self.token['access_token']}"
607
589
 
608
590
  if http_method == HttpMethod.GET:
609
591
  if additional_query_params:
@@ -641,6 +623,7 @@ class Shopware6AdminAPIClientBase:
641
623
  elif http_method == HttpMethod.DELETE:
642
624
  response = self.session.delete(
643
625
  self._format_admin_api_url(request_url),
626
+ headers=headers,
644
627
  params=additional_query_params,
645
628
  follow_redirects=self.config.follow_redirects,
646
629
  )
@@ -652,7 +635,7 @@ class Shopware6AdminAPIClientBase:
652
635
 
653
636
  try:
654
637
  response.raise_for_status() # type: ignore[possibly-undefined]
655
- except httpx.HTTPStatusError as exc:
638
+ except httpx2.HTTPStatusError as exc:
656
639
  detailed_error = f" : {exc.response.text}"
657
640
  raise ShopwareAPIError(f"{exc}{detailed_error}") from exc
658
641
 
@@ -767,13 +750,11 @@ class Shopware6AdminAPIClientBase:
767
750
  if not self.config.client_secret:
768
751
  raise ShopwareAPIError("client_secret needed")
769
752
 
770
- client = OAuth2Client(
771
- client_id=self.config.client_id,
772
- client_secret=self.config.client_secret,
773
- )
774
- self.token = client.fetch_token(
775
- url=self._format_admin_api_url("oauth/token"),
776
- grant_type="client_credentials",
753
+ # Shopware authenticates the confidential client via HTTP Basic auth
754
+ # (OAuth2 "client_secret_basic"); the body carries only the grant_type.
755
+ self.token = self._fetch_token(
756
+ {"grant_type": "client_credentials"},
757
+ auth=httpx2.BasicAuth(self.config.client_id, self.config.client_secret),
777
758
  )
778
759
  return self.token
779
760
 
@@ -784,7 +765,6 @@ class Shopware6AdminAPIClientBase:
784
765
  - we recommend to only use this grant flow for client applications that should
785
766
  perform administrative actions and require a user-based authentication
786
767
 
787
- see : https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#legacy-application-flow
788
768
  see : https://shopware.stoplight.io/docs/admin-api/ZG9jOjEwODA3NjQx-authentication-and-authorisation
789
769
  setup at admin/settings/system/user: "access_id" and "access_secret"
790
770
 
@@ -840,18 +820,25 @@ class Shopware6AdminAPIClientBase:
840
820
  if not self.config.password:
841
821
  raise ShopwareAPIError("password needed")
842
822
 
843
- client = OAuth2Client(client_id="administration")
844
- self.token = client.fetch_token(
845
- url=self._format_admin_api_url("oauth/token"),
846
- grant_type="password",
847
- username=self.config.username,
848
- password=self.config.password,
823
+ # "administration" is Shopware's built-in public client for the password
824
+ # grant; it issues a refresh_token, so this flow is auto-refreshable.
825
+ self.token = self._fetch_token(
826
+ {
827
+ "grant_type": "password",
828
+ "client_id": "administration",
829
+ "username": self.config.username,
830
+ "password": self.config.password,
831
+ }
849
832
  )
850
833
  return self.token
851
834
 
852
835
  def _get_session(self) -> None:
853
836
  """
854
- see : https://docs.authlib.org/en/latest/client/httpx.html
837
+ ensure a valid (non-expired) access token is available before a request.
838
+
839
+ an expired token is refreshed via its refresh_token when available
840
+ (password grant), otherwise a fresh token is fetched (client credentials).
841
+
855
842
  see : https://shopware.stoplight.io/docs/admin-api/ZG9jOjEwODA3NjQx-authentication-and-authorisation
856
843
 
857
844
 
@@ -879,42 +866,70 @@ class Shopware6AdminAPIClientBase:
879
866
  """
880
867
  if not self.token:
881
868
  self._get_token()
882
- if self._is_refreshable_token():
883
- self.token["expires_in"] = int(self.token["expires_at"] - time.time())
884
- client_id = "administration"
885
- self.session = OAuth2Client(
886
- client_id=client_id,
887
- token=self.token,
888
- token_endpoint=self._format_admin_api_url("oauth/token"),
889
- update_token=self._token_saver,
890
- )
891
- else:
892
- client_id = self.config.client_id
893
- self.session = OAuth2Client(client_id=client_id, token=self.token)
869
+ elif self._token_is_expired():
870
+ if self._is_refreshable_token():
871
+ self._refresh_token()
872
+ else:
873
+ self._get_token()
894
874
 
895
- def _token_saver(
896
- self,
897
- token: dict[str, Any],
898
- refresh_token: str | None = None,
899
- access_token: str | None = None,
900
- ) -> None:
875
+ def _fetch_token(self, data: dict[str, Any], auth: httpx2.Auth | None = None) -> dict[str, Any]:
901
876
  """
902
- saves the token - this is needed for automatically refreshing the "resource owner" access token.
903
- the "user_credentials" can not be refreshed
877
+ POST to the ``oauth/token`` endpoint and return the token dict.
904
878
 
905
- This is the callback for Authlib's OAuth2Client update_token parameter.
879
+ ``expires_at`` (absolute unix time) is derived from ``expires_in`` so that
880
+ token expiry can be checked locally without another round trip.
906
881
 
907
- parameter
908
- token: the token to be saved
909
- refresh_token: optional refresh token (Authlib callback parameter)
910
- access_token: optional access token (Authlib callback parameter)
882
+ parameters:
883
+ data: the OAuth2 request body (grant_type and grant-specific fields)
884
+ auth: optional HTTP Basic client auth (used by the client_credentials grant)
911
885
 
912
886
  returns
913
- None
887
+ the token dict, with an added ``expires_at`` key
888
+
889
+ raises
890
+ ShopwareAPIError: if the token endpoint returns an error status
891
+ """
892
+ response = self.session.post(
893
+ self._format_admin_api_url("oauth/token"),
894
+ json=data,
895
+ auth=auth if auth is not None else httpx2.USE_CLIENT_DEFAULT,
896
+ headers={"Content-Type": "application/json", "Accept": "application/json"},
897
+ timeout=DEFAULT_REQUEST_TIMEOUT,
898
+ follow_redirects=self.config.follow_redirects,
899
+ )
900
+ try:
901
+ response.raise_for_status()
902
+ except httpx2.HTTPStatusError as exc:
903
+ detailed_error = f" : {exc.response.text}"
904
+ raise ShopwareAPIError(f"{exc}{detailed_error}") from exc
905
+ token = dict(response.json())
906
+ if "expires_in" in token:
907
+ token["expires_at"] = time.time() + int(token["expires_in"])
908
+ return token
914
909
 
910
+ def _refresh_token(self) -> dict[str, Any]:
911
+ """
912
+ refresh the access token using the stored refresh_token (password grant).
913
+ the "client_credentials" grant has no refresh_token and is re-fetched instead.
914
+ """
915
+ self.token = self._fetch_token(
916
+ {
917
+ "grant_type": "refresh_token",
918
+ "client_id": "administration",
919
+ "refresh_token": self.token["refresh_token"],
920
+ }
921
+ )
922
+ return self.token
923
+
924
+ def _token_is_expired(self) -> bool:
925
+ """
926
+ True if the current token has an ``expires_at`` in the past.
927
+ a token without ``expires_at`` is treated as not expired.
915
928
  """
916
- _ = refresh_token, access_token # unused, but required by Authlib callback signature
917
- self.token = token
929
+ expires_at = self.token.get("expires_at")
930
+ if expires_at is None:
931
+ return False
932
+ return time.time() >= float(expires_at)
918
933
 
919
934
  def _is_refreshable_token(self) -> bool:
920
935
  """
@@ -3,7 +3,7 @@ import logging
3
3
  from typing import Any
4
4
 
5
5
  # EXT
6
- import httpx
6
+ import httpx2
7
7
  import orjson
8
8
 
9
9
  from ._http_common import (
@@ -290,7 +290,7 @@ class Shopware6StoreFrontClientBase:
290
290
  request_url: str,
291
291
  payload: PayLoad = None,
292
292
  update_header_fields: dict[str, str] | None = None,
293
- ) -> httpx.Response | None:
293
+ ) -> httpx2.Response | None:
294
294
  """
295
295
  makes a request, using the conf.store_api_sw_access_key for authentication
296
296
 
@@ -330,11 +330,11 @@ class Shopware6StoreFrontClientBase:
330
330
  if isinstance(payload, Criteria):
331
331
  payload = payload.get_dict()
332
332
  storefront_api_url = self._build_storefront_api_url(endpoint=request_url)
333
- response: httpx.Response
333
+ response: httpx2.Response
334
334
  headers = self._get_headers(update_header_fields=update_header_fields)
335
335
 
336
336
  if http_method == HttpMethod.GET:
337
- response = httpx.request(
337
+ response = httpx2.request(
338
338
  "GET",
339
339
  storefront_api_url,
340
340
  params=payload,
@@ -343,7 +343,7 @@ class Shopware6StoreFrontClientBase:
343
343
  follow_redirects=self.config.follow_redirects,
344
344
  )
345
345
  elif http_method == HttpMethod.PATCH:
346
- response = httpx.request(
346
+ response = httpx2.request(
347
347
  "PATCH",
348
348
  storefront_api_url,
349
349
  content=orjson.dumps(payload).decode(),
@@ -352,7 +352,7 @@ class Shopware6StoreFrontClientBase:
352
352
  follow_redirects=self.config.follow_redirects,
353
353
  )
354
354
  elif http_method == HttpMethod.POST:
355
- response = httpx.request(
355
+ response = httpx2.request(
356
356
  "POST",
357
357
  storefront_api_url,
358
358
  content=orjson.dumps(payload).decode(),
@@ -361,7 +361,7 @@ class Shopware6StoreFrontClientBase:
361
361
  follow_redirects=self.config.follow_redirects,
362
362
  )
363
363
  elif http_method == HttpMethod.PUT:
364
- response = httpx.request(
364
+ response = httpx2.request(
365
365
  "PUT",
366
366
  storefront_api_url,
367
367
  content=orjson.dumps(payload).decode(),
@@ -370,7 +370,7 @@ class Shopware6StoreFrontClientBase:
370
370
  follow_redirects=self.config.follow_redirects,
371
371
  )
372
372
  elif http_method == HttpMethod.DELETE:
373
- response = httpx.request(
373
+ response = httpx2.request(
374
374
  "DELETE",
375
375
  storefront_api_url,
376
376
  headers=headers,
@@ -385,7 +385,7 @@ class Shopware6StoreFrontClientBase:
385
385
 
386
386
  try:
387
387
  response.raise_for_status() # type: ignore[possibly-undefined]
388
- except httpx.HTTPStatusError as exc:
388
+ except httpx2.HTTPStatusError as exc:
389
389
  detailed_error = f" : {exc.response.text}"
390
390
  raise ShopwareAPIError(f"{exc}{detailed_error}") from exc
391
391
  return response
@@ -7,7 +7,7 @@ import sys
7
7
  import time
8
8
  from typing import TYPE_CHECKING
9
9
 
10
- import httpx
10
+ import httpx2
11
11
  import pytest
12
12
 
13
13
  if TYPE_CHECKING:
@@ -435,10 +435,10 @@ def _docker_engine_is_linux() -> bool:
435
435
  def _is_docker_container_active() -> bool:
436
436
  """Check if the local docker container is running and responding."""
437
437
  try:
438
- response = httpx.get("http://localhost/admin", timeout=REQUEST_TIMEOUT, follow_redirects=True)
438
+ response = httpx2.get("http://localhost/admin", timeout=REQUEST_TIMEOUT, follow_redirects=True)
439
439
  # Container is ready if we get a successful response or a redirect to login
440
440
  return response.status_code in (200, 302, 403)
441
- except (httpx.ConnectError, httpx.ReadError, httpx.TimeoutException):
441
+ except (httpx2.ConnectError, httpx2.ReadError, httpx2.TimeoutException):
442
442
  return False
443
443
 
444
444