httpdex 0.1.0__tar.gz
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-0.1.0/.gitignore +6 -0
- httpdex-0.1.0/PKG-INFO +60 -0
- httpdex-0.1.0/README.md +47 -0
- httpdex-0.1.0/demo.py +56 -0
- httpdex-0.1.0/httpdex/__init__.py +85 -0
- httpdex-0.1.0/httpdex/_api.py +65 -0
- httpdex-0.1.0/httpdex/_api_test.py +53 -0
- httpdex-0.1.0/httpdex/_auth.py +40 -0
- httpdex-0.1.0/httpdex/_auth_test.py +39 -0
- httpdex-0.1.0/httpdex/_client.py +1042 -0
- httpdex-0.1.0/httpdex/_client_test.py +1926 -0
- httpdex-0.1.0/httpdex/_compat_test.py +729 -0
- httpdex-0.1.0/httpdex/_cookies.py +5 -0
- httpdex-0.1.0/httpdex/_decoders.py +147 -0
- httpdex-0.1.0/httpdex/_decoders_test.py +189 -0
- httpdex-0.1.0/httpdex/_exceptions.py +116 -0
- httpdex-0.1.0/httpdex/_exceptions_test.py +129 -0
- httpdex-0.1.0/httpdex/_headers.py +76 -0
- httpdex-0.1.0/httpdex/_headers_test.py +86 -0
- httpdex-0.1.0/httpdex/_models.py +371 -0
- httpdex-0.1.0/httpdex/_models_test.py +559 -0
- httpdex-0.1.0/httpdex/_multipart.py +78 -0
- httpdex-0.1.0/httpdex/_multipart_test.py +143 -0
- httpdex-0.1.0/httpdex/_status_codes.py +68 -0
- httpdex-0.1.0/httpdex/_timeout.py +33 -0
- httpdex-0.1.0/httpdex/_timeout_test.py +44 -0
- httpdex-0.1.0/httpdex/_transports.py +101 -0
- httpdex-0.1.0/httpdex/_transports_test.py +211 -0
- httpdex-0.1.0/httpdex/_types.py +60 -0
- httpdex-0.1.0/httpdex/_url.py +217 -0
- httpdex-0.1.0/httpdex/_url_test.py +269 -0
- httpdex-0.1.0/httpdex/py.typed +0 -0
- httpdex-0.1.0/pyproject.toml +76 -0
httpdex-0.1.0/.gitignore
ADDED
httpdex-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: httpdex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A modern Python HTTP client. Drop-in replacement for httpx.
|
|
5
|
+
Author: Marcelo Trylesinski
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: anyio>=4.0
|
|
9
|
+
Requires-Dist: certifi
|
|
10
|
+
Requires-Dist: httpdex-cookies>=0.1.0
|
|
11
|
+
Requires-Dist: httpdex-core>=0.1.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# httpdex
|
|
15
|
+
|
|
16
|
+
Drop-in replacement for `httpx` with HTTP/1.1, HTTP/2, and HTTP/3 support.
|
|
17
|
+
|
|
18
|
+
## Highlights
|
|
19
|
+
|
|
20
|
+
- `httpx`-compatible API
|
|
21
|
+
- Async and sync clients
|
|
22
|
+
- HTTP/2 via `http2=True`, HTTP/3 via `http3=True`
|
|
23
|
+
- Request helpers: `httpdex.get()`, `httpdex.post()`, etc.
|
|
24
|
+
- Cookie storage via `CookieStore`
|
|
25
|
+
- ASGI-native testing with `ASGITransport` and `MockTransport`
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import httpdex
|
|
31
|
+
|
|
32
|
+
# Simple request
|
|
33
|
+
response = httpdex.get("https://example.com")
|
|
34
|
+
|
|
35
|
+
# Client with connection reuse
|
|
36
|
+
with httpdex.Client() as client:
|
|
37
|
+
response = client.get("https://example.com")
|
|
38
|
+
|
|
39
|
+
# HTTP/2
|
|
40
|
+
with httpdex.Client(http2=True) as client:
|
|
41
|
+
response = client.get("https://example.com")
|
|
42
|
+
|
|
43
|
+
# Async
|
|
44
|
+
async with httpdex.AsyncClient() as client:
|
|
45
|
+
response = await client.get("https://example.com")
|
|
46
|
+
|
|
47
|
+
# Streaming
|
|
48
|
+
with httpdex.Client() as client:
|
|
49
|
+
with client.stream("GET", "https://example.com/large") as response:
|
|
50
|
+
for chunk in response.iter_bytes():
|
|
51
|
+
process(chunk)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Related Packages
|
|
55
|
+
|
|
56
|
+
- `httpdex-core` - connection pool and transport layer
|
|
57
|
+
- `httpdex-parse` - sans-I/O HTTP/1.1 parsing
|
|
58
|
+
- `httpdex-h2` - HTTP/2 framing and HPACK
|
|
59
|
+
- `httpdex-h3` - HTTP/3 over QUIC
|
|
60
|
+
- `httpdex-cookies` - RFC 6265 cookie parsing and storage
|
httpdex-0.1.0/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# httpdex
|
|
2
|
+
|
|
3
|
+
Drop-in replacement for `httpx` with HTTP/1.1, HTTP/2, and HTTP/3 support.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
|
|
7
|
+
- `httpx`-compatible API
|
|
8
|
+
- Async and sync clients
|
|
9
|
+
- HTTP/2 via `http2=True`, HTTP/3 via `http3=True`
|
|
10
|
+
- Request helpers: `httpdex.get()`, `httpdex.post()`, etc.
|
|
11
|
+
- Cookie storage via `CookieStore`
|
|
12
|
+
- ASGI-native testing with `ASGITransport` and `MockTransport`
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import httpdex
|
|
18
|
+
|
|
19
|
+
# Simple request
|
|
20
|
+
response = httpdex.get("https://example.com")
|
|
21
|
+
|
|
22
|
+
# Client with connection reuse
|
|
23
|
+
with httpdex.Client() as client:
|
|
24
|
+
response = client.get("https://example.com")
|
|
25
|
+
|
|
26
|
+
# HTTP/2
|
|
27
|
+
with httpdex.Client(http2=True) as client:
|
|
28
|
+
response = client.get("https://example.com")
|
|
29
|
+
|
|
30
|
+
# Async
|
|
31
|
+
async with httpdex.AsyncClient() as client:
|
|
32
|
+
response = await client.get("https://example.com")
|
|
33
|
+
|
|
34
|
+
# Streaming
|
|
35
|
+
with httpdex.Client() as client:
|
|
36
|
+
with client.stream("GET", "https://example.com/large") as response:
|
|
37
|
+
for chunk in response.iter_bytes():
|
|
38
|
+
process(chunk)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Related Packages
|
|
42
|
+
|
|
43
|
+
- `httpdex-core` - connection pool and transport layer
|
|
44
|
+
- `httpdex-parse` - sans-I/O HTTP/1.1 parsing
|
|
45
|
+
- `httpdex-h2` - HTTP/2 framing and HPACK
|
|
46
|
+
- `httpdex-h3` - HTTP/3 over QUIC
|
|
47
|
+
- `httpdex-cookies` - RFC 6265 cookie parsing and storage
|
httpdex-0.1.0/demo.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Demo: httpdex HTTP client - drop-in httpx replacement.
|
|
2
|
+
|
|
3
|
+
Run with: python demo.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import httpdex
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def demo_sync() -> None:
|
|
12
|
+
print("=== httpdex Sync Client Demo ===\n")
|
|
13
|
+
|
|
14
|
+
# Simple GET request (like httpx.get)
|
|
15
|
+
response = httpdex.get("https://httpbin.org/get")
|
|
16
|
+
print(f"GET /get -> {response.status_code}")
|
|
17
|
+
print(f" Content-Type: {response.headers['content-type']}")
|
|
18
|
+
print(f" Body length: {len(response.content)} bytes")
|
|
19
|
+
print(f" Elapsed: {response.elapsed.total_seconds():.3f}s")
|
|
20
|
+
print()
|
|
21
|
+
|
|
22
|
+
# POST with JSON
|
|
23
|
+
response = httpdex.post("https://httpbin.org/post", json={"hello": "world"})
|
|
24
|
+
print(f"POST /post -> {response.status_code}")
|
|
25
|
+
data = response.json()
|
|
26
|
+
print(f" Echoed JSON: {data['json']}")
|
|
27
|
+
print()
|
|
28
|
+
|
|
29
|
+
# Using Client for connection reuse
|
|
30
|
+
with httpdex.Client() as client:
|
|
31
|
+
for path in ["/get", "/headers", "/user-agent"]:
|
|
32
|
+
response = client.get(f"https://httpbin.org{path}")
|
|
33
|
+
print(f"GET {path} -> {response.status_code}")
|
|
34
|
+
print()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def demo_async() -> None:
|
|
38
|
+
print("=== httpdex Async Client Demo ===\n")
|
|
39
|
+
|
|
40
|
+
async with httpdex.AsyncClient() as client:
|
|
41
|
+
response = await client.get("https://httpbin.org/get")
|
|
42
|
+
print(f"GET /get -> {response.status_code}")
|
|
43
|
+
|
|
44
|
+
response = await client.post("https://httpbin.org/post", json={"async": True})
|
|
45
|
+
print(f"POST /post -> {response.status_code}")
|
|
46
|
+
data = response.json()
|
|
47
|
+
print(f" Echoed JSON: {data['json']}")
|
|
48
|
+
print()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
demo_sync()
|
|
53
|
+
|
|
54
|
+
import anyio
|
|
55
|
+
|
|
56
|
+
anyio.run(demo_async)
|
|
@@ -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
|
+
]
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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")
|
|
@@ -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"
|