github-backup 0.61.2__py3-none-any.whl → 0.61.4__py3-none-any.whl

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.
github_backup/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.61.2"
1
+ __version__ = "0.61.4"
@@ -39,11 +39,11 @@ logger = logging.getLogger(__name__)
39
39
 
40
40
 
41
41
  class RepositoryUnavailableError(Exception):
42
- """Raised when a repository is unavailable due to legal reasons (e.g., DMCA takedown)."""
42
+ """Raised when a repository is unavailable due to legal reasons (e.g., DMCA takedown, TOS violation)."""
43
43
 
44
- def __init__(self, message, dmca_url=None):
44
+ def __init__(self, message, legal_url=None):
45
45
  super().__init__(message)
46
- self.dmca_url = dmca_url
46
+ self.legal_url = legal_url
47
47
 
48
48
 
49
49
  # Setup SSL context with fallback chain
@@ -647,6 +647,14 @@ def retrieve_data(args, template, query_args=None, paginated=True):
647
647
  return None
648
648
 
649
649
  def fetch_all() -> Generator[dict, None, None]:
650
+ def _extract_legal_url(response_body_bytes):
651
+ """Extract DMCA/legal notice URL from GitHub API error response body."""
652
+ try:
653
+ data = json.loads(response_body_bytes.decode("utf-8"))
654
+ return data.get("block", {}).get("html_url")
655
+ except Exception:
656
+ return None
657
+
650
658
  next_url = None
651
659
 
652
660
  while True:
@@ -661,47 +669,66 @@ def retrieve_data(args, template, query_args=None, paginated=True):
661
669
  as_app=args.as_app,
662
670
  fine=args.token_fine is not None,
663
671
  )
664
- http_response = make_request_with_retry(request, auth, args.max_retries)
665
-
666
- match http_response.getcode():
667
- case 200:
668
- # Success - Parse JSON response
669
- try:
670
- response = json.loads(http_response.read().decode("utf-8"))
671
- break # Exit retry loop and handle the data returned
672
- except (
673
- IncompleteRead,
674
- json.decoder.JSONDecodeError,
675
- TimeoutError,
676
- ) as e:
677
- logger.warning(f"{type(e).__name__} reading response")
678
- if attempt < args.max_retries:
679
- delay = calculate_retry_delay(attempt, {})
680
- logger.warning(
681
- f"Retrying read in {delay:.1f}s (attempt {attempt + 1}/{args.max_retries + 1})"
682
- )
683
- time.sleep(delay)
684
- continue # Next retry attempt
685
-
686
- case 451:
687
- # DMCA takedown - extract URL if available, then raise
688
- dmca_url = None
689
- try:
690
- response_data = json.loads(
691
- http_response.read().decode("utf-8")
692
- )
693
- dmca_url = response_data.get("block", {}).get("html_url")
694
- except Exception:
695
- pass
672
+ try:
673
+ http_response = make_request_with_retry(
674
+ request, auth, args.max_retries
675
+ )
676
+ except HTTPError as exc:
677
+ if exc.code == 451:
678
+ legal_url = _extract_legal_url(exc.read())
696
679
  raise RepositoryUnavailableError(
697
- "Repository unavailable due to legal reasons (HTTP 451)",
698
- dmca_url=dmca_url,
680
+ f"Repository unavailable due to legal reasons (HTTP {exc.code})",
681
+ legal_url=legal_url,
699
682
  )
683
+ elif exc.code == 403:
684
+ # Rate-limit 403s (x-ratelimit-remaining=0) are retried
685
+ # by make_request_with_retry — re-raise if exhausted.
686
+ if int(exc.headers.get("x-ratelimit-remaining", 1)) < 1:
687
+ raise
688
+ # Only convert to RepositoryUnavailableError if GitHub
689
+ # indicates a TOS/DMCA block (response contains "block"
690
+ # key). Other 403s (permissions, scopes) should propagate.
691
+ body = exc.read()
692
+ try:
693
+ data = json.loads(body.decode("utf-8"))
694
+ except Exception:
695
+ data = {}
696
+ if "block" in data:
697
+ raise RepositoryUnavailableError(
698
+ "Repository access blocked (HTTP 403)",
699
+ legal_url=data.get("block", {}).get("html_url"),
700
+ )
701
+ raise
702
+ else:
703
+ raise
704
+
705
+ # urlopen raises HTTPError for non-2xx, so only success gets here.
706
+ # Guard against unexpected status codes from proxies, future Python
707
+ # changes, or other edge cases we haven't considered.
708
+ status = http_response.getcode()
709
+ if status != 200:
710
+ raise Exception(
711
+ f"Unexpected HTTP {status} from {next_url or template} "
712
+ f"(expected non-2xx to raise HTTPError)"
713
+ )
700
714
 
701
- case _:
702
- raise Exception(
703
- f"API request returned HTTP {http_response.getcode()}: {http_response.reason}"
715
+ # Parse JSON response
716
+ try:
717
+ response = json.loads(http_response.read().decode("utf-8"))
718
+ break # Exit retry loop and handle the data returned
719
+ except (
720
+ IncompleteRead,
721
+ json.decoder.JSONDecodeError,
722
+ TimeoutError,
723
+ ) as e:
724
+ logger.warning(f"{type(e).__name__} reading response")
725
+ if attempt < args.max_retries:
726
+ delay = calculate_retry_delay(attempt, {})
727
+ logger.warning(
728
+ f"Retrying read in {delay:.1f}s (attempt {attempt + 1}/{args.max_retries + 1})"
704
729
  )
730
+ time.sleep(delay)
731
+ continue # Next retry attempt
705
732
  else:
706
733
  logger.error(
707
734
  f"Failed to read response after {args.max_retries + 1} attempts for {next_url or template}"
@@ -1614,7 +1641,13 @@ def retrieve_repositories(args, authenticated_user):
1614
1641
  paginated = False
1615
1642
  template = "https://{0}/repos/{1}".format(get_github_api_host(args), repo_path)
1616
1643
 
1617
- repos = retrieve_data(args, template, paginated=paginated)
1644
+ try:
1645
+ repos = retrieve_data(args, template, paginated=paginated)
1646
+ except RepositoryUnavailableError as e:
1647
+ logger.warning(f"Repository is unavailable: {e}")
1648
+ if e.legal_url:
1649
+ logger.warning(f"Legal notice: {e.legal_url}")
1650
+ return []
1618
1651
 
1619
1652
  if args.all_starred:
1620
1653
  starred_template = "https://{0}/users/{1}/starred".format(
@@ -1814,7 +1847,7 @@ def backup_repositories(args, output_directory, repositories):
1814
1847
  if args.include_milestones or args.include_everything:
1815
1848
  backup_milestones(args, repo_cwd, repository, repos_template)
1816
1849
 
1817
- if args.include_security_advisories or (args.include_everything and not repository["Private"]):
1850
+ if args.include_security_advisories or (args.include_everything and not repository.get("private", False)):
1818
1851
  backup_security_advisories(args, repo_cwd, repository, repos_template)
1819
1852
 
1820
1853
  if args.include_labels or args.include_everything:
@@ -1832,11 +1865,9 @@ def backup_repositories(args, output_directory, repositories):
1832
1865
  include_assets=args.include_assets or args.include_everything,
1833
1866
  )
1834
1867
  except RepositoryUnavailableError as e:
1835
- logger.warning(
1836
- f"Repository {repository['full_name']} is unavailable (HTTP 451)"
1837
- )
1838
- if e.dmca_url:
1839
- logger.warning(f"DMCA notice: {e.dmca_url}")
1868
+ logger.warning(f"Repository {repository['full_name']} is unavailable: {e}")
1869
+ if e.legal_url:
1870
+ logger.warning(f"Legal notice: {e.legal_url}")
1840
1871
  logger.info(f"Skipping remaining resources for {repository['full_name']}")
1841
1872
  continue
1842
1873
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-backup
3
- Version: 0.61.2
3
+ Version: 0.61.4
4
4
  Summary: backup a github user or organization
5
5
  Home-page: http://github.com/josegonzalez/python-github-backup
6
6
  Author: Jose Diaz-Gonzalez
@@ -0,0 +1,11 @@
1
+ github_backup/__init__.py,sha256=VMvQ0rdXe4KAeYeD4bUvv9UidVKRdpVBjEyznofRaLM,23
2
+ github_backup/__main__.py,sha256=3yorCiC63maaxbjdtaeAs-G-yAAxKSO3mprhGfa1Fho,271
3
+ github_backup/cli.py,sha256=gPYU0CviuHB19y8RHhcD0p8xHS3UcJbBIK6HVfWazTc,2902
4
+ github_backup/github_backup.py,sha256=xUaSVpzl3Q0rKq09fLUY5__fdGf8W3lVJk58hujF9_s,89685
5
+ github_backup-0.61.4.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
6
+ github_backup-0.61.4.dist-info/METADATA,sha256=zPCSXw4tx3cbEGyFb3uQQ02dzc2ww6R-1pOQ073s1Sc,28144
7
+ github_backup-0.61.4.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
8
+ github_backup-0.61.4.dist-info/entry_points.txt,sha256=cLPKK7MqC77QVb6HnT2mnyL_f-ciavJG_Y9vVHlCF68,57
9
+ github_backup-0.61.4.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
10
+ github_backup-0.61.4.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
+ github_backup-0.61.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (82.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,11 +0,0 @@
1
- github_backup/__init__.py,sha256=-WIJ_hJF1yvHvtvBO1JNouMIkdi1zl3hnbvBpOOoJXM,23
2
- github_backup/__main__.py,sha256=3yorCiC63maaxbjdtaeAs-G-yAAxKSO3mprhGfa1Fho,271
3
- github_backup/cli.py,sha256=gPYU0CviuHB19y8RHhcD0p8xHS3UcJbBIK6HVfWazTc,2902
4
- github_backup/github_backup.py,sha256=UJwLQ3NU4TVxmviFVRGX9bC7NPUR-hE3zjpmzEYnzUI,88140
5
- github_backup-0.61.2.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
6
- github_backup-0.61.2.dist-info/METADATA,sha256=TAUA8FnuarPk__xgEBOd5OaNKdV0QPxIFBSxneg0p7s,28144
7
- github_backup-0.61.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- github_backup-0.61.2.dist-info/entry_points.txt,sha256=cLPKK7MqC77QVb6HnT2mnyL_f-ciavJG_Y9vVHlCF68,57
9
- github_backup-0.61.2.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
10
- github_backup-0.61.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
- github_backup-0.61.2.dist-info/RECORD,,