Compare commits
No commits in common. "main" and "v0.1.2" have entirely different histories.
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
# claude
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
|
||||
# python
|
||||
__pycache__/
|
||||
|
||||
16
README.md
16
README.md
@ -22,17 +22,17 @@ you want; importing the package never fails because an extra is missing.
|
||||
`requirements.txt` (pick the extra you need):
|
||||
|
||||
```
|
||||
aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.3
|
||||
aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.3
|
||||
aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.3
|
||||
aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.2
|
||||
aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.2
|
||||
aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.2
|
||||
```
|
||||
|
||||
Direct:
|
||||
|
||||
```bash
|
||||
pip install "aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.3"
|
||||
pip install "aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.3"
|
||||
pip install "aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.3"
|
||||
pip install "aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.2"
|
||||
pip install "aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.2"
|
||||
pip install "aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.2"
|
||||
```
|
||||
|
||||
- `[curl]` → curl_cffi backend · `[noble]` → noble_tls backend · `[all]` → both.
|
||||
@ -44,8 +44,6 @@ pip install "aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-publi
|
||||
Constructing a backend whose client isn't installed raises that `RuntimeError` at
|
||||
construction, never at import.
|
||||
|
||||
Drop the `@v0.1.3` suffix from the line above to install the latest unpinned.
|
||||
|
||||
## curl_cffi backend
|
||||
|
||||
```python
|
||||
@ -175,4 +173,4 @@ are separate signals. Use this as one component, not a complete anti-bot solutio
|
||||
|
||||
## Versioning
|
||||
|
||||
Releases are tagged `vX.Y.Z`. The install line above pins a release; drop the `@vX.Y.Z` suffix to install the latest unpinned. Pin deliberately for reproducible installs.
|
||||
Tagged `vX.Y.Z`. Pin the tag in `requirements.txt`.
|
||||
|
||||
@ -4,11 +4,11 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "aioweb_tls"
|
||||
version = "0.1.3"
|
||||
description = "TLS-fingerprinting backends (curl_cffi / noble_tls) for aioweb via one injectable TLSSession, config-free, installable."
|
||||
version = "0.1.2"
|
||||
description = "TLS-fingerprinting backends for aioweb — curl_cffi / noble_tls ExtendedSession subclasses, config-free, installable."
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"aioweb @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb.git@v0.1.5",
|
||||
"aioweb @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb.git@v0.1.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@ -15,23 +15,10 @@ import asyncio
|
||||
import logging
|
||||
import math
|
||||
|
||||
import aiohttp
|
||||
from aioweb import Response
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _as_client_error(error: Exception, backend: str) -> aiohttp.ClientError:
|
||||
"""wrap a backend-native network exception as an aiohttp.ClientError
|
||||
|
||||
aioweb's request() only re-wraps aiohttp.ClientError; curl_cffi raises
|
||||
RequestException(OSError) and noble_tls raises TLSClientException(IOError), neither
|
||||
of which is an aiohttp.ClientError. translating here gives TLS backends the same
|
||||
typed failure contract as the aiohttp path on the bare request() route.
|
||||
"""
|
||||
return aiohttp.ClientError(f"{backend} request failed: {error}")
|
||||
|
||||
|
||||
try:
|
||||
from curl_cffi import AsyncSession as _CurlAsyncSession
|
||||
_CURL_ERROR = None
|
||||
@ -60,12 +47,7 @@ def _coerce_timeout(value):
|
||||
|
||||
|
||||
def _jar_to_dict(session):
|
||||
"""best-effort map of a requests-style cookie jar on session to a plain dict
|
||||
|
||||
intentionally broad: this feeds preview() only, the two backends expose differently-
|
||||
shaped jars, and a cookie read must never crash a request — so any jar that doesn't
|
||||
iterate cleanly degrades to {} rather than raising.
|
||||
"""
|
||||
"""best-effort map of a requests-style cookie jar on session to a plain dict"""
|
||||
jar = getattr(session, "cookies", None)
|
||||
if not jar:
|
||||
return {}
|
||||
@ -112,18 +94,10 @@ class CurlCffi:
|
||||
if proxy:
|
||||
kwargs["proxy"] = proxy
|
||||
|
||||
try:
|
||||
response = await session.request(method, url, impersonate=impersonate, **kwargs)
|
||||
except aiohttp.ClientError:
|
||||
raise
|
||||
except asyncio.TimeoutError:
|
||||
raise
|
||||
except OSError as error:
|
||||
# curl_cffi's RequestException subclasses OSError; translate the native
|
||||
# network error into aiohttp.ClientError. narrowed from a bare Exception so a
|
||||
# real bug (AttributeError/TypeError) isn't laundered into 'client error'
|
||||
raise _as_client_error(error, "curl_cffi") from error
|
||||
content = response.content if response.content is not None else b""
|
||||
response = await session.request(method, url, impersonate=impersonate, **kwargs)
|
||||
content = response.content
|
||||
if content is None:
|
||||
content = response.text.encode() if response.text else b""
|
||||
return Response(
|
||||
status_code=response.status_code,
|
||||
headers=dict(response.headers),
|
||||
@ -187,9 +161,9 @@ class Noble:
|
||||
async def setup(self) -> None:
|
||||
"""fetch the noble_tls Go shared library once; idempotent and concurrency-safe
|
||||
|
||||
uses noble_tls.download_if_necessary (the current API: it fetches the asset on
|
||||
first use and no-ops when it already exists). older noble_tls without that name
|
||||
is handled via update_if_necessary as a fallback.
|
||||
download_if_necessary handles the first-time fetch (no lib present);
|
||||
update_if_necessary refreshes an existing one. try download first so a
|
||||
clean environment works, falling back to update.
|
||||
|
||||
guarded by an asyncio.Lock with a check-lock-recheck so concurrent first
|
||||
requests don't both run the fetch: the fast path returns once _updated is
|
||||
@ -203,7 +177,7 @@ class Noble:
|
||||
download = getattr(noble_tls, "download_if_necessary", None)
|
||||
if download is not None:
|
||||
await download()
|
||||
elif hasattr(noble_tls, "update_if_necessary"):
|
||||
else:
|
||||
await noble_tls.update_if_necessary()
|
||||
self._updated = True
|
||||
|
||||
@ -236,16 +210,7 @@ class Noble:
|
||||
if proxy:
|
||||
kwargs["proxy"] = proxy
|
||||
|
||||
try:
|
||||
response = await session.execute_request(method=method.upper(), url=url, **kwargs)
|
||||
except aiohttp.ClientError:
|
||||
raise
|
||||
except asyncio.TimeoutError:
|
||||
raise
|
||||
except OSError as error:
|
||||
# noble_tls's TLSClientException subclasses IOError (== OSError); translate
|
||||
# the native network error, narrowed from bare Exception so a real bug surfaces
|
||||
raise _as_client_error(error, "noble_tls") from error
|
||||
response = await session.execute_request(method=method.upper(), url=url, **kwargs)
|
||||
content = getattr(response, "content", None)
|
||||
if content is None:
|
||||
text = getattr(response, "text", "") or ""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user