plexo-cli 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.
@@ -0,0 +1,9 @@
1
+ node_modules
2
+ dist
3
+ out
4
+ .DS_Store
5
+ .eslintcache
6
+ *.log*
7
+ test-results
8
+ playwright-report
9
+ *.tsbuildinfo
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.5
2
+ Name: plexo-cli
3
+ Version: 0.1.0
4
+ Summary: Multi-network bandwidth bonding download engine & CLI
5
+ Author-email: Jagjot Singh <gotogether@gmail.com>
6
+ License-Expression: MIT
7
+ Keywords: bandwidth-aggregation,download-accelerator,multi-interface,network-bonding,proxy,torrent
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Internet
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: click>=8.1.0
24
+ Requires-Dist: httpx>=0.27.0
25
+ Requires-Dist: psutil>=5.9.0
26
+ Requires-Dist: rich>=13.7.0
27
+ Requires-Dist: urllib3>=2.0.0
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
30
+ Requires-Dist: pytest>=8.0.0; extra == 'test'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # Plexo CLI (`plexo-cli`)
34
+
35
+ > **Multi-network bandwidth bonding download accelerator in your terminal.**
36
+
37
+ Combines multiple active internet connections (e.g. **Hostel Wi-Fi + Mobile 5G via USB Tethering + Ethernet**) into a single parallel stream to boost download speeds for direct files and developer tools.
38
+
39
+ ---
40
+
41
+ ## Features
42
+ - **Parallel HTTP Range Slicing**: Binds concurrent download workers to distinct physical network interfaces (source IP socket binding).
43
+ - **Zero-Touch Local Proxy (`plexo proxy`)**: Run a local forward proxy (`127.0.0.1:8899`) to automatically accelerate `pip install`, `git clone`, `curl`, and browser downloads.
44
+ - **Rich Terminal UI**: Live dashboard showing real-time throughput per individual network adapter.
45
+ - **Dynamic Failover**: Auto-detects lagging or disconnected hotspots and re-routes pending chunks to healthy connections without corrupting files.
46
+ - **Sparse File Allocation**: Instant zero-overhead pre-allocation on disk with non-blocking multi-threaded byte writes.
47
+
48
+ ---
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install plexo-cli
54
+ ```
55
+
56
+ Or install in editable mode from source:
57
+ ```bash
58
+ git clone https://github.com/Jagjotkarhu/plexo.git
59
+ cd plexo/packages/plexo-cli
60
+ pip install -e .
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Quickstart
66
+
67
+ ### 1. Scan Network Adapters
68
+ ```bash
69
+ plexo adapters
70
+ ```
71
+ Lists all discovered network interfaces (Wi-Fi, USB cellular tether, Ethernet), their local IPs, and connectivity status.
72
+
73
+ ### 2. Download a File with Aggregated Bandwidth
74
+ ```bash
75
+ plexo download https://example.com/largefile.zip -o largefile.zip
76
+ ```
77
+
78
+ ### 3. Accelerate `pip` or Terminal Tools
79
+ Start the local proxy:
80
+ ```bash
81
+ plexo proxy --port 8899
82
+ ```
83
+ Then run any tool using the proxy:
84
+ ```bash
85
+ pip install torch --proxy http://127.0.0.1:8899
86
+ ```
87
+
88
+ ---
89
+
90
+ ## License
91
+ MIT © Anmol Kapil & Jagjot Singh
@@ -0,0 +1,59 @@
1
+ # Plexo CLI (`plexo-cli`)
2
+
3
+ > **Multi-network bandwidth bonding download accelerator in your terminal.**
4
+
5
+ Combines multiple active internet connections (e.g. **Hostel Wi-Fi + Mobile 5G via USB Tethering + Ethernet**) into a single parallel stream to boost download speeds for direct files and developer tools.
6
+
7
+ ---
8
+
9
+ ## Features
10
+ - **Parallel HTTP Range Slicing**: Binds concurrent download workers to distinct physical network interfaces (source IP socket binding).
11
+ - **Zero-Touch Local Proxy (`plexo proxy`)**: Run a local forward proxy (`127.0.0.1:8899`) to automatically accelerate `pip install`, `git clone`, `curl`, and browser downloads.
12
+ - **Rich Terminal UI**: Live dashboard showing real-time throughput per individual network adapter.
13
+ - **Dynamic Failover**: Auto-detects lagging or disconnected hotspots and re-routes pending chunks to healthy connections without corrupting files.
14
+ - **Sparse File Allocation**: Instant zero-overhead pre-allocation on disk with non-blocking multi-threaded byte writes.
15
+
16
+ ---
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install plexo-cli
22
+ ```
23
+
24
+ Or install in editable mode from source:
25
+ ```bash
26
+ git clone https://github.com/Jagjotkarhu/plexo.git
27
+ cd plexo/packages/plexo-cli
28
+ pip install -e .
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Quickstart
34
+
35
+ ### 1. Scan Network Adapters
36
+ ```bash
37
+ plexo adapters
38
+ ```
39
+ Lists all discovered network interfaces (Wi-Fi, USB cellular tether, Ethernet), their local IPs, and connectivity status.
40
+
41
+ ### 2. Download a File with Aggregated Bandwidth
42
+ ```bash
43
+ plexo download https://example.com/largefile.zip -o largefile.zip
44
+ ```
45
+
46
+ ### 3. Accelerate `pip` or Terminal Tools
47
+ Start the local proxy:
48
+ ```bash
49
+ plexo proxy --port 8899
50
+ ```
51
+ Then run any tool using the proxy:
52
+ ```bash
53
+ pip install torch --proxy http://127.0.0.1:8899
54
+ ```
55
+
56
+ ---
57
+
58
+ ## License
59
+ MIT © Anmol Kapil & Jagjot Singh
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "plexo-cli"
7
+ version = "0.1.0"
8
+ description = "Multi-network bandwidth bonding download engine & CLI"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Jagjot Singh", email = "gotogether@gmail.com" },
14
+ ]
15
+ keywords = [
16
+ "download-accelerator",
17
+ "network-bonding",
18
+ "bandwidth-aggregation",
19
+ "multi-interface",
20
+ "torrent",
21
+ "proxy",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Environment :: Console",
26
+ "Intended Audience :: Developers",
27
+ "License :: OSI Approved :: MIT License",
28
+ "Operating System :: OS Independent",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.9",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Programming Language :: Python :: 3.14",
36
+ "Topic :: Internet",
37
+ "Topic :: Utilities",
38
+ ]
39
+ dependencies = [
40
+ "httpx>=0.27.0",
41
+ "urllib3>=2.0.0",
42
+ "rich>=13.7.0",
43
+ "click>=8.1.0",
44
+ "psutil>=5.9.0",
45
+ ]
46
+
47
+ [project.optional-dependencies]
48
+ test = [
49
+ "pytest>=8.0.0",
50
+ "pytest-asyncio>=0.23.0",
51
+ ]
52
+
53
+ [project.scripts]
54
+ plexo = "plexo.cli:main"
55
+
56
+ [tool.hatch.build.targets.wheel]
57
+ packages = ["src/plexo"]
58
+
59
+ [tool.pytest.ini_options]
60
+ pythonpath = ["src"]
61
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ """Plexo CLI: Multi-network bandwidth bonding download accelerator."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "Jagjot Singh"
@@ -0,0 +1,190 @@
1
+ """Command Line Interface for Plexo.
2
+
3
+ Exposes subcommands: download, adapters, proxy, info.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ # Ensure UTF-8 output on Windows consoles
14
+ if sys.platform == "win32":
15
+ try:
16
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
17
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
18
+ except Exception:
19
+ pass
20
+
21
+ import click
22
+ from rich.console import Console
23
+ from rich.panel import Panel
24
+ from rich.table import Table
25
+ from . import __version__
26
+ from .core.adapter import discover_adapters, get_bonded_pool
27
+ from .core.engine import BondingDownloadEngine, ProgressUpdate
28
+ from .proxy.server import run_proxy
29
+ from .ui.dashboard import TerminalDashboard
30
+
31
+ console = Console()
32
+
33
+
34
+ @click.group()
35
+ @click.version_option(version=__version__, prog_name="plexo")
36
+ def main():
37
+ """Plexo: Multi-Network Bandwidth Bonding Download Engine."""
38
+ pass
39
+
40
+
41
+ @main.command(name="adapters")
42
+ @click.option("--all", "show_all", is_flag=True, help="Show all adapters including offline/internal.")
43
+ def list_adapters(show_all: bool):
44
+ """Scan and benchmark active network adapters."""
45
+ console.print("\n[bold cyan]Scanning Network Interfaces...[/bold cyan]")
46
+ adapters = discover_adapters(verify_online=True)
47
+
48
+ table = Table(title="[bold green]Network Interfaces[/bold green]")
49
+ table.add_column("Interface Name", style="bold white")
50
+ table.add_column("Type", style="magenta")
51
+ table.add_column("IPv4 Address", style="cyan")
52
+ table.add_column("Link Speed", style="yellow")
53
+ table.add_column("Internet Route", style="bold")
54
+
55
+ display_list = adapters if show_all else [a for a in adapters if a.is_online]
56
+
57
+ for a in display_list:
58
+ status_text = "[bold green]ONLINE[/bold green]" if a.is_online else "[dim red]OFFLINE[/dim red]"
59
+ speed_str = f"{a.speed_mbps} Mbps" if a.speed_mbps > 0 else "Unknown"
60
+ table.add_row(a.name, a.kind.upper(), a.ip_address, speed_str, status_text)
61
+
62
+ console.print(table)
63
+ online_count = sum(1 for a in adapters if a.is_online)
64
+
65
+ if online_count >= 2:
66
+ console.print(
67
+ f"\n[bold green][OK] Ready for bonding:[/bold green] [yellow]{online_count}[/yellow] independent connections detected.\n"
68
+ )
69
+ elif online_count == 1:
70
+ console.print(
71
+ "\n[yellow][INFO] 1 connection active.[/yellow] Plug in Phone 5G USB tethering or Ethernet to enable multi-network bonding.\n"
72
+ )
73
+ else:
74
+ console.print("\n[bold red][X] No online connections found.[/bold red]\n")
75
+
76
+
77
+ @main.command(name="download")
78
+ @click.argument("url")
79
+ @click.option("-o", "--output", "output_path", type=click.Path(), default=None, help="Output destination path.")
80
+ @click.option("--chunk-size", default=8, type=int, help="Chunk size in MB (default: 8 MB).")
81
+ @click.option("--workers", default=2, type=int, help="Workers per network adapter (default: 2).")
82
+ def download_file(url: str, output_path: Optional[str], chunk_size: int, workers: int):
83
+ """Download a file using bonded multi-network connections."""
84
+ adapters = get_bonded_pool()
85
+ if not adapters:
86
+ console.print("[bold red]Error: No active online network adapters detected.[/bold red]")
87
+ raise click.Abort()
88
+
89
+ dashboard = TerminalDashboard(console=console)
90
+ chunk_bytes = chunk_size * 1024 * 1024
91
+
92
+ engine = BondingDownloadEngine(
93
+ url=url,
94
+ output_path=output_path,
95
+ adapters=adapters,
96
+ chunk_size=chunk_bytes,
97
+ workers_per_adapter=workers,
98
+ )
99
+
100
+ probe = engine._prepare()
101
+ dashboard.setup(probe.filename, probe.total_bytes, adapters)
102
+
103
+ start_time = time.time()
104
+ with dashboard.progress:
105
+ final_path = engine.execute(progress_callback=dashboard.update)
106
+
107
+ elapsed = time.time() - start_time
108
+ total_bytes = final_path.stat().st_size
109
+
110
+ # Calculate checksum
111
+ from .core.sparse import SparseFileWriter
112
+ with SparseFileWriter(final_path, total_bytes) as writer:
113
+ checksum = writer.calculate_checksum()
114
+
115
+ dashboard.finish(final_path.name, total_bytes, elapsed, checksum)
116
+
117
+
118
+ @main.command(name="proxy")
119
+ @click.option("--host", default="127.0.0.1", help="Proxy listening host (default: 127.0.0.1).")
120
+ @click.option("--port", default=8899, type=int, help="Proxy listening port (default: 8899).")
121
+ def start_proxy(host: str, port: int):
122
+ """Launch local acceleration proxy for pip, git, and curl."""
123
+ run_proxy(host=host, port=port)
124
+
125
+
126
+ @main.command(name="torrent")
127
+ @click.argument("source")
128
+ @click.option("-o", "--output-dir", default=".", help="Directory to save downloaded files.")
129
+ def download_torrent(source: str, output_dir: str):
130
+ """Download a torrent or magnet link across bonded network adapters."""
131
+ from .torrent.metadata import parse_magnet, parse_torrent_file
132
+ from .torrent.client import TorrentBondingEngine
133
+
134
+ adapters = get_bonded_pool()
135
+ if not adapters:
136
+ console.print("[bold red]Error: No active online network adapters detected.[/bold red]")
137
+ raise click.Abort()
138
+
139
+ if source.startswith("magnet:?"):
140
+ meta = parse_magnet(source)
141
+ else:
142
+ meta = parse_torrent_file(source)
143
+
144
+ console.print(
145
+ Panel.fit(
146
+ f"[bold cyan]Plexo Torrent Swarm Engine[/bold cyan]\n"
147
+ f"Name: [bold yellow]{meta.name}[/bold yellow]\n"
148
+ f"Info Hash: [dim]{meta.info_hash_hex}[/dim]\n"
149
+ f"Active Network Links: [bold green]{len(adapters)}[/bold green]",
150
+ border_style="cyan",
151
+ )
152
+ )
153
+
154
+ dashboard = TerminalDashboard(console=console)
155
+ dashboard.setup(meta.name, meta.total_length, adapters)
156
+
157
+ engine = TorrentBondingEngine(metadata=meta, output_dir=output_dir, adapters=adapters)
158
+
159
+ start_time = time.time()
160
+ with dashboard.progress:
161
+ final_path = engine.download(progress_callback=dashboard.update)
162
+
163
+ elapsed = time.time() - start_time
164
+ total_bytes = final_path.stat().st_size
165
+
166
+ from .core.sparse import SparseFileWriter
167
+ with SparseFileWriter(final_path, total_bytes) as writer:
168
+ checksum = writer.calculate_checksum()
169
+
170
+ dashboard.finish(final_path.name, total_bytes, elapsed, checksum)
171
+
172
+
173
+ @main.command(name="info")
174
+ def show_info():
175
+ """Display Plexo environment and bonding capabilities."""
176
+ adapters = get_bonded_pool()
177
+ console.print(
178
+ Panel.fit(
179
+ f"[bold cyan]Plexo CLI v{__version__}[/bold cyan]\n"
180
+ f"Bonding Engine: [bold green]Active[/bold green]\n"
181
+ f"Verified Online Adapters: [bold yellow]{len(adapters)}[/bold yellow]\n"
182
+ f"Default Proxy Port: [bold magenta]8899[/bold magenta]",
183
+ border_style="cyan",
184
+ title="System Capabilities",
185
+ )
186
+ )
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
@@ -0,0 +1 @@
1
+ """Core multi-adapter bonding and download engine."""
@@ -0,0 +1,139 @@
1
+ """Network interface discovery and binding module.
2
+
3
+ Detects active network adapters (Wi-Fi, USB cellular tether, Ethernet),
4
+ filters internal/virtual networks, and verifies outward internet reachability.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ipaddress
10
+ import platform
11
+ import socket
12
+ import subprocess
13
+ from dataclasses import dataclass
14
+ from typing import List, Optional
15
+ import psutil
16
+ import urllib3
17
+
18
+
19
+ @dataclass
20
+ class NetworkAdapter:
21
+ """Represents a discovered physical or virtual network interface."""
22
+ name: str
23
+ ip_address: str
24
+ netmask: str
25
+ kind: str # "wifi", "usb", "ethernet", "other"
26
+ is_up: bool
27
+ speed_mbps: int = 0
28
+ gateway: Optional[str] = None
29
+ is_online: bool = False
30
+
31
+ @property
32
+ def display_name(self) -> str:
33
+ kind_label = self.kind.upper()
34
+ return f"{self.name} [{kind_label}] ({self.ip_address})"
35
+
36
+
37
+ def classify_interface(name: str) -> str:
38
+ """Classifies interface type based on OS naming conventions."""
39
+ name_lower = name.lower()
40
+ if any(k in name_lower for k in ("wi-fi", "wifi", "wireless", "wlan", "802.11")):
41
+ return "wifi"
42
+ if any(k in name_lower for k in ("rndis", "tether", "remote ndis", "apple mobile", "usb", "enx")):
43
+ return "usb"
44
+ if any(k in name_lower for k in ("ethernet", "lan", "eth", "en0", "en1")):
45
+ return "ethernet"
46
+ return "other"
47
+
48
+
49
+ def is_usable_ip(ip_str: str) -> bool:
50
+ """Checks if an IPv4 address is routable and not loopback/link-local."""
51
+ try:
52
+ ip = ipaddress.IPv4Address(ip_str)
53
+ if ip.is_loopback:
54
+ return False
55
+ if ip.is_link_local: # 169.254.x.x
56
+ return False
57
+ if ip.is_unspecified: # 0.0.0.0
58
+ return False
59
+ return True
60
+ except (ValueError, ipaddress.AddressValueError):
61
+ return False
62
+
63
+
64
+ def test_adapter_connectivity(ip_address: str, timeout: float = 2.0) -> bool:
65
+ """Verifies that an outgoing connection bound to this source IP can reach the internet."""
66
+ test_hosts = [("1.1.1.1", 53), ("8.8.8.8", 53)]
67
+ for host, port in test_hosts:
68
+ sock = None
69
+ try:
70
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
71
+ sock.settimeout(timeout)
72
+ # Bind specifically to this adapter's source IP
73
+ sock.bind((ip_address, 0))
74
+ sock.connect((host, port))
75
+ return True
76
+ except (socket.error, OSError):
77
+ continue
78
+ finally:
79
+ if sock:
80
+ sock.close()
81
+ return False
82
+
83
+
84
+ def discover_adapters(verify_online: bool = True) -> List[NetworkAdapter]:
85
+ """Discovers all active, routable network adapters."""
86
+ adapters: List[NetworkAdapter] = []
87
+ seen_ips = set()
88
+
89
+ if_addrs = psutil.net_if_addrs()
90
+ if_stats = psutil.net_if_stats()
91
+
92
+ # Exclude common internal virtual switches unless they provide genuine routes
93
+ virtual_keywords = ("vEthernet", "Hyper-V", "VirtualBox", "VMware", "Loopback", "WSL")
94
+
95
+ for iface_name, addrs in if_addrs.items():
96
+ if any(v.lower() in iface_name.lower() for v in virtual_keywords):
97
+ continue
98
+
99
+ stats = if_stats.get(iface_name)
100
+ is_up = stats.isup if stats else True
101
+ if not is_up:
102
+ continue
103
+
104
+ for addr in addrs:
105
+ # We focus on IPv4 for multi-adapter bonding
106
+ if addr.family == socket.AF_INET:
107
+ ip = addr.address
108
+ if ip in seen_ips or not is_usable_ip(ip):
109
+ continue
110
+
111
+ seen_ips.add(ip)
112
+ kind = classify_interface(iface_name)
113
+ speed = stats.speed if stats else 0
114
+
115
+ online = False
116
+ if verify_online:
117
+ online = test_adapter_connectivity(ip)
118
+
119
+ adapter = NetworkAdapter(
120
+ name=iface_name,
121
+ ip_address=ip,
122
+ netmask=addr.netmask or "255.255.255.0",
123
+ kind=kind,
124
+ is_up=is_up,
125
+ speed_mbps=speed,
126
+ is_online=online if verify_online else is_up,
127
+ )
128
+ adapters.append(adapter)
129
+
130
+ # Sort so online adapters appear first, prioritized by speed/type
131
+ adapters.sort(key=lambda a: (not a.is_online, a.kind == "other", a.name))
132
+ return adapters
133
+
134
+
135
+ def get_bonded_pool(min_adapters: int = 1) -> List[NetworkAdapter]:
136
+ """Returns all verified online adapters ready for bonding."""
137
+ adapters = discover_adapters(verify_online=True)
138
+ online = [a for a in adapters if a.is_online]
139
+ return online if online else adapters