proxy-scraper-cli 1.7.1__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.
Files changed (57) hide show
  1. proxy_scraper_cli-1.7.1.dist-info/METADATA +794 -0
  2. proxy_scraper_cli-1.7.1.dist-info/RECORD +57 -0
  3. proxy_scraper_cli-1.7.1.dist-info/WHEEL +5 -0
  4. proxy_scraper_cli-1.7.1.dist-info/entry_points.txt +4 -0
  5. proxy_scraper_cli-1.7.1.dist-info/licenses/LICENSE +21 -0
  6. proxy_scraper_cli-1.7.1.dist-info/top_level.txt +1 -0
  7. proxyscraper/__init__.py +12 -0
  8. proxyscraper/__main__.py +7 -0
  9. proxyscraper/agent.py +635 -0
  10. proxyscraper/api.py +139 -0
  11. proxyscraper/app.py +595 -0
  12. proxyscraper/asndb.py +181 -0
  13. proxyscraper/blocklist.py +92 -0
  14. proxyscraper/checker.py +514 -0
  15. proxyscraper/cli.py +269 -0
  16. proxyscraper/compat.py +83 -0
  17. proxyscraper/completion.py +196 -0
  18. proxyscraper/exporters.py +118 -0
  19. proxyscraper/fetchcache.py +103 -0
  20. proxyscraper/geo.py +150 -0
  21. proxyscraper/geodb.py +143 -0
  22. proxyscraper/handshake.py +153 -0
  23. proxyscraper/history.py +106 -0
  24. proxyscraper/judges.py +159 -0
  25. proxyscraper/mcp_entry.py +34 -0
  26. proxyscraper/mcp_server.py +220 -0
  27. proxyscraper/netio.py +167 -0
  28. proxyscraper/options.py +274 -0
  29. proxyscraper/output.py +181 -0
  30. proxyscraper/pages.py +212 -0
  31. proxyscraper/parsing.py +192 -0
  32. proxyscraper/paths.py +55 -0
  33. proxyscraper/pipeline.py +434 -0
  34. proxyscraper/preferences.py +24 -0
  35. proxyscraper/publish.py +236 -0
  36. proxyscraper/server/__init__.py +41 -0
  37. proxyscraper/server/core.py +518 -0
  38. proxyscraper/server/http.py +164 -0
  39. proxyscraper/server/pool.py +195 -0
  40. proxyscraper/server/socks.py +65 -0
  41. proxyscraper/server/status.py +108 -0
  42. proxyscraper/server/upstream.py +119 -0
  43. proxyscraper/site/apple-touch-icon.png +0 -0
  44. proxyscraper/site/googleaac1161b7853c5b5.html +1 -0
  45. proxyscraper/site/index.html +934 -0
  46. proxyscraper/site/logo.png +0 -0
  47. proxyscraper/site/og.png +0 -0
  48. proxyscraper/sources.json +395 -0
  49. proxyscraper/sources.py +491 -0
  50. proxyscraper/targets.py +76 -0
  51. proxyscraper/ui/__init__.py +54 -0
  52. proxyscraper/ui/dashboard.py +344 -0
  53. proxyscraper/ui/keys.py +82 -0
  54. proxyscraper/ui/report.py +209 -0
  55. proxyscraper/ui/serve.py +142 -0
  56. proxyscraper/ui/widgets.py +250 -0
  57. proxyscraper/ui/wizard.py +595 -0
@@ -0,0 +1,142 @@
1
+ """Live view of the rotating proxy server (--serve)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+
8
+ from rich import box
9
+ from rich.align import Align
10
+ from rich.console import Group
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+
15
+ from ..server import RotatingServer
16
+ from . import widgets
17
+ from .widgets import (
18
+ ACCENT,
19
+ BAD,
20
+ GOOD,
21
+ MUTED,
22
+ TYPE_STYLE,
23
+ WARN,
24
+ card,
25
+ fmt,
26
+ fmt_duration,
27
+ pct,
28
+ row,
29
+ shown_proxy,
30
+ table,
31
+ )
32
+
33
+
34
+ def _mb(n: int) -> str:
35
+ return f"{n / 2**20:.1f} MB"
36
+
37
+
38
+ class ServeDashboard:
39
+ def __init__(self, server: RotatingServer):
40
+ self.server = server
41
+
42
+ def __rich__(self):
43
+ server, st, pool = self.server, self.server.stats, self.server.pool
44
+ # if it listens on all addresses (Docker), it can still be reached locally via 127.0.0.1
45
+ shown_host = {"0.0.0.0": "127.0.0.1", "": "127.0.0.1", "::": "[::1]"}.get(server.host, server.host)
46
+ if ":" in shown_host and not shown_host.startswith("["):
47
+ shown_host = f"[{shown_host}]" # IPv6 in URLs only in brackets
48
+ address = f"{shown_host}:{server.port}"
49
+ width = widgets.console.size.width
50
+ uptime = max(time.perf_counter() - st.started, 1e-6)
51
+ usable = pool.usable
52
+
53
+ title = Table.grid(expand=True)
54
+ title.add_column()
55
+ title.add_column(justify="right")
56
+ title.add_row(
57
+ Text.assemble(("● ", f"bold {GOOD}"), ("Proxy server running on ", "bold"), (address, f"bold {ACCENT}")),
58
+ Text(f"⏱ {fmt_duration(uptime)}", style=MUTED),
59
+ )
60
+
61
+ usage = Table.grid(padding=(0, 2))
62
+ usage.add_column(style=MUTED, no_wrap=True)
63
+ usage.add_column(overflow="fold")
64
+ # with a password every example needs the login; the password itself is never shown
65
+ from_env = server.password and os.environ.get("PROXY_SCRAPER_SERVE_PASSWORD") == server.password
66
+ pw = "$PROXY_SCRAPER_SERVE_PASSWORD" if from_env else "PASSWORD" if server.password else "x"
67
+ login = f"any:{pw}@" if server.password else ""
68
+ usage.add_row("Test", Text(f"curl -x http://{login}{address} https://api.ipify.org", style="bold"))
69
+ usage.add_row("SOCKS5", Text(f"curl -x socks5h://{login}{address} https://api.ipify.org"))
70
+ usage.add_row("One country", Text(f"curl -x http://country-de:{pw}@{address} https://api.ipify.org"))
71
+ usage.add_row("Fixed session", Text(f"curl -x http://session-abc:{pw}@{address} … (keeps the same proxy)"))
72
+ usage.add_row("Terminal", Text(f"export http_proxy=http://{login}{address} https_proxy=http://{login}{address}"))
73
+ usage.add_row("Status (JSON)", Text(f"curl {'-u any:' + pw + ' ' if server.password else ''}"
74
+ f"http://{address}/__proxy-scraper/status"))
75
+ usage.add_row("Prometheus", Text(f"http://{'any:' + pw + '@' if server.password else ''}"
76
+ f"{address}/__proxy-scraper/metrics"))
77
+ mode = pool.strategy + (f" · sticky {pool.sticky_seconds:g} s" if pool.sticky_seconds else "")
78
+ if server.revived:
79
+ mode += f" · {fmt(server.revived)} brought back"
80
+ if server.last_refill:
81
+ ago = fmt_duration(time.time() - server.last_refill)
82
+ mode += f" · {fmt(server.refilled)} added by refills (last {ago} ago)"
83
+ usage.add_row("Rotation", Text(mode, style=MUTED))
84
+
85
+ cards = row(
86
+ card("Requests", fmt(st.requests), f"{st.requests / uptime * 60:.1f} per minute"),
87
+ card("Successful", pct(st.ok, st.requests) if st.requests else "–",
88
+ f"{fmt(st.failed)} failed", f"bold {GOOD}" if not st.failed else f"bold {WARN}"),
89
+ card("Active", fmt(st.active), "open connections", f"bold {ACCENT}"),
90
+ card("Pool", f"{fmt(len(usable))} / {fmt(len(pool.entries))}",
91
+ f"{fmt(len(pool.tls_capable))} for HTTPS · {fmt(len(pool.entries) - len(usable))} out"),
92
+ card("Traffic", _mb(st.bytes_down), f"↑ {_mb(st.bytes_up)}"),
93
+ )
94
+
95
+ recent = table()
96
+ recent.add_column("", width=1)
97
+ recent.add_column("Target", ratio=2, no_wrap=True, overflow="ellipsis")
98
+ recent.add_column("via proxy", ratio=2, no_wrap=True, overflow="ellipsis")
99
+ if width >= 100:
100
+ recent.add_column("Client", style=MUTED, no_wrap=True)
101
+ recent.add_column("Attempts", justify="right", width=8)
102
+ recent.add_column("Time", justify="right", width=8)
103
+ for log in reversed(st.recent):
104
+ ptype = log.via.split("://", 1)[0]
105
+ cells = [
106
+ Text("✔", style=GOOD) if log.ok else Text("✘", style=BAD),
107
+ log.target,
108
+ Text(shown_via(log.via), style=TYPE_STYLE.get(ptype, MUTED)),
109
+ ]
110
+ if width >= 100:
111
+ cells.append(log.client)
112
+ cells += [str(log.attempts), f"{fmt(log.ms)} ms"]
113
+ recent.add_row(*cells)
114
+
115
+ busiest = table()
116
+ busiest.add_column("Proxy", no_wrap=True, overflow="ellipsis")
117
+ busiest.add_column("OK", justify="right", style=GOOD)
118
+ busiest.add_column("Errors", justify="right", style=MUTED)
119
+ busiest.add_column("Latency", justify="right")
120
+ for entry in sorted(pool.entries, key=lambda e: (-e.ok, e.result.latency))[:5]:
121
+ r = entry.result
122
+ style = MUTED if entry.disabled else TYPE_STYLE.get(r.ptype, "")
123
+ name = Text(f"{r.ptype}://{shown_proxy(r.proxy)}", style=style)
124
+ busiest.add_row(name, fmt(entry.ok), fmt(entry.fail), f"{fmt(r.latency)} ms")
125
+
126
+ waiting = Align.center(Text("waiting for the first connection …", style=MUTED))
127
+ return Group(
128
+ Panel(Group(title, Text(""), usage), box=box.HEAVY, border_style=ACCENT, padding=(0, 1)),
129
+ cards,
130
+ row(
131
+ widgets.panel(recent if st.recent else waiting, "Recent connections", GOOD),
132
+ widgets.panel(busiest, "Most used proxies"),
133
+ ratios=(3, 2),
134
+ ),
135
+ Text(" Ctrl+C stops the server", style=MUTED),
136
+ )
137
+
138
+
139
+ def shown_via(via: str) -> str:
140
+ """'socks5://user:pass@1.2.3.4:1080' -> password masked; '–' (no proxy) stays."""
141
+ scheme, sep, proxy = via.partition("://")
142
+ return f"{scheme}://{shown_proxy(proxy)}" if sep else via
@@ -0,0 +1,250 @@
1
+ """Colors, formatting and reusable building blocks for the terminal UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from datetime import datetime
7
+ from typing import Optional, Sequence
8
+ from urllib.parse import unquote
9
+
10
+ from rich import box
11
+ from rich.align import Align
12
+ from rich.console import Console, Group
13
+ from rich.panel import Panel
14
+ from rich.progress import ProgressColumn
15
+ from rich.table import Table
16
+ from rich.text import Text
17
+
18
+ from .. import __version__
19
+ from ..geo import flag
20
+
21
+ console = Console(highlight=False)
22
+
23
+ # One palette for everything (the lime is the brand color of the logo and the website). rich
24
+ # downsamples it automatically on terminals with fewer colors.
25
+ ACCENT = "#D4F77A"
26
+ GOOD = "#34D399"
27
+ WARN = "#FBBF24"
28
+ BAD = "#F87171"
29
+ MUTED = "#8B949E"
30
+ BORDER = "#30363D"
31
+ TYPE_STYLE = {"http": "#60A5FA", "socks4": "#C084FC", "socks5": "#34D399"}
32
+ ANON_STYLE = {"elite": ("E", GOOD), "anonymous": ("A", WARN), "transparent": ("T", BAD)}
33
+ ANON_LABEL = {"elite": "Elite", "anonymous": "Anonymous", "transparent": "Transp."}
34
+ PHASES = ("Sources", "Collect", "Check", "Done")
35
+ SPARK = "▁▂▃▄▅▆▇█"
36
+ LATENCY_EDGES = (300, 700, 1500, 3000, 6000)
37
+ LATENCY_LABELS = ("< 0.3 s", "< 0.7 s", "< 1.5 s", "< 3 s", "< 6 s", "≥ 6 s")
38
+ # below a 0.2 % hit rate the network is probably blocking proxy connections (firewall)
39
+ BLOCKED_HIT_RATE = 0.002
40
+
41
+
42
+ # --------------------------------------------------------------------------- #
43
+ # Formatting
44
+ # --------------------------------------------------------------------------- #
45
+
46
+ def fmt(n: float) -> str:
47
+ """Thousands separators: 12345 -> 12,345"""
48
+ return f"{n:,.0f}"
49
+
50
+
51
+ def pct(part: float, whole: float) -> str:
52
+ return f"{part / whole * 100:.1f}%" if whole else "–"
53
+
54
+
55
+ def fmt_duration(seconds: float) -> str:
56
+ seconds = int(seconds)
57
+ if seconds < 60:
58
+ return f"{seconds} s"
59
+ m, s = divmod(seconds, 60)
60
+ if m < 60:
61
+ return f"{m} min {s:02d} s"
62
+ h, m = divmod(m, 60)
63
+ return f"{h} h {m:02d} min"
64
+
65
+
66
+ def latency_style(ms: int) -> str:
67
+ return GOOD if ms < 1000 else WARN if ms < 3000 else BAD
68
+
69
+
70
+ def bar(value: float, maximum: float, width: int, style: str) -> Text:
71
+ """Bar with eighth steps for fine resolution in little space."""
72
+ width = max(width, 1)
73
+ filled = 0 if maximum <= 0 else min(value / maximum, 1.0) * width
74
+ full = int(filled)
75
+ rest = int((filled - full) * 8)
76
+ text = Text("█" * full, style=style)
77
+ if rest and full < width:
78
+ text.append(" ▏▎▍▌▋▊▉"[rest], style=style)
79
+ full += 1
80
+ text.append("·" * (width - full), style=MUTED)
81
+ return text
82
+
83
+
84
+ def sparkline(values: Sequence[float], width: int) -> Text:
85
+ values = list(values)[-width:]
86
+ if not values:
87
+ return Text("")
88
+ hi = max(values) or 1
89
+ return Text("".join(SPARK[min(int(v / hi * (len(SPARK) - 1) + 0.5), len(SPARK) - 1)] for v in values), style=ACCENT)
90
+
91
+
92
+ def type_badge(ptype: str) -> Text:
93
+ return Text(ptype, style=f"bold {TYPE_STYLE.get(ptype, 'white')}")
94
+
95
+
96
+ def country_cell(cc: str) -> Text:
97
+ return Text(f"{flag(cc)} {cc}" if cc else " ··", style="" if cc else MUTED)
98
+
99
+
100
+ def https_cell(value: Optional[bool]) -> Text:
101
+ if value is None:
102
+ return Text("·", style=MUTED)
103
+ return Text("✔", style=GOOD) if value else Text("✘", style=BAD)
104
+
105
+
106
+ def anon_cell(level: str) -> Text:
107
+ letter, style = ANON_STYLE.get(level, ("·", MUTED))
108
+ return Text(letter, style=f"bold {style}" if level else style)
109
+
110
+
111
+ def short_url(url: str) -> str:
112
+ gh = "https://raw.githubusercontent.com/"
113
+ if url.startswith(gh):
114
+ return "gh:" + url[len(gh):]
115
+ return url.split("://", 1)[-1]
116
+
117
+
118
+ class CountColumn(ProgressColumn):
119
+ """12,345 / 1,000,000 instead of 12345/1000000."""
120
+
121
+ def render(self, task) -> Text:
122
+ return Text.assemble((fmt(task.completed), "bold"), (f" / {fmt(task.total or 0)}", MUTED))
123
+
124
+
125
+ # --------------------------------------------------------------------------- #
126
+ # Building blocks
127
+ # --------------------------------------------------------------------------- #
128
+
129
+ def banner() -> Panel:
130
+ grid = Table.grid(expand=True)
131
+ grid.add_column()
132
+ grid.add_column(justify="right")
133
+ grid.add_row(
134
+ Text.assemble(("◆ proxy-scraper", f"bold {ACCENT}"), (f" v{__version__}", MUTED)),
135
+ Text(datetime.now().strftime("%Y-%m-%d %H:%M"), style=MUTED),
136
+ )
137
+ grid.add_row(Text("Free proxies that actually work – collected, checked, learned.", style=MUTED), "")
138
+ return Panel(grid, box=box.ROUNDED, border_style=ACCENT, padding=(0, 2))
139
+
140
+
141
+ def phase_bar(current: int) -> Text:
142
+ text = Text()
143
+ for i, name in enumerate(PHASES):
144
+ if i:
145
+ text.append(" ─ ", style=GOOD if i <= current else BORDER)
146
+ if i < current:
147
+ text.append(f"✔ {name}", style=GOOD)
148
+ elif i == current:
149
+ text.append(f"● {i + 1} {name}", style=f"bold {ACCENT}")
150
+ else:
151
+ text.append(f"○ {i + 1} {name}", style=MUTED)
152
+ return text
153
+
154
+
155
+ def header(current: int, started: float) -> Table:
156
+ grid = Table.grid(expand=True, padding=(0, 1))
157
+ grid.add_column()
158
+ grid.add_column(justify="right")
159
+ grid.add_row(phase_bar(current), Text(f"⏱ {fmt_duration(time.perf_counter() - started)}", style=MUTED))
160
+ return grid
161
+
162
+
163
+ _section_open = False
164
+
165
+
166
+ def section(title: str) -> None:
167
+ """Starts a section; the following info()/note() lines hang off its left border line."""
168
+ global _section_open
169
+ if _section_open:
170
+ section_end()
171
+ line = Text()
172
+ line.append(" ╭─ ", style=BORDER)
173
+ line.append(title, style=f"bold {ACCENT}")
174
+ line.append(" ", style=BORDER)
175
+ line.append("─" * max(console.size.width - line.cell_len - 2, 4), style=BORDER)
176
+ console.print(line)
177
+ _section_open = True
178
+
179
+
180
+ def section_end() -> None:
181
+ global _section_open
182
+ if _section_open:
183
+ console.print(Text(" ╰─", style=BORDER))
184
+ _section_open = False
185
+
186
+
187
+ def _gutter() -> Text:
188
+ # color only for the border character – not as the base style, which would bleed into the whole line
189
+ gutter = Text()
190
+ gutter.append(" │ " if _section_open else " ", style=BORDER)
191
+ return gutter
192
+
193
+
194
+ def info(label: str, value, style: str = "") -> None:
195
+ """Uniform info line – with a border line inside a section, indented otherwise."""
196
+ line = _gutter()
197
+ line.append(f"{label:<14}", style=MUTED)
198
+ line.append_text(value if isinstance(value, Text) else Text(str(value), style=style or "bold"))
199
+ console.print(line)
200
+
201
+
202
+ def note(message: str, style: str = WARN, icon: str = "⚠") -> None:
203
+ console.print(_gutter() + Text(f"{icon} ", style=style) + Text.from_markup(message))
204
+
205
+
206
+ def card(label: str, value: str, sub, style: str = "bold") -> Panel:
207
+ # all cards in a row should stay the same height – long subtitles are shortened instead of wrapped
208
+ sub = sub if isinstance(sub, Text) else Text(sub, style=MUTED)
209
+ sub.no_wrap, sub.overflow = True, "ellipsis"
210
+ body = Group(Text(label.upper(), style=f"bold {MUTED}"), Text(value, style=style), sub)
211
+ return Panel(body, box=box.ROUNDED, border_style=BORDER, padding=(0, 1))
212
+
213
+
214
+ def table(**kwargs) -> Table:
215
+ """Uniform, airy table without border lines (the frame comes from the panel)."""
216
+ kwargs.setdefault("expand", True)
217
+ return Table(box=None, header_style=f"bold {MUTED}", pad_edge=False, **kwargs)
218
+
219
+
220
+ def panel_title(title: str, style: str = BORDER) -> Text:
221
+ """Keep titles readable: with a subtle frame, muted-light instead of the dark frame color."""
222
+ return Text(f" {title} ", style=f"bold {MUTED if style == BORDER else style}")
223
+
224
+
225
+ def panel(renderable, title: str, style: str = BORDER, **kwargs) -> Panel:
226
+ return Panel(renderable, title=panel_title(title, style), title_align="left", box=box.ROUNDED,
227
+ border_style=style, **kwargs)
228
+
229
+
230
+ def row(*renderables, ratios: Optional[Sequence[int]] = None) -> Table:
231
+ grid = Table.grid(expand=True)
232
+ for i, _ in enumerate(renderables):
233
+ grid.add_column(ratio=(ratios[i] if ratios else 1))
234
+ grid.add_row(*renderables)
235
+ return grid
236
+
237
+
238
+ def centered(text: str, style: str = MUTED) -> Align:
239
+ return Align.center(Text(text, style=style))
240
+
241
+
242
+ def shown_proxy(proxy: str) -> str:
243
+ """Proxy for the terminal: passwords are masked ('alice:•••@1.2.3.4:1080'), the files keep them."""
244
+ auth, sep, address = proxy.rpartition("@")
245
+ if not sep:
246
+ return proxy
247
+ user = unquote(auth.partition(":")[0])
248
+ # user names come from third-party lists – never let control characters (newline, ESC) into the terminal
249
+ user = "".join(c if c.isprintable() else "?" for c in user)
250
+ return f"{user}:•••@{address}"