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