syshttp 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.
- syshttp/AGENTS.md +817 -0
- syshttp/README.md +177 -0
- syshttp/__init__.py +71 -0
- syshttp/_fileio.py +78 -0
- syshttp/_parse.py +30 -0
- syshttp/auth.py +17 -0
- syshttp/autopatch.py +382 -0
- syshttp/client/__init__.py +70 -0
- syshttp/client/_body.py +156 -0
- syshttp/client/_client.py +413 -0
- syshttp/client/_context.py +107 -0
- syshttp/client/_deadline.py +94 -0
- syshttp/client/_default.py +72 -0
- syshttp/client/_fetch.py +98 -0
- syshttp/client/_follow.py +243 -0
- syshttp/client/_headers.py +238 -0
- syshttp/client/_pool.py +170 -0
- syshttp/client/_prepare.py +77 -0
- syshttp/client/_response.py +301 -0
- syshttp/client/_route.py +75 -0
- syshttp/client/_settings.py +12 -0
- syshttp/client/_state.py +322 -0
- syshttp/client/_status.py +59 -0
- syshttp/client/_tlstunnel.py +114 -0
- syshttp/client/_transport.py +550 -0
- syshttp/cookies.py +391 -0
- syshttp/env.py +144 -0
- syshttp/exceptions.py +29 -0
- syshttp/hooks.py +314 -0
- syshttp/jar.py +133 -0
- syshttp/proxy.py +341 -0
- syshttp/py.typed +0 -0
- syshttp/retry.py +34 -0
- syshttp/tls.py +60 -0
- syshttp-0.1.0.dist-info/METADATA +219 -0
- syshttp-0.1.0.dist-info/RECORD +39 -0
- syshttp-0.1.0.dist-info/WHEEL +4 -0
- syshttp-0.1.0.dist-info/entry_points.txt +6 -0
- syshttp-0.1.0.dist-info/licenses/LICENSE +21 -0
syshttp/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# syshttp
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/syshttp/)
|
|
4
|
+
[](https://pypi.org/project/syshttp/)
|
|
5
|
+
[](https://github.com/jose-pr/syshttp/blob/main/LICENSE)
|
|
6
|
+
[](https://jose-pr.github.io/syshttp/)
|
|
7
|
+
[](https://github.com/jose-pr/syshttp/actions/workflows/test.yml)
|
|
8
|
+
|
|
9
|
+
**HTTP plumbing that behaves the same in every client you build on it**:
|
|
10
|
+
which proxy to use, with which credential, for which hosts; how a prefix
|
|
11
|
+
gateway is spoken to; which cookies and tokens go with which origin; and TLS
|
|
12
|
+
through the operating system's own certificate verifier. By default it
|
|
13
|
+
routes the way the system does: the system's proxy configuration, PAC scripts
|
|
14
|
+
and WPAD included, unless a proxy variable says otherwise. An HTTP client on the
|
|
15
|
+
standard library puts all of them together; `requests` is needed by none of
|
|
16
|
+
it. Documentation: <https://jose-pr.github.io/syshttp/>.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Routing is [proxylib](https://pypi.org/project/proxylib/)'s** —
|
|
21
|
+
`proxy.proxy_map()` turns the settings below into one proxylib proxy map,
|
|
22
|
+
and proxylib's client integrations send through it.
|
|
23
|
+
- **The system's proxy, by default** — with no proxy variable set, the
|
|
24
|
+
system's configuration decides per URL: WinHTTP on Windows, CFNetwork on
|
|
25
|
+
macOS, libproxy or the desktop's settings elsewhere, PAC and WPAD included.
|
|
26
|
+
A list (`PROXY a; PROXY b; DIRECT`) is tried in order, the next entry when
|
|
27
|
+
one does not connect; with no proxy connecting and no DIRECT the request
|
|
28
|
+
fails rather than going direct.
|
|
29
|
+
- **Proxies with credentials** — a proxy URL from `SYSHTTP_PROXY`, else the
|
|
30
|
+
usual `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` for the request's scheme.
|
|
31
|
+
The credential is a header value sent verbatim to that proxy only, so any
|
|
32
|
+
scheme works (`Basic`, `Bearer`, `Negotiate`). `NO_PROXY` is honoured
|
|
33
|
+
(CIDR included), and every redirect hop is decided again.
|
|
34
|
+
- **Prefix gateways** — `SYSHTTP_PROXY_TYPE=prefix:/fetch/` sends
|
|
35
|
+
`GET https://api.example.com/x` to `<proxy>/fetch/https://api.example.com/x`
|
|
36
|
+
(the client, requests, httpx and urllib; the client sends an `https:` origin
|
|
37
|
+
through an `https` gateway only). Responses, redirects and cookies still use
|
|
38
|
+
the caller's URL.
|
|
39
|
+
- **Prefixes of a tool's own** — `SYSHTTP_PREFIX=MYTOOL_` makes the package
|
|
40
|
+
read `MYTOOL_HTTPS_PROXY`, `MYTOOL_SYSHTTP_HOME` and so on instead of the
|
|
41
|
+
defaults; `MYTOOL_:` falls back to them, variable by variable. An
|
|
42
|
+
`HTTPEnv("MYTOOL_")` object keeps one configuration apart from another in
|
|
43
|
+
the same process.
|
|
44
|
+
- **Cookie and token jars** — Netscape cookie files with curl's matching
|
|
45
|
+
rules, plus per-host token files. Both are chosen by the origin's host,
|
|
46
|
+
never by the proxy's or the gateway's.
|
|
47
|
+
- **URL hooks** — `SYSHTTP_HOOKS_<KEY>=<regex>` plus `_PY=module:callable`
|
|
48
|
+
runs your code before matching requests: a login, a signature, a canned
|
|
49
|
+
response.
|
|
50
|
+
- **The system's TLS client, or your own TLS context** — by default
|
|
51
|
+
[sslcontext](https://pypi.org/project/sslcontext/)'s system context: the
|
|
52
|
+
operating system verifies certificates (CryptoAPI, the Security framework),
|
|
53
|
+
with no `certifi` bundle involved, and a server that asks for a client
|
|
54
|
+
certificate gets one from the OS store (smart cards included), chosen as a
|
|
55
|
+
browser chooses. Or `HTTPClient(ssl_context=...)` for a given identity, a
|
|
56
|
+
cipher list or a pinned protocol version.
|
|
57
|
+
- **An HTTP client on the standard library** — `HTTPClient` and `fetch()`:
|
|
58
|
+
retry with backoff, redirects that keep a credential with its origin, and
|
|
59
|
+
all of the above, on `http.client` and `ssl` alone. Its transport is
|
|
60
|
+
replaceable: any object with `send()` (a `requests` or `httpx` one, a fake
|
|
61
|
+
in a test), chosen per client or by `SYSHTTP_DEFAULT_TRANSPORT`.
|
|
62
|
+
- **Other programs' clients, patched on import** — `SYSHTTP_IMPORTPATCH=1`
|
|
63
|
+
(or `requests:urllib:httpx:aiohttp:pip`) makes those libraries route and
|
|
64
|
+
verify the same way in any program of the environment, through
|
|
65
|
+
[importpatch](https://pypi.org/project/importpatch/); what a caller sets
|
|
66
|
+
itself still wins.
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install syshttp
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Extra | Adds | Needed for |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `pac` | a JavaScript engine for proxylib | PAC scripts proxylib evaluates itself (Linux without libproxy) |
|
|
77
|
+
|
|
78
|
+
syshttp depends on proxylib (the system proxy), sslcontext (the system's
|
|
79
|
+
TLS client: the OS verifier through truststore on Python 3.10+, OpenSSL over
|
|
80
|
+
the default store on 3.9) and importpatch (its `.pth` hooks imports at
|
|
81
|
+
interpreter start; nothing is patched unless `SYSHTTP_IMPORTPATCH` says so).
|
|
82
|
+
|
|
83
|
+
## Quick start
|
|
84
|
+
|
|
85
|
+
Ask where a request would go, the way every client here does:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import proxylib
|
|
89
|
+
from syshttp import proxy
|
|
90
|
+
|
|
91
|
+
for entry in proxylib.lookup(proxy.proxy_map(), "https://api.example.com/"):
|
|
92
|
+
print("DIRECT" if entry is None else entry.url) # tried in this order
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
One request, routed and verified the way the system does (no extra to install):
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from syshttp import fetch
|
|
99
|
+
|
|
100
|
+
response = fetch("https://api.example.com/thing")
|
|
101
|
+
print(response.status, response.json())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A client with file-backed cookies and tokens, kept across requests:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from syshttp.client import HTTPClient
|
|
108
|
+
|
|
109
|
+
with HTTPClient(cookies=True, tokens=True) as client:
|
|
110
|
+
response = client.request("GET", "https://api.example.com/thing")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A separate configuration, isolated from the process environment:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from syshttp import HTTPEnv, fetch
|
|
117
|
+
|
|
118
|
+
env = HTTPEnv("MYTOOL_", environ={"MYTOOL_SYSHTTP_PROXY": "http://proxy.internal:3128"})
|
|
119
|
+
response = fetch("https://api.example.com/thing", env=env)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API overview
|
|
123
|
+
|
|
124
|
+
| Module | Purpose |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| `syshttp.env` | `HTTPEnv`: the prefixes and mapping every setting is read from |
|
|
127
|
+
| `syshttp.proxy` | `proxy_map()`: the settings as one proxylib proxy map |
|
|
128
|
+
| `syshttp.client` | `HTTPClient` and `fetch()`: the HTTP client, its transport protocol and response |
|
|
129
|
+
| `syshttp.jar` | file and memory cookie/token jars |
|
|
130
|
+
| `syshttp.cookies` | Netscape cookie files and curl's cookie rules |
|
|
131
|
+
| `syshttp.hooks` | URL hooks declared in the environment |
|
|
132
|
+
| `syshttp.tls` | the OS verifier as an `SSLContext` |
|
|
133
|
+
| `syshttp.autopatch` | requests, urllib, httpx, aiohttp and pip patched on import |
|
|
134
|
+
| `syshttp.retry` | the transient statuses, `Retry-After` |
|
|
135
|
+
|
|
136
|
+
## Settings
|
|
137
|
+
|
|
138
|
+
Every setting is read as `<PREFIX><NAME>` for each prefix of
|
|
139
|
+
`SYSHTTP_PREFIX` (`:`-separated; default none, the names as written). The
|
|
140
|
+
full list is on the docs site:
|
|
141
|
+
<https://jose-pr.github.io/syshttp/settings/>.
|
|
142
|
+
|
|
143
|
+
| Variable | Sets |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `SYSHTTP_PREFIX` | the prefixes every other setting is read under |
|
|
146
|
+
| `SYSHTTP_PROXY` | the proxy URL; else `HTTPS_PROXY`, `HTTP_PROXY`, `ALL_PROXY`, else the system's |
|
|
147
|
+
| `SYSHTTP_SYSTEM_PROXY` | `0` to stop consulting the system's proxy configuration |
|
|
148
|
+
| `SYSHTTP_PROXY_AUTH` | the proxy credential, sent verbatim; else `HTTP_PROXY_AUTH` |
|
|
149
|
+
| `SYSHTTP_PROXY_AUTH_HEADER` | the header it is sent in (default `Proxy-Authorization`) |
|
|
150
|
+
| `SYSHTTP_PROXY_TYPE` | `connect` (default) or `prefix[:/endpoint]` |
|
|
151
|
+
| `SYSHTTP_HOME` | the jar directory (default: the per-user state directory) |
|
|
152
|
+
| `SYSHTTP_IMPORTPATCH` | `1`, or `:`-separated names: the client libraries patched on import |
|
|
153
|
+
| `SYSHTTP_HOOKS_<KEY>` | a URL hook's pattern, with `_PY` / `_CURL` targets |
|
|
154
|
+
| `SYSHTTP_DEFAULT_TRANSPORT` | `module:attr` or `<file>.py:attr`: the transport of a client given none (it names code to import) |
|
|
155
|
+
| `CA_BUNDLE` | the CA bundle a client verifies with (default: the OS verifier) |
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
dotagents pyvenv # or any Python 3.9+ virtual environment
|
|
161
|
+
.pyvenv/<tag>/Scripts/python -m pip install -e ".[dev]"
|
|
162
|
+
.pyvenv/<tag>/Scripts/python -m pytest -q
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Releasing
|
|
166
|
+
|
|
167
|
+
This project follows [Semantic Versioning](https://semver.org/) and keeps a
|
|
168
|
+
[`CHANGELOG.md`](https://github.com/jose-pr/syshttp/blob/main/CHANGELOG.md).
|
|
169
|
+
Pushing a tag matching `v*` triggers the release workflow: test gate → build
|
|
170
|
+
(checking the tag names the version built) → a strict docs build as a gate →
|
|
171
|
+
GitHub release → publish. The release workflow never deploys the docs site
|
|
172
|
+
itself: for a final release its last job dispatches the docs workflow at the
|
|
173
|
+
tag, which owns every Pages deploy.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT — see [LICENSE](https://github.com/jose-pr/syshttp/blob/main/LICENSE).
|
syshttp/__init__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""syshttp -- HTTP plumbing that follows the system, and an HTTP client on the
|
|
2
|
+
standard library that uses it.
|
|
3
|
+
|
|
4
|
+
Public surface (the package's AGENTS.md documents each name):
|
|
5
|
+
env.HTTPEnv — where settings come from: <PREFIX><NAME> for each
|
|
6
|
+
prefix of SYSHTTP_PREFIX (default: none)
|
|
7
|
+
proxy.proxy_map — the routing as a proxylib ProxyMap: SYSHTTP_PROXY with
|
|
8
|
+
its credential, else the *_PROXY variables or the
|
|
9
|
+
system's configuration, NO_PROXY first (-x / -U /
|
|
10
|
+
--noproxy overrides included)
|
|
11
|
+
proxy.configured_proxy / proxy_type — SYSHTTP_PROXY as a proxylib Proxy; SYSHTTP_PROXY_TYPE
|
|
12
|
+
(connect | prefix[:/endpoint])
|
|
13
|
+
client.HTTPClient / fetch — the client: proxy map, system TLS, cookies, tokens,
|
|
14
|
+
hooks, retries; replaceable transport. ``fetch`` is
|
|
15
|
+
also ``syshttp.fetch``
|
|
16
|
+
cookies.load_netscape / save_netscape / cookie_applies / set_cookie_specs
|
|
17
|
+
— Netscape jars and curl's cookie rules
|
|
18
|
+
tls.os_ssl_context / new_os_context — the system's TLS client (sslcontext) as an SSLContext
|
|
19
|
+
retry.TRANSIENT_STATUSES / retry_after — what counts as transient, Retry-After
|
|
20
|
+
jar.FileCookieJar / FileTokenJar — file-backed jars under SYSHTTP_HOME
|
|
21
|
+
hooks — SYSHTTP_HOOKS_<KEY> URL hooks (a curl wrapper,
|
|
22
|
+
a syshttp callable) matched on the caller's URL
|
|
23
|
+
autopatch — SYSHTTP_IMPORTPATCH: requests, urllib, httpx, aiohttp,
|
|
24
|
+
pip patched on import to route and verify the same way
|
|
25
|
+
|
|
26
|
+
Nothing here imports ``requests``: only ``autopatch`` patches it, in a program
|
|
27
|
+
that imports it. Importing ``syshttp`` itself loads neither proxylib nor the
|
|
28
|
+
client.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from .env import HTTPEnv
|
|
35
|
+
from .exceptions import ConfigurationError, HookError, ResponseError, SysHTTPError
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"ConfigurationError",
|
|
39
|
+
"HTTPEnv",
|
|
40
|
+
"HookError",
|
|
41
|
+
"NoReachableProxyError",
|
|
42
|
+
"ResponseError",
|
|
43
|
+
"SysHTTPError",
|
|
44
|
+
"configured_proxy",
|
|
45
|
+
"fetch",
|
|
46
|
+
"proxy_map",
|
|
47
|
+
"proxy_type",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Read from syshttp.proxy when first asked for: importing syshttp must not
|
|
51
|
+
# import proxylib, whose own import of urllib.request runs syshttp's
|
|
52
|
+
# import-time patch (syshttp.autopatch) while proxylib is half built.
|
|
53
|
+
_FROM_PROXY = ("NoReachableProxyError", "configured_proxy", "proxy_map", "proxy_type")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def __getattr__(name: str) -> Any:
|
|
57
|
+
if name in _FROM_PROXY:
|
|
58
|
+
from . import proxy
|
|
59
|
+
|
|
60
|
+
return getattr(proxy, name)
|
|
61
|
+
if name == "fetch":
|
|
62
|
+
from .client import fetch
|
|
63
|
+
|
|
64
|
+
return fetch
|
|
65
|
+
raise AttributeError("module %r has no attribute %r" % (__name__, name))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
__version__ = version("syshttp")
|
|
70
|
+
except PackageNotFoundError: # a source tree on sys.path, not installed
|
|
71
|
+
__version__ = "0+unknown"
|
syshttp/_fileio.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Writing files that hold secrets: owner-only and replaced whole."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
#: ``os.replace`` onto a file another handle has open fails with
|
|
11
|
+
#: ``PermissionError`` on Windows until that handle closes, which a reader
|
|
12
|
+
#: does within microseconds.
|
|
13
|
+
_REPLACE_TRIES = 10
|
|
14
|
+
_REPLACE_WAIT = 0.01
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _make_directories(directory: Path) -> None:
|
|
18
|
+
"""Create ``directory`` and its missing parents with mode ``0o700``; a
|
|
19
|
+
directory that exists is left as it is."""
|
|
20
|
+
missing = []
|
|
21
|
+
while not directory.exists() and directory != directory.parent:
|
|
22
|
+
missing.append(directory)
|
|
23
|
+
directory = directory.parent
|
|
24
|
+
for created in reversed(missing):
|
|
25
|
+
try:
|
|
26
|
+
created.mkdir(mode=0o700)
|
|
27
|
+
except FileExistsError:
|
|
28
|
+
continue
|
|
29
|
+
try:
|
|
30
|
+
created.chmod(0o700)
|
|
31
|
+
except OSError: # a filesystem without modes
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def read_text(path: Path) -> str:
|
|
36
|
+
"""The UTF-8 text of ``path`` (undecodable bytes dropped). On Windows a
|
|
37
|
+
file being replaced refuses to open for an instant: that is retried."""
|
|
38
|
+
for attempt in range(_REPLACE_TRIES):
|
|
39
|
+
try:
|
|
40
|
+
return Path(path).read_text(encoding="utf-8", errors="ignore")
|
|
41
|
+
except PermissionError:
|
|
42
|
+
if attempt == _REPLACE_TRIES - 1:
|
|
43
|
+
raise
|
|
44
|
+
time.sleep(_REPLACE_WAIT)
|
|
45
|
+
raise AssertionError("unreachable") # pragma: no cover
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _replace(source: str, target: Path) -> None:
|
|
49
|
+
for attempt in range(_REPLACE_TRIES):
|
|
50
|
+
try:
|
|
51
|
+
os.replace(source, target)
|
|
52
|
+
return
|
|
53
|
+
except PermissionError:
|
|
54
|
+
if attempt == _REPLACE_TRIES - 1:
|
|
55
|
+
raise
|
|
56
|
+
time.sleep(_REPLACE_WAIT)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def write_private(path: Path, data: bytes) -> None:
|
|
60
|
+
"""Write ``data`` to ``path`` so that a reader sees the old file or the
|
|
61
|
+
new one, never part of either, and only the owner can read it (``0o600``;
|
|
62
|
+
missing parent directories are ``0o700``). The bytes are written as
|
|
63
|
+
given: no newline translation."""
|
|
64
|
+
path = Path(path)
|
|
65
|
+
_make_directories(path.parent)
|
|
66
|
+
fd, temporary = tempfile.mkstemp(dir=str(path.parent), prefix=path.name + ".", suffix=".tmp")
|
|
67
|
+
try:
|
|
68
|
+
with os.fdopen(fd, "wb") as handle:
|
|
69
|
+
handle.write(data)
|
|
70
|
+
handle.flush()
|
|
71
|
+
os.fsync(handle.fileno())
|
|
72
|
+
_replace(temporary, path)
|
|
73
|
+
except BaseException:
|
|
74
|
+
try:
|
|
75
|
+
os.unlink(temporary)
|
|
76
|
+
except OSError:
|
|
77
|
+
pass
|
|
78
|
+
raise
|
syshttp/_parse.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Readers for the numbers and dates that headers and cookie files carry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import email.utils
|
|
6
|
+
from datetime import timezone
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ascii_int(value: str) -> Optional[int]:
|
|
11
|
+
"""``value`` as a non-negative integer when it is only the digits
|
|
12
|
+
``0-9`` (``str.isdigit`` also accepts ``"²"``); else ``None``."""
|
|
13
|
+
value = value.strip()
|
|
14
|
+
return int(value) if value.isascii() and value.isdigit() else None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def http_date(value: str) -> Optional[float]:
|
|
18
|
+
"""An HTTP date (IMF-fixdate, RFC 850 or asctime) as a unix time, or
|
|
19
|
+
``None`` when unreadable. A date without a zone is UTC (RFC 9110
|
|
20
|
+
section 5.6.7)."""
|
|
21
|
+
try:
|
|
22
|
+
when = email.utils.parsedate_to_datetime(value)
|
|
23
|
+
except (TypeError, ValueError, IndexError, OverflowError):
|
|
24
|
+
return None
|
|
25
|
+
if when.tzinfo is None:
|
|
26
|
+
when = when.replace(tzinfo=timezone.utc)
|
|
27
|
+
try:
|
|
28
|
+
return when.timestamp()
|
|
29
|
+
except (ValueError, OverflowError, OSError):
|
|
30
|
+
return None
|
syshttp/auth.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Protocol
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING: # the client imports this module: types only
|
|
6
|
+
from .client import HTTPClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = ("AuthProvider",)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthProvider(Protocol):
|
|
13
|
+
"""Something that makes ``client`` authenticated for ``url`` (a login,
|
|
14
|
+
a token refresh): ``HTTPClient`` calls it before each request, and
|
|
15
|
+
``fetch(reauth=...)`` again after a 401 or 403."""
|
|
16
|
+
|
|
17
|
+
def ensure(self, client: "HTTPClient", url: str) -> None: ...
|