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,162 @@
1
+ """CLI helper utilities.
2
+
3
+ Contains non-UI functions: clipboard operations, export formatting,
4
+ API key resolution, async runner, and proxy template application.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import csv
11
+ import os
12
+ import platform
13
+ import shutil
14
+ import subprocess
15
+ from enum import StrEnum
16
+ from io import StringIO
17
+ from typing import TYPE_CHECKING
18
+
19
+ import typer
20
+ from beartype import beartype
21
+
22
+ from asockslib._console import console
23
+ from asockslib.exceptions import ASocksError
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Coroutine
27
+
28
+
29
+ class ExportFormat(StrEnum):
30
+ """Supported export formats."""
31
+
32
+ txt = "txt"
33
+ json = "json"
34
+ csv = "csv"
35
+
36
+
37
+ # ── Clipboard ─────────────────────────────────────────────────────────────── #
38
+
39
+
40
+ @beartype
41
+ def copy_to_clipboard(text: str) -> bool:
42
+ """Try to copy text to the system clipboard.
43
+
44
+ Supports macOS (pbcopy), Linux (xclip/xsel) and Windows (clip).
45
+ """
46
+ system = platform.system()
47
+ try:
48
+ if system == "Darwin" and shutil.which("pbcopy"):
49
+ subprocess.run(["pbcopy"], input=text.encode(), check=True) # noqa: S603, S607
50
+ return True
51
+ if system == "Linux":
52
+ for cmd in ("xclip", "xsel"):
53
+ if shutil.which(cmd):
54
+ args = (
55
+ [cmd, "-selection", "clipboard"]
56
+ if cmd == "xclip"
57
+ else [cmd, "--clipboard", "--input"]
58
+ )
59
+ subprocess.run(args, input=text.encode(), check=True) # noqa: S603
60
+ return True
61
+ if system == "Windows" and shutil.which("clip"):
62
+ subprocess.run(["clip"], input=text.encode(), check=True) # noqa: S603, S607
63
+ return True
64
+ except (subprocess.SubprocessError, OSError):
65
+ pass
66
+ return False
67
+
68
+
69
+ # ── API key ───────────────────────────────────────────────────────────────── #
70
+
71
+
72
+ @beartype
73
+ def get_api_key() -> str:
74
+ """Read API key from ``ASOCKS_API_KEY`` env var."""
75
+ key = os.environ.get("ASOCKS_API_KEY", "")
76
+ if not key:
77
+ console.print("[red]Error:[/red] ASOCKS_API_KEY environment variable not set.")
78
+ raise typer.Exit(code=1)
79
+ return key
80
+
81
+
82
+ # ── Async runner ──────────────────────────────────────────────────────────── #
83
+
84
+
85
+ def run_async[T](coro: Coroutine[object, object, T]) -> T:
86
+ """Run a coroutine synchronously with error handling."""
87
+ try:
88
+ return asyncio.run(coro)
89
+ except ASocksError as exc:
90
+ console.print(f"[red]Error:[/red] {exc.message}")
91
+ raise typer.Exit(code=1) from None
92
+
93
+
94
+ # ── Export formatting ─────────────────────────────────────────────────────── #
95
+
96
+
97
+ @beartype
98
+ def format_output(proxies: list[str], fmt: ExportFormat) -> str:
99
+ """Format proxy list for export."""
100
+ import json
101
+
102
+ if fmt == ExportFormat.txt:
103
+ return "\n".join(proxies)
104
+ if fmt == ExportFormat.json:
105
+ return json.dumps(proxies, indent=2)
106
+ buf = StringIO()
107
+ writer = csv.writer(buf)
108
+ writer.writerow(["proxy_url"])
109
+ for p in proxies:
110
+ writer.writerow([p])
111
+ return buf.getvalue()
112
+
113
+
114
+ # ── Proxy template application ────────────────────────────────────────────── #
115
+
116
+
117
+ @beartype
118
+ def apply_proxy_template(
119
+ proxy_url: str,
120
+ template: str,
121
+ port_id: int = 0,
122
+ name: str = "",
123
+ ) -> str:
124
+ """Apply a template to a standard proxy URL.
125
+
126
+ Parses ``protocol://login:password@host:port`` and substitutes
127
+ values into template placeholders.
128
+ """
129
+ protocol = "socks5"
130
+ login = ""
131
+ password = ""
132
+ host = ""
133
+ port_str = "0"
134
+ try:
135
+ proto_rest = proxy_url.split("://", 1)
136
+ protocol = proto_rest[0]
137
+ rest = proto_rest[1] if len(proto_rest) > 1 else proxy_url
138
+ if "@" in rest:
139
+ auth, hostport = rest.rsplit("@", 1)
140
+ parts = auth.split(":", 1)
141
+ login = parts[0]
142
+ password = parts[1] if len(parts) > 1 else ""
143
+ else:
144
+ hostport = rest
145
+ hp = hostport.rsplit(":", 1)
146
+ host = hp[0]
147
+ port_str = hp[1] if len(hp) > 1 else "0"
148
+ except (IndexError, ValueError):
149
+ pass
150
+
151
+ refresh = f"https://api.asocks.com/v2/proxy/refresh-ip/{port_id}"
152
+ return (
153
+ template.replace("{protocol}", protocol)
154
+ .replace("{id}", str(port_id))
155
+ .replace("{login}", login)
156
+ .replace("{password}", password)
157
+ .replace("{ip}", host)
158
+ .replace("{port}", port_str)
159
+ .replace("{refresh_link}", refresh)
160
+ .replace("{name}", name)
161
+ .replace("{external_ip}", "")
162
+ )
@@ -0,0 +1,52 @@
1
+ """Rich console output functions for the CLI.
2
+
3
+ All Rich table printing and clipboard notification logic is
4
+ centralized here, keeping command handlers clean.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from beartype import beartype
10
+ from rich.table import Table
11
+
12
+ from asockslib._console import console
13
+ from asockslib.cli._helpers import copy_to_clipboard
14
+
15
+
16
+ @beartype
17
+ def print_proxy_table(
18
+ urls: list[str],
19
+ *,
20
+ title: str = "Proxies",
21
+ url_header: str = "Proxy",
22
+ ) -> None:
23
+ """Print a proxy table and auto-copy to clipboard."""
24
+ table = Table(title=title)
25
+ table.add_column("#", style="dim")
26
+ table.add_column(url_header, style="green", overflow="fold")
27
+ for i, u in enumerate(urls, 1):
28
+ table.add_row(str(i), u)
29
+ console.print(table)
30
+
31
+ plain = "\n".join(urls)
32
+ copied = copy_to_clipboard(plain)
33
+
34
+ console.print()
35
+ if copied:
36
+ console.print(f"[green]📋 Copied {len(urls)} proxies to clipboard![/green]")
37
+ else:
38
+ console.print("[dim]── Copy-friendly list ──[/dim]")
39
+ console.print(plain)
40
+ console.print("[dim]────────────────────────[/dim]")
41
+
42
+
43
+ @beartype
44
+ def print_copy_result(urls: list[str]) -> None:
45
+ """Copy proxy URLs to clipboard and show result."""
46
+ plain = "\n".join(urls)
47
+ if copy_to_clipboard(plain):
48
+ console.print(f"\n[green]📋 Copied {len(urls)} proxies to clipboard![/green]")
49
+ else:
50
+ console.print("\n[dim]── Copy-friendly list ──[/dim]")
51
+ console.print(plain)
52
+ console.print("[dim]────────────────────────[/dim]")
@@ -0,0 +1,377 @@
1
+ """Raw API sub-commands (``asocks api ...``).
2
+
3
+ Low-level CLI wrappers around every ASocks API endpoint.
4
+ Intended for advanced users and debugging.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Any
11
+
12
+ import typer
13
+ from beartype import beartype
14
+ from rich.table import Table
15
+
16
+ from asockslib._console import console
17
+ from asockslib.cli._helpers import ExportFormat, format_output, get_api_key, run_async
18
+ from asockslib.client import ASocksClient
19
+ from asockslib.models import (
20
+ CityInfo,
21
+ CountryInfo,
22
+ CreatePortRequest,
23
+ CreateTemplateRequest,
24
+ StateInfo,
25
+ WhitelistAddRequest,
26
+ )
27
+
28
+
29
+ def register_api_commands(api_app: typer.Typer) -> None:
30
+ """Register all raw API sub-commands on the Typer app."""
31
+
32
+ @api_app.command()
33
+ @beartype
34
+ def countries() -> None:
35
+ """List countries (GET /v2/proxy/countries)."""
36
+
37
+ async def _countries() -> list[CountryInfo]:
38
+ async with ASocksClient(api_key=get_api_key()) as client:
39
+ return await client.get_countries()
40
+
41
+ items = run_async(_countries())
42
+ table = Table(title=f"Countries ({len(items)})")
43
+ table.add_column("ID", style="cyan")
44
+ table.add_column("Name", style="green")
45
+ table.add_column("Code", style="yellow")
46
+ for c in items:
47
+ table.add_row(str(c.id), c.name, c.code)
48
+ console.print(table)
49
+
50
+ @api_app.command()
51
+ @beartype
52
+ def states(
53
+ country_id: int = typer.Option(..., "--country-id", "-c", help="Country ID"),
54
+ ) -> None:
55
+ """List states/regions (GET /v2/proxy/states)."""
56
+
57
+ async def _states() -> list[StateInfo]:
58
+ async with ASocksClient(api_key=get_api_key()) as client:
59
+ return await client.get_states(country_id=country_id)
60
+
61
+ items = run_async(_states())
62
+ table = Table(title=f"States ({len(items)})")
63
+ table.add_column("ID", style="cyan")
64
+ table.add_column("Name", style="green")
65
+ for s in items:
66
+ table.add_row(str(s.id), s.name)
67
+ console.print(table)
68
+
69
+ @api_app.command()
70
+ @beartype
71
+ def cities(
72
+ country_id: int = typer.Option(..., "--country-id", "-c", help="Country ID"),
73
+ state_id: int | None = typer.Option(None, "--state-id", "-s", help="State ID"),
74
+ ) -> None:
75
+ """List cities (GET /v2/proxy/cities)."""
76
+
77
+ async def _cities() -> list[CityInfo]:
78
+ async with ASocksClient(api_key=get_api_key()) as client:
79
+ return await client.get_cities(country_id=country_id, state_id=state_id)
80
+
81
+ items = run_async(_cities())
82
+ table = Table(title=f"Cities ({len(items)})")
83
+ table.add_column("ID", style="cyan")
84
+ table.add_column("Name", style="green")
85
+ for c in items:
86
+ table.add_row(str(c.id), c.name)
87
+ console.print(table)
88
+
89
+ @api_app.command()
90
+ @beartype
91
+ def asns(
92
+ country_id: int | None = typer.Option(None, "--country-id", "-c", help="Country ID"),
93
+ state_id: int | None = typer.Option(None, "--state-id", "-s", help="State ID"),
94
+ city_id: int | None = typer.Option(None, "--city-id", help="City ID"),
95
+ page: int = typer.Option(1, "--page", "-p", help="Page number"),
96
+ ) -> None:
97
+ """List ASN providers (GET /v2/proxy/asn)."""
98
+ from asockslib.models import ASNListResponse
99
+
100
+ async def _asns() -> ASNListResponse:
101
+ async with ASocksClient(api_key=get_api_key()) as client:
102
+ return await client.get_asns(
103
+ country_id=country_id,
104
+ state_id=state_id,
105
+ city_id=city_id,
106
+ page=page,
107
+ )
108
+
109
+ response = run_async(_asns())
110
+ items = response.items
111
+ table = Table(
112
+ title=(
113
+ f"ASNs (page {response.current_page}/{response.last_page}, total {response.total})"
114
+ )
115
+ )
116
+ table.add_column("ASN", style="cyan")
117
+ table.add_column("Name", style="green")
118
+ for a in items:
119
+ table.add_row(str(a.asn), a.name)
120
+ console.print(table)
121
+
122
+ @api_app.command()
123
+ @beartype
124
+ def plan() -> None:
125
+ """Show plan info (GET /v2/plan/info)."""
126
+
127
+ async def _plan() -> dict[str, Any]:
128
+ async with ASocksClient(api_key=get_api_key()) as client:
129
+ return await client.get_plan_info()
130
+
131
+ data = run_async(_plan())
132
+ msg = data.get("message", data)
133
+ console.print_json(json.dumps(msg, indent=2, default=str))
134
+
135
+ @api_app.command()
136
+ @beartype
137
+ def search(
138
+ limit: int = typer.Argument(10, help="Number of proxies"),
139
+ country: str = typer.Option("", "--country", "-c", help="ISO country code"),
140
+ ) -> None:
141
+ """Search proxies without creating ports (POST /v2/proxy/search)."""
142
+
143
+ async def _search() -> list[str]:
144
+ async with ASocksClient(api_key=get_api_key()) as client:
145
+ return await client.search_proxies(country=country, limit=limit)
146
+
147
+ proxies = run_async(_search())
148
+ if not proxies:
149
+ console.print("[yellow]No proxies found.[/yellow]")
150
+ raise typer.Exit()
151
+ table = Table(title=f"Available Proxies ({len(proxies)})")
152
+ table.add_column("#", style="dim")
153
+ table.add_column("Proxy (IP:Port)", style="green")
154
+ for i, p in enumerate(proxies, 1):
155
+ table.add_row(str(i), p)
156
+ console.print(table)
157
+
158
+ @api_app.command()
159
+ @beartype
160
+ def generate(
161
+ count: int = typer.Argument(1, help="Number of ports"),
162
+ country: str = typer.Option("US", "--country", "-c", help="ISO country code"),
163
+ city: str = typer.Option("", "--city", help="City name"),
164
+ state: str = typer.Option("", "--state", "-s", help="State name"),
165
+ name: str = typer.Option("", "--name", "-n", help="Port name"),
166
+ ttl: int = typer.Option(1, "--ttl", help="TTL in days"),
167
+ traffic_limit: int = typer.Option(10, "--traffic-limit", help="Traffic limit (GB)"),
168
+ type_id: int = typer.Option(
169
+ 1, "--type-id", help="Connection type: 1=keep-proxy, 2=keep-conn, 3=rotate"
170
+ ),
171
+ proxy_type_id: int = typer.Option(
172
+ 1, "--proxy-type-id", help="Proxy type: 1=residential, 3=mobile, 4=corporate"
173
+ ),
174
+ server_port_type_id: int = typer.Option(
175
+ 0, "--server-port-type-id", help="Port type: 0=shared, 1=dedicated"
176
+ ),
177
+ fmt: ExportFormat = typer.Option( # noqa: B008 — required Typer CLI pattern
178
+ ExportFormat.txt, "--format", "-f", help="Export format"
179
+ ),
180
+ output: str | None = typer.Option(None, "--output", "-o", help="Output file"),
181
+ ) -> None:
182
+ """Create ports directly (POST /v2/proxy/generate)."""
183
+
184
+ async def _create() -> list[str]:
185
+ req = CreatePortRequest(
186
+ country_code=country,
187
+ city=city,
188
+ state=state,
189
+ name=name,
190
+ count=count,
191
+ ttl=ttl,
192
+ type_id=type_id,
193
+ proxy_type_id=proxy_type_id,
194
+ server_port_type_id=server_port_type_id,
195
+ traffic_limit=traffic_limit,
196
+ )
197
+ async with ASocksClient(api_key=get_api_key()) as client:
198
+ ports = await client.create_ports(req)
199
+ return [p.proxy_url for p in ports]
200
+
201
+ urls = run_async(_create())
202
+ if not urls:
203
+ console.print("[yellow]No ports created.[/yellow]")
204
+ raise typer.Exit()
205
+
206
+ content = format_output(urls, fmt)
207
+ if output:
208
+ with open(output, "w") as f:
209
+ f.write(content)
210
+ console.print(f"[green]Saved {len(urls)} proxies to {output}[/green]")
211
+ else:
212
+ console.print(content)
213
+
214
+ @api_app.command()
215
+ @beartype
216
+ def archive(
217
+ port_id: int = typer.Argument(..., help="Port ID"),
218
+ ) -> None:
219
+ """Archive port (POST /v2/proxy/archive)."""
220
+
221
+ async def _archive() -> bool:
222
+ async with ASocksClient(api_key=get_api_key()) as client:
223
+ return await client.archive_port(port_id)
224
+
225
+ ok = run_async(_archive())
226
+ console.print(f"[green]Port {port_id} archived.[/green]" if ok else "[red]Failed.[/red]")
227
+
228
+ @api_app.command()
229
+ @beartype
230
+ def unarchive(
231
+ port_id: int = typer.Argument(..., help="Port ID"),
232
+ ) -> None:
233
+ """Unarchive port (POST /v2/proxy/unarchive)."""
234
+
235
+ async def _unarchive() -> bool:
236
+ async with ASocksClient(api_key=get_api_key()) as client:
237
+ return await client.unarchive_port(port_id)
238
+
239
+ ok = run_async(_unarchive())
240
+ console.print(f"[green]Port {port_id} unarchived.[/green]" if ok else "[red]Failed.[/red]")
241
+
242
+ @api_app.command(name="refresh-ip")
243
+ @beartype
244
+ def refresh_ip(
245
+ port_id: int = typer.Argument(..., help="Port ID"),
246
+ ) -> None:
247
+ """Refresh port IP (GET /v2/proxy/refresh-ip)."""
248
+
249
+ async def _refresh() -> bool:
250
+ async with ASocksClient(api_key=get_api_key()) as client:
251
+ return await client.refresh_ip(port_id)
252
+
253
+ ok = run_async(_refresh())
254
+ console.print(
255
+ f"[green]IP refreshed for port {port_id}.[/green]" if ok else "[red]Failed.[/red]"
256
+ )
257
+
258
+ @api_app.command()
259
+ @beartype
260
+ def rename(
261
+ port_id: int = typer.Argument(..., help="Port ID"),
262
+ name: str = typer.Argument(..., help="New name"),
263
+ ) -> None:
264
+ """Rename port (POST /v2/proxy/change-name)."""
265
+
266
+ async def _rename() -> bool:
267
+ async with ASocksClient(api_key=get_api_key()) as client:
268
+ return await client.change_port_name(port_id, name)
269
+
270
+ ok = run_async(_rename())
271
+ console.print(f"[green]Renamed to '{name}'.[/green]" if ok else "[red]Failed.[/red]")
272
+
273
+ @api_app.command()
274
+ @beartype
275
+ def traffic() -> None:
276
+ """Total spent traffic (GET /v2/proxy/total-spent-traffic)."""
277
+
278
+ async def _traffic() -> dict[str, Any]:
279
+ async with ASocksClient(api_key=get_api_key()) as client:
280
+ return await client.get_total_spent_traffic()
281
+
282
+ data = run_async(_traffic())
283
+ total = data.get("total_spent_traffic", "N/A")
284
+ console.print(f"Total spent traffic: [cyan]{total}[/cyan]")
285
+
286
+ @api_app.command(name="change-credentials")
287
+ @beartype
288
+ def change_credentials() -> None:
289
+ """Change credentials for all proxies (GET /v2/proxy/change-credentials)."""
290
+
291
+ async def _change() -> bool:
292
+ async with ASocksClient(api_key=get_api_key()) as client:
293
+ return await client.change_credentials()
294
+
295
+ ok = run_async(_change())
296
+ console.print("[green]Credentials changed.[/green]" if ok else "[red]Failed.[/red]")
297
+
298
+ @api_app.command()
299
+ @beartype
300
+ def templates(
301
+ page: int = typer.Option(1, "--page", "-p", help="Page number"),
302
+ ) -> None:
303
+ """List templates (GET /v2/proxy-template)."""
304
+
305
+ async def _templates() -> dict[str, Any]:
306
+ async with ASocksClient(api_key=get_api_key()) as client:
307
+ return await client.list_templates(page=page)
308
+
309
+ data = run_async(_templates())
310
+ console.print_json(json.dumps(data, indent=2, default=str))
311
+
312
+ @api_app.command(name="template-create")
313
+ @beartype
314
+ def template_create(
315
+ label: str = typer.Option(..., "--label", "-l", help="Template name"),
316
+ template: str = typer.Option(..., "--template", "-t", help="Template string"),
317
+ ) -> None:
318
+ """Create template (POST /v2/proxy-template/create-template)."""
319
+
320
+ async def _create() -> dict[str, Any]:
321
+ async with ASocksClient(api_key=get_api_key()) as client:
322
+ req = CreateTemplateRequest(label=label, template=template)
323
+ return await client.create_template(req)
324
+
325
+ data = run_async(_create())
326
+ if data.get("success"):
327
+ console.print("[green]Template created.[/green]")
328
+ console.print_json(json.dumps(data, indent=2, default=str))
329
+
330
+ @api_app.command(name="template-delete")
331
+ @beartype
332
+ def template_delete(
333
+ template_id: int = typer.Argument(..., help="Template ID"),
334
+ ) -> None:
335
+ """Delete template (DELETE /v2/proxy-template/delete-template)."""
336
+
337
+ async def _delete() -> bool:
338
+ async with ASocksClient(api_key=get_api_key()) as client:
339
+ return await client.delete_template(template_id)
340
+
341
+ ok = run_async(_delete())
342
+ console.print(
343
+ f"[green]Template {template_id} deleted.[/green]" if ok else "[red]Failed.[/red]"
344
+ )
345
+
346
+ @api_app.command(name="whitelist-add")
347
+ @beartype
348
+ def whitelist_add(
349
+ ip: str = typer.Argument(..., help="IP address"),
350
+ description: str = typer.Option("", "--desc", "-d", help="Description"),
351
+ ) -> None:
352
+ """Add IP to whitelist (POST /v2/whitelist/add)."""
353
+
354
+ async def _add() -> dict[str, Any]:
355
+ async with ASocksClient(api_key=get_api_key()) as client:
356
+ req = WhitelistAddRequest(ip=ip, description=description)
357
+ return await client.add_whitelist_ip(req)
358
+
359
+ data = run_async(_add())
360
+ if data.get("success"):
361
+ console.print(f"[green]IP {ip} added to whitelist.[/green]")
362
+ else:
363
+ console.print(f"[red]Failed to add {ip}.[/red]")
364
+
365
+ @api_app.command(name="whitelist-remove")
366
+ @beartype
367
+ def whitelist_remove(
368
+ ip: str = typer.Argument(..., help="IP address"),
369
+ ) -> None:
370
+ """Remove IP from whitelist (DELETE /v2/whitelist/delete)."""
371
+
372
+ async def _remove() -> bool:
373
+ async with ASocksClient(api_key=get_api_key()) as client:
374
+ return await client.delete_whitelist_ip(ip)
375
+
376
+ ok = run_async(_remove())
377
+ console.print(f"[green]IP {ip} removed.[/green]" if ok else "[red]Failed.[/red]")