github-backup 0.56.0__py3-none-any.whl → 0.57.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.56.0"
1
+ __version__ = "0.57.0"
@@ -440,6 +440,12 @@ def parse_args(args=None):
440
440
  dest="include_assets",
441
441
  help="include assets alongside release information; only applies if including releases",
442
442
  )
443
+ parser.add_argument(
444
+ "--skip-assets-on",
445
+ dest="skip_assets_on",
446
+ nargs="*",
447
+ help="skip asset downloads for these repositories",
448
+ )
443
449
  parser.add_argument(
444
450
  "--attachments",
445
451
  action="store_true",
@@ -561,7 +567,7 @@ def get_github_host(args):
561
567
 
562
568
 
563
569
  def read_file_contents(file_uri):
564
- return open(file_uri[len(FILE_URI_PREFIX):], "rt").readline().strip()
570
+ return open(file_uri[len(FILE_URI_PREFIX) :], "rt").readline().strip()
565
571
 
566
572
 
567
573
  def get_github_repo_url(args, repository):
@@ -631,7 +637,7 @@ def retrieve_data_gen(args, template, query_args=None, single_request=False):
631
637
  pass
632
638
  raise RepositoryUnavailableError(
633
639
  "Repository unavailable due to legal reasons (HTTP 451)",
634
- dmca_url=dmca_url
640
+ dmca_url=dmca_url,
635
641
  )
636
642
 
637
643
  # Check if we got correct data
@@ -709,7 +715,7 @@ def retrieve_data_gen(args, template, query_args=None, single_request=False):
709
715
  # Parse Link header: <https://api.github.com/...?per_page=100&after=cursor>; rel="next"
710
716
  for link in link_header.split(","):
711
717
  if 'rel="next"' in link:
712
- next_url = link[link.find("<") + 1:link.find(">")]
718
+ next_url = link[link.find("<") + 1 : link.find(">")]
713
719
  break
714
720
  if not next_url:
715
721
  break
@@ -763,9 +769,7 @@ def _get_response(request, auth, template):
763
769
  return r, errors
764
770
 
765
771
 
766
- def _construct_request(
767
- per_page, query_args, template, auth, as_app=None, fine=False
768
- ):
772
+ def _construct_request(per_page, query_args, template, auth, as_app=None, fine=False):
769
773
  # If template is already a full URL with query params (from Link header), use it directly
770
774
  if "?" in template and template.startswith("http"):
771
775
  request_url = template
@@ -796,9 +800,6 @@ def _construct_request(
796
800
  else:
797
801
  auth = auth.encode("ascii")
798
802
  request.add_header("Authorization", "token ".encode("ascii") + auth)
799
- request.add_header(
800
- "Accept", "application/vnd.github.machine-man-preview+json"
801
- )
802
803
 
803
804
  log_url = template if "?" not in template else template.split("?")[0]
804
805
  if querystring:
@@ -1480,9 +1481,11 @@ def download_attachments(
1480
1481
  manifest = {
1481
1482
  "issue_number": number,
1482
1483
  "issue_type": item_type,
1483
- "repository": f"{args.user}/{args.repository}"
1484
- if hasattr(args, "repository") and args.repository
1485
- else args.user,
1484
+ "repository": (
1485
+ f"{args.user}/{args.repository}"
1486
+ if hasattr(args, "repository") and args.repository
1487
+ else args.user
1488
+ ),
1486
1489
  "manifest_updated_at": datetime.now(timezone.utc).isoformat(),
1487
1490
  "attachments": attachment_metadata_list,
1488
1491
  }
@@ -1538,9 +1541,7 @@ def retrieve_repositories(args, authenticated_user):
1538
1541
  else:
1539
1542
  repo_path = "{0}/{1}".format(args.user, args.repository)
1540
1543
  single_request = True
1541
- template = "https://{0}/repos/{1}".format(
1542
- get_github_api_host(args), repo_path
1543
- )
1544
+ template = "https://{0}/repos/{1}".format(get_github_api_host(args), repo_path)
1544
1545
 
1545
1546
  repos = retrieve_data(args, template, single_request=single_request)
1546
1547
 
@@ -1565,7 +1566,10 @@ def retrieve_repositories(args, authenticated_user):
1565
1566
  repos.extend(gists)
1566
1567
 
1567
1568
  if args.include_starred_gists:
1568
- if not authenticated_user.get("login") or args.user.lower() != authenticated_user["login"].lower():
1569
+ if (
1570
+ not authenticated_user.get("login")
1571
+ or args.user.lower() != authenticated_user["login"].lower()
1572
+ ):
1569
1573
  logger.warning(
1570
1574
  "Cannot retrieve starred gists for '%s'. GitHub only allows access to the authenticated user's starred gists.",
1571
1575
  args.user,
@@ -1673,9 +1677,11 @@ def backup_repositories(args, output_directory, repositories):
1673
1677
 
1674
1678
  include_gists = args.include_gists or args.include_starred_gists
1675
1679
  include_starred = args.all_starred and repository.get("is_starred")
1676
- if (args.include_repository or args.include_everything) or (
1677
- include_gists and repository.get("is_gist")
1678
- ) or include_starred:
1680
+ if (
1681
+ (args.include_repository or args.include_everything)
1682
+ or (include_gists and repository.get("is_gist"))
1683
+ or include_starred
1684
+ ):
1679
1685
  repo_name = (
1680
1686
  repository.get("name")
1681
1687
  if not repository.get("is_gist")
@@ -1735,7 +1741,9 @@ def backup_repositories(args, output_directory, repositories):
1735
1741
  include_assets=args.include_assets or args.include_everything,
1736
1742
  )
1737
1743
  except RepositoryUnavailableError as e:
1738
- logger.warning(f"Repository {repository['full_name']} is unavailable (HTTP 451)")
1744
+ logger.warning(
1745
+ f"Repository {repository['full_name']} is unavailable (HTTP 451)"
1746
+ )
1739
1747
  if e.dmca_url:
1740
1748
  logger.warning(f"DMCA notice: {e.dmca_url}")
1741
1749
  logger.info(f"Skipping remaining resources for {repository['full_name']}")
@@ -1795,7 +1803,11 @@ def backup_issues(args, repo_cwd, repository, repos_template):
1795
1803
  modified = os.path.getmtime(issue_file)
1796
1804
  modified = datetime.fromtimestamp(modified).strftime("%Y-%m-%dT%H:%M:%SZ")
1797
1805
  if modified > issue["updated_at"]:
1798
- logger.info("Skipping issue {0} because it wasn't modified since last backup".format(number))
1806
+ logger.info(
1807
+ "Skipping issue {0} because it wasn't modified since last backup".format(
1808
+ number
1809
+ )
1810
+ )
1799
1811
  continue
1800
1812
 
1801
1813
  if args.include_issue_comments or args.include_everything:
@@ -1869,7 +1881,11 @@ def backup_pulls(args, repo_cwd, repository, repos_template):
1869
1881
  modified = os.path.getmtime(pull_file)
1870
1882
  modified = datetime.fromtimestamp(modified).strftime("%Y-%m-%dT%H:%M:%SZ")
1871
1883
  if modified > pull["updated_at"]:
1872
- logger.info("Skipping pull request {0} because it wasn't modified since last backup".format(number))
1884
+ logger.info(
1885
+ "Skipping pull request {0} because it wasn't modified since last backup".format(
1886
+ number
1887
+ )
1888
+ )
1873
1889
  continue
1874
1890
  if args.include_pull_comments or args.include_everything:
1875
1891
  template = comments_regular_template.format(number)
@@ -1919,9 +1935,11 @@ def backup_milestones(args, repo_cwd, repository, repos_template):
1919
1935
  elif written_count == 0:
1920
1936
  logger.info("{0} milestones unchanged, skipped write".format(total))
1921
1937
  else:
1922
- logger.info("Saved {0} of {1} milestones to disk ({2} unchanged)".format(
1923
- written_count, total, total - written_count
1924
- ))
1938
+ logger.info(
1939
+ "Saved {0} of {1} milestones to disk ({2} unchanged)".format(
1940
+ written_count, total, total - written_count
1941
+ )
1942
+ )
1925
1943
 
1926
1944
 
1927
1945
  def backup_labels(args, repo_cwd, repository, repos_template):
@@ -1975,6 +1993,20 @@ def backup_releases(args, repo_cwd, repository, repos_template, include_assets=F
1975
1993
  )
1976
1994
  releases = releases[: args.number_of_latest_releases]
1977
1995
 
1996
+ # Check if this repo should skip asset downloads (case-insensitive)
1997
+ skip_assets = False
1998
+ if include_assets:
1999
+ repo_name = repository.get("name", "").lower()
2000
+ repo_full_name = repository.get("full_name", "").lower()
2001
+ skip_repos = [r.lower() for r in (args.skip_assets_on or [])]
2002
+ skip_assets = repo_name in skip_repos or repo_full_name in skip_repos
2003
+ if skip_assets:
2004
+ logger.info(
2005
+ "Skipping assets for {0} ({1} releases) due to --skip-assets-on".format(
2006
+ repository.get("name"), len(releases)
2007
+ )
2008
+ )
2009
+
1978
2010
  # for each release, store it
1979
2011
  written_count = 0
1980
2012
  for release in releases:
@@ -1986,7 +2018,7 @@ def backup_releases(args, repo_cwd, repository, repos_template, include_assets=F
1986
2018
  if json_dump_if_changed(release, output_filepath):
1987
2019
  written_count += 1
1988
2020
 
1989
- if include_assets:
2021
+ if include_assets and not skip_assets:
1990
2022
  assets = retrieve_data(args, release["assets_url"])
1991
2023
  if len(assets) > 0:
1992
2024
  # give release asset files somewhere to live & download them (not including source archives)
@@ -2008,9 +2040,11 @@ def backup_releases(args, repo_cwd, repository, repos_template, include_assets=F
2008
2040
  elif written_count == 0:
2009
2041
  logger.info("{0} releases unchanged, skipped write".format(total))
2010
2042
  else:
2011
- logger.info("Saved {0} of {1} releases to disk ({2} unchanged)".format(
2012
- written_count, total, total - written_count
2013
- ))
2043
+ logger.info(
2044
+ "Saved {0} of {1} releases to disk ({2} unchanged)".format(
2045
+ written_count, total, total - written_count
2046
+ )
2047
+ )
2014
2048
 
2015
2049
 
2016
2050
  def fetch_repository(
@@ -2024,9 +2058,12 @@ def fetch_repository(
2024
2058
  ):
2025
2059
  if bare_clone:
2026
2060
  if os.path.exists(local_dir):
2027
- clone_exists = subprocess.check_output(
2028
- ["git", "rev-parse", "--is-bare-repository"], cwd=local_dir
2029
- ) == b"true\n"
2061
+ clone_exists = (
2062
+ subprocess.check_output(
2063
+ ["git", "rev-parse", "--is-bare-repository"], cwd=local_dir
2064
+ )
2065
+ == b"true\n"
2066
+ )
2030
2067
  else:
2031
2068
  clone_exists = False
2032
2069
  else:
@@ -2047,7 +2084,9 @@ def fetch_repository(
2047
2084
  )
2048
2085
  else:
2049
2086
  logger.info(
2050
- "Skipping {0} (repository not accessible - may be empty, private, or credentials invalid)".format(name)
2087
+ "Skipping {0} (repository not accessible - may be empty, private, or credentials invalid)".format(
2088
+ name
2089
+ )
2051
2090
  )
2052
2091
  return
2053
2092
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-backup
3
- Version: 0.56.0
3
+ Version: 0.57.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
@@ -80,8 +80,8 @@ CLI Help output::
80
80
  [--keychain-name OSX_KEYCHAIN_ITEM_NAME]
81
81
  [--keychain-account OSX_KEYCHAIN_ITEM_ACCOUNT]
82
82
  [--releases] [--latest-releases NUMBER_OF_LATEST_RELEASES]
83
- [--skip-prerelease] [--assets] [--attachments]
84
- [--exclude [REPOSITORY [REPOSITORY ...]]
83
+ [--skip-prerelease] [--assets] [--skip-assets-on [REPO ...]]
84
+ [--attachments] [--exclude [REPOSITORY [REPOSITORY ...]]
85
85
  [--throttle-limit THROTTLE_LIMIT] [--throttle-pause THROTTLE_PAUSE]
86
86
  USER
87
87
 
@@ -163,6 +163,9 @@ CLI Help output::
163
163
  --skip-prerelease skip prerelease and draft versions; only applies if including releases
164
164
  --assets include assets alongside release information; only
165
165
  applies if including releases
166
+ --skip-assets-on [REPO ...]
167
+ skip asset downloads for these repositories (e.g.
168
+ --skip-assets-on repo1 owner/repo2)
166
169
  --attachments download user-attachments from issues and pull requests
167
170
  to issues/attachments/{issue_number}/ and
168
171
  pulls/attachments/{pull_number}/ directories
@@ -204,6 +207,37 @@ Customise the permissions for your use case, but for a personal account full bac
204
207
  **Repository permissions**: Read access to contents, issues, metadata, pull requests, and webhooks.
205
208
 
206
209
 
210
+ GitHub Apps
211
+ ~~~~~~~~~~~
212
+
213
+ GitHub Apps are ideal for organization backups in CI/CD. Tokens are scoped to specific repositories and expire after 1 hour.
214
+
215
+ **One-time setup:**
216
+
217
+ 1. Create a GitHub App at *Settings -> Developer Settings -> GitHub Apps -> New GitHub App*
218
+ 2. Set a name and homepage URL (can be any URL)
219
+ 3. Uncheck "Webhook > Active" (not needed for backups)
220
+ 4. Set permissions (same as fine-grained tokens above)
221
+ 5. Click "Create GitHub App", then note the **App ID** shown on the next page
222
+ 6. Under "Private keys", click "Generate a private key" and save the downloaded file
223
+ 7. Go to *Install App* in your app's settings
224
+ 8. Select the account/organization and which repositories to back up
225
+
226
+ **CI/CD usage with GitHub Actions:**
227
+
228
+ Store the App ID as a repository variable and the private key contents as a secret, then use ``actions/create-github-app-token``::
229
+
230
+ - uses: actions/create-github-app-token@v1
231
+ id: app-token
232
+ with:
233
+ app-id: ${{ vars.APP_ID }}
234
+ private-key: ${{ secrets.APP_PRIVATE_KEY }}
235
+
236
+ - run: github-backup myorg -t ${{ steps.app-token.outputs.token }} --as-app -o ./backup --all
237
+
238
+ Note: Installation tokens expire after 1 hour. For long-running backups, use a fine-grained personal access token instead.
239
+
240
+
207
241
  Prefer SSH
208
242
  ~~~~~~~~~~
209
243
 
@@ -391,6 +425,9 @@ Debug an error/block or incomplete backup into a temporary directory. Omit "incr
391
425
 
392
426
  github-backup -f $FINE_ACCESS_TOKEN -o /tmp/github-backup/ -l debug -P --all-starred --starred --watched --followers --following --issues --issue-comments --issue-events --pulls --pull-comments --pull-commits --labels --milestones --repositories --wikis --releases --assets --pull-details --gists --starred-gists $GH_USER
393
427
 
428
+ Pipe a token from stdin to avoid storing it in environment variables or command history (Unix-like systems only)::
429
+
430
+ my-secret-manager get github-token | github-backup user -t file:///dev/stdin -o /backup --repositories
394
431
 
395
432
  Restoring from Backup
396
433
  =====================
@@ -0,0 +1,11 @@
1
+ github_backup/__init__.py,sha256=jmnoPljZ40w5Zc7DEZpxkJSfC9yHbRjV50qzvnfEbdI,23
2
+ github_backup/__main__.py,sha256=3yorCiC63maaxbjdtaeAs-G-yAAxKSO3mprhGfa1Fho,271
3
+ github_backup/cli.py,sha256=zMUU0pTnh9KPqv8K--KNpUNf-yaA1qWeI6AdC1KVrn8,2361
4
+ github_backup/github_backup.py,sha256=_P0WVPpZpyzs3PPkwtmIWDu2mkmeGXd2EePymeEtOV0,81890
5
+ github_backup-0.57.0.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
6
+ github_backup-0.57.0.dist-info/METADATA,sha256=4Jni1TDR6oVFdIKCzjy6hacTh1TQrigaAeXVyidL-As,25921
7
+ github_backup-0.57.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ github_backup-0.57.0.dist-info/entry_points.txt,sha256=cLPKK7MqC77QVb6HnT2mnyL_f-ciavJG_Y9vVHlCF68,57
9
+ github_backup-0.57.0.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
10
+ github_backup-0.57.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
+ github_backup-0.57.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- github_backup/__init__.py,sha256=pLF99dIRilisWyZd9CliPPWqIlV81Xy8XtGEMuThTaM,23
2
- github_backup/__main__.py,sha256=3yorCiC63maaxbjdtaeAs-G-yAAxKSO3mprhGfa1Fho,271
3
- github_backup/cli.py,sha256=zMUU0pTnh9KPqv8K--KNpUNf-yaA1qWeI6AdC1KVrn8,2361
4
- github_backup/github_backup.py,sha256=wlVrA50q5iP8BbJ_6WIIMEP7MKl_B3cKm2zvEtNYMS4,80777
5
- github_backup-0.56.0.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
6
- github_backup-0.56.0.dist-info/METADATA,sha256=0N-CqtE9r3amkSHGkrgR3YeG7wylthaM0Q2RDct5vbc,24230
7
- github_backup-0.56.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- github_backup-0.56.0.dist-info/entry_points.txt,sha256=cLPKK7MqC77QVb6HnT2mnyL_f-ciavJG_Y9vVHlCF68,57
9
- github_backup-0.56.0.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
10
- github_backup-0.56.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
- github_backup-0.56.0.dist-info/RECORD,,