syshttp 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 (101) hide show
  1. syshttp-0.1.0/.gitignore +38 -0
  2. syshttp-0.1.0/CHANGELOG.md +343 -0
  3. syshttp-0.1.0/LICENSE +21 -0
  4. syshttp-0.1.0/PKG-INFO +219 -0
  5. syshttp-0.1.0/README.md +177 -0
  6. syshttp-0.1.0/docs/api/autopatch.md +3 -0
  7. syshttp-0.1.0/docs/api/client.md +15 -0
  8. syshttp-0.1.0/docs/api/cookies.md +3 -0
  9. syshttp-0.1.0/docs/api/env.md +3 -0
  10. syshttp-0.1.0/docs/api/exceptions.md +3 -0
  11. syshttp-0.1.0/docs/api/hooks.md +3 -0
  12. syshttp-0.1.0/docs/api/jar.md +3 -0
  13. syshttp-0.1.0/docs/api/proxy.md +3 -0
  14. syshttp-0.1.0/docs/api/tls_retry.md +5 -0
  15. syshttp-0.1.0/docs/changelog.md +1 -0
  16. syshttp-0.1.0/docs/index.md +22 -0
  17. syshttp-0.1.0/docs/settings.md +119 -0
  18. syshttp-0.1.0/mkdocs.yml +59 -0
  19. syshttp-0.1.0/pyproject.toml +85 -0
  20. syshttp-0.1.0/src/syshttp/AGENTS.md +817 -0
  21. syshttp-0.1.0/src/syshttp/__init__.py +71 -0
  22. syshttp-0.1.0/src/syshttp/_fileio.py +78 -0
  23. syshttp-0.1.0/src/syshttp/_parse.py +30 -0
  24. syshttp-0.1.0/src/syshttp/auth.py +17 -0
  25. syshttp-0.1.0/src/syshttp/autopatch.py +382 -0
  26. syshttp-0.1.0/src/syshttp/client/__init__.py +70 -0
  27. syshttp-0.1.0/src/syshttp/client/_body.py +156 -0
  28. syshttp-0.1.0/src/syshttp/client/_client.py +413 -0
  29. syshttp-0.1.0/src/syshttp/client/_context.py +107 -0
  30. syshttp-0.1.0/src/syshttp/client/_deadline.py +94 -0
  31. syshttp-0.1.0/src/syshttp/client/_default.py +72 -0
  32. syshttp-0.1.0/src/syshttp/client/_fetch.py +98 -0
  33. syshttp-0.1.0/src/syshttp/client/_follow.py +243 -0
  34. syshttp-0.1.0/src/syshttp/client/_headers.py +238 -0
  35. syshttp-0.1.0/src/syshttp/client/_pool.py +170 -0
  36. syshttp-0.1.0/src/syshttp/client/_prepare.py +77 -0
  37. syshttp-0.1.0/src/syshttp/client/_response.py +301 -0
  38. syshttp-0.1.0/src/syshttp/client/_route.py +75 -0
  39. syshttp-0.1.0/src/syshttp/client/_settings.py +12 -0
  40. syshttp-0.1.0/src/syshttp/client/_state.py +322 -0
  41. syshttp-0.1.0/src/syshttp/client/_status.py +59 -0
  42. syshttp-0.1.0/src/syshttp/client/_tlstunnel.py +114 -0
  43. syshttp-0.1.0/src/syshttp/client/_transport.py +550 -0
  44. syshttp-0.1.0/src/syshttp/cookies.py +391 -0
  45. syshttp-0.1.0/src/syshttp/env.py +144 -0
  46. syshttp-0.1.0/src/syshttp/exceptions.py +29 -0
  47. syshttp-0.1.0/src/syshttp/hooks.py +314 -0
  48. syshttp-0.1.0/src/syshttp/jar.py +133 -0
  49. syshttp-0.1.0/src/syshttp/proxy.py +341 -0
  50. syshttp-0.1.0/src/syshttp/py.typed +0 -0
  51. syshttp-0.1.0/src/syshttp/retry.py +34 -0
  52. syshttp-0.1.0/src/syshttp/tls.py +60 -0
  53. syshttp-0.1.0/tests/client_fakes.py +129 -0
  54. syshttp-0.1.0/tests/client_wire.py +411 -0
  55. syshttp-0.1.0/tests/conftest.py +43 -0
  56. syshttp-0.1.0/tests/data/ca.pem +11 -0
  57. syshttp-0.1.0/tests/data/client-rsa.key +28 -0
  58. syshttp-0.1.0/tests/data/client-rsa.pem +16 -0
  59. syshttp-0.1.0/tests/data/server.key +28 -0
  60. syshttp-0.1.0/tests/data/server.pem +16 -0
  61. syshttp-0.1.0/tests/test_autopatch.py +389 -0
  62. syshttp-0.1.0/tests/test_client_credential_redirects.py +74 -0
  63. syshttp-0.1.0/tests/test_client_deadline.py +182 -0
  64. syshttp-0.1.0/tests/test_client_default.py +244 -0
  65. syshttp-0.1.0/tests/test_client_default_transport.py +165 -0
  66. syshttp-0.1.0/tests/test_client_errors.py +64 -0
  67. syshttp-0.1.0/tests/test_client_guards.py +106 -0
  68. syshttp-0.1.0/tests/test_client_paths.py +151 -0
  69. syshttp-0.1.0/tests/test_client_peer_text.py +61 -0
  70. syshttp-0.1.0/tests/test_client_redirect_origin.py +122 -0
  71. syshttp-0.1.0/tests/test_client_redirects.py +217 -0
  72. syshttp-0.1.0/tests/test_client_reply_bounds.py +73 -0
  73. syshttp-0.1.0/tests/test_client_reply_limits.py +189 -0
  74. syshttp-0.1.0/tests/test_client_response.py +68 -0
  75. syshttp-0.1.0/tests/test_client_routing.py +421 -0
  76. syshttp-0.1.0/tests/test_client_secure_cookies.py +116 -0
  77. syshttp-0.1.0/tests/test_client_state.py +372 -0
  78. syshttp-0.1.0/tests/test_client_tls.py +264 -0
  79. syshttp-0.1.0/tests/test_client_urls.py +64 -0
  80. syshttp-0.1.0/tests/test_client_write_redirects.py +229 -0
  81. syshttp-0.1.0/tests/test_cookie_hosts.py +114 -0
  82. syshttp-0.1.0/tests/test_cookie_injection.py +162 -0
  83. syshttp-0.1.0/tests/test_cookies.py +363 -0
  84. syshttp-0.1.0/tests/test_edges.py +69 -0
  85. syshttp-0.1.0/tests/test_env.py +188 -0
  86. syshttp-0.1.0/tests/test_exceptions.py +113 -0
  87. syshttp-0.1.0/tests/test_fetch.py +274 -0
  88. syshttp-0.1.0/tests/test_jar_files.py +137 -0
  89. syshttp-0.1.0/tests/test_jars_proxy.py +247 -0
  90. syshttp-0.1.0/tests/test_os_context.py +93 -0
  91. syshttp-0.1.0/tests/test_proxy.py +106 -0
  92. syshttp-0.1.0/tests/test_proxy_auth.py +409 -0
  93. syshttp-0.1.0/tests/test_retry.py +50 -0
  94. syshttp-0.1.0/tests/test_switches.py +153 -0
  95. syshttp-0.1.0/tests/test_system_proxy.py +234 -0
  96. syshttp-0.1.0/tests/test_transport_body.py +294 -0
  97. syshttp-0.1.0/tests/test_transport_pool.py +411 -0
  98. syshttp-0.1.0/tests/test_transport_prepare.py +217 -0
  99. syshttp-0.1.0/tests/test_transport_send.py +630 -0
  100. syshttp-0.1.0/tests/test_transport_tls.py +127 -0
  101. syshttp-0.1.0/tests/test_url_hooks.py +258 -0
@@ -0,0 +1,38 @@
1
+ # Category rule: nothing dot-prefixed at the repo root gets tracked except the
2
+ # git config files and CI re-included below (a category beats a list).
3
+ /.*
4
+ !/.gitignore
5
+ !/.gitattributes
6
+ !/.github/
7
+
8
+ # Private agent config. No trailing slash on `.agents`: it is usually a
9
+ # symlink, which a directory-only pattern would not match. `*.local.*` is the
10
+ # unshared override and is never committed.
11
+ .agents
12
+ *.local.*
13
+ CLAUDE*
14
+ .claude
15
+
16
+ # Build/dist output and generated docs site
17
+ build/
18
+ dist/
19
+ site/
20
+
21
+ # Python
22
+ __pycache__/
23
+ *.py[cod]
24
+ .pytest_cache/
25
+ .hypothesis/
26
+ .mypy_cache/
27
+ .ruff_cache/
28
+ .coverage*
29
+ htmlcov/
30
+ *.egg-info/
31
+ .pyvenv/
32
+ .venv/
33
+
34
+ # OS/editor
35
+ .DS_Store
36
+ Thumbs.db
37
+ .idea/
38
+ .vscode/
@@ -0,0 +1,343 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-10-20
11
+
12
+ ### Added
13
+
14
+ - `syshttp.client`: an HTTP client on the standard library, so that no feature
15
+ of syshttp needs `requests` (which only `syshttp.autopatch` patches, in a
16
+ program that imports it). `HTTPClient(transport=None, *, headers, timeout,
17
+ deadline, user_agent, env, proxymap, ssl_context, proxy_ssl_context, verify,
18
+ cookies, tokens, auth_provider, retries, backoff, status_forcelist,
19
+ cookie_hosts)` sends through a transport and follows redirects with one rule
20
+ for credentials (`Authorization`, `Cookie` and `Proxy-Authorization` go to the
21
+ origin they were given for and to no other scheme, host or port). The default `HTTPTransport` (on `http.client`,
22
+ with a per-thread connection pool, framed bodies, `gzip` / `deflate`
23
+ decoding and a bounded reply size) routes every request and redirect hop by
24
+ `proxy.proxy_map(env=env)` (or `proxymap=`): the first entry that accepts a
25
+ connection, an `http` or `https` proxy (`CONNECT`; an `https` proxy over TLS
26
+ with the origin's TLS inside it), a URL-prefix gateway, a proxy's
27
+ credential to the proxy alone; and verifies with the caller's `ssl_context`,
28
+ else the `CA_BUNDLE` bundle or `verify=<path>`, else the system's context
29
+ (`proxy_ssl_context` for an `https` proxy or gateway). Cookies (RFC 6265,
30
+ per origin, on every hop, in a jar by host with `cookies=`), tokens, `_PY`
31
+ hooks, `auth_provider` and retries (`Retry-After` honoured, a status that
32
+ outlasts them returned) work as they did on the session. `HTTPResponse`
33
+ reads its body whole as `content`, `text()` and `json()`.
34
+ - `syshttp.fetch()` (defined in `syshttp.client`, re-exported from the root
35
+ without importing it): `fetch(url, method="GET", *, headers, data, json,
36
+ timeout, client, transport, reauth, **client_options)` returns the
37
+ `HTTPResponse` with its body read, whatever the status; a given `client` is
38
+ left open and a new one is closed; `reauth` runs once after a 401 or 403.
39
+ - The transport protocol is public: `Transport` (`send()` and `close()`),
40
+ `PreparedRequest` and `prepare_request()` give another transport (on
41
+ `requests`, `httpx`, or a fake) the checks and body framing of syshttp's own.
42
+ `SYSHTTP_DEFAULT_TRANSPORT` (`module:attr` or `<file>.py:attr`, read under the
43
+ prefixes) names the transport of a client given none; a value that cannot be
44
+ used is `ConfigurationError`. `hooks.load_object()` is the loader it shares
45
+ with the `_PY` hooks.
46
+ - `syshttp.exceptions`: `SysHTTPError` (the base of what goes wrong at run
47
+ time), `ConfigurationError` (a `ValueError`), `HookError` (a `RuntimeError`)
48
+ and `ResponseError` (a `ValueError`), exported from the root. A malformed
49
+ setting, a hook that cannot be loaded, a failing hook and a body that is not
50
+ JSON raise them; every one is still the `ValueError` or `RuntimeError` it
51
+ was, so existing handlers keep working. A caller's own mistake stays a plain
52
+ `ValueError` / `TypeError`.
53
+ - Every public module declares `__all__` (`proxy` also lists its setting-name
54
+ constants, `PROXY` and `DEFAULT_AUTH_HEADER` among them).
55
+ - `syshttp.env.HTTPEnv(prefixes=None, environ=None)`: every setting is a
56
+ variable with a full name -- syshttp's own carry `SYSHTTP_`
57
+ (`SYSHTTP_PROXY`, `SYSHTTP_HOME`, `SYSHTTP_HOOKS_<KEY>`,
58
+ `SYSHTTP_IMPORTPATCH`), conventional ones keep theirs (`HTTPS_PROXY`,
59
+ `NO_PROXY`, `HTTP_PROXY_AUTH`, `CA_BUNDLE`) -- read as `<PREFIX><NAME>` for
60
+ each prefix of `SYSHTTP_PREFIX` in turn, a `:`-separated list used
61
+ literally (default: one empty prefix, the names as written). The first
62
+ prefix that sets a variable wins, each variable on its own:
63
+ `SYSHTTP_PREFIX=MYTOOL_` reads only `MYTOOL_<NAME>`, `MYTOOL_:` falls back
64
+ to the unprefixed names. `find()` returns the variable found with its
65
+ value, for messages. Every function that reads a setting takes `env=`,
66
+ `new_session()` and the `fetch` `*_auto` helpers included.
67
+ - `CA_BUNDLE` (under the prefixes) names the CA bundle a session's
68
+ `verify=True` uses; sessions no longer read requests' own
69
+ `REQUESTS_CA_BUNDLE` / `CURL_CA_BUNDLE`.
70
+ - The system's proxy configuration is the last proxy source, decided for the
71
+ URL being requested: WinHTTP on Windows, CFNetwork on macOS, libproxy or the
72
+ desktop's settings elsewhere, PAC scripts and WPAD included (through
73
+ proxylib, a new dependency). It comes after `SYSHTTP_PROXY` and the
74
+ `*_PROXY` variables, after the `NO_PROXY` check, and is always spoken to as
75
+ `connect`. `SYSHTTP_SYSTEM_PROXY=0` turns it off. `proxy.plan()`,
76
+ `proxy_url()`, `resolve()` and `proxy_authorization()` take the request URL
77
+ (`url=` / `target=`). `proxy.configured_proxy_url()` gives the
78
+ environment's answer alone. The `pac` extra adds a JavaScript engine for PAC
79
+ scripts proxylib evaluates itself (Linux without libproxy).
80
+ - `syshttp.autopatch`: `SYSHTTP_IMPORTPATCH=1` (or `:`-separated names:
81
+ `requests`, `urllib`, `httpx`, `aiohttp`; read under the prefixes like
82
+ every setting) patches those libraries as they are imported,
83
+ in any program of the environment, so requests without a proxy or TLS
84
+ setting of the caller's route by syshttp and verify with the system's
85
+ context. Through importpatch, a new dependency, and its entry points;
86
+ `patch()` / `unpatch()` do it from code.
87
+ - The system's answer is a list (`PROXY a; PROXY b; DIRECT`): the first proxy
88
+ that accepts a connection is used (proxylib's `first_working_proxy`), and a
89
+ DIRECT reached first is direct. No reachable proxy and no DIRECT raises
90
+ `proxy.NoReachableProxyError` (a `ConnectionError`, exported from the
91
+ root); a session raises `requests.exceptions.ProxyError` for it.
92
+ - `new_session(ssl_context=..., proxy_ssl_context=...)` (and the same on the
93
+ `fetch` `*_auto` helpers): a caller's `ssl.SSLContext` is used as given for
94
+ every https origin, or for an `https://` proxy. That covers mutual TLS,
95
+ ciphers and protocol versions. It cannot be combined with a `verify` other
96
+ than `True`.
97
+ - `hooks.wrapper_environment()`: the `SYSHTTP_HOOK_*` variables a `_CURL`
98
+ wrapper run receives.
99
+ - `jar.default_root()`: the per-user state directory used when
100
+ `SYSHTTP_HOME` is unset.
101
+ - `syshttp.__version__`.
102
+ - Hosts that share cookies: `SYSHTTP_COOKIE_HOSTS` (hosts split by `,`,
103
+ groups by `;`) or `new_session(cookie_hosts=...)` declares names that reach
104
+ one service. In a group, a cookie a member sets for itself or another
105
+ member belongs to the first host, every member's requests carry the
106
+ group's cookies, and any member can delete them; a `Domain` outside the
107
+ group is still judged on the host that sent it. `cookies.host_aliases()`
108
+ builds the mapping, which `set_cookie_specs`, `merge_set_cookie_headers`
109
+ and `host_only_policy` take as `aliases=`.
110
+ - The shipped header and the settings page list every variable syshttp
111
+ reads under its own name (the proxy family, `REQUEST_METHOD`, the CA bundle
112
+ variables, the state-directory variables, importpatch's and proxylib's).
113
+
114
+ ### Changed
115
+
116
+ - `syshttp.fetch` is a function, not a module: `fetch()` replaces the helpers
117
+ below. `_PY` hooks are called as `hook(client, method, url, kwargs)` with the
118
+ keyword arguments of `HTTPClient.request()`, and may return an
119
+ `HTTPResponse`; `auth.AuthProvider.ensure(client, url)` takes the client.
120
+ `CA_BUNDLE` is the bundle a client's `verify=True` uses. `cookies.py` keeps
121
+ the Netscape rows, `set_cookie_specs`, `cookie_applies` and `host_aliases`
122
+ only; the client keeps its cookies as those rows.
123
+ - Routing is proxylib 2's (`proxylib>=2.0.2,<3`; 2.0.1 breaks pip's cached
124
+ requests). `proxy.proxy_map(proxy,
125
+ noproxy, proxy_user, *, env)` is the settings as one proxylib proxy map,
126
+ read at each lookup; `proxy.configured_proxy()` is `SYSHTTP_PROXY` as a
127
+ proxylib `Proxy`; `proxy.system_map()` is the system's configuration
128
+ (`proxylib.auto_proxy`). The `*_PROXY` variables are read per scheme
129
+ (lowercase first, httpoxy-safe), `NO_PROXY` takes CIDR entries, and curl's
130
+ `-x ""` is direct. `SYSHTTP_PROXY_AUTH` goes to `SYSHTTP_PROXY` only and
131
+ `HTTP_PROXY_AUTH` to the machine's proxies only.
132
+ - A list from the system (`PROXY a; PROXY b; DIRECT`) is tried in order by the
133
+ session, httpx and aiohttp (proxylib's failover), replacing the probe of
134
+ the entry above; urllib takes the first reachable entry. No proxy
135
+ connecting and no DIRECT is the client's own connection error;
136
+ `NoReachableProxyError` is proxylib's.
137
+ - The session's `ProxyAdapter` is proxylib's `ProxyMapAdapter` with the
138
+ system's TLS; `adapter.route(url)` returns the entries it will try.
139
+ `proxies=` given to `new_session` are requests' own, with no syshttp
140
+ credential added.
141
+ - `syshttp.autopatch` routes through `proxylib.patch()` and patches TLS only;
142
+ requests' TLS is a pool hook on every `HTTPAdapter`. A caller's own
143
+ proxies and `trust_env = False` still win over syshttp (proxylib 2.0.2's
144
+ rule). aiohttp is not offered
145
+ URL-prefix gateways. New target `pip` (`pip._vendor.requests`, proxylib
146
+ 2.0.1's `pip` target): pip's adapters route through the map unless pip is
147
+ given `--proxy` or `--no-proxy-env`; pip keeps its own TLS context.
148
+
149
+ - The jar constructors (`FileCookieJar`, `FileTokenJar`) no longer create
150
+ their directory; it is created at the first write.
151
+
152
+ - A session returns the last response when a status in `status_forcelist`
153
+ outlasts its retries, as plain requests does; it used to raise
154
+ `requests.exceptions.RetryError`, which the fetch helpers (`-> (status,
155
+ text)`) passed on. The `*_auto` helpers take `new_session`'s options as
156
+ `**session_options` (every keyword they had still works) and their
157
+ `timeout` may be a `(connect, read)` pair.
158
+ - Every dependency's floor is its newest release that supports Python 3.9:
159
+ the `session` extra requires `requests>=2.32.5` and `urllib3>=2.6.3` (was
160
+ `requests>=2.25`), and `sslcontext>=2.1.0`. Below requests 2.32.3 a caller's
161
+ `ssl_context` was silently ignored, urllib3 1.26 raises a bare `ValueError`
162
+ for an https IP-literal host, and urllib3 up to 2.2 leaks the socket of a
163
+ failed TLS handshake through an https proxy. On Python 3.9, botocore pins `urllib3<1.27`, so `syshttp[session]`
164
+ cannot share an environment with it there.
165
+
166
+
167
+ - The header says that jars, tokens, `_PY` hooks and `auth_provider` apply to
168
+ `session.request` and not to a bare `session.send()`, which gets the proxy
169
+ decision and TLS only.
170
+
171
+ - `new_session()` no longer calls `urllib3.disable_warnings`, which silenced
172
+ `InsecureRequestWarning` for the whole process; with `verify=False` urllib3
173
+ warns as it does for any requests session. The undocumented
174
+ `syshttp.warnings` module is gone.
175
+
176
+ ### Removed
177
+
178
+ - `syshttp.session` (`new_session()` and its `requests` adapter), the
179
+ `session` extra (and its `requests` and `urllib3` floors; `requests` stays a
180
+ development dependency for the tests of `syshttp.autopatch`), and the helpers
181
+ of the old `syshttp.fetch` module: `request_text`, `request_json`,
182
+ `request_with_reauth`, `request_text_auto` and `request_json_auto`. Their
183
+ replacements are `HTTPClient` and `fetch()`. A per-request `verify=` and
184
+ `cert=` are gone with them: build a client with `verify=` or an
185
+ `ssl_context` that holds the identity. `cookies.apply_to_session`,
186
+ `merge_set_cookie_headers` and `host_only_policy`, which only a `requests`
187
+ session used, are gone too.
188
+ - `proxy.proxy_url`, `configured_proxy_url`, `system_proxy`,
189
+ `system_proxy_map`, `first_reachable`, `PROBE_MAX_AGE`, `PROBE_TIMEOUT`,
190
+ `resolve`, `proxy_authorization`, `configured_authorization`, `userinfo`,
191
+ `basic_authorization`, `strip_auth`, `redact`, `prefix_url`,
192
+ `should_bypass`, `bypassed_by`, `no_proxy_from_env`, `proxies_from_env`,
193
+ `plan` and `ProxyPlan`: `proxy_map()` and proxylib's `Proxy` replace them.
194
+
195
+ ### Changed (earlier)
196
+
197
+ - The package stands alone as `syshttp`. It was previously the `httplib`
198
+ library of the dotagents `net` overlay, which shipped with a curl-compatible
199
+ command (`httplib.cli`, `python -m httplib`) that is now the `curlike`
200
+ package. Imports move from `httplib` to `syshttp`, and settings from the
201
+ dotagents names to the prefixed ones: `AGENTS_PROXY` → `SYSHTTP_PROXY`,
202
+ `AGENTS_PROXY_AUTH` → `SYSHTTP_PROXY_AUTH`, `AGENTS_PROXY_AUTH_HEADER` →
203
+ `SYSHTTP_PROXY_AUTH_HEADER`, `AGENTS_PROXY_TYPE` → `SYSHTTP_PROXY_TYPE`,
204
+ `AGENTS_HOME` → `SYSHTTP_HOME`, `NET_HOOKS_<KEY>` → `SYSHTTP_HOOKS_<KEY>`,
205
+ `NET_HOOK_*` → `SYSHTTP_HOOK_*`. A tool's own namespace is a prefix:
206
+ `SYSHTTP_PREFIX=AGENTS_` reads `AGENTS_SYSHTTP_PROXY`,
207
+ `AGENTS_SYSHTTP_HOOKS_<KEY>` and so on.
208
+ - The system's TLS client by default: `tls.new_os_context()` (and so
209
+ `os_ssl_context()` and every session with `verify=True`) returns
210
+ `sslcontext.system_context()` (sslcontext 2 is a new dependency).
211
+ Certificates are verified by the operating system on Python 3.10+
212
+ (CryptoAPI on Windows, the Security framework on macOS, OpenSSL over the
213
+ system store elsewhere), by OpenSSL over the default store on 3.9, and a
214
+ server that asks for a client certificate gets one from the OS store,
215
+ chosen as a browser chooses. `new_os_context(client_certificates=False)`
216
+ offers none, for a caller loading its own. A `cafile` / `capath` still
217
+ replaces the store with a plain `ssl` context.
218
+ - A session without caller `proxies` decides the proxy for every request and
219
+ every redirect hop, from the environment as it is then and the system's
220
+ answer for that URL. A direct decision is final: requests' `trust_env` cannot
221
+ re-add a proxy. `adapter.route(url)` shows the decision.
222
+ - Without `SYSHTTP_HOME`, the jars live in the per-user state directory
223
+ instead of the dotagents store directory.
224
+ - `proxy.resolve()` and `proxy.proxy_authorization()`: the keyword `env: bool`
225
+ is now `configured: bool`.
226
+ - `hooks.hooks_from_env()`, `matching()`, `skipped()` and `call_py_hooks()`
227
+ take `env: HTTPEnv` (keyword-only) instead of a mapping `environ`.
228
+ `hooks.PREFIX`, `SKIP_ENV`, `URL_ENV`, `KEY_ENV` and `CURL_ENV` are
229
+ replaced by the setting names `HOOKS`, `HOOK_SKIP`, `HOOK_URL`, `HOOK_KEY`
230
+ and `HOOK_CURL`.
231
+ - `proxy.AUTH_VARS`, `AUTH_HEADER_VAR` and `TYPE_VAR` are replaced by the
232
+ setting names `PROXY_AUTH`, `PROXY_AUTH_HEADER` and `PROXY_TYPE`.
233
+
234
+ ### Fixed
235
+
236
+ - A by-host cookie file keeps a cookie its host set for a parent domain
237
+ (`Domain=example.com` from `www.example.com`); it was never saved.
238
+ - The proxy credential reaches a proxy whose host is configured in upper
239
+ case. requests lower-cases the proxy URL it hands to `proxy_headers`, so the
240
+ credential used to be dropped and the proxy answered 407.
241
+ - `Set-Cookie` is read as RFC 6265 section 5.2 reads it, not through
242
+ `http.cookies.SimpleCookie`: a value with a space or JSON is kept, an
243
+ unknown attribute (`Priority`, `Partitioned`) is no longer a cookie of its
244
+ own, `Max-Age` beats `Expires`, and a header with no `name=value` is
245
+ skipped. A `Domain` without a leading dot covers subdomains. A cookie with
246
+ no `Path` gets the directory of the request path
247
+ (`set_cookie_specs(..., request_path=)`).
248
+ - A session holds one copy of each cookie. `merge_set_cookie_headers` no
249
+ longer re-sets what requests already stored (it used to add a second copy
250
+ with path `/` and a dotless domain); it applies only the deletions requests
251
+ misses, for redirect hops too.
252
+ - A cookie set without `Domain` stays on its host: rows loaded from a jar and
253
+ cookies a server sets are no longer sent to subdomains
254
+ (`new_session` uses `cookies.host_only_policy()`).
255
+ - A cookie for a single-label host (`localhost`, `wiki`) is loaded from its
256
+ jar file, and is saved under its own name instead of `localhost.local`.
257
+ - `FileCookieJar` keeps `HttpOnly` and the include-subdomains flag.
258
+ - `load_netscape` keeps a row whose value is empty, and an expiry written
259
+ with a non-ASCII digit is a session cookie instead of a `ValueError`.
260
+ `retry_after` returns `None` for such a value, and reads an HTTP date
261
+ written without a zone (`asctime`) as UTC.
262
+ - A cookie expiry beyond 400 days is cut to 400 days.
263
+ - A hook whose `_PY` / `_CURL` target is blank is ignored instead of failing
264
+ every request, and `HTTPEnv.family` strips values as `get` does.
265
+ - Hook KEYs are compared without regard to case: `HOOK_SKIP=mykey` did not
266
+ suppress `MYKEY`, the form Windows holds. `Hook.key` and `skipped()` are
267
+ upper-case.
268
+ - A `<file>.py:callable` hook's module is loaded once per process, not on
269
+ every request (its module-level state was reset each time), and two files
270
+ with one name no longer share a module name.
271
+ - `request_text_auto` and `request_json_auto` retry a 408 by default, as
272
+ `new_session` does (their own list had dropped it), and close the session
273
+ they built instead of leaking its connection pool. The five fetch helpers
274
+ have docstrings.
275
+ - `new_session(user_agent=...)` sets the User-Agent; the default
276
+ `python-requests/x` was sent whatever was given.
277
+ - The guards that keep a hook or `auth_provider.ensure` from running again for
278
+ its own requests are per thread: while one thread was inside a hook, the
279
+ other threads' requests skipped hooks and `ensure`.
280
+ - A request-level `verify=` (`False`, or a bundle) beside `ssl_context`, which
281
+ was silently ignored, raises `ValueError` as the same value does at session
282
+ level. `CA_BUNDLE` is not a request-level value, and a session with an
283
+ `ssl_context` does not consult it.
284
+ - The import patch replaces requests 2.32.3's own default context (a certifi
285
+ bundle) in a pool, as it does for the later versions that leave it out.
286
+ - Token and cookie files are replaced whole by a rename, so a reader never sees
287
+ an empty or half-written file and concurrent savers no longer truncate each
288
+ other's file.
289
+ - `SYSHTTP_HOME=~/jars` is expanded instead of creating a directory named `~`.
290
+ - Jar files are written with LF on Windows too, as curl writes them.
291
+ - A session whose cookie jar cannot be read (an error while iterating
292
+ `session.cookies`) no longer saves an empty list over the host's cookies, and
293
+ writes a host's file only when its cookies changed instead of after every
294
+ response.
295
+
296
+ ### Security
297
+
298
+ - The client closes six gaps found in review. A `Set-Cookie` with a control
299
+ character in it is ignored whole (RFC 6265bis 5.6), `load_netscape` ends a
300
+ line at a newline only (it ended one at `\x0b`, `\x0c`, `\x1c`-`\x1e` and
301
+ `\x85` too, so a value could become a whole row for another domain),
302
+ `save_netscape` writes no row with a control character or a tab in a field,
303
+ and a by-host jar file gives the client only the rows that cover its host or
304
+ group. A plain-http response can no longer set a Secure cookie, or set,
305
+ replace or delete one that is stored (RFC 6265bis 5.7; the loopback hosts
306
+ count as secure, as in curl). A redirect to another scheme, host or port
307
+ drops a caller's `Cookie` and `Proxy-Authorization` header with
308
+ `Authorization`. `HTTPTransport.send(stream=False)` and
309
+ `HTTPResponse.buffered()` hold a decoded reply to `MAX_REPLY_BYTES` (a
310
+ kilobyte of gzip decoded to tens of megabytes), and the too-large error names
311
+ the URL as scheme, host, port and path, without its query. An `https:` origin
312
+ is no longer sent through a plain `prefix+http` gateway, which would carry its
313
+ tokens and Secure cookies in clear text: the entry is skipped, and with
314
+ nothing else left the request raises `proxylib.UnsupportedProxyError`.
315
+ - A `Set-Cookie` can no longer name a domain its host does not own. Any
316
+ host could set or delete another host's cookie (`Domain=bank.example` from
317
+ `evil.example`) or one for a top level domain; such a header is now skipped
318
+ on a set and on a deletion, in the session, in `FileCookieJar` and in
319
+ `set_cookie_specs` (which curlike's `-c` uses). A public suffix is
320
+ recognised without the Public Suffix List (a name with no dot, or a
321
+ `co.uk`-style registry).
322
+ - Token and cookie files are created `0600`, and the directories this library
323
+ creates `0700`, instead of world-readable under a usual umask.
324
+ - A client certificate passed as `cert=` is no longer loaded into the shared OS
325
+ context (or into the caller's `ssl_context`), where every later session, and
326
+ every other user of that context, presented it too. It gets a context of the
327
+ session's own; `cert=` beside `ssl_context` raises `ValueError`.
328
+ - A URL-prefix gateway is spoken to with `proxy_ssl_context` (or the system's
329
+ verifier), no longer with the `ssl_context` and client certificate meant for
330
+ the origin.
331
+ - A token, or a proxy credential, containing CR, LF or NUL raises a
332
+ `ValueError` that names the jar key or variable instead of requests'
333
+ `InvalidHeader` carrying the whole secret.
334
+ - No setting can be chosen by a CGI client unless the program reads the
335
+ unprefixed names: syshttp's own all start `SYSHTTP_`, which a request
336
+ header never becomes, and a CGI program sets a prefix list without the
337
+ empty entry so that `HTTP_PROXY` and `HTTP_PROXY_AUTH` are not read.
338
+ - A failing hook's message names its URL as scheme, host, port and path, no
339
+ longer with the userinfo, query and fragment (`user:pass@`,
340
+ `?access_token=`).
341
+
342
+ [Unreleased]: https://github.com/jose-pr/syshttp/compare/v0.1.0...HEAD
343
+ [0.1.0]: https://github.com/jose-pr/syshttp/releases/tag/v0.1.0
syshttp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jose A.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
syshttp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.5
2
+ Name: syshttp
3
+ Version: 0.1.0
4
+ Summary: HTTP plumbing that follows the system: the system proxy (PAC, WPAD) and the OS certificate verifier by default, proxy credentials and prefix gateways, Netscape cookie jars, OS trust-store TLS, URL hooks, and an HTTP client on the standard library
5
+ Project-URL: Homepage, https://github.com/jose-pr/syshttp
6
+ Project-URL: Documentation, https://jose-pr.github.io/syshttp/
7
+ Project-URL: Issues, https://github.com/jose-pr/syshttp/issues
8
+ Author-email: jose-pr <jose-pr@coqui.dev>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Internet :: WWW/HTTP
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: importpatch<0.3,>=0.2.1
24
+ Requires-Dist: proxylib<3,>=2.0.2
25
+ Requires-Dist: sslcontext<3,>=2.1.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: aiohttp; extra == 'dev'
28
+ Requires-Dist: black; extra == 'dev'
29
+ Requires-Dist: build; extra == 'dev'
30
+ Requires-Dist: httpx; extra == 'dev'
31
+ Requires-Dist: mypy<2; extra == 'dev'
32
+ Requires-Dist: pytest; extra == 'dev'
33
+ Requires-Dist: requests; extra == 'dev'
34
+ Requires-Dist: types-requests; extra == 'dev'
35
+ Provides-Extra: docs
36
+ Requires-Dist: mkdocs; extra == 'docs'
37
+ Requires-Dist: mkdocs-material; extra == 'docs'
38
+ Requires-Dist: mkdocstrings[python]; extra == 'docs'
39
+ Provides-Extra: pac
40
+ Requires-Dist: proxylib[pac]<3,>=2.0.2; extra == 'pac'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # syshttp
44
+
45
+ [![Version](https://img.shields.io/pypi/v/syshttp.svg)](https://pypi.org/project/syshttp/)
46
+ [![Python versions](https://img.shields.io/pypi/pyversions/syshttp.svg)](https://pypi.org/project/syshttp/)
47
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jose-pr/syshttp/blob/main/LICENSE)
48
+ [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://jose-pr.github.io/syshttp/)
49
+ [![CI](https://img.shields.io/github/actions/workflow/status/jose-pr/syshttp/test.yml)](https://github.com/jose-pr/syshttp/actions/workflows/test.yml)
50
+
51
+ **HTTP plumbing that behaves the same in every client you build on it**:
52
+ which proxy to use, with which credential, for which hosts; how a prefix
53
+ gateway is spoken to; which cookies and tokens go with which origin; and TLS
54
+ through the operating system's own certificate verifier. By default it
55
+ routes the way the system does: the system's proxy configuration, PAC scripts
56
+ and WPAD included, unless a proxy variable says otherwise. An HTTP client on the
57
+ standard library puts all of them together; `requests` is needed by none of
58
+ it. Documentation: <https://jose-pr.github.io/syshttp/>.
59
+
60
+ ## Features
61
+
62
+ - **Routing is [proxylib](https://pypi.org/project/proxylib/)'s** —
63
+ `proxy.proxy_map()` turns the settings below into one proxylib proxy map,
64
+ and proxylib's client integrations send through it.
65
+ - **The system's proxy, by default** — with no proxy variable set, the
66
+ system's configuration decides per URL: WinHTTP on Windows, CFNetwork on
67
+ macOS, libproxy or the desktop's settings elsewhere, PAC and WPAD included.
68
+ A list (`PROXY a; PROXY b; DIRECT`) is tried in order, the next entry when
69
+ one does not connect; with no proxy connecting and no DIRECT the request
70
+ fails rather than going direct.
71
+ - **Proxies with credentials** — a proxy URL from `SYSHTTP_PROXY`, else the
72
+ usual `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` for the request's scheme.
73
+ The credential is a header value sent verbatim to that proxy only, so any
74
+ scheme works (`Basic`, `Bearer`, `Negotiate`). `NO_PROXY` is honoured
75
+ (CIDR included), and every redirect hop is decided again.
76
+ - **Prefix gateways** — `SYSHTTP_PROXY_TYPE=prefix:/fetch/` sends
77
+ `GET https://api.example.com/x` to `<proxy>/fetch/https://api.example.com/x`
78
+ (the client, requests, httpx and urllib; the client sends an `https:` origin
79
+ through an `https` gateway only). Responses, redirects and cookies still use
80
+ the caller's URL.
81
+ - **Prefixes of a tool's own** — `SYSHTTP_PREFIX=MYTOOL_` makes the package
82
+ read `MYTOOL_HTTPS_PROXY`, `MYTOOL_SYSHTTP_HOME` and so on instead of the
83
+ defaults; `MYTOOL_:` falls back to them, variable by variable. An
84
+ `HTTPEnv("MYTOOL_")` object keeps one configuration apart from another in
85
+ the same process.
86
+ - **Cookie and token jars** — Netscape cookie files with curl's matching
87
+ rules, plus per-host token files. Both are chosen by the origin's host,
88
+ never by the proxy's or the gateway's.
89
+ - **URL hooks** — `SYSHTTP_HOOKS_<KEY>=<regex>` plus `_PY=module:callable`
90
+ runs your code before matching requests: a login, a signature, a canned
91
+ response.
92
+ - **The system's TLS client, or your own TLS context** — by default
93
+ [sslcontext](https://pypi.org/project/sslcontext/)'s system context: the
94
+ operating system verifies certificates (CryptoAPI, the Security framework),
95
+ with no `certifi` bundle involved, and a server that asks for a client
96
+ certificate gets one from the OS store (smart cards included), chosen as a
97
+ browser chooses. Or `HTTPClient(ssl_context=...)` for a given identity, a
98
+ cipher list or a pinned protocol version.
99
+ - **An HTTP client on the standard library** — `HTTPClient` and `fetch()`:
100
+ retry with backoff, redirects that keep a credential with its origin, and
101
+ all of the above, on `http.client` and `ssl` alone. Its transport is
102
+ replaceable: any object with `send()` (a `requests` or `httpx` one, a fake
103
+ in a test), chosen per client or by `SYSHTTP_DEFAULT_TRANSPORT`.
104
+ - **Other programs' clients, patched on import** — `SYSHTTP_IMPORTPATCH=1`
105
+ (or `requests:urllib:httpx:aiohttp:pip`) makes those libraries route and
106
+ verify the same way in any program of the environment, through
107
+ [importpatch](https://pypi.org/project/importpatch/); what a caller sets
108
+ itself still wins.
109
+
110
+ ## Installation
111
+
112
+ ```bash
113
+ pip install syshttp
114
+ ```
115
+
116
+ | Extra | Adds | Needed for |
117
+ | --- | --- | --- |
118
+ | `pac` | a JavaScript engine for proxylib | PAC scripts proxylib evaluates itself (Linux without libproxy) |
119
+
120
+ syshttp depends on proxylib (the system proxy), sslcontext (the system's
121
+ TLS client: the OS verifier through truststore on Python 3.10+, OpenSSL over
122
+ the default store on 3.9) and importpatch (its `.pth` hooks imports at
123
+ interpreter start; nothing is patched unless `SYSHTTP_IMPORTPATCH` says so).
124
+
125
+ ## Quick start
126
+
127
+ Ask where a request would go, the way every client here does:
128
+
129
+ ```python
130
+ import proxylib
131
+ from syshttp import proxy
132
+
133
+ for entry in proxylib.lookup(proxy.proxy_map(), "https://api.example.com/"):
134
+ print("DIRECT" if entry is None else entry.url) # tried in this order
135
+ ```
136
+
137
+ One request, routed and verified the way the system does (no extra to install):
138
+
139
+ ```python
140
+ from syshttp import fetch
141
+
142
+ response = fetch("https://api.example.com/thing")
143
+ print(response.status, response.json())
144
+ ```
145
+
146
+ A client with file-backed cookies and tokens, kept across requests:
147
+
148
+ ```python
149
+ from syshttp.client import HTTPClient
150
+
151
+ with HTTPClient(cookies=True, tokens=True) as client:
152
+ response = client.request("GET", "https://api.example.com/thing")
153
+ ```
154
+
155
+ A separate configuration, isolated from the process environment:
156
+
157
+ ```python
158
+ from syshttp import HTTPEnv, fetch
159
+
160
+ env = HTTPEnv("MYTOOL_", environ={"MYTOOL_SYSHTTP_PROXY": "http://proxy.internal:3128"})
161
+ response = fetch("https://api.example.com/thing", env=env)
162
+ ```
163
+
164
+ ## API overview
165
+
166
+ | Module | Purpose |
167
+ | --- | --- |
168
+ | `syshttp.env` | `HTTPEnv`: the prefixes and mapping every setting is read from |
169
+ | `syshttp.proxy` | `proxy_map()`: the settings as one proxylib proxy map |
170
+ | `syshttp.client` | `HTTPClient` and `fetch()`: the HTTP client, its transport protocol and response |
171
+ | `syshttp.jar` | file and memory cookie/token jars |
172
+ | `syshttp.cookies` | Netscape cookie files and curl's cookie rules |
173
+ | `syshttp.hooks` | URL hooks declared in the environment |
174
+ | `syshttp.tls` | the OS verifier as an `SSLContext` |
175
+ | `syshttp.autopatch` | requests, urllib, httpx, aiohttp and pip patched on import |
176
+ | `syshttp.retry` | the transient statuses, `Retry-After` |
177
+
178
+ ## Settings
179
+
180
+ Every setting is read as `<PREFIX><NAME>` for each prefix of
181
+ `SYSHTTP_PREFIX` (`:`-separated; default none, the names as written). The
182
+ full list is on the docs site:
183
+ <https://jose-pr.github.io/syshttp/settings/>.
184
+
185
+ | Variable | Sets |
186
+ | --- | --- |
187
+ | `SYSHTTP_PREFIX` | the prefixes every other setting is read under |
188
+ | `SYSHTTP_PROXY` | the proxy URL; else `HTTPS_PROXY`, `HTTP_PROXY`, `ALL_PROXY`, else the system's |
189
+ | `SYSHTTP_SYSTEM_PROXY` | `0` to stop consulting the system's proxy configuration |
190
+ | `SYSHTTP_PROXY_AUTH` | the proxy credential, sent verbatim; else `HTTP_PROXY_AUTH` |
191
+ | `SYSHTTP_PROXY_AUTH_HEADER` | the header it is sent in (default `Proxy-Authorization`) |
192
+ | `SYSHTTP_PROXY_TYPE` | `connect` (default) or `prefix[:/endpoint]` |
193
+ | `SYSHTTP_HOME` | the jar directory (default: the per-user state directory) |
194
+ | `SYSHTTP_IMPORTPATCH` | `1`, or `:`-separated names: the client libraries patched on import |
195
+ | `SYSHTTP_HOOKS_<KEY>` | a URL hook's pattern, with `_PY` / `_CURL` targets |
196
+ | `SYSHTTP_DEFAULT_TRANSPORT` | `module:attr` or `<file>.py:attr`: the transport of a client given none (it names code to import) |
197
+ | `CA_BUNDLE` | the CA bundle a client verifies with (default: the OS verifier) |
198
+
199
+ ## Development
200
+
201
+ ```bash
202
+ dotagents pyvenv # or any Python 3.9+ virtual environment
203
+ .pyvenv/<tag>/Scripts/python -m pip install -e ".[dev]"
204
+ .pyvenv/<tag>/Scripts/python -m pytest -q
205
+ ```
206
+
207
+ ### Releasing
208
+
209
+ This project follows [Semantic Versioning](https://semver.org/) and keeps a
210
+ [`CHANGELOG.md`](https://github.com/jose-pr/syshttp/blob/main/CHANGELOG.md).
211
+ Pushing a tag matching `v*` triggers the release workflow: test gate → build
212
+ (checking the tag names the version built) → a strict docs build as a gate →
213
+ GitHub release → publish. The release workflow never deploys the docs site
214
+ itself: for a final release its last job dispatches the docs workflow at the
215
+ tag, which owns every Pages deploy.
216
+
217
+ ## License
218
+
219
+ MIT — see [LICENSE](https://github.com/jose-pr/syshttp/blob/main/LICENSE).