wexample-api 0.0.74__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 (25) hide show
  1. wexample_api-0.0.74/PKG-INFO +188 -0
  2. wexample_api-0.0.74/README.md +167 -0
  3. wexample_api-0.0.74/pyproject.toml +88 -0
  4. wexample_api-0.0.74/src/wexample_api/__init__.py +0 -0
  5. wexample_api-0.0.74/src/wexample_api/__pycache__/__init__.py +0 -0
  6. wexample_api-0.0.74/src/wexample_api/common/__init__.py +0 -0
  7. wexample_api-0.0.74/src/wexample_api/common/__pycache__/__init__.py +0 -0
  8. wexample_api-0.0.74/src/wexample_api/common/abstract_gateway.py +333 -0
  9. wexample_api-0.0.74/src/wexample_api/common/http_request_payload.py +74 -0
  10. wexample_api-0.0.74/src/wexample_api/const/__init__.py +0 -0
  11. wexample_api-0.0.74/src/wexample_api/const/__pycache__/__init__.py +0 -0
  12. wexample_api-0.0.74/src/wexample_api/const/http.py +13 -0
  13. wexample_api-0.0.74/src/wexample_api/demo/__init__.py +0 -0
  14. wexample_api-0.0.74/src/wexample_api/demo/demo_simple_gateway.py +57 -0
  15. wexample_api-0.0.74/src/wexample_api/enums/__init__.py +0 -0
  16. wexample_api-0.0.74/src/wexample_api/enums/__pycache__/__init__.py +0 -0
  17. wexample_api-0.0.74/src/wexample_api/enums/http.py +26 -0
  18. wexample_api-0.0.74/src/wexample_api/errors/__init__.py +0 -0
  19. wexample_api-0.0.74/src/wexample_api/errors/gateway_authentication_error.py +7 -0
  20. wexample_api-0.0.74/src/wexample_api/errors/gateway_connexion_error.py +7 -0
  21. wexample_api-0.0.74/src/wexample_api/py.typed +0 -0
  22. wexample_api-0.0.74/tests/common/__init__.py +0 -0
  23. wexample_api-0.0.74/tests/common/test_http_request_payload.py +108 -0
  24. wexample_api-0.0.74/tests/demo/__init__.py +0 -0
  25. wexample_api-0.0.74/tests/demo/test_demo_simple_gateway.py +145 -0
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.1
2
+ Name: wexample-api
3
+ Version: 0.0.74
4
+ Summary: Some python basic helpers for apis.
5
+ Author-Email: weeger <contact@wexample.com>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Project-URL: homepage, https://github.com/wexample/python-api
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: attrs>=23.1.0
13
+ Requires-Dist: cattrs>=23.1.0
14
+ Requires-Dist: requests
15
+ Requires-Dist: wexample-helpers==0.0.78
16
+ Requires-Dist: wexample-prompt==0.0.90
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest; extra == "dev"
19
+ Requires-Dist: pytest-cov; extra == "dev"
20
+ Description-Content-Type: text/markdown
21
+
22
+ # wexample-api
23
+
24
+ Version: 0.0.74
25
+
26
+ Some python basic helpers for apis.
27
+
28
+ ## Tests
29
+
30
+ This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
31
+
32
+ ### Installation
33
+
34
+ First, install the required testing dependencies:
35
+ ```bash
36
+ .venv/bin/python -m pip install pytest pytest-cov
37
+ ```
38
+
39
+ ### Basic Usage
40
+
41
+ Run all tests with coverage:
42
+ ```bash
43
+ .venv/bin/python -m pytest --cov --cov-report=html
44
+ ```
45
+
46
+ ### Common Commands
47
+ ```bash
48
+ # Run tests with coverage for a specific module
49
+ .venv/bin/python -m pytest --cov=your_module
50
+
51
+ # Show which lines are not covered
52
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
53
+
54
+ # Generate an HTML coverage report
55
+ .venv/bin/python -m pytest --cov=your_module --cov-report=html
56
+
57
+ # Combine terminal and HTML reports
58
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
59
+
60
+ # Run specific test file with coverage
61
+ .venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
62
+ ```
63
+
64
+ ### Viewing HTML Reports
65
+
66
+ After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.
67
+
68
+ ### Coverage Threshold
69
+
70
+ To enforce a minimum coverage percentage:
71
+ ```bash
72
+ .venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
73
+ ```
74
+
75
+ This will cause the test suite to fail if coverage drops below 80%.
76
+
77
+ ## Code Quality & Typing
78
+
79
+ All the suite packages follow strict quality standards:
80
+
81
+ - **Type hints**: Full type coverage with mypy validation
82
+ - **Code formatting**: Enforced with black and isort
83
+ - **Linting**: Comprehensive checks with custom scripts and tools
84
+ - **Testing**: High test coverage requirements
85
+
86
+ These standards ensure reliability and maintainability across the suite.
87
+
88
+ ## Versioning & Compatibility Policy
89
+
90
+ Wexample packages follow **Semantic Versioning** (SemVer):
91
+
92
+ - **MAJOR**: Breaking changes
93
+ - **MINOR**: New features, backward compatible
94
+ - **PATCH**: Bug fixes, backward compatible
95
+
96
+ We maintain backward compatibility within major versions and provide clear migration guides for breaking changes.
97
+
98
+ ## Changelog
99
+
100
+ See [CHANGELOG.md](CHANGELOG.md) for detailed version history and release notes.
101
+
102
+ Major changes are documented with migration guides when applicable.
103
+
104
+ ## Migration Notes
105
+
106
+ When upgrading between major versions, refer to the migration guides in the documentation.
107
+
108
+ Breaking changes are clearly documented with upgrade paths and examples.
109
+
110
+ ## Known Limitations & Roadmap
111
+
112
+ Current limitations and planned features are tracked in the GitHub issues.
113
+
114
+ See the [project roadmap](https://github.com/wexample/python-api/issues) for upcoming features and improvements.
115
+
116
+ ## Security Policy
117
+
118
+ ### Reporting Vulnerabilities
119
+
120
+ If you discover a security vulnerability, please email security@wexample.com.
121
+
122
+ **Do not** open public issues for security vulnerabilities.
123
+
124
+ We take security seriously and will respond promptly to verified reports.
125
+
126
+ ## Privacy & Telemetry
127
+
128
+ This package does **not** collect any telemetry or usage data.
129
+
130
+ Your privacy is respected — no data is transmitted to external services.
131
+
132
+ ## Support Channels
133
+
134
+ - **GitHub Issues**: Bug reports and feature requests
135
+ - **GitHub Discussions**: Questions and community support
136
+ - **Documentation**: Comprehensive guides and API reference
137
+ - **Email**: contact@wexample.com for general inquiries
138
+
139
+ Community support is available through GitHub Discussions.
140
+
141
+ ## Contribution Guidelines
142
+
143
+ We welcome contributions to the Wexample suite!
144
+
145
+ ### How to Contribute
146
+
147
+ 1. **Fork** the repository
148
+ 2. **Create** a feature branch
149
+ 3. **Make** your changes
150
+ 4. **Test** thoroughly
151
+ 5. **Submit** a pull request
152
+
153
+ ## Maintainers & Authors
154
+
155
+ Maintained by the Wexample team and community contributors.
156
+
157
+ See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the full list of contributors.
158
+
159
+ ## License
160
+
161
+ MIT
162
+
163
+ ## Useful Links
164
+
165
+ - **Homepage**: https://github.com/wexample/python-api
166
+ - **Documentation**: [docs.wexample.com](https://docs.wexample.com)
167
+ - **Issue Tracker**: https://github.com/wexample/python-api/issues
168
+ - **Discussions**: https://github.com/wexample/python-api/discussions
169
+ - **PyPI**: [pypi.org/project/wexample-api](https://pypi.org/project/wexample-api/)
170
+
171
+ ## Integration in the Suite
172
+
173
+ This package is part of the **Wexample Suite** — a collection of high-quality Python packages designed to work seamlessly together.
174
+
175
+ ### Related Packages
176
+
177
+ The suite includes packages for configuration management, file handling, prompts, and more. Each package can be used independently or as part of the integrated suite.
178
+
179
+ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the complete package ecosystem.
180
+
181
+ # About us
182
+
183
+ Wexample stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.
184
+
185
+ This packages suite embodies this spirit. Trusted by professionals and enthusiasts alike, it delivers a consistent, high-quality foundation for modern development — open, elegant, and battle-tested. Its reputation is built on years of collaboration, refinement, and rigorous attention to detail, making it a natural choice for those who demand both robustness and beauty in their tools.
186
+
187
+ Wexample cultivates a culture of mastery. Each package, each contribution carries the mark of a community that values precision, ethics, and innovation — a community proud to shape the future of digital craftsmanship.
188
+
@@ -0,0 +1,167 @@
1
+ # wexample-api
2
+
3
+ Version: 0.0.74
4
+
5
+ Some python basic helpers for apis.
6
+
7
+ ## Tests
8
+
9
+ This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
10
+
11
+ ### Installation
12
+
13
+ First, install the required testing dependencies:
14
+ ```bash
15
+ .venv/bin/python -m pip install pytest pytest-cov
16
+ ```
17
+
18
+ ### Basic Usage
19
+
20
+ Run all tests with coverage:
21
+ ```bash
22
+ .venv/bin/python -m pytest --cov --cov-report=html
23
+ ```
24
+
25
+ ### Common Commands
26
+ ```bash
27
+ # Run tests with coverage for a specific module
28
+ .venv/bin/python -m pytest --cov=your_module
29
+
30
+ # Show which lines are not covered
31
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
32
+
33
+ # Generate an HTML coverage report
34
+ .venv/bin/python -m pytest --cov=your_module --cov-report=html
35
+
36
+ # Combine terminal and HTML reports
37
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
38
+
39
+ # Run specific test file with coverage
40
+ .venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
41
+ ```
42
+
43
+ ### Viewing HTML Reports
44
+
45
+ After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.
46
+
47
+ ### Coverage Threshold
48
+
49
+ To enforce a minimum coverage percentage:
50
+ ```bash
51
+ .venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
52
+ ```
53
+
54
+ This will cause the test suite to fail if coverage drops below 80%.
55
+
56
+ ## Code Quality & Typing
57
+
58
+ All the suite packages follow strict quality standards:
59
+
60
+ - **Type hints**: Full type coverage with mypy validation
61
+ - **Code formatting**: Enforced with black and isort
62
+ - **Linting**: Comprehensive checks with custom scripts and tools
63
+ - **Testing**: High test coverage requirements
64
+
65
+ These standards ensure reliability and maintainability across the suite.
66
+
67
+ ## Versioning & Compatibility Policy
68
+
69
+ Wexample packages follow **Semantic Versioning** (SemVer):
70
+
71
+ - **MAJOR**: Breaking changes
72
+ - **MINOR**: New features, backward compatible
73
+ - **PATCH**: Bug fixes, backward compatible
74
+
75
+ We maintain backward compatibility within major versions and provide clear migration guides for breaking changes.
76
+
77
+ ## Changelog
78
+
79
+ See [CHANGELOG.md](CHANGELOG.md) for detailed version history and release notes.
80
+
81
+ Major changes are documented with migration guides when applicable.
82
+
83
+ ## Migration Notes
84
+
85
+ When upgrading between major versions, refer to the migration guides in the documentation.
86
+
87
+ Breaking changes are clearly documented with upgrade paths and examples.
88
+
89
+ ## Known Limitations & Roadmap
90
+
91
+ Current limitations and planned features are tracked in the GitHub issues.
92
+
93
+ See the [project roadmap](https://github.com/wexample/python-api/issues) for upcoming features and improvements.
94
+
95
+ ## Security Policy
96
+
97
+ ### Reporting Vulnerabilities
98
+
99
+ If you discover a security vulnerability, please email security@wexample.com.
100
+
101
+ **Do not** open public issues for security vulnerabilities.
102
+
103
+ We take security seriously and will respond promptly to verified reports.
104
+
105
+ ## Privacy & Telemetry
106
+
107
+ This package does **not** collect any telemetry or usage data.
108
+
109
+ Your privacy is respected — no data is transmitted to external services.
110
+
111
+ ## Support Channels
112
+
113
+ - **GitHub Issues**: Bug reports and feature requests
114
+ - **GitHub Discussions**: Questions and community support
115
+ - **Documentation**: Comprehensive guides and API reference
116
+ - **Email**: contact@wexample.com for general inquiries
117
+
118
+ Community support is available through GitHub Discussions.
119
+
120
+ ## Contribution Guidelines
121
+
122
+ We welcome contributions to the Wexample suite!
123
+
124
+ ### How to Contribute
125
+
126
+ 1. **Fork** the repository
127
+ 2. **Create** a feature branch
128
+ 3. **Make** your changes
129
+ 4. **Test** thoroughly
130
+ 5. **Submit** a pull request
131
+
132
+ ## Maintainers & Authors
133
+
134
+ Maintained by the Wexample team and community contributors.
135
+
136
+ See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the full list of contributors.
137
+
138
+ ## License
139
+
140
+ MIT
141
+
142
+ ## Useful Links
143
+
144
+ - **Homepage**: https://github.com/wexample/python-api
145
+ - **Documentation**: [docs.wexample.com](https://docs.wexample.com)
146
+ - **Issue Tracker**: https://github.com/wexample/python-api/issues
147
+ - **Discussions**: https://github.com/wexample/python-api/discussions
148
+ - **PyPI**: [pypi.org/project/wexample-api](https://pypi.org/project/wexample-api/)
149
+
150
+ ## Integration in the Suite
151
+
152
+ This package is part of the **Wexample Suite** — a collection of high-quality Python packages designed to work seamlessly together.
153
+
154
+ ### Related Packages
155
+
156
+ The suite includes packages for configuration management, file handling, prompts, and more. Each package can be used independently or as part of the integrated suite.
157
+
158
+ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the complete package ecosystem.
159
+
160
+ # About us
161
+
162
+ Wexample stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.
163
+
164
+ This packages suite embodies this spirit. Trusted by professionals and enthusiasts alike, it delivers a consistent, high-quality foundation for modern development — open, elegant, and battle-tested. Its reputation is built on years of collaboration, refinement, and rigorous attention to detail, making it a natural choice for those who demand both robustness and beauty in their tools.
165
+
166
+ Wexample cultivates a culture of mastery. Each package, each contribution carries the mark of a community that values precision, ethics, and innovation — a community proud to shape the future of digital craftsmanship.
167
+
@@ -0,0 +1,88 @@
1
+ [build-system]
2
+ requires = [
3
+ "pdm-backend",
4
+ ]
5
+ build-backend = "pdm.backend"
6
+
7
+ [project]
8
+ name = "wexample-api"
9
+ version = "0.0.74"
10
+ description = "Some python basic helpers for apis."
11
+ authors = [
12
+ { name = "weeger", email = "contact@wexample.com" },
13
+ ]
14
+ requires-python = ">=3.10"
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+ dependencies = [
21
+ "attrs>=23.1.0",
22
+ "cattrs>=23.1.0",
23
+ "requests",
24
+ "wexample-helpers==0.0.78",
25
+ "wexample-prompt==0.0.90",
26
+ ]
27
+
28
+ [project.readme]
29
+ file = "README.md"
30
+ content-type = "text/markdown"
31
+
32
+ [project.license]
33
+ text = "MIT"
34
+
35
+ [project.urls]
36
+ homepage = "https://github.com/wexample/python-api"
37
+
38
+ [project.optional-dependencies]
39
+ dev = [
40
+ "pytest",
41
+ "pytest-cov",
42
+ ]
43
+
44
+ [tool.setuptools.packages.find]
45
+ include = [
46
+ "*",
47
+ ]
48
+ exclude = [
49
+ "wexample_api.testing*",
50
+ ]
51
+
52
+ [tool.pdm]
53
+ distribution = true
54
+
55
+ [tool.pdm.build]
56
+ package-dir = "src"
57
+ packages = [
58
+ { include = "wexample_api", from = "src" },
59
+ ]
60
+
61
+ [tool.pytest.ini_options]
62
+ testpaths = [
63
+ "tests",
64
+ ]
65
+ pythonpath = [
66
+ "src",
67
+ ]
68
+
69
+ [tool.coverage.run]
70
+ source = [
71
+ "wexample_api",
72
+ ]
73
+ omit = [
74
+ "*/tests/*",
75
+ "*/.venv/*",
76
+ "*/venv/*",
77
+ ]
78
+
79
+ [tool.coverage.report]
80
+ exclude_lines = [
81
+ "pragma: no cover",
82
+ "def __repr__",
83
+ "raise AssertionError",
84
+ "raise NotImplementedError",
85
+ "if __name__ == .__main__.:",
86
+ "if TYPE_CHECKING:",
87
+ "@abstractmethod",
88
+ ]
File without changes
@@ -0,0 +1,333 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from collections.abc import Mapping
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ import requests
8
+ from wexample_helpers.classes.field import public_field
9
+ from wexample_helpers.classes.mixin.has_snake_short_class_name_class_mixin import (
10
+ HasSnakeShortClassNameClassMixin,
11
+ )
12
+ from wexample_helpers.classes.mixin.has_two_steps_init import HasTwoStepInit
13
+ from wexample_helpers.decorator.base_class import base_class
14
+ from wexample_prompt.mixins.with_io_manager import WithIoManager
15
+
16
+ from wexample_api.enums.http import ContentType, HttpMethod
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Mapping
20
+
21
+ from wexample_helpers.const.types import StringsList
22
+
23
+ from wexample_api.common.http_request_payload import HttpRequestPayload
24
+ from wexample_api.enums.http import Header
25
+
26
+
27
+ @base_class
28
+ class AbstractGateway(
29
+ HasSnakeShortClassNameClassMixin,
30
+ WithIoManager,
31
+ HasTwoStepInit,
32
+ ):
33
+ # Base configuration
34
+ base_url: str | None = public_field(default=None, description="Base API URL")
35
+ # State
36
+ connected: bool = public_field(default=False, description="Connection state")
37
+ # Default request configuration
38
+ default_headers: dict[str, str] = public_field(
39
+ factory=dict, description="Default headers for requests"
40
+ )
41
+ last_exception: Any = public_field(
42
+ default=None, description="Last exception encountered during request"
43
+ )
44
+ last_request_time: float | None = public_field(
45
+ default=None, description="Timestamp of last request"
46
+ )
47
+ quiet: bool = public_field(
48
+ default=False, description="If True, only show errors and warnings"
49
+ )
50
+ rate_limit_delay: float = public_field(
51
+ default=1.0, description="Minimum delay between requests in seconds"
52
+ )
53
+ timeout: int = public_field(default=30, description="Request timeout in seconds")
54
+
55
+ @classmethod
56
+ def get_class_name_suffix(cls) -> str | None:
57
+ return "GatewayService"
58
+
59
+ def check_connexion(self) -> bool:
60
+ return self.connected
61
+
62
+ def check_status_code(self, expected_status_codes: int | list[int] = 200) -> bool:
63
+ return (
64
+ self.make_request(
65
+ endpoint="", expected_status_codes=expected_status_codes, quiet=True
66
+ )
67
+ is not None
68
+ )
69
+
70
+ def clear_error(self) -> None:
71
+ self.last_exception = None
72
+
73
+ def connect(self) -> bool:
74
+ self.connected = True
75
+ return True
76
+
77
+ def format_response_content(self, response: requests.Response | None) -> str:
78
+ """Extract and format response content for logging."""
79
+ if response is None:
80
+ return "Null response"
81
+
82
+ try:
83
+ return response.json()
84
+ except (ValueError, AttributeError):
85
+ return response.text
86
+
87
+ def get_base_url(self) -> str | None:
88
+ return self.base_url
89
+
90
+ def get_expected_env_keys(self) -> StringsList:
91
+ return []
92
+
93
+ def get_last_error(self) -> Exception | None:
94
+ return self.last_exception
95
+
96
+ def handle_api_response(
97
+ self,
98
+ response: requests.Response | None,
99
+ request_context: HttpRequestPayload,
100
+ exception: Exception | None = None,
101
+ fatal_on_error: bool = False,
102
+ quiet: bool | None = None,
103
+ ) -> requests.Response | None:
104
+ self.last_exception = exception
105
+ is_quiet = self.quiet if quiet is None else quiet
106
+
107
+ if response is None:
108
+ if not is_quiet:
109
+ self.io.properties(
110
+ self._create_request_details(request_context),
111
+ title="Request Details",
112
+ )
113
+ if exception:
114
+ self.io.error(str(exception), exception=exception, fatal=fatal_on_error)
115
+ return None
116
+
117
+ if not is_quiet:
118
+ self.io.debug(
119
+ message=f"{request_context.method} | {response.status_code} -> {request_context.url}",
120
+ symbol="🌍",
121
+ )
122
+
123
+ # If no filtering is configured, accept all responses
124
+ if request_context.expected_status_codes is None:
125
+ return response
126
+
127
+ if response.status_code in request_context.expected_status_codes:
128
+ return response
129
+
130
+ # Combine request details with response content
131
+ details = {
132
+ **self._create_request_details(request_context, response.status_code),
133
+ "Response Content": self.format_response_content(response),
134
+ }
135
+
136
+ if not is_quiet:
137
+ self.io.properties(details, title="Request Details")
138
+
139
+ self.io.error(
140
+ message=(
141
+ str(exception) if exception else self._extract_error_message(response)
142
+ ),
143
+ exception=exception,
144
+ fatal=fatal_on_error,
145
+ )
146
+ return response
147
+
148
+ def has_error(self) -> bool:
149
+ return self.last_exception is not None
150
+
151
+ def make_request(
152
+ self,
153
+ endpoint: str,
154
+ method: HttpMethod = HttpMethod.GET,
155
+ data: dict[str, Any] | bytes | None = None,
156
+ query_params: dict[str, Any] | None = None,
157
+ headers: dict[str, str] | None = None,
158
+ files: dict[str, Any] | list[tuple] | None = None,
159
+ call_origin: str | None = None,
160
+ expected_status_codes: int | list[int] | None = None,
161
+ fatal_if_unexpected: bool = False,
162
+ quiet: bool = False,
163
+ stream: bool = False,
164
+ timeout: int | None = None,
165
+ raise_exceptions: bool = False,
166
+ ) -> requests.Response | None:
167
+ from wexample_helpers.errors.gateway_error import GatewayError
168
+
169
+ from wexample_api.common.http_request_payload import HttpRequestPayload
170
+ from wexample_api.enums.http import Header
171
+
172
+ payload = HttpRequestPayload.from_endpoint(
173
+ base_url=self.get_base_url(),
174
+ endpoint=endpoint,
175
+ method=method,
176
+ data=data,
177
+ query_params=query_params,
178
+ headers={**self.default_headers, **(headers or {})},
179
+ call_origin=call_origin,
180
+ expected_status_codes=expected_status_codes,
181
+ )
182
+
183
+ if not self.connected:
184
+ self.connect()
185
+
186
+ self._handle_rate_limiting()
187
+
188
+ # Determine how to send the data based on Content-Type header
189
+ content_type = self._get_header_value(payload.headers, Header.CONTENT_TYPE)
190
+
191
+ if files:
192
+ content_type = ContentType.MULTIPART.value
193
+ payload.headers.pop(Header.CONTENT_TYPE.value, None)
194
+
195
+ if payload.data is not None:
196
+ if isinstance(payload.data, bytes):
197
+ self.io.log(f"Sending binary payload ({len(payload.data)} bytes)")
198
+ else:
199
+ self.io.log(f"Sending {type(payload.data).__name__} payload")
200
+
201
+ request_kwargs: dict[str, Any] = {
202
+ "method": payload.method.value,
203
+ "url": payload.url,
204
+ "params": payload.query_params,
205
+ "headers": payload.headers,
206
+ "timeout": timeout or self.timeout,
207
+ "stream": stream,
208
+ }
209
+
210
+ if files:
211
+ request_kwargs["data"] = data or {}
212
+ request_kwargs["files"] = files
213
+ elif content_type in (
214
+ ContentType.FORM_URLENCODED.value,
215
+ ContentType.OCTET_STREAM.value,
216
+ ContentType.TEXT.value,
217
+ ):
218
+ request_kwargs["data"] = data
219
+ else:
220
+ request_kwargs["json"] = data
221
+
222
+ try:
223
+ response = requests.request(**request_kwargs)
224
+ except requests.exceptions.RequestException as exc:
225
+ gateway_error = GatewayError(f"Request failed: {exc}")
226
+ gateway_error.__cause__ = exc
227
+
228
+ if raise_exceptions:
229
+ raise gateway_error
230
+
231
+ return self.handle_api_response(
232
+ response=None,
233
+ request_context=payload,
234
+ exception=gateway_error,
235
+ fatal_on_error=fatal_if_unexpected,
236
+ quiet=quiet,
237
+ )
238
+
239
+ # Only check status code if expected_status_codes is explicitly provided
240
+ exception = None
241
+ if expected_status_codes is not None:
242
+ expected = (
243
+ {expected_status_codes}
244
+ if isinstance(expected_status_codes, int)
245
+ else set(expected_status_codes)
246
+ )
247
+ if response.status_code not in expected:
248
+ exception = GatewayError(self._extract_error_message(response))
249
+ exception.response = (
250
+ response # Attach response to exception for debugging
251
+ )
252
+ if raise_exceptions and exception:
253
+ raise exception
254
+
255
+ return self.handle_api_response(
256
+ response=response,
257
+ request_context=payload,
258
+ exception=exception,
259
+ fatal_on_error=fatal_if_unexpected,
260
+ quiet=quiet,
261
+ )
262
+
263
+ def setup(self) -> AbstractGateway:
264
+ if self.default_headers is None:
265
+ self.default_headers = {}
266
+
267
+ return self
268
+
269
+ def _create_request_details(
270
+ self, request_context: HttpRequestPayload, status_code: int | None = None
271
+ ) -> dict[str, Any]:
272
+ """Create request details dictionary for logging."""
273
+ from wexample_helpers.helpers.cli import cli_make_clickable_path
274
+
275
+ details: dict[str, Any] = {
276
+ "URL": request_context.url,
277
+ "Method": request_context.method,
278
+ }
279
+ if request_context.call_origin:
280
+ details["Call Origin"] = cli_make_clickable_path(
281
+ request_context.call_origin
282
+ )
283
+ if request_context.data:
284
+ if isinstance(request_context.data, bytes):
285
+ details["Data"] = f"<Binary data: {len(request_context.data)} bytes>"
286
+ else:
287
+ details["Data"] = request_context.data
288
+ if request_context.query_params:
289
+ details["Query Parameters"] = request_context.query_params
290
+ if status_code is not None:
291
+ details["Status"] = status_code
292
+ return details
293
+
294
+ def _extract_error_message(self, response: requests.Response) -> str:
295
+ """Extract error message from response."""
296
+ message = f"HTTP {response.status_code}"
297
+ try:
298
+ data = response.json()
299
+ if isinstance(data, dict):
300
+ message = data.get("message", data.get("error", message))
301
+ except (ValueError, AttributeError):
302
+ if response.text:
303
+ message = response.text
304
+ return message
305
+
306
+ def _get_header_value(
307
+ self,
308
+ headers: Mapping[str, str] | None,
309
+ name: Header,
310
+ ) -> str | None:
311
+ """
312
+ Case-insensitive lookup of a header followed by normalisation:
313
+ - keep only the part before the first ';'
314
+ - trim whitespace
315
+ - convert to lower-case
316
+ """
317
+ if not headers:
318
+ return None
319
+ raw = next(
320
+ (v for k, v in headers.items() if k.lower() == name.value.lower()),
321
+ None,
322
+ )
323
+ if raw is None:
324
+ return None
325
+
326
+ return raw.split(";", 1)[0].strip().lower() or None
327
+
328
+ def _handle_rate_limiting(self) -> None:
329
+ if self.last_request_time is not None:
330
+ elapsed = time.time() - self.last_request_time
331
+ if elapsed < self.rate_limit_delay:
332
+ time.sleep(self.rate_limit_delay - elapsed)
333
+ self.last_request_time = time.time()
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from wexample_helpers.classes.base_class import BaseClass
6
+ from wexample_helpers.classes.field import public_field
7
+ from wexample_helpers.decorator.base_class import base_class
8
+
9
+ from wexample_api.enums.http import HttpMethod
10
+
11
+
12
+ @base_class
13
+ class HttpRequestPayload(BaseClass):
14
+ call_origin: str | None = public_field(
15
+ default=None,
16
+ description="Optional identifier of the request origin",
17
+ )
18
+ data: dict[str, Any] | bytes | None = public_field(
19
+ default=None,
20
+ description="Request body as a dictionary, raw bytes, or None",
21
+ )
22
+ expected_status_codes: list[int] | None = public_field(
23
+ default=None,
24
+ description="Optional list of expected HTTP status codes. If None, all responses are accepted.",
25
+ )
26
+ headers: dict[str, str] | None = public_field(
27
+ default=None,
28
+ description="Optional HTTP headers for the request",
29
+ )
30
+ method: HttpMethod = public_field(
31
+ default=HttpMethod.GET,
32
+ description="HTTP method to use for the request",
33
+ )
34
+ query_params: dict[str, Any] | None = public_field(
35
+ default=None,
36
+ description="Optional query parameters to append to the URL",
37
+ )
38
+ url: str = public_field(
39
+ description="Target URL for the HTTP request",
40
+ )
41
+
42
+ @classmethod
43
+ def from_endpoint(
44
+ cls,
45
+ base_url: str | None,
46
+ endpoint: str,
47
+ method: HttpMethod = HttpMethod.GET,
48
+ data: dict[str, Any] | bytes | None = None,
49
+ query_params: dict[str, Any] | None = None,
50
+ headers: dict[str, str] | None = None,
51
+ call_origin: str | None = None,
52
+ expected_status_codes: int | list[int] | None = None,
53
+ ) -> HttpRequestPayload:
54
+ if base_url:
55
+ url = f"{base_url.rstrip('/')}/{endpoint.lstrip('/')}"
56
+ else:
57
+ url = endpoint
58
+
59
+ if isinstance(expected_status_codes, int):
60
+ expected_status_codes = [expected_status_codes]
61
+
62
+ return cls(
63
+ url=url,
64
+ method=method,
65
+ data=data,
66
+ query_params=query_params,
67
+ headers=headers,
68
+ call_origin=call_origin,
69
+ expected_status_codes=expected_status_codes,
70
+ )
71
+
72
+ @classmethod
73
+ def from_url(cls, url: str, call_origin: str | None = None) -> HttpRequestPayload:
74
+ return cls(url=url, call_origin=call_origin)
File without changes
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from wexample_api.enums.http import HttpMethod
4
+
5
+ HTTP_METHOD_MAP = {
6
+ "GET": HttpMethod.GET,
7
+ "POST": HttpMethod.POST,
8
+ "PUT": HttpMethod.PUT,
9
+ "DELETE": HttpMethod.DELETE,
10
+ "PATCH": HttpMethod.PATCH,
11
+ "OPTIONS": HttpMethod.OPTIONS,
12
+ "HEAD": HttpMethod.HEAD,
13
+ }
File without changes
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from wexample_helpers.decorator.base_class import base_class
6
+
7
+ from wexample_api.common.abstract_gateway import AbstractGateway
8
+
9
+
10
+ @base_class
11
+ class DemoSimpleGateway(AbstractGateway):
12
+ """A simple implementation of AbstractGateway for demonstration purposes."""
13
+
14
+ def check_connection(self) -> bool:
15
+ # Always return True for demo purposes
16
+ return True
17
+
18
+ def create_item(self, item_data: dict[str, Any]) -> dict[str, Any]:
19
+ """Demo method to create an item."""
20
+ from wexample_api.enums.http import HttpMethod
21
+
22
+ response = self.make_request(
23
+ method=HttpMethod.POST,
24
+ endpoint="/items",
25
+ data=item_data,
26
+ call_origin=__file__,
27
+ )
28
+ return response.json()
29
+
30
+ def delete_item(self, item_id: str) -> None:
31
+ """Demo method to delete an item."""
32
+ from wexample_api.enums.http import HttpMethod
33
+
34
+ self.make_request(
35
+ method=HttpMethod.DELETE, endpoint=f"/items/{item_id}", call_origin=__file__
36
+ )
37
+
38
+ def get_user_info(self) -> dict[str, Any]:
39
+ """Demo method to get user information."""
40
+ from wexample_api.enums.http import HttpMethod
41
+
42
+ response = self.make_request(
43
+ method=HttpMethod.GET, endpoint="/user", call_origin=__file__
44
+ )
45
+ return response.json()
46
+
47
+ def update_item(self, item_id: str, item_data: dict[str, Any]) -> dict[str, Any]:
48
+ """Demo method to update an item."""
49
+ from wexample_api.enums.http import HttpMethod
50
+
51
+ response = self.make_request(
52
+ method=HttpMethod.PUT,
53
+ endpoint=f"/items/{item_id}",
54
+ data=item_data,
55
+ call_origin=__file__,
56
+ )
57
+ return response.json()
File without changes
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class HttpMethod(Enum):
7
+ GET: str = "GET"
8
+ POST: str = "POST"
9
+ PUT: str = "PUT"
10
+ DELETE: str = "DELETE"
11
+ PATCH: str = "PATCH"
12
+ OPTIONS: str = "OPTIONS"
13
+ HEAD: str = "HEAD"
14
+
15
+
16
+ class ContentType(Enum):
17
+ JSON: str = "application/json"
18
+ FORM_URLENCODED: str = "application/x-www-form-urlencoded"
19
+ MULTIPART: str = "multipart/form-data"
20
+ TEXT: str = "text/plain"
21
+ OCTET_STREAM: str = "application/octet-stream"
22
+
23
+
24
+ class Header(Enum):
25
+ CONTENT_TYPE: str = "Content-Type"
26
+ AUTHORIZATION: str = "Authorization"
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from wexample_helpers.errors.gateway_error import GatewayError
4
+
5
+
6
+ class GatewayAuthenticationError(GatewayError):
7
+ """Raised when authentication to the API fails."""
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from wexample_helpers.errors.gateway_error import GatewayError
4
+
5
+
6
+ class GatewayConnectionError(GatewayError):
7
+ """Raised when connection to the API fails."""
File without changes
File without changes
@@ -0,0 +1,108 @@
1
+ """Tests for HttpRequestPayload class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import unittest
6
+
7
+ from wexample_api.enums.http import HttpMethod
8
+
9
+
10
+ class TestHttpRequestPayload(unittest.TestCase):
11
+ """Test cases for HttpRequestPayload class."""
12
+
13
+ def test_direct_instantiation(self) -> None:
14
+ """Test direct instantiation of HttpRequestPayload."""
15
+ from wexample_api.common.http_request_payload import HttpRequestPayload
16
+
17
+ url = "https://api.example.com/endpoint"
18
+ method = HttpMethod.PUT
19
+ data = {"status": "active"}
20
+ query_params = {"version": "2"}
21
+ headers = {"Content-Type": "application/json"}
22
+
23
+ payload = HttpRequestPayload(
24
+ url=url,
25
+ method=method,
26
+ data=data,
27
+ query_params=query_params,
28
+ headers=headers,
29
+ )
30
+
31
+ self.assertEqual(payload.url, url)
32
+ self.assertEqual(payload.method, method)
33
+ self.assertEqual(payload.data, data)
34
+ self.assertEqual(payload.query_params, query_params)
35
+ self.assertEqual(payload.headers, headers)
36
+
37
+ def test_from_endpoint_basic(self) -> None:
38
+ """Test creating HttpRequestPayload from endpoint with basic parameters."""
39
+ from wexample_api.common.http_request_payload import HttpRequestPayload
40
+
41
+ base_url = "https://api.example.com"
42
+ endpoint = "/users"
43
+ payload = HttpRequestPayload.from_endpoint(base_url, endpoint)
44
+
45
+ self.assertEqual(payload.url, "https://api.example.com/users")
46
+ self.assertEqual(payload.method, HttpMethod.GET)
47
+ self.assertIsNone(payload.data)
48
+ self.assertIsNone(payload.query_params)
49
+ self.assertIsNone(payload.headers)
50
+
51
+ def test_from_endpoint_with_all_parameters(self) -> None:
52
+ """Test creating HttpRequestPayload with all optional parameters."""
53
+ from wexample_api.common.http_request_payload import HttpRequestPayload
54
+
55
+ base_url = "https://api.example.com"
56
+ endpoint = "/users"
57
+ method = HttpMethod.POST
58
+ data = {"name": "John", "age": 30}
59
+ query_params = {"filter": "active"}
60
+ headers = {"Authorization": "Bearer token"}
61
+
62
+ payload = HttpRequestPayload.from_endpoint(
63
+ base_url=base_url,
64
+ endpoint=endpoint,
65
+ method=method,
66
+ data=data,
67
+ query_params=query_params,
68
+ headers=headers,
69
+ )
70
+
71
+ self.assertEqual(payload.url, "https://api.example.com/users")
72
+ self.assertEqual(payload.method, HttpMethod.POST)
73
+ self.assertEqual(payload.data, data)
74
+ self.assertEqual(payload.query_params, query_params)
75
+ self.assertEqual(payload.headers, headers)
76
+
77
+ def test_from_endpoint_with_trailing_slash(self) -> None:
78
+ """Test creating HttpRequestPayload with trailing slash in base_url."""
79
+ from wexample_api.common.http_request_payload import HttpRequestPayload
80
+
81
+ base_url = "https://api.example.com/"
82
+ endpoint = "/users"
83
+ payload = HttpRequestPayload.from_endpoint(base_url, endpoint)
84
+
85
+ self.assertEqual(payload.url, "https://api.example.com/users")
86
+
87
+ def test_from_endpoint_without_leading_slash(self) -> None:
88
+ """Test creating HttpRequestPayload without leading slash in endpoint."""
89
+ from wexample_api.common.http_request_payload import HttpRequestPayload
90
+
91
+ base_url = "https://api.example.com"
92
+ endpoint = "users"
93
+ payload = HttpRequestPayload.from_endpoint(base_url, endpoint)
94
+
95
+ self.assertEqual(payload.url, "https://api.example.com/users")
96
+
97
+ def test_from_url(self) -> None:
98
+ """Test creating HttpRequestPayload from URL."""
99
+ from wexample_api.common.http_request_payload import HttpRequestPayload
100
+
101
+ url = "https://api.example.com/endpoint"
102
+ payload = HttpRequestPayload.from_url(url)
103
+
104
+ self.assertEqual(payload.url, url)
105
+ self.assertEqual(payload.method, HttpMethod.GET)
106
+ self.assertIsNone(payload.data)
107
+ self.assertIsNone(payload.query_params)
108
+ self.assertIsNone(payload.headers)
File without changes
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+ from unittest.mock import patch
5
+
6
+ import pytest
7
+
8
+ if TYPE_CHECKING:
9
+ from unittest.mock import MagicMock
10
+
11
+ from wexample_prompt.common.io_manager import IoManager
12
+
13
+ from wexample_api.demo.demo_simple_gateway import DemoSimpleGateway
14
+
15
+
16
+ def create_mock_response(status_code=200, json_data=None) -> MagicMock:
17
+ from unittest.mock import MagicMock
18
+
19
+ from requests import Response
20
+
21
+ mock_response = MagicMock(spec=Response)
22
+ mock_response.status_code = status_code
23
+ mock_response.json.return_value = json_data or {}
24
+ return mock_response
25
+
26
+
27
+ @pytest.fixture
28
+ def gateway(io_manager) -> DemoSimpleGateway:
29
+ from wexample_api.demo.demo_simple_gateway import DemoSimpleGateway
30
+
31
+ return DemoSimpleGateway(base_url="https://api.example.com", io=io_manager)
32
+
33
+
34
+ @pytest.fixture
35
+ def io_manager() -> IoManager:
36
+ from wexample_prompt.common.io_manager import IoManager
37
+
38
+ return IoManager()
39
+
40
+
41
+ def test_check_connection(gateway) -> None:
42
+ assert gateway.check_connection() is True
43
+
44
+
45
+ @patch("requests.request")
46
+ def test_create_item(mock_request, gateway) -> None:
47
+ # Arrange
48
+ item_data = {"name": "Test Item"}
49
+ expected_response = {"id": 1, **item_data}
50
+ mock_request.return_value = create_mock_response(json_data=expected_response)
51
+ gateway.connected = True
52
+
53
+ # Act
54
+ result = gateway.create_item(item_data)
55
+
56
+ # Assert
57
+ assert result == expected_response
58
+ mock_request.assert_called_once_with(
59
+ method="POST",
60
+ url="https://api.example.com/items",
61
+ json=item_data,
62
+ params=None,
63
+ headers={},
64
+ timeout=30,
65
+ stream=False,
66
+ )
67
+
68
+
69
+ @patch("requests.request")
70
+ def test_delete_item(mock_request, gateway) -> None:
71
+ # Arrange
72
+ item_id = "123"
73
+ mock_request.return_value = create_mock_response()
74
+ gateway.connected = True
75
+
76
+ # Act
77
+ gateway.delete_item(item_id)
78
+
79
+ # Assert
80
+ mock_request.assert_called_once_with(
81
+ method="DELETE",
82
+ url=f"https://api.example.com/items/{item_id}",
83
+ json=None,
84
+ params=None,
85
+ headers={},
86
+ timeout=30,
87
+ stream=False,
88
+ )
89
+
90
+
91
+ @patch("requests.request")
92
+ def test_get_user_info(mock_request, gateway) -> None:
93
+ # Arrange
94
+ expected_data = {"id": 1, "name": "Test User"}
95
+ mock_request.return_value = create_mock_response(json_data=expected_data)
96
+ gateway.connected = True
97
+
98
+ # Act
99
+ result = gateway.get_user_info()
100
+
101
+ # Assert
102
+ assert result == expected_data
103
+ mock_request.assert_called_once_with(
104
+ method="GET",
105
+ url="https://api.example.com/user",
106
+ json=None,
107
+ params=None,
108
+ headers={},
109
+ timeout=30,
110
+ stream=False,
111
+ )
112
+
113
+
114
+ def test_not_connected_error(gateway) -> None:
115
+ # Arrange
116
+ gateway.connected = False
117
+
118
+ # Act & Assert
119
+ with pytest.raises(AttributeError):
120
+ gateway.get_user_info()
121
+
122
+
123
+ @patch("requests.request")
124
+ def test_update_item(mock_request, gateway) -> None:
125
+ # Arrange
126
+ item_id = "123"
127
+ item_data = {"name": "Updated Item"}
128
+ expected_response = {"id": item_id, **item_data}
129
+ mock_request.return_value = create_mock_response(json_data=expected_response)
130
+ gateway.connected = True
131
+
132
+ # Act
133
+ result = gateway.update_item(item_id, item_data)
134
+
135
+ # Assert
136
+ assert result == expected_response
137
+ mock_request.assert_called_once_with(
138
+ method="PUT",
139
+ url=f"https://api.example.com/items/{item_id}",
140
+ json=item_data,
141
+ params=None,
142
+ headers={},
143
+ timeout=30,
144
+ stream=False,
145
+ )