proxy-scraper-cli 1.8.1__tar.gz → 1.9.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 (108) hide show
  1. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/PKG-INFO +79 -1
  2. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/README.md +78 -0
  3. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/PKG-INFO +79 -1
  4. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/SOURCES.txt +4 -0
  5. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/__init__.py +1 -1
  6. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/pages.py +7 -4
  7. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/parsing.py +7 -7
  8. proxy_scraper_cli-1.9.0/proxyscraper/proxypages.py +241 -0
  9. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/publish.py +15 -1
  10. proxy_scraper_cli-1.9.0/proxyscraper/report.py +264 -0
  11. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/site/index.html +6 -2
  12. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/sources.py +3 -1
  13. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_mine_sources.py +8 -0
  14. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_parsing.py +7 -5
  15. proxy_scraper_cli-1.9.0/tests/test_proxy_pages.py +93 -0
  16. proxy_scraper_cli-1.9.0/tests/test_report.py +113 -0
  17. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_uptime.py +12 -0
  18. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/LICENSE +0 -0
  19. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
  20. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
  21. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
  22. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
  23. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/__main__.py +0 -0
  24. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/agent.py +0 -0
  25. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/api.py +0 -0
  26. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/app.py +0 -0
  27. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/asndb.py +0 -0
  28. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/blocklist.py +0 -0
  29. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/charts.py +0 -0
  30. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/checker.py +0 -0
  31. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/cli.py +0 -0
  32. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/compat.py +0 -0
  33. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/completion.py +0 -0
  34. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/exporters.py +0 -0
  35. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/fetchcache.py +0 -0
  36. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/geo.py +0 -0
  37. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/geodb.py +0 -0
  38. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/handshake.py +0 -0
  39. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/history.py +0 -0
  40. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/judges.py +0 -0
  41. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/mcp_entry.py +0 -0
  42. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/mcp_server.py +0 -0
  43. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/netio.py +0 -0
  44. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/options.py +0 -0
  45. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/output.py +0 -0
  46. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/paths.py +0 -0
  47. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/pipeline.py +0 -0
  48. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/preferences.py +0 -0
  49. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/__init__.py +0 -0
  50. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/core.py +0 -0
  51. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/http.py +0 -0
  52. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/pool.py +0 -0
  53. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/socks.py +0 -0
  54. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/status.py +0 -0
  55. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/upstream.py +0 -0
  56. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/site/apple-touch-icon.png +0 -0
  57. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
  58. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/site/logo.png +0 -0
  59. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/site/og.png +0 -0
  60. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/sites.py +0 -0
  61. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/sources.json +0 -0
  62. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/targets.py +0 -0
  63. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/__init__.py +0 -0
  64. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/dashboard.py +0 -0
  65. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/keys.py +0 -0
  66. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/report.py +0 -0
  67. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/serve.py +0 -0
  68. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/widgets.py +0 -0
  69. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/wizard.py +0 -0
  70. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/pyproject.toml +0 -0
  71. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/setup.cfg +0 -0
  72. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_agent.py +0 -0
  73. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_api.py +0 -0
  74. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_app.py +0 -0
  75. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_asndb.py +0 -0
  76. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_auth.py +0 -0
  77. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_blocklist.py +0 -0
  78. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_charts.py +0 -0
  79. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_checker.py +0 -0
  80. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_compat.py +0 -0
  81. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_completion.py +0 -0
  82. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_discovery_growth.py +0 -0
  83. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_exporters.py +0 -0
  84. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_fetchcache.py +0 -0
  85. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_geodb.py +0 -0
  86. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_judges.py +0 -0
  87. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_learning.py +0 -0
  88. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_live_api.py +0 -0
  89. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_mcp_server.py +0 -0
  90. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_options.py +0 -0
  91. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_output_stdout.py +0 -0
  92. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_packaging.py +0 -0
  93. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_pages.py +0 -0
  94. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_paths.py +0 -0
  95. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_pipeline.py +0 -0
  96. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_proxy_sources.py +0 -0
  97. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_publish.py +0 -0
  98. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_robustness.py +0 -0
  99. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_serve_password.py +0 -0
  100. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_serve_refill.py +0 -0
  101. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_serve_v2.py +0 -0
  102. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_server.py +0 -0
  103. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_server_fixes.py +0 -0
  104. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_sites.py +0 -0
  105. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_tamper.py +0 -0
  106. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_targets.py +0 -0
  107. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_ui.py +0 -0
  108. {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.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.8.1
3
+ Version: 1.9.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
@@ -578,6 +578,84 @@ for p in proxies:
578
578
  continue # free proxies come and go – just take the next one
579
579
  ```
580
580
 
581
+ **httpx** (`pip install "httpx[socks]"`) – straight from the hourly list, no scan
582
+
583
+ ```python
584
+ import httpx
585
+ from proxyscraper import live_proxies
586
+
587
+ for p in live_proxies(types=["http", "socks5"], https=True, min_uptime=90, limit=10): # httpx can't do SOCKS4
588
+ try:
589
+ with httpx.Client(proxy=p.url, timeout=10) as client:
590
+ print(p.url, "→", client.get("https://api.ipify.org").text)
591
+ break
592
+ except httpx.HTTPError:
593
+ continue # next one
594
+ ```
595
+
596
+ **aiohttp** (`pip install aiohttp aiohttp-socks` – aiohttp alone can't do SOCKS)
597
+
598
+ ```python
599
+ import asyncio
600
+
601
+ import aiohttp
602
+ from aiohttp_socks import ProxyConnector, ProxyError
603
+ from proxyscraper import live_proxies_async
604
+
605
+
606
+ async def main():
607
+ for p in await live_proxies_async(https=True, min_uptime=90, limit=10):
608
+ try:
609
+ async with aiohttp.ClientSession(connector=ProxyConnector.from_url(p.url)) as session:
610
+ async with session.get("https://api.ipify.org", timeout=aiohttp.ClientTimeout(total=10)) as r:
611
+ print(p.url, "→", await r.text())
612
+ return
613
+ except (aiohttp.ClientError, ProxyError, asyncio.TimeoutError, OSError):
614
+ continue # free proxies come and go – just take the next one
615
+
616
+ asyncio.run(main())
617
+ ```
618
+
619
+ **Scrapy** – a downloader middleware that sends every request, retries included, through the next proxy. Scrapy only speaks HTTP proxies, `https=True` picks the ones that can tunnel `https://` pages
620
+
621
+ ```python
622
+ import itertools
623
+
624
+ import scrapy
625
+ from scrapy.crawler import CrawlerProcess
626
+ from proxyscraper import live_proxies
627
+
628
+ PROXIES = [p.url for p in live_proxies(types="http", https=True, min_uptime=50)]
629
+ if not PROXIES:
630
+ raise SystemExit("No proxy matches right now – loosen the filters (e.g. min_uptime)")
631
+ POOL = itertools.cycle(PROXIES)
632
+
633
+
634
+ class RotatingProxy:
635
+ def process_request(self, request, spider=None): # newer Scrapy leaves out spider
636
+ request.meta["proxy"] = next(POOL)
637
+
638
+
639
+ class IpSpider(scrapy.Spider):
640
+ name = "ip"
641
+ start_urls = [f"https://api.ipify.org/?n={i}" for i in range(3)]
642
+ custom_settings = {
643
+ "DOWNLOADER_MIDDLEWARES": {f"{__name__}.RotatingProxy": 350},
644
+ "RETRY_TIMES": 5, "DOWNLOAD_TIMEOUT": 15,
645
+ }
646
+
647
+ def parse(self, response):
648
+ print(response.meta["proxy"], "→", response.text)
649
+
650
+
651
+ if __name__ == "__main__":
652
+ process = CrawlerProcess()
653
+ process.crawl(IpSpider)
654
+ process.start()
655
+ ```
656
+
657
+ 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
+
581
659
  **proxychains, Clash / Mihomo** – ready-made configs with `--export`
582
660
 
583
661
  ```bash
@@ -538,6 +538,84 @@ for p in proxies:
538
538
  continue # free proxies come and go – just take the next one
539
539
  ```
540
540
 
541
+ **httpx** (`pip install "httpx[socks]"`) – straight from the hourly list, no scan
542
+
543
+ ```python
544
+ import httpx
545
+ from proxyscraper import live_proxies
546
+
547
+ for p in live_proxies(types=["http", "socks5"], https=True, min_uptime=90, limit=10): # httpx can't do SOCKS4
548
+ try:
549
+ with httpx.Client(proxy=p.url, timeout=10) as client:
550
+ print(p.url, "→", client.get("https://api.ipify.org").text)
551
+ break
552
+ except httpx.HTTPError:
553
+ continue # next one
554
+ ```
555
+
556
+ **aiohttp** (`pip install aiohttp aiohttp-socks` – aiohttp alone can't do SOCKS)
557
+
558
+ ```python
559
+ import asyncio
560
+
561
+ import aiohttp
562
+ from aiohttp_socks import ProxyConnector, ProxyError
563
+ from proxyscraper import live_proxies_async
564
+
565
+
566
+ async def main():
567
+ for p in await live_proxies_async(https=True, min_uptime=90, limit=10):
568
+ try:
569
+ async with aiohttp.ClientSession(connector=ProxyConnector.from_url(p.url)) as session:
570
+ async with session.get("https://api.ipify.org", timeout=aiohttp.ClientTimeout(total=10)) as r:
571
+ print(p.url, "→", await r.text())
572
+ return
573
+ except (aiohttp.ClientError, ProxyError, asyncio.TimeoutError, OSError):
574
+ continue # free proxies come and go – just take the next one
575
+
576
+ asyncio.run(main())
577
+ ```
578
+
579
+ **Scrapy** – a downloader middleware that sends every request, retries included, through the next proxy. Scrapy only speaks HTTP proxies, `https=True` picks the ones that can tunnel `https://` pages
580
+
581
+ ```python
582
+ import itertools
583
+
584
+ import scrapy
585
+ from scrapy.crawler import CrawlerProcess
586
+ from proxyscraper import live_proxies
587
+
588
+ PROXIES = [p.url for p in live_proxies(types="http", https=True, min_uptime=50)]
589
+ if not PROXIES:
590
+ raise SystemExit("No proxy matches right now – loosen the filters (e.g. min_uptime)")
591
+ POOL = itertools.cycle(PROXIES)
592
+
593
+
594
+ class RotatingProxy:
595
+ def process_request(self, request, spider=None): # newer Scrapy leaves out spider
596
+ request.meta["proxy"] = next(POOL)
597
+
598
+
599
+ class IpSpider(scrapy.Spider):
600
+ name = "ip"
601
+ start_urls = [f"https://api.ipify.org/?n={i}" for i in range(3)]
602
+ custom_settings = {
603
+ "DOWNLOADER_MIDDLEWARES": {f"{__name__}.RotatingProxy": 350},
604
+ "RETRY_TIMES": 5, "DOWNLOAD_TIMEOUT": 15,
605
+ }
606
+
607
+ def parse(self, response):
608
+ print(response.meta["proxy"], "→", response.text)
609
+
610
+
611
+ if __name__ == "__main__":
612
+ process = CrawlerProcess()
613
+ process.crawl(IpSpider)
614
+ process.start()
615
+ ```
616
+
617
+ 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
+
541
619
  **proxychains, Clash / Mihomo** – ready-made configs with `--export`
542
620
 
543
621
  ```bash
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxy-scraper-cli
3
- Version: 1.8.1
3
+ Version: 1.9.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
@@ -578,6 +578,84 @@ for p in proxies:
578
578
  continue # free proxies come and go – just take the next one
579
579
  ```
580
580
 
581
+ **httpx** (`pip install "httpx[socks]"`) – straight from the hourly list, no scan
582
+
583
+ ```python
584
+ import httpx
585
+ from proxyscraper import live_proxies
586
+
587
+ for p in live_proxies(types=["http", "socks5"], https=True, min_uptime=90, limit=10): # httpx can't do SOCKS4
588
+ try:
589
+ with httpx.Client(proxy=p.url, timeout=10) as client:
590
+ print(p.url, "→", client.get("https://api.ipify.org").text)
591
+ break
592
+ except httpx.HTTPError:
593
+ continue # next one
594
+ ```
595
+
596
+ **aiohttp** (`pip install aiohttp aiohttp-socks` – aiohttp alone can't do SOCKS)
597
+
598
+ ```python
599
+ import asyncio
600
+
601
+ import aiohttp
602
+ from aiohttp_socks import ProxyConnector, ProxyError
603
+ from proxyscraper import live_proxies_async
604
+
605
+
606
+ async def main():
607
+ for p in await live_proxies_async(https=True, min_uptime=90, limit=10):
608
+ try:
609
+ async with aiohttp.ClientSession(connector=ProxyConnector.from_url(p.url)) as session:
610
+ async with session.get("https://api.ipify.org", timeout=aiohttp.ClientTimeout(total=10)) as r:
611
+ print(p.url, "→", await r.text())
612
+ return
613
+ except (aiohttp.ClientError, ProxyError, asyncio.TimeoutError, OSError):
614
+ continue # free proxies come and go – just take the next one
615
+
616
+ asyncio.run(main())
617
+ ```
618
+
619
+ **Scrapy** – a downloader middleware that sends every request, retries included, through the next proxy. Scrapy only speaks HTTP proxies, `https=True` picks the ones that can tunnel `https://` pages
620
+
621
+ ```python
622
+ import itertools
623
+
624
+ import scrapy
625
+ from scrapy.crawler import CrawlerProcess
626
+ from proxyscraper import live_proxies
627
+
628
+ PROXIES = [p.url for p in live_proxies(types="http", https=True, min_uptime=50)]
629
+ if not PROXIES:
630
+ raise SystemExit("No proxy matches right now – loosen the filters (e.g. min_uptime)")
631
+ POOL = itertools.cycle(PROXIES)
632
+
633
+
634
+ class RotatingProxy:
635
+ def process_request(self, request, spider=None): # newer Scrapy leaves out spider
636
+ request.meta["proxy"] = next(POOL)
637
+
638
+
639
+ class IpSpider(scrapy.Spider):
640
+ name = "ip"
641
+ start_urls = [f"https://api.ipify.org/?n={i}" for i in range(3)]
642
+ custom_settings = {
643
+ "DOWNLOADER_MIDDLEWARES": {f"{__name__}.RotatingProxy": 350},
644
+ "RETRY_TIMES": 5, "DOWNLOAD_TIMEOUT": 15,
645
+ }
646
+
647
+ def parse(self, response):
648
+ print(response.meta["proxy"], "→", response.text)
649
+
650
+
651
+ if __name__ == "__main__":
652
+ process = CrawlerProcess()
653
+ process.crawl(IpSpider)
654
+ process.start()
655
+ ```
656
+
657
+ 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
+
581
659
  **proxychains, Clash / Mihomo** – ready-made configs with `--export`
582
660
 
583
661
  ```bash
@@ -36,7 +36,9 @@ proxyscraper/parsing.py
36
36
  proxyscraper/paths.py
37
37
  proxyscraper/pipeline.py
38
38
  proxyscraper/preferences.py
39
+ proxyscraper/proxypages.py
39
40
  proxyscraper/publish.py
41
+ proxyscraper/report.py
40
42
  proxyscraper/sites.py
41
43
  proxyscraper/sources.json
42
44
  proxyscraper/sources.py
@@ -86,8 +88,10 @@ tests/test_pages.py
86
88
  tests/test_parsing.py
87
89
  tests/test_paths.py
88
90
  tests/test_pipeline.py
91
+ tests/test_proxy_pages.py
89
92
  tests/test_proxy_sources.py
90
93
  tests/test_publish.py
94
+ tests/test_report.py
91
95
  tests/test_robustness.py
92
96
  tests/test_serve_password.py
93
97
  tests/test_serve_refill.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.8.1"
4
+ __version__ = "1.9.0"
5
5
 
6
6
 
7
7
  def __getattr__(name):
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  from datetime import datetime
11
11
  from html import escape
12
12
  from pathlib import Path
13
- from typing import Callable, Dict, List, Tuple
13
+ from typing import Callable, Dict, List, Sequence, Tuple
14
14
 
15
15
  SITE_URL = "https://maximilianfeix.github.io/proxy-scraper/"
16
16
  REPO_URL = "https://github.com/maximilianfeix/proxy-scraper"
@@ -121,7 +121,8 @@ def _render(path: str, title: str, what: str, rows: List[dict], updated: datetim
121
121
  lede = (f"No {what} passed every check in the last run ({when}). The list is checked again every hour, "
122
122
  "so look again later or try the full list.")
123
123
  body_rows = "\n".join(
124
- f"<tr><td class=\"mono\">{escape(r['proxy'])}</td><td>{escape(r['ptype'].upper())}</td>"
124
+ f"<tr><td class=\"mono\"><a href=\"{root}proxy/{escape(r['proxy'].replace(':', '-'))}/\">"
125
+ f"{escape(r['proxy'])}</a></td><td>{escape(r['ptype'].upper())}</td>"
125
126
  f"<td>{escape(r.get('country') or '–')}</td><td>{r['latency']:,} ms</td>"
126
127
  f"<td>{'yes' if r.get('https') else 'no'}</td><td>{escape(r.get('anonymity') or '–')}</td>"
127
128
  f"<td>{escape((r.get('org') or '–')[:40])}</td></tr>"
@@ -196,8 +197,9 @@ def _nav(specs, counts: Dict[str, int], current: str, root: str) -> str:
196
197
  f"<nav aria-label=\"By country\"><h2>By country</h2><ul>{links(countries)}</ul></nav>")
197
198
 
198
199
 
199
- def write_pages(rows: List[dict], out: Path, updated: datetime) -> List[str]:
200
- """Writes every page with its proxies.txt and the sitemap. -> the paths listed in the sitemap."""
200
+ def write_pages(rows: List[dict], out: Path, updated: datetime, extra: Sequence[str] = ()) -> List[str]:
201
+ """Writes every page with its proxies.txt and the sitemap (plus the `extra` paths, e.g. the proxy pages)
202
+ -> the paths listed in the sitemap."""
201
203
  rows = sorted(rows, key=lambda r: r["latency"])
202
204
  specs = page_specs()
203
205
  selected = {p: [r for r in rows if keep(r)] for p, _, _, keep in specs}
@@ -212,6 +214,7 @@ def write_pages(rows: List[dict], out: Path, updated: datetime) -> List[str]:
212
214
  (folder / "proxies.txt").write_text("".join(f"{r['url']}\n" for r in page_rows), encoding="utf-8")
213
215
  if not path.startswith("country/") or counts[path] >= SITEMAP_MIN:
214
216
  listed.append(path)
217
+ listed.extend(extra)
215
218
  lastmod = updated.strftime("%Y-%m-%dT%H:%M:%S+00:00")
216
219
  urls = "".join(f"<url><loc>{SITE_URL}{p}</loc><lastmod>{lastmod}</lastmod><changefreq>hourly</changefreq></url>"
217
220
  for p in listed)
@@ -44,8 +44,8 @@ _SPACE_SEPARATED_RE = re.compile(rb"\d\.\d{1,3}[ \t]+\d{2,5}(?![\d.])")
44
44
 
45
45
 
46
46
  # Bump whenever parsing finds different proxies in the same bytes: the fetch cache stores parsed keys and
47
- # reloads every list once when this changes (2: plain lists of "auto" sources are read as http)
48
- PARSER_VERSION = 2
47
+ # reloads every list once when this changes (2: plain lists of "auto" sources are read, 3: as http and socks5)
48
+ PARSER_VERSION = 3
49
49
 
50
50
 
51
51
  def _needs_full_regex(data: bytes) -> bool:
@@ -59,8 +59,9 @@ def extract_candidates(data: bytes, default_type: str) -> Set[RawCandidate]:
59
59
 
60
60
  Lines with a type:// prefix keep their own type; everything else gets the type of the source.
61
61
  For sources of type "auto" the plain lines only count when they are the majority: a generic proxies.txt of
62
- plain ip:port lines is read as HTTP, the most common kind (the checks sort out the rest). In a list of mostly
63
- prefixed lines, the odd plain one has an unknown type and is left out.
62
+ plain ip:port lines doesn't say its protocol, so they're tried as HTTP and as SOCKS5 (measured on 20,000 such
63
+ entries: 44 worked as http, 32 as socks5, none as both; an unreachable address fails fast for both). In a list
64
+ of mostly prefixed lines, the odd plain one has an unknown type and is left out.
64
65
  """
65
66
  out: Set[RawCandidate] = set()
66
67
  with_scheme = set()
@@ -71,8 +72,6 @@ def extract_candidates(data: bytes, default_type: str) -> Set[RawCandidate]:
71
72
  out.add((ptype, ip, port, auth))
72
73
  with_scheme.add((ip, port))
73
74
  auto = default_type == "auto"
74
- if auto:
75
- default_type = "http"
76
75
  regex = PROXY_RE if _needs_full_regex(data) else PLAIN_RE
77
76
  pairs = set(regex.findall(data))
78
77
  if b'"ip"' in data:
@@ -81,7 +80,8 @@ def extract_candidates(data: bytes, default_type: str) -> Set[RawCandidate]:
81
80
  plain = pairs - with_scheme
82
81
  if auto and len(plain) <= len(out):
83
82
  return out
84
- out.update((default_type, ip, port, b"") for ip, port in plain)
83
+ for ptype in ("http", "socks5") if auto else (default_type,):
84
+ out.update((ptype, ip, port, b"") for ip, port in plain)
85
85
  return out
86
86
 
87
87
 
@@ -0,0 +1,241 @@
1
+ """A page per proxy on the live list: what it is, how reliable it has been this week, which big sites let it
2
+ through, and the commands to try it yourself.
3
+
4
+ Only proxies that were on the list for at least half of the week's runs are indexed – thousands of pages that
5
+ come and go every hour would look like spam to search engines. The others get a page too, with noindex.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections import defaultdict
11
+ from datetime import datetime
12
+ from html import escape
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional, Sequence, Tuple
15
+
16
+ from .pages import COUNTRIES, CSS, SITE_URL, _short_name
17
+
18
+ INDEX_MIN_UPTIME = 50 # percent of the week's runs
19
+ SITE_NAMES = {"google": "Google", "reddit": "Reddit", "amazon": "Amazon"}
20
+
21
+ EXTRA_CSS = """
22
+ .wrap > * { min-width: 0; }
23
+ h1.mono { font-size: clamp(28px, 7vw, 64px); letter-spacing: -.03em; overflow-wrap: anywhere; }
24
+ .facts { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 160px), 1fr)); gap: 1px;
25
+ background: var(--line);
26
+ border: 1px solid var(--line); border-radius: 16px; overflow: hidden; margin: 0; }
27
+ .facts div { background: var(--panel); padding: 14px 16px; }
28
+ .facts dt { color: var(--muted); font-size: 13px; }
29
+ .facts dd { margin: 4px 0 0; font-size: 17px; }
30
+ h2 { font-size: 22px; letter-spacing: -.02em; margin: 0 0 12px; font-weight: 600; }
31
+ .timeline { display: flex; gap: 2px; align-items: stretch; height: 34px; }
32
+ .cell { flex: 1; min-width: 2px; border-radius: 2px; background: var(--line); }
33
+ .cell.on { background: var(--signal-ink); }
34
+ .axis { display: flex; justify-content: space-between; color: var(--muted); font-size: 13px; margin-top: 6px; }
35
+ .cmd code { white-space: pre-wrap; word-break: break-all; }
36
+ .cmd { position: relative; background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
37
+ padding: 12px 56px 12px 14px; margin: 0 0 10px; overflow-x: auto; font-size: 14px; }
38
+ .cmd button { position: absolute; top: 8px; right: 8px; font: inherit; font-size: 13px; padding: 4px 10px;
39
+ border-radius: 99px; border: 1px solid var(--line); background: var(--bg); color: var(--text);
40
+ cursor: pointer; }
41
+ .lede { overflow-wrap: anywhere; }
42
+ .note { color: var(--muted); font-size: 14px; }
43
+ .status { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); }
44
+ .status::before { content: ""; width: 9px; height: 9px; border-radius: 50%; background: var(--signal-ink); }
45
+ .status.off::before { background: var(--muted); }
46
+ """
47
+
48
+ COPY_JS = """<script>
49
+ document.querySelectorAll(".cmd button").forEach(b => b.onclick = () => {
50
+ navigator.clipboard.writeText(b.previousElementSibling.textContent).then(() => {
51
+ b.textContent = "Copied"; setTimeout(() => { b.textContent = "Copy"; }, 1500);
52
+ });
53
+ });
54
+ </script>"""
55
+
56
+
57
+ def page_path(proxy: str) -> str:
58
+ """'1.2.3.4:1080' -> 'proxy/1.2.3.4-1080/'"""
59
+ return "proxy/" + proxy.replace(":", "-") + "/"
60
+
61
+
62
+ def _country(cc: str) -> str:
63
+ return _short_name(COUNTRIES[cc]) if cc in COUNTRIES else (cc or "unknown")
64
+
65
+
66
+ def _commands(row: dict) -> List[str]:
67
+ target = "https://api.ipify.org" if row.get("https") else "http://api.ipify.org"
68
+ scheme = "socks5h" if row["ptype"] == "socks5" else row["ptype"] # socks5h: DNS through the proxy too
69
+ return [f"curl -x {scheme}://{row['proxy']} {target}",
70
+ f'python -c "import requests; print(requests.get(\'{target}\', '
71
+ f'proxies={{\'http\': \'{row["url"]}\', \'https\': \'{row["url"]}\'}}, timeout=10).text)"']
72
+
73
+
74
+ def _cmd(command: str) -> str:
75
+ return f'<div class="cmd"><code class="mono">{escape(command)}</code><button type="button">Copy</button></div>'
76
+
77
+
78
+ def _timeline(bits: Optional[int], runs: Sequence[datetime]) -> str:
79
+ """One cell per run of the week, oldest left – filled where the proxy was on the list."""
80
+ if bits is None or not runs:
81
+ return ""
82
+ bits &= (1 << len(runs)) - 1 # bits seeded from a streak can reach further back than the history
83
+ cells = "".join(f'<span class="cell{" on" if bits >> k & 1 else ""}"></span>'
84
+ for k in range(len(runs) - 1, -1, -1)) # bit 0 = the newest run
85
+ first = runs[0].strftime("%b %d, %H:%M UTC")
86
+ return (f'<section aria-label="Timeline"><h2>Every check this week</h2>'
87
+ f'<div class="timeline" role="img" aria-label="On the list in {bin(bits).count("1")} '
88
+ f'of the last {len(runs)} hourly checks">{cells}</div>'
89
+ f'<div class="axis"><span>{escape(first)}</span><span>now</span></div></section>')
90
+
91
+
92
+ def _summary(entries: List[dict]) -> dict:
93
+ """One address, several protocols: the best of each value (the page is indexed by the best uptime too)."""
94
+ def first(key):
95
+ return next((r[key] for r in entries if r.get(key) not in (None, "")), None)
96
+ uptimes = [r["uptime_7d"] for r in entries if r.get("uptime_7d") is not None]
97
+ https = [r.get("https") for r in entries]
98
+ rank = {"elite": 3, "anonymous": 2, "transparent": 1}
99
+ sites = {k for r in entries for k, ok in (r.get("sites") or {}).items() if ok}
100
+ return {
101
+ "uptime": max(uptimes) if uptimes else None,
102
+ "latency": min(r["latency"] for r in entries) if all("latency" in r for r in entries) else None,
103
+ "https": True if True in https else False if False in https else None,
104
+ "anonymity": max((r.get("anonymity") or "" for r in entries), key=lambda a: rank.get(a, 0)) or None,
105
+ "sites": [SITE_NAMES[k] for k in SITE_NAMES if k in sites],
106
+ "country": first("country") or "", "org": first("org"), "asn": first("asn"), "exit_ip": first("exit_ip"),
107
+ "hosting": first("hosting"), "blocklisted": first("blocklisted"),
108
+ "first_seen": min((r["first_seen"] for r in entries if r.get("first_seen")), default=None),
109
+ }
110
+
111
+
112
+ def _yes_no(value, yes: str, no: str) -> str:
113
+ return yes if value else no if value is False else "unknown"
114
+
115
+
116
+ def _render(proxy: str, entries: List[dict], updated: datetime, bits: Optional[int], runs: Sequence[datetime],
117
+ indexed: bool, gone: bool = False) -> str:
118
+ root = "../../"
119
+ info = _summary(entries)
120
+ urls = " · ".join(escape(r["url"]) for r in entries)
121
+ cc = info["country"]
122
+ uptime = info["uptime"]
123
+ first_seen = (info["first_seen"][:16].replace("T", " ") + " UTC") if info["first_seen"] else "unknown"
124
+ facts = [("Protocol", ", ".join(r["ptype"].upper() for r in entries)),
125
+ ("Uptime this week", f"{uptime} %" if uptime is not None else "not measured yet")]
126
+ if not gone:
127
+ facts[1:1] = [
128
+ ("Country", _country(cc)),
129
+ ("Provider", f"{info['org'] or 'unknown'}" + (f" (AS{info['asn']})" if info["asn"] else "")),
130
+ ("Latency", f"{info['latency']:,} ms" if info["latency"] is not None else "unknown"),
131
+ ("HTTPS", _yes_no(info["https"], "yes, verified TLS", "no")),
132
+ ("Anonymity", info["anonymity"] or "unknown"),
133
+ ]
134
+ facts += [
135
+ ("Gets through to", ", ".join(info["sites"]) if info["sites"] else "none of the checked sites"),
136
+ ("Datacenter exit", _yes_no(info["hosting"], "yes", "no")),
137
+ ("Spam blocklist", _yes_no(info["blocklisted"], "listed", "not listed")),
138
+ ("First seen", first_seen),
139
+ ("Exit IP", info["exit_ip"] or "unknown"),
140
+ ]
141
+ else:
142
+ facts.append(("First seen", first_seen))
143
+ facts_html = "".join(f"<div><dt>{escape(k)}</dt><dd>{escape(v)}</dd></div>" for k, v in facts)
144
+ fastest = min(entries, key=lambda r: r.get("latency") or 0)
145
+ commands = "".join(_cmd(_commands(r)[0]) for r in entries)
146
+ python = _commands(fastest)[1]
147
+ where = f" ({_country(cc)})" if cc else ""
148
+ title = f"{'/'.join(r['ptype'].upper() for r in entries)} proxy {proxy}{where}"
149
+ reliability = f", on the list in {uptime} % of this week's hourly checks" if uptime is not None else ""
150
+ description = (f"Free {', '.join(r['ptype'].upper() for r in entries)} proxy {proxy}"
151
+ f"{' in ' + _country(cc) if cc else ''}{reliability}. "
152
+ "Latency, provider, HTTPS support and commands to test it.")
153
+ robots = "" if indexed else '<meta name="robots" content="noindex">\n'
154
+ checked = updated.strftime("%Y-%m-%d %H:%M UTC")
155
+ if gone:
156
+ status = (f'<span class="status off">Didn\'t pass the last check ({escape(checked)})</span><br>'
157
+ "It was on the list for most of the week, so it may well come back.")
158
+ back = f'<a href="{root}#list">All working proxies</a>'
159
+ else:
160
+ status = f'<span class="status">Worked in the check at {escape(checked)}</span>'
161
+ back = f'<a href="{root}?country={escape(cc)}#list">All proxies in {escape(_country(cc))}</a>'
162
+ return f"""<!doctype html>
163
+ <html lang="en">
164
+ <head>
165
+ <meta charset="utf-8">
166
+ <meta name="viewport" content="width=device-width, initial-scale=1">
167
+ <title>{escape(title)} | proxy-scraper</title>
168
+ <meta name="description" content="{escape(description)}">
169
+ {robots}<link rel="canonical" href="{SITE_URL}{page_path(proxy)}">
170
+ <link rel="icon" href="{root}logo.png">
171
+ <link rel="preconnect" href="https://fonts.googleapis.com">
172
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
173
+ <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400..700&family=Geist+Mono:wght@400;500&display=swap"
174
+ rel="stylesheet">
175
+ <link rel="stylesheet" href="../style.css">
176
+ </head>
177
+ <body>
178
+ <div class="wrap">
179
+ <header>
180
+ <a class="brand" href="{root}"><img src="{root}logo.png" alt="">proxy-scraper</a>
181
+ {back}
182
+ </header>
183
+ <main>
184
+ <h1 class="mono">{escape(proxy)}</h1>
185
+ <p class="lede">{status}<br>{urls}</p>
186
+ </main>
187
+ <dl class="facts">{facts_html}</dl>
188
+ {_timeline(bits, runs)}
189
+ <section aria-label="Test it"><h2>Test it from your machine</h2>
190
+ <p class="note">Free proxies come and go, and some behave differently from your network. This prints the IP
191
+ a website sees – the proxy's, if it works.</p>
192
+ {commands}
193
+ {_cmd(python)}
194
+ <p class="note">Never send passwords or personal data through a public proxy. Its operator can read everything
195
+ that isn't encrypted.</p>
196
+ </section>
197
+ <footer><span>Checked every hour by <a href="https://github.com/maximilianfeix/proxy-scraper">proxy-scraper</a>,
198
+ open source.</span></footer>
199
+ </div>
200
+ {COPY_JS}
201
+ </body>
202
+ </html>
203
+ """
204
+
205
+
206
+ def write_proxy_pages(rows: List[dict], out: Path, updated: datetime, timelines: Dict[str, int],
207
+ runs: Sequence[datetime], gone: Optional[Dict[str, Tuple[int, int, str]]] = None) -> List[str]:
208
+ """A page per ip:port -> the paths that should be in the sitemap (the reliable ones).
209
+
210
+ timelines: url -> bits (bit 0 = this run), runs: the times of the week's runs, oldest first.
211
+ gone: url -> (bits, uptime, first_seen) of reliable proxies that missed this run – their page stays, so a
212
+ link from the sitemap doesn't turn into a 404 the first time a proxy misses a check."""
213
+ (out / "proxy").mkdir(parents=True, exist_ok=True)
214
+ (out / "proxy" / "style.css").write_text(CSS + EXTRA_CSS, encoding="utf-8") # shared: ~1,500 pages
215
+ by_address: Dict[str, List[dict]] = defaultdict(list)
216
+ for r in rows:
217
+ by_address[r["proxy"]].append(r)
218
+ missing: Dict[str, List[dict]] = defaultdict(list)
219
+ for url, (bits, uptime, first_seen) in (gone or {}).items():
220
+ ptype, _, proxy = url.partition("://")
221
+ if proxy and proxy not in by_address and uptime >= INDEX_MIN_UPTIME:
222
+ missing[proxy].append({"url": url, "ptype": ptype, "proxy": proxy, "uptime_7d": uptime,
223
+ "first_seen": first_seen, "_bits": bits})
224
+ indexed = []
225
+ for proxy, entries, is_gone in [*((p, e, False) for p, e in by_address.items()),
226
+ *((p, e, True) for p, e in missing.items())]:
227
+ entries.sort(key=lambda r: r["ptype"])
228
+ uptime = _summary(entries)["uptime"]
229
+ is_indexed = uptime is not None and uptime >= INDEX_MIN_UPTIME
230
+ bits = None
231
+ for r in entries:
232
+ b = r.get("_bits", timelines.get(r["url"]))
233
+ if b is not None:
234
+ bits = (bits or 0) | b
235
+ folder = out / page_path(proxy)
236
+ folder.mkdir(parents=True, exist_ok=True)
237
+ (folder / "index.html").write_text(_render(proxy, entries, updated, bits, runs, is_indexed, is_gone),
238
+ encoding="utf-8")
239
+ if is_indexed:
240
+ indexed.append(page_path(proxy))
241
+ return sorted(indexed)
@@ -24,6 +24,8 @@ from typing import Dict, List, Optional
24
24
  from .charts import THEMES, countries_svg, trend_svg
25
25
  from .pages import write_pages
26
26
  from .parsing import PROXY_TYPES
27
+ from .proxypages import write_proxy_pages
28
+ from .report import weekly_facts, write_report
27
29
  from .sites import SITES
28
30
 
29
31
  SKIP_EXIT_CODE = 78
@@ -294,8 +296,20 @@ def publish(run_dir: Path, out: Path, minimum: int = 20, now: Optional[datetime]
294
296
  if asset.suffix in (".html", ".png"):
295
297
  (out / asset.name).write_bytes(asset.read_bytes())
296
298
  (out / ".nojekyll").write_text("", encoding="utf-8") # Pages should serve the files unchanged
297
- write_pages(rows, out, now) # static pages per protocol and country plus sitemap.xml, for search engines
299
+ # a page per proxy (with its week as a timeline), then the pages per protocol and country plus sitemap.xml
300
+ try:
301
+ run_times = [datetime.fromisoformat(run["updated"]) for run in runs[-UPTIME_RUNS:]]
302
+ except (KeyError, TypeError, ValueError):
303
+ run_times = [] # a broken entry: no timeline rather than one whose cells don't match the times
304
+ timelines = {url: e["bits"] for url, e in listed.items()} if run_times else {}
305
+ current = {r["url"] for r in rows}
306
+ gone = {url: (e["bits"], uptime(e["bits"], runs, now, 7 * 24), e["first_seen"])
307
+ for url, e in listed.items() if url not in current} if run_times else {}
308
+ proxy_pages = write_proxy_pages(rows, out, now, timelines, run_times, gone=gone)
309
+ write_pages(rows, out, now, extra=proxy_pages)
298
310
  write_json(out / "history.json", runs)
311
+ # the weekly report: lifetimes, sites, countries – a page, markdown and a short post (report.py)
312
+ write_report(weekly_facts(rows, runs, {url: e["bits"] for url, e in listed.items()}, now), out)
299
313
  for theme in THEMES: # charts for the README, which embeds them from GitHub Pages
300
314
  (out / f"chart-{theme}.svg").write_text(trend_svg(runs, now, theme), encoding="utf-8")
301
315
  (out / f"countries-{theme}.svg").write_text(countries_svg(stats["countries"], theme), encoding="utf-8")