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.
@@ -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]")