chore: import upstream snapshot with attribution
CI / test (3.10) (push) Failing after 1s
CI / test (3.12) (push) Failing after 0s
CI / skillgen-check (push) Failing after 0s
CI / security-scan (push) Failing after 0s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:09:14 +08:00
commit d88fd01084
727 changed files with 235247 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
"""
Authentication handlers.
Auth objects are callables that modify a request before it is sent.
DigestAuth is the most interesting: it participates in a full request/response cycle,
reading the 401 response to build the challenge before re-sending.
"""
import hashlib
import time
from models import Request, Response
class Auth:
"""Base class for all authentication handlers."""
def auth_flow(self, request: Request):
"""Modify the request. May yield to inspect the response."""
raise NotImplementedError
class BasicAuth(Auth):
"""HTTP Basic Authentication."""
def __init__(self, username: str, password: str):
self.username = username
self.password = password
def auth_flow(self, request: Request):
import base64
credentials = f"{self.username}:{self.password}".encode()
encoded = base64.b64encode(credentials).decode()
request.headers["Authorization"] = f"Basic {encoded}"
yield request
class BearerAuth(Auth):
"""Bearer token authentication."""
def __init__(self, token: str):
self.token = token
def auth_flow(self, request: Request):
request.headers["Authorization"] = f"Bearer {self.token}"
yield request
class DigestAuth(Auth):
"""
HTTP Digest Authentication.
Requires a full request/response cycle: sends the initial request,
reads the 401 WWW-Authenticate header, then re-sends with credentials.
This is the only auth handler that reads from Response.
"""
def __init__(self, username: str, password: str):
self.username = username
self.password = password
self._nonce_count = 0
def auth_flow(self, request: Request):
yield request # first attempt, no credentials
# This handler must inspect the Response to continue
response = yield
if response.status_code == 401:
challenge = self._parse_challenge(response)
credentials = self._build_credentials(request, challenge)
request.headers["Authorization"] = credentials
yield request
def _parse_challenge(self, response: Response) -> dict:
"""Extract digest parameters from the WWW-Authenticate header."""
header = response.headers.get("www-authenticate", "")
params = {}
for part in header.replace("Digest ", "").split(","):
if "=" in part:
key, _, value = part.strip().partition("=")
params[key.strip()] = value.strip().strip('"')
return params
def _build_credentials(self, request: Request, challenge: dict) -> str:
"""Compute the Authorization header value for a digest challenge."""
self._nonce_count += 1
nc = f"{self._nonce_count:08x}"
cnonce = hashlib.md5(str(time.time()).encode()).hexdigest()[:8]
realm = challenge.get("realm", "")
nonce = challenge.get("nonce", "")
ha1 = hashlib.md5(f"{self.username}:{realm}:{self.password}".encode()).hexdigest()
ha2 = hashlib.md5(f"{request.method}:{request.url.path}".encode()).hexdigest()
response_hash = hashlib.md5(f"{ha1}:{nonce}:{nc}:{cnonce}:auth:{ha2}".encode()).hexdigest()
return (
f'Digest username="{self.username}", realm="{realm}", '
f'nonce="{nonce}", uri="{request.url.path}", '
f'nc={nc}, cnonce="{cnonce}", response="{response_hash}"'
)
class NetRCAuth(Auth):
"""Load credentials from ~/.netrc based on the request host."""
def auth_flow(self, request: Request):
import netrc
try:
credentials = netrc.netrc().authenticators(request.url.host)
if credentials:
username, _, password = credentials
basic = BasicAuth(username, password)
yield from basic.auth_flow(request)
return
except Exception:
pass
yield request
+161
View File
@@ -0,0 +1,161 @@
"""
The main Client and AsyncClient classes.
BaseClient holds all shared logic. Client and AsyncClient extend it for sync/async.
This is the integration hub of the library - it imports from every other module.
"""
from models import Request, Response, URL, Headers, Cookies
from auth import Auth, BasicAuth
from transport import BaseTransport, HTTPTransport, AsyncHTTPTransport
from exceptions import TooManyRedirects, InvalidURL
from utils import build_url_with_params, obfuscate_sensitive_headers
DEFAULT_MAX_REDIRECTS = 20
class Timeout:
def __init__(self, timeout=5.0, *, connect=None, read=None, write=None, pool=None):
self.connect = connect or timeout
self.read = read or timeout
self.write = write or timeout
self.pool = pool or timeout
class Limits:
def __init__(self, max_connections=100, max_keepalive_connections=20, keepalive_expiry=5.0):
self.max_connections = max_connections
self.max_keepalive_connections = max_keepalive_connections
self.keepalive_expiry = keepalive_expiry
class BaseClient:
"""
Shared implementation for Client and AsyncClient.
Handles auth, redirects, cookies, and header defaults.
"""
def __init__(
self,
*,
auth=None,
headers=None,
cookies=None,
timeout=Timeout(),
max_redirects=DEFAULT_MAX_REDIRECTS,
base_url="",
):
self._auth = auth
self._headers = Headers(headers or {})
self._cookies = Cookies(cookies or {})
self._timeout = timeout
self._max_redirects = max_redirects
self._base_url = URL(base_url) if base_url else None
def _build_request(self, method: str, url: str, **kwargs) -> Request:
if self._base_url:
url = self._base_url.raw.rstrip("/") + "/" + url.lstrip("/")
if kwargs.get("params"):
url = build_url_with_params(url, kwargs.pop("params"))
headers = Headers(kwargs.get("headers", {}))
for k, v in self._headers.items():
if k not in headers:
headers[k] = v
return Request(method, url, headers=headers, content=kwargs.get("content"), cookies=self._cookies)
def _merge_cookies(self, response: Response) -> None:
for name, value in response.cookies.items():
self._cookies.set(name, value)
class Client(BaseClient):
"""Synchronous HTTP client."""
def __init__(self, *, transport: BaseTransport = None, **kwargs):
super().__init__(**kwargs)
self._transport = transport or HTTPTransport()
def request(self, method: str, url: str, **kwargs) -> Response:
request = self._build_request(method, url, **kwargs)
auth = kwargs.get("auth") or self._auth
if auth:
flow = auth.auth_flow(request)
request = next(flow)
response = self._transport.handle_request(request)
self._merge_cookies(response)
if auth:
try:
flow.send(response)
except StopIteration:
pass
return response
def get(self, url: str, **kwargs) -> Response:
return self.request("GET", url, **kwargs)
def post(self, url: str, **kwargs) -> Response:
return self.request("POST", url, **kwargs)
def put(self, url: str, **kwargs) -> Response:
return self.request("PUT", url, **kwargs)
def patch(self, url: str, **kwargs) -> Response:
return self.request("PATCH", url, **kwargs)
def delete(self, url: str, **kwargs) -> Response:
return self.request("DELETE", url, **kwargs)
def head(self, url: str, **kwargs) -> Response:
return self.request("HEAD", url, **kwargs)
def send(self, request: Request) -> Response:
return self._transport.handle_request(request)
def close(self) -> None:
self._transport.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
class AsyncClient(BaseClient):
"""Asynchronous HTTP client."""
def __init__(self, *, transport=None, **kwargs):
super().__init__(**kwargs)
self._transport = transport or AsyncHTTPTransport()
async def request(self, method: str, url: str, **kwargs) -> Response:
request = self._build_request(method, url, **kwargs)
response = await self._transport.handle_async_request(request)
self._merge_cookies(response)
return response
async def get(self, url: str, **kwargs) -> Response:
return await self.request("GET", url, **kwargs)
async def post(self, url: str, **kwargs) -> Response:
return await self.request("POST", url, **kwargs)
async def put(self, url: str, **kwargs) -> Response:
return await self.request("PUT", url, **kwargs)
async def patch(self, url: str, **kwargs) -> Response:
return await self.request("PATCH", url, **kwargs)
async def delete(self, url: str, **kwargs) -> Response:
return await self.request("DELETE", url, **kwargs)
async def send(self, request: Request) -> Response:
return await self._transport.handle_async_request(request)
async def aclose(self) -> None:
await self._transport.aclose()
async def __aenter__(self):
return self
async def __aexit__(self, *args):
await self.aclose()
+90
View File
@@ -0,0 +1,90 @@
"""
httpx-like exception hierarchy.
All exceptions inherit from HTTPError at the top.
"""
class HTTPError(Exception):
"""Base class for all httpx exceptions."""
def __init__(self, message, *, request=None):
self.request = request
super().__init__(message)
class RequestError(HTTPError):
"""An error occurred while issuing a request."""
class TransportError(RequestError):
"""An error occurred at the transport layer."""
class TimeoutException(TransportError):
"""A timeout occurred."""
class ConnectTimeout(TimeoutException):
"""Timed out while connecting to the host."""
class ReadTimeout(TimeoutException):
"""Timed out while receiving data from the host."""
class WriteTimeout(TimeoutException):
"""Timed out while sending data to the host."""
class PoolTimeout(TimeoutException):
"""Timed out waiting to acquire a connection from the pool."""
class NetworkError(TransportError):
"""A network error occurred."""
class ConnectError(NetworkError):
"""Failed to establish a connection."""
class ReadError(NetworkError):
"""Failed to receive data from the network."""
class WriteError(NetworkError):
"""Failed to send data through the network."""
class CloseError(NetworkError):
"""Failed to close a connection."""
class ProxyError(TransportError):
"""An error occurred while establishing a proxy connection."""
class ProtocolError(TransportError):
"""A protocol was violated."""
class DecodingError(RequestError):
"""Decoding of the response failed."""
class TooManyRedirects(RequestError):
"""Too many redirects."""
class HTTPStatusError(HTTPError):
"""A 4xx or 5xx response was received."""
def __init__(self, message, *, request, response):
self.response = response
super().__init__(message, request=request)
class InvalidURL(Exception):
"""URL is improperly formed or cannot be parsed."""
class CookieConflict(Exception):
"""Attempted to look up a cookie by name but multiple cookies exist."""
+120
View File
@@ -0,0 +1,120 @@
"""
Core data models: URL, Headers, Cookies, Request, Response.
These are the central data types that everything else in the library references.
"""
import json as _json
from exceptions import HTTPStatusError
class URL:
def __init__(self, url: str):
self.raw = url
self.scheme, _, rest = url.partition("://")
self.host, _, self.path = rest.partition("/")
self.path = "/" + self.path
def copy_with(self, **kwargs) -> "URL":
return URL(kwargs.get("url", self.raw))
def __str__(self):
return self.raw
def __repr__(self):
return f"URL({self.raw!r})"
class Headers:
def __init__(self, headers=None):
self._store = {}
for k, v in (headers or {}).items():
self._store[k.lower()] = v
def get(self, key: str, default=None):
return self._store.get(key.lower(), default)
def items(self):
return self._store.items()
def __setitem__(self, key, value):
self._store[key.lower()] = value
def __getitem__(self, key):
return self._store[key.lower()]
def __contains__(self, key):
return key.lower() in self._store
class Cookies:
def __init__(self, cookies=None):
self._jar = dict(cookies or {})
def set(self, name: str, value: str, domain: str = "") -> None:
self._jar[name] = value
def get(self, name: str, default=None):
return self._jar.get(name, default)
def delete(self, name: str) -> None:
self._jar.pop(name, None)
def clear(self) -> None:
self._jar.clear()
def items(self):
return self._jar.items()
class Request:
def __init__(self, method: str, url, *, headers=None, content=None, cookies=None):
self.method = method.upper()
self.url = URL(url) if isinstance(url, str) else url
self.headers = Headers(headers)
self.content = content or b""
self.cookies = Cookies(cookies)
def __repr__(self):
return f"<Request [{self.method}]>"
class Response:
def __init__(self, status_code: int, *, headers=None, content=None, request=None):
self.status_code = status_code
self.headers = Headers(headers)
self.content = content or b""
self.request = request
@property
def text(self) -> str:
return self.content.decode("utf-8", errors="replace")
def json(self):
return _json.loads(self.content)
def read(self) -> bytes:
return self.content
@property
def is_success(self) -> bool:
return 200 <= self.status_code < 300
@property
def is_error(self) -> bool:
return self.status_code >= 400
def raise_for_status(self) -> None:
if self.is_error:
message = f"{self.status_code} Error"
raise HTTPStatusError(message, request=self.request, response=self)
@property
def cookies(self) -> Cookies:
jar = Cookies()
for header in self.headers.get("set-cookie", "").split(","):
if "=" in header:
name, _, value = header.strip().partition("=")
jar.set(name.strip(), value.split(";")[0].strip())
return jar
def __repr__(self):
return f"<Response [{self.status_code}]>"
+135
View File
@@ -0,0 +1,135 @@
"""
Transport layer: connection management and low-level HTTP sending.
HTTPTransport wraps a connection pool. ProxyTransport sits in front of it.
MockTransport is used in tests.
"""
from models import Request, Response
from exceptions import TransportError, ConnectError, TimeoutException
class BaseTransport:
"""Sync transport interface."""
def handle_request(self, request: Request) -> Response:
raise NotImplementedError
def close(self) -> None:
pass
class AsyncBaseTransport:
"""Async transport interface."""
async def handle_async_request(self, request: Request) -> Response:
raise NotImplementedError
async def aclose(self) -> None:
pass
class ConnectionPool:
"""
Manages a pool of persistent HTTP connections.
Keys connections by (scheme, host, port).
"""
def __init__(self, max_connections=100, max_keepalive_connections=20):
self.max_connections = max_connections
self.max_keepalive_connections = max_keepalive_connections
self._pool = {}
def _get_connection_key(self, request: Request) -> tuple:
url = request.url
port = 443 if url.scheme == "https" else 80
return (url.scheme, url.host, port)
def get_connection(self, request: Request):
key = self._get_connection_key(request)
return self._pool.get(key)
def return_connection(self, request: Request, conn) -> None:
key = self._get_connection_key(request)
if len(self._pool) < self.max_keepalive_connections:
self._pool[key] = conn
def close(self) -> None:
self._pool.clear()
class HTTPTransport(BaseTransport):
"""
The main sync HTTP transport.
Uses a ConnectionPool for connection reuse.
"""
def __init__(self, verify=True, cert=None, limits=None):
self.verify = verify
self.cert = cert
self._pool = ConnectionPool()
def handle_request(self, request: Request) -> Response:
conn = self._pool.get_connection(request)
try:
response = self._send(request, conn)
self._pool.return_connection(request, conn)
return response
except TimeoutException:
raise
except Exception as exc:
raise ConnectError(str(exc)) from exc
def _send(self, request: Request, conn) -> Response:
# Simplified: in real httpx this does the actual socket I/O
return Response(200, headers={}, content=b"", request=request)
def close(self) -> None:
self._pool.close()
class AsyncHTTPTransport(AsyncBaseTransport):
"""The async variant of HTTPTransport."""
def __init__(self, verify=True, cert=None):
self.verify = verify
self.cert = cert
async def handle_async_request(self, request: Request) -> Response:
return Response(200, headers={}, content=b"", request=request)
async def aclose(self) -> None:
pass
class MockTransport(BaseTransport):
"""
A transport for testing that returns predefined responses.
Pass a handler function that receives a Request and returns a Response.
"""
def __init__(self, handler):
self.handler = handler
def handle_request(self, request: Request) -> Response:
return self.handler(request)
class ProxyTransport(BaseTransport):
"""
Routes requests through an HTTP/HTTPS proxy.
Wraps an inner transport and prepends proxy connection handling.
"""
def __init__(self, proxy_url: str, *, inner: BaseTransport = None):
self.proxy_url = proxy_url
self._inner = inner or HTTPTransport()
def handle_request(self, request: Request) -> Response:
try:
return self._inner.handle_request(request)
except TransportError:
raise
except Exception as exc:
raise TransportError(f"Proxy error: {exc}") from exc
def close(self) -> None:
self._inner.close()
+85
View File
@@ -0,0 +1,85 @@
"""
Utility functions shared across the library.
Small helpers that don't belong in any one module.
"""
import re
from models import Cookies
SENSITIVE_HEADERS = {"authorization", "cookie", "set-cookie", "proxy-authorization"}
def primitive_value_to_str(value) -> str:
"""Convert a primitive value to its string representation."""
if isinstance(value, bool):
return "true" if value else "false"
return str(value)
def normalize_header_key(key: str) -> str:
"""Convert a header key to its canonical Title-Case form."""
return "-".join(word.capitalize() for word in key.split("-"))
def flatten_queryparams(params: dict) -> list:
"""
Expand a params dict into a flat list of (key, value) pairs.
List values become multiple pairs with the same key.
"""
result = []
for key, value in params.items():
if isinstance(value, list):
for item in value:
result.append((key, primitive_value_to_str(item)))
else:
result.append((key, primitive_value_to_str(value)))
return result
def parse_content_type(content_type: str) -> tuple:
"""
Parse a Content-Type header value.
Returns (media_type, params_dict).
Example: 'application/json; charset=utf-8' -> ('application/json', {'charset': 'utf-8'})
"""
parts = [p.strip() for p in content_type.split(";")]
media_type = parts[0]
params = {}
for part in parts[1:]:
if "=" in part:
key, _, value = part.partition("=")
params[key.strip()] = value.strip()
return media_type, params
def obfuscate_sensitive_headers(headers: dict) -> dict:
"""Return a copy of headers with sensitive values replaced by [obfuscated]."""
return {
k: "[obfuscated]" if k.lower() in SENSITIVE_HEADERS else v
for k, v in headers.items()
}
def unset_all_cookies(cookies: Cookies) -> None:
"""Clear all cookies from a cookie jar in place."""
cookies.clear()
def is_known_encoding(encoding: str) -> bool:
"""Check if a character encoding label is recognized by Python's codec system."""
import codecs
try:
codecs.lookup(encoding)
return True
except LookupError:
return False
def build_url_with_params(base_url: str, params: dict) -> str:
"""Append query parameters to a URL string."""
if not params:
return base_url
pairs = flatten_queryparams(params)
query = "&".join(f"{k}={v}" for k, v in pairs)
separator = "&" if "?" in base_url else "?"
return f"{base_url}{separator}{query}"