github-backup 0.59.0__py3-none-any.whl → 0.61.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 +92 -24
- {github_backup-0.59.0.dist-info → github_backup-0.61.0.dist-info}/METADATA +10 -7
- github_backup-0.61.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.61.0.dist-info}/WHEEL +0 -0
- {github_backup-0.59.0.dist-info → github_backup-0.61.0.dist-info}/entry_points.txt +0 -0
- {github_backup-0.59.0.dist-info → github_backup-0.61.0.dist-info}/licenses/LICENSE.txt +0 -0
- {github_backup-0.59.0.dist-info → github_backup-0.61.0.dist-info}/top_level.txt +0 -0
- {github_backup-0.59.0.dist-info → github_backup-0.61.0.dist-info}/zip-safe +0 -0
github_backup/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.61.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")
|
|
@@ -302,6 +310,12 @@ def parse_args(args=None):
|
|
|
302
310
|
dest="include_milestones",
|
|
303
311
|
help="include milestones in backup",
|
|
304
312
|
)
|
|
313
|
+
parser.add_argument(
|
|
314
|
+
"--security-advisories",
|
|
315
|
+
action="store_true",
|
|
316
|
+
dest="include_security_advisories",
|
|
317
|
+
help="include security advisories in backup",
|
|
318
|
+
)
|
|
305
319
|
parser.add_argument(
|
|
306
320
|
"--repositories",
|
|
307
321
|
action="store_true",
|
|
@@ -468,6 +482,13 @@ def parse_args(args=None):
|
|
|
468
482
|
parser.add_argument(
|
|
469
483
|
"--exclude", dest="exclude", help="names of repositories to exclude", nargs="*"
|
|
470
484
|
)
|
|
485
|
+
parser.add_argument(
|
|
486
|
+
"--retries",
|
|
487
|
+
dest="max_retries",
|
|
488
|
+
type=non_negative_int,
|
|
489
|
+
default=5,
|
|
490
|
+
help="maximum number of retries for API calls (default: 5)",
|
|
491
|
+
)
|
|
471
492
|
return parser.parse_args(args)
|
|
472
493
|
|
|
473
494
|
|
|
@@ -622,7 +643,7 @@ def retrieve_data(args, template, query_args=None, paginated=True):
|
|
|
622
643
|
def _extract_next_page_url(link_header):
|
|
623
644
|
for link in link_header.split(","):
|
|
624
645
|
if 'rel="next"' in link:
|
|
625
|
-
return link[link.find("<") + 1:link.find(">")]
|
|
646
|
+
return link[link.find("<") + 1 : link.find(">")]
|
|
626
647
|
return None
|
|
627
648
|
|
|
628
649
|
def fetch_all() -> Generator[dict, None, None]:
|
|
@@ -631,7 +652,7 @@ def retrieve_data(args, template, query_args=None, paginated=True):
|
|
|
631
652
|
while True:
|
|
632
653
|
# FIRST: Fetch response
|
|
633
654
|
|
|
634
|
-
for attempt in range(
|
|
655
|
+
for attempt in range(args.max_retries + 1):
|
|
635
656
|
request = _construct_request(
|
|
636
657
|
per_page=per_page if paginated else None,
|
|
637
658
|
query_args=query_args,
|
|
@@ -640,7 +661,7 @@ def retrieve_data(args, template, query_args=None, paginated=True):
|
|
|
640
661
|
as_app=args.as_app,
|
|
641
662
|
fine=args.token_fine is not None,
|
|
642
663
|
)
|
|
643
|
-
http_response = make_request_with_retry(request, auth)
|
|
664
|
+
http_response = make_request_with_retry(request, auth, args.max_retries)
|
|
644
665
|
|
|
645
666
|
match http_response.getcode():
|
|
646
667
|
case 200:
|
|
@@ -654,10 +675,10 @@ def retrieve_data(args, template, query_args=None, paginated=True):
|
|
|
654
675
|
TimeoutError,
|
|
655
676
|
) as e:
|
|
656
677
|
logger.warning(f"{type(e).__name__} reading response")
|
|
657
|
-
if attempt <
|
|
678
|
+
if attempt < args.max_retries:
|
|
658
679
|
delay = calculate_retry_delay(attempt, {})
|
|
659
680
|
logger.warning(
|
|
660
|
-
f"Retrying in {delay:.1f}s (attempt {attempt + 1}/{
|
|
681
|
+
f"Retrying read in {delay:.1f}s (attempt {attempt + 1}/{args.max_retries + 1})"
|
|
661
682
|
)
|
|
662
683
|
time.sleep(delay)
|
|
663
684
|
continue # Next retry attempt
|
|
@@ -683,10 +704,10 @@ def retrieve_data(args, template, query_args=None, paginated=True):
|
|
|
683
704
|
)
|
|
684
705
|
else:
|
|
685
706
|
logger.error(
|
|
686
|
-
f"Failed to read response after {
|
|
707
|
+
f"Failed to read response after {args.max_retries + 1} attempts for {next_url or template}"
|
|
687
708
|
)
|
|
688
709
|
raise Exception(
|
|
689
|
-
f"Failed to read response after {
|
|
710
|
+
f"Failed to read response after {args.max_retries + 1} attempts for {next_url or template}"
|
|
690
711
|
)
|
|
691
712
|
|
|
692
713
|
# SECOND: Process and paginate
|
|
@@ -718,7 +739,7 @@ def retrieve_data(args, template, query_args=None, paginated=True):
|
|
|
718
739
|
return list(fetch_all())
|
|
719
740
|
|
|
720
741
|
|
|
721
|
-
def make_request_with_retry(request, auth):
|
|
742
|
+
def make_request_with_retry(request, auth, max_retries=5):
|
|
722
743
|
"""Make HTTP request with automatic retry for transient errors."""
|
|
723
744
|
|
|
724
745
|
def is_retryable_status(status_code, headers):
|
|
@@ -730,40 +751,49 @@ def make_request_with_retry(request, auth):
|
|
|
730
751
|
return int(headers.get("x-ratelimit-remaining", 1)) < 1
|
|
731
752
|
return False
|
|
732
753
|
|
|
733
|
-
for attempt in range(
|
|
754
|
+
for attempt in range(max_retries + 1):
|
|
734
755
|
try:
|
|
735
756
|
return urlopen(request, context=https_ctx)
|
|
736
757
|
|
|
737
758
|
except HTTPError as exc:
|
|
738
759
|
# HTTPError can be used as a response-like object
|
|
739
760
|
if not is_retryable_status(exc.code, exc.headers):
|
|
761
|
+
logger.error(
|
|
762
|
+
f"API Error: {exc.code} {exc.reason} for {request.full_url}"
|
|
763
|
+
)
|
|
740
764
|
raise # Non-retryable error
|
|
741
765
|
|
|
742
|
-
if attempt >=
|
|
743
|
-
logger.error(
|
|
766
|
+
if attempt >= max_retries:
|
|
767
|
+
logger.error(
|
|
768
|
+
f"HTTP {exc.code} failed after {max_retries + 1} attempts for {request.full_url}"
|
|
769
|
+
)
|
|
744
770
|
raise
|
|
745
771
|
|
|
746
772
|
delay = calculate_retry_delay(attempt, exc.headers)
|
|
747
773
|
logger.warning(
|
|
748
|
-
f"HTTP {exc.code}, retrying in {delay:.1f}s "
|
|
749
|
-
f"(attempt {attempt + 1}/{
|
|
774
|
+
f"HTTP {exc.code} ({exc.reason}), retrying in {delay:.1f}s "
|
|
775
|
+
f"(attempt {attempt + 1}/{max_retries + 1}) for {request.full_url}"
|
|
750
776
|
)
|
|
751
777
|
if auth is None and exc.code in (403, 429):
|
|
752
778
|
logger.info("Hint: Authenticate to raise your GitHub rate limit")
|
|
753
779
|
time.sleep(delay)
|
|
754
780
|
|
|
755
781
|
except (URLError, socket.error) as e:
|
|
756
|
-
if attempt >=
|
|
757
|
-
logger.error(
|
|
782
|
+
if attempt >= max_retries:
|
|
783
|
+
logger.error(
|
|
784
|
+
f"Connection error failed after {max_retries + 1} attempts: {e} for {request.full_url}"
|
|
785
|
+
)
|
|
758
786
|
raise
|
|
759
787
|
delay = calculate_retry_delay(attempt, {})
|
|
760
788
|
logger.warning(
|
|
761
789
|
f"Connection error: {e}, retrying in {delay:.1f}s "
|
|
762
|
-
f"(attempt {attempt + 1}/{
|
|
790
|
+
f"(attempt {attempt + 1}/{max_retries + 1}) for {request.full_url}"
|
|
763
791
|
)
|
|
764
792
|
time.sleep(delay)
|
|
765
793
|
|
|
766
|
-
raise Exception(
|
|
794
|
+
raise Exception(
|
|
795
|
+
f"Request failed after {max_retries + 1} attempts"
|
|
796
|
+
) # pragma: no cover
|
|
767
797
|
|
|
768
798
|
|
|
769
799
|
def _construct_request(per_page, query_args, template, auth, as_app=None, fine=False):
|
|
@@ -1579,9 +1609,7 @@ def filter_repositories(args, unfiltered_repositories):
|
|
|
1579
1609
|
repositories = [r for r in repositories if not r.get("archived")]
|
|
1580
1610
|
if args.starred_skip_size_over is not None:
|
|
1581
1611
|
if args.starred_skip_size_over <= 0:
|
|
1582
|
-
logger.warning(
|
|
1583
|
-
"--starred-skip-size-over must be greater than 0, ignoring"
|
|
1584
|
-
)
|
|
1612
|
+
logger.warning("--starred-skip-size-over must be greater than 0, ignoring")
|
|
1585
1613
|
else:
|
|
1586
1614
|
size_limit_kb = args.starred_skip_size_over * 1024
|
|
1587
1615
|
filtered = []
|
|
@@ -1590,7 +1618,9 @@ def filter_repositories(args, unfiltered_repositories):
|
|
|
1590
1618
|
size_mb = r.get("size", 0) / 1024
|
|
1591
1619
|
logger.info(
|
|
1592
1620
|
"Skipping starred repo {0} ({1:.0f} MB) due to --starred-skip-size-over {2}".format(
|
|
1593
|
-
r.get("full_name", r.get("name")),
|
|
1621
|
+
r.get("full_name", r.get("name")),
|
|
1622
|
+
size_mb,
|
|
1623
|
+
args.starred_skip_size_over,
|
|
1594
1624
|
)
|
|
1595
1625
|
)
|
|
1596
1626
|
else:
|
|
@@ -1694,6 +1724,9 @@ def backup_repositories(args, output_directory, repositories):
|
|
|
1694
1724
|
if args.include_milestones or args.include_everything:
|
|
1695
1725
|
backup_milestones(args, repo_cwd, repository, repos_template)
|
|
1696
1726
|
|
|
1727
|
+
if args.include_security_advisories or args.include_everything:
|
|
1728
|
+
backup_security_advisories(args, repo_cwd, repository, repos_template)
|
|
1729
|
+
|
|
1697
1730
|
if args.include_labels or args.include_everything:
|
|
1698
1731
|
backup_labels(args, repo_cwd, repository, repos_template)
|
|
1699
1732
|
|
|
@@ -1910,6 +1943,41 @@ def backup_milestones(args, repo_cwd, repository, repos_template):
|
|
|
1910
1943
|
)
|
|
1911
1944
|
|
|
1912
1945
|
|
|
1946
|
+
def backup_security_advisories(args, repo_cwd, repository, repos_template):
|
|
1947
|
+
advisory_cwd = os.path.join(repo_cwd, "security-advisories")
|
|
1948
|
+
if args.skip_existing and os.path.isdir(advisory_cwd):
|
|
1949
|
+
return
|
|
1950
|
+
|
|
1951
|
+
logger.info("Retrieving {0} security advisories".format(repository["full_name"]))
|
|
1952
|
+
mkdir_p(repo_cwd, advisory_cwd)
|
|
1953
|
+
|
|
1954
|
+
template = "{0}/{1}/security-advisories".format(repos_template, repository["full_name"])
|
|
1955
|
+
|
|
1956
|
+
_advisories = retrieve_data(args, template)
|
|
1957
|
+
|
|
1958
|
+
advisories = {}
|
|
1959
|
+
for advisory in _advisories:
|
|
1960
|
+
advisories[advisory["ghsa_id"]] = advisory
|
|
1961
|
+
|
|
1962
|
+
written_count = 0
|
|
1963
|
+
for ghsa_id, advisory in list(advisories.items()):
|
|
1964
|
+
advisory_file = "{0}/{1}.json".format(advisory_cwd, ghsa_id)
|
|
1965
|
+
if json_dump_if_changed(advisory, advisory_file):
|
|
1966
|
+
written_count += 1
|
|
1967
|
+
|
|
1968
|
+
total = len(advisories)
|
|
1969
|
+
if written_count == total:
|
|
1970
|
+
logger.info("Saved {0} security advisories to disk".format(total))
|
|
1971
|
+
elif written_count == 0:
|
|
1972
|
+
logger.info("{0} security advisories unchanged, skipped write".format(total))
|
|
1973
|
+
else:
|
|
1974
|
+
logger.info(
|
|
1975
|
+
"Saved {0} of {1} security advisories to disk ({2} unchanged)".format(
|
|
1976
|
+
written_count, total, total - written_count
|
|
1977
|
+
)
|
|
1978
|
+
)
|
|
1979
|
+
|
|
1980
|
+
|
|
1913
1981
|
def backup_labels(args, repo_cwd, repository, repos_template):
|
|
1914
1982
|
label_cwd = os.path.join(repo_cwd, "labels")
|
|
1915
1983
|
output_file = "{0}/labels.json".format(label_cwd)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.61.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
|
|
@@ -73,9 +73,9 @@ CLI Help output::
|
|
|
73
73
|
[--watched] [--followers] [--following] [--all]
|
|
74
74
|
[--issues] [--issue-comments] [--issue-events] [--pulls]
|
|
75
75
|
[--pull-comments] [--pull-commits] [--pull-details]
|
|
76
|
-
[--labels] [--hooks] [--milestones] [--
|
|
77
|
-
[--bare] [--no-prune] [--lfs] [--wikis]
|
|
78
|
-
[--starred-gists] [--skip-archived] [--skip-existing]
|
|
76
|
+
[--labels] [--hooks] [--milestones] [--security-advisories]
|
|
77
|
+
[--repositories] [--bare] [--no-prune] [--lfs] [--wikis]
|
|
78
|
+
[--gists] [--starred-gists] [--skip-archived] [--skip-existing]
|
|
79
79
|
[-L [LANGUAGES ...]] [-N NAME_REGEX] [-H GITHUB_HOST]
|
|
80
80
|
[-O] [-R REPOSITORY] [-P] [-F] [--prefer-ssh] [-v]
|
|
81
81
|
[--keychain-name OSX_KEYCHAIN_ITEM_NAME]
|
|
@@ -85,7 +85,7 @@ CLI Help output::
|
|
|
85
85
|
[--skip-assets-on [SKIP_ASSETS_ON ...]] [--attachments]
|
|
86
86
|
[--throttle-limit THROTTLE_LIMIT]
|
|
87
87
|
[--throttle-pause THROTTLE_PAUSE]
|
|
88
|
-
[--exclude [EXCLUDE ...]]
|
|
88
|
+
[--exclude [EXCLUDE ...]] [--retries MAX_RETRIES]
|
|
89
89
|
USER
|
|
90
90
|
|
|
91
91
|
Backup a github account
|
|
@@ -131,6 +131,8 @@ CLI Help output::
|
|
|
131
131
|
--hooks include hooks in backup (works only when
|
|
132
132
|
authenticated)
|
|
133
133
|
--milestones include milestones in backup
|
|
134
|
+
--security-advisories
|
|
135
|
+
include security advisories in backup
|
|
134
136
|
--repositories include repository clone in backup
|
|
135
137
|
--bare clone bare repositories
|
|
136
138
|
--no-prune disable prune option for git fetch
|
|
@@ -182,7 +184,8 @@ CLI Help output::
|
|
|
182
184
|
--throttle-limit to be set)
|
|
183
185
|
--exclude [EXCLUDE ...]
|
|
184
186
|
names of repositories to exclude
|
|
185
|
-
|
|
187
|
+
--retries MAX_RETRIES
|
|
188
|
+
maximum number of retries for API calls (default: 5)
|
|
186
189
|
|
|
187
190
|
Usage Details
|
|
188
191
|
=============
|
|
@@ -430,7 +433,7 @@ Quietly and incrementally backup useful Github user data (public and private rep
|
|
|
430
433
|
export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
431
434
|
GH_USER=YOUR-GITHUB-USER
|
|
432
435
|
|
|
433
|
-
github-backup -f $FINE_ACCESS_TOKEN --prefer-ssh -o ~/github-backup/ -l error -P -i --all-starred --starred --watched --followers --following --issues --issue-comments --issue-events --pulls --pull-comments --pull-commits --labels --milestones --repositories --wikis --releases --assets --attachments --pull-details --gists --starred-gists $GH_USER
|
|
436
|
+
github-backup -f $FINE_ACCESS_TOKEN --prefer-ssh -o ~/github-backup/ -l error -P -i --all-starred --starred --watched --followers --following --issues --issue-comments --issue-events --pulls --pull-comments --pull-commits --labels --milestones --security-advisories --repositories --wikis --releases --assets --attachments --pull-details --gists --starred-gists $GH_USER
|
|
434
437
|
|
|
435
438
|
Debug an error/block or incomplete backup into a temporary directory. Omit "incremental" to fill a previous incomplete backup. ::
|
|
436
439
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
github_backup/__init__.py,sha256=57kaeWxmyt5jDwUTomJhqevhkw_IXJwindDD018ZEtY,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=G3zwRZjwsg2UqY6sQvIG78VyuyrY5mFEeizPZgQe9SM,84189
|
|
5
|
+
github_backup-0.61.0.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
|
|
6
|
+
github_backup-0.61.0.dist-info/METADATA,sha256=yHsvlCYXWXH3ccO4YWXFsD9tyz1omV5-O31f5u5gmAc,26693
|
|
7
|
+
github_backup-0.61.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
github_backup-0.61.0.dist-info/entry_points.txt,sha256=cLPKK7MqC77QVb6HnT2mnyL_f-ciavJG_Y9vVHlCF68,57
|
|
9
|
+
github_backup-0.61.0.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
|
|
10
|
+
github_backup-0.61.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
11
|
+
github_backup-0.61.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
|