github-dependents-info 2.0.1__tar.gz → 2.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-dependents-info
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Collect information about dependencies between a github repo and other repositories. Results available in JSON, markdown and badges.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
37
37
  [![Dependencies Status](https://img.shields.io/badge/dependencies-up%20to%20date-brightgreen.svg)](https://github.com/nvuillam/github-dependents-info/pulls?utf8=%E2%9C%93&q=is%3Apr%20author%3Aapp%2Fdependabot)
38
38
 
39
39
  [![GitHub contributors](https://img.shields.io/github/contributors/nvuillam/github-dependents-info.svg)](https://github.com/nvuillam/github-dependents-info/graphs/contributors/)<!-- gh-dependents-info-used-by-start -->
40
- [![Generated by github-dependents-info](https://img.shields.io/static/v1?label=Used%20by&message=22&color=informational&logo=slickpic)](https://github.com/nvuillam/github-dependents-info/blob/main/docs/github-dependents-info.md)<!-- gh-dependents-info-used-by-end -->
40
+ [![Generated by github-dependents-info](https://img.shields.io/static/v1?label=Used%20by&message=44&color=informational&logo=slickpic)](https://github.com/nvuillam/github-dependents-info/blob/main/docs/github-dependents-info.md)<!-- gh-dependents-info-used-by-end -->
41
41
  [![GitHub Sponsors](https://img.shields.io/github/sponsors/nvuillam)](https://github.com/sponsors/nvuillam)
42
42
  [![MegaLinter](https://github.com/nvuillam/github-dependents-info/actions/workflows/mega-linter.yml/badge.svg)](https://github.com/nvuillam/github-dependents-info/actions/workflows/mega-linter.yml)
43
43
  [![License](https://img.shields.io/github/license/nvuillam/github-dependents-info)](https://github.com/nvuillam/github-dependents-info/blob/master/LICENSE)
@@ -10,7 +10,7 @@
10
10
  [![Dependencies Status](https://img.shields.io/badge/dependencies-up%20to%20date-brightgreen.svg)](https://github.com/nvuillam/github-dependents-info/pulls?utf8=%E2%9C%93&q=is%3Apr%20author%3Aapp%2Fdependabot)
11
11
 
12
12
  [![GitHub contributors](https://img.shields.io/github/contributors/nvuillam/github-dependents-info.svg)](https://github.com/nvuillam/github-dependents-info/graphs/contributors/)<!-- gh-dependents-info-used-by-start -->
13
- [![Generated by github-dependents-info](https://img.shields.io/static/v1?label=Used%20by&message=22&color=informational&logo=slickpic)](https://github.com/nvuillam/github-dependents-info/blob/main/docs/github-dependents-info.md)<!-- gh-dependents-info-used-by-end -->
13
+ [![Generated by github-dependents-info](https://img.shields.io/static/v1?label=Used%20by&message=44&color=informational&logo=slickpic)](https://github.com/nvuillam/github-dependents-info/blob/main/docs/github-dependents-info.md)<!-- gh-dependents-info-used-by-end -->
14
14
  [![GitHub Sponsors](https://img.shields.io/github/sponsors/nvuillam)](https://github.com/sponsors/nvuillam)
15
15
  [![MegaLinter](https://github.com/nvuillam/github-dependents-info/actions/workflows/mega-linter.yml/badge.svg)](https://github.com/nvuillam/github-dependents-info/actions/workflows/mega-linter.yml)
16
16
  [![License](https://img.shields.io/github/license/nvuillam/github-dependents-info)](https://github.com/nvuillam/github-dependents-info/blob/master/LICENSE)
@@ -50,6 +50,10 @@ class GithubDependentsInfo:
50
50
  self.badges = {}
51
51
  self.result = {}
52
52
  self.time_delay = options["time_delay"] if "time_delay" in options else 0.1
53
+ self.http_retry_attempts = options.get("http_retry_attempts", 5)
54
+ self.http_retry_initial_delay = options.get("http_retry_initial_delay", max(self.time_delay, 1.0))
55
+ self.http_retry_backoff = options.get("http_retry_backoff", 2.0)
56
+ self.http_retry_max_delay = options.get("http_retry_max_delay", 60.0)
53
57
 
54
58
  def collect(self):
55
59
  """Main entry point - synchronous wrapper for async collection."""
@@ -627,13 +631,64 @@ class GithubDependentsInfo:
627
631
  follow_redirects=True,
628
632
  )
629
633
 
634
+ def _compute_retry_delay(self, attempt: int, response: httpx.Response | None = None) -> float:
635
+ """Calculate delay before next retry using headers or exponential backoff."""
636
+ if response is not None:
637
+ retry_after = response.headers.get("Retry-After")
638
+ if retry_after:
639
+ try:
640
+ wait_seconds = float(retry_after)
641
+ if wait_seconds > 0:
642
+ return min(wait_seconds, self.http_retry_max_delay)
643
+ except ValueError:
644
+ pass
645
+ delay = self.http_retry_initial_delay * (self.http_retry_backoff ** max(attempt - 1, 0))
646
+ return min(delay, self.http_retry_max_delay)
647
+
630
648
  async def fetch_page(self, client, url, semaphore):
631
649
  """Fetch a single page with rate limiting."""
632
- async with semaphore:
633
- await asyncio.sleep(self.time_delay)
634
- response = await client.get(url)
635
- response.raise_for_status()
636
- return response.text
650
+ last_error = None
651
+ for attempt in range(1, self.http_retry_attempts + 1):
652
+ try:
653
+ async with semaphore:
654
+ await asyncio.sleep(self.time_delay)
655
+ response = await client.get(url)
656
+ response.raise_for_status()
657
+ return response.text
658
+ except httpx.HTTPStatusError as exc: # type: ignore[attr-defined]
659
+ last_error = exc
660
+ status_code = exc.response.status_code
661
+ should_retry = status_code == 429 or 500 <= status_code < 600
662
+ if not should_retry or attempt == self.http_retry_attempts:
663
+ raise
664
+ delay = self._compute_retry_delay(attempt, response=exc.response)
665
+ if self.debug:
666
+ logging.warning(
667
+ "HTTP %s while fetching %s (attempt %s/%s). Retrying in %.1fs",
668
+ status_code,
669
+ url,
670
+ attempt,
671
+ self.http_retry_attempts,
672
+ delay,
673
+ )
674
+ await asyncio.sleep(delay)
675
+ except (httpx.RequestError, httpx.TimeoutException) as exc: # type: ignore[attr-defined]
676
+ last_error = exc
677
+ if attempt == self.http_retry_attempts:
678
+ raise
679
+ delay = self._compute_retry_delay(attempt)
680
+ if self.debug:
681
+ logging.warning(
682
+ "Request error while fetching %s (attempt %s/%s): %s. Retrying in %.1fs",
683
+ url,
684
+ attempt,
685
+ self.http_retry_attempts,
686
+ exc,
687
+ delay,
688
+ )
689
+ await asyncio.sleep(delay)
690
+ if last_error is not None:
691
+ raise last_error
637
692
 
638
693
  async def fetch_all_package_pages(self, client, package):
639
694
  """Fetch all pages for a package in parallel."""
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
5
5
 
6
6
  [project]
7
7
  name = "github-dependents-info"
8
- version = "2.0.1"
8
+ version = "2.0.2"
9
9
  description = "Collect information about dependencies between a github repo and other repositories. Results available in JSON, markdown and badges."
10
10
  readme = "README.md"
11
11
  license = "MIT"