pharox-sdk 0.2.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zacarias
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: pharox-sdk
3
+ Version: 0.2.0
4
+ Summary: Typed Python SDK for pharox — remote mode (HTTP) and local mode (direct toolkit)
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: fzaca
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Dist: httpx (>=0.28,<0.29)
17
+ Requires-Dist: pharox (>=0.8.1,<0.9.0)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # pharox-sdk
21
+
22
+ Python SDK for the [Pharox](https://github.com/fzaca/pharox) proxy lifecycle management ecosystem.
23
+
24
+ Works in two interchangeable modes:
25
+
26
+ - **Remote mode** — HTTP client for a running `pharox-service`
27
+ - **Local mode** — direct access to `pharox-toolkit` via `IAsyncStorage` (no service required)
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install pharox-sdk
33
+ ```
34
+
35
+ ## Quick start
36
+
37
+ ```python
38
+ from pharox_sdk import PharoxSDK
39
+
40
+ # Remote mode
41
+ sdk = PharoxSDK.remote("http://localhost:8000", api_key="my-key")
42
+
43
+ # Local mode (no service needed)
44
+ from pharox import AsyncInMemoryStorage
45
+ sdk = PharoxSDK.local(AsyncInMemoryStorage())
46
+
47
+ # Same interface in both modes
48
+ async with sdk.with_lease("my-pool") as lease:
49
+ if lease:
50
+ print(lease.proxy_id)
51
+ ```
52
+
53
+ ## Links
54
+
55
+ - [pharox-toolkit](https://github.com/fzaca/pharox) — core library
56
+ - [pharox-service](https://github.com/fzaca/pharox-service) — FastAPI service
57
+
@@ -0,0 +1,37 @@
1
+ # pharox-sdk
2
+
3
+ Python SDK for the [Pharox](https://github.com/fzaca/pharox) proxy lifecycle management ecosystem.
4
+
5
+ Works in two interchangeable modes:
6
+
7
+ - **Remote mode** — HTTP client for a running `pharox-service`
8
+ - **Local mode** — direct access to `pharox-toolkit` via `IAsyncStorage` (no service required)
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install pharox-sdk
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ ```python
19
+ from pharox_sdk import PharoxSDK
20
+
21
+ # Remote mode
22
+ sdk = PharoxSDK.remote("http://localhost:8000", api_key="my-key")
23
+
24
+ # Local mode (no service needed)
25
+ from pharox import AsyncInMemoryStorage
26
+ sdk = PharoxSDK.local(AsyncInMemoryStorage())
27
+
28
+ # Same interface in both modes
29
+ async with sdk.with_lease("my-pool") as lease:
30
+ if lease:
31
+ print(lease.proxy_id)
32
+ ```
33
+
34
+ ## Links
35
+
36
+ - [pharox-toolkit](https://github.com/fzaca/pharox) — core library
37
+ - [pharox-service](https://github.com/fzaca/pharox-service) — FastAPI service
@@ -0,0 +1,47 @@
1
+ [tool.poetry]
2
+ name = "pharox-sdk"
3
+ version = "0.2.0"
4
+ description = "Typed Python SDK for pharox — remote mode (HTTP) and local mode (direct toolkit)"
5
+ authors = ["fzaca"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ packages = [{ include = "pharox_sdk", from = "src" }]
9
+
10
+ [tool.poetry.dependencies]
11
+ python = "^3.10"
12
+ pharox = "^0.8.1"
13
+ httpx = "^0.28"
14
+
15
+ [tool.poetry.group.dev.dependencies]
16
+ pytest = "^8.0"
17
+ pytest-asyncio = "^0.23"
18
+ respx = "^0.21"
19
+ mypy = "^1.10"
20
+ ruff = "^0.4"
21
+ commitizen = "^3.27"
22
+
23
+ [tool.commitizen]
24
+ name = "cz_conventional_commits"
25
+ tag_format = "v$version"
26
+ version_provider = "poetry"
27
+ update_changelog_on_bump = true
28
+
29
+ [tool.pytest.ini_options]
30
+ asyncio_mode = "auto"
31
+ asyncio_default_fixture_loop_scope = "function"
32
+ testpaths = ["tests"]
33
+
34
+ [tool.mypy]
35
+ python_version = "3.10"
36
+ strict = true
37
+ ignore_missing_imports = true
38
+
39
+ [tool.ruff]
40
+ line-length = 88
41
+
42
+ [tool.ruff.lint]
43
+ select = ["E", "F", "I"]
44
+
45
+ [build-system]
46
+ requires = ["poetry-core"]
47
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,12 @@
1
+ """pharox-sdk — typed Python SDK for the Pharox proxy ecosystem."""
2
+
3
+ from .client import PharoxClient
4
+ from .exceptions import RemoteError, SDKError
5
+ from .sdk import PharoxSDK
6
+
7
+ __all__ = [
8
+ "PharoxClient",
9
+ "PharoxSDK",
10
+ "RemoteError",
11
+ "SDKError",
12
+ ]
@@ -0,0 +1,232 @@
1
+ """Async HTTP client for pharox-service (remote mode low-level API)."""
2
+ from datetime import datetime
3
+ from typing import Any, Optional
4
+ from uuid import UUID
5
+
6
+ import httpx
7
+ from pharox import (
8
+ Lease,
9
+ LeaseStatus,
10
+ ProxyFilters,
11
+ ProxyPool,
12
+ ProxyProtocol,
13
+ ProxyStatus,
14
+ SelectorStrategy,
15
+ )
16
+
17
+ from .exceptions import RemoteError
18
+
19
+
20
+ def _parse_pool(data: dict[str, Any]) -> ProxyPool:
21
+ return ProxyPool(
22
+ id=UUID(data["id"]),
23
+ name=data["name"],
24
+ description=data.get("description", ""),
25
+ )
26
+
27
+
28
+ def _parse_lease(data: dict[str, Any]) -> Lease:
29
+ pool_id_str = data.get("pool_id") or ""
30
+ return Lease(
31
+ id=UUID(data["id"]),
32
+ proxy_id=UUID(data["proxy_id"]),
33
+ consumer_id=UUID(data["consumer_id"]),
34
+ pool_id=UUID(pool_id_str) if pool_id_str else None,
35
+ status=LeaseStatus(data["status"]),
36
+ acquired_at=datetime.fromisoformat(data["acquired_at"]),
37
+ expires_at=datetime.fromisoformat(data["expires_at"]),
38
+ )
39
+
40
+
41
+ class PharoxClient:
42
+ """
43
+ Low-level async HTTP client for pharox-service.
44
+
45
+ Use as an async context manager or call ``aclose()`` when done.
46
+
47
+ Parameters
48
+ ----------
49
+ base_url:
50
+ Base URL of pharox-service (e.g. ``"http://localhost:8000"``).
51
+ api_key:
52
+ API key for authentication.
53
+ timeout:
54
+ Request timeout in seconds. Defaults to 30.
55
+ """
56
+
57
+ def __init__(
58
+ self, base_url: str, api_key: str, timeout: float = 30.0
59
+ ) -> None:
60
+ self._http = httpx.AsyncClient(
61
+ base_url=base_url.rstrip("/"),
62
+ headers={"X-API-Key": api_key},
63
+ timeout=timeout,
64
+ )
65
+
66
+ async def aclose(self) -> None:
67
+ await self._http.aclose()
68
+
69
+ async def __aenter__(self) -> "PharoxClient":
70
+ return self
71
+
72
+ async def __aexit__(self, *_: Any) -> None:
73
+ await self.aclose()
74
+
75
+ def _raise_for_status(self, response: httpx.Response) -> None:
76
+ if response.is_error:
77
+ try:
78
+ detail = response.json().get("detail", response.text)
79
+ except Exception:
80
+ detail = response.text
81
+ raise RemoteError(response.status_code, str(detail))
82
+
83
+ async def _request(
84
+ self,
85
+ method: str,
86
+ url: str,
87
+ **kwargs: Any,
88
+ ) -> httpx.Response:
89
+ """Execute an HTTP request, wrapping network errors as RemoteError."""
90
+ try:
91
+ return await self._http.request(method, url, **kwargs)
92
+ except httpx.TimeoutException as exc:
93
+ raise RemoteError(0, f"Request timed out: {exc}") from exc
94
+ except httpx.NetworkError as exc:
95
+ raise RemoteError(0, f"Network error: {exc}") from exc
96
+
97
+ # ------------------------------------------------------------------
98
+ # Pools
99
+ # ------------------------------------------------------------------
100
+
101
+ async def create_pool(
102
+ self, name: str, description: str = ""
103
+ ) -> ProxyPool:
104
+ r = await self._request(
105
+ "POST", "/v1/pools/", json={"name": name, "description": description}
106
+ )
107
+ self._raise_for_status(r)
108
+ return _parse_pool(r.json())
109
+
110
+ async def list_pools(self) -> list[ProxyPool]:
111
+ r = await self._request("GET", "/v1/pools/")
112
+ self._raise_for_status(r)
113
+ return [_parse_pool(p) for p in r.json()]
114
+
115
+ async def get_pool(self, pool_id: str) -> ProxyPool:
116
+ r = await self._request("GET", f"/v1/pools/{pool_id}")
117
+ self._raise_for_status(r)
118
+ return _parse_pool(r.json())
119
+
120
+ async def delete_pool(self, pool_id: str) -> None:
121
+ r = await self._request("DELETE", f"/v1/pools/{pool_id}")
122
+ self._raise_for_status(r)
123
+
124
+ # ------------------------------------------------------------------
125
+ # Proxies
126
+ # ------------------------------------------------------------------
127
+
128
+ async def add_proxy(
129
+ self,
130
+ pool_id: str,
131
+ host: str,
132
+ port: int,
133
+ protocol: ProxyProtocol = ProxyProtocol.HTTP,
134
+ username: Optional[str] = None,
135
+ password: Optional[str] = None,
136
+ country: Optional[str] = None,
137
+ city: Optional[str] = None,
138
+ latitude: Optional[float] = None,
139
+ longitude: Optional[float] = None,
140
+ ) -> dict[str, Any]:
141
+ payload: dict[str, Any] = {
142
+ "host": host,
143
+ "port": port,
144
+ "protocol": protocol.value,
145
+ }
146
+ if username:
147
+ payload["username"] = username
148
+ if password:
149
+ payload["password"] = password
150
+ if country:
151
+ payload["country"] = country
152
+ if city:
153
+ payload["city"] = city
154
+ if latitude is not None:
155
+ payload["latitude"] = latitude
156
+ if longitude is not None:
157
+ payload["longitude"] = longitude
158
+
159
+ r = await self._request(
160
+ "POST", f"/v1/pools/{pool_id}/proxies/", json=payload
161
+ )
162
+ self._raise_for_status(r)
163
+ return r.json() # type: ignore[no-any-return]
164
+
165
+ async def list_proxies(self, pool_id: str) -> list[dict[str, Any]]:
166
+ r = await self._request("GET", f"/v1/pools/{pool_id}/proxies/")
167
+ self._raise_for_status(r)
168
+ return r.json() # type: ignore[no-any-return]
169
+
170
+ async def update_proxy_status(
171
+ self, pool_id: str, proxy_id: str, status: ProxyStatus
172
+ ) -> dict[str, Any]:
173
+ r = await self._request(
174
+ "PATCH",
175
+ f"/v1/pools/{pool_id}/proxies/{proxy_id}",
176
+ json={"status": status.value},
177
+ )
178
+ self._raise_for_status(r)
179
+ return r.json() # type: ignore[no-any-return]
180
+
181
+ async def delete_proxy(self, pool_id: str, proxy_id: str) -> None:
182
+ r = await self._request(
183
+ "DELETE", f"/v1/pools/{pool_id}/proxies/{proxy_id}"
184
+ )
185
+ self._raise_for_status(r)
186
+
187
+ # ------------------------------------------------------------------
188
+ # Leases
189
+ # ------------------------------------------------------------------
190
+
191
+ async def acquire_lease(
192
+ self,
193
+ pool_id: str,
194
+ consumer_id: str,
195
+ ttl_seconds: int = 300,
196
+ filters: Optional[ProxyFilters] = None,
197
+ selector: Optional[SelectorStrategy] = None,
198
+ ) -> Optional[Lease]:
199
+ """
200
+ Returns the acquired Lease or None if no proxy is available (HTTP 409).
201
+ Raises RemoteError for any other non-2xx response.
202
+ """
203
+ payload: dict[str, Any] = {
204
+ "pool_id": pool_id,
205
+ "consumer_id": consumer_id,
206
+ "ttl_seconds": ttl_seconds,
207
+ "strategy": (
208
+ selector.value
209
+ if selector
210
+ else SelectorStrategy.FIRST_AVAILABLE.value
211
+ ),
212
+ }
213
+ if filters:
214
+ payload["filters"] = filters.model_dump(exclude_none=True)
215
+
216
+ r = await self._request("POST", "/v1/leases/", json=payload)
217
+ if r.status_code == 409:
218
+ return None
219
+ self._raise_for_status(r)
220
+ return _parse_lease(r.json())
221
+
222
+ async def get_lease(self, lease_id: str) -> Optional[Lease]:
223
+ r = await self._request("GET", f"/v1/leases/{lease_id}")
224
+ if r.status_code == 404:
225
+ return None
226
+ self._raise_for_status(r)
227
+ return _parse_lease(r.json())
228
+
229
+ async def release_lease(self, lease_id: str) -> Lease:
230
+ r = await self._request("POST", f"/v1/leases/{lease_id}/release", json={})
231
+ self._raise_for_status(r)
232
+ return _parse_lease(r.json())
@@ -0,0 +1,11 @@
1
+ class SDKError(Exception):
2
+ """Base error for pharox-sdk."""
3
+
4
+
5
+ class RemoteError(SDKError):
6
+ """Raised when the pharox-service returns an unexpected HTTP error."""
7
+
8
+ def __init__(self, status_code: int, detail: str) -> None:
9
+ self.status_code = status_code
10
+ self.detail = detail
11
+ super().__init__(f"HTTP {status_code}: {detail}")
@@ -0,0 +1,241 @@
1
+ """Unified dual-mode PharoxSDK — remote (HTTP) or local (direct toolkit)."""
2
+ from contextlib import asynccontextmanager
3
+ from typing import AsyncIterator, Optional
4
+
5
+ from pharox import (
6
+ IAsyncStorage,
7
+ Lease,
8
+ ProxyFilters,
9
+ ProxyPool,
10
+ SelectorStrategy,
11
+ )
12
+
13
+ from .client import PharoxClient
14
+ from .exceptions import RemoteError
15
+
16
+
17
+ class PharoxSDK:
18
+ """
19
+ Dual-mode SDK that mirrors the async ProxyManager semantics.
20
+
21
+ Use the class methods to create an instance:
22
+
23
+ .. code-block:: python
24
+
25
+ # Remote mode — talks to a running pharox-service
26
+ sdk = PharoxSDK.remote("http://localhost:8000", api_key="secret")
27
+
28
+ # Local mode — uses an IAsyncStorage directly (no HTTP overhead)
29
+ sdk = PharoxSDK.local(storage)
30
+
31
+ Both modes expose the same interface:
32
+ ``acquire_proxy``, ``release_proxy``, ``with_lease``.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ *,
38
+ client: Optional[PharoxClient] = None,
39
+ storage: Optional[IAsyncStorage] = None,
40
+ ) -> None:
41
+ if (client is None) == (storage is None):
42
+ raise ValueError(
43
+ "Provide exactly one of client= (remote) or storage= (local)."
44
+ )
45
+ self._client = client
46
+ self._storage = storage
47
+ # name → id cache used only in remote mode
48
+ self._pool_cache: dict[str, str] = {}
49
+
50
+ # ------------------------------------------------------------------
51
+ # Constructors
52
+ # ------------------------------------------------------------------
53
+
54
+ @classmethod
55
+ def remote(
56
+ cls, base_url: str, api_key: str, timeout: float = 30.0
57
+ ) -> "PharoxSDK":
58
+ """Create an SDK instance that calls a remote pharox-service."""
59
+ return cls(client=PharoxClient(base_url, api_key, timeout=timeout))
60
+
61
+ @classmethod
62
+ def local(cls, storage: IAsyncStorage) -> "PharoxSDK":
63
+ """Create an SDK instance that uses a local storage backend directly."""
64
+ return cls(storage=storage)
65
+
66
+ # ------------------------------------------------------------------
67
+ # Lifecycle
68
+ # ------------------------------------------------------------------
69
+
70
+ async def aclose(self) -> None:
71
+ if self._client:
72
+ await self._client.aclose()
73
+
74
+ async def __aenter__(self) -> "PharoxSDK":
75
+ return self
76
+
77
+ async def __aexit__(self, *_: object) -> None:
78
+ await self.aclose()
79
+
80
+ # ------------------------------------------------------------------
81
+ # Pool helpers (remote mode)
82
+ # ------------------------------------------------------------------
83
+
84
+ async def _resolve_pool_id(self, pool_name: str) -> Optional[str]:
85
+ """Look up pool_id by name, using a local name cache."""
86
+ if pool_name in self._pool_cache:
87
+ return self._pool_cache[pool_name]
88
+ if self._client is None: # pragma: no cover
89
+ raise RuntimeError("_resolve_pool_id called in local mode")
90
+ pools = await self._client.list_pools()
91
+ self._pool_cache = {p.name: str(p.id) for p in pools}
92
+ return self._pool_cache.get(pool_name)
93
+
94
+ # ------------------------------------------------------------------
95
+ # Public API — mirrors ProxyManager async semantics
96
+ # ------------------------------------------------------------------
97
+
98
+ async def acquire_proxy(
99
+ self,
100
+ pool_name: str,
101
+ consumer_id: str = "default",
102
+ ttl_seconds: int = 300,
103
+ filters: Optional[ProxyFilters] = None,
104
+ selector: Optional[SelectorStrategy] = None,
105
+ ) -> Optional[Lease]:
106
+ """
107
+ Acquire a proxy lease from the named pool.
108
+
109
+ Returns the Lease on success, or None if no proxy is available.
110
+ """
111
+ if self._client is not None:
112
+ return await self._acquire_remote(
113
+ pool_name, consumer_id, ttl_seconds, filters, selector
114
+ )
115
+ return await self._acquire_local(
116
+ pool_name, consumer_id, ttl_seconds, filters, selector
117
+ )
118
+
119
+ async def release_proxy(self, lease: Lease) -> None:
120
+ """Release a previously acquired lease."""
121
+ if self._client is not None:
122
+ await self._client.release_lease(str(lease.id))
123
+ elif self._storage is not None:
124
+ await self._storage.release_lease(lease)
125
+ else: # pragma: no cover
126
+ raise RuntimeError("SDK has neither client nor storage")
127
+
128
+ @asynccontextmanager
129
+ async def with_lease(
130
+ self,
131
+ pool_name: str,
132
+ consumer_id: str = "default",
133
+ ttl_seconds: int = 300,
134
+ filters: Optional[ProxyFilters] = None,
135
+ selector: Optional[SelectorStrategy] = None,
136
+ ) -> AsyncIterator[Optional[Lease]]:
137
+ """
138
+ Async context manager that acquires a lease and releases it on exit.
139
+
140
+ .. code-block:: python
141
+
142
+ async with sdk.with_lease("my-pool") as lease:
143
+ if lease:
144
+ print(lease.proxy_id)
145
+ """
146
+ lease = await self.acquire_proxy(
147
+ pool_name,
148
+ consumer_id=consumer_id,
149
+ ttl_seconds=ttl_seconds,
150
+ filters=filters,
151
+ selector=selector,
152
+ )
153
+ try:
154
+ yield lease
155
+ finally:
156
+ if lease is not None:
157
+ await self.release_proxy(lease)
158
+
159
+ # ------------------------------------------------------------------
160
+ # Admin helpers (remote mode only)
161
+ # ------------------------------------------------------------------
162
+
163
+ async def create_pool(
164
+ self, name: str, description: str = ""
165
+ ) -> ProxyPool:
166
+ """Create a pool in the remote service."""
167
+ self._require_remote()
168
+ assert self._client is not None
169
+ pool = await self._client.create_pool(name, description)
170
+ self._pool_cache[pool.name] = str(pool.id)
171
+ return pool
172
+
173
+ async def list_pools(self) -> list[ProxyPool]:
174
+ """List all pools from the remote service."""
175
+ self._require_remote()
176
+ assert self._client is not None
177
+ pools = await self._client.list_pools()
178
+ self._pool_cache = {p.name: str(p.id) for p in pools}
179
+ return pools
180
+
181
+ # ------------------------------------------------------------------
182
+ # Private helpers
183
+ # ------------------------------------------------------------------
184
+
185
+ def _require_remote(self) -> None:
186
+ if self._client is None:
187
+ raise RuntimeError(
188
+ "This method is only available in remote mode."
189
+ )
190
+
191
+ async def _acquire_remote(
192
+ self,
193
+ pool_name: str,
194
+ consumer_id: str,
195
+ ttl_seconds: int,
196
+ filters: Optional[ProxyFilters],
197
+ selector: Optional[SelectorStrategy],
198
+ ) -> Optional[Lease]:
199
+ if self._client is None: # pragma: no cover
200
+ raise RuntimeError("_acquire_remote called in local mode")
201
+ pool_id = await self._resolve_pool_id(pool_name)
202
+ if pool_id is None:
203
+ from pharox import PoolNotFoundError
204
+ raise PoolNotFoundError(pool_name)
205
+ try:
206
+ return await self._client.acquire_lease(
207
+ pool_id=pool_id,
208
+ consumer_id=consumer_id,
209
+ ttl_seconds=ttl_seconds,
210
+ filters=filters,
211
+ selector=selector,
212
+ )
213
+ except RemoteError as exc:
214
+ if exc.status_code == 404:
215
+ # Pool was in cache but no longer exists — evict and raise
216
+ self._pool_cache.pop(pool_name, None)
217
+ from pharox import PoolNotFoundError
218
+ raise PoolNotFoundError(pool_name) from exc
219
+ raise
220
+
221
+ async def _acquire_local(
222
+ self,
223
+ pool_name: str,
224
+ consumer_id: str,
225
+ ttl_seconds: int,
226
+ filters: Optional[ProxyFilters],
227
+ selector: Optional[SelectorStrategy],
228
+ ) -> Optional[Lease]:
229
+ if self._storage is None: # pragma: no cover
230
+ raise RuntimeError("_acquire_local called in remote mode")
231
+ await self._storage.ensure_consumer(consumer_id)
232
+ await self._storage.cleanup_expired_leases()
233
+ strategy = selector or SelectorStrategy.FIRST_AVAILABLE
234
+ proxy = await self._storage.find_available_proxy(
235
+ pool_name, filters, strategy
236
+ )
237
+ if proxy is None:
238
+ return None
239
+ return await self._storage.create_lease(
240
+ proxy, consumer_id, ttl_seconds
241
+ )