fasthttpc 0.6.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.
Files changed (70) hide show
  1. fasthttpc/__init__.py +102 -0
  2. fasthttpc/_api.py +272 -0
  3. fasthttpc/_backend.py +589 -0
  4. fasthttpc/_log.py +40 -0
  5. fasthttpc/async_session.py +1724 -0
  6. fasthttpc/auth.py +328 -0
  7. fasthttpc/buffer.py +137 -0
  8. fasthttpc/cache.py +151 -0
  9. fasthttpc/config.py +207 -0
  10. fasthttpc/cookies.py +186 -0
  11. fasthttpc/crypto_math.py +202 -0
  12. fasthttpc/fingerprint.py +768 -0
  13. fasthttpc/http3/__init__.py +18 -0
  14. fasthttpc/http3/altsvc.py +115 -0
  15. fasthttpc/http3/async_client.py +234 -0
  16. fasthttpc/http3/core.py +276 -0
  17. fasthttpc/http3/fingerprint.py +95 -0
  18. fasthttpc/http3/manager.py +199 -0
  19. fasthttpc/http3/options.py +37 -0
  20. fasthttpc/http3/sync_client.py +174 -0
  21. fasthttpc/http3/verify.py +21 -0
  22. fasthttpc/metrics.py +108 -0
  23. fasthttpc/models/__init__.py +0 -0
  24. fasthttpc/models/headers.py +109 -0
  25. fasthttpc/models/request.py +100 -0
  26. fasthttpc/models/response.py +558 -0
  27. fasthttpc/models/urls.py +104 -0
  28. fasthttpc/multipart.py +119 -0
  29. fasthttpc/plugins.py +146 -0
  30. fasthttpc/protocol/__init__.py +0 -0
  31. fasthttpc/protocol/async_h2.py +774 -0
  32. fasthttpc/protocol/dns_over_https.py +202 -0
  33. fasthttpc/protocol/http1_parser.py +190 -0
  34. fasthttpc/protocol/http2_parser.py +1753 -0
  35. fasthttpc/py.typed +0 -0
  36. fasthttpc/retry.py +156 -0
  37. fasthttpc/security/__init__.py +0 -0
  38. fasthttpc/security/alerts.py +39 -0
  39. fasthttpc/security/cert_validator.py +441 -0
  40. fasthttpc/security/client_cert.py +344 -0
  41. fasthttpc/security/keyshare.py +265 -0
  42. fasthttpc/security/mtls.py +282 -0
  43. fasthttpc/security/session_tickets.py +466 -0
  44. fasthttpc/security/tls12.py +1435 -0
  45. fasthttpc/security/tls12_legacy.py +299 -0
  46. fasthttpc/security/tls_adapter.py +1111 -0
  47. fasthttpc/session.py +1372 -0
  48. fasthttpc/streaming.py +218 -0
  49. fasthttpc/timeout.py +9 -0
  50. fasthttpc/tls13/__init__.py +26 -0
  51. fasthttpc/tls13/_compat.py +216 -0
  52. fasthttpc/tls13/_records.py +35 -0
  53. fasthttpc/tls13/async_stream.py +173 -0
  54. fasthttpc/tls13/connection.py +833 -0
  55. fasthttpc/tls13/errors.py +22 -0
  56. fasthttpc/tls13/events.py +57 -0
  57. fasthttpc/tls13/sync_socket.py +102 -0
  58. fasthttpc/tls_crafter.py +523 -0
  59. fasthttpc/tls_tools.py +623 -0
  60. fasthttpc/transport/__init__.py +0 -0
  61. fasthttpc/transport/connection_pool.py +366 -0
  62. fasthttpc/transport/happy_eyeballs.py +144 -0
  63. fasthttpc/transport/lingering.py +50 -0
  64. fasthttpc/transport/mock.py +83 -0
  65. fasthttpc/transport/proxy.py +410 -0
  66. fasthttpc-0.6.0.dist-info/METADATA +232 -0
  67. fasthttpc-0.6.0.dist-info/RECORD +70 -0
  68. fasthttpc-0.6.0.dist-info/WHEEL +5 -0
  69. fasthttpc-0.6.0.dist-info/licenses/LICENSE +21 -0
  70. fasthttpc-0.6.0.dist-info/top_level.txt +1 -0
fasthttpc/__init__.py ADDED
@@ -0,0 +1,102 @@
1
+ """
2
+ fasthttpc — Pure-Python HTTP istemcisi
3
+
4
+ TLS 1.3 fingerprinting, Chrome/Firefox/Safari impersonation,
5
+ async destek, akıllı connection pool ve plugin sistemi içerir.
6
+
7
+ Hızlı başlangıç::
8
+
9
+ from fasthttpc import Session, Timeout
10
+
11
+ with Session(impersonate="chrome_131") as s:
12
+ r = s.get("https://httpbin.org/get")
13
+ print(r.status_code, r.elapsed)
14
+
15
+ Async::
16
+
17
+ from fasthttpc import AsyncSession
18
+
19
+ async with AsyncSession() as s:
20
+ r = await s.get("https://httpbin.org/get")
21
+ """
22
+
23
+ # `cryptography` alt modülleri import edilmeden ÖNCE ayarlanmalı. Bazı
24
+ # ortamlarda (özellikle Pydroid3/Android'deki OpenSSL 3.0) legacy provider
25
+ # yüklenemez ve import ölümcül bir RuntimeError ile patlar. fasthttpc yalnızca
26
+ # modern algoritmalar kullanır (AES-GCM, ChaCha20-Poly1305, x25519, ECDSA,
27
+ # RSA-PSS, Ed25519) — hiçbiri legacy provider'a ihtiyaç duymaz, dolayısıyla
28
+ # onu devre dışı bırakmak güvenli ve gereklidir.
29
+ import os as _os
30
+ _os.environ.setdefault("CRYPTOGRAPHY_OPENSSL_NO_LEGACY", "1")
31
+
32
+ from .session import Session
33
+ from .async_session import AsyncSession
34
+ from .config import Timeout, Limits
35
+ from .retry import Retry
36
+ from .transport.mock import MockTransport, BaseTransport
37
+ from .multipart import encode_multipart
38
+ from .models.response import Response, HTTPStatusError
39
+ from .models.request import Request
40
+ from .models.headers import Headers
41
+ from .models.urls import URL
42
+ from .cache import ResponseCache
43
+ from .cookies import CookieJar, Cookie
44
+ from ._log import enable_debug_logging
45
+ from .protocol.dns_over_https import DoHResolver
46
+ from .fingerprint import (
47
+ get_profile,
48
+ available_profiles,
49
+ ja3_from_client_hello,
50
+ ja3_for_profile,
51
+ )
52
+ from .metrics import RequestMetrics
53
+ from .tls_tools import (
54
+ TLSFragmenter,
55
+ TLSDowngradeDetector,
56
+ ClientHelloEntropyScorer,
57
+ )
58
+ from .async_session import ProxyRotator
59
+ from .plugins import Plugin, RetryPlugin, LoggingPlugin, RateLimitPlugin
60
+ from .auth import Auth, BasicAuth, BearerAuth, DigestAuth
61
+ from ._api import USE_CLIENT_DEFAULT
62
+
63
+ __version__ = "0.6.0"
64
+ __author__ = "Buğra"
65
+ __all__ = [
66
+ "Session",
67
+ "AsyncSession",
68
+ "Timeout",
69
+ "Limits",
70
+ "Retry",
71
+ "MockTransport",
72
+ "BaseTransport",
73
+ "encode_multipart",
74
+ "Response",
75
+ "HTTPStatusError",
76
+ "Request",
77
+ "Headers",
78
+ "URL",
79
+ "ResponseCache",
80
+ "CookieJar",
81
+ "Cookie",
82
+ "DoHResolver",
83
+ "enable_debug_logging",
84
+ "get_profile",
85
+ "available_profiles",
86
+ "ja3_from_client_hello",
87
+ "ja3_for_profile",
88
+ "RequestMetrics",
89
+ "TLSFragmenter",
90
+ "TLSDowngradeDetector",
91
+ "ClientHelloEntropyScorer",
92
+ "Plugin",
93
+ "RetryPlugin",
94
+ "LoggingPlugin",
95
+ "RateLimitPlugin",
96
+ "ProxyRotator",
97
+ "Auth",
98
+ "BasicAuth",
99
+ "BearerAuth",
100
+ "DigestAuth",
101
+ "USE_CLIENT_DEFAULT",
102
+ ]
fasthttpc/_api.py ADDED
@@ -0,0 +1,272 @@
1
+ """Session / AsyncSession'ın ortak, G/Ç'siz yardımcıları (requests/httpx uyumu).
2
+
3
+ params birleştirme, istek düzeyi çerez/zaman aşımı, gövde kodlama, URL'deki
4
+ ``user:pass@`` ayrıştırma ve hook sözlüklerinin normalleştirilmesi burada
5
+ durur; senkron ve asenkron oturum aynı kodu kullanır (davranış farkı olmaz).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import contextvars
10
+ from http.cookies import SimpleCookie
11
+ from typing import Any, Callable, Optional
12
+ from urllib.parse import unquote, urlencode, urlsplit
13
+
14
+ from .config import Timeout
15
+ from .multipart import encode_multipart
16
+
17
+
18
+ class _UseClientDefault:
19
+ """``auth=`` / ``timeout=`` için "oturum varsayılanını kullan" işareti (httpx ile aynı ad)."""
20
+
21
+ def __repr__(self) -> str:
22
+ return "USE_CLIENT_DEFAULT"
23
+
24
+
25
+ USE_CLIENT_DEFAULT: Any = _UseClientDefault()
26
+
27
+
28
+ # --- params -----------------------------------------------------------------
29
+
30
+ def _encode_params(params) -> str:
31
+ if params is None:
32
+ return ""
33
+ if isinstance(params, bytes):
34
+ return params.decode("latin-1")
35
+ if isinstance(params, str):
36
+ return params
37
+ items = params.items() if hasattr(params, "items") else params
38
+ flat = []
39
+ for k, v in items:
40
+ if v is None:
41
+ continue # requests gibi: None → parametre yok
42
+ if isinstance(v, (list, tuple)):
43
+ flat.extend((k, x) for x in v if x is not None)
44
+ else:
45
+ flat.append((k, v))
46
+ return urlencode([(k, _scalar(v)) for k, v in flat])
47
+
48
+
49
+ def _scalar(v):
50
+ if isinstance(v, bool):
51
+ return "true" if v else "false" # httpx ile aynı
52
+ return v
53
+
54
+
55
+ def merge_params(session_params, request_params) -> str:
56
+ """Oturum ve istek parametrelerini tek query string'e çevirir.
57
+
58
+ İkisi de sözlükse istek aynı anahtarı ezer (``None`` anahtarı kaldırır);
59
+ aksi hâlde sıralı birleştirilir.
60
+ """
61
+ if session_params is None:
62
+ return _encode_params(request_params)
63
+ if request_params is None:
64
+ return _encode_params(session_params)
65
+ if isinstance(session_params, dict) and isinstance(request_params, dict):
66
+ merged = {k: v for k, v in session_params.items()}
67
+ merged.update(request_params)
68
+ return _encode_params(merged)
69
+ return "&".join(p for p in (_encode_params(session_params),
70
+ _encode_params(request_params)) if p)
71
+
72
+
73
+ def apply_params(url: str, session_params, request_params) -> str:
74
+ """``params`` sorgusunu URL'in mevcut sorgusuna EKLER (requests gibi)."""
75
+ extra = merge_params(session_params, request_params)
76
+ if not extra:
77
+ return url
78
+ frag = ""
79
+ if "#" in url:
80
+ url, _, frag = url.partition("#")
81
+ frag = "#" + frag
82
+ sep = "&" if "?" in url else "?"
83
+ if url.endswith(("?", "&")):
84
+ sep = ""
85
+ return f"{url}{sep}{extra}{frag}"
86
+
87
+
88
+ # --- URL'deki kimlik ----------------------------------------------------------
89
+
90
+ def split_userinfo(url) -> "tuple[str, Optional[tuple[str, str]]]":
91
+ """``https://u:p@host/x`` -> (``https://host/x``, ``("u", "p")``)."""
92
+ username = getattr(url, "username", None)
93
+ if username is not None and not isinstance(url, str): # URL nesnesi
94
+ creds = (username, getattr(url, "password", "") or "")
95
+ return str(url), (creds if any(creds) else None)
96
+ url = str(url)
97
+ netloc = urlsplit(url).netloc
98
+ if "@" not in netloc:
99
+ return url, None
100
+ userinfo, _, hostport = netloc.rpartition("@")
101
+ user, _, pw = userinfo.partition(":")
102
+ clean = url.replace(netloc, hostport, 1)
103
+ creds = (unquote(user), unquote(pw))
104
+ return clean, (creds if any(creds) else None)
105
+
106
+
107
+ # --- gövde ----------------------------------------------------------------------
108
+
109
+ def _has(headers: dict, name: str) -> bool:
110
+ return any(k.lower() == name for k in headers)
111
+
112
+
113
+ def encode_body(data, json, files, headers: dict) -> bytes:
114
+ """``data/json/files`` -> bayt gövde; gerekirse Content-Type ``headers``'a eklenir."""
115
+ import json as jsonlib
116
+ if files is not None:
117
+ body, ct = encode_multipart(fields=data if isinstance(data, dict) else None,
118
+ files=files)
119
+ if not _has(headers, "content-type"):
120
+ headers["Content-Type"] = ct
121
+ return body
122
+ if json is not None:
123
+ if not _has(headers, "content-type"):
124
+ headers["Content-Type"] = "application/json"
125
+ return jsonlib.dumps(json).encode("utf-8")
126
+ if isinstance(data, (dict, list, tuple)):
127
+ if not _has(headers, "content-type"):
128
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
129
+ return urlencode(data, doseq=True).encode("ascii")
130
+ if isinstance(data, str):
131
+ return data.encode("utf-8")
132
+ if isinstance(data, (bytes, bytearray)):
133
+ return bytes(data)
134
+ return b""
135
+
136
+
137
+ # --- çerezler -------------------------------------------------------------------
138
+
139
+ def merge_cookie_header(jar_header: str, extra) -> str:
140
+ """Çerez kavanozundan gelen ``Cookie`` değerine istek çerezlerini ekler (istek kazanır)."""
141
+ if not extra:
142
+ return jar_header
143
+ pairs: dict = {}
144
+ for part in (jar_header or "").split(";"):
145
+ k, sep, v = part.strip().partition("=")
146
+ if sep:
147
+ pairs[k] = v
148
+ items = extra.items() if hasattr(extra, "items") else extra
149
+ for k, v in items:
150
+ pairs[str(k)] = str(v)
151
+ return "; ".join(f"{k}={v}" for k, v in pairs.items())
152
+
153
+
154
+ def parse_cookie_arg(cookies) -> "dict[str, str]":
155
+ """``cookies=`` (dict / (k, v) listesi / ``Cookie:`` metni) -> dict."""
156
+ if not cookies:
157
+ return {}
158
+ if isinstance(cookies, str):
159
+ sc = SimpleCookie()
160
+ sc.load(cookies)
161
+ return {k: m.value for k, m in sc.items()}
162
+ if hasattr(cookies, "as_dict"):
163
+ return dict(cookies.as_dict())
164
+ return {str(k): str(v) for k, v in (cookies.items() if hasattr(cookies, "items") else cookies)}
165
+
166
+
167
+ # --- zaman aşımı -------------------------------------------------------------------
168
+
169
+ def resolve_timeout(value, session_timeout: Timeout) -> Optional[Timeout]:
170
+ """İstek düzeyi ``timeout=`` -> Timeout; varsayılansa ``None`` (oturumunki geçerli).
171
+
172
+ float/int -> hepsi; ``(connect, read)`` ikilisi (requests); ``Timeout``;
173
+ ``None`` -> sınırsız (``Timeout(None)``).
174
+ """
175
+ if value is USE_CLIENT_DEFAULT:
176
+ return None
177
+ if isinstance(value, bool):
178
+ raise TypeError("timeout bool olamaz; saniye, (connect, read) ikilisi veya Timeout verin.")
179
+ if isinstance(value, Timeout):
180
+ return value
181
+ if value is None:
182
+ return Timeout(None)
183
+ if isinstance(value, (int, float)):
184
+ return Timeout(float(value))
185
+ if isinstance(value, (tuple, list)) and len(value) == 2:
186
+ c, r = value
187
+ return Timeout(session_timeout, connect=c, read=r)
188
+ raise TypeError(f"Geçersiz timeout değeri: {value!r}")
189
+
190
+
191
+ _TIMEOUT_OVERRIDE: "contextvars.ContextVar[Optional[tuple]]" = contextvars.ContextVar(
192
+ "fasthttpc_timeout_override", default=None)
193
+
194
+
195
+ def set_timeout_override(owner, timeout: Optional[Timeout]):
196
+ """İstek boyunca (iş parçacığı/görev başına) geçerli zaman aşımını ayarlar."""
197
+ return _TIMEOUT_OVERRIDE.set(None if timeout is None else (owner, timeout))
198
+
199
+
200
+ def reset_timeout_override(token) -> None:
201
+ _TIMEOUT_OVERRIDE.reset(token)
202
+
203
+
204
+ def effective_timeout(owner, default: Timeout) -> Timeout:
205
+ cur = _TIMEOUT_OVERRIDE.get()
206
+ return cur[1] if cur is not None and cur[0] is owner else default
207
+
208
+
209
+ # --- hook'lar -------------------------------------------------------------------
210
+
211
+ _RESPONSE_ONLY = ("response",)
212
+ _EVENT_KEYS = ("request", "response")
213
+
214
+
215
+ def _as_list(v) -> list:
216
+ if v is None:
217
+ return []
218
+ if callable(v):
219
+ return [v]
220
+ return list(v)
221
+
222
+
223
+ def normalize_hooks(hooks, allowed=_RESPONSE_ONLY, label="hooks") -> "dict[str, list]":
224
+ """``{"response": fn | [fn, ...]}`` -> ``{"response": [fn, ...]}`` (anahtarlar doğrulanır)."""
225
+ out = {k: [] for k in allowed}
226
+ for k, v in (hooks or {}).items():
227
+ if k not in allowed:
228
+ raise ValueError(f"{label}: bilinmeyen olay {k!r}; geçerli: {', '.join(allowed)}")
229
+ out[k] = _as_list(v)
230
+ return out
231
+
232
+
233
+ def normalize_event_hooks(hooks) -> "dict[str, list]":
234
+ return normalize_hooks(hooks, _EVENT_KEYS, "event_hooks")
235
+
236
+
237
+ def hook_list(mapping, key: str) -> list:
238
+ """Canlı (mutable) hook sözlüğünden ``key`` listesini okur (callable / liste kabul)."""
239
+ return _as_list((mapping or {}).get(key))
240
+
241
+
242
+ def hook_kwargs(session, timeout) -> dict:
243
+ """requests hook'larına geçirilen ``**kwargs`` (``timeout``, ``verify`` ...)."""
244
+ return {"timeout": timeout, "verify": getattr(session, "verify", True),
245
+ "proxies": None, "stream": False, "cert": None}
246
+
247
+
248
+ REDIRECT_STATUSES = (301, 302, 303, 307, 308)
249
+
250
+
251
+ def next_request(resp, method, current_url, headers, body):
252
+ """``allow_redirects=False`` iken ``r.next``: izlenecek yönlendirme isteği (ya da None)."""
253
+ from .models.request import Request
254
+ from .models.urls import URL
255
+ from .session import _resolve_location
256
+ if resp.status_code not in REDIRECT_STATUSES:
257
+ return None
258
+ location = resp.headers.get("location", "")
259
+ if not location:
260
+ return None
261
+ target = _resolve_location(URL(str(current_url)), location)
262
+ hdrs = {k: v for k, v in dict(headers or {}).items() if v is not None}
263
+ if URL(target).origin != URL(str(current_url)).origin:
264
+ hdrs = {k: v for k, v in hdrs.items()
265
+ if k.lower() not in ("authorization", "cookie", "proxy-authorization")}
266
+ if resp.status_code in (301, 302, 303):
267
+ method, body = "GET", b""
268
+ hdrs = {k: v for k, v in hdrs.items() if k.lower() not in ("content-type", "content-length")}
269
+ return Request(method, target, headers=hdrs, data=body or None)
270
+
271
+
272
+ Hook = Callable[..., Any]