httpr 0.3.2__cp39-abi3-macosx_10_12_x86_64.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.
httpr/__init__.py ADDED
@@ -0,0 +1,1079 @@
1
+ """
2
+ httpr - Blazing fast HTTP client for Python, built in Rust.
3
+
4
+ httpr is a high-performance HTTP client that can be used as a drop-in replacement
5
+ for `httpx` and `requests` in most cases.
6
+
7
+ Example:
8
+ Simple GET request:
9
+
10
+ ```python
11
+ import httpr
12
+
13
+ response = httpr.get("https://httpbin.org/get")
14
+ print(response.json())
15
+ ```
16
+
17
+ Using a client for connection pooling:
18
+
19
+ ```python
20
+ import httpr
21
+
22
+ with httpr.Client() as client:
23
+ response = client.get("https://httpbin.org/get")
24
+ print(response.status_code)
25
+ ```
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import asyncio
31
+ import sys
32
+ from collections.abc import AsyncIterator, Generator
33
+ from contextlib import asynccontextmanager, contextmanager
34
+ from functools import partial
35
+ from typing import TYPE_CHECKING, TypedDict
36
+
37
+ if sys.version_info <= (3, 11):
38
+ from typing_extensions import Unpack
39
+ else:
40
+ from typing import Unpack
41
+
42
+
43
+ from .httpr import CaseInsensitiveHeaderMap, RClient, Response, StreamingResponse
44
+
45
+ if TYPE_CHECKING:
46
+ from .httpr import ClientRequestParams, HttpMethod, RequestParams
47
+ else:
48
+
49
+ class _Unpack:
50
+ @staticmethod
51
+ def __getitem__(*args, **kwargs):
52
+ pass
53
+
54
+ Unpack = _Unpack()
55
+ RequestParams = ClientRequestParams = TypedDict
56
+
57
+
58
+ class Client(RClient):
59
+ """
60
+ A synchronous HTTP client with connection pooling.
61
+
62
+ The Client class provides a high-level interface for making HTTP requests.
63
+ It supports connection pooling, automatic cookie handling, and various
64
+ authentication methods.
65
+
66
+ Example:
67
+ Basic usage:
68
+
69
+ ```python
70
+ import httpr
71
+
72
+ # Using context manager (recommended)
73
+ with httpr.Client() as client:
74
+ response = client.get("https://httpbin.org/get")
75
+ print(response.json())
76
+
77
+ # Or manually
78
+ client = httpr.Client()
79
+ response = client.get("https://httpbin.org/get")
80
+ client.close()
81
+ ```
82
+
83
+ With configuration:
84
+
85
+ ```python
86
+ import httpr
87
+
88
+ client = httpr.Client(
89
+ auth_bearer="your-api-token",
90
+ headers={"User-Agent": "my-app/1.0"},
91
+ timeout=30,
92
+ )
93
+ ```
94
+
95
+ Attributes:
96
+ headers: Default headers sent with all requests. Excludes Cookie header.
97
+ cookies: Default cookies sent with all requests.
98
+ auth: Basic auth credentials as (username, password) tuple.
99
+ params: Default query parameters added to all requests.
100
+ timeout: Default timeout in seconds.
101
+ proxy: Proxy URL for requests.
102
+ """
103
+
104
+ def __init__(
105
+ self,
106
+ auth: tuple[str, str | None] | None = None,
107
+ auth_bearer: str | None = None,
108
+ params: dict[str, str] | None = None,
109
+ headers: dict[str, str] | None = None,
110
+ cookies: dict[str, str] | None = None,
111
+ cookie_store: bool | None = True,
112
+ referer: bool | None = True,
113
+ proxy: str | None = None,
114
+ timeout: float | None = 30,
115
+ follow_redirects: bool | None = True,
116
+ max_redirects: int | None = 20,
117
+ verify: bool | None = True,
118
+ ca_cert_file: str | None = None,
119
+ client_pem: str | None = None,
120
+ client_pem_data: bytes | None = None,
121
+ https_only: bool | None = False,
122
+ http2_only: bool | None = False,
123
+ ):
124
+ """
125
+ Initialize an HTTP client.
126
+
127
+ Args:
128
+ auth: Basic auth credentials as (username, password). Password can be None.
129
+ auth_bearer: Bearer token for Authorization header.
130
+ params: Default query parameters to include in all requests.
131
+ headers: Default headers to send with all requests.
132
+ cookies: Default cookies to send with all requests.
133
+ cookie_store: Enable persistent cookie store. Cookies from responses will be
134
+ preserved and included in subsequent requests. Default is True.
135
+ referer: Automatically set Referer header. Default is True.
136
+ proxy: Proxy URL (e.g., "http://proxy:8080" or "socks5://127.0.0.1:1080").
137
+ Falls back to HTTPR_PROXY environment variable.
138
+ timeout: Request timeout in seconds. Default is 30.
139
+ follow_redirects: Follow HTTP redirects. Default is True.
140
+ max_redirects: Maximum redirects to follow. Default is 20.
141
+ verify: Verify SSL certificates. Default is True.
142
+ ca_cert_file: Path to CA certificate bundle (PEM format).
143
+ client_pem: Path to client certificate for mTLS (PEM format).
144
+ client_pem_data: Client certificate and key as bytes for mTLS (PEM format).
145
+ Use this instead of client_pem when you have the certificate in memory.
146
+ https_only: Only allow HTTPS requests. Default is False.
147
+ http2_only: Use HTTP/2 only (False uses HTTP/1.1). Default is False.
148
+
149
+ Example:
150
+ ```python
151
+ import httpr
152
+
153
+ # Simple client
154
+ client = httpr.Client()
155
+
156
+ # Client with authentication
157
+ client = httpr.Client(
158
+ auth=("username", "password"),
159
+ timeout=60,
160
+ )
161
+
162
+ # Client with bearer token
163
+ client = httpr.Client(
164
+ auth_bearer="your-api-token",
165
+ headers={"Accept": "application/json"},
166
+ )
167
+
168
+ # Client with proxy
169
+ client = httpr.Client(proxy="http://proxy.example.com:8080")
170
+
171
+ # Client with mTLS using file path
172
+ client = httpr.Client(
173
+ client_pem="/path/to/client.pem",
174
+ ca_cert_file="/path/to/ca.pem",
175
+ )
176
+
177
+ # Client with mTLS using direct certificate data
178
+ cert_data = b"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
179
+ client = httpr.Client(
180
+ client_pem_data=cert_data,
181
+ ca_cert_file="/path/to/ca.pem",
182
+ )
183
+ ```
184
+ """
185
+ super().__init__()
186
+
187
+ def __enter__(self) -> Client:
188
+ """Enter context manager."""
189
+ return self
190
+
191
+ def __exit__(self, *args):
192
+ """Exit context manager and close client."""
193
+ del self
194
+
195
+ def close(self) -> None:
196
+ """
197
+ Close the client and release resources.
198
+
199
+ Example:
200
+ ```python
201
+ client = httpr.Client()
202
+ try:
203
+ response = client.get("https://example.com")
204
+ finally:
205
+ client.close()
206
+ ```
207
+ """
208
+ del self
209
+
210
+ def request(
211
+ self,
212
+ method: HttpMethod,
213
+ url: str,
214
+ **kwargs: Unpack[RequestParams],
215
+ ) -> Response:
216
+ """
217
+ Make an HTTP request.
218
+
219
+ Args:
220
+ method: HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS).
221
+ url: Request URL.
222
+ **kwargs: Request parameters (see below).
223
+
224
+ Keyword Args:
225
+ params (Optional[dict[str, str]]): Query parameters to append to URL.
226
+ headers (Optional[dict[str, str]]): Request headers (merged with client defaults).
227
+ cookies (Optional[dict[str, str]]): Request cookies (merged with client defaults).
228
+ auth (Optional[tuple[str, Optional[str]]]): Basic auth credentials (overrides client default).
229
+ auth_bearer (Optional[str]): Bearer token (overrides client default).
230
+ timeout (Optional[float]): Request timeout in seconds (overrides client default).
231
+ content (Optional[bytes]): Raw bytes for request body.
232
+ data (Optional[dict[str, Any]]): Form data for request body (application/x-www-form-urlencoded).
233
+ json (Optional[Any]): JSON data for request body (application/json).
234
+ files (Optional[dict[str, str]]): Files for multipart upload (dict mapping field names to file paths).
235
+
236
+ Returns:
237
+ Response object with status, headers, and body.
238
+
239
+ Raises:
240
+ ValueError: If method is not a valid HTTP method.
241
+ Exception: If request fails (timeout, connection error, etc.).
242
+
243
+ Example:
244
+ ```python
245
+ response = client.request("GET", "https://httpbin.org/get")
246
+ response = client.request("POST", "https://httpbin.org/post", json={"key": "value"})
247
+ ```
248
+
249
+ Note:
250
+ Only one of `content`, `data`, `json`, or `files` can be specified per request.
251
+ """
252
+ if method not in ["GET", "HEAD", "OPTIONS", "DELETE", "POST", "PUT", "PATCH"]:
253
+ raise ValueError(f"Unsupported HTTP method: {method}")
254
+ if "params" in kwargs and kwargs["params"] is not None:
255
+ kwargs["params"] = {k: str(v) for k, v in kwargs["params"].items()}
256
+
257
+ return super().request(method=method, url=url, **kwargs)
258
+
259
+ def get(self, url: str, **kwargs: Unpack[RequestParams]) -> Response:
260
+ """
261
+ Make a GET request.
262
+
263
+ Args:
264
+ url: Request URL.
265
+ **kwargs: Request parameters (params, headers, cookies, auth, auth_bearer, timeout).
266
+
267
+ Returns:
268
+ Response object.
269
+
270
+ Example:
271
+ ```python
272
+ response = client.get(
273
+ "https://httpbin.org/get",
274
+ params={"key": "value"},
275
+ headers={"Accept": "application/json"},
276
+ )
277
+ print(response.json())
278
+ ```
279
+ """
280
+ return self.request(method="GET", url=url, **kwargs)
281
+
282
+ def head(self, url: str, **kwargs: Unpack[RequestParams]) -> Response:
283
+ """
284
+ Make a HEAD request.
285
+
286
+ Returns only headers, no response body.
287
+
288
+ Args:
289
+ url: Request URL.
290
+ **kwargs: Request parameters (params, headers, cookies, auth, auth_bearer, timeout).
291
+
292
+ Returns:
293
+ Response object (body will be empty).
294
+
295
+ Example:
296
+ ```python
297
+ response = client.head("https://httpbin.org/get")
298
+ print(response.headers["content-length"])
299
+ ```
300
+ """
301
+ return self.request(method="HEAD", url=url, **kwargs)
302
+
303
+ def options(self, url: str, **kwargs: Unpack[RequestParams]) -> Response:
304
+ """
305
+ Make an OPTIONS request.
306
+
307
+ Args:
308
+ url: Request URL.
309
+ **kwargs: Request parameters (params, headers, cookies, auth, auth_bearer, timeout).
310
+
311
+ Returns:
312
+ Response object.
313
+
314
+ Example:
315
+ ```python
316
+ response = client.options("https://httpbin.org/get")
317
+ print(response.headers.get("allow"))
318
+ ```
319
+ """
320
+ return self.request(method="OPTIONS", url=url, **kwargs)
321
+
322
+ def delete(self, url: str, **kwargs: Unpack[RequestParams]) -> Response:
323
+ """
324
+ Make a DELETE request.
325
+
326
+ Args:
327
+ url: Request URL.
328
+ **kwargs: Request parameters (params, headers, cookies, auth, auth_bearer, timeout).
329
+
330
+ Returns:
331
+ Response object.
332
+
333
+ Example:
334
+ ```python
335
+ response = client.delete("https://httpbin.org/delete")
336
+ print(response.status_code)
337
+ ```
338
+ """
339
+ return self.request(method="DELETE", url=url, **kwargs)
340
+
341
+ def post(self, url: str, **kwargs: Unpack[RequestParams]) -> Response:
342
+ """
343
+ Make a POST request.
344
+
345
+ Args:
346
+ url: Request URL.
347
+ **kwargs: Request parameters including body options.
348
+
349
+ Keyword Args:
350
+ params (Optional[dict[str, str]]): Query parameters.
351
+ headers (Optional[dict[str, str]]): Request headers.
352
+ cookies (Optional[dict[str, str]]): Request cookies.
353
+ auth (Optional[tuple[str, Optional[str]]]): Basic auth credentials.
354
+ auth_bearer (Optional[str]): Bearer token.
355
+ timeout (Optional[float]): Request timeout.
356
+ content (Optional[bytes]): Raw bytes body.
357
+ data (Optional[dict[str, Any]]): Form-encoded body.
358
+ json (Optional[Any]): JSON body.
359
+ files (Optional[dict[str, str]]): Multipart file uploads.
360
+
361
+ Returns:
362
+ Response object.
363
+
364
+ Example:
365
+ ```python
366
+ # JSON body
367
+ response = client.post(
368
+ "https://httpbin.org/post",
369
+ json={"name": "httpr", "fast": True},
370
+ )
371
+
372
+ # Form data
373
+ response = client.post(
374
+ "https://httpbin.org/post",
375
+ data={"username": "user", "password": "pass"},
376
+ )
377
+
378
+ # File upload
379
+ response = client.post(
380
+ "https://httpbin.org/post",
381
+ files={"document": "/path/to/file.pdf"},
382
+ )
383
+ ```
384
+ """
385
+ return self.request(method="POST", url=url, **kwargs)
386
+
387
+ def put(self, url: str, **kwargs: Unpack[RequestParams]) -> Response:
388
+ """
389
+ Make a PUT request.
390
+
391
+ Args:
392
+ url: Request URL.
393
+ **kwargs: Request parameters including body options.
394
+
395
+ Returns:
396
+ Response object.
397
+
398
+ Example:
399
+ ```python
400
+ response = client.put(
401
+ "https://httpbin.org/put",
402
+ json={"key": "updated_value"},
403
+ )
404
+ ```
405
+ """
406
+ return self.request(method="PUT", url=url, **kwargs)
407
+
408
+ def patch(self, url: str, **kwargs: Unpack[RequestParams]) -> Response:
409
+ """
410
+ Make a PATCH request.
411
+
412
+ Args:
413
+ url: Request URL.
414
+ **kwargs: Request parameters including body options.
415
+
416
+ Returns:
417
+ Response object.
418
+
419
+ Example:
420
+ ```python
421
+ response = client.patch(
422
+ "https://httpbin.org/patch",
423
+ json={"field": "new_value"},
424
+ )
425
+ ```
426
+ """
427
+ return self.request(method="PATCH", url=url, **kwargs)
428
+
429
+ @contextmanager
430
+ def stream(
431
+ self,
432
+ method: HttpMethod,
433
+ url: str,
434
+ **kwargs: Unpack[RequestParams],
435
+ ) -> Generator[StreamingResponse, None, None]:
436
+ """
437
+ Make a streaming HTTP request.
438
+
439
+ Returns a context manager that yields a StreamingResponse for iterating
440
+ over the response body in chunks without buffering the entire response
441
+ in memory.
442
+
443
+ Args:
444
+ method: HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS).
445
+ url: Request URL.
446
+ **kwargs: Request parameters (same as request()).
447
+
448
+ Yields:
449
+ StreamingResponse: A response object that can be iterated to receive chunks.
450
+
451
+ Example:
452
+ Basic streaming:
453
+
454
+ ```python
455
+ with client.stream("GET", "https://example.com/large-file") as response:
456
+ for chunk in response.iter_bytes():
457
+ process(chunk)
458
+ ```
459
+
460
+ Streaming text:
461
+
462
+ ```python
463
+ with client.stream("GET", "https://example.com/text") as response:
464
+ for text in response.iter_text():
465
+ print(text, end="")
466
+ ```
467
+
468
+ Streaming lines (e.g., Server-Sent Events):
469
+
470
+ ```python
471
+ with client.stream("GET", "https://example.com/events") as response:
472
+ for line in response.iter_lines():
473
+ print(line.strip())
474
+ ```
475
+
476
+ Conditional reading:
477
+
478
+ ```python
479
+ with client.stream("GET", url) as response:
480
+ if response.status_code == 200:
481
+ content = response.read() # Read all remaining content
482
+ else:
483
+ pass # Don't read the body
484
+ ```
485
+
486
+ Note:
487
+ The response body is only read when you iterate over it or call read().
488
+ Always use this as a context manager to ensure proper cleanup.
489
+ """
490
+ if method not in ["GET", "HEAD", "OPTIONS", "DELETE", "POST", "PUT", "PATCH"]:
491
+ raise ValueError(f"Unsupported HTTP method: {method}")
492
+ if "params" in kwargs and kwargs["params"] is not None:
493
+ kwargs["params"] = {k: str(v) for k, v in kwargs["params"].items()}
494
+
495
+ response = super()._stream(method=method, url=url, **kwargs)
496
+ try:
497
+ yield response
498
+ finally:
499
+ response.close()
500
+
501
+
502
+ class AsyncClient(Client):
503
+ """
504
+ An asynchronous HTTP client for use with asyncio.
505
+
506
+ AsyncClient wraps the synchronous Client using asyncio.run_in_executor(),
507
+ providing an async interface while leveraging the Rust implementation's
508
+ performance.
509
+
510
+ Example:
511
+ Basic usage:
512
+
513
+ ```python
514
+ import asyncio
515
+ import httpr
516
+
517
+ async def main():
518
+ async with httpr.AsyncClient() as client:
519
+ response = await client.get("https://httpbin.org/get")
520
+ print(response.json())
521
+
522
+ asyncio.run(main())
523
+ ```
524
+
525
+ Concurrent requests:
526
+
527
+ ```python
528
+ import asyncio
529
+ import httpr
530
+
531
+ async def main():
532
+ async with httpr.AsyncClient() as client:
533
+ tasks = [
534
+ client.get("https://httpbin.org/get"),
535
+ client.get("https://httpbin.org/ip"),
536
+ ]
537
+ responses = await asyncio.gather(*tasks)
538
+ for response in responses:
539
+ print(response.json())
540
+
541
+ asyncio.run(main())
542
+ ```
543
+
544
+ Note:
545
+ AsyncClient runs synchronous Rust code in a thread executor.
546
+ It provides concurrency benefits for I/O-bound tasks but is not
547
+ native async I/O.
548
+ """
549
+
550
+ def __init__(self, *args, **kwargs):
551
+ """
552
+ Initialize an async HTTP client.
553
+
554
+ Accepts the same parameters as Client.
555
+ """
556
+ super().__init__(*args, **kwargs)
557
+
558
+ async def __aenter__(self) -> AsyncClient:
559
+ """Enter async context manager."""
560
+ return self
561
+
562
+ async def __aexit__(self, *args):
563
+ """Exit async context manager and close client."""
564
+ del self
565
+
566
+ async def aclose(self):
567
+ """
568
+ Close the async client.
569
+
570
+ Example:
571
+ ```python
572
+ client = httpr.AsyncClient()
573
+ try:
574
+ response = await client.get("https://example.com")
575
+ finally:
576
+ await client.aclose()
577
+ ```
578
+ """
579
+ del self
580
+ return
581
+
582
+ async def _run_sync_asyncio(self, fn, *args, **kwargs):
583
+ """Run a synchronous function in an executor."""
584
+ loop = asyncio.get_running_loop()
585
+ return await loop.run_in_executor(None, partial(fn, *args, **kwargs))
586
+
587
+ async def request( # type: ignore[override]
588
+ self,
589
+ method: HttpMethod,
590
+ url: str,
591
+ **kwargs: Unpack[RequestParams],
592
+ ) -> Response:
593
+ """
594
+ Make an async HTTP request.
595
+
596
+ Args:
597
+ method: HTTP method.
598
+ url: Request URL.
599
+ **kwargs: Request parameters.
600
+
601
+ Returns:
602
+ Response object.
603
+
604
+ Example:
605
+ ```python
606
+ response = await client.request("GET", "https://httpbin.org/get")
607
+ ```
608
+ """
609
+ if method not in ["GET", "HEAD", "OPTIONS", "DELETE", "POST", "PUT", "PATCH"]:
610
+ raise ValueError(f"Unsupported HTTP method: {method}")
611
+ if "params" in kwargs and kwargs["params"] is not None:
612
+ kwargs["params"] = {k: str(v) for k, v in kwargs["params"].items()}
613
+
614
+ return await self._run_sync_asyncio(super().request, method=method, url=url, **kwargs)
615
+
616
+ async def get( # type: ignore[override]
617
+ self,
618
+ url: str,
619
+ **kwargs: Unpack[RequestParams],
620
+ ) -> Response:
621
+ """
622
+ Make an async GET request.
623
+
624
+ Args:
625
+ url: Request URL.
626
+ **kwargs: Request parameters.
627
+
628
+ Returns:
629
+ Response object.
630
+
631
+ Example:
632
+ ```python
633
+ response = await client.get("https://httpbin.org/get")
634
+ ```
635
+ """
636
+ return await self.request(method="GET", url=url, **kwargs)
637
+
638
+ async def head( # type: ignore[override]
639
+ self,
640
+ url: str,
641
+ **kwargs: Unpack[RequestParams],
642
+ ) -> Response:
643
+ """
644
+ Make an async HEAD request.
645
+
646
+ Args:
647
+ url: Request URL.
648
+ **kwargs: Request parameters.
649
+
650
+ Returns:
651
+ Response object.
652
+ """
653
+ return await self.request(method="HEAD", url=url, **kwargs)
654
+
655
+ async def options( # type: ignore[override]
656
+ self,
657
+ url: str,
658
+ **kwargs: Unpack[RequestParams],
659
+ ) -> Response:
660
+ """
661
+ Make an async OPTIONS request.
662
+
663
+ Args:
664
+ url: Request URL.
665
+ **kwargs: Request parameters.
666
+
667
+ Returns:
668
+ Response object.
669
+ """
670
+ return await self.request(method="OPTIONS", url=url, **kwargs)
671
+
672
+ async def delete( # type: ignore[override]
673
+ self,
674
+ url: str,
675
+ **kwargs: Unpack[RequestParams],
676
+ ) -> Response:
677
+ """
678
+ Make an async DELETE request.
679
+
680
+ Args:
681
+ url: Request URL.
682
+ **kwargs: Request parameters.
683
+
684
+ Returns:
685
+ Response object.
686
+ """
687
+ return await self.request(method="DELETE", url=url, **kwargs)
688
+
689
+ async def post( # type: ignore[override]
690
+ self,
691
+ url: str,
692
+ **kwargs: Unpack[RequestParams],
693
+ ) -> Response:
694
+ """
695
+ Make an async POST request.
696
+
697
+ Args:
698
+ url: Request URL.
699
+ **kwargs: Request parameters including body options.
700
+
701
+ Returns:
702
+ Response object.
703
+
704
+ Example:
705
+ ```python
706
+ response = await client.post(
707
+ "https://httpbin.org/post",
708
+ json={"key": "value"},
709
+ )
710
+ ```
711
+ """
712
+ return await self.request(method="POST", url=url, **kwargs)
713
+
714
+ async def put( # type: ignore[override]
715
+ self,
716
+ url: str,
717
+ **kwargs: Unpack[RequestParams],
718
+ ) -> Response:
719
+ """
720
+ Make an async PUT request.
721
+
722
+ Args:
723
+ url: Request URL.
724
+ **kwargs: Request parameters including body options.
725
+
726
+ Returns:
727
+ Response object.
728
+ """
729
+ return await self.request(method="PUT", url=url, **kwargs)
730
+
731
+ async def patch( # type: ignore[override]
732
+ self,
733
+ url: str,
734
+ **kwargs: Unpack[RequestParams],
735
+ ) -> Response:
736
+ """
737
+ Make an async PATCH request.
738
+
739
+ Args:
740
+ url: Request URL.
741
+ **kwargs: Request parameters including body options.
742
+
743
+ Returns:
744
+ Response object.
745
+ """
746
+ return await self.request(method="PATCH", url=url, **kwargs)
747
+
748
+ @asynccontextmanager
749
+ async def stream( # type: ignore[override]
750
+ self,
751
+ method: HttpMethod,
752
+ url: str,
753
+ **kwargs: Unpack[RequestParams],
754
+ ) -> AsyncIterator[StreamingResponse]:
755
+ """
756
+ Make an async streaming HTTP request.
757
+
758
+ Returns an async context manager that yields a StreamingResponse for
759
+ iterating over the response body in chunks.
760
+
761
+ Args:
762
+ method: HTTP method.
763
+ url: Request URL.
764
+ **kwargs: Request parameters.
765
+
766
+ Yields:
767
+ StreamingResponse: A response object that can be iterated.
768
+
769
+ Example:
770
+ ```python
771
+ async with client.stream("GET", "https://example.com/large-file") as response:
772
+ for chunk in response.iter_bytes():
773
+ process(chunk)
774
+ ```
775
+
776
+ Note:
777
+ Iteration over the response is synchronous (uses iter_bytes, iter_text,
778
+ iter_lines). The async part is initiating the request and entering
779
+ the context manager.
780
+ """
781
+ if method not in ["GET", "HEAD", "OPTIONS", "DELETE", "POST", "PUT", "PATCH"]:
782
+ raise ValueError(f"Unsupported HTTP method: {method}")
783
+ if "params" in kwargs and kwargs["params"] is not None:
784
+ kwargs["params"] = {k: str(v) for k, v in kwargs["params"].items()}
785
+
786
+ # Run the sync _stream in executor
787
+ response = await self._run_sync_asyncio(super(Client, self)._stream, method=method, url=url, **kwargs)
788
+ try:
789
+ yield response
790
+ finally:
791
+ response.close()
792
+
793
+
794
+ def request(
795
+ method: HttpMethod,
796
+ url: str,
797
+ verify: bool | None = True,
798
+ ca_cert_file: str | None = None,
799
+ client_pem: str | None = None,
800
+ client_pem_data: bytes | None = None,
801
+ **kwargs: Unpack[RequestParams],
802
+ ) -> Response:
803
+ """
804
+ Make an HTTP request using a temporary client.
805
+
806
+ This is a convenience function for one-off requests. For multiple requests,
807
+ use a Client instance for better performance (connection pooling).
808
+
809
+ Args:
810
+ method: HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS).
811
+ url: Request URL.
812
+ verify: Verify SSL certificates. Default is True.
813
+ ca_cert_file: Path to CA certificate bundle.
814
+ client_pem: Path to client certificate for mTLS.
815
+ client_pem_data: Client certificate and key as bytes for mTLS.
816
+ **kwargs: Additional request parameters.
817
+
818
+ Returns:
819
+ Response object.
820
+
821
+ Example:
822
+ ```python
823
+ import httpr
824
+
825
+ response = httpr.request("GET", "https://httpbin.org/get")
826
+ response = httpr.request("POST", "https://httpbin.org/post", json={"key": "value"})
827
+ ```
828
+ """
829
+ with Client(
830
+ verify=verify,
831
+ ca_cert_file=ca_cert_file,
832
+ client_pem=client_pem,
833
+ client_pem_data=client_pem_data,
834
+ ) as client:
835
+ return client.request(method, url, **kwargs)
836
+
837
+
838
+ def get(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response:
839
+ """
840
+ Make a GET request using a temporary client.
841
+
842
+ Args:
843
+ url: Request URL.
844
+ **kwargs: Request parameters (params, headers, cookies, auth, timeout, etc.).
845
+
846
+ Returns:
847
+ Response object.
848
+
849
+ Example:
850
+ ```python
851
+ import httpr
852
+
853
+ response = httpr.get("https://httpbin.org/get", params={"key": "value"})
854
+ print(response.json())
855
+ ```
856
+ """
857
+ return request(method="GET", url=url, **kwargs)
858
+
859
+
860
+ def head(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response:
861
+ """
862
+ Make a HEAD request using a temporary client.
863
+
864
+ Args:
865
+ url: Request URL.
866
+ **kwargs: Request parameters.
867
+
868
+ Returns:
869
+ Response object (body will be empty).
870
+
871
+ Example:
872
+ ```python
873
+ import httpr
874
+
875
+ response = httpr.head("https://httpbin.org/get")
876
+ print(response.headers)
877
+ ```
878
+ """
879
+ return request(method="HEAD", url=url, **kwargs)
880
+
881
+
882
+ def options(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response:
883
+ """
884
+ Make an OPTIONS request using a temporary client.
885
+
886
+ Args:
887
+ url: Request URL.
888
+ **kwargs: Request parameters.
889
+
890
+ Returns:
891
+ Response object.
892
+
893
+ Example:
894
+ ```python
895
+ import httpr
896
+
897
+ response = httpr.options("https://httpbin.org/get")
898
+ ```
899
+ """
900
+ return request(method="OPTIONS", url=url, **kwargs)
901
+
902
+
903
+ def delete(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response:
904
+ """
905
+ Make a DELETE request using a temporary client.
906
+
907
+ Args:
908
+ url: Request URL.
909
+ **kwargs: Request parameters.
910
+
911
+ Returns:
912
+ Response object.
913
+
914
+ Example:
915
+ ```python
916
+ import httpr
917
+
918
+ response = httpr.delete("https://httpbin.org/delete")
919
+ ```
920
+ """
921
+ return request(method="DELETE", url=url, **kwargs)
922
+
923
+
924
+ def post(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response:
925
+ """
926
+ Make a POST request using a temporary client.
927
+
928
+ Args:
929
+ url: Request URL.
930
+ **kwargs: Request parameters (json, data, content, files, etc.).
931
+
932
+ Returns:
933
+ Response object.
934
+
935
+ Example:
936
+ ```python
937
+ import httpr
938
+
939
+ # JSON body
940
+ response = httpr.post("https://httpbin.org/post", json={"key": "value"})
941
+
942
+ # Form data
943
+ response = httpr.post("https://httpbin.org/post", data={"field": "value"})
944
+ ```
945
+ """
946
+ return request(method="POST", url=url, **kwargs)
947
+
948
+
949
+ def put(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response:
950
+ """
951
+ Make a PUT request using a temporary client.
952
+
953
+ Args:
954
+ url: Request URL.
955
+ **kwargs: Request parameters.
956
+
957
+ Returns:
958
+ Response object.
959
+
960
+ Example:
961
+ ```python
962
+ import httpr
963
+
964
+ response = httpr.put("https://httpbin.org/put", json={"key": "value"})
965
+ ```
966
+ """
967
+ return request(method="PUT", url=url, **kwargs)
968
+
969
+
970
+ def patch(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response:
971
+ """
972
+ Make a PATCH request using a temporary client.
973
+
974
+ Args:
975
+ url: Request URL.
976
+ **kwargs: Request parameters.
977
+
978
+ Returns:
979
+ Response object.
980
+
981
+ Example:
982
+ ```python
983
+ import httpr
984
+
985
+ response = httpr.patch("https://httpbin.org/patch", json={"field": "new_value"})
986
+ ```
987
+ """
988
+ return request(method="PATCH", url=url, **kwargs)
989
+
990
+
991
+ # Import exceptions from the Rust module
992
+ from .httpr import ( # noqa: E402
993
+ CloseError,
994
+ # Network exceptions
995
+ ConnectError,
996
+ # Timeout exceptions
997
+ ConnectTimeout,
998
+ CookieConflict,
999
+ DecodingError,
1000
+ # Base exceptions
1001
+ HTTPError,
1002
+ HTTPStatusError,
1003
+ # Other exceptions
1004
+ InvalidURL,
1005
+ # Protocol exceptions
1006
+ LocalProtocolError,
1007
+ NetworkError,
1008
+ PoolTimeout,
1009
+ ProtocolError,
1010
+ ProxyError,
1011
+ ReadError,
1012
+ ReadTimeout,
1013
+ RemoteProtocolError,
1014
+ RequestError,
1015
+ RequestNotRead,
1016
+ ResponseNotRead,
1017
+ StreamClosed,
1018
+ # Stream exceptions
1019
+ StreamConsumed,
1020
+ StreamError,
1021
+ TimeoutException,
1022
+ TooManyRedirects,
1023
+ TransportError,
1024
+ # Other transport/request exceptions
1025
+ UnsupportedProtocol,
1026
+ WriteError,
1027
+ WriteTimeout,
1028
+ )
1029
+
1030
+ __all__ = [
1031
+ # Client and request functions
1032
+ "Client",
1033
+ "AsyncClient",
1034
+ "request",
1035
+ "get",
1036
+ "head",
1037
+ "options",
1038
+ "delete",
1039
+ "post",
1040
+ "put",
1041
+ "patch",
1042
+ # Response classes
1043
+ "Response",
1044
+ "StreamingResponse",
1045
+ "CaseInsensitiveHeaderMap",
1046
+ # Base exceptions
1047
+ "HTTPError",
1048
+ "RequestError",
1049
+ "TransportError",
1050
+ "NetworkError",
1051
+ "TimeoutException",
1052
+ "ProtocolError",
1053
+ "StreamError",
1054
+ # Timeout exceptions
1055
+ "ConnectTimeout",
1056
+ "ReadTimeout",
1057
+ "WriteTimeout",
1058
+ "PoolTimeout",
1059
+ # Network exceptions
1060
+ "ConnectError",
1061
+ "ReadError",
1062
+ "WriteError",
1063
+ "CloseError",
1064
+ # Protocol exceptions
1065
+ "LocalProtocolError",
1066
+ "RemoteProtocolError",
1067
+ # Other exceptions
1068
+ "UnsupportedProtocol",
1069
+ "ProxyError",
1070
+ "TooManyRedirects",
1071
+ "HTTPStatusError",
1072
+ "DecodingError",
1073
+ "StreamConsumed",
1074
+ "ResponseNotRead",
1075
+ "RequestNotRead",
1076
+ "StreamClosed",
1077
+ "InvalidURL",
1078
+ "CookieConflict",
1079
+ ]