Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eef4b25f07 | |||
| a5e36544d4 | |||
| 72c5342a6b | |||
| 932fb71c95 | |||
| 0abc071f14 | |||
| fc27d77000 | |||
| 260b92b66a | |||
| f618b6a6a1 | |||
| aa661bd6de |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
|||||||
# claude
|
# claude
|
||||||
CLAUDE.md
|
.claude/
|
||||||
|
|
||||||
# python
|
# python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
14
README.md
14
README.md
@ -9,14 +9,16 @@ edits. **Credentials are always injected — never hardcoded.**
|
|||||||
## Install
|
## Install
|
||||||
|
|
||||||
```
|
```
|
||||||
aioproxies @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioproxies.git@v0.2.0
|
aioproxies @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioproxies.git@v0.2.2
|
||||||
# network helpers (current_ip / reset) need the extra:
|
# network helpers (current_ip / reset) need the extra:
|
||||||
aioproxies[net] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioproxies.git@v0.2.0
|
aioproxies[net] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioproxies.git@v0.2.2
|
||||||
```
|
```
|
||||||
|
|
||||||
The core has no dependencies. The `net` extra adds `aiohttp` for `current_ip` /
|
The core has no dependencies. The `net` extra adds `aiohttp` for `current_ip` /
|
||||||
`reset`.
|
`reset`.
|
||||||
|
|
||||||
|
Drop the `@v0.2.2` suffix from the line above to install the latest unpinned.
|
||||||
|
|
||||||
## Formatting
|
## Formatting
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@ -33,6 +35,8 @@ p.key() # "1.2.3.4:8080:user:pass" (canonical identity; "host:port" if a
|
|||||||
Auth-less (IP-authenticated) proxies are first-class: `"host:port"` parses and
|
Auth-less (IP-authenticated) proxies are first-class: `"host:port"` parses and
|
||||||
every render shape omits the credentials. The 4-part form splits on the first three
|
every render shape omits the credentials. The 4-part form splits on the first three
|
||||||
colons, so a password may itself contain colons (`host:port:user:pa:ss:word`).
|
colons, so a password may itself contain colons (`host:port:user:pa:ss:word`).
|
||||||
|
`url()` / `aiohttp()` percent-encode the credentials, so reserved characters
|
||||||
|
(`/ # ? @`) in a user or password still produce a valid URL.
|
||||||
|
|
||||||
## Sources
|
## Sources
|
||||||
|
|
||||||
@ -200,6 +204,12 @@ await reset("https://provider/reset-url") # rotate upstream ip
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v0.2.1
|
||||||
|
|
||||||
|
- **Legible missing-template-field error:** a `template=` placeholder not supplied to
|
||||||
|
`next(**fields)` now raises a clear `ValueError` naming the field, instead of leaking
|
||||||
|
a bare `KeyError` from `str.format`.
|
||||||
|
|
||||||
### v0.2.0
|
### v0.2.0
|
||||||
|
|
||||||
- **Proxy health for rotating lists:** `burn`/`restore`/`is_burned` (dead `-1` vs
|
- **Proxy health for rotating lists:** `burn`/`restore`/`is_burned` (dead `-1` vs
|
||||||
|
|||||||
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "aioproxies"
|
name = "aioproxies"
|
||||||
version = "0.2.0"
|
version = "0.2.2"
|
||||||
description = "proxy parsing, formatting, health, and pool management for aiohttp/aioweb, camoufox, and socks5"
|
description = "proxy parsing, formatting, health, and pool management for aiohttp/aioweb, camoufox, and socks5"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|||||||
@ -18,4 +18,4 @@ __all__ = [
|
|||||||
"to_proxy",
|
"to_proxy",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "0.2.0"
|
__version__ = "0.2.2"
|
||||||
|
|||||||
@ -58,6 +58,8 @@ class AioProxies:
|
|||||||
shuffle: bool = True,
|
shuffle: bool = True,
|
||||||
cooldown: int = 0,
|
cooldown: int = 0,
|
||||||
):
|
):
|
||||||
|
if proxies is not None and len(proxies) == 0:
|
||||||
|
raise ValueError("proxies list is empty; provide at least one proxy")
|
||||||
sources = [s for s in (template, proxies, static) if s]
|
sources = [s for s in (template, proxies, static) if s]
|
||||||
if len(sources) != 1:
|
if len(sources) != 1:
|
||||||
raise ValueError("provide exactly one of: template, proxies, static")
|
raise ValueError("provide exactly one of: template, proxies, static")
|
||||||
@ -123,7 +125,21 @@ class AioProxies:
|
|||||||
if self._static is not None:
|
if self._static is not None:
|
||||||
return self._static
|
return self._static
|
||||||
if self.template is not None:
|
if self.template is not None:
|
||||||
return parse(self.template.format(session=self.session_id(), **fields))
|
if "session" in fields:
|
||||||
|
# `session` is auto-filled with a fresh id; a caller-supplied one would
|
||||||
|
# collide in str.format with an opaque TypeError — reject it clearly
|
||||||
|
raise ValueError("'session' is filled automatically; do not pass it to next()")
|
||||||
|
try:
|
||||||
|
filled = self.template.format(session=self.session_id(), **fields)
|
||||||
|
except (KeyError, IndexError) as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"template placeholder {exc} not provided; pass it to next(**fields)"
|
||||||
|
) from exc
|
||||||
|
except ValueError as exc:
|
||||||
|
# a malformed template (e.g. an unmatched '{') makes str.format raise a
|
||||||
|
# bare ValueError; re-raise naming the cause so it isn't cryptic
|
||||||
|
raise ValueError(f"malformed proxy template {self.template!r}: {exc}") from exc
|
||||||
|
return parse(filled)
|
||||||
return self._next_from_list()
|
return self._next_from_list()
|
||||||
|
|
||||||
def _next_from_list(self) -> Proxy:
|
def _next_from_list(self) -> Proxy:
|
||||||
|
|||||||
@ -37,8 +37,10 @@ async def current_ip(
|
|||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession(timeout=t) as session:
|
async with aiohttp.ClientSession(timeout=t) as session:
|
||||||
async with session.get(test_url, proxy=p.url()) as resp:
|
async with session.get(test_url, proxy=p.url()) as resp:
|
||||||
data = await resp.json()
|
# content_type=None: an echo endpoint may return text/plain json; the
|
||||||
return data.get("ip")
|
# default would raise ContentTypeError and silently return None
|
||||||
|
data = await resp.json(content_type=None)
|
||||||
|
return data.get("ip") if isinstance(data, dict) else None
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.warning("ip check failed: %s", exc)
|
log.warning("ip check failed: %s", exc)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -13,7 +13,7 @@ input shape. `canonical_key()` extends that to dict/url forms.
|
|||||||
"""
|
"""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, Optional, Union
|
from typing import Dict, Optional, Union
|
||||||
from urllib.parse import unquote, urlsplit
|
from urllib.parse import quote, unquote, urlsplit
|
||||||
|
|
||||||
SCHEME_HTTP = "http"
|
SCHEME_HTTP = "http"
|
||||||
SCHEME_SOCKS5 = "socks5"
|
SCHEME_SOCKS5 = "socks5"
|
||||||
@ -48,9 +48,15 @@ class Proxy:
|
|||||||
return f"{host}:{self.port}"
|
return f"{host}:{self.port}"
|
||||||
|
|
||||||
def url(self, scheme: str = SCHEME_HTTP) -> str:
|
def url(self, scheme: str = SCHEME_HTTP) -> str:
|
||||||
"""render as a url, embedding auth when present"""
|
"""render as a url, embedding auth when present
|
||||||
|
|
||||||
|
credentials are percent-encoded so reserved chars (/ # ? @ :) in a user or
|
||||||
|
password produce a valid url; this mirrors the unquote() on the parse side.
|
||||||
|
"""
|
||||||
if self.has_auth:
|
if self.has_auth:
|
||||||
return f"{scheme}://{self.user}:{self.password}@{self.host}:{self.port}"
|
user = quote(str(self.user), safe="")
|
||||||
|
password = quote(str(self.password), safe="")
|
||||||
|
return f"{scheme}://{user}:{password}@{self.host}:{self.port}"
|
||||||
return f"{scheme}://{self.host}:{self.port}"
|
return f"{scheme}://{self.host}:{self.port}"
|
||||||
|
|
||||||
def aiohttp(self) -> Dict[str, str]:
|
def aiohttp(self) -> Dict[str, str]:
|
||||||
@ -133,8 +139,11 @@ def _proxy_from_dict(spec: Dict[str, str]) -> Proxy:
|
|||||||
"""normalize an aiohttp / camoufox / socks5 dict into a Proxy"""
|
"""normalize an aiohttp / camoufox / socks5 dict into a Proxy"""
|
||||||
if "server" in spec:
|
if "server" in spec:
|
||||||
proxy = _proxy_from_url(spec["server"])
|
proxy = _proxy_from_url(spec["server"])
|
||||||
user = spec.get("username")
|
# prefer explicit dict auth; otherwise fall back to auth embedded in the server
|
||||||
password = spec.get("password")
|
# URL (http://user:pass@host:port) so it isn't silently dropped — which would
|
||||||
|
# key the proxy auth-less and collide with a genuinely auth-less one
|
||||||
|
user = spec.get("username") or proxy.user
|
||||||
|
password = spec.get("password") or proxy.password
|
||||||
if user and password:
|
if user and password:
|
||||||
return Proxy(proxy.host, proxy.port, user, password)
|
return Proxy(proxy.host, proxy.port, user, password)
|
||||||
return Proxy(proxy.host, proxy.port)
|
return Proxy(proxy.host, proxy.port)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user