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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.59.0"
1
+ __version__ = "0.61.0"
@@ -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(MAX_RETRIES):
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 < MAX_RETRIES - 1:
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}/{MAX_RETRIES})"
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 {MAX_RETRIES} attempts for {next_url or template}"
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 {MAX_RETRIES} attempts for {next_url or template}"
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(MAX_RETRIES):
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 >= MAX_RETRIES - 1:
743
- logger.error(f"HTTP {exc.code} failed after {MAX_RETRIES} attempts")
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}/{MAX_RETRIES})"
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 >= MAX_RETRIES - 1:
757
- logger.error(f"Connection error failed after {MAX_RETRIES} attempts: {e}")
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}/{MAX_RETRIES})"
790
+ f"(attempt {attempt + 1}/{max_retries + 1}) for {request.full_url}"
763
791
  )
764
792
  time.sleep(delay)
765
793
 
766
- raise Exception(f"Request failed after {MAX_RETRIES} attempts") # pragma: no cover
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")), size_mb, args.starred_skip_size_over
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.59.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] [--repositories]
77
- [--bare] [--no-prune] [--lfs] [--wikis] [--gists]
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,,