1# --------------------------------------------------------------------------
2#
3# Copyright (c) Microsoft Corporation. All rights reserved.
4#
5# The MIT License (MIT)
6#
7# Permission is hereby granted, free of charge, to any person obtaining a copy
8# of this software and associated documentation files (the ""Software""), to
9# deal in the Software without restriction, including without limitation the
10# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11# sell copies of the Software, and to permit persons to whom the Software is
12# furnished to do so, subject to the following conditions:
13#
14# The above copyright notice and this permission notice shall be included in
15# all copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23# IN THE SOFTWARE.
24#
25# --------------------------------------------------------------------------
26import abc
27import copy
28from typing import (
29 Any,
30 AsyncIterable,
31 AsyncIterator,
32 Iterable,
33 Iterator,
34 Optional,
35 Union,
36 MutableMapping,
37 Dict,
38 AsyncContextManager,
39)
40
41from ..utils._utils import case_insensitive_dict
42
43from ._helpers import (
44 ParamsType,
45 FilesType,
46 set_json_body,
47 set_multipart_body,
48 set_urlencoded_body,
49 _format_parameters_helper,
50 HttpRequestBackcompatMixin,
51 set_content_body,
52)
53
54ContentType = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
55
56################################## CLASSES ######################################
57
58
59class HttpRequest(HttpRequestBackcompatMixin):
60 """An HTTP request.
61
62 It should be passed to your client's `send_request` method.
63
64 >>> from azure.core.rest import HttpRequest
65 >>> request = HttpRequest('GET', 'http://www.example.com')
66 <HttpRequest [GET], url: 'http://www.example.com'>
67 >>> response = client.send_request(request)
68 <HttpResponse: 200 OK>
69
70 :param str method: HTTP method (GET, HEAD, etc.)
71 :param str url: The url for your request
72 :keyword mapping params: Query parameters to be mapped into your URL. Your input
73 should be a mapping of query name to query value(s).
74 :keyword mapping headers: HTTP headers you want in your request. Your input should
75 be a mapping of header name to header value.
76 :keyword any json: A JSON serializable object. We handle JSON-serialization for your
77 object, so use this for more complicated data structures than `data`.
78 :keyword content: Content you want in your request body. Think of it as the kwarg you should input
79 if your data doesn't fit into `json`, `data`, or `files`. Accepts a bytes type, or a generator
80 that yields bytes.
81 :paramtype content: str or bytes or iterable[bytes] or asynciterable[bytes]
82 :keyword dict data: Form data you want in your request body. Use for form-encoded data, i.e.
83 HTML forms.
84 :keyword mapping files: Files you want to in your request body. Use for uploading files with
85 multipart encoding. Your input should be a mapping of file name to file content.
86 Use the `data` kwarg in addition if you want to include non-file data files as part of your request.
87 :ivar str url: The URL this request is against.
88 :ivar str method: The method type of this request.
89 :ivar mapping headers: The HTTP headers you passed in to your request
90 :ivar any content: The content passed in for the request
91 """
92
93 def __init__(
94 self,
95 method: str,
96 url: str,
97 *,
98 params: Optional[ParamsType] = None,
99 headers: Optional[MutableMapping[str, str]] = None,
100 json: Any = None,
101 content: Optional[ContentType] = None,
102 data: Optional[Dict[str, Any]] = None,
103 files: Optional[FilesType] = None,
104 **kwargs: Any
105 ):
106 self.url = url
107 self.method = method
108
109 if params:
110 _format_parameters_helper(self, params)
111 self._files = None
112 self._data: Any = None
113
114 default_headers = self._set_body(
115 content=content,
116 data=data,
117 files=files,
118 json=json,
119 )
120 self.headers: MutableMapping[str, str] = case_insensitive_dict(default_headers)
121 self.headers.update(headers or {})
122
123 if kwargs:
124 raise TypeError(
125 "You have passed in kwargs '{}' that are not valid kwargs.".format("', '".join(list(kwargs.keys())))
126 )
127
128 def _set_body(
129 self,
130 content: Optional[ContentType] = None,
131 data: Optional[Dict[str, Any]] = None,
132 files: Optional[FilesType] = None,
133 json: Any = None,
134 ) -> MutableMapping[str, str]:
135 """Sets the body of the request, and returns the default headers.
136
137 :param content: Content you want in your request body.
138 :type content: str or bytes or iterable[bytes] or asynciterable[bytes]
139 :param dict data: Form data you want in your request body.
140 :param dict files: Files you want to in your request body.
141 :param any json: A JSON serializable object.
142 :return: The default headers for the request
143 :rtype: MutableMapping[str, str]
144 """
145 default_headers: MutableMapping[str, str] = {}
146 if data is not None and not isinstance(data, dict):
147 # should we warn?
148 content = data
149 if content is not None:
150 default_headers, self._data = set_content_body(content)
151 return default_headers
152 if json is not None:
153 default_headers, self._data = set_json_body(json)
154 return default_headers
155 if files:
156 default_headers, self._files = set_multipart_body(files)
157 if data:
158 default_headers, self._data = set_urlencoded_body(data, has_files=bool(files))
159 return default_headers
160
161 @property
162 def content(self) -> Any:
163 """Get's the request's content
164
165 :return: The request's content
166 :rtype: any
167 """
168 return self._data or self._files
169
170 def __repr__(self) -> str:
171 return "<HttpRequest [{}], url: '{}'>".format(self.method, self.url)
172
173 def __deepcopy__(self, memo: Optional[Dict[int, Any]] = None) -> "HttpRequest":
174 try:
175 request = HttpRequest(
176 method=self.method,
177 url=self.url,
178 headers=self.headers,
179 )
180 request._data = copy.deepcopy(self._data, memo)
181 request._files = copy.deepcopy(self._files, memo)
182 self._add_backcompat_properties(request, memo)
183 return request
184 except (ValueError, TypeError):
185 return copy.copy(self)
186
187
188class _HttpResponseBase(abc.ABC):
189 """Base abstract base class for HttpResponses."""
190
191 @property
192 @abc.abstractmethod
193 def request(self) -> HttpRequest:
194 """The request that resulted in this response.
195
196 :rtype: ~azure.core.rest.HttpRequest
197 :return: The request that resulted in this response.
198 """
199
200 @property
201 @abc.abstractmethod
202 def status_code(self) -> int:
203 """The status code of this response.
204
205 :rtype: int
206 :return: The status code of this response.
207 """
208
209 @property
210 @abc.abstractmethod
211 def headers(self) -> MutableMapping[str, str]:
212 """The response headers. Must be case-insensitive.
213
214 :rtype: MutableMapping[str, str]
215 :return: The response headers. Must be case-insensitive.
216 """
217
218 @property
219 @abc.abstractmethod
220 def reason(self) -> str:
221 """The reason phrase for this response.
222
223 :rtype: str
224 :return: The reason phrase for this response.
225 """
226
227 @property
228 @abc.abstractmethod
229 def content_type(self) -> Optional[str]:
230 """The content type of the response.
231
232 :rtype: str
233 :return: The content type of the response.
234 """
235
236 @property
237 @abc.abstractmethod
238 def is_closed(self) -> bool:
239 """Whether the network connection has been closed yet.
240
241 :rtype: bool
242 :return: Whether the network connection has been closed yet.
243 """
244
245 @property
246 @abc.abstractmethod
247 def is_stream_consumed(self) -> bool:
248 """Whether the stream has been consumed.
249
250 :rtype: bool
251 :return: Whether the stream has been consumed.
252 """
253
254 @property
255 @abc.abstractmethod
256 def encoding(self) -> Optional[str]:
257 """Returns the response encoding.
258
259 :return: The response encoding. We either return the encoding set by the user,
260 or try extracting the encoding from the response's content type. If all fails,
261 we return `None`.
262 :rtype: optional[str]
263 """
264
265 @encoding.setter
266 def encoding(self, value: Optional[str]) -> None:
267 """Sets the response encoding.
268
269 :param optional[str] value: The encoding to set
270 """
271
272 @property
273 @abc.abstractmethod
274 def url(self) -> str:
275 """The URL that resulted in this response.
276
277 :rtype: str
278 :return: The URL that resulted in this response.
279 """
280
281 @property
282 @abc.abstractmethod
283 def content(self) -> bytes:
284 """Return the response's content in bytes.
285
286 :rtype: bytes
287 :return: The response's content in bytes.
288 """
289
290 @abc.abstractmethod
291 def text(self, encoding: Optional[str] = None) -> str:
292 """Returns the response body as a string.
293
294 :param optional[str] encoding: The encoding you want to decode the text with. Can
295 also be set independently through our encoding property
296 :return: The response's content decoded as a string.
297 :rtype: str
298 """
299
300 @abc.abstractmethod
301 def json(self) -> Any:
302 """Returns the whole body as a json object.
303
304 :return: The JSON deserialized response body
305 :rtype: any
306 :raises json.decoder.JSONDecodeError: if the body is not valid JSON.
307 """
308
309 @abc.abstractmethod
310 def raise_for_status(self) -> None:
311 """Raises an HttpResponseError if the response has an error status code.
312
313 If response is good, does nothing.
314
315 :raises ~azure.core.HttpResponseError: if the object has an error status code.
316 """
317
318
319class HttpResponse(_HttpResponseBase):
320 """Abstract base class for HTTP responses.
321
322 Use this abstract base class to create your own transport responses.
323
324 Responses implementing this ABC are returned from your client's `send_request` method
325 if you pass in an :class:`~azure.core.rest.HttpRequest`
326
327 >>> from azure.core.rest import HttpRequest
328 >>> request = HttpRequest('GET', 'http://www.example.com')
329 <HttpRequest [GET], url: 'http://www.example.com'>
330 >>> response = client.send_request(request)
331 <HttpResponse: 200 OK>
332 """
333
334 @abc.abstractmethod
335 def __enter__(self) -> "HttpResponse": ...
336
337 @abc.abstractmethod
338 def __exit__(self, *args: Any) -> None: ...
339
340 @abc.abstractmethod
341 def close(self) -> None: ...
342
343 @abc.abstractmethod
344 def read(self) -> bytes:
345 """Read the response's bytes.
346
347 :return: The read in bytes
348 :rtype: bytes
349 """
350
351 @abc.abstractmethod
352 def iter_raw(self, **kwargs: Any) -> Iterator[bytes]:
353 """Iterates over the response's bytes. Will not decompress in the process.
354
355 :return: An iterator of bytes from the response
356 :rtype: Iterator[str]
357 """
358
359 @abc.abstractmethod
360 def iter_bytes(self, **kwargs: Any) -> Iterator[bytes]:
361 """Iterates over the response's bytes. Will decompress in the process.
362
363 :return: An iterator of bytes from the response
364 :rtype: Iterator[str]
365 """
366
367 def __repr__(self) -> str:
368 content_type_str = ", Content-Type: {}".format(self.content_type) if self.content_type else ""
369 return "<HttpResponse: {} {}{}>".format(self.status_code, self.reason, content_type_str)
370
371
372class AsyncHttpResponse(_HttpResponseBase, AsyncContextManager["AsyncHttpResponse"]):
373 """Abstract base class for Async HTTP responses.
374
375 Use this abstract base class to create your own transport responses.
376
377 Responses implementing this ABC are returned from your async client's `send_request`
378 method if you pass in an :class:`~azure.core.rest.HttpRequest`
379
380 >>> from azure.core.rest import HttpRequest
381 >>> request = HttpRequest('GET', 'http://www.example.com')
382 <HttpRequest [GET], url: 'http://www.example.com'>
383 >>> response = await client.send_request(request)
384 <AsyncHttpResponse: 200 OK>
385 """
386
387 @abc.abstractmethod
388 async def read(self) -> bytes:
389 """Read the response's bytes into memory.
390
391 :return: The response's bytes
392 :rtype: bytes
393 """
394
395 @abc.abstractmethod
396 async def iter_raw(self, **kwargs: Any) -> AsyncIterator[bytes]:
397 """Asynchronously iterates over the response's bytes. Will not decompress in the process.
398
399 :return: An async iterator of bytes from the response
400 :rtype: AsyncIterator[bytes]
401 """
402 raise NotImplementedError()
403 # getting around mypy behavior, see https://github.com/python/mypy/issues/10732
404 yield # pylint: disable=unreachable
405
406 @abc.abstractmethod
407 async def iter_bytes(self, **kwargs: Any) -> AsyncIterator[bytes]:
408 """Asynchronously iterates over the response's bytes. Will decompress in the process.
409
410 :return: An async iterator of bytes from the response
411 :rtype: AsyncIterator[bytes]
412 """
413 raise NotImplementedError()
414 # getting around mypy behavior, see https://github.com/python/mypy/issues/10732
415 yield # pylint: disable=unreachable
416
417 @abc.abstractmethod
418 async def close(self) -> None: ...