proxy2vpn 0.1.3__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.
- proxy2vpn/__init__.py +20 -0
- proxy2vpn/cli.py +416 -0
- proxy2vpn/compose_manager.py +115 -0
- proxy2vpn/compose_utils.py +38 -0
- proxy2vpn/config.py +34 -0
- proxy2vpn/docker_ops.py +170 -0
- proxy2vpn/models.py +98 -0
- proxy2vpn/preset_manager.py +48 -0
- proxy2vpn/server_manager.py +111 -0
- proxy2vpn/typer_ext.py +379 -0
- proxy2vpn-0.1.3.dist-info/METADATA +143 -0
- proxy2vpn-0.1.3.dist-info/RECORD +15 -0
- proxy2vpn-0.1.3.dist-info/WHEEL +5 -0
- proxy2vpn-0.1.3.dist-info/entry_points.txt +2 -0
- proxy2vpn-0.1.3.dist-info/top_level.txt +1 -0
proxy2vpn/typer_ext.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from click.exceptions import UsageError
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HelpfulTyper(typer.Typer):
|
|
15
|
+
"""Typer app with smart error messages and suggestions."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
18
|
+
ctx_settings = kwargs.setdefault("context_settings", {})
|
|
19
|
+
ctx_settings.setdefault("help_option_names", ["-h", "--help"])
|
|
20
|
+
super().__init__(*args, **kwargs)
|
|
21
|
+
self.console = Console(stderr=True)
|
|
22
|
+
|
|
23
|
+
def __call__(self, *args, **kwargs): # type: ignore[override]
|
|
24
|
+
kwargs.setdefault("standalone_mode", False)
|
|
25
|
+
try:
|
|
26
|
+
return super().__call__(*args, **kwargs)
|
|
27
|
+
except UsageError as exc:
|
|
28
|
+
self._handle_usage_error(exc)
|
|
29
|
+
raise SystemExit(2)
|
|
30
|
+
except Exception as exc:
|
|
31
|
+
# Handle other exceptions more gracefully
|
|
32
|
+
if "Missing argument" in str(exc) or "required" in str(exc).lower():
|
|
33
|
+
self.console.print("\n[red]Error:[/red] Missing required arguments")
|
|
34
|
+
self.console.print(
|
|
35
|
+
"[dim]Run the command with '--help' to see required arguments.[/dim]"
|
|
36
|
+
)
|
|
37
|
+
raise SystemExit(2)
|
|
38
|
+
raise
|
|
39
|
+
|
|
40
|
+
def _handle_usage_error(self, exc: UsageError) -> None:
|
|
41
|
+
"""Handle usage errors with helpful messages."""
|
|
42
|
+
# Handle "Missing command" error for subcommand groups
|
|
43
|
+
if "Missing command" in exc.message:
|
|
44
|
+
self._show_missing_command_help(exc)
|
|
45
|
+
# Handle "No such command" error
|
|
46
|
+
elif "No such command" in exc.message:
|
|
47
|
+
self._show_no_such_command_help(exc)
|
|
48
|
+
else:
|
|
49
|
+
# For other errors, format them nicely
|
|
50
|
+
error_msg = exc.message.strip()
|
|
51
|
+
|
|
52
|
+
# Handle missing argument errors
|
|
53
|
+
if "Missing argument" in error_msg:
|
|
54
|
+
self._show_missing_argument_help(exc, error_msg)
|
|
55
|
+
# Handle missing option errors
|
|
56
|
+
elif "Missing option" in error_msg:
|
|
57
|
+
self._show_missing_option_help(exc, error_msg)
|
|
58
|
+
else:
|
|
59
|
+
# Generic error display
|
|
60
|
+
self.console.print(f"[red]Error:[/red] {error_msg}")
|
|
61
|
+
if exc.ctx:
|
|
62
|
+
self.console.print(
|
|
63
|
+
f"\nUse '[cyan]{exc.ctx.command_path} --help[/cyan]' for more information."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _show_missing_argument_help(self, exc: UsageError, error_msg: str) -> None:
|
|
67
|
+
"""Show help when required arguments are missing."""
|
|
68
|
+
import re
|
|
69
|
+
|
|
70
|
+
# Extract the argument name from the error message
|
|
71
|
+
match = re.search(r"'([^']+)'", error_msg)
|
|
72
|
+
arg_name = match.group(1) if match else "argument"
|
|
73
|
+
|
|
74
|
+
error_text = Text()
|
|
75
|
+
error_text.append("Missing required argument: ", style="red")
|
|
76
|
+
error_text.append(f"'{arg_name}'", style="yellow")
|
|
77
|
+
|
|
78
|
+
self.console.print()
|
|
79
|
+
self.console.print(error_text)
|
|
80
|
+
|
|
81
|
+
# Show command usage
|
|
82
|
+
if exc.ctx:
|
|
83
|
+
self.console.print(
|
|
84
|
+
f"\n[yellow]Usage:[/yellow] [cyan]{exc.ctx.command_path} {arg_name.upper()}[/cyan]"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Show examples
|
|
88
|
+
examples = self._get_example_commands(exc.ctx.command_path, "")
|
|
89
|
+
if examples and len(examples) > 0:
|
|
90
|
+
self.console.print("\n[blue]Example:[/blue]")
|
|
91
|
+
self.console.print(f" [dim]$[/dim] [cyan]{examples[0]}[/cyan]")
|
|
92
|
+
|
|
93
|
+
self.console.print(
|
|
94
|
+
f"\n[dim]Use '[cyan]{exc.ctx.command_path} --help[/cyan]' for more information.[/dim]"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def _show_missing_option_help(self, exc: UsageError, error_msg: str) -> None:
|
|
98
|
+
"""Show help when required options are missing."""
|
|
99
|
+
import re
|
|
100
|
+
|
|
101
|
+
# Extract the option name from the error message
|
|
102
|
+
match = re.search(r"'([^']+)'", error_msg)
|
|
103
|
+
option_name = match.group(1) if match else "option"
|
|
104
|
+
|
|
105
|
+
error_text = Text()
|
|
106
|
+
error_text.append("Missing required option: ", style="red")
|
|
107
|
+
error_text.append(f"'{option_name}'", style="yellow")
|
|
108
|
+
|
|
109
|
+
self.console.print()
|
|
110
|
+
self.console.print(error_text)
|
|
111
|
+
|
|
112
|
+
if exc.ctx:
|
|
113
|
+
self.console.print(
|
|
114
|
+
"\n[yellow]This option is required for this command.[/yellow]"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Show example with the option
|
|
118
|
+
self.console.print("\n[blue]Example:[/blue]")
|
|
119
|
+
self.console.print(
|
|
120
|
+
f" [dim]$[/dim] [cyan]{exc.ctx.command_path} {option_name} <value>[/cyan]"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
self.console.print(
|
|
124
|
+
f"\n[dim]Use '[cyan]{exc.ctx.command_path} --help[/cyan]' for more information.[/dim]"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def _show_missing_command_help(self, exc: UsageError) -> None:
|
|
128
|
+
"""Show help when a subcommand is missing."""
|
|
129
|
+
if not exc.ctx:
|
|
130
|
+
self.console.print("[red]Error:[/red] Missing command.")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Create a nice panel with available commands
|
|
134
|
+
command_path = exc.ctx.command_path
|
|
135
|
+
available_commands = exc.ctx.command.list_commands(exc.ctx) if exc.ctx else []
|
|
136
|
+
|
|
137
|
+
# Create a table of commands with their help text
|
|
138
|
+
table = Table(
|
|
139
|
+
show_header=True, header_style="bold cyan", box=None, padding=(0, 2)
|
|
140
|
+
)
|
|
141
|
+
table.add_column("Command", style="green")
|
|
142
|
+
table.add_column("Description")
|
|
143
|
+
|
|
144
|
+
# Get command descriptions
|
|
145
|
+
for cmd_name in available_commands:
|
|
146
|
+
cmd_obj = self._get_command_object(exc.ctx.command, cmd_name)
|
|
147
|
+
description = self._get_command_help(cmd_obj) if cmd_obj else ""
|
|
148
|
+
table.add_row(cmd_name, description)
|
|
149
|
+
|
|
150
|
+
# Create the error panel
|
|
151
|
+
error_text = Text()
|
|
152
|
+
error_text.append("Missing command for ", style="red")
|
|
153
|
+
error_text.append(f"'{command_path}'", style="yellow")
|
|
154
|
+
|
|
155
|
+
panel = Panel(
|
|
156
|
+
table,
|
|
157
|
+
title="[red]Error: Missing Command[/red]",
|
|
158
|
+
subtitle=f"[dim]Use '[cyan]{command_path} <command> --help[/cyan]' for details[/dim]",
|
|
159
|
+
border_style="red",
|
|
160
|
+
expand=False,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
self.console.print()
|
|
164
|
+
self.console.print(error_text)
|
|
165
|
+
self.console.print()
|
|
166
|
+
self.console.print(panel)
|
|
167
|
+
|
|
168
|
+
def _show_no_such_command_help(self, exc: UsageError) -> None:
|
|
169
|
+
"""Show help when a command doesn't exist."""
|
|
170
|
+
import re
|
|
171
|
+
|
|
172
|
+
match = re.search(r"'([^']+)'", exc.message)
|
|
173
|
+
bad_cmd = match.group(1) if match else exc.message
|
|
174
|
+
possibilities = exc.ctx.command.list_commands(exc.ctx) if exc.ctx else []
|
|
175
|
+
|
|
176
|
+
# Get close matches using different strategies
|
|
177
|
+
matches: List[str] = []
|
|
178
|
+
|
|
179
|
+
# 1. Standard fuzzy matching
|
|
180
|
+
fuzzy_matches = difflib.get_close_matches(bad_cmd, possibilities, cutoff=0.4)
|
|
181
|
+
matches.extend(fuzzy_matches)
|
|
182
|
+
|
|
183
|
+
# 2. Check for partial matches (prefix matching)
|
|
184
|
+
partial_matches = [
|
|
185
|
+
cmd for cmd in possibilities if cmd.startswith(bad_cmd.lower())
|
|
186
|
+
]
|
|
187
|
+
for pm in partial_matches:
|
|
188
|
+
if pm not in matches:
|
|
189
|
+
matches.append(pm)
|
|
190
|
+
|
|
191
|
+
# 3. Check all command paths (including subcommands)
|
|
192
|
+
all_paths = self._all_command_paths()
|
|
193
|
+
more = difflib.get_close_matches(bad_cmd, all_paths, cutoff=0.4)
|
|
194
|
+
for m in more:
|
|
195
|
+
if m not in matches:
|
|
196
|
+
matches.append(m)
|
|
197
|
+
|
|
198
|
+
# 4. Check for common typos (transposed characters, missing letters)
|
|
199
|
+
typo_matches = self._find_typo_matches(bad_cmd, possibilities)
|
|
200
|
+
for tm in typo_matches:
|
|
201
|
+
if tm not in matches:
|
|
202
|
+
matches.append(tm)
|
|
203
|
+
|
|
204
|
+
# Create error message
|
|
205
|
+
error_text = Text()
|
|
206
|
+
error_text.append("No such command: ", style="red")
|
|
207
|
+
error_text.append(f"'{bad_cmd}'", style="yellow")
|
|
208
|
+
|
|
209
|
+
self.console.print()
|
|
210
|
+
self.console.print(error_text)
|
|
211
|
+
|
|
212
|
+
# Show suggestions if available
|
|
213
|
+
if matches:
|
|
214
|
+
self.console.print("\n[green]Did you mean one of these?[/green]")
|
|
215
|
+
for match in matches[:5]: # Limit to top 5 suggestions
|
|
216
|
+
self.console.print(f" • [cyan]{match}[/cyan]")
|
|
217
|
+
|
|
218
|
+
# Show available commands
|
|
219
|
+
if possibilities:
|
|
220
|
+
self.console.print("\n[yellow]Available commands:[/yellow]")
|
|
221
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
222
|
+
table.add_column("Command", style="cyan")
|
|
223
|
+
table.add_column("Description")
|
|
224
|
+
|
|
225
|
+
for cmd_name in possibilities:
|
|
226
|
+
cmd_obj = self._get_command_object(exc.ctx.command, cmd_name)
|
|
227
|
+
description = self._get_command_help(cmd_obj) if cmd_obj else ""
|
|
228
|
+
table.add_row(cmd_name, description)
|
|
229
|
+
|
|
230
|
+
self.console.print(table)
|
|
231
|
+
|
|
232
|
+
# Show example commands if context-appropriate
|
|
233
|
+
if exc.ctx:
|
|
234
|
+
examples = self._get_example_commands(exc.ctx.command_path, bad_cmd)
|
|
235
|
+
if examples:
|
|
236
|
+
self.console.print("\n[blue]Example commands:[/blue]")
|
|
237
|
+
for example in examples:
|
|
238
|
+
self.console.print(f" [dim]$[/dim] [cyan]{example}[/cyan]")
|
|
239
|
+
|
|
240
|
+
# Show help command
|
|
241
|
+
if exc.ctx:
|
|
242
|
+
self.console.print(
|
|
243
|
+
f"\n[dim]Use '[cyan]{exc.ctx.command_path} --help[/cyan]' for more information.[/dim]"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Add documentation link if available
|
|
247
|
+
doc_link = self._get_documentation_link(exc.ctx.command_path)
|
|
248
|
+
if doc_link:
|
|
249
|
+
self.console.print(f"[dim]Documentation: [link]{doc_link}[/link][/dim]")
|
|
250
|
+
|
|
251
|
+
def _get_command_object(self, parent_command, cmd_name: str):
|
|
252
|
+
"""Get the command object for a given command name."""
|
|
253
|
+
if hasattr(parent_command, "commands"):
|
|
254
|
+
return parent_command.commands.get(cmd_name)
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
def _get_command_help(self, cmd_obj) -> str:
|
|
258
|
+
"""Extract help text from a command object."""
|
|
259
|
+
if not cmd_obj:
|
|
260
|
+
return ""
|
|
261
|
+
|
|
262
|
+
# Try to get help from various places
|
|
263
|
+
if hasattr(cmd_obj, "help"):
|
|
264
|
+
return cmd_obj.help or ""
|
|
265
|
+
elif hasattr(cmd_obj, "short_help"):
|
|
266
|
+
return cmd_obj.short_help or ""
|
|
267
|
+
elif hasattr(cmd_obj, "__doc__"):
|
|
268
|
+
doc = cmd_obj.__doc__
|
|
269
|
+
if doc:
|
|
270
|
+
# Take first line of docstring
|
|
271
|
+
return doc.strip().split("\n")[0]
|
|
272
|
+
return ""
|
|
273
|
+
|
|
274
|
+
def _find_typo_matches(self, bad_cmd: str, possibilities: List[str]) -> List[str]:
|
|
275
|
+
"""Find commands that might be typos of the bad command."""
|
|
276
|
+
typo_matches = []
|
|
277
|
+
bad_lower = bad_cmd.lower()
|
|
278
|
+
|
|
279
|
+
for cmd in possibilities:
|
|
280
|
+
cmd_lower = cmd.lower()
|
|
281
|
+
|
|
282
|
+
# Check for transposed adjacent characters
|
|
283
|
+
if len(bad_lower) == len(cmd_lower) and len(bad_lower) > 1:
|
|
284
|
+
diffs = sum(1 for a, b in zip(bad_lower, cmd_lower) if a != b)
|
|
285
|
+
if diffs <= 2: # Allow up to 2 character differences
|
|
286
|
+
typo_matches.append(cmd)
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
# Check for single missing or extra character
|
|
290
|
+
if abs(len(bad_lower) - len(cmd_lower)) == 1:
|
|
291
|
+
if len(bad_lower) < len(cmd_lower):
|
|
292
|
+
# Check if bad_cmd is missing one character
|
|
293
|
+
for i in range(len(cmd_lower)):
|
|
294
|
+
if cmd_lower[:i] + cmd_lower[i + 1 :] == bad_lower:
|
|
295
|
+
typo_matches.append(cmd)
|
|
296
|
+
break
|
|
297
|
+
else:
|
|
298
|
+
# Check if bad_cmd has one extra character
|
|
299
|
+
for i in range(len(bad_lower)):
|
|
300
|
+
if bad_lower[:i] + bad_lower[i + 1 :] == cmd_lower:
|
|
301
|
+
typo_matches.append(cmd)
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
return typo_matches
|
|
305
|
+
|
|
306
|
+
def _get_example_commands(self, command_path: str, bad_cmd: str) -> List[str]:
|
|
307
|
+
"""Get example commands based on context."""
|
|
308
|
+
examples = []
|
|
309
|
+
|
|
310
|
+
# Context-specific examples based on the command path
|
|
311
|
+
if "vpn" in command_path:
|
|
312
|
+
examples = [
|
|
313
|
+
f"{command_path} create myservice --profile default --port 8888",
|
|
314
|
+
f"{command_path} list",
|
|
315
|
+
f"{command_path} start myservice",
|
|
316
|
+
]
|
|
317
|
+
elif "profile" in command_path:
|
|
318
|
+
examples = [
|
|
319
|
+
f"{command_path} create myprofile --env-file /path/to/.env",
|
|
320
|
+
f"{command_path} list",
|
|
321
|
+
f"{command_path} delete myprofile",
|
|
322
|
+
]
|
|
323
|
+
elif "servers" in command_path:
|
|
324
|
+
examples = [
|
|
325
|
+
f"{command_path} update",
|
|
326
|
+
f"{command_path} list-providers",
|
|
327
|
+
f"{command_path} list-countries nordvpn",
|
|
328
|
+
]
|
|
329
|
+
elif "bulk" in command_path:
|
|
330
|
+
examples = [
|
|
331
|
+
f"{command_path} up",
|
|
332
|
+
f"{command_path} down",
|
|
333
|
+
f"{command_path} status",
|
|
334
|
+
]
|
|
335
|
+
elif "preset" in command_path:
|
|
336
|
+
examples = [
|
|
337
|
+
f"{command_path} list",
|
|
338
|
+
f"{command_path} apply mypreset myservice",
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
# Limit to 3 most relevant examples
|
|
342
|
+
return examples[:3]
|
|
343
|
+
|
|
344
|
+
def _get_documentation_link(self, command_path: str) -> Optional[str]:
|
|
345
|
+
"""Get documentation link for the command."""
|
|
346
|
+
# This could be configured per-project
|
|
347
|
+
# For now, return GitHub repository link
|
|
348
|
+
base_url = "https://github.com/eirenik0/proxy2vpn"
|
|
349
|
+
|
|
350
|
+
# Could map specific commands to doc sections
|
|
351
|
+
doc_sections = {
|
|
352
|
+
"vpn": f"{base_url}#vpn-management",
|
|
353
|
+
"profile": f"{base_url}#profile-management",
|
|
354
|
+
"servers": f"{base_url}#server-lists",
|
|
355
|
+
"bulk": f"{base_url}#bulk-operations",
|
|
356
|
+
"preset": f"{base_url}#presets",
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
# Find the relevant section based on command path
|
|
360
|
+
for key, url in doc_sections.items():
|
|
361
|
+
if key in command_path.lower():
|
|
362
|
+
return url
|
|
363
|
+
|
|
364
|
+
return base_url
|
|
365
|
+
|
|
366
|
+
def _all_command_paths(self) -> List[str]:
|
|
367
|
+
"""Return all command and subcommand paths for this app."""
|
|
368
|
+
|
|
369
|
+
def walk(app: typer.Typer, prefix: str = "") -> List[str]:
|
|
370
|
+
items: List[str] = []
|
|
371
|
+
for cmd in app.registered_commands:
|
|
372
|
+
items.append(f"{prefix}{cmd.name}".strip())
|
|
373
|
+
for grp in app.registered_groups:
|
|
374
|
+
full = f"{prefix}{grp.name}".strip()
|
|
375
|
+
items.append(full)
|
|
376
|
+
items.extend(walk(grp.typer_instance, f"{full} "))
|
|
377
|
+
return items
|
|
378
|
+
|
|
379
|
+
return walk(self)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proxy2vpn
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Proxy2VPN Python utilities
|
|
5
|
+
Author-email: Serhii Khalymon <serhii.khalymon@pm.me>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: vpn,proxy,utilities
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: typer
|
|
14
|
+
Requires-Dist: ruamel.yaml
|
|
15
|
+
Requires-Dist: docker
|
|
16
|
+
Requires-Dist: requests
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: towncrier; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# Proxy2VPN
|
|
21
|
+
|
|
22
|
+
Python command-line interface for managing multiple VPN containers with Docker.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
- Manage VPN credentials as reusable profiles
|
|
26
|
+
- Create and control VPN services
|
|
27
|
+
- Bulk start/stop operations
|
|
28
|
+
- Query and validate provider server locations
|
|
29
|
+
- Apply predefined presets for common setups
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
### Using uv (recommended)
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/eirenik0/proxy2vpn.git
|
|
36
|
+
cd proxy2vpn
|
|
37
|
+
uv sync
|
|
38
|
+
uv run proxy2vpn --help
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Using pip
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/eirenik0/proxy2vpn.git
|
|
44
|
+
cd proxy2vpn
|
|
45
|
+
pip install -e .
|
|
46
|
+
proxy2vpn --help
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
1. Create a profile file with your VPN credentials:
|
|
51
|
+
```bash
|
|
52
|
+
mkdir -p profiles
|
|
53
|
+
cat <<'EOF' > profiles/myprofile.env
|
|
54
|
+
OPENVPN_USER=your_username
|
|
55
|
+
OPENVPN_PASSWORD=your_password
|
|
56
|
+
EOF
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
2. Register the profile:
|
|
60
|
+
```bash
|
|
61
|
+
proxy2vpn profile create myprofile profiles/myprofile.env
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
3. Create and start a VPN service:
|
|
65
|
+
```bash
|
|
66
|
+
proxy2vpn vpn create vpn1 myprofile --port 8888 --provider protonvpn --location "New York"
|
|
67
|
+
proxy2vpn vpn start vpn1
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
4. View status and test connectivity:
|
|
71
|
+
```bash
|
|
72
|
+
proxy2vpn vpn list
|
|
73
|
+
proxy2vpn test vpn1
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Command overview
|
|
77
|
+
|
|
78
|
+
### Profiles
|
|
79
|
+
- `proxy2vpn profile create NAME ENV_FILE`
|
|
80
|
+
- `proxy2vpn profile list`
|
|
81
|
+
- `proxy2vpn profile delete NAME`
|
|
82
|
+
|
|
83
|
+
### VPN services
|
|
84
|
+
- `proxy2vpn vpn create NAME PROFILE [--port PORT] [--provider PROVIDER] [--location LOCATION]`
|
|
85
|
+
- `proxy2vpn vpn list`
|
|
86
|
+
- `proxy2vpn vpn start NAME`
|
|
87
|
+
- `proxy2vpn vpn stop NAME`
|
|
88
|
+
- `proxy2vpn vpn restart NAME`
|
|
89
|
+
- `proxy2vpn vpn logs NAME [--lines N] [--follow]`
|
|
90
|
+
- `proxy2vpn vpn delete NAME`
|
|
91
|
+
|
|
92
|
+
### Bulk operations
|
|
93
|
+
- `proxy2vpn bulk up`
|
|
94
|
+
- `proxy2vpn bulk down`
|
|
95
|
+
- `proxy2vpn bulk status`
|
|
96
|
+
- `proxy2vpn bulk ips`
|
|
97
|
+
|
|
98
|
+
### Server database
|
|
99
|
+
- `proxy2vpn servers update`
|
|
100
|
+
- `proxy2vpn servers list-providers`
|
|
101
|
+
- `proxy2vpn servers list-countries PROVIDER`
|
|
102
|
+
- `proxy2vpn servers list-cities PROVIDER COUNTRY`
|
|
103
|
+
- `proxy2vpn servers validate-location PROVIDER LOCATION`
|
|
104
|
+
|
|
105
|
+
### Presets
|
|
106
|
+
- `proxy2vpn preset list`
|
|
107
|
+
- `proxy2vpn preset apply PRESET SERVICE [--port PORT]`
|
|
108
|
+
|
|
109
|
+
### Testing
|
|
110
|
+
- `proxy2vpn test SERVICE` – verify that a proxy container is reachable
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
### Setup
|
|
115
|
+
```bash
|
|
116
|
+
# Install with development dependencies
|
|
117
|
+
uv sync
|
|
118
|
+
# or
|
|
119
|
+
pip install -e ".[dev]"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Testing
|
|
123
|
+
```bash
|
|
124
|
+
# Run tests (if available)
|
|
125
|
+
pytest
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Changelog Management
|
|
129
|
+
This project uses [Towncrier](https://towncrier.readthedocs.io/) for changelog management:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Add a news fragment for your changes
|
|
133
|
+
echo "Your feature description" > news/<PR_NUMBER>.feature.md
|
|
134
|
+
|
|
135
|
+
# Preview the changelog
|
|
136
|
+
make changelog-draft
|
|
137
|
+
|
|
138
|
+
# Build the changelog (maintainers)
|
|
139
|
+
make changelog VERSION=x.y.z
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
proxy2vpn/__init__.py,sha256=jfctUiEaedYR-08PksSDiVwe-wVJhS6ZFOc-4Xyu6Ts,384
|
|
2
|
+
proxy2vpn/cli.py,sha256=x5iJmbv_e1EN7gso4mu1x5kaJNvJI1SUJCMXIdskTrs,11831
|
|
3
|
+
proxy2vpn/compose_manager.py,sha256=dcqS8s_ZQ0w-Cq2MGlhFeEIw7dcaYJr0-JKqLmm0hOU,4200
|
|
4
|
+
proxy2vpn/compose_utils.py,sha256=XO0owJE0Th0Yvcp6Yx-elCj-dhZOiJ2schPdozztcsQ,1078
|
|
5
|
+
proxy2vpn/config.py,sha256=k5deUPaazAv8dAzs9DK6UIE8wNmOXQxqIxziy266nE4,1378
|
|
6
|
+
proxy2vpn/docker_ops.py,sha256=2UGwVKj3NJOKsl1wlrx3of8ube0WSOz0kT3aG3WpaPY,5033
|
|
7
|
+
proxy2vpn/models.py,sha256=8BKSr9omEL79EoMy2Bqn1l1zLxGPRI3IX-JS9BaWre8,3188
|
|
8
|
+
proxy2vpn/preset_manager.py,sha256=yMqledCWm37uK1amIo9dKtmrGr6Or-6JO1otpKb_GoE,1305
|
|
9
|
+
proxy2vpn/server_manager.py,sha256=oF-jJg1GfBy7T44TTErL2xBIRYYeV3_bHKjkjU1-MCY,3853
|
|
10
|
+
proxy2vpn/typer_ext.py,sha256=m0aY9VMzFJJiyljVuzA57WYW503Ups-uRkNh5X-oM6k,14724
|
|
11
|
+
proxy2vpn-0.1.3.dist-info/METADATA,sha256=cN9dsSCqQ06m_Jmu-pxflOSR_HgqYZBjJHEEON9c6Ko,3259
|
|
12
|
+
proxy2vpn-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
+
proxy2vpn-0.1.3.dist-info/entry_points.txt,sha256=8TGD35AHEg9kXgIiIZtx6vDYFyHDaxS1kwlF-befq54,48
|
|
14
|
+
proxy2vpn-0.1.3.dist-info/top_level.txt,sha256=_NBXdNrzaYifJYpH2nLe37rdEPjy9sTw4AKd2fFVCb0,10
|
|
15
|
+
proxy2vpn-0.1.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
proxy2vpn
|