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 +1 -1
- github_backup/github_backup.py +72 -33
- {github_backup-0.56.0.dist-info → github_backup-0.57.0.dist-info}/METADATA +40 -3
- github_backup-0.57.0.dist-info/RECORD +11 -0
- github_backup-0.56.0.dist-info/RECORD +0 -11
- {github_backup-0.56.0.dist-info → github_backup-0.57.0.dist-info}/WHEEL +0 -0
- {github_backup-0.56.0.dist-info → github_backup-0.57.0.dist-info}/entry_points.txt +0 -0
- {github_backup-0.56.0.dist-info → github_backup-0.57.0.dist-info}/licenses/LICENSE.txt +0 -0
- {github_backup-0.56.0.dist-info → github_backup-0.57.0.dist-info}/top_level.txt +0 -0
- {github_backup-0.56.0.dist-info → github_backup-0.57.0.dist-info}/zip-safe +0 -0
github_backup/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.57.0"
|
github_backup/github_backup.py
CHANGED
|
@@ -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":
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
|
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 (
|
|
1677
|
-
|
|
1678
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1923
|
-
|
|
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(
|
|
2012
|
-
|
|
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 =
|
|
2028
|
-
|
|
2029
|
-
|
|
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(
|
|
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.
|
|
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] [--
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|