proxy-scraper-cli 1.13.0__tar.gz → 1.14.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.
Files changed (117) hide show
  1. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/PKG-INFO +9 -1
  2. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/README.md +8 -0
  3. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxy_scraper_cli.egg-info/PKG-INFO +9 -1
  4. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxy_scraper_cli.egg-info/SOURCES.txt +1 -0
  5. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/__init__.py +1 -1
  6. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/cli.py +52 -0
  7. proxy_scraper_cli-1.14.0/tests/test_pick.py +44 -0
  8. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/LICENSE +0 -0
  9. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
  10. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
  11. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
  12. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
  13. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/__main__.py +0 -0
  14. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/action.py +0 -0
  15. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/agent.py +0 -0
  16. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/api.py +0 -0
  17. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/app.py +0 -0
  18. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/asndb.py +0 -0
  19. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/blocklist.py +0 -0
  20. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/charts.py +0 -0
  21. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/checker.py +0 -0
  22. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/compat.py +0 -0
  23. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/completion.py +0 -0
  24. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/exporters.py +0 -0
  25. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/fetchcache.py +0 -0
  26. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/geo.py +0 -0
  27. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/geodb.py +0 -0
  28. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/handshake.py +0 -0
  29. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/history.py +0 -0
  30. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/judges.py +0 -0
  31. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/mcp_entry.py +0 -0
  32. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/mcp_server.py +0 -0
  33. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/netio.py +0 -0
  34. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/options.py +0 -0
  35. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/output.py +0 -0
  36. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/pages.py +0 -0
  37. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/parsing.py +0 -0
  38. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/paths.py +0 -0
  39. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/pipeline.py +0 -0
  40. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/preferences.py +0 -0
  41. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/proxypages.py +0 -0
  42. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/publish.py +0 -0
  43. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/report.py +0 -0
  44. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/runsteps.py +0 -0
  45. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/server/__init__.py +0 -0
  46. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/server/core.py +0 -0
  47. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/server/http.py +0 -0
  48. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/server/pool.py +0 -0
  49. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/server/socks.py +0 -0
  50. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/server/status.py +0 -0
  51. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/server/upstream.py +0 -0
  52. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/site/apple-touch-icon.png +0 -0
  53. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
  54. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/site/index.html +0 -0
  55. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/site/logo.png +0 -0
  56. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/site/og.png +0 -0
  57. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/sites.py +0 -0
  58. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/sources.json +0 -0
  59. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/sources.py +0 -0
  60. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/speed.py +0 -0
  61. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/targets.py +0 -0
  62. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/ui/__init__.py +0 -0
  63. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/ui/dashboard.py +0 -0
  64. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/ui/keys.py +0 -0
  65. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/ui/report.py +0 -0
  66. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/ui/serve.py +0 -0
  67. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/ui/widgets.py +0 -0
  68. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/proxyscraper/ui/wizard.py +0 -0
  69. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/pyproject.toml +0 -0
  70. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/setup.cfg +0 -0
  71. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_action.py +0 -0
  72. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_agent.py +0 -0
  73. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_api.py +0 -0
  74. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_app.py +0 -0
  75. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_asndb.py +0 -0
  76. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_auth.py +0 -0
  77. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_blocklist.py +0 -0
  78. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_charts.py +0 -0
  79. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_checker.py +0 -0
  80. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_compat.py +0 -0
  81. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_completion.py +0 -0
  82. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_discovery_growth.py +0 -0
  83. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_exporters.py +0 -0
  84. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_fetchcache.py +0 -0
  85. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_geodb.py +0 -0
  86. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_judges.py +0 -0
  87. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_learning.py +0 -0
  88. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_live_api.py +0 -0
  89. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_mcp_server.py +0 -0
  90. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_mine_sources.py +0 -0
  91. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_options.py +0 -0
  92. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_output_stdout.py +0 -0
  93. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_packaging.py +0 -0
  94. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_pages.py +0 -0
  95. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_parsing.py +0 -0
  96. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_parsing_fuzz.py +0 -0
  97. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_paths.py +0 -0
  98. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_pipeline.py +0 -0
  99. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_proxy_pages.py +0 -0
  100. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_proxy_sources.py +0 -0
  101. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_publish.py +0 -0
  102. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_report.py +0 -0
  103. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_robustness.py +0 -0
  104. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_serve_password.py +0 -0
  105. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_serve_refill.py +0 -0
  106. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_serve_v2.py +0 -0
  107. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_server.py +0 -0
  108. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_server_closed_writer.py +0 -0
  109. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_server_fixes.py +0 -0
  110. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_sites.py +0 -0
  111. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_sources_json.py +0 -0
  112. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_speed.py +0 -0
  113. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_tamper.py +0 -0
  114. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_targets.py +0 -0
  115. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_ui.py +0 -0
  116. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_uptime.py +0 -0
  117. {proxy_scraper_cli-1.13.0 → proxy_scraper_cli-1.14.0}/tests/test_wizard.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxy-scraper-cli
3
- Version: 1.13.0
3
+ Version: 1.14.0
4
4
  Summary: Finds free HTTP, SOCKS4 and SOCKS5 proxies that actually work: 700+ sources, real checks, honeypot filtering and a rotating local proxy server.
5
5
  Author: Maximilian Feix
6
6
  License-Expression: MIT
@@ -699,6 +699,13 @@ pip download requests # git, pip, npm & co. now go through the pool
699
699
 
700
700
  `proxy` is the fastest one, `file` a text file with all of them (up to `limit`, default 20), `count` how many. Without a match the step fails, unless `fail-if-empty: false`.
701
701
 
702
+ **In the shell, without a scan** – `--pick` takes proxies from the hourly list in about half a second
703
+
704
+ ```bash
705
+ curl -x "$(proxy-scraper --pick --https-only --min-uptime 90)" https://api.ipify.org
706
+ proxy-scraper --pick 10 --country DE --works-on google > de.txt
707
+ ```
708
+
702
709
  **Without installing anything** – straight from the live list
703
710
 
704
711
  ```bash
@@ -743,6 +750,7 @@ curl.exe -x (Get-Content "$run\all.txt" -TotalCount 1) http://api.ipify.org
743
750
  | `--discover` | search GitHub for new sources right now |
744
751
  | `--no-cache` | download every list again (unchanged ones are normally skipped via ETag) |
745
752
  | `--list-sources [N]` | show the source ranking (add `--json` for scripts: url, status, hit rate, checks, last change) |
753
+ | `--pick [N]` | print N proxies (default 1) from the hourly checked list and exit – no scan, same filters, plus `--min-uptime PERCENT` and `--works-on google,reddit,…` |
746
754
  | `--serve-host ADDR` | where the proxy server listens (default `127.0.0.1`; `0.0.0.0` for Docker, with a warning) |
747
755
  | `--serve-password SECRET` | clients must send this password in the proxy login; better set `PROXY_SCRAPER_SERVE_PASSWORD` |
748
756
  | `--rotate STRATEGY` · `--sticky SEC` | how the proxy server picks proxies, see [above](#proxy-server) |
@@ -658,6 +658,13 @@ pip download requests # git, pip, npm & co. now go through the pool
658
658
 
659
659
  `proxy` is the fastest one, `file` a text file with all of them (up to `limit`, default 20), `count` how many. Without a match the step fails, unless `fail-if-empty: false`.
660
660
 
661
+ **In the shell, without a scan** – `--pick` takes proxies from the hourly list in about half a second
662
+
663
+ ```bash
664
+ curl -x "$(proxy-scraper --pick --https-only --min-uptime 90)" https://api.ipify.org
665
+ proxy-scraper --pick 10 --country DE --works-on google > de.txt
666
+ ```
667
+
661
668
  **Without installing anything** – straight from the live list
662
669
 
663
670
  ```bash
@@ -702,6 +709,7 @@ curl.exe -x (Get-Content "$run\all.txt" -TotalCount 1) http://api.ipify.org
702
709
  | `--discover` | search GitHub for new sources right now |
703
710
  | `--no-cache` | download every list again (unchanged ones are normally skipped via ETag) |
704
711
  | `--list-sources [N]` | show the source ranking (add `--json` for scripts: url, status, hit rate, checks, last change) |
712
+ | `--pick [N]` | print N proxies (default 1) from the hourly checked list and exit – no scan, same filters, plus `--min-uptime PERCENT` and `--works-on google,reddit,…` |
705
713
  | `--serve-host ADDR` | where the proxy server listens (default `127.0.0.1`; `0.0.0.0` for Docker, with a warning) |
706
714
  | `--serve-password SECRET` | clients must send this password in the proxy login; better set `PROXY_SCRAPER_SERVE_PASSWORD` |
707
715
  | `--rotate STRATEGY` · `--sticky SEC` | how the proxy server picks proxies, see [above](#proxy-server) |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxy-scraper-cli
3
- Version: 1.13.0
3
+ Version: 1.14.0
4
4
  Summary: Finds free HTTP, SOCKS4 and SOCKS5 proxies that actually work: 700+ sources, real checks, honeypot filtering and a rotating local proxy server.
5
5
  Author: Maximilian Feix
6
6
  License-Expression: MIT
@@ -699,6 +699,13 @@ pip download requests # git, pip, npm & co. now go through the pool
699
699
 
700
700
  `proxy` is the fastest one, `file` a text file with all of them (up to `limit`, default 20), `count` how many. Without a match the step fails, unless `fail-if-empty: false`.
701
701
 
702
+ **In the shell, without a scan** – `--pick` takes proxies from the hourly list in about half a second
703
+
704
+ ```bash
705
+ curl -x "$(proxy-scraper --pick --https-only --min-uptime 90)" https://api.ipify.org
706
+ proxy-scraper --pick 10 --country DE --works-on google > de.txt
707
+ ```
708
+
702
709
  **Without installing anything** – straight from the live list
703
710
 
704
711
  ```bash
@@ -743,6 +750,7 @@ curl.exe -x (Get-Content "$run\all.txt" -TotalCount 1) http://api.ipify.org
743
750
  | `--discover` | search GitHub for new sources right now |
744
751
  | `--no-cache` | download every list again (unchanged ones are normally skipped via ETag) |
745
752
  | `--list-sources [N]` | show the source ranking (add `--json` for scripts: url, status, hit rate, checks, last change) |
753
+ | `--pick [N]` | print N proxies (default 1) from the hourly checked list and exit – no scan, same filters, plus `--min-uptime PERCENT` and `--works-on google,reddit,…` |
746
754
  | `--serve-host ADDR` | where the proxy server listens (default `127.0.0.1`; `0.0.0.0` for Docker, with a warning) |
747
755
  | `--serve-password SECRET` | clients must send this password in the proxy login; better set `PROXY_SCRAPER_SERVE_PASSWORD` |
748
756
  | `--rotate STRATEGY` · `--sticky SEC` | how the proxy server picks proxies, see [above](#proxy-server) |
@@ -92,6 +92,7 @@ tests/test_pages.py
92
92
  tests/test_parsing.py
93
93
  tests/test_parsing_fuzz.py
94
94
  tests/test_paths.py
95
+ tests/test_pick.py
95
96
  tests/test_pipeline.py
96
97
  tests/test_proxy_pages.py
97
98
  tests/test_proxy_sources.py
@@ -1,7 +1,7 @@
1
1
  """proxy-scraper: collects public HTTP/SOCKS4/SOCKS5 proxies from hundreds of sources, checks them
2
2
  in parallel with its own protocol handshakes and learns which sources deliver good proxies."""
3
3
 
4
- __version__ = "1.13.0"
4
+ __version__ = "1.14.0"
5
5
 
6
6
 
7
7
  def __getattr__(name):
@@ -34,6 +34,7 @@ from .output import has_latest_results
34
34
  from .parsing import PROXY_TYPES
35
35
  from .preferences import load_last_argv, save_last_argv
36
36
  from .server.pool import STRATEGIES
37
+ from .sites import SITES
37
38
  from .targets import parse_target
38
39
  from .ui import ACCENT, BAD, MUTED, banner, note, render_source_ranking, widgets
39
40
  from .ui.keys import is_interactive
@@ -62,6 +63,36 @@ def list_sources(limit: int, as_json: bool = False) -> int:
62
63
  return 0
63
64
 
64
65
 
66
+ def pick(args) -> int:
67
+ """--pick: proxies from the hourly list, nothing but their URLs on stdout (for $(…) in a shell)."""
68
+ from .api import live_proxies
69
+
70
+ try:
71
+ found = live_proxies(types=args.types, countries=args.country or "", https=args.https_only,
72
+ anonymity=args.anonymity or "", max_latency=args.max_latency,
73
+ no_datacenter=args.no_datacenter, no_blocklisted=args.no_blocklisted,
74
+ min_uptime=args.min_uptime, works_on=args.works_on, limit=args.pick)
75
+ except (ConnectionError, ValueError) as e:
76
+ print(f"proxy-scraper: {e}", file=sys.stderr)
77
+ return 1
78
+ if not found:
79
+ print("proxy-scraper: no proxy in the live list matches these filters right now – loosen them, "
80
+ "or run a scan of your own", file=sys.stderr)
81
+ return 1
82
+ sys.stdout.write("".join(f"{p.url}\n" for p in found))
83
+ return 0
84
+
85
+
86
+ def site_list(text: str) -> List[str]:
87
+ """argparse type for --works-on: google,reddit -> ["google", "reddit"]"""
88
+ names = [n.strip().lower() for n in text.split(",") if n.strip()]
89
+ known = [s.name for s in SITES]
90
+ for n in names:
91
+ if n not in known:
92
+ raise argparse.ArgumentTypeError(f"unknown site {n!r}, use {', '.join(known)}")
93
+ return names
94
+
95
+
65
96
  def _number(kind, minimum, strict=False, maximum=None):
66
97
  """argparse type for numbers with limits – with an understandable error message."""
67
98
  def parse(text: str):
@@ -96,6 +127,7 @@ def export_list(text: str) -> List[str]:
96
127
  positive_int = _number(int, 1)
97
128
  port_number = _number(int, 1, maximum=65535)
98
129
  non_negative_int = _number(int, 0)
130
+ percent = _number(int, 0, maximum=100)
99
131
  positive_float = _number(float, 0, strict=True)
100
132
  non_negative_float = _number(float, 0)
101
133
 
@@ -166,6 +198,16 @@ def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
166
198
  f.add_argument("--target", action="append", type=target_url, metavar="URL",
167
199
  help="only proxies that reach this site (repeatable), e.g. --target google.com")
168
200
 
201
+ live = p.add_argument_group("Live list (no scan)")
202
+ live.add_argument("--pick", nargs="?", const=1, type=positive_int, metavar="N",
203
+ help="print N proxies (default 1) from the hourly checked list that match the filters above, "
204
+ "one URL per line, and exit – e.g. curl -x \"$(proxy-scraper --pick --https-only)\" …")
205
+ live.add_argument("--min-uptime", type=percent, default=0, metavar="PERCENT",
206
+ help="with --pick: only proxies on the list in at least this share of the week's checks")
207
+ live.add_argument("--works-on", type=site_list, default=[], metavar="SITES",
208
+ help=f"with --pick: only proxies that got through to these sites in the last check "
209
+ f"({', '.join(s.name for s in SITES)})")
210
+
169
211
  v = p.add_argument_group("Proxy server")
170
212
  v.add_argument("--serve", nargs="?", const=DEFAULT_SERVE_PORT, default=0, type=port_number, metavar="PORT",
171
213
  help=f"serve the hits as a rotating proxy on 127.0.0.1:PORT after the run "
@@ -209,6 +251,14 @@ def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
209
251
  args = p.parse_args(argv)
210
252
  if args.json and args.list_sources is None:
211
253
  p.error("--json only works together with --list-sources")
254
+ if (args.min_uptime or args.works_on) and args.pick is None:
255
+ p.error("--min-uptime and --works-on only work together with --pick")
256
+ if args.pick is not None: # --pick exits right away: options for a scan or the server would be silently ignored
257
+ scan_only = [flag for dest, flag in (("target", "--target"), ("serve", "--serve"), ("recheck", "--recheck"),
258
+ ("want", "--want"), ("output", "-o/--output"), ("export", "--export"))
259
+ if getattr(args, dest) != p.get_default(dest)]
260
+ if scan_only:
261
+ p.error(f"--pick takes proxies from the live list and exits – {', '.join(scan_only)} belong to a scan")
212
262
  return args
213
263
 
214
264
 
@@ -260,6 +310,8 @@ def run(argv: Optional[List[str]] = None) -> int:
260
310
  return mcp_main()
261
311
  if args.list_sources is not None:
262
312
  return list_sources(args.list_sources, args.json)
313
+ if args.pick is not None:
314
+ return pick(args)
263
315
  opts = RunOptions.from_args(args)
264
316
  if opts.output == STDOUT:
265
317
  widgets.console = Console(highlight=False, stderr=True) # stdout only carries the hits
@@ -0,0 +1,44 @@
1
+ """proxy-scraper --pick: proxies from the hourly list on stdout, with the usual filters, no scan."""
2
+
3
+ import pytest
4
+
5
+ from proxyscraper import api, cli
6
+ from tests.test_live_api import ROWS, STATS, fetch_from
7
+
8
+
9
+ @pytest.fixture(autouse=True)
10
+ def offline(monkeypatch):
11
+ rows = [dict(ROWS[0], sites={"google": True}), ROWS[1], ROWS[2]]
12
+ monkeypatch.setattr(api, "_live_fetch", fetch_from({"proxies.json": rows, "stats.json": STATS}))
13
+
14
+
15
+ def test_one_proxy_by_default_and_nothing_else_on_stdout(capsys):
16
+ assert cli.run(["--pick"]) == 0
17
+ assert capsys.readouterr().out == "http://2.2.2.2:80\n" # the fastest
18
+
19
+
20
+ def test_the_usual_filters_apply(capsys):
21
+ assert cli.run(["--pick", "5", "--types", "socks4", "socks5", "--country", "DE", "--https-only"]) == 0
22
+ assert capsys.readouterr().out.splitlines() == ["socks4://3.3.3.3:4145", "socks5://1.1.1.1:1080"]
23
+ assert cli.run(["--pick", "--min-uptime", "90", "--works-on", "google"]) == 0
24
+ assert capsys.readouterr().out == "socks5://1.1.1.1:1080\n"
25
+
26
+
27
+ def test_nothing_matching_is_exit_1_with_a_hint_on_stderr(capsys):
28
+ assert cli.run(["--pick", "--country", "JP"]) == 1
29
+ out = capsys.readouterr()
30
+ assert out.out == "" and "no proxy" in out.err.lower()
31
+
32
+
33
+ def test_pick_only_filters_need_pick(capsys):
34
+ with pytest.raises(SystemExit):
35
+ cli.run(["--min-uptime", "90"])
36
+ assert "--pick" in capsys.readouterr().err
37
+
38
+
39
+ @pytest.mark.parametrize("extra", [["--target", "discord.com"], ["--serve"], ["--recheck"], ["--want", "5"],
40
+ ["-o", "x.txt"], ["--export", "clash"]])
41
+ def test_options_for_a_scan_are_an_error_not_ignored(extra, capsys):
42
+ with pytest.raises(SystemExit):
43
+ cli.run(["--pick", *extra])
44
+ assert "--pick" in capsys.readouterr().err