xtr-http-client-contracts 4.0.2__py3-none-any.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.
Files changed (27) hide show
  1. xtr_http_client_contracts/__init__.py +47 -0
  2. xtr_http_client_contracts/exception/__init__.py +40 -0
  3. xtr_http_client_contracts/exception/client_error.py +16 -0
  4. xtr_http_client_contracts/exception/decoding_error.py +32 -0
  5. xtr_http_client_contracts/exception/http_client_error.py +21 -0
  6. xtr_http_client_contracts/exception/http_error.py +51 -0
  7. xtr_http_client_contracts/exception/invalid_argument_error.py +27 -0
  8. xtr_http_client_contracts/exception/json_error.py +19 -0
  9. xtr_http_client_contracts/exception/redirection_error.py +21 -0
  10. xtr_http_client_contracts/exception/server_error.py +15 -0
  11. xtr_http_client_contracts/exception/transport_error.py +31 -0
  12. xtr_http_client_contracts/exception/transport_timeout_error.py +27 -0
  13. xtr_http_client_contracts/http_client_interface.py +124 -0
  14. xtr_http_client_contracts/http_response_interface.py +148 -0
  15. xtr_http_client_contracts/options_defaults.py +69 -0
  16. xtr_http_client_contracts/py.typed +0 -0
  17. xtr_http_client_contracts/request_options.py +106 -0
  18. xtr_http_client_contracts/testing/__init__.py +12 -0
  19. xtr_http_client_contracts/testing/_fixture_request.py +184 -0
  20. xtr_http_client_contracts/testing/_fixture_response.py +80 -0
  21. xtr_http_client_contracts/testing/_fixture_routes.py +321 -0
  22. xtr_http_client_contracts/testing/http_client_test_case.py +1035 -0
  23. xtr_http_client_contracts/testing/test_http_server.py +213 -0
  24. xtr_http_client_contracts-4.0.2.dist-info/METADATA +271 -0
  25. xtr_http_client_contracts-4.0.2.dist-info/RECORD +27 -0
  26. xtr_http_client_contracts-4.0.2.dist-info/WHEEL +4 -0
  27. xtr_http_client_contracts-4.0.2.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,47 @@
1
+ """The async HTTP client contract: what code that calls out depends on, and nothing more.
2
+
3
+ A library that sends an HTTP request should not decide what sends it. It takes the client
4
+ interface, asks for a method and a URL — or hands over a message request — and gets back a
5
+ response whose status line and headers have arrived, its body read when, and only when, the
6
+ caller wants it.
7
+
8
+ What is here is only what appears in that contract: the client, the response it answers with,
9
+ the options a request takes, the errors any of them raises, and a conformance suite an
10
+ implementation can run against itself.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from .exception import (
16
+ ClientError,
17
+ DecodingError,
18
+ HttpClientError,
19
+ HttpError,
20
+ InvalidArgumentError,
21
+ JsonError,
22
+ RedirectionError,
23
+ ServerError,
24
+ TransportError,
25
+ TransportTimeoutError,
26
+ )
27
+ from .http_client_interface import HttpClientInterface
28
+ from .http_response_interface import HttpResponseInterface
29
+ from .options_defaults import OPTIONS_DEFAULTS
30
+ from .request_options import RequestOptions
31
+
32
+ __all__ = [
33
+ "OPTIONS_DEFAULTS",
34
+ "ClientError",
35
+ "DecodingError",
36
+ "HttpClientError",
37
+ "HttpClientInterface",
38
+ "HttpError",
39
+ "HttpResponseInterface",
40
+ "InvalidArgumentError",
41
+ "JsonError",
42
+ "RedirectionError",
43
+ "RequestOptions",
44
+ "ServerError",
45
+ "TransportError",
46
+ "TransportTimeoutError",
47
+ ]
@@ -0,0 +1,40 @@
1
+ """The errors the HTTP client contract names.
2
+
3
+ :class:`HttpClientError` is the root every one of them derives from,
4
+ including every error an implementation adds, so catching it catches anything
5
+ calling out can go wrong with, whichever client raised it.
6
+
7
+ Below it, two branches that mean different things. A
8
+ :class:`TransportError` — with :class:`TransportTimeoutError` for running out
9
+ of time — says there is no answer to look at. An :class:`HttpError` says
10
+ there is one and it is not a success: :class:`RedirectionError` for a 3xx
11
+ nobody followed, :class:`ClientError` for a 4xx, :class:`ServerError` for a
12
+ 5xx, each carrying the response. :class:`DecodingError` is the third case:
13
+ the answer arrived whole and is not what it was being read as.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from .client_error import ClientError
19
+ from .decoding_error import DecodingError
20
+ from .http_client_error import HttpClientError
21
+ from .http_error import HttpError
22
+ from .invalid_argument_error import InvalidArgumentError
23
+ from .json_error import JsonError
24
+ from .redirection_error import RedirectionError
25
+ from .server_error import ServerError
26
+ from .transport_error import TransportError
27
+ from .transport_timeout_error import TransportTimeoutError
28
+
29
+ __all__ = [
30
+ "ClientError",
31
+ "DecodingError",
32
+ "HttpClientError",
33
+ "HttpError",
34
+ "InvalidArgumentError",
35
+ "JsonError",
36
+ "RedirectionError",
37
+ "ServerError",
38
+ "TransportError",
39
+ "TransportTimeoutError",
40
+ ]
@@ -0,0 +1,16 @@
1
+ """The request was refused: a 4xx came back."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .http_error import HttpError
6
+
7
+ __all__ = ["ClientError"]
8
+
9
+
10
+ class ClientError(HttpError):
11
+ """A 4xx status: the server understood the request and would not serve it.
12
+
13
+ Retrying the same request unchanged gets the same answer, so this is the
14
+ one range worth telling apart — a 401 asks for credentials, a 404 says
15
+ there is nothing there, a 429 says to slow down.
16
+ """
@@ -0,0 +1,32 @@
1
+ """The body arrived whole but is not what it was being read as."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .http_client_error import HttpClientError
6
+
7
+ __all__ = ["DecodingError"]
8
+
9
+
10
+ class DecodingError(HttpClientError, ValueError):
11
+ """A body could not be turned into the shape that was asked for.
12
+
13
+ What :meth:`HttpResponseInterface.to_array
14
+ <xtr_http_client_contracts.http_response_interface.HttpResponseInterface.to_array>`
15
+ raises when the body is empty, is not JSON, or is JSON that is not an
16
+ object. The transfer succeeded, so this is about the payload, not the
17
+ connection.
18
+
19
+ Also a :class:`ValueError`, so code that already guards what it parses
20
+ with ``except ValueError`` keeps working without learning a new
21
+ exception.
22
+
23
+ Attributes:
24
+ reason: What is wrong with the body, naming the URL it came from.
25
+ """
26
+
27
+ reason: str
28
+
29
+ def __init__(self, reason: str) -> None:
30
+ """Record what is wrong with the body."""
31
+ self.reason = reason
32
+ super().__init__(reason)
@@ -0,0 +1,21 @@
1
+ """The root every error of the HTTP client contract derives from."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = ["HttpClientError"]
6
+
7
+
8
+ class HttpClientError(Exception):
9
+ """Base class for every error raised through the HTTP client contract.
10
+
11
+ Catch this to handle anything calling out can go wrong with, whichever
12
+ client is behind the interface; catch a subclass to handle one cause.
13
+ Every subclass carries what a caller needs as typed attributes and
14
+ composes its message from them.
15
+
16
+ A server answering is not a cause: a request that came back with a 404 or
17
+ a 503 succeeded as a transfer, so ``request()`` returns the response and
18
+ nothing is raised until someone reads its content and asks to be told.
19
+ What raises on its own is the transfer failing, a body that will not
20
+ decode, and an option no client here understands.
21
+ """
@@ -0,0 +1,51 @@
1
+ """A server answered, and the answer was not a success."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from .http_client_error import HttpClientError
8
+
9
+ if TYPE_CHECKING:
10
+ from xtr_http_client_contracts.http_response_interface import HttpResponseInterface
11
+
12
+ __all__ = ["HttpError"]
13
+
14
+
15
+ class HttpError(HttpClientError):
16
+ """The status code says the request did not get what it asked for.
17
+
18
+ Never raised by making the request: a status is an answer, not a failure
19
+ of the transfer, so it reaches the caller as a response. This is raised
20
+ where the caller asks to be told about it —
21
+ :meth:`HttpResponseInterface.get_content
22
+ <xtr_http_client_contracts.http_response_interface.HttpResponseInterface.get_content>`
23
+ or :meth:`HttpResponseInterface.to_array
24
+ <xtr_http_client_contracts.http_response_interface.HttpResponseInterface.to_array>`
25
+ with ``throw``.
26
+
27
+ Catch this to handle any unsuccessful status;
28
+ :class:`RedirectionError <...redirection_error.RedirectionError>`,
29
+ :class:`ClientError <...client_error.ClientError>` and
30
+ :class:`ServerError <...server_error.ServerError>` are the three ranges,
31
+ and they are siblings, so catching one leaves the others alone.
32
+
33
+ The response comes along because it is the only place the answer is left:
34
+ the status code, the headers and the body, which is kept.
35
+
36
+ Attributes:
37
+ response: The response that was answered with.
38
+ reason: What the status means for this request, naming the code and
39
+ the URL. The message is this, composed by whoever raised it —
40
+ the body of an error document is the most useful thing to put
41
+ there and reading it is awaited, which a constructor cannot do.
42
+ """
43
+
44
+ response: HttpResponseInterface
45
+ reason: str
46
+
47
+ def __init__(self, response: HttpResponseInterface, reason: str) -> None:
48
+ """Record the response the status came on, and what it means."""
49
+ self.response = response
50
+ self.reason = reason
51
+ super().__init__(reason)
@@ -0,0 +1,27 @@
1
+ """A method, URL or option the client cannot use; raised before anything is sent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .transport_error import TransportError
6
+
7
+ __all__ = ["InvalidArgumentError"]
8
+
9
+
10
+ # Both bases reach `BaseException`, which basedpyright flags as unsafe multiple inheritance.
11
+ # It is safe here: `ValueError` adds no state, so `TransportError.__init__` is the only one
12
+ # construction runs.
13
+ class InvalidArgumentError( # pyright: ignore[reportUnsafeMultipleInheritance]
14
+ TransportError,
15
+ ValueError,
16
+ ):
17
+ """The request was refused before it was sent, for something it asked for.
18
+
19
+ An unknown option name, an option whose value the client cannot honour, a
20
+ method or URL it cannot make a request with, or a body given both with the
21
+ request and as an option. Nothing reached the network, so there is no
22
+ response, and the same call with a corrected argument would go through.
23
+
24
+ Also a :class:`ValueError`, so code that already guards the arguments it
25
+ passes with ``except ValueError`` keeps working without learning a new
26
+ exception.
27
+ """
@@ -0,0 +1,19 @@
1
+ """A body that is not the JSON object to_array() promises."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .decoding_error import DecodingError
6
+
7
+ __all__ = ["JsonError"]
8
+
9
+
10
+ class JsonError(DecodingError):
11
+ """The body is JSON, but not the object :meth:`HttpResponseInterface.to_array` promises.
12
+
13
+ A JSON array, string, number or ``null`` where an object was asked for. The
14
+ body was read in full and parsed, so the failure is the shape of the payload,
15
+ not the transfer.
16
+
17
+ Also a :class:`DecodingError`, so code that already catches a body it could
18
+ not decode keeps catching this one.
19
+ """
@@ -0,0 +1,21 @@
1
+ """The answer points somewhere else, and nobody is following it any further."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .http_error import HttpError
6
+
7
+ __all__ = ["RedirectionError"]
8
+
9
+
10
+ class RedirectionError(HttpError):
11
+ """A 3xx status that was not followed.
12
+
13
+ Raised once ``max_redirects`` has been reached, or straight away when
14
+ following was turned off with ``max_redirects`` of zero or less — a
15
+ redirect that *is* followed never reaches the caller, since what comes
16
+ back is the response from the last hop.
17
+
18
+ Where the answer points is in the ``redirect_url`` entry of
19
+ :meth:`HttpResponseInterface.get_info
20
+ <xtr_http_client_contracts.http_response_interface.HttpResponseInterface.get_info>`.
21
+ """
@@ -0,0 +1,15 @@
1
+ """The server failed to answer the request: a 5xx came back."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .http_error import HttpError
6
+
7
+ __all__ = ["ServerError"]
8
+
9
+
10
+ class ServerError(HttpError):
11
+ """A 5xx status: the request may have been fine, the server was not.
12
+
13
+ The far side is at fault, so the same request may well succeed later.
14
+ Nothing here retries; what to do about it is the caller's to decide.
15
+ """
@@ -0,0 +1,31 @@
1
+ """The transfer itself failed, so there is no response to look at."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .http_client_error import HttpClientError
6
+
7
+ __all__ = ["TransportError"]
8
+
9
+
10
+ class TransportError(HttpClientError):
11
+ """Nothing usable came back, or the request could never be made.
12
+
13
+ A host that will not resolve, a connection refused or reset, a body that
14
+ stopped half way, a certificate the client would not accept — and the one
15
+ case that never touches the network: an option the client does not
16
+ support. A client reports that by raising rather than by ignoring it, so
17
+ an option that was spelled wrong, or that this client cannot honour,
18
+ cannot pass for one it quietly obeyed.
19
+
20
+ It is also what reading a response raises once it has been cancelled.
21
+
22
+ Attributes:
23
+ reason: What went wrong, naming the URL it went wrong for.
24
+ """
25
+
26
+ reason: str
27
+
28
+ def __init__(self, reason: str) -> None:
29
+ """Record what went wrong."""
30
+ self.reason = reason
31
+ super().__init__(reason)
@@ -0,0 +1,27 @@
1
+ """Nothing arrived in time."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .transport_error import TransportError
6
+
7
+ __all__ = ["TransportTimeoutError"]
8
+
9
+
10
+ # Both bases reach `BaseException`, which basedpyright flags as unsafe multiple inheritance.
11
+ # It is safe here: `TimeoutError` adds no state, so `TransportError.__init__` is the only one
12
+ # construction runs.
13
+ class TransportTimeoutError( # pyright: ignore[reportUnsafeMultipleInheritance]
14
+ TransportError,
15
+ TimeoutError,
16
+ ):
17
+ """The transfer went quiet for longer than it was allowed to.
18
+
19
+ Raised when the idle timeout passes with nothing coming in, when the
20
+ whole transfer outlasts ``max_duration``, or when connecting outlasts
21
+ ``max_connect_duration``.
22
+
23
+ Also a :class:`TimeoutError`, which is what every async library in the
24
+ standard library raises and what ``anyio`` cancel scopes turn a deadline
25
+ into, so code that already gives up on a timeout keeps working without
26
+ learning a new exception.
27
+ """
@@ -0,0 +1,124 @@
1
+ """The calls code that sends an HTTP request depends on."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Protocol, Self, runtime_checkable
6
+
7
+ from typing_extensions import Unpack
8
+
9
+ if TYPE_CHECKING:
10
+ from xtr_http_message_contracts import RequestInterface
11
+
12
+ from .http_response_interface import HttpResponseInterface
13
+ from .request_options import RequestOptions
14
+
15
+ __all__ = ["HttpClientInterface"]
16
+
17
+
18
+ @runtime_checkable
19
+ class HttpClientInterface(Protocol):
20
+ """Sends HTTP requests and hands back responses.
21
+
22
+ A library that calls out takes this interface, and the application
23
+ decides what is behind it — a real transfer, a scoped client, a guard, or
24
+ a client answering from memory in a test.
25
+
26
+ ```python
27
+ response = await client.request("GET", "https://example.com/orders")
28
+ orders = await response.to_array()
29
+ ```
30
+
31
+ Several requests run at once by being made from tasks of one task group.
32
+
33
+ An option name the contract does not list is refused with
34
+ :class:`InvalidArgumentError
35
+ <xtr_http_client_contracts.exception.invalid_argument_error.InvalidArgumentError>`:
36
+ ``Unsupported option "foo" passed to "<client class>", did you mean "..."?``.
37
+ An option that was spelled wrong cannot pass for one that was obeyed.
38
+ """
39
+
40
+ async def request(
41
+ self,
42
+ method: str,
43
+ url: str,
44
+ **options: Unpack[RequestOptions],
45
+ ) -> HttpResponseInterface:
46
+ """Make a request, and return once the status line and the headers are in.
47
+
48
+ The same as :meth:`send_request` with a message request carrying
49
+ ``method`` and ``url``, no headers and an empty body.
50
+
51
+ It never raises because of the status: a 404 or a 503 is an answer,
52
+ and comes back as a response.
53
+
54
+ Args:
55
+ method: The HTTP method, in upper case.
56
+ url: The URL to request, absolute or relative to the
57
+ ``base_uri`` option.
58
+ **options: How the request is to be made; anything left out
59
+ falls back to this client's defaults.
60
+
61
+ Returns:
62
+ The response, with its status and headers already in.
63
+
64
+ Raises:
65
+ TransportTimeoutError: When no answer came in time.
66
+ InvalidArgumentError: For an invalid method, URL or option, before
67
+ anything is sent.
68
+ TransportError: When no answer came.
69
+ """
70
+ ...
71
+
72
+ async def send_request(
73
+ self,
74
+ request: RequestInterface,
75
+ **options: Unpack[RequestOptions],
76
+ ) -> HttpResponseInterface:
77
+ """Send a message request, the options filling what it leaves out.
78
+
79
+ The request is authoritative:
80
+
81
+ - a relative URI is resolved against ``base_uri``;
82
+ - ``query`` is merged into the URI;
83
+ - a header from ``headers`` is sent only when the request has none of
84
+ that name;
85
+ - ``auth_basic`` and ``auth_bearer`` apply only when the request has
86
+ no ``Authorization`` header;
87
+ - ``body`` or ``json`` with a request whose body is not empty is
88
+ refused: ``Define either the request body or the "body" or "json"
89
+ option, setting both is not supported.``.
90
+
91
+ It returns, and never raises because of the status, as
92
+ :meth:`request` does.
93
+
94
+ Args:
95
+ request: The request to send.
96
+ **options: What the request leaves out; anything left out of
97
+ both falls back to this client's defaults.
98
+
99
+ Returns:
100
+ The response, with its status and headers already in.
101
+
102
+ Raises:
103
+ TransportTimeoutError: When no answer came in time.
104
+ InvalidArgumentError: For an invalid method, URL or option, and for
105
+ a body given both with the request and as an option, before
106
+ anything is sent.
107
+ TransportError: When no answer came.
108
+ """
109
+ ...
110
+
111
+ def with_options(self, **options: Unpack[RequestOptions]) -> Self:
112
+ """Return a client whose requests start from these options.
113
+
114
+ Args:
115
+ **options: The defaults of the client returned, merged over this
116
+ client's own, so what is left out is kept.
117
+
118
+ Returns:
119
+ A new client; this one is left as it was.
120
+
121
+ Raises:
122
+ InvalidArgumentError: When an option is invalid, as in :meth:`request`.
123
+ """
124
+ ...
@@ -0,0 +1,148 @@
1
+ """A response that has started arriving, and what a client adds to it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Protocol, Self, runtime_checkable
6
+
7
+ from xtr_http_message_contracts import ResponseInterface
8
+
9
+ if TYPE_CHECKING:
10
+ from types import TracebackType
11
+
12
+ __all__ = ["HttpResponseInterface"]
13
+
14
+
15
+ @runtime_checkable
16
+ class HttpResponseInterface(ResponseInterface, Protocol):
17
+ """An HTTP response whose body is read off the network when it is asked for.
18
+
19
+ It is a message response first: the status code and reason phrase, the
20
+ protocol version, the headers, the body as a stream and the ``with_*``
21
+ copies are the message contract's, so code typed against the plain
22
+ response interface takes this one as it is. A copy keeps the transfer
23
+ info and the very same body.
24
+
25
+ What a client adds:
26
+
27
+ - **A status is an answer, not a failure.** The response is handed back
28
+ once the status line and the headers are in, whatever the status.
29
+ :meth:`get_content` and :meth:`to_array` raise for a 3xx, 4xx or 5xx
30
+ when ``throw`` is left on, and return the body when it is turned off.
31
+ - **The body is kept.** It is read off the network on demand and kept —
32
+ in a temporary file past 2 MiB — so it can be read again;
33
+ ``get_body()`` is an async stream over that kept body.
34
+ - **It holds a connection until it is done with.** Leaving an
35
+ ``async with`` block closes the body, which frees the connection;
36
+ :meth:`cancel` does the same mid-transfer.
37
+ """
38
+
39
+ def get_info(self, key: str | None = None) -> object:
40
+ """Return what the transfer knows about itself, as it stands.
41
+
42
+ Never raises and never waits, so it can be read while the transfer is
43
+ going on.
44
+
45
+ These entries are always there:
46
+
47
+ - ``canceled`` (``bool``) — whether :meth:`cancel` was called.
48
+ - ``error`` (``str | None``) — what went wrong, once something has.
49
+ - ``http_code`` (``int``) — the last status code, or ``0`` before one
50
+ arrived.
51
+ - ``http_method`` (``str``) — the method of the last request made.
52
+ - ``redirect_count`` (``int``) — how many redirects were followed.
53
+ - ``redirect_url`` (``str | None``) — where a 3xx points, resolved,
54
+ when it was not followed.
55
+ - ``response_headers`` (``list[str]``) — the raw status and header
56
+ lines of every hop.
57
+ - ``start_time`` (``float``) — when the request was sent.
58
+ - ``total_time`` (``float``) — how long the transfer has taken.
59
+ - ``url`` (``str``) — the URL of the last hop.
60
+ - ``user_data`` — the ``user_data`` option, or ``None``.
61
+
62
+ ``primary_ip`` and ``retry_count`` are there when they are known.
63
+
64
+ Args:
65
+ key: One entry to look up. ``None`` asks for all of them.
66
+
67
+ Returns:
68
+ A mapping of every entry when ``key`` is ``None``; otherwise the
69
+ value under ``key``, or ``None`` when there is no such entry.
70
+ """
71
+ ...
72
+
73
+ async def cancel(self) -> None:
74
+ """Abandon the transfer and let go of what it was holding.
75
+
76
+ Afterwards ``get_info("canceled")`` reads ``True`` and
77
+ ``get_info("error")`` reads ``Response has been canceled.``, and
78
+ reading the body raises :class:`TransportError
79
+ <xtr_http_client_contracts.exception.transport_error.TransportError>`
80
+ with that message.
81
+ """
82
+ ...
83
+
84
+ async def get_content(self, throw: bool = True) -> bytes:
85
+ """Read the whole body, waiting for the rest of it to arrive.
86
+
87
+ Args:
88
+ throw: Whether an unsuccessful status is to be raised rather than
89
+ returned. The message is ``HTTP <code> returned for "<url>".``,
90
+ or the title and detail of the body when it is a JSON problem
91
+ document (RFC 7807, or its hydra form).
92
+
93
+ Returns:
94
+ Every byte of the body, empty when there was none.
95
+
96
+ Raises:
97
+ TransportError: When the transfer failed or was cancelled.
98
+ RedirectionError: On a 3xx once ``max_redirects`` was reached,
99
+ when ``throw``.
100
+ ClientError: On a 4xx, when ``throw``.
101
+ ServerError: On a 5xx, when ``throw``.
102
+ """
103
+ ...
104
+
105
+ async def to_array(self, throw: bool = True) -> dict[str, object]:
106
+ """Read the whole body and decode it as a JSON object.
107
+
108
+ Args:
109
+ throw: Whether an unsuccessful status is to be raised rather than
110
+ returned, as in :meth:`get_content`.
111
+
112
+ Returns:
113
+ What the body decoded to.
114
+
115
+ Raises:
116
+ DecodingError: When the body is empty or is not JSON.
117
+ JsonError: When the body is JSON but not an object.
118
+ TransportError: When the transfer failed or was cancelled.
119
+ RedirectionError: On a 3xx once ``max_redirects`` was reached,
120
+ when ``throw``.
121
+ ClientError: On a 4xx, when ``throw``.
122
+ ServerError: On a 5xx, when ``throw``.
123
+ """
124
+ ...
125
+
126
+ async def __aenter__(self) -> Self:
127
+ """Enter a block that owns the transfer.
128
+
129
+ Returns:
130
+ This response.
131
+ """
132
+ ...
133
+
134
+ async def __aexit__(
135
+ self,
136
+ exc_type: type[BaseException] | None,
137
+ exc: BaseException | None,
138
+ traceback: TracebackType | None,
139
+ ) -> None:
140
+ """Close the body however the block ends, and nothing else.
141
+
142
+ Args:
143
+ exc_type: The class of the exception the block is leaving with,
144
+ or ``None`` when it is leaving normally.
145
+ exc: That exception.
146
+ traceback: Its traceback.
147
+ """
148
+ ...