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,667 @@
|
|
|
1
|
+
"""Interactive geo-data selection via fuzzy search.
|
|
2
|
+
|
|
3
|
+
Loads data from ``geo.json`` and provides convenient autocomplete
|
|
4
|
+
prompts for the CLI: country -> state -> city -> ASN.
|
|
5
|
+
|
|
6
|
+
Every prompt is **validated** — only values that exist in the
|
|
7
|
+
geo-data can be submitted.
|
|
8
|
+
|
|
9
|
+
Example::
|
|
10
|
+
|
|
11
|
+
from asockslib.geo_picker import GeoPicker
|
|
12
|
+
|
|
13
|
+
picker = GeoPicker()
|
|
14
|
+
country = picker.pick_country()
|
|
15
|
+
state = picker.pick_state("US")
|
|
16
|
+
city = picker.pick_city("US", "California")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from beartype import beartype
|
|
27
|
+
from prompt_toolkit import PromptSession
|
|
28
|
+
from prompt_toolkit.formatted_text import HTML
|
|
29
|
+
|
|
30
|
+
from asockslib._console import console
|
|
31
|
+
from asockslib.geo_picker._completer import (
|
|
32
|
+
FuzzyMatchCompleter,
|
|
33
|
+
make_choice_validator,
|
|
34
|
+
pt_style,
|
|
35
|
+
)
|
|
36
|
+
from asockslib.geo_picker._messages import PROXY_TEMPLATES, get_message
|
|
37
|
+
from asockslib.geo_picker._types import (
|
|
38
|
+
CONNECTION_TYPES,
|
|
39
|
+
PROXY_TYPES,
|
|
40
|
+
SERVER_PORT_TYPES,
|
|
41
|
+
PickedASN,
|
|
42
|
+
PickedCity,
|
|
43
|
+
PickedConnectionType,
|
|
44
|
+
PickedCountry,
|
|
45
|
+
PickedProxyType,
|
|
46
|
+
PickedServerPortType,
|
|
47
|
+
PickedState,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
_GEO_JSON_PATH = Path(__file__).parent.parent / "geo.json"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GeoPicker:
|
|
54
|
+
"""Interactive geo-data picker from local ``geo.json``.
|
|
55
|
+
|
|
56
|
+
Loads the geo file once and caches it in memory.
|
|
57
|
+
All ``pick_*`` methods show a fuzzy-search autocomplete prompt
|
|
58
|
+
that displays **all** options when input is empty.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
lang: UI language (only ``"en"`` is supported now).
|
|
62
|
+
geo_json_path: Custom path to ``geo.json``.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
lang: str = "en",
|
|
68
|
+
geo_json_path: Path | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
self._lang = "en"
|
|
71
|
+
self._path = geo_json_path or _GEO_JSON_PATH
|
|
72
|
+
self._data: dict[str, Any] | None = None
|
|
73
|
+
|
|
74
|
+
def _load(self) -> dict[str, Any]:
|
|
75
|
+
"""Lazy-load geo.json."""
|
|
76
|
+
data = self._data
|
|
77
|
+
if data is None:
|
|
78
|
+
with open(self._path, encoding="utf-8") as f:
|
|
79
|
+
data = json.load(f)
|
|
80
|
+
self._data = data
|
|
81
|
+
return data
|
|
82
|
+
|
|
83
|
+
# ── Core autocomplete ─────────────────────────────────────────────── #
|
|
84
|
+
|
|
85
|
+
def _autocomplete(
|
|
86
|
+
self,
|
|
87
|
+
message: str,
|
|
88
|
+
choices: list[str],
|
|
89
|
+
meta: dict[str, Any] | None = None,
|
|
90
|
+
) -> str | None:
|
|
91
|
+
"""Run an autocomplete prompt with strict validation.
|
|
92
|
+
|
|
93
|
+
Only values present in *choices* can be submitted.
|
|
94
|
+
Returns ``None`` on Ctrl-C / EOF.
|
|
95
|
+
"""
|
|
96
|
+
completer = FuzzyMatchCompleter(choices)
|
|
97
|
+
_lower_lookup: dict[str, str] = {c.strip().lower(): c for c in choices}
|
|
98
|
+
|
|
99
|
+
validator = make_choice_validator(
|
|
100
|
+
choices,
|
|
101
|
+
error_msg=get_message("invalid_choice"),
|
|
102
|
+
error_chars=get_message("invalid_chars"),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
session: PromptSession[str] = PromptSession(
|
|
106
|
+
completer=completer,
|
|
107
|
+
complete_while_typing=True,
|
|
108
|
+
validator=validator,
|
|
109
|
+
validate_while_typing=False,
|
|
110
|
+
style=pt_style(),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _pre_run() -> None:
|
|
114
|
+
session.default_buffer.start_completion()
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
result: str = session.prompt(
|
|
118
|
+
HTML(f"<qmark>?</qmark> <question>{message}</question> "),
|
|
119
|
+
pre_run=_pre_run,
|
|
120
|
+
)
|
|
121
|
+
except (KeyboardInterrupt, EOFError):
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
stripped = result.strip()
|
|
125
|
+
if stripped in choices:
|
|
126
|
+
return stripped
|
|
127
|
+
key = stripped.lower()
|
|
128
|
+
if key in _lower_lookup:
|
|
129
|
+
return _lower_lookup[key]
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
# ── Public pick methods ───────────────────────────────────────────── #
|
|
133
|
+
|
|
134
|
+
@beartype
|
|
135
|
+
def pick_country(self, message: str | None = None) -> PickedCountry | None:
|
|
136
|
+
"""Select a country via fuzzy search."""
|
|
137
|
+
msg = message or get_message("pick_country")
|
|
138
|
+
data = self._load()
|
|
139
|
+
|
|
140
|
+
_title_to_code: dict[str, str] = {}
|
|
141
|
+
titles: list[str] = []
|
|
142
|
+
for code, info in sorted(data.items(), key=lambda x: x[1]["name"]):
|
|
143
|
+
title = f"{info['code']:>2} {info['name']}"
|
|
144
|
+
titles.append(title)
|
|
145
|
+
_title_to_code[title] = code
|
|
146
|
+
|
|
147
|
+
result = self._autocomplete(msg, titles)
|
|
148
|
+
if result is None:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
code = _title_to_code.get(result)
|
|
152
|
+
if code and code in data:
|
|
153
|
+
info = data[code]
|
|
154
|
+
return PickedCountry(code=code, name=info["name"], id=info["id"])
|
|
155
|
+
|
|
156
|
+
m = re.match(r"^\s*([A-Z]{2})\s{2,}(.+)$", result.strip())
|
|
157
|
+
if m:
|
|
158
|
+
code = m.group(1)
|
|
159
|
+
if code in data:
|
|
160
|
+
info = data[code]
|
|
161
|
+
return PickedCountry(code=code, name=info["name"], id=info["id"])
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
@beartype
|
|
165
|
+
def pick_state(
|
|
166
|
+
self,
|
|
167
|
+
country_code: str,
|
|
168
|
+
message: str | None = None,
|
|
169
|
+
allow_skip: bool = True,
|
|
170
|
+
) -> PickedState | None:
|
|
171
|
+
"""Select a state/region via fuzzy search."""
|
|
172
|
+
msg = message or get_message("pick_state")
|
|
173
|
+
data = self._load()
|
|
174
|
+
country = data.get(country_code.upper())
|
|
175
|
+
if not country:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
states = country.get("states", {})
|
|
179
|
+
state_items = [
|
|
180
|
+
(name, sdata) for name, sdata in sorted(states.items()) if name != "_no_state"
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
if not state_items:
|
|
184
|
+
console.print(f"[dim]{get_message('no_states')}[/dim]")
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
skip_label = get_message("skip")
|
|
188
|
+
cities_word = get_message("cities_suffix")
|
|
189
|
+
|
|
190
|
+
_title_to_state: dict[str, str] = {}
|
|
191
|
+
titles: list[str] = []
|
|
192
|
+
if allow_skip:
|
|
193
|
+
titles.append(skip_label)
|
|
194
|
+
for name, sdata in state_items:
|
|
195
|
+
city_count = len(sdata.get("cities", []))
|
|
196
|
+
title = f"{name} ({city_count} {cities_word})"
|
|
197
|
+
titles.append(title)
|
|
198
|
+
_title_to_state[title] = name
|
|
199
|
+
|
|
200
|
+
result = self._autocomplete(msg, titles)
|
|
201
|
+
if result is None or result.startswith("⏭"):
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
state_name = _title_to_state.get(result)
|
|
205
|
+
if state_name is None:
|
|
206
|
+
m = re.match(r"^(.+?)\s{2}\(\d+\s", result)
|
|
207
|
+
if m:
|
|
208
|
+
state_name = m.group(1).strip()
|
|
209
|
+
|
|
210
|
+
if state_name is None:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
for name, sdata in state_items:
|
|
214
|
+
if name == state_name or name.lower() == state_name.lower():
|
|
215
|
+
return PickedState(name=name, id=sdata["id"])
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
@beartype
|
|
219
|
+
def pick_city(
|
|
220
|
+
self,
|
|
221
|
+
country_code: str,
|
|
222
|
+
state_name: str | None = None,
|
|
223
|
+
message: str | None = None,
|
|
224
|
+
allow_skip: bool = True,
|
|
225
|
+
) -> PickedCity | None:
|
|
226
|
+
"""Select a city via fuzzy search."""
|
|
227
|
+
msg = message or get_message("pick_city")
|
|
228
|
+
data = self._load()
|
|
229
|
+
country = data.get(country_code.upper())
|
|
230
|
+
if not country:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
cities: list[dict[str, Any]] = []
|
|
234
|
+
states = country.get("states", {})
|
|
235
|
+
|
|
236
|
+
if state_name:
|
|
237
|
+
state = states.get(state_name)
|
|
238
|
+
if not state:
|
|
239
|
+
for sname, sdata in states.items():
|
|
240
|
+
if sname.lower() == state_name.lower():
|
|
241
|
+
state = sdata
|
|
242
|
+
break
|
|
243
|
+
if state:
|
|
244
|
+
cities = state.get("cities", [])
|
|
245
|
+
else:
|
|
246
|
+
for sdata in states.values():
|
|
247
|
+
cities.extend(sdata.get("cities", []))
|
|
248
|
+
|
|
249
|
+
if not cities:
|
|
250
|
+
console.print(f"[dim]{get_message('no_cities')}[/dim]")
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
sorted_cities = sorted(cities, key=lambda x: x["name"])
|
|
254
|
+
skip_label = get_message("skip")
|
|
255
|
+
|
|
256
|
+
_name_to_city: dict[str, dict[str, Any]] = {}
|
|
257
|
+
_lower_to_city: dict[str, dict[str, Any]] = {}
|
|
258
|
+
titles: list[str] = []
|
|
259
|
+
if allow_skip:
|
|
260
|
+
titles.append(skip_label)
|
|
261
|
+
for city in sorted_cities:
|
|
262
|
+
cname = city["name"]
|
|
263
|
+
titles.append(cname)
|
|
264
|
+
_name_to_city[cname] = city
|
|
265
|
+
_lower_to_city[cname.lower()] = city
|
|
266
|
+
|
|
267
|
+
result = self._autocomplete(msg, titles)
|
|
268
|
+
if result is None or result.startswith("⏭"):
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
city_data = _name_to_city.get(result)
|
|
272
|
+
if city_data is None:
|
|
273
|
+
city_data = _lower_to_city.get(result.strip().lower())
|
|
274
|
+
|
|
275
|
+
if city_data is not None:
|
|
276
|
+
return PickedCity(name=city_data["name"], id=city_data["id"])
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
@beartype
|
|
280
|
+
def pick_asn(
|
|
281
|
+
self,
|
|
282
|
+
country_code: str,
|
|
283
|
+
message: str | None = None,
|
|
284
|
+
allow_skip: bool = True,
|
|
285
|
+
) -> PickedASN | None:
|
|
286
|
+
"""Select an ASN provider via fuzzy search."""
|
|
287
|
+
msg = message or get_message("pick_asn")
|
|
288
|
+
data = self._load()
|
|
289
|
+
country = data.get(country_code.upper())
|
|
290
|
+
if not country:
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
asns = country.get("asns", [])
|
|
294
|
+
if not asns:
|
|
295
|
+
console.print(f"[dim]{get_message('no_asns')}[/dim]")
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
sorted_asns = sorted(asns, key=lambda x: x["asn"])
|
|
299
|
+
skip_label = get_message("skip")
|
|
300
|
+
|
|
301
|
+
_label_to_asn: dict[str, dict[str, Any]] = {}
|
|
302
|
+
titles: list[str] = []
|
|
303
|
+
if allow_skip:
|
|
304
|
+
titles.append(skip_label)
|
|
305
|
+
for asn in sorted_asns:
|
|
306
|
+
name = asn.get("name", "")
|
|
307
|
+
label = f"AS{asn['asn']}" + (f" {name}" if name else "")
|
|
308
|
+
titles.append(label)
|
|
309
|
+
_label_to_asn[label] = asn
|
|
310
|
+
|
|
311
|
+
result = self._autocomplete(msg, titles)
|
|
312
|
+
if result is None or result.startswith("⏭"):
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
asn_data = _label_to_asn.get(result)
|
|
316
|
+
if asn_data is None:
|
|
317
|
+
rl = result.strip().lower()
|
|
318
|
+
for label, adata in _label_to_asn.items():
|
|
319
|
+
if label.lower() == rl:
|
|
320
|
+
asn_data = adata
|
|
321
|
+
break
|
|
322
|
+
|
|
323
|
+
if asn_data is not None:
|
|
324
|
+
return PickedASN(number=asn_data["asn"], name=asn_data.get("name", ""))
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
# ── Type pickers ──────────────────────────────────────────────────── #
|
|
328
|
+
|
|
329
|
+
@beartype
|
|
330
|
+
def _pick_from_list(
|
|
331
|
+
self,
|
|
332
|
+
message: str,
|
|
333
|
+
items: list[tuple[int, str]],
|
|
334
|
+
) -> tuple[int, str] | None:
|
|
335
|
+
"""Generic picker for static (id, label) lists."""
|
|
336
|
+
_label_to_id: dict[str, int] = {}
|
|
337
|
+
titles: list[str] = []
|
|
338
|
+
for type_id, label in items:
|
|
339
|
+
titles.append(label)
|
|
340
|
+
_label_to_id[label] = type_id
|
|
341
|
+
|
|
342
|
+
result = self._autocomplete(message, titles)
|
|
343
|
+
if result is None:
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
tid = _label_to_id.get(result)
|
|
347
|
+
if tid is None:
|
|
348
|
+
rl = result.strip().lower()
|
|
349
|
+
for label, t in _label_to_id.items():
|
|
350
|
+
if label.lower() == rl:
|
|
351
|
+
tid = t
|
|
352
|
+
result = label
|
|
353
|
+
break
|
|
354
|
+
if tid is not None:
|
|
355
|
+
return tid, result
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
@beartype
|
|
359
|
+
def pick_connection_type(self, message: str | None = None) -> PickedConnectionType | None:
|
|
360
|
+
"""Select a connection type."""
|
|
361
|
+
msg = message or get_message("pick_connection_type")
|
|
362
|
+
picked = self._pick_from_list(msg, CONNECTION_TYPES)
|
|
363
|
+
if picked:
|
|
364
|
+
return PickedConnectionType(id=picked[0], label=picked[1])
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
@beartype
|
|
368
|
+
def pick_proxy_type(self, message: str | None = None) -> PickedProxyType | None:
|
|
369
|
+
"""Select a proxy type."""
|
|
370
|
+
msg = message or get_message("pick_proxy_type")
|
|
371
|
+
picked = self._pick_from_list(msg, PROXY_TYPES)
|
|
372
|
+
if picked:
|
|
373
|
+
return PickedProxyType(id=picked[0], label=picked[1])
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
@beartype
|
|
377
|
+
def pick_server_port_type(self, message: str | None = None) -> PickedServerPortType | None:
|
|
378
|
+
"""Select a server port type."""
|
|
379
|
+
msg = message or get_message("pick_server_port_type")
|
|
380
|
+
picked = self._pick_from_list(msg, SERVER_PORT_TYPES)
|
|
381
|
+
if picked:
|
|
382
|
+
return PickedServerPortType(id=picked[0], label=picked[1])
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
# ── Free-text input ───────────────────────────────────────────────── #
|
|
386
|
+
|
|
387
|
+
@beartype
|
|
388
|
+
def _free_input(
|
|
389
|
+
self,
|
|
390
|
+
message: str,
|
|
391
|
+
default: str = "",
|
|
392
|
+
allow_empty: bool = True,
|
|
393
|
+
) -> str:
|
|
394
|
+
"""Show a free-text input prompt."""
|
|
395
|
+
session: PromptSession[str] = PromptSession(style=pt_style())
|
|
396
|
+
placeholder = HTML(f"<ansigray>{default}</ansigray>") if default else None
|
|
397
|
+
try:
|
|
398
|
+
result: str = session.prompt(
|
|
399
|
+
HTML(f"<qmark>?</qmark> <question>{message}</question> "),
|
|
400
|
+
placeholder=placeholder,
|
|
401
|
+
)
|
|
402
|
+
except (KeyboardInterrupt, EOFError):
|
|
403
|
+
return default
|
|
404
|
+
|
|
405
|
+
stripped = result.strip()
|
|
406
|
+
if not stripped and allow_empty:
|
|
407
|
+
return default
|
|
408
|
+
return stripped
|
|
409
|
+
|
|
410
|
+
# ── Action / number / misc pickers ────────────────────────────────── #
|
|
411
|
+
|
|
412
|
+
@beartype
|
|
413
|
+
def pick_action(self, message: str | None = None) -> str | None:
|
|
414
|
+
"""Pick wizard action: create proxies or find best proxies.
|
|
415
|
+
|
|
416
|
+
Returns ``"create"``, ``"best"`` or ``None``.
|
|
417
|
+
"""
|
|
418
|
+
msg = message or get_message("pick_action")
|
|
419
|
+
action_create = get_message("action_create")
|
|
420
|
+
action_best = get_message("action_best")
|
|
421
|
+
choices = [action_create, action_best]
|
|
422
|
+
|
|
423
|
+
result = self._autocomplete(msg, choices)
|
|
424
|
+
if result is None:
|
|
425
|
+
return None
|
|
426
|
+
if result == action_best:
|
|
427
|
+
return "best"
|
|
428
|
+
return "create"
|
|
429
|
+
|
|
430
|
+
@beartype
|
|
431
|
+
def pick_count(self, message: str | None = None, default: int = 10) -> int:
|
|
432
|
+
"""Pick number of proxies to create."""
|
|
433
|
+
msg = message or get_message("pick_count")
|
|
434
|
+
choices = ["1", "5", "10", "25", "50", "100", "250", "500", "1000"]
|
|
435
|
+
result = self._autocomplete(msg, choices)
|
|
436
|
+
if result and result.isdigit():
|
|
437
|
+
return int(result)
|
|
438
|
+
return default
|
|
439
|
+
|
|
440
|
+
@beartype
|
|
441
|
+
def pick_keep(self, message: str | None = None, default: int = 10) -> int:
|
|
442
|
+
"""Pick how many best proxies to keep."""
|
|
443
|
+
msg = message or get_message("pick_keep")
|
|
444
|
+
choices = ["1", "3", "5", "10", "20", "50"]
|
|
445
|
+
result = self._autocomplete(msg, choices)
|
|
446
|
+
if result and result.isdigit():
|
|
447
|
+
return int(result)
|
|
448
|
+
return default
|
|
449
|
+
|
|
450
|
+
@beartype
|
|
451
|
+
def pick_name(self, message: str | None = None) -> str:
|
|
452
|
+
"""Pick port name (free text, optional)."""
|
|
453
|
+
msg = message or get_message("pick_name")
|
|
454
|
+
return self._free_input(msg, default="", allow_empty=True)
|
|
455
|
+
|
|
456
|
+
@beartype
|
|
457
|
+
def pick_format(self, message: str | None = None) -> str:
|
|
458
|
+
"""Pick export format (``txt``, ``json``, ``csv``)."""
|
|
459
|
+
msg = message or get_message("pick_format")
|
|
460
|
+
labels = [
|
|
461
|
+
"txt — one proxy per line",
|
|
462
|
+
"json — JSON array",
|
|
463
|
+
"csv — CSV with header",
|
|
464
|
+
]
|
|
465
|
+
result = self._autocomplete(msg, labels)
|
|
466
|
+
if result:
|
|
467
|
+
return result.split(" ")[0]
|
|
468
|
+
return "txt"
|
|
469
|
+
|
|
470
|
+
@beartype
|
|
471
|
+
def pick_output(self, message: str | None = None) -> str:
|
|
472
|
+
"""Pick output file path (optional)."""
|
|
473
|
+
msg = message or get_message("pick_output")
|
|
474
|
+
return self._free_input(msg, default="", allow_empty=True)
|
|
475
|
+
|
|
476
|
+
@beartype
|
|
477
|
+
def pick_timeout(self, message: str | None = None, default: float = 15.0) -> float:
|
|
478
|
+
"""Pick benchmark timeout."""
|
|
479
|
+
msg = message or get_message("pick_timeout")
|
|
480
|
+
choices = ["5", "10", "15", "30", "60"]
|
|
481
|
+
result = self._autocomplete(msg, choices)
|
|
482
|
+
if result and result.replace(".", "", 1).isdigit():
|
|
483
|
+
return float(result)
|
|
484
|
+
return default
|
|
485
|
+
|
|
486
|
+
@beartype
|
|
487
|
+
def pick_concurrency(self, message: str | None = None, default: int = 50) -> int:
|
|
488
|
+
"""Pick benchmark concurrency."""
|
|
489
|
+
msg = message or get_message("pick_concurrency")
|
|
490
|
+
choices = ["10", "25", "50", "100", "200"]
|
|
491
|
+
result = self._autocomplete(msg, choices)
|
|
492
|
+
if result and result.isdigit():
|
|
493
|
+
return int(result)
|
|
494
|
+
return default
|
|
495
|
+
|
|
496
|
+
# ── Proxy template picker ─────────────────────────────────────────── #
|
|
497
|
+
|
|
498
|
+
@beartype
|
|
499
|
+
def pick_proxy_template(self, message: str | None = None) -> str:
|
|
500
|
+
"""Select a proxy output template.
|
|
501
|
+
|
|
502
|
+
Returns the template string, e.g.
|
|
503
|
+
``"{protocol}://{login}:{password}@{ip}:{port}"``.
|
|
504
|
+
"""
|
|
505
|
+
msg = message or get_message("pick_proxy_template")
|
|
506
|
+
_label_to_tpl: dict[str, str] = {}
|
|
507
|
+
titles: list[str] = []
|
|
508
|
+
for tpl, label in PROXY_TEMPLATES:
|
|
509
|
+
titles.append(label)
|
|
510
|
+
_label_to_tpl[label] = tpl
|
|
511
|
+
|
|
512
|
+
result = self._autocomplete(msg, titles)
|
|
513
|
+
if result is None:
|
|
514
|
+
return "{protocol}://{login}:{password}@{ip}:{port}"
|
|
515
|
+
|
|
516
|
+
tpl = _label_to_tpl.get(result)
|
|
517
|
+
if tpl is None:
|
|
518
|
+
rl = result.strip().lower()
|
|
519
|
+
for label, t in _label_to_tpl.items():
|
|
520
|
+
if label.lower() == rl:
|
|
521
|
+
tpl = t
|
|
522
|
+
break
|
|
523
|
+
return tpl or "{protocol}://{login}:{password}@{ip}:{port}"
|
|
524
|
+
|
|
525
|
+
# ── Full wizards ──────────────────────────────────────────────────── #
|
|
526
|
+
|
|
527
|
+
@beartype
|
|
528
|
+
def pick_full(self) -> dict[str, str | int | None]:
|
|
529
|
+
"""Full wizard: connection type -> proxy type -> port type -> geo."""
|
|
530
|
+
result: dict[str, str | int | None] = {
|
|
531
|
+
"country_code": None,
|
|
532
|
+
"country_name": None,
|
|
533
|
+
"state": None,
|
|
534
|
+
"city": None,
|
|
535
|
+
"city_id": None,
|
|
536
|
+
"type_id": None,
|
|
537
|
+
"proxy_type_id": None,
|
|
538
|
+
"server_port_type_id": None,
|
|
539
|
+
"traffic_limit": None,
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
conn = self.pick_connection_type()
|
|
543
|
+
if conn:
|
|
544
|
+
result["type_id"] = conn.id
|
|
545
|
+
|
|
546
|
+
ptype = self.pick_proxy_type()
|
|
547
|
+
if ptype:
|
|
548
|
+
result["proxy_type_id"] = ptype.id
|
|
549
|
+
|
|
550
|
+
spt = self.pick_server_port_type()
|
|
551
|
+
if spt:
|
|
552
|
+
result["server_port_type_id"] = spt.id
|
|
553
|
+
|
|
554
|
+
country = self.pick_country()
|
|
555
|
+
if not country:
|
|
556
|
+
return result
|
|
557
|
+
result["country_code"] = country.code
|
|
558
|
+
result["country_name"] = country.name
|
|
559
|
+
|
|
560
|
+
state = self.pick_state(country.code)
|
|
561
|
+
if state:
|
|
562
|
+
result["state"] = state.name
|
|
563
|
+
|
|
564
|
+
city = self.pick_city(
|
|
565
|
+
country.code,
|
|
566
|
+
state_name=state.name if state else None,
|
|
567
|
+
)
|
|
568
|
+
if city:
|
|
569
|
+
result["city"] = city.name
|
|
570
|
+
result["city_id"] = city.id
|
|
571
|
+
|
|
572
|
+
if result.get("server_port_type_id") == 1:
|
|
573
|
+
traffic_msg = get_message("pick_traffic_limit")
|
|
574
|
+
traffic_choices = ["1", "5", "10", "25", "50", "100", "250", "500", "1000"]
|
|
575
|
+
traffic_result = self._autocomplete(traffic_msg, traffic_choices)
|
|
576
|
+
if traffic_result and traffic_result.isdigit():
|
|
577
|
+
result["traffic_limit"] = int(traffic_result)
|
|
578
|
+
|
|
579
|
+
return result
|
|
580
|
+
|
|
581
|
+
@beartype
|
|
582
|
+
def pick_wizard(self) -> dict[str, str | int | float | None]:
|
|
583
|
+
"""Full interactive wizard — everything in one flow.
|
|
584
|
+
|
|
585
|
+
Returns a dict with all wizard parameters.
|
|
586
|
+
"""
|
|
587
|
+
result: dict[str, str | int | float | None] = {
|
|
588
|
+
"action": None,
|
|
589
|
+
"country_code": None,
|
|
590
|
+
"country_name": None,
|
|
591
|
+
"state": None,
|
|
592
|
+
"city": None,
|
|
593
|
+
"city_id": None,
|
|
594
|
+
"type_id": 1,
|
|
595
|
+
"proxy_type_id": 1,
|
|
596
|
+
"server_port_type_id": 0,
|
|
597
|
+
"traffic_limit": 10,
|
|
598
|
+
"count": 10,
|
|
599
|
+
"keep": 10,
|
|
600
|
+
"ttl": 1,
|
|
601
|
+
"name": "",
|
|
602
|
+
"format": "txt",
|
|
603
|
+
"output": "",
|
|
604
|
+
"timeout": 15.0,
|
|
605
|
+
"concurrency": 50,
|
|
606
|
+
"proxy_template": "{protocol}://{login}:{password}@{ip}:{port}",
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
action = self.pick_action()
|
|
610
|
+
if action is None:
|
|
611
|
+
return result
|
|
612
|
+
result["action"] = action
|
|
613
|
+
|
|
614
|
+
ptype = self.pick_proxy_type()
|
|
615
|
+
if ptype:
|
|
616
|
+
result["proxy_type_id"] = ptype.id
|
|
617
|
+
|
|
618
|
+
conn = self.pick_connection_type()
|
|
619
|
+
if conn:
|
|
620
|
+
result["type_id"] = conn.id
|
|
621
|
+
|
|
622
|
+
spt = self.pick_server_port_type()
|
|
623
|
+
if spt:
|
|
624
|
+
result["server_port_type_id"] = spt.id
|
|
625
|
+
|
|
626
|
+
country = self.pick_country()
|
|
627
|
+
if not country:
|
|
628
|
+
return result
|
|
629
|
+
result["country_code"] = country.code
|
|
630
|
+
result["country_name"] = country.name
|
|
631
|
+
|
|
632
|
+
state = self.pick_state(country.code)
|
|
633
|
+
if state:
|
|
634
|
+
result["state"] = state.name
|
|
635
|
+
|
|
636
|
+
city = self.pick_city(
|
|
637
|
+
country.code,
|
|
638
|
+
state_name=state.name if state else None,
|
|
639
|
+
)
|
|
640
|
+
if city:
|
|
641
|
+
result["city"] = city.name
|
|
642
|
+
result["city_id"] = city.id
|
|
643
|
+
|
|
644
|
+
if action == "best":
|
|
645
|
+
result["count"] = self.pick_count(default=100)
|
|
646
|
+
result["keep"] = self.pick_keep(default=10)
|
|
647
|
+
else:
|
|
648
|
+
result["count"] = self.pick_count(default=10)
|
|
649
|
+
|
|
650
|
+
result["name"] = self.pick_name()
|
|
651
|
+
|
|
652
|
+
if result.get("server_port_type_id") == 1:
|
|
653
|
+
traffic_msg = get_message("pick_traffic_limit")
|
|
654
|
+
traffic_choices = ["1", "5", "10", "25", "50", "100", "250", "500", "1000"]
|
|
655
|
+
traffic_result = self._autocomplete(traffic_msg, traffic_choices)
|
|
656
|
+
if traffic_result and traffic_result.isdigit():
|
|
657
|
+
result["traffic_limit"] = int(traffic_result)
|
|
658
|
+
|
|
659
|
+
result["proxy_template"] = self.pick_proxy_template()
|
|
660
|
+
result["format"] = self.pick_format()
|
|
661
|
+
result["output"] = self.pick_output()
|
|
662
|
+
|
|
663
|
+
if action == "best":
|
|
664
|
+
result["timeout"] = self.pick_timeout()
|
|
665
|
+
result["concurrency"] = self.pick_concurrency()
|
|
666
|
+
|
|
667
|
+
return result
|