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 +1 -1
- github_backup/github_backup.py +78 -47
- {github_backup-0.61.2.dist-info → github_backup-0.61.4.dist-info}/METADATA +1 -1
- github_backup-0.61.4.dist-info/RECORD +11 -0
- {github_backup-0.61.2.dist-info → github_backup-0.61.4.dist-info}/WHEEL +1 -1
- github_backup-0.61.2.dist-info/RECORD +0 -11
- {github_backup-0.61.2.dist-info → github_backup-0.61.4.dist-info}/entry_points.txt +0 -0
- {github_backup-0.61.2.dist-info → github_backup-0.61.4.dist-info}/licenses/LICENSE.txt +0 -0
- {github_backup-0.61.2.dist-info → github_backup-0.61.4.dist-info}/top_level.txt +0 -0
- {github_backup-0.61.2.dist-info → github_backup-0.61.4.dist-info}/zip-safe +0 -0
github_backup/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.61.
|
|
1
|
+
__version__ = "0.61.4"
|
github_backup/github_backup.py
CHANGED
|
@@ -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,
|
|
44
|
+
def __init__(self, message, legal_url=None):
|
|
45
45
|
super().__init__(message)
|
|
46
|
-
self.
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
|
698
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -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,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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|