proxy-scraper-cli 1.9.0__tar.gz → 1.10.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 (113) hide show
  1. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/PKG-INFO +32 -12
  2. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/README.md +30 -11
  3. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxy_scraper_cli.egg-info/PKG-INFO +32 -12
  4. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxy_scraper_cli.egg-info/SOURCES.txt +5 -0
  5. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxy_scraper_cli.egg-info/requires.txt +1 -0
  6. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/__init__.py +1 -1
  7. proxy_scraper_cli-1.10.0/proxyscraper/action.py +96 -0
  8. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/cli.py +20 -5
  9. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/completion.py +61 -3
  10. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/exporters.py +43 -0
  11. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/parsing.py +6 -1
  12. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/report.py +2 -0
  13. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/server/core.py +23 -11
  14. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/site/index.html +157 -15
  15. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/sites.py +45 -6
  16. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/pyproject.toml +1 -1
  17. proxy_scraper_cli-1.10.0/tests/test_action.py +69 -0
  18. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_completion.py +35 -0
  19. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_exporters.py +22 -0
  20. proxy_scraper_cli-1.10.0/tests/test_parsing_fuzz.py +83 -0
  21. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_report.py +6 -0
  22. proxy_scraper_cli-1.10.0/tests/test_server_closed_writer.py +72 -0
  23. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_sites.py +31 -1
  24. proxy_scraper_cli-1.10.0/tests/test_sources_json.py +36 -0
  25. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/LICENSE +0 -0
  26. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
  27. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
  28. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
  29. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/__main__.py +0 -0
  30. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/agent.py +0 -0
  31. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/api.py +0 -0
  32. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/app.py +0 -0
  33. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/asndb.py +0 -0
  34. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/blocklist.py +0 -0
  35. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/charts.py +0 -0
  36. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/checker.py +0 -0
  37. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/compat.py +0 -0
  38. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/fetchcache.py +0 -0
  39. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/geo.py +0 -0
  40. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/geodb.py +0 -0
  41. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/handshake.py +0 -0
  42. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/history.py +0 -0
  43. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/judges.py +0 -0
  44. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/mcp_entry.py +0 -0
  45. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/mcp_server.py +0 -0
  46. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/netio.py +0 -0
  47. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/options.py +0 -0
  48. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/output.py +0 -0
  49. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/pages.py +0 -0
  50. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/paths.py +0 -0
  51. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/pipeline.py +0 -0
  52. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/preferences.py +0 -0
  53. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/proxypages.py +0 -0
  54. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/publish.py +0 -0
  55. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/server/__init__.py +0 -0
  56. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/server/http.py +0 -0
  57. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/server/pool.py +0 -0
  58. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/server/socks.py +0 -0
  59. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/server/status.py +0 -0
  60. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/server/upstream.py +0 -0
  61. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/site/apple-touch-icon.png +0 -0
  62. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
  63. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/site/logo.png +0 -0
  64. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/site/og.png +0 -0
  65. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/sources.json +0 -0
  66. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/sources.py +0 -0
  67. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/targets.py +0 -0
  68. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/ui/__init__.py +0 -0
  69. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/ui/dashboard.py +0 -0
  70. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/ui/keys.py +0 -0
  71. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/ui/report.py +0 -0
  72. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/ui/serve.py +0 -0
  73. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/ui/widgets.py +0 -0
  74. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/proxyscraper/ui/wizard.py +0 -0
  75. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/setup.cfg +0 -0
  76. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_agent.py +0 -0
  77. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_api.py +0 -0
  78. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_app.py +0 -0
  79. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_asndb.py +0 -0
  80. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_auth.py +0 -0
  81. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_blocklist.py +0 -0
  82. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_charts.py +0 -0
  83. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_checker.py +0 -0
  84. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_compat.py +0 -0
  85. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_discovery_growth.py +0 -0
  86. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_fetchcache.py +0 -0
  87. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_geodb.py +0 -0
  88. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_judges.py +0 -0
  89. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_learning.py +0 -0
  90. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_live_api.py +0 -0
  91. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_mcp_server.py +0 -0
  92. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_mine_sources.py +0 -0
  93. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_options.py +0 -0
  94. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_output_stdout.py +0 -0
  95. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_packaging.py +0 -0
  96. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_pages.py +0 -0
  97. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_parsing.py +0 -0
  98. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_paths.py +0 -0
  99. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_pipeline.py +0 -0
  100. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_proxy_pages.py +0 -0
  101. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_proxy_sources.py +0 -0
  102. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_publish.py +0 -0
  103. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_robustness.py +0 -0
  104. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_serve_password.py +0 -0
  105. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_serve_refill.py +0 -0
  106. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_serve_v2.py +0 -0
  107. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_server.py +0 -0
  108. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_server_fixes.py +0 -0
  109. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_tamper.py +0 -0
  110. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_targets.py +0 -0
  111. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_ui.py +0 -0
  112. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.0}/tests/test_uptime.py +0 -0
  113. {proxy_scraper_cli-1.9.0 → proxy_scraper_cli-1.10.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.9.0
3
+ Version: 1.10.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
@@ -35,6 +35,7 @@ Provides-Extra: mcp
35
35
  Requires-Dist: mcp<3,>=2.2; python_version >= "3.10" and extra == "mcp"
36
36
  Provides-Extra: dev
37
37
  Requires-Dist: pytest; extra == "dev"
38
+ Requires-Dist: hypothesis; extra == "dev"
38
39
  Requires-Dist: ruff==0.16.8; extra == "dev"
39
40
  Dynamic: license-file
40
41
 
@@ -138,11 +139,12 @@ python3 proxy_scraper.py
138
139
 
139
140
  Every [release](https://github.com/maximilianfeix/proxy-scraper/releases/latest) also ships a wheel you can install with `pip install <file>.whl`, and a multi-arch image (amd64/arm64) on `ghcr.io`. In the container the wizard never shows up, it runs straight away. For the proxy server use `--serve --serve-host 0.0.0.0` with `-p 127.0.0.1:8899:8899`, so the port is only open on your own machine.
140
141
 
141
- Tab completion for bash, zsh and fish:
142
+ Tab completion for bash, zsh, fish and PowerShell:
142
143
 
143
144
  ```bash
144
145
  eval "$(proxy-scraper --completion zsh)" # in ~/.zshrc (after compinit), same for bash in ~/.bashrc
145
146
  proxy-scraper --completion fish > ~/.config/fish/completions/proxy-scraper.fish
147
+ proxy-scraper --completion powershell | Out-String | Invoke-Expression # in $PROFILE
146
148
  ```
147
149
 
148
150
  Installed, the learned state lives in your user data folder (`~/Library/Application Support/proxy-scraper`, `%LOCALAPPDATA%\proxy-scraper` or `~/.local/share/proxy-scraper`; override with `PROXY_SCRAPER_HOME`) and results go to `./results`. Run from a clone, both stay inside the project.
@@ -656,14 +658,14 @@ if __name__ == "__main__":
656
658
 
657
659
  In a Scrapy project, put `RotatingProxy` in `middlewares.py` and add it to `DOWNLOADER_MIDDLEWARES` in `settings.py`. For long crawls, reload the pool now and then – the list changes every hour.
658
660
 
659
- **proxychains, Clash / Mihomo** – ready-made configs with `--export`
661
+ **proxychains, Clash / Mihomo, sing-box** – ready-made configs with `--export`
660
662
 
661
663
  ```bash
662
- proxy-scraper --want 30 -y --export proxychains,clash
664
+ proxy-scraper --want 30 -y --export proxychains,clash,singbox
663
665
  proxychains4 -f results/latest/proxychains.conf curl https://api.ipify.org
664
666
  ```
665
667
 
666
- `clash.yaml` has all HTTP and SOCKS5 proxies plus a `url-test` group that always picks the fastest. Both configs leave out HTTP proxies that can't tunnel (`CONNECT`), because these tools tunnel everything.
668
+ `clash.yaml` has all HTTP and SOCKS5 proxies plus a `url-test` group that always picks the fastest. `singbox.json` does the same for sing-box, SOCKS4 included, and opens a local proxy: `sing-box run -c results/latest/singbox.json`, then use `127.0.0.1:2080` as HTTP or SOCKS5 proxy. All three leave out HTTP proxies that can't tunnel (`CONNECT`), because these tools tunnel everything.
667
669
 
668
670
  **In a pipe** – `-o -` prints the hits to stdout, the interface moves to stderr
669
671
 
@@ -679,6 +681,22 @@ export HTTPS_PROXY=http://127.0.0.1:8899 HTTP_PROXY=http://127.0.0.1:8899
679
681
  pip download requests # git, pip, npm & co. now go through the pool
680
682
  ```
681
683
 
684
+ **In a GitHub workflow** – the action picks working proxies for the next steps
685
+
686
+ ```yaml
687
+ - id: proxies
688
+ uses: maximilianfeix/proxy-scraper@v1
689
+ with:
690
+ types: socks5
691
+ https: true
692
+ min-uptime: 90 # on the list for 90 %+ of the week
693
+ works-on: google # optional: google, reddit, amazon
694
+ recheck: true # optional: check them again from the runner
695
+ - run: curl -x "${{ steps.proxies.outputs.proxy }}" https://api.ipify.org
696
+ ```
697
+
698
+ `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`.
699
+
682
700
  **Without installing anything** – straight from the live list
683
701
 
684
702
  ```bash
@@ -722,16 +740,16 @@ curl.exe -x (Get-Content "$run\all.txt" -TotalCount 1) http://api.ipify.org
722
740
  | `-t`, `--timeout S` | timeout per proxy (default: 8 s) |
723
741
  | `--discover` | search GitHub for new sources right now |
724
742
  | `--no-cache` | download every list again (unchanged ones are normally skipped via ETag) |
725
- | `--list-sources [N]` | show the source ranking |
743
+ | `--list-sources [N]` | show the source ranking (add `--json` for scripts: url, status, hit rate, checks, last change) |
726
744
  | `--serve-host ADDR` | where the proxy server listens (default `127.0.0.1`; `0.0.0.0` for Docker, with a warning) |
727
745
  | `--serve-password SECRET` | clients must send this password in the proxy login; better set `PROXY_SCRAPER_SERVE_PASSWORD` |
728
746
  | `--rotate STRATEGY` · `--sticky SEC` | how the proxy server picks proxies, see [above](#proxy-server) |
729
747
  | `--serve-refill HOURS` | while serving, check fresh proxies every HOURS and add the hits to the pool |
730
748
  | `--serve [PORT]` | afterwards serve as a rotating proxy on `127.0.0.1:PORT` (default: 8899) |
731
749
  | `-o FILE` | also write all hits to this file; `-o -` prints them to stdout |
732
- | `--export FORMATS` | extra files for other tools: `proxychains`, `clash`, `curl` or `all` |
750
+ | `--export FORMATS` | extra files for other tools: `proxychains`, `clash`, `singbox`, `curl` or `all` |
733
751
  | `-V`, `--version` | print the version |
734
- | `--completion SHELL` | print the tab completion script for bash, zsh or fish |
752
+ | `--completion SHELL` | print the tab completion script for bash, zsh, fish or PowerShell |
735
753
 
736
754
  Everything else: `proxy-scraper --help`
737
755
 
@@ -813,10 +831,12 @@ Only for things that don't matter. Public proxies are run by strangers who can r
813
831
 
814
832
  ## Roadmap
815
833
 
816
- What's next is tracked in the milestone [**v1.8**](../../milestone/8) – ideas and wishes are welcome as an [issue](../../issues/new/choose). During Hacktoberfest there are [beginner-friendly issues](../../issues?q=is%3Aopen+label%3Ahacktoberfest) with pointers on where to start.
834
+ What's next is in the [open issues](../../issues) – ideas and wishes are welcome as an [issue](../../issues/new/choose). During Hacktoberfest there are [beginner-friendly issues](../../issues?q=is%3Aopen+label%3Ahacktoberfest) with pointers on where to start.
835
+
836
+ - [ ] [Measure throughput, not just latency](../../issues/140)
837
+ - [ ] [Daily snapshots as a dataset](../../issues/143)
817
838
 
818
- - [ ] [Protocol detection on the same connection](../../issues/44)
819
- - [ ] [sing-box export](../../issues/113), [PowerShell completion](../../issues/114), [shareable filters on the website](../../issues/116)
839
+ Shipped in v1.8 and v1.9: uptime per proxy and `stable.txt`, which proxies get through to Google, Reddit and Amazon, `live_proxies()` in Python, a page per proxy with its week of checks, a weekly report, live charts in this README, shareable filters on the website, sources mined from other scrapers' lists, untyped lists tried as HTTP and SOCKS5, `--export singbox`, PowerShell completion and recipes for httpx, aiohttp and Scrapy.
820
840
 
821
841
  Shipped in [v1.7](../../milestone/7): an MCP server so AI agents get working proxies and can load pages through them, 28 new sources and a GitHub search that runs daily and keeps what it found, and a cleaner website.
822
842
 
@@ -861,7 +881,7 @@ proxyscraper/
861
881
  ├── geodb.py DB-IP country database (monthly, binary search)
862
882
  ├── targets.py target sites for --target
863
883
  ├── output.py result files
864
- ├── exporters.py proxychains, Clash and curl formats (--export)
884
+ ├── exporters.py proxychains, Clash, sing-box and curl formats (--export)
865
885
  ├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · core
866
886
  ├── api.py find_proxies() / check_proxies() for Python
867
887
  ├── agent.py MCP tools without the SDK: live list, filters, fetch through proxies
@@ -98,11 +98,12 @@ python3 proxy_scraper.py
98
98
 
99
99
  Every [release](https://github.com/maximilianfeix/proxy-scraper/releases/latest) also ships a wheel you can install with `pip install <file>.whl`, and a multi-arch image (amd64/arm64) on `ghcr.io`. In the container the wizard never shows up, it runs straight away. For the proxy server use `--serve --serve-host 0.0.0.0` with `-p 127.0.0.1:8899:8899`, so the port is only open on your own machine.
100
100
 
101
- Tab completion for bash, zsh and fish:
101
+ Tab completion for bash, zsh, fish and PowerShell:
102
102
 
103
103
  ```bash
104
104
  eval "$(proxy-scraper --completion zsh)" # in ~/.zshrc (after compinit), same for bash in ~/.bashrc
105
105
  proxy-scraper --completion fish > ~/.config/fish/completions/proxy-scraper.fish
106
+ proxy-scraper --completion powershell | Out-String | Invoke-Expression # in $PROFILE
106
107
  ```
107
108
 
108
109
  Installed, the learned state lives in your user data folder (`~/Library/Application Support/proxy-scraper`, `%LOCALAPPDATA%\proxy-scraper` or `~/.local/share/proxy-scraper`; override with `PROXY_SCRAPER_HOME`) and results go to `./results`. Run from a clone, both stay inside the project.
@@ -616,14 +617,14 @@ if __name__ == "__main__":
616
617
 
617
618
  In a Scrapy project, put `RotatingProxy` in `middlewares.py` and add it to `DOWNLOADER_MIDDLEWARES` in `settings.py`. For long crawls, reload the pool now and then – the list changes every hour.
618
619
 
619
- **proxychains, Clash / Mihomo** – ready-made configs with `--export`
620
+ **proxychains, Clash / Mihomo, sing-box** – ready-made configs with `--export`
620
621
 
621
622
  ```bash
622
- proxy-scraper --want 30 -y --export proxychains,clash
623
+ proxy-scraper --want 30 -y --export proxychains,clash,singbox
623
624
  proxychains4 -f results/latest/proxychains.conf curl https://api.ipify.org
624
625
  ```
625
626
 
626
- `clash.yaml` has all HTTP and SOCKS5 proxies plus a `url-test` group that always picks the fastest. Both configs leave out HTTP proxies that can't tunnel (`CONNECT`), because these tools tunnel everything.
627
+ `clash.yaml` has all HTTP and SOCKS5 proxies plus a `url-test` group that always picks the fastest. `singbox.json` does the same for sing-box, SOCKS4 included, and opens a local proxy: `sing-box run -c results/latest/singbox.json`, then use `127.0.0.1:2080` as HTTP or SOCKS5 proxy. All three leave out HTTP proxies that can't tunnel (`CONNECT`), because these tools tunnel everything.
627
628
 
628
629
  **In a pipe** – `-o -` prints the hits to stdout, the interface moves to stderr
629
630
 
@@ -639,6 +640,22 @@ export HTTPS_PROXY=http://127.0.0.1:8899 HTTP_PROXY=http://127.0.0.1:8899
639
640
  pip download requests # git, pip, npm & co. now go through the pool
640
641
  ```
641
642
 
643
+ **In a GitHub workflow** – the action picks working proxies for the next steps
644
+
645
+ ```yaml
646
+ - id: proxies
647
+ uses: maximilianfeix/proxy-scraper@v1
648
+ with:
649
+ types: socks5
650
+ https: true
651
+ min-uptime: 90 # on the list for 90 %+ of the week
652
+ works-on: google # optional: google, reddit, amazon
653
+ recheck: true # optional: check them again from the runner
654
+ - run: curl -x "${{ steps.proxies.outputs.proxy }}" https://api.ipify.org
655
+ ```
656
+
657
+ `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`.
658
+
642
659
  **Without installing anything** – straight from the live list
643
660
 
644
661
  ```bash
@@ -682,16 +699,16 @@ curl.exe -x (Get-Content "$run\all.txt" -TotalCount 1) http://api.ipify.org
682
699
  | `-t`, `--timeout S` | timeout per proxy (default: 8 s) |
683
700
  | `--discover` | search GitHub for new sources right now |
684
701
  | `--no-cache` | download every list again (unchanged ones are normally skipped via ETag) |
685
- | `--list-sources [N]` | show the source ranking |
702
+ | `--list-sources [N]` | show the source ranking (add `--json` for scripts: url, status, hit rate, checks, last change) |
686
703
  | `--serve-host ADDR` | where the proxy server listens (default `127.0.0.1`; `0.0.0.0` for Docker, with a warning) |
687
704
  | `--serve-password SECRET` | clients must send this password in the proxy login; better set `PROXY_SCRAPER_SERVE_PASSWORD` |
688
705
  | `--rotate STRATEGY` · `--sticky SEC` | how the proxy server picks proxies, see [above](#proxy-server) |
689
706
  | `--serve-refill HOURS` | while serving, check fresh proxies every HOURS and add the hits to the pool |
690
707
  | `--serve [PORT]` | afterwards serve as a rotating proxy on `127.0.0.1:PORT` (default: 8899) |
691
708
  | `-o FILE` | also write all hits to this file; `-o -` prints them to stdout |
692
- | `--export FORMATS` | extra files for other tools: `proxychains`, `clash`, `curl` or `all` |
709
+ | `--export FORMATS` | extra files for other tools: `proxychains`, `clash`, `singbox`, `curl` or `all` |
693
710
  | `-V`, `--version` | print the version |
694
- | `--completion SHELL` | print the tab completion script for bash, zsh or fish |
711
+ | `--completion SHELL` | print the tab completion script for bash, zsh, fish or PowerShell |
695
712
 
696
713
  Everything else: `proxy-scraper --help`
697
714
 
@@ -773,10 +790,12 @@ Only for things that don't matter. Public proxies are run by strangers who can r
773
790
 
774
791
  ## Roadmap
775
792
 
776
- What's next is tracked in the milestone [**v1.8**](../../milestone/8) – ideas and wishes are welcome as an [issue](../../issues/new/choose). During Hacktoberfest there are [beginner-friendly issues](../../issues?q=is%3Aopen+label%3Ahacktoberfest) with pointers on where to start.
793
+ What's next is in the [open issues](../../issues) – ideas and wishes are welcome as an [issue](../../issues/new/choose). During Hacktoberfest there are [beginner-friendly issues](../../issues?q=is%3Aopen+label%3Ahacktoberfest) with pointers on where to start.
794
+
795
+ - [ ] [Measure throughput, not just latency](../../issues/140)
796
+ - [ ] [Daily snapshots as a dataset](../../issues/143)
777
797
 
778
- - [ ] [Protocol detection on the same connection](../../issues/44)
779
- - [ ] [sing-box export](../../issues/113), [PowerShell completion](../../issues/114), [shareable filters on the website](../../issues/116)
798
+ Shipped in v1.8 and v1.9: uptime per proxy and `stable.txt`, which proxies get through to Google, Reddit and Amazon, `live_proxies()` in Python, a page per proxy with its week of checks, a weekly report, live charts in this README, shareable filters on the website, sources mined from other scrapers' lists, untyped lists tried as HTTP and SOCKS5, `--export singbox`, PowerShell completion and recipes for httpx, aiohttp and Scrapy.
780
799
 
781
800
  Shipped in [v1.7](../../milestone/7): an MCP server so AI agents get working proxies and can load pages through them, 28 new sources and a GitHub search that runs daily and keeps what it found, and a cleaner website.
782
801
 
@@ -821,7 +840,7 @@ proxyscraper/
821
840
  ├── geodb.py DB-IP country database (monthly, binary search)
822
841
  ├── targets.py target sites for --target
823
842
  ├── output.py result files
824
- ├── exporters.py proxychains, Clash and curl formats (--export)
843
+ ├── exporters.py proxychains, Clash, sing-box and curl formats (--export)
825
844
  ├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · core
826
845
  ├── api.py find_proxies() / check_proxies() for Python
827
846
  ├── agent.py MCP tools without the SDK: live list, filters, fetch through proxies
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxy-scraper-cli
3
- Version: 1.9.0
3
+ Version: 1.10.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
@@ -35,6 +35,7 @@ Provides-Extra: mcp
35
35
  Requires-Dist: mcp<3,>=2.2; python_version >= "3.10" and extra == "mcp"
36
36
  Provides-Extra: dev
37
37
  Requires-Dist: pytest; extra == "dev"
38
+ Requires-Dist: hypothesis; extra == "dev"
38
39
  Requires-Dist: ruff==0.16.8; extra == "dev"
39
40
  Dynamic: license-file
40
41
 
@@ -138,11 +139,12 @@ python3 proxy_scraper.py
138
139
 
139
140
  Every [release](https://github.com/maximilianfeix/proxy-scraper/releases/latest) also ships a wheel you can install with `pip install <file>.whl`, and a multi-arch image (amd64/arm64) on `ghcr.io`. In the container the wizard never shows up, it runs straight away. For the proxy server use `--serve --serve-host 0.0.0.0` with `-p 127.0.0.1:8899:8899`, so the port is only open on your own machine.
140
141
 
141
- Tab completion for bash, zsh and fish:
142
+ Tab completion for bash, zsh, fish and PowerShell:
142
143
 
143
144
  ```bash
144
145
  eval "$(proxy-scraper --completion zsh)" # in ~/.zshrc (after compinit), same for bash in ~/.bashrc
145
146
  proxy-scraper --completion fish > ~/.config/fish/completions/proxy-scraper.fish
147
+ proxy-scraper --completion powershell | Out-String | Invoke-Expression # in $PROFILE
146
148
  ```
147
149
 
148
150
  Installed, the learned state lives in your user data folder (`~/Library/Application Support/proxy-scraper`, `%LOCALAPPDATA%\proxy-scraper` or `~/.local/share/proxy-scraper`; override with `PROXY_SCRAPER_HOME`) and results go to `./results`. Run from a clone, both stay inside the project.
@@ -656,14 +658,14 @@ if __name__ == "__main__":
656
658
 
657
659
  In a Scrapy project, put `RotatingProxy` in `middlewares.py` and add it to `DOWNLOADER_MIDDLEWARES` in `settings.py`. For long crawls, reload the pool now and then – the list changes every hour.
658
660
 
659
- **proxychains, Clash / Mihomo** – ready-made configs with `--export`
661
+ **proxychains, Clash / Mihomo, sing-box** – ready-made configs with `--export`
660
662
 
661
663
  ```bash
662
- proxy-scraper --want 30 -y --export proxychains,clash
664
+ proxy-scraper --want 30 -y --export proxychains,clash,singbox
663
665
  proxychains4 -f results/latest/proxychains.conf curl https://api.ipify.org
664
666
  ```
665
667
 
666
- `clash.yaml` has all HTTP and SOCKS5 proxies plus a `url-test` group that always picks the fastest. Both configs leave out HTTP proxies that can't tunnel (`CONNECT`), because these tools tunnel everything.
668
+ `clash.yaml` has all HTTP and SOCKS5 proxies plus a `url-test` group that always picks the fastest. `singbox.json` does the same for sing-box, SOCKS4 included, and opens a local proxy: `sing-box run -c results/latest/singbox.json`, then use `127.0.0.1:2080` as HTTP or SOCKS5 proxy. All three leave out HTTP proxies that can't tunnel (`CONNECT`), because these tools tunnel everything.
667
669
 
668
670
  **In a pipe** – `-o -` prints the hits to stdout, the interface moves to stderr
669
671
 
@@ -679,6 +681,22 @@ export HTTPS_PROXY=http://127.0.0.1:8899 HTTP_PROXY=http://127.0.0.1:8899
679
681
  pip download requests # git, pip, npm & co. now go through the pool
680
682
  ```
681
683
 
684
+ **In a GitHub workflow** – the action picks working proxies for the next steps
685
+
686
+ ```yaml
687
+ - id: proxies
688
+ uses: maximilianfeix/proxy-scraper@v1
689
+ with:
690
+ types: socks5
691
+ https: true
692
+ min-uptime: 90 # on the list for 90 %+ of the week
693
+ works-on: google # optional: google, reddit, amazon
694
+ recheck: true # optional: check them again from the runner
695
+ - run: curl -x "${{ steps.proxies.outputs.proxy }}" https://api.ipify.org
696
+ ```
697
+
698
+ `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`.
699
+
682
700
  **Without installing anything** – straight from the live list
683
701
 
684
702
  ```bash
@@ -722,16 +740,16 @@ curl.exe -x (Get-Content "$run\all.txt" -TotalCount 1) http://api.ipify.org
722
740
  | `-t`, `--timeout S` | timeout per proxy (default: 8 s) |
723
741
  | `--discover` | search GitHub for new sources right now |
724
742
  | `--no-cache` | download every list again (unchanged ones are normally skipped via ETag) |
725
- | `--list-sources [N]` | show the source ranking |
743
+ | `--list-sources [N]` | show the source ranking (add `--json` for scripts: url, status, hit rate, checks, last change) |
726
744
  | `--serve-host ADDR` | where the proxy server listens (default `127.0.0.1`; `0.0.0.0` for Docker, with a warning) |
727
745
  | `--serve-password SECRET` | clients must send this password in the proxy login; better set `PROXY_SCRAPER_SERVE_PASSWORD` |
728
746
  | `--rotate STRATEGY` · `--sticky SEC` | how the proxy server picks proxies, see [above](#proxy-server) |
729
747
  | `--serve-refill HOURS` | while serving, check fresh proxies every HOURS and add the hits to the pool |
730
748
  | `--serve [PORT]` | afterwards serve as a rotating proxy on `127.0.0.1:PORT` (default: 8899) |
731
749
  | `-o FILE` | also write all hits to this file; `-o -` prints them to stdout |
732
- | `--export FORMATS` | extra files for other tools: `proxychains`, `clash`, `curl` or `all` |
750
+ | `--export FORMATS` | extra files for other tools: `proxychains`, `clash`, `singbox`, `curl` or `all` |
733
751
  | `-V`, `--version` | print the version |
734
- | `--completion SHELL` | print the tab completion script for bash, zsh or fish |
752
+ | `--completion SHELL` | print the tab completion script for bash, zsh, fish or PowerShell |
735
753
 
736
754
  Everything else: `proxy-scraper --help`
737
755
 
@@ -813,10 +831,12 @@ Only for things that don't matter. Public proxies are run by strangers who can r
813
831
 
814
832
  ## Roadmap
815
833
 
816
- What's next is tracked in the milestone [**v1.8**](../../milestone/8) – ideas and wishes are welcome as an [issue](../../issues/new/choose). During Hacktoberfest there are [beginner-friendly issues](../../issues?q=is%3Aopen+label%3Ahacktoberfest) with pointers on where to start.
834
+ What's next is in the [open issues](../../issues) – ideas and wishes are welcome as an [issue](../../issues/new/choose). During Hacktoberfest there are [beginner-friendly issues](../../issues?q=is%3Aopen+label%3Ahacktoberfest) with pointers on where to start.
835
+
836
+ - [ ] [Measure throughput, not just latency](../../issues/140)
837
+ - [ ] [Daily snapshots as a dataset](../../issues/143)
817
838
 
818
- - [ ] [Protocol detection on the same connection](../../issues/44)
819
- - [ ] [sing-box export](../../issues/113), [PowerShell completion](../../issues/114), [shareable filters on the website](../../issues/116)
839
+ Shipped in v1.8 and v1.9: uptime per proxy and `stable.txt`, which proxies get through to Google, Reddit and Amazon, `live_proxies()` in Python, a page per proxy with its week of checks, a weekly report, live charts in this README, shareable filters on the website, sources mined from other scrapers' lists, untyped lists tried as HTTP and SOCKS5, `--export singbox`, PowerShell completion and recipes for httpx, aiohttp and Scrapy.
820
840
 
821
841
  Shipped in [v1.7](../../milestone/7): an MCP server so AI agents get working proxies and can load pages through them, 28 new sources and a GitHub search that runs daily and keeps what it found, and a cleaner website.
822
842
 
@@ -861,7 +881,7 @@ proxyscraper/
861
881
  ├── geodb.py DB-IP country database (monthly, binary search)
862
882
  ├── targets.py target sites for --target
863
883
  ├── output.py result files
864
- ├── exporters.py proxychains, Clash and curl formats (--export)
884
+ ├── exporters.py proxychains, Clash, sing-box and curl formats (--export)
865
885
  ├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · core
866
886
  ├── api.py find_proxies() / check_proxies() for Python
867
887
  ├── agent.py MCP tools without the SDK: live list, filters, fetch through proxies
@@ -9,6 +9,7 @@ proxy_scraper_cli.egg-info/requires.txt
9
9
  proxy_scraper_cli.egg-info/top_level.txt
10
10
  proxyscraper/__init__.py
11
11
  proxyscraper/__main__.py
12
+ proxyscraper/action.py
12
13
  proxyscraper/agent.py
13
14
  proxyscraper/api.py
14
15
  proxyscraper/app.py
@@ -62,6 +63,7 @@ proxyscraper/ui/report.py
62
63
  proxyscraper/ui/serve.py
63
64
  proxyscraper/ui/widgets.py
64
65
  proxyscraper/ui/wizard.py
66
+ tests/test_action.py
65
67
  tests/test_agent.py
66
68
  tests/test_api.py
67
69
  tests/test_app.py
@@ -86,6 +88,7 @@ tests/test_output_stdout.py
86
88
  tests/test_packaging.py
87
89
  tests/test_pages.py
88
90
  tests/test_parsing.py
91
+ tests/test_parsing_fuzz.py
89
92
  tests/test_paths.py
90
93
  tests/test_pipeline.py
91
94
  tests/test_proxy_pages.py
@@ -97,8 +100,10 @@ tests/test_serve_password.py
97
100
  tests/test_serve_refill.py
98
101
  tests/test_serve_v2.py
99
102
  tests/test_server.py
103
+ tests/test_server_closed_writer.py
100
104
  tests/test_server_fixes.py
101
105
  tests/test_sites.py
106
+ tests/test_sources_json.py
102
107
  tests/test_tamper.py
103
108
  tests/test_targets.py
104
109
  tests/test_ui.py
@@ -3,6 +3,7 @@ certifi
3
3
 
4
4
  [dev]
5
5
  pytest
6
+ hypothesis
6
7
  ruff==0.16.8
7
8
 
8
9
  [fast]
@@ -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.9.0"
4
+ __version__ = "1.10.0"
5
5
 
6
6
 
7
7
  def __getattr__(name):
@@ -0,0 +1,96 @@
1
+ """The GitHub Action (action.yml): working proxies from the hourly list for the next steps of a workflow.
2
+
3
+ Inputs come in as PS_* environment variables, results go to $GITHUB_OUTPUT:
4
+ proxy the fastest one, ready to use (socks5://1.2.3.4:1080)
5
+ file a text file with all of them, one per line, fastest first
6
+ count how many
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import sys
13
+ import tempfile
14
+ from pathlib import Path
15
+ from typing import List
16
+
17
+ from .api import check_proxies, live_proxies
18
+
19
+
20
+ class InputError(ValueError):
21
+ pass
22
+
23
+
24
+ def _flag(name: str, default: bool = False) -> bool:
25
+ value = os.environ.get(name, "").strip().lower()
26
+ if not value:
27
+ return default
28
+ if value in ("true", "yes", "1"):
29
+ return True
30
+ if value in ("false", "no", "0"):
31
+ return False
32
+ raise InputError(f"{name[3:].lower().replace('_', '-')} must be true or false, not {value!r}")
33
+
34
+
35
+ def _number(name: str, default: int) -> int:
36
+ value = os.environ.get(name, "").strip()
37
+ if not value:
38
+ return default
39
+ if not value.isdigit():
40
+ raise InputError(f"{name[3:].lower().replace('_', '-')} must be a whole number, not {value!r}")
41
+ return int(value)
42
+
43
+
44
+ def _list(name: str) -> List[str]:
45
+ return [part.strip().lower() for part in os.environ.get(name, "").split(",") if part.strip()]
46
+
47
+
48
+ def _output(**values: str) -> None:
49
+ path = os.environ.get("GITHUB_OUTPUT")
50
+ lines = "".join(f"{key}={value}\n" for key, value in values.items())
51
+ if path:
52
+ with open(path, "a", encoding="utf-8") as fh:
53
+ fh.write(lines)
54
+ else:
55
+ sys.stdout.write(lines)
56
+
57
+
58
+ def main() -> int:
59
+ try:
60
+ limit = _number("PS_LIMIT", 20)
61
+ recheck = _flag("PS_RECHECK")
62
+ fail_if_empty = _flag("PS_FAIL_IF_EMPTY", True)
63
+ filters = {"types": _list("PS_TYPES") or ["http", "socks4", "socks5"],
64
+ "countries": os.environ.get("PS_COUNTRIES", "").strip(),
65
+ "https": _flag("PS_HTTPS"), "min_uptime": _number("PS_MIN_UPTIME", 0),
66
+ "works_on": _list("PS_WORKS_ON")}
67
+ # a recheck starts from all of them – the limit applies to what still works from here
68
+ found = live_proxies(**filters, limit=0 if recheck else limit)
69
+ except (InputError, ValueError) as e:
70
+ print(f"::error::{e}")
71
+ return 1
72
+ except ConnectionError as e:
73
+ print(f"::error::The live list couldn't be loaded: {e}")
74
+ return 1
75
+ urls = [p.url for p in found]
76
+ if recheck and urls:
77
+ print(f"Checking {len(urls)} proxies again from this runner …")
78
+ urls = [r.url for r in sorted(check_proxies(urls, https=filters["https"]), key=lambda r: r.latency)]
79
+ if limit:
80
+ urls = urls[:limit]
81
+ # a file of its own per step: a job may use the action twice (socks5 in one step, http in the next)
82
+ fd, name = tempfile.mkstemp(prefix="proxies-", suffix=".txt", dir=os.environ.get("RUNNER_TEMP") or None)
83
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
84
+ fh.write("".join(f"{u}\n" for u in urls))
85
+ file = Path(name)
86
+ _output(proxy=urls[0] if urls else "", file=str(file), count=str(len(urls)))
87
+ print(f"{len(urls)} working {'proxy' if len(urls) == 1 else 'proxies'}" + (f", fastest: {urls[0]}" if urls else ""))
88
+ if not urls and fail_if_empty:
89
+ print("::error::No proxy matches these filters right now – loosen them (min-uptime, works-on, countries) "
90
+ "or set fail-if-empty: false")
91
+ return 1
92
+ return 0
93
+
94
+
95
+ if __name__ == "__main__":
96
+ sys.exit(main())
@@ -4,9 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  import argparse
6
6
  import asyncio
7
+ import json
7
8
  import os
8
9
  import sys
9
10
  from collections import Counter
11
+ from datetime import datetime, timezone
10
12
  from typing import List, Optional
11
13
 
12
14
  from rich.console import Console
@@ -38,13 +40,22 @@ from .ui.keys import is_interactive
38
40
  from .ui.wizard import run_wizard
39
41
 
40
42
 
41
- def list_sources(limit: int) -> int:
42
- """Ranking of every known source by learned hit rate."""
43
+ def list_sources(limit: int, as_json: bool = False) -> int:
44
+ """Ranking of every known source by learned hit rate – as a table, or as JSON on stdout for scripts."""
43
45
  quality = srcs.SourceStats()
44
46
  curated, _meta = srcs.load_source_file()
45
47
  urls = set(curated) | set(srcs.load_discovered()) | set(quality.records)
46
48
  ranking = quality.ranking(urls)
47
49
  rows = [(u, r, quality.skip_reason(u) or "active") for u, r in ranking if r.runs][:limit]
50
+ if as_json:
51
+ def stamp(t: float) -> Optional[str]:
52
+ return datetime.fromtimestamp(t, timezone.utc).isoformat(timespec="seconds") if t else None
53
+ sys.stdout.write(json.dumps([
54
+ {"url": u, "status": status, "hit_rate": round(r.score, 4), "checked": round(r.checked),
55
+ "working": round(r.working), "proxies": r.count, "runs": r.runs,
56
+ "first_seen": stamp(r.first_seen), "last_change": stamp(r.last_change)}
57
+ for u, r, status in rows], indent=1) + "\n")
58
+ return 0
48
59
  reasons = Counter(quality.skip_reason(u) or "active" for u, _ in ranking)
49
60
  widgets.console.print(banner())
50
61
  render_source_ranking(rows, len(urls), reasons)
@@ -181,7 +192,7 @@ def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
181
192
  "(the interface moves to stderr then)")
182
193
  o.add_argument("--export", type=export_list, metavar="FORMATS",
183
194
  help=f"extra formats in the results folder: {', '.join(EXPORTERS)} or all "
184
- "(e.g. --export proxychains,clash)")
195
+ "(e.g. --export proxychains,singbox)")
185
196
 
186
197
  s = p.add_argument_group("Sources")
187
198
  s.add_argument("--discover", action="store_true",
@@ -194,7 +205,11 @@ def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
194
205
  help="reload every list completely (otherwise unchanged ones are skipped via ETag)")
195
206
  s.add_argument("--list-sources", nargs="?", const=50, type=positive_int, metavar="N",
196
207
  help="show the source ranking by hit rate (default: top 50) and exit")
197
- return p.parse_args(argv)
208
+ s.add_argument("--json", action="store_true", help="with --list-sources: print the ranking as JSON to stdout")
209
+ args = p.parse_args(argv)
210
+ if args.json and args.list_sources is None:
211
+ p.error("--json only works together with --list-sources")
212
+ return args
198
213
 
199
214
 
200
215
  def last_options() -> Optional[RunOptions]:
@@ -244,7 +259,7 @@ def run(argv: Optional[List[str]] = None) -> int:
244
259
  from .mcp_entry import main as mcp_main
245
260
  return mcp_main()
246
261
  if args.list_sources is not None:
247
- return list_sources(args.list_sources)
262
+ return list_sources(args.list_sources, args.json)
248
263
  opts = RunOptions.from_args(args)
249
264
  if opts.output == STDOUT:
250
265
  widgets.console = Console(highlight=False, stderr=True) # stdout only carries the hits
@@ -1,4 +1,4 @@
1
- """Tab completion for bash, zsh and fish – generated straight from the argparse parser.
1
+ """Tab completion for bash, zsh, fish and PowerShell – generated straight from the argparse parser.
2
2
 
3
3
  That way the script can never go stale: every new option shows up automatically, with its choices
4
4
  (--types, --rotate, …) and the start of its help text as the description (zsh, fish).
@@ -12,7 +12,7 @@ import sys
12
12
  from dataclasses import dataclass
13
13
  from typing import List, Optional, Tuple
14
14
 
15
- SHELLS = ("bash", "zsh", "fish")
15
+ SHELLS = ("bash", "zsh", "fish", "powershell")
16
16
  PROG = "proxy-scraper"
17
17
  FILE_OPTIONS = {"--recheck": ("live",), "--output": ()} # the value is a file path (or one of these words)
18
18
  OWN_HELP = {"help": "show help", "version": "show the version"} # shorter than argparse's own texts
@@ -181,8 +181,66 @@ def fish(opts: List[Option]) -> str:
181
181
  return "\n".join(lines) + "\n"
182
182
 
183
183
 
184
+ def _ps_quote(text: str) -> str:
185
+ return "'" + text.replace("'", "''") + "'"
186
+
187
+
188
+ def powershell(opts: List[Option]) -> str:
189
+ """A native argument completer: options with their help as tooltip, the choices after an option (also for
190
+ several values in a row, --types http socks5), and PowerShell's own path completion for file options."""
191
+ rows = []
192
+ for o in opts:
193
+ # an optional value without choices (--serve, --list-sources): options may follow right away, like in bash
194
+ expects_value = o.takes_value and not (o.optional_value and not o.is_file and not o.choices)
195
+ fields = [f"Flags = @({', '.join(_ps_quote(f) for f in o.flags)})", f"Help = {_ps_quote(o.help)}",
196
+ f"Choices = @({', '.join(_ps_quote(c) for c in (*o.choices, *o.extra))})",
197
+ f"Value = ${str(expects_value).lower()}", f"Multi = ${str(o.multi).lower()}",
198
+ f"File = ${str(o.is_file).lower()}"]
199
+ rows.append(" @{ " + "; ".join(fields) + " }")
200
+ table = ",\n".join(rows)
201
+ return f"""# PowerShell completion for {PROG}
202
+ # enable: {PROG} --completion powershell | Out-String | Invoke-Expression (add that line to $PROFILE)
203
+ Register-ArgumentCompleter -Native -CommandName '{PROG}' -ScriptBlock {{
204
+ param($wordToComplete, $commandAst, $cursorPosition)
205
+ $options = @(
206
+ {table}
207
+ )
208
+ $words = @($commandAst.CommandElements | Where-Object {{ $_.Extent.EndOffset -le $cursorPosition }} |
209
+ ForEach-Object {{ $_.ToString() }})
210
+ if ($wordToComplete -and $words.Count -gt 0 -and $words[-1] -eq $wordToComplete) {{
211
+ $words = @($words | Select-Object -SkipLast 1)
212
+ }}
213
+ # the last option before the cursor, and whether the cursor is right after it
214
+ $last = $null; $distance = 0
215
+ for ($i = $words.Count - 1; $i -ge 1; $i--) {{
216
+ if ($words[$i].StartsWith('-')) {{ $last = $options | Where-Object {{ $_.Flags -contains $words[$i] }} |
217
+ Select-Object -First 1; break }}
218
+ $distance++
219
+ }}
220
+ if ($last -and $last.Value -and -not $wordToComplete.StartsWith('-') -and ($distance -eq 0 -or $last.Multi)) {{
221
+ if ($last.Choices.Count -gt 0) {{
222
+ $last.Choices | Where-Object {{ $_ -like "$wordToComplete*" }} | ForEach-Object {{
223
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
224
+ }}
225
+ if ($last.File) {{ # a file works too (--recheck live or a path)
226
+ [System.Management.Automation.CompletionCompleters]::CompleteFilename($wordToComplete)
227
+ }}
228
+ }}
229
+ return # nothing to suggest (a number, a path): PowerShell falls back to its own completion
230
+ }}
231
+ foreach ($option in $options) {{
232
+ foreach ($flag in $option.Flags) {{
233
+ if ($flag -like "$wordToComplete*") {{
234
+ [System.Management.Automation.CompletionResult]::new($flag, $flag, 'ParameterName', $option.Help)
235
+ }}
236
+ }}
237
+ }}
238
+ }}
239
+ """
240
+
241
+
184
242
  def script(parser: argparse.ArgumentParser, shell: str) -> str:
185
- return {"bash": bash, "zsh": zsh, "fish": fish}[shell](options(parser))
243
+ return {"bash": bash, "zsh": zsh, "fish": fish, "powershell": powershell}[shell](options(parser))
186
244
 
187
245
 
188
246
  class CompletionAction(argparse.Action):