proxy-scraper-cli 1.19.0__tar.gz → 1.21.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 (123) hide show
  1. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/PKG-INFO +75 -3
  2. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/README.md +74 -2
  3. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxy_scraper_cli.egg-info/PKG-INFO +75 -3
  4. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxy_scraper_cli.egg-info/SOURCES.txt +3 -0
  5. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/__init__.py +1 -1
  6. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/agent.py +31 -8
  7. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/api.py +77 -3
  8. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/app.py +5 -3
  9. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/cli.py +6 -0
  10. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/options.py +9 -0
  11. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/pipeline.py +22 -8
  12. proxy_scraper_cli-1.21.0/proxyscraper/server/api.py +186 -0
  13. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/core.py +28 -7
  14. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/pool.py +9 -0
  15. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/sources.py +24 -0
  16. proxy_scraper_cli-1.21.0/tests/test_own_sources.py +98 -0
  17. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_rotator.py +92 -0
  18. proxy_scraper_cli-1.21.0/tests/test_serve_api.py +153 -0
  19. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_server.py +2 -1
  20. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/LICENSE +0 -0
  21. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
  22. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
  23. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
  24. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
  25. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/__main__.py +0 -0
  26. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/action.py +0 -0
  27. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/asndb.py +0 -0
  28. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/blocklist.py +0 -0
  29. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/charts.py +0 -0
  30. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/checker.py +0 -0
  31. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/compat.py +0 -0
  32. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/completion.py +0 -0
  33. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/exporters.py +0 -0
  34. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/fetchcache.py +0 -0
  35. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/geo.py +0 -0
  36. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/geodb.py +0 -0
  37. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/handshake.py +0 -0
  38. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/history.py +0 -0
  39. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/judges.py +0 -0
  40. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/mcp_entry.py +0 -0
  41. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/mcp_server.py +0 -0
  42. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/netio.py +0 -0
  43. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/output.py +0 -0
  44. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/pages.py +0 -0
  45. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/parsing.py +0 -0
  46. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/paths.py +0 -0
  47. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/preferences.py +0 -0
  48. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/proxypages.py +0 -0
  49. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/publish.py +0 -0
  50. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ranking.py +0 -0
  51. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/report.py +0 -0
  52. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/runsteps.py +0 -0
  53. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/__init__.py +0 -0
  54. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/http.py +0 -0
  55. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/socks.py +0 -0
  56. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/status.py +0 -0
  57. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/upstream.py +0 -0
  58. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/site/apple-touch-icon.png +0 -0
  59. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
  60. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/site/index.html +0 -0
  61. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/site/logo.png +0 -0
  62. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/site/og.png +0 -0
  63. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/sites.py +0 -0
  64. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/sources.json +0 -0
  65. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/speed.py +0 -0
  66. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/targets.py +0 -0
  67. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/__init__.py +0 -0
  68. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/dashboard.py +0 -0
  69. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/keys.py +0 -0
  70. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/report.py +0 -0
  71. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/serve.py +0 -0
  72. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/widgets.py +0 -0
  73. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/wizard.py +0 -0
  74. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/pyproject.toml +0 -0
  75. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/setup.cfg +0 -0
  76. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_action.py +0 -0
  77. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_agent.py +0 -0
  78. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_api.py +0 -0
  79. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_app.py +0 -0
  80. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_asndb.py +0 -0
  81. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_auth.py +0 -0
  82. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_blocklist.py +0 -0
  83. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_charts.py +0 -0
  84. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_checker.py +0 -0
  85. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_compat.py +0 -0
  86. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_completion.py +0 -0
  87. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_discovery_growth.py +0 -0
  88. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_exporters.py +0 -0
  89. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_fetchcache.py +0 -0
  90. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_geodb.py +0 -0
  91. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_judges.py +0 -0
  92. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_learning.py +0 -0
  93. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_live_api.py +0 -0
  94. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_mcp_server.py +0 -0
  95. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_mine_sources.py +0 -0
  96. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_options.py +0 -0
  97. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_output_stdout.py +0 -0
  98. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_packaging.py +0 -0
  99. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_pages.py +0 -0
  100. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_parsing.py +0 -0
  101. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_parsing_fuzz.py +0 -0
  102. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_paths.py +0 -0
  103. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_pick.py +0 -0
  104. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_pipeline.py +0 -0
  105. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_proxy_pages.py +0 -0
  106. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_proxy_sources.py +0 -0
  107. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_publish.py +0 -0
  108. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_report.py +0 -0
  109. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_robustness.py +0 -0
  110. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_serve_password.py +0 -0
  111. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_serve_refill.py +0 -0
  112. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_serve_v2.py +0 -0
  113. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_server_closed_writer.py +0 -0
  114. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_server_fixes.py +0 -0
  115. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_sites.py +0 -0
  116. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_sources_json.py +0 -0
  117. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_speed.py +0 -0
  118. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_speed_ranking.py +0 -0
  119. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_tamper.py +0 -0
  120. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_targets.py +0 -0
  121. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_ui.py +0 -0
  122. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_uptime.py +0 -0
  123. {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.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.19.0
3
+ Version: 1.21.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
@@ -52,6 +52,7 @@ Dynamic: license-file
52
52
  [![Live proxies](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmaximilianfeix%2Fproxy-scraper%2Fproxy-list%2Fbadges%2Ftotal.json&style=flat-square&labelColor=121113)](#live-list)
53
53
  [![PyPI](https://img.shields.io/pypi/v/proxy-scraper-cli?style=flat-square&color=D4F77A&labelColor=121113&label=pypi)](https://pypi.org/project/proxy-scraper-cli/)
54
54
  [![Python](https://img.shields.io/badge/python-3.9–3.13-D4F77A?style=flat-square&labelColor=121113)](pyproject.toml)
55
+ [![Stars](https://img.shields.io/github/stars/maximilianfeix/proxy-scraper?style=flat-square&color=D4F77A&labelColor=121113)](https://github.com/maximilianfeix/proxy-scraper/stargazers)
55
56
  [![License](https://img.shields.io/badge/license-MIT-D4F77A?style=flat-square&labelColor=121113)](LICENSE)
56
57
 
57
58
  <a href="https://maximilianfeix.github.io/proxy-scraper/"><img src="https://img.shields.io/badge/Browse_the_live_list-D4F77A?style=for-the-badge&labelColor=121113" alt="Browse the live list"></a>
@@ -94,7 +95,7 @@ Most free proxy lists are 95 % dead, and a good part of the rest are honeypots o
94
95
  - [Options](#options)
95
96
  - [GitHub Actions](#github-actions)
96
97
  - [FAQ](#faq)
97
- - [Roadmap](#roadmap) · [Contributing](#contributing) · [Acknowledgements](#acknowledgements)
98
+ - [Roadmap](#roadmap) · [Contributing](#contributing) · [Community](#community) · [Acknowledgements](#acknowledgements)
98
99
 
99
100
  </details>
100
101
 
@@ -384,6 +385,7 @@ macOS, Linux and Windows, Python 3.9 to 3.13. Only two dependencies: `rich` and
384
385
  | Only proxies that reach *your* target site | ❌ | ✅ `--target` |
385
386
  | Learns which sources are worth it | ❌ | ✅ |
386
387
  | Usable as a single rotating proxy | ❌ | ✅ `--serve` |
388
+ | API to fetch a proxy (proxy_pool compatible) | ❌ | ✅ [`/get`](#pool-api) |
387
389
  | Ready-made list without running anything | ✅ | ✅ [live list](#live-list) |
388
390
 
389
391
  <a id="examples"></a>
@@ -456,6 +458,21 @@ with ProxyRotator(country="DE") as rotator:
456
458
 
457
459
  Usually a few seconds; when many proxies in a row are down it can take a minute (`timeout=` is per attempt). There's `aget()` with `async with` for asyncio code.
458
460
 
461
+ Already using requests, httpx, Playwright or Scrapy? `proxy_url()` gives you one local proxy address that rotates behind the scenes – every connection goes out through another proxy from the list, best first, dead ones skipped:
462
+
463
+ ```python
464
+ import requests
465
+ from proxyscraper import ProxyRotator
466
+
467
+ with ProxyRotator(country="DE") as rotator:
468
+ proxy = rotator.proxy_url() # http://country-de:…@127.0.0.1:…
469
+ requests.get("https://api.ipify.org", proxies={"http": proxy, "https": proxy})
470
+ # httpx.Client(proxy=proxy) · scrapy: meta={"proxy": proxy} · curl -x "$proxy"
471
+ # Playwright wants the login apart: chromium.launch(proxy=rotator.playwright_proxy())
472
+ ```
473
+
474
+ It runs on 127.0.0.1 with a random password, takes over each new hourly list by itself and stops with the `with` block.
475
+
459
476
  <a id="proxy-server"></a>
460
477
 
461
478
  ## Rotating proxy server
@@ -504,6 +521,38 @@ Without a password, `--serve-host` means **anyone who reaches the port can use i
504
521
 
505
522
  In testing: 20 of 20 HTTPS requests succeeded, over 15 different exit IPs. In the wizard this is **Proxy server right away**.
506
523
 
524
+ <a id="pool-api"></a>
525
+
526
+ ### Proxy pool API
527
+
528
+ Some programs want a proxy *address*, not a proxy – to hand it to a browser, a worker or a queue. The same port answers plain HTTP requests with one:
529
+
530
+ ```bash
531
+ curl http://127.0.0.1:8899/get # one proxy as JSON, chosen like a connection would be
532
+ curl "http://127.0.0.1:8899/get?country=DE&https=1&format=txt" # → http://203.0.113.7:8080
533
+ curl "http://127.0.0.1:8899/all?protocol=socks5&limit=20" # the 20 best SOCKS5 proxies
534
+ curl "http://127.0.0.1:8899/report?proxy=203.0.113.7:8080&ok=0" # it failed you – three times and it's out
535
+ ```
536
+
537
+ | Endpoint | What it does |
538
+ |---|---|
539
+ | `/get` | one proxy, picked by the `--rotate` strategy |
540
+ | `/pop` | like `/get`, and the proxy leaves the pool |
541
+ | `/all` | every usable proxy, best first (`limit=N`) |
542
+ | `/count` | totals by type and country |
543
+ | `/delete?proxy=IP:PORT` | take a proxy out |
544
+ | `/report?proxy=IP:PORT&ok=0` | feedback from your own requests |
545
+
546
+ Filters work on `/get`, `/pop` and `/all`: `country=DE,AT`, `protocol=socks5`, `https=1`, `anonymity=elite`, `max_latency=1500`, and `format=txt` for plain URLs. With `--serve-password` the API wants it as Basic auth, like the status page.
547
+
548
+ **Coming from [jhao104/proxy_pool](https://github.com/jhao104/proxy_pool)?** The endpoints, `type=https` and the JSON fields (`proxy`, `https`, `region`, `anonymous`, `check_count`, `fail_count`, …) are the same, so point your code at port 8899 and it keeps working – without Redis, and with proxies that passed the honeypot, tampering and TLS checks:
549
+
550
+ ```python
551
+ import requests
552
+ proxy = requests.get("http://127.0.0.1:8899/get?type=https").json()["url"] # e.g. socks5://…, type included
553
+ requests.get("https://example.com", proxies={"http": proxy, "https": proxy})
554
+ ```
555
+
507
556
  <a id="discord-bot"></a>
508
557
 
509
558
  ## Discord and Telegram bot
@@ -543,6 +592,15 @@ flowchart LR
543
592
  </div>
544
593
  </details>
545
594
 
595
+ ### Your own lists
596
+
597
+ ```bash
598
+ proxy-scraper --source https://example.com/my-list.txt --source socks5=./socks.txt # on top of the 700+ sources
599
+ proxy-scraper --only-sources --source bought.txt --want 50 # only yours
600
+ ```
601
+
602
+ Any text with `ip:port` works; lines like `socks5://user:pass@host:port` keep their type, bare ones are tried as HTTP and SOCKS5 unless you write `http=…`.
603
+
546
604
  <a id="output"></a>
547
605
 
548
606
  ## Output
@@ -909,7 +967,7 @@ proxyscraper/
909
967
  ├── targets.py target sites for --target
910
968
  ├── output.py result files
911
969
  ├── exporters.py proxychains, Clash, sing-box and curl formats (--export)
912
- ├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · core
970
+ ├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · api · core
913
971
  ├── api.py find_proxies() / check_proxies() for Python
914
972
  ├── agent.py MCP tools without the SDK: live list, filters, fetch through proxies
915
973
  ├── mcp_server.py MCP server (proxy-scraper-mcp) for AI agents
@@ -922,6 +980,20 @@ proxyscraper/
922
980
 
923
981
  </details>
924
982
 
983
+ <a id="community"></a>
984
+
985
+ ## Community
986
+
987
+ Questions, ideas and things you built with it go to [Discussions](https://github.com/maximilianfeix/proxy-scraper/discussions) – bugs to the [issues](../../issues). If proxy-scraper saves you time, a ⭐ helps others find it.
988
+
989
+ <a href="https://star-history.com/#maximilianfeix/proxy-scraper&Date">
990
+ <picture>
991
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date&theme=dark">
992
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date">
993
+ <img alt="Star history of proxy-scraper" src="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date" width="600">
994
+ </picture>
995
+ </a>
996
+
925
997
  <a id="acknowledgements"></a>
926
998
 
927
999
  ## Acknowledgements
@@ -11,6 +11,7 @@
11
11
  [![Live proxies](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmaximilianfeix%2Fproxy-scraper%2Fproxy-list%2Fbadges%2Ftotal.json&style=flat-square&labelColor=121113)](#live-list)
12
12
  [![PyPI](https://img.shields.io/pypi/v/proxy-scraper-cli?style=flat-square&color=D4F77A&labelColor=121113&label=pypi)](https://pypi.org/project/proxy-scraper-cli/)
13
13
  [![Python](https://img.shields.io/badge/python-3.9–3.13-D4F77A?style=flat-square&labelColor=121113)](pyproject.toml)
14
+ [![Stars](https://img.shields.io/github/stars/maximilianfeix/proxy-scraper?style=flat-square&color=D4F77A&labelColor=121113)](https://github.com/maximilianfeix/proxy-scraper/stargazers)
14
15
  [![License](https://img.shields.io/badge/license-MIT-D4F77A?style=flat-square&labelColor=121113)](LICENSE)
15
16
 
16
17
  <a href="https://maximilianfeix.github.io/proxy-scraper/"><img src="https://img.shields.io/badge/Browse_the_live_list-D4F77A?style=for-the-badge&labelColor=121113" alt="Browse the live list"></a>
@@ -53,7 +54,7 @@ Most free proxy lists are 95 % dead, and a good part of the rest are honeypots o
53
54
  - [Options](#options)
54
55
  - [GitHub Actions](#github-actions)
55
56
  - [FAQ](#faq)
56
- - [Roadmap](#roadmap) · [Contributing](#contributing) · [Acknowledgements](#acknowledgements)
57
+ - [Roadmap](#roadmap) · [Contributing](#contributing) · [Community](#community) · [Acknowledgements](#acknowledgements)
57
58
 
58
59
  </details>
59
60
 
@@ -343,6 +344,7 @@ macOS, Linux and Windows, Python 3.9 to 3.13. Only two dependencies: `rich` and
343
344
  | Only proxies that reach *your* target site | ❌ | ✅ `--target` |
344
345
  | Learns which sources are worth it | ❌ | ✅ |
345
346
  | Usable as a single rotating proxy | ❌ | ✅ `--serve` |
347
+ | API to fetch a proxy (proxy_pool compatible) | ❌ | ✅ [`/get`](#pool-api) |
346
348
  | Ready-made list without running anything | ✅ | ✅ [live list](#live-list) |
347
349
 
348
350
  <a id="examples"></a>
@@ -415,6 +417,21 @@ with ProxyRotator(country="DE") as rotator:
415
417
 
416
418
  Usually a few seconds; when many proxies in a row are down it can take a minute (`timeout=` is per attempt). There's `aget()` with `async with` for asyncio code.
417
419
 
420
+ Already using requests, httpx, Playwright or Scrapy? `proxy_url()` gives you one local proxy address that rotates behind the scenes – every connection goes out through another proxy from the list, best first, dead ones skipped:
421
+
422
+ ```python
423
+ import requests
424
+ from proxyscraper import ProxyRotator
425
+
426
+ with ProxyRotator(country="DE") as rotator:
427
+ proxy = rotator.proxy_url() # http://country-de:…@127.0.0.1:…
428
+ requests.get("https://api.ipify.org", proxies={"http": proxy, "https": proxy})
429
+ # httpx.Client(proxy=proxy) · scrapy: meta={"proxy": proxy} · curl -x "$proxy"
430
+ # Playwright wants the login apart: chromium.launch(proxy=rotator.playwright_proxy())
431
+ ```
432
+
433
+ It runs on 127.0.0.1 with a random password, takes over each new hourly list by itself and stops with the `with` block.
434
+
418
435
  <a id="proxy-server"></a>
419
436
 
420
437
  ## Rotating proxy server
@@ -463,6 +480,38 @@ Without a password, `--serve-host` means **anyone who reaches the port can use i
463
480
 
464
481
  In testing: 20 of 20 HTTPS requests succeeded, over 15 different exit IPs. In the wizard this is **Proxy server right away**.
465
482
 
483
+ <a id="pool-api"></a>
484
+
485
+ ### Proxy pool API
486
+
487
+ Some programs want a proxy *address*, not a proxy – to hand it to a browser, a worker or a queue. The same port answers plain HTTP requests with one:
488
+
489
+ ```bash
490
+ curl http://127.0.0.1:8899/get # one proxy as JSON, chosen like a connection would be
491
+ curl "http://127.0.0.1:8899/get?country=DE&https=1&format=txt" # → http://203.0.113.7:8080
492
+ curl "http://127.0.0.1:8899/all?protocol=socks5&limit=20" # the 20 best SOCKS5 proxies
493
+ curl "http://127.0.0.1:8899/report?proxy=203.0.113.7:8080&ok=0" # it failed you – three times and it's out
494
+ ```
495
+
496
+ | Endpoint | What it does |
497
+ |---|---|
498
+ | `/get` | one proxy, picked by the `--rotate` strategy |
499
+ | `/pop` | like `/get`, and the proxy leaves the pool |
500
+ | `/all` | every usable proxy, best first (`limit=N`) |
501
+ | `/count` | totals by type and country |
502
+ | `/delete?proxy=IP:PORT` | take a proxy out |
503
+ | `/report?proxy=IP:PORT&ok=0` | feedback from your own requests |
504
+
505
+ Filters work on `/get`, `/pop` and `/all`: `country=DE,AT`, `protocol=socks5`, `https=1`, `anonymity=elite`, `max_latency=1500`, and `format=txt` for plain URLs. With `--serve-password` the API wants it as Basic auth, like the status page.
506
+
507
+ **Coming from [jhao104/proxy_pool](https://github.com/jhao104/proxy_pool)?** The endpoints, `type=https` and the JSON fields (`proxy`, `https`, `region`, `anonymous`, `check_count`, `fail_count`, …) are the same, so point your code at port 8899 and it keeps working – without Redis, and with proxies that passed the honeypot, tampering and TLS checks:
508
+
509
+ ```python
510
+ import requests
511
+ proxy = requests.get("http://127.0.0.1:8899/get?type=https").json()["url"] # e.g. socks5://…, type included
512
+ requests.get("https://example.com", proxies={"http": proxy, "https": proxy})
513
+ ```
514
+
466
515
  <a id="discord-bot"></a>
467
516
 
468
517
  ## Discord and Telegram bot
@@ -502,6 +551,15 @@ flowchart LR
502
551
  </div>
503
552
  </details>
504
553
 
554
+ ### Your own lists
555
+
556
+ ```bash
557
+ proxy-scraper --source https://example.com/my-list.txt --source socks5=./socks.txt # on top of the 700+ sources
558
+ proxy-scraper --only-sources --source bought.txt --want 50 # only yours
559
+ ```
560
+
561
+ Any text with `ip:port` works; lines like `socks5://user:pass@host:port` keep their type, bare ones are tried as HTTP and SOCKS5 unless you write `http=…`.
562
+
505
563
  <a id="output"></a>
506
564
 
507
565
  ## Output
@@ -868,7 +926,7 @@ proxyscraper/
868
926
  ├── targets.py target sites for --target
869
927
  ├── output.py result files
870
928
  ├── exporters.py proxychains, Clash, sing-box and curl formats (--export)
871
- ├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · core
929
+ ├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · api · core
872
930
  ├── api.py find_proxies() / check_proxies() for Python
873
931
  ├── agent.py MCP tools without the SDK: live list, filters, fetch through proxies
874
932
  ├── mcp_server.py MCP server (proxy-scraper-mcp) for AI agents
@@ -881,6 +939,20 @@ proxyscraper/
881
939
 
882
940
  </details>
883
941
 
942
+ <a id="community"></a>
943
+
944
+ ## Community
945
+
946
+ Questions, ideas and things you built with it go to [Discussions](https://github.com/maximilianfeix/proxy-scraper/discussions) – bugs to the [issues](../../issues). If proxy-scraper saves you time, a ⭐ helps others find it.
947
+
948
+ <a href="https://star-history.com/#maximilianfeix/proxy-scraper&Date">
949
+ <picture>
950
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date&theme=dark">
951
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date">
952
+ <img alt="Star history of proxy-scraper" src="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date" width="600">
953
+ </picture>
954
+ </a>
955
+
884
956
  <a id="acknowledgements"></a>
885
957
 
886
958
  ## Acknowledgements
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxy-scraper-cli
3
- Version: 1.19.0
3
+ Version: 1.21.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
@@ -52,6 +52,7 @@ Dynamic: license-file
52
52
  [![Live proxies](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmaximilianfeix%2Fproxy-scraper%2Fproxy-list%2Fbadges%2Ftotal.json&style=flat-square&labelColor=121113)](#live-list)
53
53
  [![PyPI](https://img.shields.io/pypi/v/proxy-scraper-cli?style=flat-square&color=D4F77A&labelColor=121113&label=pypi)](https://pypi.org/project/proxy-scraper-cli/)
54
54
  [![Python](https://img.shields.io/badge/python-3.9–3.13-D4F77A?style=flat-square&labelColor=121113)](pyproject.toml)
55
+ [![Stars](https://img.shields.io/github/stars/maximilianfeix/proxy-scraper?style=flat-square&color=D4F77A&labelColor=121113)](https://github.com/maximilianfeix/proxy-scraper/stargazers)
55
56
  [![License](https://img.shields.io/badge/license-MIT-D4F77A?style=flat-square&labelColor=121113)](LICENSE)
56
57
 
57
58
  <a href="https://maximilianfeix.github.io/proxy-scraper/"><img src="https://img.shields.io/badge/Browse_the_live_list-D4F77A?style=for-the-badge&labelColor=121113" alt="Browse the live list"></a>
@@ -94,7 +95,7 @@ Most free proxy lists are 95 % dead, and a good part of the rest are honeypots o
94
95
  - [Options](#options)
95
96
  - [GitHub Actions](#github-actions)
96
97
  - [FAQ](#faq)
97
- - [Roadmap](#roadmap) · [Contributing](#contributing) · [Acknowledgements](#acknowledgements)
98
+ - [Roadmap](#roadmap) · [Contributing](#contributing) · [Community](#community) · [Acknowledgements](#acknowledgements)
98
99
 
99
100
  </details>
100
101
 
@@ -384,6 +385,7 @@ macOS, Linux and Windows, Python 3.9 to 3.13. Only two dependencies: `rich` and
384
385
  | Only proxies that reach *your* target site | ❌ | ✅ `--target` |
385
386
  | Learns which sources are worth it | ❌ | ✅ |
386
387
  | Usable as a single rotating proxy | ❌ | ✅ `--serve` |
388
+ | API to fetch a proxy (proxy_pool compatible) | ❌ | ✅ [`/get`](#pool-api) |
387
389
  | Ready-made list without running anything | ✅ | ✅ [live list](#live-list) |
388
390
 
389
391
  <a id="examples"></a>
@@ -456,6 +458,21 @@ with ProxyRotator(country="DE") as rotator:
456
458
 
457
459
  Usually a few seconds; when many proxies in a row are down it can take a minute (`timeout=` is per attempt). There's `aget()` with `async with` for asyncio code.
458
460
 
461
+ Already using requests, httpx, Playwright or Scrapy? `proxy_url()` gives you one local proxy address that rotates behind the scenes – every connection goes out through another proxy from the list, best first, dead ones skipped:
462
+
463
+ ```python
464
+ import requests
465
+ from proxyscraper import ProxyRotator
466
+
467
+ with ProxyRotator(country="DE") as rotator:
468
+ proxy = rotator.proxy_url() # http://country-de:…@127.0.0.1:…
469
+ requests.get("https://api.ipify.org", proxies={"http": proxy, "https": proxy})
470
+ # httpx.Client(proxy=proxy) · scrapy: meta={"proxy": proxy} · curl -x "$proxy"
471
+ # Playwright wants the login apart: chromium.launch(proxy=rotator.playwright_proxy())
472
+ ```
473
+
474
+ It runs on 127.0.0.1 with a random password, takes over each new hourly list by itself and stops with the `with` block.
475
+
459
476
  <a id="proxy-server"></a>
460
477
 
461
478
  ## Rotating proxy server
@@ -504,6 +521,38 @@ Without a password, `--serve-host` means **anyone who reaches the port can use i
504
521
 
505
522
  In testing: 20 of 20 HTTPS requests succeeded, over 15 different exit IPs. In the wizard this is **Proxy server right away**.
506
523
 
524
+ <a id="pool-api"></a>
525
+
526
+ ### Proxy pool API
527
+
528
+ Some programs want a proxy *address*, not a proxy – to hand it to a browser, a worker or a queue. The same port answers plain HTTP requests with one:
529
+
530
+ ```bash
531
+ curl http://127.0.0.1:8899/get # one proxy as JSON, chosen like a connection would be
532
+ curl "http://127.0.0.1:8899/get?country=DE&https=1&format=txt" # → http://203.0.113.7:8080
533
+ curl "http://127.0.0.1:8899/all?protocol=socks5&limit=20" # the 20 best SOCKS5 proxies
534
+ curl "http://127.0.0.1:8899/report?proxy=203.0.113.7:8080&ok=0" # it failed you – three times and it's out
535
+ ```
536
+
537
+ | Endpoint | What it does |
538
+ |---|---|
539
+ | `/get` | one proxy, picked by the `--rotate` strategy |
540
+ | `/pop` | like `/get`, and the proxy leaves the pool |
541
+ | `/all` | every usable proxy, best first (`limit=N`) |
542
+ | `/count` | totals by type and country |
543
+ | `/delete?proxy=IP:PORT` | take a proxy out |
544
+ | `/report?proxy=IP:PORT&ok=0` | feedback from your own requests |
545
+
546
+ Filters work on `/get`, `/pop` and `/all`: `country=DE,AT`, `protocol=socks5`, `https=1`, `anonymity=elite`, `max_latency=1500`, and `format=txt` for plain URLs. With `--serve-password` the API wants it as Basic auth, like the status page.
547
+
548
+ **Coming from [jhao104/proxy_pool](https://github.com/jhao104/proxy_pool)?** The endpoints, `type=https` and the JSON fields (`proxy`, `https`, `region`, `anonymous`, `check_count`, `fail_count`, …) are the same, so point your code at port 8899 and it keeps working – without Redis, and with proxies that passed the honeypot, tampering and TLS checks:
549
+
550
+ ```python
551
+ import requests
552
+ proxy = requests.get("http://127.0.0.1:8899/get?type=https").json()["url"] # e.g. socks5://…, type included
553
+ requests.get("https://example.com", proxies={"http": proxy, "https": proxy})
554
+ ```
555
+
507
556
  <a id="discord-bot"></a>
508
557
 
509
558
  ## Discord and Telegram bot
@@ -543,6 +592,15 @@ flowchart LR
543
592
  </div>
544
593
  </details>
545
594
 
595
+ ### Your own lists
596
+
597
+ ```bash
598
+ proxy-scraper --source https://example.com/my-list.txt --source socks5=./socks.txt # on top of the 700+ sources
599
+ proxy-scraper --only-sources --source bought.txt --want 50 # only yours
600
+ ```
601
+
602
+ Any text with `ip:port` works; lines like `socks5://user:pass@host:port` keep their type, bare ones are tried as HTTP and SOCKS5 unless you write `http=…`.
603
+
546
604
  <a id="output"></a>
547
605
 
548
606
  ## Output
@@ -909,7 +967,7 @@ proxyscraper/
909
967
  ├── targets.py target sites for --target
910
968
  ├── output.py result files
911
969
  ├── exporters.py proxychains, Clash, sing-box and curl formats (--export)
912
- ├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · core
970
+ ├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · api · core
913
971
  ├── api.py find_proxies() / check_proxies() for Python
914
972
  ├── agent.py MCP tools without the SDK: live list, filters, fetch through proxies
915
973
  ├── mcp_server.py MCP server (proxy-scraper-mcp) for AI agents
@@ -922,6 +980,20 @@ proxyscraper/
922
980
 
923
981
  </details>
924
982
 
983
+ <a id="community"></a>
984
+
985
+ ## Community
986
+
987
+ Questions, ideas and things you built with it go to [Discussions](https://github.com/maximilianfeix/proxy-scraper/discussions) – bugs to the [issues](../../issues). If proxy-scraper saves you time, a ⭐ helps others find it.
988
+
989
+ <a href="https://star-history.com/#maximilianfeix/proxy-scraper&Date">
990
+ <picture>
991
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date&theme=dark">
992
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date">
993
+ <img alt="Star history of proxy-scraper" src="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date" width="600">
994
+ </picture>
995
+ </a>
996
+
925
997
  <a id="acknowledgements"></a>
926
998
 
927
999
  ## Acknowledgements
@@ -48,6 +48,7 @@ proxyscraper/sources.py
48
48
  proxyscraper/speed.py
49
49
  proxyscraper/targets.py
50
50
  proxyscraper/server/__init__.py
51
+ proxyscraper/server/api.py
51
52
  proxyscraper/server/core.py
52
53
  proxyscraper/server/http.py
53
54
  proxyscraper/server/pool.py
@@ -88,6 +89,7 @@ tests/test_mcp_server.py
88
89
  tests/test_mine_sources.py
89
90
  tests/test_options.py
90
91
  tests/test_output_stdout.py
92
+ tests/test_own_sources.py
91
93
  tests/test_packaging.py
92
94
  tests/test_pages.py
93
95
  tests/test_parsing.py
@@ -101,6 +103,7 @@ tests/test_publish.py
101
103
  tests/test_report.py
102
104
  tests/test_robustness.py
103
105
  tests/test_rotator.py
106
+ tests/test_serve_api.py
104
107
  tests/test_serve_password.py
105
108
  tests/test_serve_refill.py
106
109
  tests/test_serve_v2.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.19.0"
4
+ __version__ = "1.21.0"
5
5
 
6
6
 
7
7
  def __getattr__(name):
@@ -458,6 +458,22 @@ def _to_text(body: bytes, headers, content_type: str, raw_html: bool) -> str:
458
458
  return text if raw_html or "html" not in content_type.lower() else html_to_text(text)
459
459
 
460
460
 
461
+ def _require_match(live: LiveList, protocol: str, countries: List[str], tls: bool) -> None:
462
+ if not any(True for _ in matching(live.rows, protocol, countries, https_only=tls, run_hours=live.run_hours)):
463
+ what = " ".join(x for x in (countries[0] if countries else "", "" if protocol == "any" else protocol,
464
+ "HTTPS-capable" if tls else "") if x)
465
+ raise AgentError(f"No {what} proxy in the live list right now."
466
+ + (shortage_note(live.rows, 1, 0, countries) or ""))
467
+
468
+
469
+ def _wishes(protocol: str, countries: List[str]) -> List[str]:
470
+ """What the rotating server reads from the proxy login: country-de, type-socks5."""
471
+ wishes = [f"country-{countries[0].lower()}"] if countries else []
472
+ if protocol != "any":
473
+ wishes.append(f"type-{protocol}")
474
+ return wishes
475
+
476
+
461
477
  class PageFetcher:
462
478
  """Loads pages through the rotating server of this package, running on 127.0.0.1 with a random password:
463
479
  the same failover as `--serve`, and HTTPS only through proxies that passed the verified-TLS test.
@@ -503,6 +519,19 @@ class PageFetcher:
503
519
  self._list_id = list_id
504
520
  return live
505
521
 
522
+ async def local_proxy(self, protocol: str = "any", country: str = "") -> str:
523
+ """The URL of the rotating server behind this fetcher, with the login, for any HTTP client:
524
+ http://country-de:<password>@127.0.0.1:<port>. Every connection through it gets a proxy of its own."""
525
+ protocol = _check_protocol(protocol)
526
+ countries = normalize_countries([country] if country else [])
527
+ _require_match(await self._ready(), protocol, countries, tls=False)
528
+ user = "-".join(_wishes(protocol, countries)) or "any"
529
+ return f"http://{user}:{self._password}@127.0.0.1:{self.server.port}"
530
+
531
+ async def refresh(self) -> None:
532
+ """Take over a new run's list, if there is one (fetch does that by itself)."""
533
+ await self._ready()
534
+
506
535
  async def fetch(self, url: str, protocol: str = "any", country: str = "", max_chars: int = 20000,
507
536
  raw_html: bool = False, progress: Optional[Progress] = None, tick: float = 5.0,
508
537
  with_body: bool = False) -> dict:
@@ -511,15 +540,9 @@ class PageFetcher:
511
540
  countries = normalize_countries([country] if country else [])
512
541
  live = await self._ready()
513
542
  tls = urlsplit(url).scheme == "https"
514
- if not any(True for _ in matching(live.rows, protocol, countries, https_only=tls, run_hours=live.run_hours)):
515
- what = " ".join(x for x in (countries[0] if countries else "", "" if protocol == "any" else protocol,
516
- "HTTPS-capable" if tls else "") if x)
517
- raise AgentError(f"No {what} proxy in the live list right now."
518
- + (shortage_note(live.rows, 1, 0, countries) or ""))
543
+ _require_match(live, protocol, countries, tls)
519
544
  session = secrets.token_hex(6)
520
- wishes = [f"country-{countries[0].lower()}"] if countries else []
521
- if protocol != "any":
522
- wishes.append(f"type-{protocol}")
545
+ wishes = _wishes(protocol, countries)
523
546
  pool, port = self.server.pool, self.server.port
524
547
  started = time.monotonic()
525
548
  result, faults, suspects = None, [], []
@@ -27,12 +27,14 @@ import asyncio
27
27
  import contextlib
28
28
  import email.message
29
29
  import io
30
+ import logging
30
31
  import os
31
32
  import re
32
33
  import tempfile
33
34
  import threading
34
35
  from dataclasses import dataclass, field
35
36
  from typing import Dict, Iterable, List, Optional, Tuple
37
+ from urllib.parse import urlsplit
36
38
 
37
39
  from rich.console import Console
38
40
 
@@ -42,6 +44,8 @@ from .parsing import PROXY_TYPES
42
44
  from .targets import parse_target
43
45
  from .ui import widgets
44
46
 
47
+ log = logging.getLogger(__name__)
48
+
45
49
  __all__ = [
46
50
  "CheckResult",
47
51
  "LiveProxy",
@@ -309,6 +313,15 @@ class ProxyResponse:
309
313
  raise ConnectionError(f"the body from {self.final_url} was cut off at 2 MB")
310
314
 
311
315
 
316
+ REFRESH_SECONDS = 300 # proxy_url(): how often the pool behind it looks for a new run's list
317
+
318
+
319
+ def _playwright(url: str) -> Dict[str, str]:
320
+ parts = urlsplit(url)
321
+ return {"server": f"{parts.scheme}://{parts.hostname}:{parts.port}", "username": parts.username or "",
322
+ "password": parts.password or ""}
323
+
324
+
312
325
  class ProxyRotator:
313
326
  """Loads URLs through proxies from the hourly list and switches to the next one when a proxy fails –
314
327
  the loop every scraper writes by hand. HTTPS only through proxies with verified TLS.
@@ -330,6 +343,7 @@ class ProxyRotator:
330
343
  self._loop: Optional[asyncio.AbstractEventLoop] = None # the sync API's own loop, in a thread
331
344
  self._thread: Optional[threading.Thread] = None
332
345
  self._start_lock = threading.Lock()
346
+ self._refresher: Optional[asyncio.Task] = None
333
347
 
334
348
  def _use(self, mode: str) -> None:
335
349
  if self._mode is None:
@@ -367,7 +381,43 @@ class ProxyRotator:
367
381
  proxy_country=page["proxy_country"], elapsed_ms=page["elapsed_ms"],
368
382
  truncated=page["body_truncated"])
369
383
 
384
+ async def aproxy_url(self) -> str:
385
+ self._use("async")
386
+ return await self._proxy_url()
387
+
388
+ async def _proxy_url(self) -> str:
389
+ from .agent import AgentError
390
+
391
+ if self._fetcher is None:
392
+ self._fetcher = self._make_fetcher()
393
+ try:
394
+ url = await self._fetcher.local_proxy(self.protocol, self.country)
395
+ except AgentError as e:
396
+ raise ConnectionError(str(e)) from None
397
+ if self._refresher is None: # nobody calls get() on this path, so the pool refreshes by itself
398
+ self._refresher = asyncio.ensure_future(self._keep_fresh())
399
+ return url
400
+
401
+ async def aplaywright_proxy(self) -> Dict[str, str]:
402
+ self._use("async")
403
+ return _playwright(await self._proxy_url())
404
+
405
+ async def _keep_fresh(self) -> None:
406
+ from .agent import AgentError
407
+
408
+ while True:
409
+ await asyncio.sleep(REFRESH_SECONDS)
410
+ try:
411
+ await self._fetcher.refresh()
412
+ except AgentError:
413
+ pass # GitHub not reachable right now: the pool keeps what it has, next try later
414
+ except Exception as e: # a broken list must not end the refreshing for good
415
+ log.warning("proxy-scraper: could not refresh the proxy list: %s", e)
416
+
370
417
  async def aclose(self) -> None:
418
+ if self._refresher is not None:
419
+ self._refresher.cancel()
420
+ self._refresher = None
371
421
  if self._fetcher is not None:
372
422
  await self._fetcher.close()
373
423
  self._fetcher = None
@@ -380,13 +430,37 @@ class ProxyRotator:
380
430
 
381
431
  # sync: one event loop in a background thread keeps the proxy server alive between calls
382
432
  def get(self, url: str) -> ProxyResponse:
383
- with self._start_lock: # several threads calling get() at once still share one loop
384
- self._use("sync")
433
+ return self._run_sync(self._aget(url))
434
+
435
+ def _run_sync(self, coro):
436
+ with self._start_lock: # several threads calling at once still share one loop
437
+ try:
438
+ self._use("sync")
439
+ except RuntimeError:
440
+ coro.close()
441
+ raise
385
442
  if self._loop is None:
386
443
  self._loop = asyncio.new_event_loop()
387
444
  self._thread = threading.Thread(target=self._loop.run_forever, name="proxy-rotator", daemon=True)
388
445
  self._thread.start()
389
- return asyncio.run_coroutine_threadsafe(self._aget(url), self._loop).result()
446
+ return asyncio.run_coroutine_threadsafe(coro, self._loop).result()
447
+
448
+ def proxy_url(self) -> str:
449
+ """A local proxy URL for any library – requests, httpx, aiohttp, Playwright, Scrapy, curl:
450
+
451
+ with ProxyRotator(country="DE") as rotator:
452
+ proxy = rotator.proxy_url()
453
+ requests.get(url, proxies={"http": proxy, "https": proxy})
454
+
455
+ Every connection through it gets a proxy from the live list (best first, with failover), HTTPS only
456
+ through verified-TLS ones. It lives as long as the rotator and takes over each new run's list."""
457
+ return self._run_sync(self._proxy_url())
458
+
459
+ def playwright_proxy(self) -> Dict[str, str]:
460
+ """proxy_url() the way Playwright wants it – it ignores a login inside the server address:
461
+
462
+ browser = p.chromium.launch(proxy=rotator.playwright_proxy())"""
463
+ return _playwright(self.proxy_url())
390
464
 
391
465
  def close(self) -> None:
392
466
  with self._start_lock:
@@ -327,8 +327,10 @@ class Run:
327
327
  plan = await collect_sources(self.opts, self.quality)
328
328
  info("Sources", Text.assemble(
329
329
  (fmt(len(plan.sources)), f"bold {ACCENT}"), " active ",
330
- (f"({fmt(plan.n_curated)} curated · {fmt(plan.n_meta)} from {plan.meta_ok}/{plan.meta_total} "
331
- f"meta lists · {fmt(plan.n_discovered)} discovered)", MUTED),
330
+ (f"({fmt(plan.n_own)} own)" if self.opts.only_sources else
331
+ f"({fmt(plan.n_curated)} curated · {fmt(plan.n_meta)} from {plan.meta_ok}/{plan.meta_total} "
332
+ f"meta lists · {fmt(plan.n_discovered)} discovered"
333
+ + (f" · {fmt(plan.n_own)} own)" if plan.n_own else ")"), MUTED),
332
334
  ))
333
335
  if plan.discovery_ran and not plan.discovery_token:
334
336
  note("GitHub discovery is limited without a token – run `gh auth login` or set GITHUB_TOKEN.",
@@ -355,7 +357,7 @@ class Run:
355
357
  hosts = sorted(INSECURE_HOSTS)
356
358
  note(f"Certificate could not be verified (TLS inspection on this network?), loaded anyway: "
357
359
  f"{', '.join(hosts[:4])}{' …' if len(hosts) > 4 else ''}", MUTED, "ℹ")
358
- return prioritize(res, self.quality, self.history, self.opts.types)
360
+ return prioritize(res, self.quality, self.history, self.opts.types, listed_only=self.opts.only_sources)
359
361
 
360
362
  # ------------------------------------------------------------------ phase 3+4: check, learn, report
361
363