finalsa-common-http-client 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from importlib import metadata
5
+ from urllib.parse import urljoin
6
+
7
+ from finalsa.traceability import get_w3c_traceparent, get_w3c_tracestate
8
+ from finalsa.traceability.functions import (
9
+ HTTP_HEADER_TRACEPARENT,
10
+ HTTP_HEADER_TRACESTATE,
11
+ )
12
+
13
+ Headers = Mapping[str, str]
14
+
15
+
16
+ def normalize_base_url(
17
+ base_url: str | None,
18
+ default_scheme: str,
19
+ ) -> str | None:
20
+ """Return a normalized base URL or None when no usable value is provided."""
21
+ if not base_url:
22
+ return None
23
+ value = base_url.strip()
24
+ if not value:
25
+ return None
26
+ if "://" not in value:
27
+ value = f"{default_scheme}://{value}"
28
+ return value.rstrip("/")
29
+
30
+
31
+ def ensure_scheme(candidate: str, default_scheme: str) -> str:
32
+ """Ensure that the provided candidate has an explicit scheme."""
33
+ trimmed = candidate.strip()
34
+ if "://" in trimmed:
35
+ return trimmed
36
+ return f"{default_scheme}://{trimmed}"
37
+
38
+
39
+ def build_url(
40
+ *,
41
+ path: str | None,
42
+ url: str | None,
43
+ base_url: str | None,
44
+ default_scheme: str,
45
+ ) -> str:
46
+ """Resolve the final request URL from the supplied inputs."""
47
+ if url:
48
+ return ensure_scheme(url, default_scheme)
49
+
50
+ if path is None:
51
+ raise ValueError("Either 'path' or 'url' must be provided.")
52
+
53
+ candidate = path.strip()
54
+ if candidate.startswith(("http://", "https://")):
55
+ return candidate
56
+ if base_url:
57
+ base = base_url if base_url.endswith("/") else f"{base_url}/"
58
+ return urljoin(base, candidate.lstrip("/"))
59
+ return ensure_scheme(candidate, default_scheme)
60
+
61
+
62
+ def merge_headers(
63
+ default_headers: Mapping[str, str],
64
+ headers: Mapping[str, str] | None,
65
+ ) -> dict[str, str]:
66
+ """Merge default headers with any per-request overrides."""
67
+ merged = dict(default_headers)
68
+ if headers:
69
+ merged.update(headers)
70
+ merged[HTTP_HEADER_TRACEPARENT] = get_w3c_traceparent()
71
+ merged[HTTP_HEADER_TRACESTATE] = get_w3c_tracestate()
72
+ return merged
73
+
74
+
75
+ def build_default_headers(
76
+ default_headers: Mapping[str, str],
77
+ user_headers: Mapping[str, str] | None,
78
+ service_name: str,
79
+ ) -> dict[str, str]:
80
+ """Construct the default header set for an HTTP client."""
81
+ headers = dict(default_headers)
82
+ headers.setdefault("User-Agent", f"{service_name}/{get_package_version()}")
83
+ if user_headers:
84
+ headers.update(user_headers)
85
+ return headers
86
+
87
+
88
+ def get_package_version(package_name: str = "finalsa-http-client") -> str:
89
+ """Return the installed package version or a sensible fallback in dev mode."""
90
+ try:
91
+ return metadata.version(package_name)
92
+ except metadata.PackageNotFoundError:
93
+ return "0.0.0"
94
+
@@ -0,0 +1,3 @@
1
+ from .base import BaseAsyncHttpClient
2
+
3
+ __all__ = ["BaseAsyncHttpClient"]
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ import aiohttp
7
+ from aiohttp import ClientSession, ClientTimeout
8
+
9
+ from finalsa.http import _shared as shared
10
+
11
+
12
+ class BaseAsyncHttpClient:
13
+ """Reusable aiohttp-based HTTP client with sane defaults."""
14
+
15
+ DEFAULT_HEADERS: Mapping[str, str] = {
16
+ "Accept": "application/json",
17
+ "Content-Type": "application/json",
18
+ }
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ base_url: str | None = None,
24
+ default_headers: Mapping[str, str] | None = None,
25
+ timeout: float | ClientTimeout | None = None,
26
+ default_scheme: str = "http",
27
+ service_name: str = "finalsa-http-client",
28
+ trust_env: bool = False,
29
+ raise_for_status: bool = True,
30
+ session: ClientSession | None = None,
31
+ ) -> None:
32
+ self._default_scheme = default_scheme
33
+ self.base_url = shared.normalize_base_url(base_url, default_scheme)
34
+ self._timeout = self._normalize_timeout(timeout)
35
+ self._trust_env = trust_env
36
+ self._raise_for_status = raise_for_status
37
+ self._session = session
38
+ self._owns_session = session is None
39
+ self._default_headers = shared.build_default_headers(
40
+ self.DEFAULT_HEADERS,
41
+ default_headers,
42
+ service_name,
43
+ )
44
+
45
+ async def __aenter__(self) -> BaseAsyncHttpClient:
46
+ await self._ensure_session()
47
+ return self
48
+
49
+ async def __aexit__(self, exc_type, exc, tb) -> None: # type: ignore[override]
50
+ await self.close()
51
+
52
+ @property
53
+ def session(self) -> ClientSession | None:
54
+ """Return the underlying aiohttp session, if it has been created."""
55
+ return self._session
56
+
57
+ @property
58
+ def default_headers(self) -> Mapping[str, str]:
59
+ """Return a copy of the default headers."""
60
+ return dict(self._default_headers)
61
+
62
+ def update_default_headers(self, headers: Mapping[str, str]) -> None:
63
+ """Update the headers that will be sent with every request."""
64
+ if not headers:
65
+ return
66
+ self._default_headers.update(headers)
67
+
68
+ def build_url(self, path_or_url: str) -> str:
69
+ """Resolve the final URL for a request."""
70
+ return shared.build_url(
71
+ path=path_or_url,
72
+ url=None,
73
+ base_url=self.base_url,
74
+ default_scheme=self._default_scheme,
75
+ )
76
+
77
+ async def request(
78
+ self,
79
+ method: str,
80
+ path: str | None = None,
81
+ *,
82
+ url: str | None = None,
83
+ headers: Mapping[str, str] | None = None,
84
+ params: Mapping[str, str | int | float | None] | None = None,
85
+ json: Any | None = None,
86
+ data: Any | None = None,
87
+ timeout: float | ClientTimeout | None = None,
88
+ **kwargs: Any,
89
+ ) -> aiohttp.ClientResponse:
90
+ """Send an HTTP request using aiohttp.ClientSession.request."""
91
+ if path is None and url is None:
92
+ raise ValueError("Either 'path' or 'url' must be provided.")
93
+
94
+ session = await self._ensure_session()
95
+ resolved_url = shared.build_url(
96
+ path=path,
97
+ url=url,
98
+ base_url=self.base_url,
99
+ default_scheme=self._default_scheme,
100
+ )
101
+ merged_headers = shared.merge_headers(self._default_headers, headers)
102
+ request_timeout = self._normalize_timeout(timeout) or self._timeout
103
+
104
+ return await session.request(
105
+ method.upper(),
106
+ resolved_url,
107
+ headers=merged_headers,
108
+ params=params,
109
+ json=json,
110
+ data=data,
111
+ timeout=request_timeout,
112
+ **kwargs,
113
+ )
114
+
115
+ async def get(self, path: str, **kwargs: Any) -> aiohttp.ClientResponse:
116
+ return await self.request("GET", path, **kwargs)
117
+
118
+ async def post(self, path: str, **kwargs: Any) -> aiohttp.ClientResponse:
119
+ return await self.request("POST", path, **kwargs)
120
+
121
+ async def put(self, path: str, **kwargs: Any) -> aiohttp.ClientResponse:
122
+ return await self.request("PUT", path, **kwargs)
123
+
124
+ async def patch(self, path: str, **kwargs: Any) -> aiohttp.ClientResponse:
125
+ return await self.request("PATCH", path, **kwargs)
126
+
127
+ async def delete(self, path: str, **kwargs: Any) -> aiohttp.ClientResponse:
128
+ return await self.request("DELETE", path, **kwargs)
129
+
130
+ async def close(self) -> None:
131
+ """Close the underlying aiohttp session if we created it."""
132
+ if self._session and self._owns_session and not self._session.closed:
133
+ await self._session.close()
134
+ if self._owns_session:
135
+ self._session = None
136
+
137
+ async def _ensure_session(self) -> ClientSession:
138
+ if self._session is None or self._session.closed:
139
+ self._session = aiohttp.ClientSession(
140
+ timeout=self._timeout,
141
+ trust_env=self._trust_env,
142
+ raise_for_status=self._raise_for_status,
143
+ )
144
+ return self._session
145
+
146
+ def _normalize_timeout(
147
+ self,
148
+ timeout: float | ClientTimeout | None,
149
+ ) -> ClientTimeout | None:
150
+ if timeout is None:
151
+ return None
152
+ if isinstance(timeout, ClientTimeout):
153
+ return timeout
154
+ return ClientTimeout(total=timeout)
@@ -0,0 +1 @@
1
+ from .base import BaseSyncHttpClient as BaseSyncHttpClient
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ import requests
7
+
8
+ from finalsa.http import _shared as shared
9
+
10
+
11
+ class BaseSyncHttpClient:
12
+ """Reusable requests-based HTTP client with sane defaults."""
13
+
14
+ DEFAULT_HEADERS: Mapping[str, str] = {
15
+ "Accept": "application/json",
16
+ "Content-Type": "application/json",
17
+ }
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ base_url: str | None = None,
23
+ default_headers: Mapping[str, str] | None = None,
24
+ timeout: float | tuple[float, float] | None = None,
25
+ default_scheme: str = "http",
26
+ service_name: str = "finalsa-http-client",
27
+ trust_env: bool = True,
28
+ raise_for_status: bool = True,
29
+ session: requests.Session | None = None,
30
+ ) -> None:
31
+ self._default_scheme = default_scheme
32
+ self.base_url = shared.normalize_base_url(base_url, default_scheme)
33
+ self._timeout = timeout
34
+ self._trust_env = trust_env
35
+ self._raise_for_status = raise_for_status
36
+ self._session = session
37
+ self._owns_session = session is None
38
+ self._default_headers = shared.build_default_headers(
39
+ self.DEFAULT_HEADERS,
40
+ default_headers,
41
+ service_name,
42
+ )
43
+
44
+ def __enter__(self) -> BaseSyncHttpClient:
45
+ self._ensure_session()
46
+ return self
47
+
48
+ def __exit__(self, exc_type, exc, tb) -> None: # type: ignore[override]
49
+ self.close()
50
+
51
+ @property
52
+ def session(self) -> requests.Session | None:
53
+ """Return the underlying requests session, if it has been created."""
54
+ return self._session
55
+
56
+ @property
57
+ def default_headers(self) -> Mapping[str, str]:
58
+ """Return a copy of the default headers."""
59
+ return dict(self._default_headers)
60
+
61
+ def update_default_headers(self, headers: Mapping[str, str]) -> None:
62
+ """Update the headers that will be sent with every request."""
63
+ if not headers:
64
+ return
65
+ self._default_headers.update(headers)
66
+
67
+ def build_url(self, path_or_url: str) -> str:
68
+ """Resolve the final URL for a request."""
69
+ return shared.build_url(
70
+ path=path_or_url,
71
+ url=None,
72
+ base_url=self.base_url,
73
+ default_scheme=self._default_scheme,
74
+ )
75
+
76
+ def request(
77
+ self,
78
+ method: str,
79
+ path: str | None = None,
80
+ *,
81
+ url: str | None = None,
82
+ headers: Mapping[str, str] | None = None,
83
+ params: Mapping[str, str | int | float | None] | None = None,
84
+ json: Any | None = None,
85
+ data: Any | None = None,
86
+ timeout: float | tuple[float, float] | None = None,
87
+ **kwargs: Any,
88
+ ) -> requests.Response:
89
+ """Send an HTTP request using requests.Session.request."""
90
+ if path is None and url is None:
91
+ raise ValueError("Either 'path' or 'url' must be provided.")
92
+
93
+ session = self._ensure_session()
94
+ resolved_url = shared.build_url(
95
+ path=path,
96
+ url=url,
97
+ base_url=self.base_url,
98
+ default_scheme=self._default_scheme,
99
+ )
100
+ merged_headers = shared.merge_headers(self._default_headers, headers)
101
+ request_timeout = timeout if timeout is not None else self._timeout
102
+
103
+ response = session.request(
104
+ method.upper(),
105
+ resolved_url,
106
+ headers=merged_headers,
107
+ params=params,
108
+ json=json,
109
+ data=data,
110
+ timeout=request_timeout,
111
+ **kwargs,
112
+ )
113
+ if self._raise_for_status:
114
+ response.raise_for_status()
115
+ return response
116
+
117
+ def get(self, path: str, **kwargs: Any) -> requests.Response:
118
+ return self.request("GET", path, **kwargs)
119
+
120
+ def post(self, path: str, **kwargs: Any) -> requests.Response:
121
+ return self.request("POST", path, **kwargs)
122
+
123
+ def put(self, path: str, **kwargs: Any) -> requests.Response:
124
+ return self.request("PUT", path, **kwargs)
125
+
126
+ def patch(self, path: str, **kwargs: Any) -> requests.Response:
127
+ return self.request("PATCH", path, **kwargs)
128
+
129
+ def delete(self, path: str, **kwargs: Any) -> requests.Response:
130
+ return self.request("DELETE", path, **kwargs)
131
+
132
+ def close(self) -> None:
133
+ """Close the underlying requests session if we created it."""
134
+ if self._session and self._owns_session:
135
+ self._session.close()
136
+ self._session = None
137
+
138
+ def _ensure_session(self) -> requests.Session:
139
+ if self._session is None:
140
+ session = requests.Session()
141
+ session.trust_env = self._trust_env
142
+ self._session = session
143
+ return self._session
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.4
2
+ Name: finalsa-common-http-client
3
+ Version: 0.0.1
4
+ Summary: HTTP client library for common data types used in business applications
5
+ Project-URL: Homepage, https://github.com/finalsa/finalsa-http-client
6
+ Project-URL: Documentation, https://github.com/finalsa/finalsa-http-client#readme
7
+ Project-URL: Repository, https://github.com/finalsa/finalsa-http-client.git
8
+ Project-URL: Issues, https://github.com/finalsa/finalsa-http-client/issues
9
+ Project-URL: Changelog, https://github.com/finalsa/finalsa-http-client/blob/main/CHANGELOG.md
10
+ Author-email: Luis Jimenez <luis@finalsa.com>
11
+ License: MIT License
12
+
13
+ Copyright (c) 2021 Luis Diego Jiménez Delgado
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+ License-File: LICENSE.md
33
+ Keywords: client,finalsa,http
34
+ Classifier: Development Status :: 4 - Beta
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python :: 3
39
+ Classifier: Programming Language :: Python :: 3.9
40
+ Classifier: Programming Language :: Python :: 3.10
41
+ Classifier: Programming Language :: Python :: 3.11
42
+ Classifier: Programming Language :: Python :: 3.12
43
+ Classifier: Topic :: Software Development :: Internationalization
44
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
45
+ Classifier: Topic :: Software Development :: Localization
46
+ Classifier: Typing :: Typed
47
+ Requires-Python: >=3.9
48
+ Requires-Dist: aiohttp>=3.10.5
49
+ Requires-Dist: fastapi>=0.116.1
50
+ Requires-Dist: finalsa-traceability>=1.0.1
51
+ Requires-Dist: requests>=2.32.4
52
+ Description-Content-Type: text/markdown
53
+
54
+ # Finalsa HTTP Client
55
+
56
+ `finalsa-http-client` provides small, well-tested base classes for building HTTP
57
+ clients that share a consistent set of defaults across Finalsa services. One
58
+ implementation wraps `requests` for synchronous workloads and the other wraps
59
+ `aiohttp` for asyncio-aware code.
60
+
61
+ - Normalizes `base_url` values and fills in schemes automatically.
62
+ - Builds a predictable `User-Agent` header (e.g. `finalsa-http-client/0.0.1`).
63
+ - Merges per-request headers with service defaults.
64
+ - Manages the underlying session lifecycle (context manager friendly).
65
+ - Lets you opt into strict `raise_for_status` behavior on every request.
66
+
67
+ ## Installation
68
+
69
+ ```bash
70
+ python -m pip install finalsa-http-client
71
+ ```
72
+
73
+ ## Synchronous usage
74
+
75
+ ```python
76
+ from finalsa.http.sync import BaseSyncHttpClient
77
+
78
+ with BaseSyncHttpClient(
79
+ base_url="https://api.example.test",
80
+ service_name="billing-api",
81
+ default_headers={"X-App": "billing"},
82
+ timeout=5, # seconds
83
+ trust_env=False,
84
+ ) as client:
85
+ response = client.post(
86
+ "/v1/invoices",
87
+ json={"customer_id": "cust_123", "total": 1999},
88
+ params={"dry_run": False},
89
+ )
90
+ print(response.json())
91
+ ```
92
+
93
+ Key synchronous notes:
94
+
95
+ - `base_url` can be provided without a scheme (`api.example.test`), the client
96
+ will inject the configured `default_scheme` (defaults to `http`).
97
+ - Use `.build_url()` when you need the normalized URL without sending a request.
98
+ - `service_name` is used to stamp the `User-Agent`. You can still call
99
+ `.update_default_headers()` later if you need to augment the defaults.
100
+ - Set `trust_env=False` (default is `True`) to keep the underlying `requests`
101
+ session from honoring proxy-related environment variables.
102
+ - Pass `url="https://status.example.test/health"` to `.request()` to bypass the
103
+ configured `base_url`.
104
+
105
+ ## Asynchronous usage
106
+
107
+ ```python
108
+ import asyncio
109
+
110
+ from finalsa.http.async import BaseAsyncHttpClient
111
+
112
+
113
+ async def main() -> None:
114
+ async with BaseAsyncHttpClient(
115
+ base_url="https://api.example.test",
116
+ service_name="billing-api",
117
+ trust_env=True, # honor proxy-related env vars
118
+ timeout=10,
119
+ ) as client:
120
+ response = await client.get("/v1/health", params={"extended": True})
121
+ payload = await response.json()
122
+ print(payload)
123
+
124
+
125
+ asyncio.run(main())
126
+ ```
127
+
128
+ Asynchronous specifics:
129
+
130
+ - Timeouts accept either a float (seconds) or an explicit `aiohttp.ClientTimeout`.
131
+ - `service_name` mirrors the sync client parameter and controls the leading
132
+ portion of the `User-Agent` header.
133
+ - `trust_env=True` allows `aiohttp` to reuse proxy / SSL options from the host
134
+ environment.
135
+ - You can supply an existing `aiohttp.ClientSession` via the `session` keyword
136
+ when you want to keep full control over connection pooling. The client will
137
+ detect that it does not own the session and skip closing it.
138
+
139
+ ## URL handling & headers
140
+
141
+ - Both clients share helpers in `finalsa.http._shared` for URL resolution,
142
+ header merging, and scheme enforcement.
143
+ - Per-request headers passed to `.request()` (or `.get()`, `.post()`, etc.) are
144
+ merged on top of the client's defaults without mutating the stored defaults.
145
+ - When neither `path` nor `url` is provided, `.request()` raises `ValueError` to
146
+ surface misconfigurations early.
147
+
148
+ ## Sessions, timeouts & errors
149
+
150
+ - Entering the client as a context manager ensures the underlying session is
151
+ created once and cleaned up automatically. You can also call `.close()` when
152
+ managing the lifecycle manually.
153
+ - `BaseSyncHttpClient` defaults to `raise_for_status=True` and will immediately
154
+ raise any non-2xx response. The async variant passes the same flag through to
155
+ `aiohttp.ClientSession`.
156
+ - Supplying your own session (`requests.Session` or `aiohttp.ClientSession`)
157
+ allows you to plug in custom retry adapters, authentication, or tracing
158
+ middleware while still benefiting from URL building and header management.
159
+
160
+ ## Migrating from `@responses.activate`
161
+
162
+ Many of our legacy tests used the `responses` package to stub outgoing HTTP
163
+ traffic. You can keep the same ergonomics with `BaseSyncHttpClient` and adopt a
164
+ similar pattern for asyncio-based code.
165
+
166
+ ### Sync tests (requests + responses)
167
+
168
+ ```python
169
+ import responses
170
+ from finalsa.http.sync import BaseSyncHttpClient
171
+
172
+
173
+ @responses.activate
174
+ def test_creates_invoice() -> None:
175
+ responses.add(
176
+ method=responses.POST,
177
+ url="https://api.example.test/v1/invoices",
178
+ json={"id": "inv_123", "status": "draft"},
179
+ status=201,
180
+ )
181
+
182
+ client = BaseSyncHttpClient(base_url="https://api.example.test")
183
+
184
+ response = client.post("/v1/invoices", json={"total": 5000})
185
+
186
+ assert response.json()["status"] == "draft"
187
+ assert len(responses.calls) == 1
188
+ ```
189
+
190
+ Nothing special is required: because the client reuses `requests.Session`, any
191
+ `responses.activate` or `responses.RequestsMock` context automatically captures
192
+ the outgoing call.
193
+
194
+ ### Async tests (aiohttp + aioresponses)
195
+
196
+ Use [`aioresponses`](https://github.com/pnuckowski/aioresponses) (or your async
197
+ HTTP mock of choice) to intercept `aiohttp` traffic:
198
+
199
+ ```python
200
+ import pytest
201
+ from aioresponses import aioresponses
202
+ from finalsa.http.async import BaseAsyncHttpClient
203
+
204
+
205
+ @pytest.mark.asyncio
206
+ async def test_health_check() -> None:
207
+ client = BaseAsyncHttpClient(base_url="https://api.example.test")
208
+
209
+ with aioresponses() as mocked:
210
+ mocked.get(
211
+ "https://api.example.test/v1/health",
212
+ payload={"status": "ok"},
213
+ status=200,
214
+ )
215
+
216
+ response = await client.get("/v1/health")
217
+ assert await response.json() == {"status": "ok"}
218
+ assert ("GET", "https://api.example.test/v1/health") in mocked.requests
219
+ ```
220
+
221
+ Behind the scenes the async client creates an `aiohttp.ClientSession`, so tools
222
+ like `aioresponses`, `aresponses`, or `pytest-aiohttp` slot in naturally. For
223
+ more brittle tests you can also inject your own `ClientSession` (e.g. one backed
224
+ by a stub transport) through the `session` constructor argument.
225
+
226
+ ## Development
227
+
228
+ ```bash
229
+ python -m pip install -e ".[test]"
230
+ pytest
231
+ ```
232
+
233
+ The test suite demonstrates additional patterns (custom timeouts, header
234
+ merging, session injection) if you need more examples. Feel free to open an
235
+ issue or pull request on GitHub with improvements or questions.
@@ -0,0 +1,9 @@
1
+ finalsa/http/_shared.py,sha256=cYrac3Tjai7fv8KiyKi7Y5M5u88Q-lcTFS3XsEgy-IQ,2729
2
+ finalsa/http/async/__init__.py,sha256=L25WfVpUpPZlMv6-cgeWlNvmK5UJOhLZ1NOndcIsA9w,73
3
+ finalsa/http/async/base.py,sha256=lAsouBWu6j811xyXm9-aAJvNscCnrYYrhzUVnELJC6c,5301
4
+ finalsa/http/sync/__init__.py,sha256=ocLE6dofwYCD9_rJpIP1KhlIujRFcaJfhoaeXVrSCVE,59
5
+ finalsa/http/sync/base.py,sha256=Z0CfY05BfnyMLDNEpGAjmZUx1tLznaqehTn81IsHTCA,4741
6
+ finalsa_common_http_client-0.0.1.dist-info/METADATA,sha256=YaWFDN60zuIMtcAy029J-kXkEDY2dTwfqJJ5SBS6bHY,9056
7
+ finalsa_common_http_client-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ finalsa_common_http_client-0.0.1.dist-info/licenses/LICENSE.md,sha256=_lu-V-f2tGID1BS2V_W6D2XWppBsylFF1J2KEpfIXN0,1084
9
+ finalsa_common_http_client-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Luis Diego Jiménez Delgado
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.