github-backup 0.59.0__py3-none-any.whl → 0.60.0__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 +48 -24
- {github_backup-0.59.0.dist-info → github_backup-0.60.0.dist-info}/METADATA +3 -2
- github_backup-0.60.0.dist-info/RECORD +11 -0
- github_backup-0.59.0.dist-info/RECORD +0 -11
- {github_backup-0.59.0.dist-info → github_backup-0.60.0.dist-info}/WHEEL +0 -0
- {github_backup-0.59.0.dist-info → github_backup-0.60.0.dist-info}/entry_points.txt +0 -0
- {github_backup-0.59.0.dist-info → github_backup-0.60.0.dist-info}/licenses/LICENSE.txt +0 -0
- {github_backup-0.59.0.dist-info → github_backup-0.60.0.dist-info}/top_level.txt +0 -0
- {github_backup-0.59.0.dist-info → github_backup-0.60.0.dist-info}/zip-safe +0 -0
github_backup/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.60.0"
|
github_backup/github_backup.py
CHANGED
|
@@ -74,9 +74,6 @@ else:
|
|
|
74
74
|
" 3. Debian/Ubuntu: apt-get install ca-certificates\n\n"
|
|
75
75
|
)
|
|
76
76
|
|
|
77
|
-
# Retry configuration
|
|
78
|
-
MAX_RETRIES = 5
|
|
79
|
-
|
|
80
77
|
|
|
81
78
|
def logging_subprocess(
|
|
82
79
|
popenargs, stdout_log_level=logging.DEBUG, stderr_log_level=logging.ERROR, **kwargs
|
|
@@ -144,6 +141,17 @@ def mask_password(url, secret="*****"):
|
|
|
144
141
|
return url.replace(parsed.password, secret)
|
|
145
142
|
|
|
146
143
|
|
|
144
|
+
def non_negative_int(value):
|
|
145
|
+
"""Argparse type validator for non-negative integers."""
|
|
146
|
+
try:
|
|
147
|
+
ivalue = int(value)
|
|
148
|
+
except ValueError:
|
|
149
|
+
raise argparse.ArgumentTypeError(f"'{value}' is not a valid integer")
|
|
150
|
+
if ivalue < 0:
|
|
151
|
+
raise argparse.ArgumentTypeError(f"{value} must be 0 or greater")
|
|
152
|
+
return ivalue
|
|
153
|
+
|
|
154
|
+
|
|
147
155
|
def parse_args(args=None):
|
|
148
156
|
parser = argparse.ArgumentParser(description="Backup a github account")
|
|
149
157
|
parser.add_argument("user", metavar="USER", type=str, help="github username")
|
|
@@ -468,6 +476,13 @@ def parse_args(args=None):
|
|
|
468
476
|
parser.add_argument(
|
|
469
477
|
"--exclude", dest="exclude", help="names of repositories to exclude", nargs="*"
|
|
470
478
|
)
|
|
479
|
+
parser.add_argument(
|
|
480
|
+
"--retries",
|
|
481
|
+
dest="max_retries",
|
|
482
|
+
type=non_negative_int,
|
|
483
|
+
default=5,
|
|
484
|
+
help="maximum number of retries for API calls (default: 5)",
|
|
485
|
+
)
|
|
471
486
|
return parser.parse_args(args)
|
|
472
487
|
|
|
473
488
|
|
|
@@ -622,7 +637,7 @@ def retrieve_data(args, template, query_args=None, paginated=True):
|
|
|
622
637
|
def _extract_next_page_url(link_header):
|
|
623
638
|
for link in link_header.split(","):
|
|
624
639
|
if 'rel="next"' in link:
|
|
625
|
-
return link[link.find("<") + 1:link.find(">")]
|
|
640
|
+
return link[link.find("<") + 1 : link.find(">")]
|
|
626
641
|
return None
|
|
627
642
|
|
|
628
643
|
def fetch_all() -> Generator[dict, None, None]:
|
|
@@ -631,7 +646,7 @@ def retrieve_data(args, template, query_args=None, paginated=True):
|
|
|
631
646
|
while True:
|
|
632
647
|
# FIRST: Fetch response
|
|
633
648
|
|
|
634
|
-
for attempt in range(
|
|
649
|
+
for attempt in range(args.max_retries + 1):
|
|
635
650
|
request = _construct_request(
|
|
636
651
|
per_page=per_page if paginated else None,
|
|
637
652
|
query_args=query_args,
|
|
@@ -640,7 +655,7 @@ def retrieve_data(args, template, query_args=None, paginated=True):
|
|
|
640
655
|
as_app=args.as_app,
|
|
641
656
|
fine=args.token_fine is not None,
|
|
642
657
|
)
|
|
643
|
-
http_response = make_request_with_retry(request, auth)
|
|
658
|
+
http_response = make_request_with_retry(request, auth, args.max_retries)
|
|
644
659
|
|
|
645
660
|
match http_response.getcode():
|
|
646
661
|
case 200:
|
|
@@ -654,10 +669,10 @@ def retrieve_data(args, template, query_args=None, paginated=True):
|
|
|
654
669
|
TimeoutError,
|
|
655
670
|
) as e:
|
|
656
671
|
logger.warning(f"{type(e).__name__} reading response")
|
|
657
|
-
if attempt <
|
|
672
|
+
if attempt < args.max_retries:
|
|
658
673
|
delay = calculate_retry_delay(attempt, {})
|
|
659
674
|
logger.warning(
|
|
660
|
-
f"Retrying in {delay:.1f}s (attempt {attempt + 1}/{
|
|
675
|
+
f"Retrying read in {delay:.1f}s (attempt {attempt + 1}/{args.max_retries + 1})"
|
|
661
676
|
)
|
|
662
677
|
time.sleep(delay)
|
|
663
678
|
continue # Next retry attempt
|
|
@@ -683,10 +698,10 @@ def retrieve_data(args, template, query_args=None, paginated=True):
|
|
|
683
698
|
)
|
|
684
699
|
else:
|
|
685
700
|
logger.error(
|
|
686
|
-
f"Failed to read response after {
|
|
701
|
+
f"Failed to read response after {args.max_retries + 1} attempts for {next_url or template}"
|
|
687
702
|
)
|
|
688
703
|
raise Exception(
|
|
689
|
-
f"Failed to read response after {
|
|
704
|
+
f"Failed to read response after {args.max_retries + 1} attempts for {next_url or template}"
|
|
690
705
|
)
|
|
691
706
|
|
|
692
707
|
# SECOND: Process and paginate
|
|
@@ -718,7 +733,7 @@ def retrieve_data(args, template, query_args=None, paginated=True):
|
|
|
718
733
|
return list(fetch_all())
|
|
719
734
|
|
|
720
735
|
|
|
721
|
-
def make_request_with_retry(request, auth):
|
|
736
|
+
def make_request_with_retry(request, auth, max_retries=5):
|
|
722
737
|
"""Make HTTP request with automatic retry for transient errors."""
|
|
723
738
|
|
|
724
739
|
def is_retryable_status(status_code, headers):
|
|
@@ -730,40 +745,49 @@ def make_request_with_retry(request, auth):
|
|
|
730
745
|
return int(headers.get("x-ratelimit-remaining", 1)) < 1
|
|
731
746
|
return False
|
|
732
747
|
|
|
733
|
-
for attempt in range(
|
|
748
|
+
for attempt in range(max_retries + 1):
|
|
734
749
|
try:
|
|
735
750
|
return urlopen(request, context=https_ctx)
|
|
736
751
|
|
|
737
752
|
except HTTPError as exc:
|
|
738
753
|
# HTTPError can be used as a response-like object
|
|
739
754
|
if not is_retryable_status(exc.code, exc.headers):
|
|
755
|
+
logger.error(
|
|
756
|
+
f"API Error: {exc.code} {exc.reason} for {request.full_url}"
|
|
757
|
+
)
|
|
740
758
|
raise # Non-retryable error
|
|
741
759
|
|
|
742
|
-
if attempt >=
|
|
743
|
-
logger.error(
|
|
760
|
+
if attempt >= max_retries:
|
|
761
|
+
logger.error(
|
|
762
|
+
f"HTTP {exc.code} failed after {max_retries + 1} attempts for {request.full_url}"
|
|
763
|
+
)
|
|
744
764
|
raise
|
|
745
765
|
|
|
746
766
|
delay = calculate_retry_delay(attempt, exc.headers)
|
|
747
767
|
logger.warning(
|
|
748
|
-
f"HTTP {exc.code}, retrying in {delay:.1f}s "
|
|
749
|
-
f"(attempt {attempt + 1}/{
|
|
768
|
+
f"HTTP {exc.code} ({exc.reason}), retrying in {delay:.1f}s "
|
|
769
|
+
f"(attempt {attempt + 1}/{max_retries + 1}) for {request.full_url}"
|
|
750
770
|
)
|
|
751
771
|
if auth is None and exc.code in (403, 429):
|
|
752
772
|
logger.info("Hint: Authenticate to raise your GitHub rate limit")
|
|
753
773
|
time.sleep(delay)
|
|
754
774
|
|
|
755
775
|
except (URLError, socket.error) as e:
|
|
756
|
-
if attempt >=
|
|
757
|
-
logger.error(
|
|
776
|
+
if attempt >= max_retries:
|
|
777
|
+
logger.error(
|
|
778
|
+
f"Connection error failed after {max_retries + 1} attempts: {e} for {request.full_url}"
|
|
779
|
+
)
|
|
758
780
|
raise
|
|
759
781
|
delay = calculate_retry_delay(attempt, {})
|
|
760
782
|
logger.warning(
|
|
761
783
|
f"Connection error: {e}, retrying in {delay:.1f}s "
|
|
762
|
-
f"(attempt {attempt + 1}/{
|
|
784
|
+
f"(attempt {attempt + 1}/{max_retries + 1}) for {request.full_url}"
|
|
763
785
|
)
|
|
764
786
|
time.sleep(delay)
|
|
765
787
|
|
|
766
|
-
raise Exception(
|
|
788
|
+
raise Exception(
|
|
789
|
+
f"Request failed after {max_retries + 1} attempts"
|
|
790
|
+
) # pragma: no cover
|
|
767
791
|
|
|
768
792
|
|
|
769
793
|
def _construct_request(per_page, query_args, template, auth, as_app=None, fine=False):
|
|
@@ -1579,9 +1603,7 @@ def filter_repositories(args, unfiltered_repositories):
|
|
|
1579
1603
|
repositories = [r for r in repositories if not r.get("archived")]
|
|
1580
1604
|
if args.starred_skip_size_over is not None:
|
|
1581
1605
|
if args.starred_skip_size_over <= 0:
|
|
1582
|
-
logger.warning(
|
|
1583
|
-
"--starred-skip-size-over must be greater than 0, ignoring"
|
|
1584
|
-
)
|
|
1606
|
+
logger.warning("--starred-skip-size-over must be greater than 0, ignoring")
|
|
1585
1607
|
else:
|
|
1586
1608
|
size_limit_kb = args.starred_skip_size_over * 1024
|
|
1587
1609
|
filtered = []
|
|
@@ -1590,7 +1612,9 @@ def filter_repositories(args, unfiltered_repositories):
|
|
|
1590
1612
|
size_mb = r.get("size", 0) / 1024
|
|
1591
1613
|
logger.info(
|
|
1592
1614
|
"Skipping starred repo {0} ({1:.0f} MB) due to --starred-skip-size-over {2}".format(
|
|
1593
|
-
r.get("full_name", r.get("name")),
|
|
1615
|
+
r.get("full_name", r.get("name")),
|
|
1616
|
+
size_mb,
|
|
1617
|
+
args.starred_skip_size_over,
|
|
1594
1618
|
)
|
|
1595
1619
|
)
|
|
1596
1620
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.60.0
|
|
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
|
|
@@ -182,7 +182,8 @@ CLI Help output::
|
|
|
182
182
|
--throttle-limit to be set)
|
|
183
183
|
--exclude [EXCLUDE ...]
|
|
184
184
|
names of repositories to exclude
|
|
185
|
-
|
|
185
|
+
--retries MAX_RETRIES
|
|
186
|
+
maximum number of retries for API calls (default: 5)
|
|
186
187
|
|
|
187
188
|
Usage Details
|
|
188
189
|
=============
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
github_backup/__init__.py,sha256=XCDU0PzRSGNchH5cFRqniZdytnPgXmacSgzeC0BxXiI,23
|
|
2
|
+
github_backup/__main__.py,sha256=3yorCiC63maaxbjdtaeAs-G-yAAxKSO3mprhGfa1Fho,271
|
|
3
|
+
github_backup/cli.py,sha256=ifnOne5AaEI7laofgIq3geURJtvbA-bzxSQgLcmZgd0,2345
|
|
4
|
+
github_backup/github_backup.py,sha256=DJ1mFWELLAZBbM8UXmsHOxin3OBGFIFZk_SXQdhv_7w,82562
|
|
5
|
+
github_backup-0.60.0.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
|
|
6
|
+
github_backup-0.60.0.dist-info/METADATA,sha256=Lwu1h4UYm6z8OUq9iieqgS_7ITlsB-oQujHB0h302PM,26529
|
|
7
|
+
github_backup-0.60.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
github_backup-0.60.0.dist-info/entry_points.txt,sha256=cLPKK7MqC77QVb6HnT2mnyL_f-ciavJG_Y9vVHlCF68,57
|
|
9
|
+
github_backup-0.60.0.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
|
|
10
|
+
github_backup-0.60.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
11
|
+
github_backup-0.60.0.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
github_backup/__init__.py,sha256=HMyZ2jNndR4_RS4EcSAHrVoKBifJVKSLuqmMuKnl_JI,23
|
|
2
|
-
github_backup/__main__.py,sha256=3yorCiC63maaxbjdtaeAs-G-yAAxKSO3mprhGfa1Fho,271
|
|
3
|
-
github_backup/cli.py,sha256=ifnOne5AaEI7laofgIq3geURJtvbA-bzxSQgLcmZgd0,2345
|
|
4
|
-
github_backup/github_backup.py,sha256=VkUaqxAMT3ooQ4xGp54xpUtAJL4h4zZ8b_-EtrFi2T4,81615
|
|
5
|
-
github_backup-0.59.0.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
|
|
6
|
-
github_backup-0.59.0.dist-info/METADATA,sha256=_IyD8iBRAXp-Kkz1Ru0pkwjzVNDECjB6jKN6IR3n-Sk,26421
|
|
7
|
-
github_backup-0.59.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
github_backup-0.59.0.dist-info/entry_points.txt,sha256=cLPKK7MqC77QVb6HnT2mnyL_f-ciavJG_Y9vVHlCF68,57
|
|
9
|
-
github_backup-0.59.0.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
|
|
10
|
-
github_backup-0.59.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
11
|
-
github_backup-0.59.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|