httpr 0.3.2__tar.gz → 0.4.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 (79) hide show
  1. {httpr-0.3.2 → httpr-0.4.0}/.github/workflows/mkdocs.yml +6 -3
  2. {httpr-0.3.2 → httpr-0.4.0}/Cargo.lock +1 -1
  3. {httpr-0.3.2 → httpr-0.4.0}/Cargo.toml +1 -1
  4. {httpr-0.3.2 → httpr-0.4.0}/PKG-INFO +1 -1
  5. {httpr-0.3.2 → httpr-0.4.0}/benchmark/benchmark_cbor.py +1 -1
  6. {httpr-0.3.2 → httpr-0.4.0}/docs/tutorial/making-requests.md +0 -39
  7. {httpr-0.3.2 → httpr-0.4.0}/docs/tutorial/response-handling.md +1 -5
  8. {httpr-0.3.2 → httpr-0.4.0}/httpr/__init__.py +43 -0
  9. {httpr-0.3.2 → httpr-0.4.0}/pyproject.toml +1 -1
  10. {httpr-0.3.2 → httpr-0.4.0}/src/lib.rs +11 -44
  11. httpr-0.4.0/tests/unit/test_cbor.py +104 -0
  12. {httpr-0.3.2 → httpr-0.4.0}/tests/unit/test_client.py +80 -0
  13. httpr-0.3.2/tests/unit/test_cbor.py +0 -94
  14. {httpr-0.3.2 → httpr-0.4.0}/.github/actions/set-version/action.yml +0 -0
  15. {httpr-0.3.2 → httpr-0.4.0}/.github/copilot-instructions.md +0 -0
  16. {httpr-0.3.2 → httpr-0.4.0}/.github/workflows/CI.yml +0 -0
  17. {httpr-0.3.2 → httpr-0.4.0}/.github/workflows/CI.yml.backup +0 -0
  18. {httpr-0.3.2 → httpr-0.4.0}/.github/workflows/benchmark.yml +0 -0
  19. {httpr-0.3.2 → httpr-0.4.0}/.github/workflows/claude-code-review.yml +0 -0
  20. {httpr-0.3.2 → httpr-0.4.0}/.github/workflows/claude.yml +0 -0
  21. {httpr-0.3.2 → httpr-0.4.0}/.github/workflows/copilot-setup-steps.yml +0 -0
  22. {httpr-0.3.2 → httpr-0.4.0}/.github/workflows/set_version.py +0 -0
  23. {httpr-0.3.2 → httpr-0.4.0}/.gitignore +0 -0
  24. {httpr-0.3.2 → httpr-0.4.0}/.pre-commit-config.yaml +0 -0
  25. {httpr-0.3.2 → httpr-0.4.0}/CLAUDE.md +0 -0
  26. {httpr-0.3.2 → httpr-0.4.0}/LICENSE +0 -0
  27. {httpr-0.3.2 → httpr-0.4.0}/README.md +0 -0
  28. {httpr-0.3.2 → httpr-0.4.0}/Taskfile.yaml +0 -0
  29. {httpr-0.3.2 → httpr-0.4.0}/benchmark/README.md +0 -0
  30. {httpr-0.3.2 → httpr-0.4.0}/benchmark/__init__.py +0 -0
  31. {httpr-0.3.2 → httpr-0.4.0}/benchmark/benchmark.py +0 -0
  32. {httpr-0.3.2 → httpr-0.4.0}/benchmark/generate_image.py +0 -0
  33. {httpr-0.3.2 → httpr-0.4.0}/benchmark/server.py +0 -0
  34. {httpr-0.3.2 → httpr-0.4.0}/benchmark.jpg +0 -0
  35. {httpr-0.3.2 → httpr-0.4.0}/docs/advanced/cookies.md +0 -0
  36. {httpr-0.3.2 → httpr-0.4.0}/docs/advanced/index.md +0 -0
  37. {httpr-0.3.2 → httpr-0.4.0}/docs/advanced/proxy.md +0 -0
  38. {httpr-0.3.2 → httpr-0.4.0}/docs/advanced/ssl-tls.md +0 -0
  39. {httpr-0.3.2 → httpr-0.4.0}/docs/api/async-client.md +0 -0
  40. {httpr-0.3.2 → httpr-0.4.0}/docs/api/client.md +0 -0
  41. {httpr-0.3.2 → httpr-0.4.0}/docs/api/functions.md +0 -0
  42. {httpr-0.3.2 → httpr-0.4.0}/docs/api/index.md +0 -0
  43. {httpr-0.3.2 → httpr-0.4.0}/docs/api/response.md +0 -0
  44. {httpr-0.3.2 → httpr-0.4.0}/docs/index.md +0 -0
  45. {httpr-0.3.2 → httpr-0.4.0}/docs/quickstart.md +0 -0
  46. {httpr-0.3.2 → httpr-0.4.0}/docs/tutorial/async.md +0 -0
  47. {httpr-0.3.2 → httpr-0.4.0}/docs/tutorial/authentication.md +0 -0
  48. {httpr-0.3.2 → httpr-0.4.0}/docs/tutorial/index.md +0 -0
  49. {httpr-0.3.2 → httpr-0.4.0}/docs/writings/index.md +0 -0
  50. {httpr-0.3.2 → httpr-0.4.0}/docs/writings/posts/2025-02-24-python-http-clients-suck.md +0 -0
  51. {httpr-0.3.2 → httpr-0.4.0}/httpr/httpr.pyi +0 -0
  52. {httpr-0.3.2 → httpr-0.4.0}/httpr/py.typed +0 -0
  53. {httpr-0.3.2 → httpr-0.4.0}/httpr.code-workspace +0 -0
  54. {httpr-0.3.2 → httpr-0.4.0}/mkdocs.yml +0 -0
  55. {httpr-0.3.2 → httpr-0.4.0}/scripts/generate_certs.py +0 -0
  56. {httpr-0.3.2 → httpr-0.4.0}/src/exceptions.rs +0 -0
  57. {httpr-0.3.2 → httpr-0.4.0}/src/response.rs +0 -0
  58. {httpr-0.3.2 → httpr-0.4.0}/src/traits.rs +0 -0
  59. {httpr-0.3.2 → httpr-0.4.0}/src/utils.rs +0 -0
  60. {httpr-0.3.2 → httpr-0.4.0}/tests/benchmark/__init__.py +0 -0
  61. {httpr-0.3.2 → httpr-0.4.0}/tests/benchmark/test_performance.py +0 -0
  62. {httpr-0.3.2 → httpr-0.4.0}/tests/conftest.py +0 -0
  63. {httpr-0.3.2 → httpr-0.4.0}/tests/e2e/__init__.py +0 -0
  64. {httpr-0.3.2 → httpr-0.4.0}/tests/e2e/test_async.py +0 -0
  65. {httpr-0.3.2 → httpr-0.4.0}/tests/e2e/test_auth.py +0 -0
  66. {httpr-0.3.2 → httpr-0.4.0}/tests/e2e/test_redirects.py +0 -0
  67. {httpr-0.3.2 → httpr-0.4.0}/tests/e2e/test_ssl.py +0 -0
  68. {httpr-0.3.2 → httpr-0.4.0}/tests/e2e/test_streaming.py +0 -0
  69. {httpr-0.3.2 → httpr-0.4.0}/tests/e2e/test_uploads.py +0 -0
  70. {httpr-0.3.2 → httpr-0.4.0}/tests/unit/__init__.py +0 -0
  71. {httpr-0.3.2 → httpr-0.4.0}/tests/unit/cbor_test_server.py +0 -0
  72. {httpr-0.3.2 → httpr-0.4.0}/tests/unit/httpx_conns.py +0 -0
  73. {httpr-0.3.2 → httpr-0.4.0}/tests/unit/test_asyncclient.py +0 -0
  74. {httpr-0.3.2 → httpr-0.4.0}/tests/unit/test_defs.py +0 -0
  75. {httpr-0.3.2 → httpr-0.4.0}/tests/unit/test_docs.py +0 -0
  76. {httpr-0.3.2 → httpr-0.4.0}/tests/unit/test_exceptions.py +0 -0
  77. {httpr-0.3.2 → httpr-0.4.0}/tests/unit/test_ssl.py +0 -0
  78. {httpr-0.3.2 → httpr-0.4.0}/tests/unit/test_streaming.py +0 -0
  79. {httpr-0.3.2 → httpr-0.4.0}/uv.lock +0 -0
@@ -36,6 +36,9 @@ jobs:
36
36
  # Capture source commit BEFORE switching branches
37
37
  SOURCE_COMMIT=$(git rev-parse --short HEAD)
38
38
 
39
+ # Save the built site before switching branches
40
+ cp -r site /tmp/site-backup
41
+
39
42
  # Fetch gh-pages branch
40
43
  git fetch origin gh-pages:gh-pages || true
41
44
 
@@ -58,12 +61,12 @@ jobs:
58
61
  # Remove all tracked files
59
62
  git rm -rf . || true
60
63
 
61
- # Copy new docs from site/ directory
62
- if [ ! -d "site" ] || [ -z "$(ls -A site 2>/dev/null)" ]; then
64
+ # Copy new docs from backup
65
+ if [ ! -d "/tmp/site-backup" ] || [ -z "$(ls -A /tmp/site-backup 2>/dev/null)" ]; then
63
66
  echo "Error: site/ directory is empty or missing"
64
67
  exit 1
65
68
  fi
66
- cp -r site/* .
69
+ cp -r /tmp/site-backup/* .
67
70
 
68
71
  # Restore benchmark data if it existed
69
72
  if [ -f /tmp/bench-backup/data.js ] || [ -f /tmp/bench-backup/index.html ]; then
@@ -481,7 +481,7 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
481
481
 
482
482
  [[package]]
483
483
  name = "httpr"
484
- version = "0.3.2"
484
+ version = "0.4.0"
485
485
  dependencies = [
486
486
  "anyhow",
487
487
  "bytes",
@@ -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.3.2"
5
+ version = "0.4.0"
6
6
  edition = "2021"
7
7
  description = "Fast HTTP client for python"
8
8
  authors = ["thomasht86"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpr
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -1,6 +1,6 @@
1
1
  """CBOR vs JSON benchmark for httpr.
2
2
 
3
- This benchmark compares JSON and CBOR serialization/deserialization
3
+ This benchmark compares JSON and CBOR response deserialization
4
4
  performance for large vector data using httpr only.
5
5
  """
6
6
 
@@ -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
- # Request CBOR data by setting Accept header
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.3.2"
14
+ version = "0.4.0"
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, ACCEPT, CONTENT_TYPE, COOKIE},
15
+ header::{HeaderValue, COOKIE},
16
16
  multipart,
17
17
  redirect::Policy,
18
18
  Body, Identity, Method,
@@ -145,7 +145,7 @@ impl RClient {
145
145
  let mut client_builder = reqwest::Client::builder();
146
146
 
147
147
  // Headers || Cookies
148
- if headers.is_some() || cookies.is_some() {
148
+ let headers_headermap = if headers.is_some() || cookies.is_some() {
149
149
  let headers = headers.unwrap_or_else(|| IndexMap::with_hasher(RandomState::default()));
150
150
  let mut headers_headermap = headers.to_headermap();
151
151
  if let Some(cookies) = cookies {
@@ -156,7 +156,10 @@ impl RClient {
156
156
  .map_err(|e| map_anyhow_error(anyhow::Error::new(e)))?,
157
157
  );
158
158
  }
159
- client_builder = client_builder.default_headers(headers_headermap);
159
+ client_builder = client_builder.default_headers(headers_headermap.clone());
160
+ headers_headermap
161
+ } else {
162
+ reqwest::header::HeaderMap::new()
160
163
  };
161
164
 
162
165
  // Cookie_store
@@ -231,7 +234,7 @@ impl RClient {
231
234
  let client = Arc::new(Mutex::new(
232
235
  client_builder.build().map_err(map_reqwest_error)?,
233
236
  ));
234
- let headers = Arc::new(Mutex::new(reqwest::header::HeaderMap::new()));
237
+ let headers = Arc::new(Mutex::new(headers_headermap));
235
238
 
236
239
  Ok(RClient {
237
240
  client,
@@ -449,27 +452,9 @@ impl RClient {
449
452
  if let Some(form_data) = data_value {
450
453
  request_builder = request_builder.form(&form_data);
451
454
  }
452
- // Json - check if we should use CBOR based on Accept header
455
+ // Json - always serialize as JSON regardless of Accept header
453
456
  if let Some(json_data) = json_value {
454
- // Check if Accept header is set to application/cbor
455
- let use_cbor = combined_headers
456
- .get(&ACCEPT)
457
- .and_then(|v| v.to_str().ok())
458
- .map(|s| s.contains("application/cbor"))
459
- .unwrap_or(false);
460
-
461
- if use_cbor {
462
- // Serialize as CBOR
463
- let mut cbor_bytes = Vec::new();
464
- serde_cbor_2::to_writer(&mut cbor_bytes, &json_data)
465
- .map_err(|e| anyhow!("Failed to serialize CBOR: {}", e))?;
466
- request_builder = request_builder
467
- .header(CONTENT_TYPE, "application/cbor")
468
- .body(cbor_bytes);
469
- } else {
470
- // Serialize as JSON (default)
471
- request_builder = request_builder.json(&json_data);
472
- }
457
+ request_builder = request_builder.json(&json_data);
473
458
  }
474
459
  // Files
475
460
  if let Some(files) = files {
@@ -640,27 +625,9 @@ impl RClient {
640
625
  if let Some(form_data) = data_value {
641
626
  request_builder = request_builder.form(&form_data);
642
627
  }
643
- // Json - check if we should use CBOR based on Accept header
628
+ // Json - always serialize as JSON regardless of Accept header
644
629
  if let Some(json_data) = json_value {
645
- // Check if Accept header is set to application/cbor
646
- let use_cbor = combined_headers
647
- .get(&ACCEPT)
648
- .and_then(|v| v.to_str().ok())
649
- .map(|s| s.contains("application/cbor"))
650
- .unwrap_or(false);
651
-
652
- if use_cbor {
653
- // Serialize as CBOR
654
- let mut cbor_bytes = Vec::new();
655
- serde_cbor_2::to_writer(&mut cbor_bytes, &json_data)
656
- .map_err(|e| anyhow!("Failed to serialize CBOR: {}", e))?;
657
- request_builder = request_builder
658
- .header(CONTENT_TYPE, "application/cbor")
659
- .body(cbor_bytes);
660
- } else {
661
- // Serialize as JSON (default)
662
- request_builder = request_builder.json(&json_data);
663
- }
630
+ request_builder = request_builder.json(&json_data);
664
631
  }
665
632
  // Files
666
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
@@ -278,6 +279,85 @@ def test_client_post_files(test_files):
278
279
  assert json_data["files"] == {"file1": "aaa111", "file2": "bbb222"}
279
280
 
280
281
 
282
+ def test_constructor_headers_accessible():
283
+ """Test that headers passed to constructor are accessible via the headers property."""
284
+ client = httpr.Client(headers={"X-Custom": "value", "User-Agent": "test-agent"})
285
+ assert client.headers == {"x-custom": "value", "user-agent": "test-agent"}
286
+ client.close()
287
+
288
+
289
+ def test_constructor_headers_with_cookies():
290
+ """Test that cookies are excluded from headers getter when passed to constructor."""
291
+ client = httpr.Client(
292
+ headers={"X-Custom": "value"},
293
+ cookies={"session": "abc123"},
294
+ )
295
+ # Cookies should not appear in headers getter
296
+ assert client.headers == {"x-custom": "value"}
297
+ # But cookies should be accessible via cookies getter
298
+ assert client.cookies == {"session": "abc123"}
299
+ client.close()
300
+
301
+
302
+ def test_constructor_cookies_only():
303
+ """Test that cookies-only constructor doesn't expose Cookie header."""
304
+ client = httpr.Client(cookies={"session": "abc123"})
305
+ # Headers should be empty (no Cookie header exposed)
306
+ assert client.headers == {}
307
+ # Cookies should be accessible
308
+ assert client.cookies == {"session": "abc123"}
309
+ client.close()
310
+
311
+
312
+ def test_setter_overwrites_constructor_headers():
313
+ """Test that setting headers overwrites constructor headers."""
314
+ client = httpr.Client(headers={"X-Original": "original"})
315
+ assert client.headers == {"x-original": "original"}
316
+ # Overwrite with new headers
317
+ client.headers = {"X-New": "new"}
318
+ assert client.headers == {"x-new": "new"}
319
+ # Original header should be gone
320
+ assert "x-original" not in client.headers
321
+ client.close()
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
+
281
361
  def test_graceful_invalid_header_handling(base_url_ssl, ca_bundle):
282
362
  """Test that invalid header values are handled gracefully without crashing."""
283
363
  client = httpr.Client(ca_cert_file=ca_bundle)
@@ -1,94 +0,0 @@
1
- """Tests for transparent CBOR serialization/deserialization support."""
2
-
3
- import cbor2
4
- import pytest
5
-
6
- import httpr
7
-
8
-
9
- def test_cbor_serialization_transparent(base_url_ssl, ca_bundle):
10
- """Test transparent CBOR serialization when Accept header is set."""
11
- client = httpr.Client(ca_cert_file=ca_bundle)
12
-
13
- # Create test data
14
- test_data = {
15
- "name": "httpr",
16
- "version": "0.1.0",
17
- "numbers": [1, 2, 3, 4, 5],
18
- "nested": {
19
- "key1": "value1",
20
- "key2": 42,
21
- },
22
- }
23
-
24
- # Send with Accept: application/cbor header - should use CBOR automatically
25
- response = client.post(f"{base_url_ssl}/anything", json=test_data, headers={"Accept": "application/cbor"})
26
-
27
- assert response.status_code == 200
28
- json_data = response.json()
29
-
30
- # httpbin echoes the request, check that Content-Type was set correctly
31
- assert json_data["headers"]["Content-Type"] == "application/cbor"
32
-
33
- # Verify the CBOR data was sent by checking the data field exists
34
- assert "data" in json_data
35
-
36
-
37
- def test_json_serialization_default(base_url_ssl, ca_bundle):
38
- """Test that JSON is used by default when Accept header is not set."""
39
- client = httpr.Client(ca_cert_file=ca_bundle)
40
-
41
- test_data = {"test": "data"}
42
-
43
- # Send without Accept header - should use JSON (default)
44
- response = client.post(
45
- f"{base_url_ssl}/anything",
46
- json=test_data,
47
- )
48
-
49
- assert response.status_code == 200
50
- json_data = response.json()
51
-
52
- # Should use JSON by default
53
- assert json_data["headers"]["Content-Type"] == "application/json"
54
- assert json_data["json"] == test_data
55
-
56
-
57
- def test_cbor_types():
58
- """Test various CBOR data types in serialization."""
59
- test_data = {
60
- "string": "test",
61
- "int": 42,
62
- "float": 3.14159,
63
- "bool_true": True,
64
- "bool_false": False,
65
- "null": None,
66
- "array": [1, 2, 3],
67
- "object": {"nested": "value"},
68
- }
69
-
70
- cbor_bytes = cbor2.dumps(test_data)
71
- decoded = cbor2.loads(cbor_bytes)
72
-
73
- assert decoded["string"] == "test"
74
- assert decoded["int"] == 42
75
- assert decoded["float"] == pytest.approx(3.14159)
76
- assert decoded["bool_true"] is True
77
- assert decoded["bool_false"] is False
78
- assert decoded["null"] is None
79
- assert decoded["array"] == [1, 2, 3]
80
- assert decoded["object"]["nested"] == "value"
81
-
82
-
83
- @pytest.mark.asyncio
84
- async def test_cbor_async_transparent(base_url_ssl, ca_bundle):
85
- """Test transparent CBOR with async client."""
86
- async with httpr.AsyncClient(ca_cert_file=ca_bundle) as client:
87
- test_data = {"async": True, "value": 999}
88
-
89
- # Use Accept header to trigger CBOR serialization
90
- response = await client.post(f"{base_url_ssl}/anything", json=test_data, headers={"Accept": "application/cbor"})
91
-
92
- assert response.status_code == 200
93
- json_data = response.json()
94
- assert json_data["headers"]["Content-Type"] == "application/cbor"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes