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