Coverage for src/httpx/_client.py: 95%
73 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-05-27 16:49 +0100
« prev ^ index » next coverage.py v7.6.12, created at 2025-05-27 16:49 +0100
1import contextlib
2import types
3import typing
5from ._content import Content
6from ._headers import Headers
7from ._pool import Transport, open_connection_pool
8from ._request import Request
9from ._response import Response
10from ._streams import Stream
11from ._urls import URL
13__all__ = ["Client", "Content", "open_client"]
16class Client:
17 def __init__(
18 self,
19 transport: Transport,
20 url: URL | str | None = None,
21 headers: Headers | typing.Mapping[str, str] | None = None,
22 ):
23 if url is None:
24 url = ""
25 if headers is None:
26 headers = {"User-Agent": "dev"}
28 self.transport = transport
29 self.url = URL(url)
30 self.headers = Headers(headers)
31 self.via = RedirectMiddleware(self.transport)
33 def build_request(
34 self,
35 method: str,
36 url: URL | str,
37 headers: Headers | typing.Mapping[str, str] | None = None,
38 content: Content | Stream | bytes | None = None,
39 ) -> Request:
40 return Request(
41 method=method,
42 url=self.url.join(url),
43 headers=self.headers.copy_update(headers),
44 content=content,
45 )
47 def request(
48 self,
49 method: str,
50 url: URL | str,
51 headers: Headers | typing.Mapping[str, str] | None = None,
52 content: Content | Stream | bytes | None = None,
53 ) -> Response:
54 request = self.build_request(method, url, headers=headers, content=content)
55 with self.via.send(request) as response:
56 response.read()
57 return response
59 @contextlib.contextmanager
60 def stream(
61 self,
62 method: str,
63 url: URL | str,
64 headers: Headers | typing.Mapping[str, str] | None = None,
65 content: Content | Stream | bytes | None = None,
66 ) -> typing.Iterator[Response]:
67 request = self.build_request(method, url, headers=headers, content=content)
68 with self.via.send(request) as response:
69 yield response
71 def get(
72 self,
73 url: URL | str,
74 headers: Headers | typing.Mapping[str, str] | None = None,
75 ):
76 return self.request("GET", url, headers=headers)
78 def post(
79 self,
80 url: URL | str,
81 headers: Headers | typing.Mapping[str, str] | None = None,
82 content: Content | Stream | bytes | None = None,
83 ):
84 return self.request("POST", url, headers=headers, content=content)
86 def put(
87 self,
88 url: URL | str,
89 headers: Headers | typing.Mapping[str, str] | None = None,
90 content: Content | Stream | bytes | None = None,
91 ):
92 return self.request("PUT", url, headers=headers, content=content)
94 def patch(
95 self,
96 url: URL | str,
97 headers: Headers | typing.Mapping[str, str] | None = None,
98 content: Content | Stream | bytes | None = None,
99 ):
100 return self.request("PATCH", url, headers=headers, content=content)
102 def delete(
103 self,
104 url: URL | str,
105 headers: Headers | typing.Mapping[str, str] | None = None,
106 ):
107 return self.request("DELETE", url, headers=headers)
109 def close(self):
110 self.transport.close()
112 def __enter__(self):
113 return self
115 def __exit__(
116 self,
117 exc_type: type[BaseException] | None = None,
118 exc_value: BaseException | None = None,
119 traceback: types.TracebackType | None = None
120 ):
121 self.close()
123 def __repr__(self):
124 return f"<Client [{self.transport.description()}]>"
127class RedirectMiddleware(Transport):
128 def __init__(self, transport: Transport) -> None:
129 self._transport = transport
131 def is_redirect(self, response: Response) -> bool:
132 return (
133 response.code in (301, 302, 303, 307, 308)
134 and "Location" in response.headers
135 )
137 def build_redirect_request(self, request: Request, response: Response) -> Request:
138 raise NotImplementedError()
140 @contextlib.contextmanager
141 def send(self, request: Request) -> typing.Iterator[Response]:
142 while True:
143 with self._transport.send(request) as response:
144 if not self.is_redirect(response):
145 yield response
146 return
148 # If we have a redirect, then we read the body of the response.
149 # Ensures that the HTTP connection is available for a new
150 # request/response cycle.
151 response.read()
153 # We've made a request-response and now need to issue a redirect request.
154 request = self.build_redirect_request(request, response)
156 def aclose(self):
157 pass
160def open_client(
161 transport: Transport | None = None,
162 url: URL | str | None = None,
163 headers: Headers | typing.Mapping[str, str] | None = None,
164):
165 if transport is None:
166 transport = open_connection_pool()
167 return Client(transport=transport, url=url, headers=headers)