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

1import contextlib 

2import types 

3import typing 

4 

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 

12 

13__all__ = ["Client", "Content", "open_client"] 

14 

15 

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"} 

27 

28 self.transport = transport 

29 self.url = URL(url) 

30 self.headers = Headers(headers) 

31 self.via = RedirectMiddleware(self.transport) 

32 

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 ) 

46 

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 

58 

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 

70 

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) 

77 

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) 

85 

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) 

93 

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) 

101 

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) 

108 

109 def close(self): 

110 self.transport.close() 

111 

112 def __enter__(self): 

113 return self 

114 

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() 

122 

123 def __repr__(self): 

124 return f"<Client [{self.transport.description()}]>" 

125 

126 

127class RedirectMiddleware(Transport): 

128 def __init__(self, transport: Transport) -> None: 

129 self._transport = transport 

130 

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 ) 

136 

137 def build_redirect_request(self, request: Request, response: Response) -> Request: 

138 raise NotImplementedError() 

139 

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 

147 

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() 

152 

153 # We've made a request-response and now need to issue a redirect request. 

154 request = self.build_redirect_request(request, response) 

155 

156 def aclose(self): 

157 pass 

158 

159 

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)