httpdex 0.1.0__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.
- httpdex/__init__.py +85 -0
- httpdex/_api.py +65 -0
- httpdex/_api_test.py +53 -0
- httpdex/_auth.py +40 -0
- httpdex/_auth_test.py +39 -0
- httpdex/_client.py +1042 -0
- httpdex/_client_test.py +1926 -0
- httpdex/_compat_test.py +729 -0
- httpdex/_cookies.py +5 -0
- httpdex/_decoders.py +147 -0
- httpdex/_decoders_test.py +189 -0
- httpdex/_exceptions.py +116 -0
- httpdex/_exceptions_test.py +129 -0
- httpdex/_headers.py +76 -0
- httpdex/_headers_test.py +86 -0
- httpdex/_models.py +371 -0
- httpdex/_models_test.py +559 -0
- httpdex/_multipart.py +78 -0
- httpdex/_multipart_test.py +143 -0
- httpdex/_status_codes.py +68 -0
- httpdex/_timeout.py +33 -0
- httpdex/_timeout_test.py +44 -0
- httpdex/_transports.py +101 -0
- httpdex/_transports_test.py +211 -0
- httpdex/_types.py +60 -0
- httpdex/_url.py +217 -0
- httpdex/_url_test.py +269 -0
- httpdex/py.typed +0 -0
- httpdex-0.1.0.dist-info/METADATA +60 -0
- httpdex-0.1.0.dist-info/RECORD +31 -0
- httpdex-0.1.0.dist-info/WHEEL +4 -0
httpdex/__init__.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from httpdex._api import delete, get, head, options, patch, post, put, request
|
|
4
|
+
from httpdex._auth import BasicAuth, DigestAuth
|
|
5
|
+
from httpdex._client import AsyncClient, Client
|
|
6
|
+
from httpdex._cookies import CookieStore
|
|
7
|
+
from httpdex._exceptions import (
|
|
8
|
+
CloseError,
|
|
9
|
+
ConnectError,
|
|
10
|
+
ConnectTimeout,
|
|
11
|
+
DecodingError,
|
|
12
|
+
HTTPError,
|
|
13
|
+
HTTPStatusError,
|
|
14
|
+
InvalidURL,
|
|
15
|
+
NetworkError,
|
|
16
|
+
PoolTimeout,
|
|
17
|
+
ReadError,
|
|
18
|
+
ReadTimeout,
|
|
19
|
+
RequestError,
|
|
20
|
+
RequestNotRead,
|
|
21
|
+
ResponseNotRead,
|
|
22
|
+
StreamClosed,
|
|
23
|
+
StreamConsumed,
|
|
24
|
+
StreamError,
|
|
25
|
+
TimeoutException,
|
|
26
|
+
TooManyRedirects,
|
|
27
|
+
TransportError,
|
|
28
|
+
UnsupportedProtocol,
|
|
29
|
+
WriteError,
|
|
30
|
+
WriteTimeout,
|
|
31
|
+
)
|
|
32
|
+
from httpdex._headers import Headers
|
|
33
|
+
from httpdex._models import Request, Response
|
|
34
|
+
from httpdex._status_codes import codes
|
|
35
|
+
from httpdex._timeout import Timeout
|
|
36
|
+
from httpdex._transports import ASGITransport, MockTransport
|
|
37
|
+
from httpdex._url import URL, QueryParams
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"ASGITransport",
|
|
41
|
+
"AsyncClient",
|
|
42
|
+
"BasicAuth",
|
|
43
|
+
"Client",
|
|
44
|
+
"CloseError",
|
|
45
|
+
"CookieStore",
|
|
46
|
+
"ConnectError",
|
|
47
|
+
"ConnectTimeout",
|
|
48
|
+
"DecodingError",
|
|
49
|
+
"DigestAuth",
|
|
50
|
+
"HTTPError",
|
|
51
|
+
"HTTPStatusError",
|
|
52
|
+
"Headers",
|
|
53
|
+
"InvalidURL",
|
|
54
|
+
"MockTransport",
|
|
55
|
+
"NetworkError",
|
|
56
|
+
"PoolTimeout",
|
|
57
|
+
"QueryParams",
|
|
58
|
+
"ReadError",
|
|
59
|
+
"ReadTimeout",
|
|
60
|
+
"Request",
|
|
61
|
+
"RequestError",
|
|
62
|
+
"RequestNotRead",
|
|
63
|
+
"Response",
|
|
64
|
+
"ResponseNotRead",
|
|
65
|
+
"StreamClosed",
|
|
66
|
+
"StreamConsumed",
|
|
67
|
+
"StreamError",
|
|
68
|
+
"Timeout",
|
|
69
|
+
"TimeoutException",
|
|
70
|
+
"TooManyRedirects",
|
|
71
|
+
"TransportError",
|
|
72
|
+
"URL",
|
|
73
|
+
"UnsupportedProtocol",
|
|
74
|
+
"WriteError",
|
|
75
|
+
"WriteTimeout",
|
|
76
|
+
"codes",
|
|
77
|
+
"delete",
|
|
78
|
+
"get",
|
|
79
|
+
"head",
|
|
80
|
+
"options",
|
|
81
|
+
"patch",
|
|
82
|
+
"post",
|
|
83
|
+
"put",
|
|
84
|
+
"request",
|
|
85
|
+
]
|
httpdex/_api.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from httpdex._client import Client
|
|
7
|
+
from httpdex._headers import Headers
|
|
8
|
+
from httpdex._models import Response
|
|
9
|
+
from httpdex._url import URL
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def request(
|
|
13
|
+
method: str,
|
|
14
|
+
url: URL | str,
|
|
15
|
+
*,
|
|
16
|
+
params: Mapping[str, str] | Sequence[tuple[str, str]] | None = None,
|
|
17
|
+
content: str | bytes | None = None,
|
|
18
|
+
data: Mapping[str, Any] | None = None,
|
|
19
|
+
json: Any | None = None,
|
|
20
|
+
headers: Headers | Mapping[str, str] | Sequence[tuple[str, str]] | None = None,
|
|
21
|
+
cookies: dict[str, str] | None = None,
|
|
22
|
+
follow_redirects: bool = False,
|
|
23
|
+
timeout: float | None = 5.0,
|
|
24
|
+
) -> Response:
|
|
25
|
+
with Client() as client:
|
|
26
|
+
return client.request(
|
|
27
|
+
method,
|
|
28
|
+
url,
|
|
29
|
+
params=params,
|
|
30
|
+
content=content,
|
|
31
|
+
data=data,
|
|
32
|
+
json=json,
|
|
33
|
+
headers=headers,
|
|
34
|
+
cookies=cookies,
|
|
35
|
+
follow_redirects=follow_redirects,
|
|
36
|
+
timeout=timeout,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get(url: URL | str, **kwargs: Any) -> Response:
|
|
41
|
+
return request("GET", url, **kwargs)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def post(url: URL | str, **kwargs: Any) -> Response:
|
|
45
|
+
return request("POST", url, **kwargs)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def put(url: URL | str, **kwargs: Any) -> Response:
|
|
49
|
+
return request("PUT", url, **kwargs)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def patch(url: URL | str, **kwargs: Any) -> Response:
|
|
53
|
+
return request("PATCH", url, **kwargs)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def delete(url: URL | str, **kwargs: Any) -> Response:
|
|
57
|
+
return request("DELETE", url, **kwargs)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def head(url: URL | str, **kwargs: Any) -> Response:
|
|
61
|
+
return request("HEAD", url, **kwargs)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def options(url: URL | str, **kwargs: Any) -> Response:
|
|
65
|
+
return request("OPTIONS", url, **kwargs)
|
httpdex/_api_test.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
import httpdex_core
|
|
6
|
+
|
|
7
|
+
import httpdex
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _ok_handler(raw_request: bytes) -> bytes:
|
|
11
|
+
body = b'{"ok": true}'
|
|
12
|
+
return (
|
|
13
|
+
b"HTTP/1.1 200 OK\r\n"
|
|
14
|
+
b"Content-Type: application/json\r\n"
|
|
15
|
+
b"Content-Length: " + str(len(body)).encode() + b"\r\n"
|
|
16
|
+
b"\r\n" + body
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _patch_client_backend(client: httpdex.Client) -> None:
|
|
21
|
+
client._pool._backend = httpdex_core.MockSyncBackend(_ok_handler) # type: ignore[assignment]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_get() -> None:
|
|
25
|
+
with patch.object(httpdex._api, "Client") as MockClient:
|
|
26
|
+
real_client = httpdex.Client()
|
|
27
|
+
_patch_client_backend(real_client)
|
|
28
|
+
MockClient.return_value.__enter__ = lambda self: real_client
|
|
29
|
+
MockClient.return_value.__exit__ = lambda self, *args: real_client.close()
|
|
30
|
+
response = httpdex.get("http://example.com/")
|
|
31
|
+
assert response.status_code == 200
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_post() -> None:
|
|
35
|
+
with patch.object(httpdex._api, "Client") as MockClient:
|
|
36
|
+
real_client = httpdex.Client()
|
|
37
|
+
_patch_client_backend(real_client)
|
|
38
|
+
MockClient.return_value.__enter__ = lambda self: real_client
|
|
39
|
+
MockClient.return_value.__exit__ = lambda self, *args: real_client.close()
|
|
40
|
+
response = httpdex.post("http://example.com/", json={"key": "value"})
|
|
41
|
+
assert response.status_code == 200
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_all_methods() -> None:
|
|
45
|
+
with patch.object(httpdex._api, "Client") as MockClient:
|
|
46
|
+
real_client = httpdex.Client()
|
|
47
|
+
_patch_client_backend(real_client)
|
|
48
|
+
MockClient.return_value.__enter__ = lambda self: real_client
|
|
49
|
+
MockClient.return_value.__exit__ = lambda self, *args: real_client.close()
|
|
50
|
+
for func in [httpdex.put, httpdex.patch, httpdex.delete, httpdex.head, httpdex.options]:
|
|
51
|
+
response = func("http://example.com/")
|
|
52
|
+
# Client is reused, connection gets reused or recreated.
|
|
53
|
+
assert response is not None
|
httpdex/_auth.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from httpdex._models import Request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BasicAuth:
|
|
11
|
+
"""HTTP Basic authentication."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, username: str | bytes, password: str | bytes) -> None:
|
|
14
|
+
if isinstance(username, str):
|
|
15
|
+
username = username.encode("latin-1")
|
|
16
|
+
if isinstance(password, str):
|
|
17
|
+
password = password.encode("latin-1")
|
|
18
|
+
self._username = username
|
|
19
|
+
self._password = password
|
|
20
|
+
|
|
21
|
+
def __call__(self, request: Request) -> Request:
|
|
22
|
+
credentials = base64.b64encode(self._username + b":" + self._password).decode("ascii")
|
|
23
|
+
request.headers._raw.append((b"authorization", f"Basic {credentials}".encode("latin-1")))
|
|
24
|
+
return request
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DigestAuth:
|
|
28
|
+
"""HTTP Digest authentication (stub)."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, username: str | bytes, password: str | bytes) -> None:
|
|
31
|
+
if isinstance(username, str):
|
|
32
|
+
username = username.encode("latin-1")
|
|
33
|
+
if isinstance(password, str):
|
|
34
|
+
password = password.encode("latin-1")
|
|
35
|
+
self._username = username
|
|
36
|
+
self._password = password
|
|
37
|
+
|
|
38
|
+
def __call__(self, request: Request) -> Request:
|
|
39
|
+
# TODO: implement digest auth challenge-response
|
|
40
|
+
raise NotImplementedError("DigestAuth is not yet implemented")
|
httpdex/_auth_test.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from httpdex._auth import BasicAuth, DigestAuth
|
|
4
|
+
from httpdex._models import Request
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_basic_auth_str_credentials() -> None:
|
|
8
|
+
auth = BasicAuth(username="user", password="pass")
|
|
9
|
+
req = Request("GET", "https://example.com/")
|
|
10
|
+
req = auth(req)
|
|
11
|
+
assert "authorization" in req.headers
|
|
12
|
+
assert req.headers["authorization"].startswith("Basic ")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_basic_auth_bytes_credentials() -> None:
|
|
16
|
+
auth = BasicAuth(username=b"user", password=b"pass")
|
|
17
|
+
req = Request("GET", "https://example.com/")
|
|
18
|
+
req = auth(req)
|
|
19
|
+
assert "authorization" in req.headers
|
|
20
|
+
assert req.headers["authorization"].startswith("Basic ")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_basic_auth_mixed_credentials() -> None:
|
|
24
|
+
auth = BasicAuth(username="user", password=b"pass")
|
|
25
|
+
req = Request("GET", "https://example.com/")
|
|
26
|
+
req = auth(req)
|
|
27
|
+
assert "authorization" in req.headers
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_digest_auth_init_str() -> None:
|
|
31
|
+
auth = DigestAuth(username="user", password="pass")
|
|
32
|
+
assert auth._username == b"user"
|
|
33
|
+
assert auth._password == b"pass"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_digest_auth_init_bytes() -> None:
|
|
37
|
+
auth = DigestAuth(username=b"user", password=b"pass")
|
|
38
|
+
assert auth._username == b"user"
|
|
39
|
+
assert auth._password == b"pass"
|