xyz17711-proxy-pool 0.1.0__tar.gz
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.
- xyz17711_proxy_pool-0.1.0/.gitignore +18 -0
- xyz17711_proxy_pool-0.1.0/PKG-INFO +68 -0
- xyz17711_proxy_pool-0.1.0/README.md +55 -0
- xyz17711_proxy_pool-0.1.0/pyproject.toml +21 -0
- xyz17711_proxy_pool-0.1.0/src/xyz17711_proxy_pool/__init__.py +4 -0
- xyz17711_proxy_pool-0.1.0/src/xyz17711_proxy_pool/client.py +26 -0
- xyz17711_proxy_pool-0.1.0/src/xyz17711_proxy_pool/langchain_tools.py +39 -0
- xyz17711_proxy_pool-0.1.0/src/xyz17711_proxy_pool/models.py +38 -0
- xyz17711_proxy_pool-0.1.0/tests/__init__.py +0 -0
- xyz17711_proxy_pool-0.1.0/tests/test_client.py +45 -0
- xyz17711_proxy_pool-0.1.0/tests/test_langchain_tools.py +43 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# This is the public repo's OWN .gitignore -- clients/ in the internal monorepo is an
|
|
2
|
+
# exact 1:1 mirror of github.com/17711-services/clients, so this file ships as-is to that
|
|
3
|
+
# repo's own root and applies there too (a contributor cloning the public repo and running the
|
|
4
|
+
# tests below needs this same coverage). Never add anything internal-only here -- that belongs in
|
|
5
|
+
# the platform monorepo's own root .gitignore instead.
|
|
6
|
+
|
|
7
|
+
python/packages/*/src/**/__pycache__/
|
|
8
|
+
python/packages/*/tests/**/__pycache__/
|
|
9
|
+
python/**/.pytest_cache/
|
|
10
|
+
python/**/*.egg-info/
|
|
11
|
+
python/.venv/
|
|
12
|
+
python/.venv-sync/
|
|
13
|
+
python/packages/*/dist/
|
|
14
|
+
|
|
15
|
+
node/node_modules/
|
|
16
|
+
node/packages/*/node_modules/
|
|
17
|
+
node/packages/*/dist/
|
|
18
|
+
node/packages/*/*.tsbuildinfo
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: xyz17711-proxy-pool
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generated typed client for the proxy-pool 17711.xyz microservice.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: xyz17711>=0.1.0
|
|
8
|
+
Provides-Extra: langchain
|
|
9
|
+
Requires-Dist: langchain-core<2.0,>=1.2; extra == 'langchain'
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# xyz17711-proxy-pool
|
|
15
|
+
|
|
16
|
+
[](https://github.com/17711-services/clients/actions/workflows/publish-python.yml)
|
|
17
|
+
[](https://pypi.org/project/xyz17711-proxy-pool/)
|
|
18
|
+
[](https://pypi.org/project/xyz17711-proxy-pool/)
|
|
19
|
+
[](https://github.com/17711-services/clients/blob/main/LICENSE)
|
|
20
|
+
|
|
21
|
+
Typed Python client for **proxy-pool**, part of the
|
|
22
|
+
[17711.xyz](https://api.17711.xyz) microservice fleet -- pay-per-call APIs settled in USDC via
|
|
23
|
+
[x402](https://x402.org), listed on [x402scan](https://www.x402scan.com/server/f1c7b972-3405-4b75-8b4f-d1bfbd6305ff). See the
|
|
24
|
+
[Python family README](https://github.com/17711-services/clients/blob/main/python/README.md)
|
|
25
|
+
for the full guide (payment configuration, LangChain integration, logging, error handling) -- this
|
|
26
|
+
page only covers what's specific to this one package.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install "xyz17711[proxy-pool]"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Methods
|
|
35
|
+
|
|
36
|
+
| Method | Description |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `get_best_proxy` | Returns one of the fastest verified-alive proxies matching the filter, randomized to spread load ($0.001, x402) |
|
|
39
|
+
|
|
40
|
+
Every method above has both a sync form and an async form (the `a`-prefixed sibling, e.g.
|
|
41
|
+
`aget_best_proxy` next to `get_best_proxy`), sharing one underlying client.
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from xyz17711_proxy_pool import Client
|
|
47
|
+
|
|
48
|
+
client = Client("https://api.17711.xyz/registry/proxy-pool")
|
|
49
|
+
# see the Methods table above for every available call and its request/response model
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## LangChain
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install "xyz17711-proxy-pool[langchain]"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from xyz17711_proxy_pool import Client
|
|
60
|
+
from xyz17711_proxy_pool.langchain_tools import ProxyPoolToolkit
|
|
61
|
+
|
|
62
|
+
client = Client("https://api.17711.xyz/registry/proxy-pool")
|
|
63
|
+
tools = ProxyPoolToolkit(client=client).get_tools()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT -- see the repository's own `LICENSE`.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# xyz17711-proxy-pool
|
|
2
|
+
|
|
3
|
+
[](https://github.com/17711-services/clients/actions/workflows/publish-python.yml)
|
|
4
|
+
[](https://pypi.org/project/xyz17711-proxy-pool/)
|
|
5
|
+
[](https://pypi.org/project/xyz17711-proxy-pool/)
|
|
6
|
+
[](https://github.com/17711-services/clients/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
Typed Python client for **proxy-pool**, part of the
|
|
9
|
+
[17711.xyz](https://api.17711.xyz) microservice fleet -- pay-per-call APIs settled in USDC via
|
|
10
|
+
[x402](https://x402.org), listed on [x402scan](https://www.x402scan.com/server/f1c7b972-3405-4b75-8b4f-d1bfbd6305ff). See the
|
|
11
|
+
[Python family README](https://github.com/17711-services/clients/blob/main/python/README.md)
|
|
12
|
+
for the full guide (payment configuration, LangChain integration, logging, error handling) -- this
|
|
13
|
+
page only covers what's specific to this one package.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install "xyz17711[proxy-pool]"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Methods
|
|
22
|
+
|
|
23
|
+
| Method | Description |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `get_best_proxy` | Returns one of the fastest verified-alive proxies matching the filter, randomized to spread load ($0.001, x402) |
|
|
26
|
+
|
|
27
|
+
Every method above has both a sync form and an async form (the `a`-prefixed sibling, e.g.
|
|
28
|
+
`aget_best_proxy` next to `get_best_proxy`), sharing one underlying client.
|
|
29
|
+
|
|
30
|
+
## Quickstart
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from xyz17711_proxy_pool import Client
|
|
34
|
+
|
|
35
|
+
client = Client("https://api.17711.xyz/registry/proxy-pool")
|
|
36
|
+
# see the Methods table above for every available call and its request/response model
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## LangChain
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install "xyz17711-proxy-pool[langchain]"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from xyz17711_proxy_pool import Client
|
|
47
|
+
from xyz17711_proxy_pool.langchain_tools import ProxyPoolToolkit
|
|
48
|
+
|
|
49
|
+
client = Client("https://api.17711.xyz/registry/proxy-pool")
|
|
50
|
+
tools = ProxyPoolToolkit(client=client).get_tools()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT -- see the repository's own `LICENSE`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xyz17711-proxy-pool"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Generated typed client for the proxy-pool 17711.xyz microservice."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
dependencies = ["xyz17711>=0.1.0"]
|
|
9
|
+
|
|
10
|
+
# Only needed to import `langchain_tools` (never the base `client`/`models` modules) -- see that
|
|
11
|
+
# module's own doc comment for why it's an extra, never a hard dependency.
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
langchain = ["langchain-core>=1.2,<2.0"]
|
|
14
|
+
test = ["pytest>=8"]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["hatchling"]
|
|
18
|
+
build-backend = "hatchling.build"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/xyz17711_proxy_pool"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Typed HTTP client for proxy-pool -- one method per operation, each with a sync form and an async (`a`-prefixed) form."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from xyz17711_core import Client as _BaseClient
|
|
8
|
+
|
|
9
|
+
from .models import * # noqa: F401,F403 -- re-exported for callers building requests
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Client(_BaseClient):
|
|
15
|
+
"""Typed client for proxy-pool. Every method has a sync form and an async form (the `a`-prefixed sibling, e.g. `aget_best_proxy` next to `get_best_proxy`) sharing the same underlying connection/wallet -- pick whichever fits your own code."""
|
|
16
|
+
|
|
17
|
+
def get_best_proxy(self, anonymity: Optional[str] = None, city: Optional[str] = None, country: Optional[str] = None, protocol: Optional[str] = None) -> GetBestProxyResponse:
|
|
18
|
+
"""Returns one of the fastest verified-alive proxies matching the filter, randomized to spread load ($0.001, x402)"""
|
|
19
|
+
logger.debug("get_best_proxy: calling %s", "/v1/proxy-pool/get_best_proxy")
|
|
20
|
+
return GetBestProxyResponse.model_validate(self._get("/v1/proxy-pool/get_best_proxy", {"anonymity": anonymity, "city": city, "country": country, "protocol": protocol}))
|
|
21
|
+
|
|
22
|
+
async def aget_best_proxy(self, anonymity: Optional[str] = None, city: Optional[str] = None, country: Optional[str] = None, protocol: Optional[str] = None) -> GetBestProxyResponse:
|
|
23
|
+
"""Returns one of the fastest verified-alive proxies matching the filter, randomized to spread load ($0.001, x402) (async)"""
|
|
24
|
+
logger.debug("aget_best_proxy: calling %s", "/v1/proxy-pool/get_best_proxy")
|
|
25
|
+
return GetBestProxyResponse.model_validate(await self._aget("/v1/proxy-pool/get_best_proxy", {"anonymity": anonymity, "city": city, "country": country, "protocol": protocol}))
|
|
26
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""LangChain `StructuredTool`s for proxy-pool, grouped in one `BaseToolkit` -- one tool per operation, each with both a sync and an async implementation. Requires the `langchain` extra: pip install "xyz17711-proxy-pool[langchain]"."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, List, Optional
|
|
7
|
+
|
|
8
|
+
from langchain_core.tools import BaseTool, BaseToolkit, StructuredTool
|
|
9
|
+
|
|
10
|
+
from .client import Client
|
|
11
|
+
from .models import * # noqa: F401,F403 -- request models reused as tool args_schema
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProxyPoolToolkit(BaseToolkit):
|
|
17
|
+
"""LangChain toolkit for proxy-pool -- one StructuredTool per operation, reusing this package's own generated client and request models directly. Each tool carries both a sync and an async (`coroutine=`) implementation, so `.invoke()` and `.ainvoke()` both work."""
|
|
18
|
+
|
|
19
|
+
client: Client
|
|
20
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
21
|
+
|
|
22
|
+
def get_tools(self) -> List[BaseTool]:
|
|
23
|
+
logger.debug("ProxyPoolToolkit.get_tools: building 1 tool(s)")
|
|
24
|
+
def _get_best_proxy(anonymity: Optional[str] = None, city: Optional[str] = None, country: Optional[str] = None, protocol: Optional[str] = None) -> Any:
|
|
25
|
+
logger.debug("ProxyPoolToolkit.get_tools: calling get_best_proxy")
|
|
26
|
+
return self.client.get_best_proxy(anonymity=anonymity, city=city, country=country, protocol=protocol).model_dump()
|
|
27
|
+
|
|
28
|
+
async def _aget_best_proxy(anonymity: Optional[str] = None, city: Optional[str] = None, country: Optional[str] = None, protocol: Optional[str] = None) -> Any:
|
|
29
|
+
logger.debug("ProxyPoolToolkit.get_tools: calling get_best_proxy (async)")
|
|
30
|
+
return (await self.client.aget_best_proxy(anonymity=anonymity, city=city, country=country, protocol=protocol)).model_dump()
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
StructuredTool.from_function(
|
|
34
|
+
func=_get_best_proxy,
|
|
35
|
+
coroutine=_aget_best_proxy,
|
|
36
|
+
name="get_best_proxy",
|
|
37
|
+
description="Returns one of the fastest verified-alive proxies matching the filter, randomized to spread load ($0.001, x402)",
|
|
38
|
+
),
|
|
39
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Pydantic request/response models for proxy-pool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProxyView(BaseModel):
|
|
11
|
+
"""A `SourceProxy` joined with its `CheckedProxy` result, if it has been checked at least once -- what every read tool actually filters/returns."""
|
|
12
|
+
anonymity: str
|
|
13
|
+
checked_alive: Optional[bool] = None # `None` if this service has never checked this proxy yet.
|
|
14
|
+
checked_at: Optional[int] = None
|
|
15
|
+
checked_consecutive_successes: int # See `CheckedProxy::consecutive_successes`'s own doc comment. `0` if never checked.
|
|
16
|
+
checked_ema_latency_ms: Optional[float] = None # See `CheckedProxy::ema_latency_ms`'s own doc comment.
|
|
17
|
+
checked_latency_ms: Optional[int] = None
|
|
18
|
+
checked_protocol: Optional[str] = None
|
|
19
|
+
city: str
|
|
20
|
+
country: str
|
|
21
|
+
id: str
|
|
22
|
+
ip: str
|
|
23
|
+
port: int
|
|
24
|
+
protocols: List[str]
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def _dummy(cls) -> "ProxyView":
|
|
28
|
+
"""A minimal, always-constructible instance -- test fixtures only, never real data."""
|
|
29
|
+
return cls(anonymity="test", checked_alive=None, checked_at=None, checked_consecutive_successes=0, checked_ema_latency_ms=None, checked_latency_ms=None, checked_protocol=None, city="test", country="test", id="test", ip="test", port=0, protocols=[])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GetBestProxyResponse(BaseModel):
|
|
33
|
+
proxy: ProxyView
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def _dummy(cls) -> "GetBestProxyResponse":
|
|
37
|
+
"""A minimal, always-constructible instance -- test fixtures only, never real data."""
|
|
38
|
+
return cls(proxy=ProxyView._dummy())
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Verifies every proxy-pool client method (sync and async) sends a request to the expected path/HTTP verb and parses a well-formed response into the expected type."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from xyz17711_proxy_pool import Client
|
|
10
|
+
from xyz17711_proxy_pool.models import * # noqa: F401,F403
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _mock_client(handler) -> Client:
|
|
14
|
+
"""A Client wired to a fake transport for BOTH its sync and async HTTP clients -- no real network call ever happens in this test module. Found live: the async client is normally constructed LAZILY on first async use, so a test that only mocks `_http` (sync) silently lets an async test make a REAL network call instead of using the mock."""
|
|
15
|
+
client = Client("https://example.test")
|
|
16
|
+
client._http = httpx.Client(base_url="https://example.test", transport=httpx.MockTransport(handler))
|
|
17
|
+
client._ahttp = httpx.AsyncClient(base_url="https://example.test", transport=httpx.MockTransport(handler))
|
|
18
|
+
return client
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_get_best_proxy_calls_the_expected_path():
|
|
22
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
23
|
+
assert request.method == "GET"
|
|
24
|
+
assert request.url.path == "/v1/proxy-pool/get_best_proxy"
|
|
25
|
+
return httpx.Response(200, json=GetBestProxyResponse._dummy().model_dump(mode='json'))
|
|
26
|
+
|
|
27
|
+
client = _mock_client(handler)
|
|
28
|
+
result = client.get_best_proxy(anonymity=None, city=None, country=None, protocol=None)
|
|
29
|
+
assert isinstance(result, GetBestProxyResponse)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_aget_best_proxy_calls_the_expected_path():
|
|
33
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
34
|
+
assert request.method == "GET"
|
|
35
|
+
assert request.url.path == "/v1/proxy-pool/get_best_proxy"
|
|
36
|
+
return httpx.Response(200, json=GetBestProxyResponse._dummy().model_dump(mode='json'))
|
|
37
|
+
|
|
38
|
+
client = _mock_client(handler)
|
|
39
|
+
|
|
40
|
+
async def run():
|
|
41
|
+
return await client.aget_best_proxy(anonymity=None, city=None, country=None, protocol=None)
|
|
42
|
+
|
|
43
|
+
result = asyncio.run(run())
|
|
44
|
+
assert isinstance(result, GetBestProxyResponse)
|
|
45
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Verifies every proxy-pool LangChain tool through the real StructuredTool (both `.invoke()` and `.ainvoke()`), not just the underlying client method -- catches schema-shape bugs a plain client-method test would miss. Requires the `langchain` extra."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from xyz17711_proxy_pool import Client
|
|
10
|
+
from xyz17711_proxy_pool.langchain_tools import ProxyPoolToolkit
|
|
11
|
+
from xyz17711_proxy_pool.models import * # noqa: F401,F403
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _mock_client(handler) -> Client:
|
|
15
|
+
client = Client("https://example.test")
|
|
16
|
+
client._http = httpx.Client(base_url="https://example.test", transport=httpx.MockTransport(handler))
|
|
17
|
+
client._ahttp = httpx.AsyncClient(base_url="https://example.test", transport=httpx.MockTransport(handler))
|
|
18
|
+
return client
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_get_best_proxy_tool_invoke():
|
|
22
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
23
|
+
return httpx.Response(200, json=GetBestProxyResponse._dummy().model_dump(mode='json'))
|
|
24
|
+
|
|
25
|
+
toolkit = ProxyPoolToolkit(client=_mock_client(handler))
|
|
26
|
+
tool = next(t for t in toolkit.get_tools() if t.name == "get_best_proxy")
|
|
27
|
+
result = tool.invoke({"anonymity": None, "city": None, "country": None, "protocol": None})
|
|
28
|
+
assert result is not None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_get_best_proxy_tool_ainvoke():
|
|
32
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
33
|
+
return httpx.Response(200, json=GetBestProxyResponse._dummy().model_dump(mode='json'))
|
|
34
|
+
|
|
35
|
+
toolkit = ProxyPoolToolkit(client=_mock_client(handler))
|
|
36
|
+
tool = next(t for t in toolkit.get_tools() if t.name == "get_best_proxy")
|
|
37
|
+
|
|
38
|
+
async def run():
|
|
39
|
+
return await tool.ainvoke({"anonymity": None, "city": None, "country": None, "protocol": None})
|
|
40
|
+
|
|
41
|
+
result = asyncio.run(run())
|
|
42
|
+
assert result is not None
|
|
43
|
+
|