punkreq 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.
Files changed (41) hide show
  1. punkreq-0.1.0/.gitignore +13 -0
  2. punkreq-0.1.0/LICENSE +28 -0
  3. punkreq-0.1.0/PKG-INFO +145 -0
  4. punkreq-0.1.0/README.md +113 -0
  5. punkreq-0.1.0/punkreq/__init__.py +84 -0
  6. punkreq-0.1.0/punkreq/_auth.py +43 -0
  7. punkreq-0.1.0/punkreq/_client.py +591 -0
  8. punkreq-0.1.0/punkreq/_config.py +174 -0
  9. punkreq-0.1.0/punkreq/_connect.py +131 -0
  10. punkreq-0.1.0/punkreq/_content.py +180 -0
  11. punkreq-0.1.0/punkreq/_cookies.py +189 -0
  12. punkreq-0.1.0/punkreq/_decoders.py +289 -0
  13. punkreq-0.1.0/punkreq/_exceptions.py +177 -0
  14. punkreq-0.1.0/punkreq/_headers.py +118 -0
  15. punkreq-0.1.0/punkreq/_models.py +404 -0
  16. punkreq-0.1.0/punkreq/_multipart.py +160 -0
  17. punkreq-0.1.0/punkreq/_pool.py +295 -0
  18. punkreq-0.1.0/punkreq/_proxies.py +240 -0
  19. punkreq-0.1.0/punkreq/_redirects.py +70 -0
  20. punkreq-0.1.0/punkreq/_transport.py +249 -0
  21. punkreq-0.1.0/punkreq/_urls.py +401 -0
  22. punkreq-0.1.0/punkreq/_version.py +1 -0
  23. punkreq-0.1.0/punkreq/asyncio.py +13 -0
  24. punkreq-0.1.0/punkreq/py.typed +0 -0
  25. punkreq-0.1.0/punkreq/tonio.py +13 -0
  26. punkreq-0.1.0/pyproject.toml +116 -0
  27. punkreq-0.1.0/tests/test_client.py +653 -0
  28. punkreq-0.1.0/tests/test_config.py +88 -0
  29. punkreq-0.1.0/tests/test_content.py +113 -0
  30. punkreq-0.1.0/tests/test_cookies.py +78 -0
  31. punkreq-0.1.0/tests/test_decoders.py +127 -0
  32. punkreq-0.1.0/tests/test_e2e_client.py +122 -0
  33. punkreq-0.1.0/tests/test_e2e_transport.py +169 -0
  34. punkreq-0.1.0/tests/test_exceptions.py +74 -0
  35. punkreq-0.1.0/tests/test_headers.py +127 -0
  36. punkreq-0.1.0/tests/test_models.py +243 -0
  37. punkreq-0.1.0/tests/test_multipart.py +95 -0
  38. punkreq-0.1.0/tests/test_pool.py +283 -0
  39. punkreq-0.1.0/tests/test_proxies.py +262 -0
  40. punkreq-0.1.0/tests/test_redirects.py +126 -0
  41. punkreq-0.1.0/tests/test_urls.py +221 -0
@@ -0,0 +1,13 @@
1
+ *.DS_Store
2
+ __pycache__
3
+
4
+ *.sublime-*
5
+ .venv
6
+ .vscode
7
+
8
+ .pytest_cache
9
+ .ruff_cache
10
+
11
+ build/*
12
+ dist/*
13
+ uv.lock
punkreq-0.1.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ Copyright 2026 Giovanni Barillari
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are
5
+ met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of the copyright holder nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
21
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
24
+ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
punkreq-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: punkreq
3
+ Version: 0.1.0
4
+ Summary: An async HTTP client built on httpunk
5
+ Project-URL: Homepage, https://github.com/gi0baro/punkreq
6
+ Project-URL: Funding, https://github.com/sponsors/gi0baro
7
+ Project-URL: Source, https://github.com/gi0baro/punkreq
8
+ Project-URL: Issues, https://github.com/gi0baro/punkreq/issues
9
+ Author-email: Giovanni Barillari <g@baro.dev>
10
+ License-Expression: BSD-3-Clause
11
+ License-File: LICENSE
12
+ Keywords: async,client,http,httpunk
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: BSD License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: httpunk~=0.1.0
27
+ Provides-Extra: brotli
28
+ Requires-Dist: brotli~=1.2.0; extra == 'brotli'
29
+ Provides-Extra: tonio
30
+ Requires-Dist: tonio~=0.8.1; (python_version >= '3.14') and extra == 'tonio'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # punkreq
34
+
35
+ punkreq is an async HTTP client for Python, built on top of
36
+ [httpunk](https://github.com/gi0baro/httpunk).
37
+
38
+ > **Note:** punkreq is currently in alpha stage.
39
+
40
+ > **Note:** punkreq was built with substantial help from LLMs, under human supervision.
41
+
42
+ ## In a nutshell
43
+
44
+ - Async only, with multiple runtime backends: import the client from the
45
+ module matching your runtime – `punkreq.asyncio` or `punkreq.tonio`
46
+ - HTTP/1.1 and HTTP/2, negotiated via TLS ALPN by default; HTTP/2 prior
47
+ knowledge with `http1=False`
48
+ - Connection pooling with keep-alive (h1) and multiplexing (h2)
49
+ - Transparent response decompression: gzip and deflate everywhere, zstd on
50
+ Python 3.14+, brotli with the `punkreq[brotli]` extra
51
+ - Conservative automatic retries — only requests the server provably never
52
+ processed (HTTP/2 graceful GOAWAY, refused streams, keep-alive races)
53
+ - Opt-in cookie jar, basic/bearer auth, multipart uploads
54
+ - HTTP proxies: `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` environment support,
55
+ CONNECT tunneling for https destinations.
56
+ - Zero runtime dependencies beyond httpunk
57
+
58
+ ## Installation
59
+
60
+ ```shell
61
+ pip install punkreq
62
+ ```
63
+
64
+ ## Quickstart
65
+
66
+ ```python
67
+ import asyncio
68
+ from punkreq.asyncio import Client
69
+
70
+ async def main():
71
+ async with Client(base_url="https://api.example.com") as client:
72
+ response = await client.post("/items", json={"name": "widget"})
73
+ response.raise_for_status()
74
+ print(await response.json())
75
+
76
+ asyncio.run(main())
77
+ ```
78
+
79
+ One-off requests without managing a client:
80
+
81
+ ```python
82
+ from punkreq import asyncio as punkreq
83
+
84
+ response = await punkreq.get("https://www.example.com")
85
+ print(response.status_code, response.headers["content-type"])
86
+ ```
87
+
88
+ Responses resolve as soon as the head arrives; the body is read on demand —
89
+ `await response.read()` / `text()` / `json()`, or streamed:
90
+
91
+ ```python
92
+ async with Client() as client:
93
+ response = await client.get("https://example.com/large.bin")
94
+ async for chunk in response.iter_bytes(chunk_size=65536):
95
+ ...
96
+ ```
97
+
98
+ Reading to the end releases the connection automatically. When you might stop
99
+ early, use the request as a context manager so the response is always closed:
100
+
101
+ ```python
102
+ async with client.get("https://example.com/huge.ndjson") as response:
103
+ async for line in response.iter_lines():
104
+ if found(line):
105
+ break # the block exit closes the response and frees the connection
106
+ ```
107
+
108
+ ## Configuration
109
+
110
+ ```python
111
+ import punkreq
112
+ from punkreq.asyncio import Client
113
+
114
+ client = Client(
115
+ base_url="https://api.example.com",
116
+ headers={"x-api-key": "..."},
117
+ auth=("user", "pass"), # or punkreq.BearerAuth("token")
118
+ cookies={}, # enables the cookie jar (off by default)
119
+ timeout=punkreq.Timeout(5.0, total=30.0), # no timeouts by default
120
+ limits=punkreq.Limits(max_connections=100),
121
+ proxy="http://proxy.internal:3128", # env vars honored by default
122
+ verify=True, # bool | CA bundle path | ssl.SSLContext
123
+ )
124
+ ```
125
+
126
+ By default: redirects on (max 10), no timeouts, no cookie jar unless requested,
127
+ HTTP/2 enabled, 90s keep-alive expiry, unlimited connections.
128
+
129
+ `Timeout` has four independent fields — `total` (a deadline for the whole
130
+ request, redirects included), `connect`, `read` and `pool`. There is no write
131
+ timeout: request bodies are written full-duplex from background tasks, so
132
+ `total` is the bound for slow uploads.
133
+
134
+ ## Backends
135
+
136
+ punkreq never asks for a backend at call sites — pick it by import:
137
+
138
+ ```python
139
+ from punkreq.asyncio import Client # asyncio, available everywhere
140
+ from punkreq.tonio import Client # tonio, free-threaded CPython >= 3.14 on Unix
141
+ ```
142
+
143
+ ## License
144
+
145
+ punkreq is released under the BSD License.
@@ -0,0 +1,113 @@
1
+ # punkreq
2
+
3
+ punkreq is an async HTTP client for Python, built on top of
4
+ [httpunk](https://github.com/gi0baro/httpunk).
5
+
6
+ > **Note:** punkreq is currently in alpha stage.
7
+
8
+ > **Note:** punkreq was built with substantial help from LLMs, under human supervision.
9
+
10
+ ## In a nutshell
11
+
12
+ - Async only, with multiple runtime backends: import the client from the
13
+ module matching your runtime – `punkreq.asyncio` or `punkreq.tonio`
14
+ - HTTP/1.1 and HTTP/2, negotiated via TLS ALPN by default; HTTP/2 prior
15
+ knowledge with `http1=False`
16
+ - Connection pooling with keep-alive (h1) and multiplexing (h2)
17
+ - Transparent response decompression: gzip and deflate everywhere, zstd on
18
+ Python 3.14+, brotli with the `punkreq[brotli]` extra
19
+ - Conservative automatic retries — only requests the server provably never
20
+ processed (HTTP/2 graceful GOAWAY, refused streams, keep-alive races)
21
+ - Opt-in cookie jar, basic/bearer auth, multipart uploads
22
+ - HTTP proxies: `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` environment support,
23
+ CONNECT tunneling for https destinations.
24
+ - Zero runtime dependencies beyond httpunk
25
+
26
+ ## Installation
27
+
28
+ ```shell
29
+ pip install punkreq
30
+ ```
31
+
32
+ ## Quickstart
33
+
34
+ ```python
35
+ import asyncio
36
+ from punkreq.asyncio import Client
37
+
38
+ async def main():
39
+ async with Client(base_url="https://api.example.com") as client:
40
+ response = await client.post("/items", json={"name": "widget"})
41
+ response.raise_for_status()
42
+ print(await response.json())
43
+
44
+ asyncio.run(main())
45
+ ```
46
+
47
+ One-off requests without managing a client:
48
+
49
+ ```python
50
+ from punkreq import asyncio as punkreq
51
+
52
+ response = await punkreq.get("https://www.example.com")
53
+ print(response.status_code, response.headers["content-type"])
54
+ ```
55
+
56
+ Responses resolve as soon as the head arrives; the body is read on demand —
57
+ `await response.read()` / `text()` / `json()`, or streamed:
58
+
59
+ ```python
60
+ async with Client() as client:
61
+ response = await client.get("https://example.com/large.bin")
62
+ async for chunk in response.iter_bytes(chunk_size=65536):
63
+ ...
64
+ ```
65
+
66
+ Reading to the end releases the connection automatically. When you might stop
67
+ early, use the request as a context manager so the response is always closed:
68
+
69
+ ```python
70
+ async with client.get("https://example.com/huge.ndjson") as response:
71
+ async for line in response.iter_lines():
72
+ if found(line):
73
+ break # the block exit closes the response and frees the connection
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ ```python
79
+ import punkreq
80
+ from punkreq.asyncio import Client
81
+
82
+ client = Client(
83
+ base_url="https://api.example.com",
84
+ headers={"x-api-key": "..."},
85
+ auth=("user", "pass"), # or punkreq.BearerAuth("token")
86
+ cookies={}, # enables the cookie jar (off by default)
87
+ timeout=punkreq.Timeout(5.0, total=30.0), # no timeouts by default
88
+ limits=punkreq.Limits(max_connections=100),
89
+ proxy="http://proxy.internal:3128", # env vars honored by default
90
+ verify=True, # bool | CA bundle path | ssl.SSLContext
91
+ )
92
+ ```
93
+
94
+ By default: redirects on (max 10), no timeouts, no cookie jar unless requested,
95
+ HTTP/2 enabled, 90s keep-alive expiry, unlimited connections.
96
+
97
+ `Timeout` has four independent fields — `total` (a deadline for the whole
98
+ request, redirects included), `connect`, `read` and `pool`. There is no write
99
+ timeout: request bodies are written full-duplex from background tasks, so
100
+ `total` is the bound for slow uploads.
101
+
102
+ ## Backends
103
+
104
+ punkreq never asks for a backend at call sites — pick it by import:
105
+
106
+ ```python
107
+ from punkreq.asyncio import Client # asyncio, available everywhere
108
+ from punkreq.tonio import Client # tonio, free-threaded CPython >= 3.14 on Unix
109
+ ```
110
+
111
+ ## License
112
+
113
+ punkreq is released under the BSD License.
@@ -0,0 +1,84 @@
1
+ from ._auth import Auth, BasicAuth, BearerAuth
2
+ from ._client import USE_CLIENT_DEFAULT, ResponseHandle, UseClientDefault
3
+ from ._config import Limits, Proxy, Timeout
4
+ from ._content import AsyncByteStream, ByteStream
5
+ from ._cookies import Cookies
6
+ from ._exceptions import (
7
+ CloseError,
8
+ ConnectError,
9
+ ConnectTimeout,
10
+ CookieConflict,
11
+ DecodingError,
12
+ HTTPError,
13
+ HTTPStatusError,
14
+ InvalidURL,
15
+ LocalProtocolError,
16
+ NetworkError,
17
+ PoolTimeout,
18
+ ProtocolError,
19
+ ProxyError,
20
+ ReadError,
21
+ ReadTimeout,
22
+ RemoteProtocolError,
23
+ RequestError,
24
+ RequestNotRead,
25
+ StreamClosed,
26
+ StreamConsumed,
27
+ StreamError,
28
+ TimeoutException,
29
+ TooManyRedirects,
30
+ TransportError,
31
+ UnsupportedProtocol,
32
+ WriteError,
33
+ )
34
+ from ._headers import Headers
35
+ from ._models import Request, Response
36
+ from ._urls import URL, QueryParams
37
+ from ._version import __version__ as __version__
38
+
39
+
40
+ __all__ = [
41
+ "URL",
42
+ "USE_CLIENT_DEFAULT",
43
+ "AsyncByteStream",
44
+ "Auth",
45
+ "BasicAuth",
46
+ "BearerAuth",
47
+ "ByteStream",
48
+ "CloseError",
49
+ "ConnectError",
50
+ "ConnectTimeout",
51
+ "CookieConflict",
52
+ "Cookies",
53
+ "DecodingError",
54
+ "HTTPError",
55
+ "HTTPStatusError",
56
+ "Headers",
57
+ "InvalidURL",
58
+ "Limits",
59
+ "LocalProtocolError",
60
+ "NetworkError",
61
+ "PoolTimeout",
62
+ "ProtocolError",
63
+ "Proxy",
64
+ "ProxyError",
65
+ "QueryParams",
66
+ "ReadError",
67
+ "ReadTimeout",
68
+ "RemoteProtocolError",
69
+ "Request",
70
+ "RequestError",
71
+ "RequestNotRead",
72
+ "Response",
73
+ "ResponseHandle",
74
+ "StreamClosed",
75
+ "StreamConsumed",
76
+ "StreamError",
77
+ "Timeout",
78
+ "TimeoutException",
79
+ "TooManyRedirects",
80
+ "TransportError",
81
+ "UnsupportedProtocol",
82
+ "UseClientDefault",
83
+ "WriteError",
84
+ ]
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import typing
5
+
6
+ from ._models import Request
7
+
8
+
9
+ __all__ = ["Auth", "BasicAuth", "BearerAuth"]
10
+
11
+ AuthTypes = typing.Union["Auth", typing.Tuple[str, str], None]
12
+
13
+
14
+ class Auth:
15
+ """Base class: override `apply` to mutate the outgoing request."""
16
+
17
+ def apply(self, request: Request) -> None:
18
+ raise NotImplementedError()
19
+
20
+
21
+ class BasicAuth(Auth):
22
+ def __init__(self, username: str, password: str = "") -> None:
23
+ token = base64.b64encode(f"{username}:{password}".encode()).decode("ascii")
24
+ self._header = f"Basic {token}"
25
+
26
+ def apply(self, request: Request) -> None:
27
+ request.headers["authorization"] = self._header
28
+
29
+
30
+ class BearerAuth(Auth):
31
+ def __init__(self, token: str) -> None:
32
+ self._header = f"Bearer {token}"
33
+
34
+ def apply(self, request: Request) -> None:
35
+ request.headers["authorization"] = self._header
36
+
37
+
38
+ def coerce_auth(auth: AuthTypes) -> Auth | None:
39
+ if auth is None or isinstance(auth, Auth):
40
+ return auth
41
+ if isinstance(auth, tuple) and len(auth) == 2:
42
+ return BasicAuth(auth[0], auth[1])
43
+ raise TypeError(f"Invalid 'auth' argument: {auth!r}")