httpr 0.3.3__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {httpr-0.3.3 → httpr-0.4.1}/Cargo.lock +1 -1
- {httpr-0.3.3 → httpr-0.4.1}/Cargo.toml +1 -1
- {httpr-0.3.3 → httpr-0.4.1}/PKG-INFO +1 -1
- {httpr-0.3.3 → httpr-0.4.1}/benchmark/benchmark_cbor.py +1 -1
- {httpr-0.3.3 → httpr-0.4.1}/docs/tutorial/making-requests.md +0 -39
- {httpr-0.3.3 → httpr-0.4.1}/docs/tutorial/response-handling.md +1 -5
- {httpr-0.3.3 → httpr-0.4.1}/httpr/__init__.py +43 -0
- {httpr-0.3.3 → httpr-0.4.1}/pyproject.toml +1 -1
- {httpr-0.3.3 → httpr-0.4.1}/src/lib.rs +5 -41
- httpr-0.4.1/tests/unit/test_cbor.py +104 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/unit/test_client.py +38 -0
- httpr-0.4.1/uv.lock +7201 -0
- httpr-0.3.3/.github/workflows/claude-code-review.yml +0 -57
- httpr-0.3.3/.github/workflows/claude.yml +0 -50
- httpr-0.3.3/tests/unit/test_cbor.py +0 -94
- httpr-0.3.3/uv.lock +0 -4605
- {httpr-0.3.3 → httpr-0.4.1}/.github/actions/set-version/action.yml +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/.github/copilot-instructions.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/.github/workflows/CI.yml +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/.github/workflows/CI.yml.backup +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/.github/workflows/benchmark.yml +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/.github/workflows/copilot-setup-steps.yml +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/.github/workflows/mkdocs.yml +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/.github/workflows/set_version.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/.gitignore +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/.pre-commit-config.yaml +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/CLAUDE.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/LICENSE +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/README.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/Taskfile.yaml +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/benchmark/README.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/benchmark/__init__.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/benchmark/benchmark.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/benchmark/generate_image.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/benchmark/server.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/benchmark.jpg +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/advanced/cookies.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/advanced/index.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/advanced/proxy.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/advanced/ssl-tls.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/api/async-client.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/api/client.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/api/functions.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/api/index.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/api/response.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/index.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/quickstart.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/tutorial/async.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/tutorial/authentication.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/tutorial/index.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/writings/index.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/docs/writings/posts/2025-02-24-python-http-clients-suck.md +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/httpr/httpr.pyi +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/httpr/py.typed +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/httpr.code-workspace +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/mkdocs.yml +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/scripts/generate_certs.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/src/exceptions.rs +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/src/response.rs +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/src/traits.rs +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/src/utils.rs +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/benchmark/__init__.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/benchmark/test_performance.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/conftest.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/e2e/__init__.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/e2e/test_async.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/e2e/test_auth.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/e2e/test_redirects.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/e2e/test_ssl.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/e2e/test_streaming.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/e2e/test_uploads.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/unit/__init__.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/unit/cbor_test_server.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/unit/httpx_conns.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/unit/test_asyncclient.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/unit/test_defs.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/unit/test_docs.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/unit/test_exceptions.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/unit/test_ssl.py +0 -0
- {httpr-0.3.3 → httpr-0.4.1}/tests/unit/test_streaming.py +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name = "httpr"
|
|
3
3
|
# Version is set dynamically by CI from git tags (e.g., v1.2.3 -> 1.2.3).
|
|
4
4
|
# Do not edit manually. See .github/actions/set-version/ for details.
|
|
5
|
-
version = "0.
|
|
5
|
+
version = "0.4.1"
|
|
6
6
|
edition = "2021"
|
|
7
7
|
description = "Fast HTTP client for python"
|
|
8
8
|
authors = ["thomasht86"]
|
|
@@ -162,45 +162,6 @@ response = httpr.post(
|
|
|
162
162
|
print(response.json()["json"])
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
### CBOR Data (Transparent)
|
|
166
|
-
|
|
167
|
-
CBOR (Concise Binary Object Representation) encoding is automatically used when you set the `Accept: application/cbor` header:
|
|
168
|
-
|
|
169
|
-
```python
|
|
170
|
-
import httpr
|
|
171
|
-
|
|
172
|
-
# Simply add Accept header - serialization happens transparently
|
|
173
|
-
response = httpr.post(
|
|
174
|
-
"https://api.example.com/data",
|
|
175
|
-
json={ # Use json parameter as normal
|
|
176
|
-
"name": "John Doe",
|
|
177
|
-
"values": [1, 2, 3, 4, 5],
|
|
178
|
-
"metadata": {"version": 1}
|
|
179
|
-
},
|
|
180
|
-
headers={"Accept": "application/cbor"} # Triggers CBOR serialization
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
# Content-Type is automatically set to application/cbor
|
|
184
|
-
# Server receives CBOR-encoded data
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
CBOR offers several advantages over JSON:
|
|
188
|
-
|
|
189
|
-
- **Smaller size**: Binary encoding is more compact than text
|
|
190
|
-
- **Faster processing**: No text parsing overhead
|
|
191
|
-
- **Better for binary data**: Native support for byte arrays
|
|
192
|
-
- **Type preservation**: Maintains exact numeric types
|
|
193
|
-
|
|
194
|
-
Use CBOR when:
|
|
195
|
-
|
|
196
|
-
- Working with large arrays or datasets
|
|
197
|
-
- Building high-performance APIs
|
|
198
|
-
- Developing IoT or embedded applications
|
|
199
|
-
- Reducing bandwidth usage is critical
|
|
200
|
-
|
|
201
|
-
!!! note
|
|
202
|
-
The server must support CBOR requests. Check the API documentation to confirm CBOR is accepted.
|
|
203
|
-
|
|
204
165
|
### Form Data
|
|
205
166
|
|
|
206
167
|
Send URL-encoded form data:
|
|
@@ -112,11 +112,7 @@ When the server returns `Content-Type: application/cbor`, the `json()` method au
|
|
|
112
112
|
```python
|
|
113
113
|
import httpr
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
response = httpr.get(
|
|
117
|
-
"https://api.example.com/data",
|
|
118
|
-
headers={"Accept": "application/cbor"}
|
|
119
|
-
)
|
|
115
|
+
response = httpr.get("https://api.example.com/data")
|
|
120
116
|
|
|
121
117
|
# json() automatically detects and deserializes CBOR based on Content-Type
|
|
122
118
|
data = response.json() # Works transparently with CBOR!
|
|
@@ -42,6 +42,40 @@ else:
|
|
|
42
42
|
|
|
43
43
|
from .httpr import CaseInsensitiveHeaderMap, RClient, Response, StreamingResponse
|
|
44
44
|
|
|
45
|
+
|
|
46
|
+
class CaseInsensitiveDict(dict[str, str]):
|
|
47
|
+
"""A dict subclass that provides case-insensitive key access."""
|
|
48
|
+
|
|
49
|
+
def __getitem__(self, key: str) -> str:
|
|
50
|
+
return super().__getitem__(key.lower())
|
|
51
|
+
|
|
52
|
+
def __setitem__(self, key: str, value: str) -> None:
|
|
53
|
+
super().__setitem__(key.lower(), value)
|
|
54
|
+
|
|
55
|
+
def __delitem__(self, key: str) -> None:
|
|
56
|
+
super().__delitem__(key.lower())
|
|
57
|
+
|
|
58
|
+
def __contains__(self, key: object) -> bool:
|
|
59
|
+
if not isinstance(key, str):
|
|
60
|
+
return False
|
|
61
|
+
return super().__contains__(key.lower())
|
|
62
|
+
|
|
63
|
+
def get(self, key: str, default: str | None = None) -> str | None: # type: ignore[override]
|
|
64
|
+
return super().get(key.lower(), default)
|
|
65
|
+
|
|
66
|
+
def pop(self, key: str, *args: str) -> str: # type: ignore[override]
|
|
67
|
+
return super().pop(key.lower(), *args)
|
|
68
|
+
|
|
69
|
+
def setdefault(self, key: str, default: str | None = None) -> str | None: # type: ignore[override]
|
|
70
|
+
return super().setdefault(key.lower(), default) # type: ignore[arg-type]
|
|
71
|
+
|
|
72
|
+
def update(self, other: dict[str, str] | None = None, **kwargs: str) -> None: # type: ignore[override]
|
|
73
|
+
if other is not None:
|
|
74
|
+
super().update({k.lower(): v for k, v in other.items()})
|
|
75
|
+
if kwargs:
|
|
76
|
+
super().update({k.lower(): v for k, v in kwargs.items()})
|
|
77
|
+
|
|
78
|
+
|
|
45
79
|
if TYPE_CHECKING:
|
|
46
80
|
from .httpr import ClientRequestParams, HttpMethod, RequestParams
|
|
47
81
|
else:
|
|
@@ -207,6 +241,15 @@ class Client(RClient):
|
|
|
207
241
|
"""
|
|
208
242
|
del self
|
|
209
243
|
|
|
244
|
+
@property
|
|
245
|
+
def headers(self) -> dict[str, str]:
|
|
246
|
+
"""Headers configured for this client (case-insensitive access)."""
|
|
247
|
+
return CaseInsensitiveDict(super().headers)
|
|
248
|
+
|
|
249
|
+
@headers.setter
|
|
250
|
+
def headers(self, value: dict[str, str] | None) -> None:
|
|
251
|
+
RClient.headers.__set__(self, value) # type: ignore[attr-defined]
|
|
252
|
+
|
|
210
253
|
def request(
|
|
211
254
|
self,
|
|
212
255
|
method: HttpMethod,
|
|
@@ -11,7 +11,7 @@ keywords = [ "python", "request",]
|
|
|
11
11
|
classifiers = [ "Programming Language :: Rust", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries :: Python Modules",]
|
|
12
12
|
# Version is set dynamically by CI from git tags (e.g., v1.2.3 -> 1.2.3).
|
|
13
13
|
# Do not edit manually. See .github/actions/set-version/ for details.
|
|
14
|
-
version = "0.
|
|
14
|
+
version = "0.4.1"
|
|
15
15
|
dependencies = []
|
|
16
16
|
[[project.authors]]
|
|
17
17
|
name = "thomasht86"
|
|
@@ -12,7 +12,7 @@ use pyo3::prelude::*;
|
|
|
12
12
|
use pyo3::types::PyBytes;
|
|
13
13
|
use pythonize::depythonize;
|
|
14
14
|
use reqwest::{
|
|
15
|
-
header::{HeaderValue,
|
|
15
|
+
header::{HeaderValue, COOKIE},
|
|
16
16
|
multipart,
|
|
17
17
|
redirect::Policy,
|
|
18
18
|
Body, Identity, Method,
|
|
@@ -452,27 +452,9 @@ impl RClient {
|
|
|
452
452
|
if let Some(form_data) = data_value {
|
|
453
453
|
request_builder = request_builder.form(&form_data);
|
|
454
454
|
}
|
|
455
|
-
// Json -
|
|
455
|
+
// Json - always serialize as JSON regardless of Accept header
|
|
456
456
|
if let Some(json_data) = json_value {
|
|
457
|
-
|
|
458
|
-
let use_cbor = combined_headers
|
|
459
|
-
.get(&ACCEPT)
|
|
460
|
-
.and_then(|v| v.to_str().ok())
|
|
461
|
-
.map(|s| s.contains("application/cbor"))
|
|
462
|
-
.unwrap_or(false);
|
|
463
|
-
|
|
464
|
-
if use_cbor {
|
|
465
|
-
// Serialize as CBOR
|
|
466
|
-
let mut cbor_bytes = Vec::new();
|
|
467
|
-
serde_cbor_2::to_writer(&mut cbor_bytes, &json_data)
|
|
468
|
-
.map_err(|e| anyhow!("Failed to serialize CBOR: {}", e))?;
|
|
469
|
-
request_builder = request_builder
|
|
470
|
-
.header(CONTENT_TYPE, "application/cbor")
|
|
471
|
-
.body(cbor_bytes);
|
|
472
|
-
} else {
|
|
473
|
-
// Serialize as JSON (default)
|
|
474
|
-
request_builder = request_builder.json(&json_data);
|
|
475
|
-
}
|
|
457
|
+
request_builder = request_builder.json(&json_data);
|
|
476
458
|
}
|
|
477
459
|
// Files
|
|
478
460
|
if let Some(files) = files {
|
|
@@ -643,27 +625,9 @@ impl RClient {
|
|
|
643
625
|
if let Some(form_data) = data_value {
|
|
644
626
|
request_builder = request_builder.form(&form_data);
|
|
645
627
|
}
|
|
646
|
-
// Json -
|
|
628
|
+
// Json - always serialize as JSON regardless of Accept header
|
|
647
629
|
if let Some(json_data) = json_value {
|
|
648
|
-
|
|
649
|
-
let use_cbor = combined_headers
|
|
650
|
-
.get(&ACCEPT)
|
|
651
|
-
.and_then(|v| v.to_str().ok())
|
|
652
|
-
.map(|s| s.contains("application/cbor"))
|
|
653
|
-
.unwrap_or(false);
|
|
654
|
-
|
|
655
|
-
if use_cbor {
|
|
656
|
-
// Serialize as CBOR
|
|
657
|
-
let mut cbor_bytes = Vec::new();
|
|
658
|
-
serde_cbor_2::to_writer(&mut cbor_bytes, &json_data)
|
|
659
|
-
.map_err(|e| anyhow!("Failed to serialize CBOR: {}", e))?;
|
|
660
|
-
request_builder = request_builder
|
|
661
|
-
.header(CONTENT_TYPE, "application/cbor")
|
|
662
|
-
.body(cbor_bytes);
|
|
663
|
-
} else {
|
|
664
|
-
// Serialize as JSON (default)
|
|
665
|
-
request_builder = request_builder.json(&json_data);
|
|
666
|
-
}
|
|
630
|
+
request_builder = request_builder.json(&json_data);
|
|
667
631
|
}
|
|
668
632
|
// Files
|
|
669
633
|
if let Some(files) = files {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Tests for CBOR response deserialization support."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
5
|
+
|
|
6
|
+
import cbor2
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import httpr
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CborHandler(BaseHTTPRequestHandler):
|
|
13
|
+
"""Serves CBOR responses for testing."""
|
|
14
|
+
|
|
15
|
+
def do_GET(self):
|
|
16
|
+
if self.path == "/cbor/echo":
|
|
17
|
+
data = {"message": "CBOR response", "count": 42, "items": [1, 2, 3, 4, 5]}
|
|
18
|
+
elif self.path == "/cbor/large":
|
|
19
|
+
data = [[i + j * 0.1 for j in range(1024)] for i in range(10)]
|
|
20
|
+
else:
|
|
21
|
+
self.send_error(404)
|
|
22
|
+
return
|
|
23
|
+
body = cbor2.dumps(data)
|
|
24
|
+
self.send_response(200)
|
|
25
|
+
self.send_header("Content-Type", "application/cbor")
|
|
26
|
+
self.send_header("Content-Length", str(len(body)))
|
|
27
|
+
self.end_headers()
|
|
28
|
+
self.wfile.write(body)
|
|
29
|
+
|
|
30
|
+
def log_message(self, format, *args):
|
|
31
|
+
pass # Suppress logs during tests
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture(scope="module")
|
|
35
|
+
def cbor_server():
|
|
36
|
+
"""Start a simple CBOR test server on a random free port."""
|
|
37
|
+
server = HTTPServer(("127.0.0.1", 0), CborHandler)
|
|
38
|
+
port = server.server_address[1]
|
|
39
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
40
|
+
thread.start()
|
|
41
|
+
yield f"http://127.0.0.1:{port}"
|
|
42
|
+
server.shutdown()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_json_serialization_default(base_url_ssl, ca_bundle):
|
|
46
|
+
"""Test that JSON is used by default when Accept header is not set."""
|
|
47
|
+
client = httpr.Client(ca_cert_file=ca_bundle)
|
|
48
|
+
|
|
49
|
+
test_data = {"test": "data"}
|
|
50
|
+
|
|
51
|
+
# Send without Accept header - should use JSON (default)
|
|
52
|
+
response = client.post(
|
|
53
|
+
f"{base_url_ssl}/anything",
|
|
54
|
+
json=test_data,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
assert response.status_code == 200
|
|
58
|
+
json_data = response.json()
|
|
59
|
+
|
|
60
|
+
# Should use JSON by default
|
|
61
|
+
assert json_data["headers"]["Content-Type"] == "application/json"
|
|
62
|
+
assert json_data["json"] == test_data
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_cbor_response_json_auto_detect(cbor_server):
|
|
66
|
+
"""Test that response.json() auto-detects CBOR from Content-Type."""
|
|
67
|
+
client = httpr.Client()
|
|
68
|
+
response = client.get(f"{cbor_server}/cbor/echo")
|
|
69
|
+
|
|
70
|
+
assert response.status_code == 200
|
|
71
|
+
assert "application/cbor" in response.headers["content-type"]
|
|
72
|
+
|
|
73
|
+
# json() should transparently deserialize CBOR
|
|
74
|
+
data = response.json()
|
|
75
|
+
assert data["message"] == "CBOR response"
|
|
76
|
+
assert data["count"] == 42
|
|
77
|
+
assert data["items"] == [1, 2, 3, 4, 5]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_cbor_response_explicit(cbor_server):
|
|
81
|
+
"""Test that response.cbor() explicitly deserializes CBOR."""
|
|
82
|
+
client = httpr.Client()
|
|
83
|
+
response = client.get(f"{cbor_server}/cbor/echo")
|
|
84
|
+
|
|
85
|
+
assert response.status_code == 200
|
|
86
|
+
|
|
87
|
+
# cbor() should explicitly deserialize
|
|
88
|
+
data = response.cbor()
|
|
89
|
+
assert data["message"] == "CBOR response"
|
|
90
|
+
assert data["count"] == 42
|
|
91
|
+
assert data["items"] == [1, 2, 3, 4, 5]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_cbor_response_large_data(cbor_server):
|
|
95
|
+
"""Test CBOR deserialization with large dataset."""
|
|
96
|
+
client = httpr.Client()
|
|
97
|
+
response = client.get(f"{cbor_server}/cbor/large")
|
|
98
|
+
|
|
99
|
+
assert response.status_code == 200
|
|
100
|
+
|
|
101
|
+
data = response.json()
|
|
102
|
+
assert isinstance(data, list)
|
|
103
|
+
assert len(data) == 10
|
|
104
|
+
assert len(data[0]) == 1024
|
|
@@ -76,6 +76,7 @@ def test_client_setters(base_url_ssl, ca_bundle):
|
|
|
76
76
|
assert response.status_code == 200
|
|
77
77
|
assert client.auth == ("user", "password")
|
|
78
78
|
assert client.headers == {"x-test": "TesT"} # Headers are lowercased (necessary for HTTP/2)
|
|
79
|
+
assert client.headers["X-Test"] == "TesT" # but still accessible case-insensitively
|
|
79
80
|
assert client.cookies == {"ccc": "ddd", "cccc": "dddd"}
|
|
80
81
|
assert client.params == {"x": "aaa", "y": "bbb"}
|
|
81
82
|
assert client.timeout == 20.0
|
|
@@ -320,6 +321,43 @@ def test_setter_overwrites_constructor_headers():
|
|
|
320
321
|
client.close()
|
|
321
322
|
|
|
322
323
|
|
|
324
|
+
def test_client_headers_case_insensitive():
|
|
325
|
+
"""Test that client.headers supports case-insensitive access."""
|
|
326
|
+
client = httpr.Client(headers={"X-Custom": "value", "Content-Type": "application/json"})
|
|
327
|
+
|
|
328
|
+
# Case-insensitive access should work
|
|
329
|
+
assert client.headers["x-custom"] == "value"
|
|
330
|
+
assert client.headers["X-Custom"] == "value"
|
|
331
|
+
assert client.headers["X-CUSTOM"] == "value"
|
|
332
|
+
|
|
333
|
+
# Contains check should be case-insensitive
|
|
334
|
+
assert "x-custom" in client.headers
|
|
335
|
+
assert "X-Custom" in client.headers
|
|
336
|
+
|
|
337
|
+
# get() should be case-insensitive
|
|
338
|
+
assert client.headers.get("X-Custom") == "value"
|
|
339
|
+
assert client.headers.get("x-custom") == "value"
|
|
340
|
+
|
|
341
|
+
# Dict equality should still work (keys are lowercased)
|
|
342
|
+
assert client.headers == {"x-custom": "value", "content-type": "application/json"}
|
|
343
|
+
|
|
344
|
+
# Test mutating methods on the returned dict (doesn't affect client, but should work)
|
|
345
|
+
headers = client.headers
|
|
346
|
+
headers["X-New"] = "new-value"
|
|
347
|
+
assert headers["x-new"] == "new-value"
|
|
348
|
+
|
|
349
|
+
del headers["X-New"]
|
|
350
|
+
assert "x-new" not in headers
|
|
351
|
+
|
|
352
|
+
headers["X-Pop"] = "pop-value"
|
|
353
|
+
assert headers.pop("X-POP") == "pop-value"
|
|
354
|
+
|
|
355
|
+
assert headers.setdefault("X-Default", "default") == "default"
|
|
356
|
+
assert headers["x-default"] == "default"
|
|
357
|
+
|
|
358
|
+
client.close()
|
|
359
|
+
|
|
360
|
+
|
|
323
361
|
def test_graceful_invalid_header_handling(base_url_ssl, ca_bundle):
|
|
324
362
|
"""Test that invalid header values are handled gracefully without crashing."""
|
|
325
363
|
client = httpr.Client(ca_cert_file=ca_bundle)
|