Coverage for src/httpx/_client.py: 95%

1 import contextlib

2 import types

3 import typing

4

5 from ._content import Content

6 from ._headers import Headers

7 from ._pool import Transport, open_connection_pool

8 from ._request import Request

9 from ._response import Response

10 from ._streams import Stream

11 from ._urls import URL

12

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

14

15

16 class 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

127 class 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

160 def 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)