asockslib 0.1.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.
- asockslib/__init__.py +117 -0
- asockslib/_console.py +18 -0
- asockslib/_port_utils.py +132 -0
- asockslib/benchmark.py +365 -0
- asockslib/cli/__init__.py +72 -0
- asockslib/cli/_helpers.py +162 -0
- asockslib/cli/_output.py +52 -0
- asockslib/cli/api_commands.py +377 -0
- asockslib/cli/commands.py +485 -0
- asockslib/client.py +634 -0
- asockslib/exceptions.py +89 -0
- asockslib/geo.json +649269 -0
- asockslib/geo_picker/__init__.py +30 -0
- asockslib/geo_picker/_completer.py +90 -0
- asockslib/geo_picker/_messages.py +89 -0
- asockslib/geo_picker/_types.py +89 -0
- asockslib/geo_picker/picker.py +667 -0
- asockslib/models/__init__.py +72 -0
- asockslib/models/directory.py +65 -0
- asockslib/models/enums.py +65 -0
- asockslib/models/port.py +187 -0
- asockslib/models/requests.py +89 -0
- asockslib/models/responses.py +20 -0
- asockslib/proxy_pool.py +659 -0
- asockslib/quick.py +174 -0
- asockslib/smart_proxy.py +251 -0
- asockslib-0.1.0.dist-info/METADATA +228 -0
- asockslib-0.1.0.dist-info/RECORD +30 -0
- asockslib-0.1.0.dist-info/WHEEL +4 -0
- asockslib-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""Main CLI commands: wizard, get, balance, list, delete, info.
|
|
2
|
+
|
|
3
|
+
User-facing commands that form the primary CLI experience.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import csv
|
|
9
|
+
import json
|
|
10
|
+
from io import StringIO
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
from beartype import beartype
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
from asockslib._console import console
|
|
18
|
+
from asockslib.cli._helpers import (
|
|
19
|
+
ExportFormat,
|
|
20
|
+
apply_proxy_template,
|
|
21
|
+
format_output,
|
|
22
|
+
get_api_key,
|
|
23
|
+
run_async,
|
|
24
|
+
)
|
|
25
|
+
from asockslib.cli._output import print_copy_result, print_proxy_table
|
|
26
|
+
from asockslib.client import ASocksClient
|
|
27
|
+
from asockslib.geo_picker import GeoPicker
|
|
28
|
+
from asockslib.models import CreatePortRequest, PortFilterParams
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def register_commands(app: typer.Typer) -> None:
|
|
32
|
+
"""Register all main commands on the Typer app."""
|
|
33
|
+
|
|
34
|
+
@app.command()
|
|
35
|
+
@beartype
|
|
36
|
+
def wizard() -> None:
|
|
37
|
+
"""Interactive wizard — create proxies or find the fastest ones.
|
|
38
|
+
|
|
39
|
+
Step-by-step interactive assistant:
|
|
40
|
+
|
|
41
|
+
1. Action: create proxies / find best
|
|
42
|
+
2. Proxy type, connection type, port type
|
|
43
|
+
3. Country -> state -> city (fuzzy search)
|
|
44
|
+
4. Count, TTL, name
|
|
45
|
+
5. Export format and output file
|
|
46
|
+
6. Benchmark settings (for find-best mode)
|
|
47
|
+
|
|
48
|
+
Example::
|
|
49
|
+
|
|
50
|
+
asocks wizard
|
|
51
|
+
"""
|
|
52
|
+
picker = GeoPicker()
|
|
53
|
+
params = picker.pick_wizard()
|
|
54
|
+
|
|
55
|
+
action = params.get("action", "create")
|
|
56
|
+
country = str(params["country_code"]) if params["country_code"] else "US"
|
|
57
|
+
state = str(params["state"]) if params["state"] else ""
|
|
58
|
+
city = str(params["city"]) if params["city"] else ""
|
|
59
|
+
count = int(params.get("count") or 10)
|
|
60
|
+
ttl = int(params.get("ttl") or 1)
|
|
61
|
+
name = str(params.get("name") or "")
|
|
62
|
+
type_id = int(params.get("type_id") or 1)
|
|
63
|
+
proxy_type_id = int(params.get("proxy_type_id") or 1)
|
|
64
|
+
server_port_type_id = int(params.get("server_port_type_id") or 0)
|
|
65
|
+
traffic_limit = int(params.get("traffic_limit") or 10)
|
|
66
|
+
fmt_str = str(params.get("format") or "txt")
|
|
67
|
+
fmt = ExportFormat(fmt_str) if fmt_str in ("txt", "json", "csv") else ExportFormat.txt
|
|
68
|
+
output = str(params.get("output") or "")
|
|
69
|
+
timeout = float(params.get("timeout") or 15.0)
|
|
70
|
+
concurrency = int(params.get("concurrency") or 50)
|
|
71
|
+
proxy_template = str(
|
|
72
|
+
params.get("proxy_template") or "{protocol}://{login}:{password}@{ip}:{port}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
action_label = "Find best" if action == "best" else "Create"
|
|
76
|
+
summary = Table(title="📋 Parameters")
|
|
77
|
+
summary.add_column("", style="cyan")
|
|
78
|
+
summary.add_column("", style="green")
|
|
79
|
+
summary.add_row("Action", action_label)
|
|
80
|
+
summary.add_row("Country", country)
|
|
81
|
+
if state:
|
|
82
|
+
summary.add_row("State", state)
|
|
83
|
+
if city:
|
|
84
|
+
summary.add_row("City", city)
|
|
85
|
+
summary.add_row("Count", str(count))
|
|
86
|
+
if action == "best":
|
|
87
|
+
keep = int(params.get("keep") or 10)
|
|
88
|
+
summary.add_row("Keep best", str(keep))
|
|
89
|
+
summary.add_row("TTL", f"{ttl} days")
|
|
90
|
+
summary.add_row("Format", fmt_str)
|
|
91
|
+
if output:
|
|
92
|
+
summary.add_row("Output", output)
|
|
93
|
+
console.print(summary)
|
|
94
|
+
console.print()
|
|
95
|
+
|
|
96
|
+
if action == "best":
|
|
97
|
+
_wizard_best_proxies(
|
|
98
|
+
country=country,
|
|
99
|
+
state=state,
|
|
100
|
+
city=city,
|
|
101
|
+
count=count,
|
|
102
|
+
keep=int(params.get("keep") or 10),
|
|
103
|
+
ttl=ttl,
|
|
104
|
+
name=name,
|
|
105
|
+
type_id=type_id,
|
|
106
|
+
proxy_type_id=proxy_type_id,
|
|
107
|
+
server_port_type_id=server_port_type_id,
|
|
108
|
+
traffic_limit=traffic_limit,
|
|
109
|
+
fmt=fmt,
|
|
110
|
+
output=output,
|
|
111
|
+
timeout=timeout,
|
|
112
|
+
concurrency=concurrency,
|
|
113
|
+
proxy_template=proxy_template,
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
_wizard_create_proxies(
|
|
117
|
+
country=country,
|
|
118
|
+
state=state,
|
|
119
|
+
city=city,
|
|
120
|
+
count=count,
|
|
121
|
+
ttl=ttl,
|
|
122
|
+
name=name,
|
|
123
|
+
type_id=type_id,
|
|
124
|
+
proxy_type_id=proxy_type_id,
|
|
125
|
+
server_port_type_id=server_port_type_id,
|
|
126
|
+
traffic_limit=traffic_limit,
|
|
127
|
+
fmt=fmt,
|
|
128
|
+
output=output,
|
|
129
|
+
proxy_template=proxy_template,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@app.command()
|
|
133
|
+
@beartype
|
|
134
|
+
def get(
|
|
135
|
+
country: str = typer.Argument(..., help="Country code (US, DE, GB, ...)"),
|
|
136
|
+
count: int = typer.Option(10, "--count", "-n", help="Number of proxies"),
|
|
137
|
+
verify: bool = typer.Option(
|
|
138
|
+
False, "--verify", "-v", help="Verify proxies before returning"
|
|
139
|
+
),
|
|
140
|
+
fmt: ExportFormat = typer.Option( # noqa: B008 — required Typer CLI pattern
|
|
141
|
+
ExportFormat.txt,
|
|
142
|
+
"--format",
|
|
143
|
+
"-f",
|
|
144
|
+
help="Export format: txt/json/csv",
|
|
145
|
+
),
|
|
146
|
+
output: str | None = typer.Option(None, "--output", "-o", help="Output file"),
|
|
147
|
+
timeout: float = typer.Option(3.0, "--timeout", "-t", help="Verify timeout (sec)"),
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Get proxies quickly — one command, one country code.
|
|
150
|
+
|
|
151
|
+
Examples::
|
|
152
|
+
|
|
153
|
+
asocks get US
|
|
154
|
+
asocks get DE -n 20
|
|
155
|
+
asocks get GB -n 5 --verify
|
|
156
|
+
asocks get US -n 100 -f json -o proxies.json
|
|
157
|
+
"""
|
|
158
|
+
from asockslib.quick import get_proxies
|
|
159
|
+
|
|
160
|
+
async def _get() -> list[str]:
|
|
161
|
+
return await get_proxies(
|
|
162
|
+
country.upper(),
|
|
163
|
+
count=count,
|
|
164
|
+
api_key=get_api_key(),
|
|
165
|
+
verify=verify,
|
|
166
|
+
timeout=timeout,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
with console.status(f"[bold]Getting {count} proxies for {country.upper()}...[/bold]"):
|
|
170
|
+
urls = run_async(_get())
|
|
171
|
+
|
|
172
|
+
if not urls:
|
|
173
|
+
console.print("[yellow]No proxies created.[/yellow]")
|
|
174
|
+
raise typer.Exit()
|
|
175
|
+
|
|
176
|
+
content = format_output(urls, fmt)
|
|
177
|
+
if output:
|
|
178
|
+
with open(output, "w") as f:
|
|
179
|
+
f.write(content)
|
|
180
|
+
console.print(f"[green]✅ Saved {len(urls)} proxies to {output}[/green]")
|
|
181
|
+
else:
|
|
182
|
+
print_proxy_table(urls, title=f"Proxies ({len(urls)})")
|
|
183
|
+
|
|
184
|
+
@app.command()
|
|
185
|
+
@beartype
|
|
186
|
+
def balance() -> None:
|
|
187
|
+
"""Show account balance."""
|
|
188
|
+
|
|
189
|
+
async def _balance() -> tuple[float, float, float]:
|
|
190
|
+
async with ASocksClient(api_key=get_api_key()) as client:
|
|
191
|
+
bal = await client.get_balance()
|
|
192
|
+
return bal.balance, bal.balance_traffic, bal.all_available_traffic
|
|
193
|
+
|
|
194
|
+
b, bt, aat = run_async(_balance())
|
|
195
|
+
table = Table(title="💰 Balance")
|
|
196
|
+
table.add_column("Metric", style="cyan")
|
|
197
|
+
table.add_column("Value", style="green")
|
|
198
|
+
table.add_row("Balance (USD)", f"${b:.4f}")
|
|
199
|
+
table.add_row("Traffic Balance", f"{bt}")
|
|
200
|
+
table.add_row("Available Traffic", f"{aat}")
|
|
201
|
+
console.print(table)
|
|
202
|
+
|
|
203
|
+
@app.command(name="list")
|
|
204
|
+
@beartype
|
|
205
|
+
def list_ports(
|
|
206
|
+
country: str | None = typer.Option(None, "--country", "-c", help="Filter by country code"),
|
|
207
|
+
status: int | None = typer.Option(
|
|
208
|
+
None, "--status", help="Filter: 0=inactive, 1=active, 2=expired"
|
|
209
|
+
),
|
|
210
|
+
page: int = typer.Option(1, "--page", "-p", help="Page number"),
|
|
211
|
+
per_page: int = typer.Option(50, "--per-page", help="Items per page"),
|
|
212
|
+
) -> None:
|
|
213
|
+
"""List proxy ports.
|
|
214
|
+
|
|
215
|
+
Examples::
|
|
216
|
+
|
|
217
|
+
asocks list
|
|
218
|
+
asocks list --country US
|
|
219
|
+
asocks list --status 1
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
async def _list() -> tuple[list[Any], int]:
|
|
223
|
+
async with ASocksClient(api_key=get_api_key()) as client:
|
|
224
|
+
filters = PortFilterParams(
|
|
225
|
+
countryName=country,
|
|
226
|
+
status=status,
|
|
227
|
+
page=page,
|
|
228
|
+
per_page=per_page,
|
|
229
|
+
)
|
|
230
|
+
resp = await client.list_ports(filters)
|
|
231
|
+
return resp.items, resp.total
|
|
232
|
+
|
|
233
|
+
items, total = run_async(_list())
|
|
234
|
+
table = Table(title=f"Ports (page {page}, total {total})")
|
|
235
|
+
table.add_column("ID", style="cyan")
|
|
236
|
+
table.add_column("Name", style="white")
|
|
237
|
+
table.add_column("Protocol", style="blue")
|
|
238
|
+
table.add_column("Host:Port", style="green")
|
|
239
|
+
table.add_column("Country", style="yellow")
|
|
240
|
+
table.add_column("Status", style="magenta")
|
|
241
|
+
proxy_urls: list[str] = []
|
|
242
|
+
for p in items:
|
|
243
|
+
status_emoji = (
|
|
244
|
+
"🟢" if str(p.status) == "1" else ("🔴" if str(p.status) == "0" else "⏰")
|
|
245
|
+
)
|
|
246
|
+
table.add_row(
|
|
247
|
+
str(p.id),
|
|
248
|
+
p.name or "-",
|
|
249
|
+
p.protocol,
|
|
250
|
+
f"{p.host}:{p.port}",
|
|
251
|
+
p.country_code or p.country,
|
|
252
|
+
f"{status_emoji} {p.status}",
|
|
253
|
+
)
|
|
254
|
+
proxy_urls.append(p.proxy_url)
|
|
255
|
+
console.print(table)
|
|
256
|
+
|
|
257
|
+
if proxy_urls:
|
|
258
|
+
print_copy_result(proxy_urls)
|
|
259
|
+
|
|
260
|
+
@app.command()
|
|
261
|
+
@beartype
|
|
262
|
+
def info(
|
|
263
|
+
port_id: int = typer.Argument(..., help="Port ID"),
|
|
264
|
+
) -> None:
|
|
265
|
+
"""Show port details."""
|
|
266
|
+
|
|
267
|
+
async def _info() -> dict[str, Any]:
|
|
268
|
+
async with ASocksClient(api_key=get_api_key()) as client:
|
|
269
|
+
p = await client.get_port(port_id)
|
|
270
|
+
return p.model_dump()
|
|
271
|
+
|
|
272
|
+
data = run_async(_info())
|
|
273
|
+
console.print_json(json.dumps(data, indent=2, default=str))
|
|
274
|
+
|
|
275
|
+
@app.command()
|
|
276
|
+
@beartype
|
|
277
|
+
def delete(
|
|
278
|
+
port_id: int = typer.Argument(..., help="Port ID to delete"),
|
|
279
|
+
) -> None:
|
|
280
|
+
"""Delete (release) a proxy port."""
|
|
281
|
+
|
|
282
|
+
async def _delete() -> bool:
|
|
283
|
+
async with ASocksClient(api_key=get_api_key()) as client:
|
|
284
|
+
return await client.delete_port(port_id)
|
|
285
|
+
|
|
286
|
+
ok = run_async(_delete())
|
|
287
|
+
if ok:
|
|
288
|
+
console.print(f"[green]✅ Port {port_id} deleted.[/green]")
|
|
289
|
+
else:
|
|
290
|
+
console.print(f"[red]❌ Failed to delete port {port_id}.[/red]")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ── Wizard sub-commands ───────────────────────────────────────────────────── #
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@beartype
|
|
297
|
+
def _wizard_create_proxies(
|
|
298
|
+
*,
|
|
299
|
+
country: str,
|
|
300
|
+
state: str,
|
|
301
|
+
city: str,
|
|
302
|
+
count: int,
|
|
303
|
+
ttl: int,
|
|
304
|
+
name: str,
|
|
305
|
+
type_id: int,
|
|
306
|
+
proxy_type_id: int,
|
|
307
|
+
server_port_type_id: int,
|
|
308
|
+
traffic_limit: int,
|
|
309
|
+
fmt: ExportFormat,
|
|
310
|
+
output: str,
|
|
311
|
+
proxy_template: str = "{protocol}://{login}:{password}@{ip}:{port}",
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Create proxies from wizard parameters."""
|
|
314
|
+
|
|
315
|
+
async def _create() -> list[str]:
|
|
316
|
+
req = CreatePortRequest(
|
|
317
|
+
country_code=country,
|
|
318
|
+
city=city,
|
|
319
|
+
state=state,
|
|
320
|
+
name=name,
|
|
321
|
+
count=count,
|
|
322
|
+
ttl=ttl,
|
|
323
|
+
type_id=type_id,
|
|
324
|
+
proxy_type_id=proxy_type_id,
|
|
325
|
+
server_port_type_id=server_port_type_id,
|
|
326
|
+
traffic_limit=traffic_limit,
|
|
327
|
+
)
|
|
328
|
+
async with ASocksClient(api_key=get_api_key()) as client:
|
|
329
|
+
ports = await client.create_ports(req)
|
|
330
|
+
return [p.format_with_template(proxy_template) for p in ports]
|
|
331
|
+
|
|
332
|
+
with console.status("[bold]Creating proxies...[/bold]"):
|
|
333
|
+
urls = run_async(_create())
|
|
334
|
+
|
|
335
|
+
if not urls:
|
|
336
|
+
console.print("[yellow]No ports were created.[/yellow]")
|
|
337
|
+
raise typer.Exit()
|
|
338
|
+
|
|
339
|
+
content = format_output(urls, fmt)
|
|
340
|
+
if output:
|
|
341
|
+
with open(output, "w") as f:
|
|
342
|
+
f.write(content)
|
|
343
|
+
console.print(f"[green]✅ Saved {len(urls)} proxies to {output}[/green]")
|
|
344
|
+
else:
|
|
345
|
+
print_proxy_table(urls, title=f"✅ Created {len(urls)} proxies")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@beartype
|
|
349
|
+
def _wizard_best_proxies(
|
|
350
|
+
*,
|
|
351
|
+
country: str,
|
|
352
|
+
state: str,
|
|
353
|
+
city: str,
|
|
354
|
+
count: int,
|
|
355
|
+
keep: int,
|
|
356
|
+
ttl: int,
|
|
357
|
+
name: str,
|
|
358
|
+
type_id: int,
|
|
359
|
+
proxy_type_id: int,
|
|
360
|
+
server_port_type_id: int,
|
|
361
|
+
traffic_limit: int,
|
|
362
|
+
fmt: ExportFormat,
|
|
363
|
+
output: str,
|
|
364
|
+
timeout: float,
|
|
365
|
+
concurrency: int,
|
|
366
|
+
proxy_template: str = "{protocol}://{login}:{password}@{ip}:{port}",
|
|
367
|
+
) -> None:
|
|
368
|
+
"""Find best proxies: create -> benchmark -> keep fastest."""
|
|
369
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
|
|
370
|
+
|
|
371
|
+
from asockslib.benchmark import FindBestResult, find_best_proxies
|
|
372
|
+
|
|
373
|
+
with Progress(
|
|
374
|
+
SpinnerColumn(),
|
|
375
|
+
TextColumn("[progress.description]{task.description}"),
|
|
376
|
+
BarColumn(),
|
|
377
|
+
TextColumn("{task.completed}/{task.total}"),
|
|
378
|
+
console=console,
|
|
379
|
+
) as progress:
|
|
380
|
+
task_id = progress.add_task("Initializing...", total=count)
|
|
381
|
+
|
|
382
|
+
def _progress_cb(stage: str, current: int, total_n: int) -> None:
|
|
383
|
+
labels = {
|
|
384
|
+
"creating": "🏗️ Creating proxies",
|
|
385
|
+
"benchmarking": "⚡ Benchmarking",
|
|
386
|
+
"deleting": "🗑️ Deleting slow proxies",
|
|
387
|
+
}
|
|
388
|
+
progress.update(
|
|
389
|
+
task_id,
|
|
390
|
+
description=labels.get(stage, stage),
|
|
391
|
+
completed=current,
|
|
392
|
+
total=total_n,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
async def _find() -> FindBestResult:
|
|
396
|
+
async with ASocksClient(api_key=get_api_key()) as client:
|
|
397
|
+
return await find_best_proxies(
|
|
398
|
+
client,
|
|
399
|
+
country_code=country,
|
|
400
|
+
city=city,
|
|
401
|
+
state=state,
|
|
402
|
+
total=count,
|
|
403
|
+
keep=keep,
|
|
404
|
+
timeout=timeout,
|
|
405
|
+
concurrency=concurrency,
|
|
406
|
+
proxy_type_id=proxy_type_id,
|
|
407
|
+
server_port_type_id=server_port_type_id,
|
|
408
|
+
progress_callback=_progress_cb,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
result = run_async(_find())
|
|
412
|
+
|
|
413
|
+
# Summary table
|
|
414
|
+
console.print()
|
|
415
|
+
summary = Table(title="🏆 Best Proxies Summary")
|
|
416
|
+
summary.add_column("Metric", style="cyan")
|
|
417
|
+
summary.add_column("Value", style="green")
|
|
418
|
+
summary.add_row("Created", str(result.total_created))
|
|
419
|
+
summary.add_row("Tested", str(result.total_tested))
|
|
420
|
+
summary.add_row("Alive", str(result.total_alive))
|
|
421
|
+
summary.add_row("Best kept", str(len(result.best)))
|
|
422
|
+
summary.add_row("Deleted", str(result.total_deleted))
|
|
423
|
+
if result.delete_errors:
|
|
424
|
+
summary.add_row("Delete errors", str(len(result.delete_errors)))
|
|
425
|
+
summary.add_row("Avg latency", f"{result.avg_latency_ms:.0f} ms")
|
|
426
|
+
summary.add_row("Min latency", f"{result.min_latency_ms:.0f} ms")
|
|
427
|
+
summary.add_row("Max latency", f"{result.max_latency_ms:.0f} ms")
|
|
428
|
+
console.print(summary)
|
|
429
|
+
|
|
430
|
+
# Best proxies table
|
|
431
|
+
proxy_urls = [
|
|
432
|
+
apply_proxy_template(p.proxy_url, proxy_template, port_id=p.port_id) for p in result.best
|
|
433
|
+
]
|
|
434
|
+
if result.best:
|
|
435
|
+
console.print()
|
|
436
|
+
proxy_table = Table(title=f"Top {len(result.best)} Proxies")
|
|
437
|
+
proxy_table.add_column("#", style="dim")
|
|
438
|
+
proxy_table.add_column("Latency", style="yellow")
|
|
439
|
+
proxy_table.add_column("IP", style="cyan")
|
|
440
|
+
proxy_table.add_column("Port ID", style="dim")
|
|
441
|
+
proxy_table.add_column("Proxy", style="green", overflow="fold")
|
|
442
|
+
for i, (p, formatted) in enumerate(zip(result.best, proxy_urls, strict=True), 1):
|
|
443
|
+
lat = f"{p.latency_ms:.0f} ms" if p.latency_ms else "N/A"
|
|
444
|
+
proxy_table.add_row(str(i), lat, p.external_ip, str(p.port_id), formatted)
|
|
445
|
+
console.print(proxy_table)
|
|
446
|
+
|
|
447
|
+
print_copy_result(proxy_urls)
|
|
448
|
+
|
|
449
|
+
# Export
|
|
450
|
+
if fmt == ExportFormat.txt:
|
|
451
|
+
export_data = "\n".join(proxy_urls) + "\n"
|
|
452
|
+
elif fmt == ExportFormat.json:
|
|
453
|
+
export_data = json.dumps(
|
|
454
|
+
[
|
|
455
|
+
{
|
|
456
|
+
"proxy": apply_proxy_template(p.proxy_url, proxy_template, port_id=p.port_id),
|
|
457
|
+
"port_id": p.port_id,
|
|
458
|
+
"latency_ms": p.latency_ms,
|
|
459
|
+
"external_ip": p.external_ip,
|
|
460
|
+
}
|
|
461
|
+
for p in result.best
|
|
462
|
+
],
|
|
463
|
+
indent=2,
|
|
464
|
+
)
|
|
465
|
+
else:
|
|
466
|
+
buf = StringIO()
|
|
467
|
+
writer = csv.writer(buf)
|
|
468
|
+
writer.writerow(["proxy", "port_id", "latency_ms", "external_ip"])
|
|
469
|
+
for p in result.best:
|
|
470
|
+
writer.writerow(
|
|
471
|
+
[
|
|
472
|
+
apply_proxy_template(p.proxy_url, proxy_template, port_id=p.port_id),
|
|
473
|
+
p.port_id,
|
|
474
|
+
p.latency_ms,
|
|
475
|
+
p.external_ip,
|
|
476
|
+
]
|
|
477
|
+
)
|
|
478
|
+
export_data = buf.getvalue()
|
|
479
|
+
|
|
480
|
+
if output:
|
|
481
|
+
with open(output, "w") as f:
|
|
482
|
+
f.write(export_data)
|
|
483
|
+
console.print(f"\n[green]✅ Saved to {output}[/green]")
|
|
484
|
+
elif not result.best:
|
|
485
|
+
console.print("[yellow]No proxies survived benchmarking.[/yellow]")
|