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.
@@ -0,0 +1,6 @@
1
+ .coverage
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ target/
6
+ .venv/
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
@@ -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"