githubtakeout 0.1.1__tar.gz → 0.1.3__tar.gz
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.
- {githubtakeout-0.1.1 → githubtakeout-0.1.3}/PKG-INFO +10 -6
- {githubtakeout-0.1.1 → githubtakeout-0.1.3}/README.md +8 -1
- {githubtakeout-0.1.1 → githubtakeout-0.1.3}/githubtakeout.egg-info/PKG-INFO +10 -6
- {githubtakeout-0.1.1 → githubtakeout-0.1.3}/githubtakeout.py +77 -43
- {githubtakeout-0.1.1 → githubtakeout-0.1.3}/pyproject.toml +2 -5
- {githubtakeout-0.1.1 → githubtakeout-0.1.3}/LICENSE +0 -0
- {githubtakeout-0.1.1 → githubtakeout-0.1.3}/githubtakeout.egg-info/SOURCES.txt +0 -0
- {githubtakeout-0.1.1 → githubtakeout-0.1.3}/githubtakeout.egg-info/dependency_links.txt +0 -0
- {githubtakeout-0.1.1 → githubtakeout-0.1.3}/githubtakeout.egg-info/entry_points.txt +0 -0
- {githubtakeout-0.1.1 → githubtakeout-0.1.3}/githubtakeout.egg-info/requires.txt +0 -0
- {githubtakeout-0.1.1 → githubtakeout-0.1.3}/githubtakeout.egg-info/top_level.txt +0 -0
- {githubtakeout-0.1.1 → githubtakeout-0.1.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: githubtakeout
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Archive public Git Repos and Gists from GitHub
|
|
5
5
|
Author: Corey Goldberg
|
|
6
6
|
Maintainer: Corey Goldberg
|
|
@@ -13,12 +13,9 @@ Classifier: Environment :: Console
|
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Programming Language :: Python
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
-
Requires-Python: >=3.
|
|
18
|
+
Requires-Python: >=3.12
|
|
22
19
|
Description-Content-Type: text/markdown
|
|
23
20
|
License-File: LICENSE
|
|
24
21
|
Requires-Dist: GitPython==3.1.44
|
|
@@ -56,7 +53,7 @@ as tarballs (`.tar.gz`) using the `--format=tar` option.
|
|
|
56
53
|
|
|
57
54
|
## Requirements:
|
|
58
55
|
|
|
59
|
-
- Python 3.
|
|
56
|
+
- Python 3.12+
|
|
60
57
|
- Git 1.7+
|
|
61
58
|
- Python packages:
|
|
62
59
|
- GitPython
|
|
@@ -105,6 +102,13 @@ options:
|
|
|
105
102
|
|
|
106
103
|
## Usage Examples:
|
|
107
104
|
|
|
105
|
+
### Install from PyPI with pipx, Run:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
pipx install githubtakeout
|
|
109
|
+
githubtakeout <github username>
|
|
110
|
+
```
|
|
111
|
+
|
|
108
112
|
### Create/Activate Virtual Environment, Install from PyPI, Run:
|
|
109
113
|
|
|
110
114
|
```
|
|
@@ -29,7 +29,7 @@ as tarballs (`.tar.gz`) using the `--format=tar` option.
|
|
|
29
29
|
|
|
30
30
|
## Requirements:
|
|
31
31
|
|
|
32
|
-
- Python 3.
|
|
32
|
+
- Python 3.12+
|
|
33
33
|
- Git 1.7+
|
|
34
34
|
- Python packages:
|
|
35
35
|
- GitPython
|
|
@@ -78,6 +78,13 @@ options:
|
|
|
78
78
|
|
|
79
79
|
## Usage Examples:
|
|
80
80
|
|
|
81
|
+
### Install from PyPI with pipx, Run:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
pipx install githubtakeout
|
|
85
|
+
githubtakeout <github username>
|
|
86
|
+
```
|
|
87
|
+
|
|
81
88
|
### Create/Activate Virtual Environment, Install from PyPI, Run:
|
|
82
89
|
|
|
83
90
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: githubtakeout
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Archive public Git Repos and Gists from GitHub
|
|
5
5
|
Author: Corey Goldberg
|
|
6
6
|
Maintainer: Corey Goldberg
|
|
@@ -13,12 +13,9 @@ Classifier: Environment :: Console
|
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Programming Language :: Python
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
-
Requires-Python: >=3.
|
|
18
|
+
Requires-Python: >=3.12
|
|
22
19
|
Description-Content-Type: text/markdown
|
|
23
20
|
License-File: LICENSE
|
|
24
21
|
Requires-Dist: GitPython==3.1.44
|
|
@@ -56,7 +53,7 @@ as tarballs (`.tar.gz`) using the `--format=tar` option.
|
|
|
56
53
|
|
|
57
54
|
## Requirements:
|
|
58
55
|
|
|
59
|
-
- Python 3.
|
|
56
|
+
- Python 3.12+
|
|
60
57
|
- Git 1.7+
|
|
61
58
|
- Python packages:
|
|
62
59
|
- GitPython
|
|
@@ -105,6 +102,13 @@ options:
|
|
|
105
102
|
|
|
106
103
|
## Usage Examples:
|
|
107
104
|
|
|
105
|
+
### Install from PyPI with pipx, Run:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
pipx install githubtakeout
|
|
109
|
+
githubtakeout <github username>
|
|
110
|
+
```
|
|
111
|
+
|
|
108
112
|
### Create/Activate Virtual Environment, Install from PyPI, Run:
|
|
109
113
|
|
|
110
114
|
```
|
|
@@ -5,6 +5,8 @@ import getpass
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
7
|
import shutil
|
|
8
|
+
import stat
|
|
9
|
+
import sys
|
|
8
10
|
import tarfile
|
|
9
11
|
import urllib
|
|
10
12
|
import zipfile
|
|
@@ -43,57 +45,86 @@ def archive(local_repo_dir, archive_format='zip', is_gist=False):
|
|
|
43
45
|
archive_path = os.path.join(parent_dir, archive_name)
|
|
44
46
|
logger.info(f'creating archive: {archive_path}')
|
|
45
47
|
if archive_format == 'tar':
|
|
46
|
-
with tarfile.open(archive_path, 'w:gz') as
|
|
47
|
-
|
|
48
|
+
with tarfile.open(archive_path, 'w:gz') as tar_archive:
|
|
49
|
+
tar_archive.add(local_repo_dir, arcname=base_name)
|
|
48
50
|
elif archive_format == 'zip':
|
|
49
|
-
with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as
|
|
51
|
+
with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zip_archive:
|
|
50
52
|
repo_path = Path(local_repo_dir)
|
|
51
53
|
for entry in repo_path.rglob('*'):
|
|
52
54
|
path = os.path.join(base_name, entry.relative_to(repo_path))
|
|
53
|
-
|
|
55
|
+
zip_archive.write(entry, arcname=path)
|
|
54
56
|
return archive_path
|
|
55
57
|
|
|
56
58
|
|
|
57
|
-
def
|
|
58
|
-
try:
|
|
59
|
-
shutil.rmtree(local_repo_dir)
|
|
60
|
-
except FileNotFoundError:
|
|
61
|
-
pass
|
|
62
|
-
repo_path = urllib.parse.urlparse(repo_url).path
|
|
63
|
-
logger.info(f'cloning repo: {repo_path} to {local_repo_dir}')
|
|
64
|
-
start = default_timer()
|
|
59
|
+
def clone(repo_url, local_repo_dir, include_history):
|
|
65
60
|
try:
|
|
66
61
|
if include_history:
|
|
67
|
-
|
|
62
|
+
# full clone
|
|
63
|
+
repo = git.Repo.clone_from(repo_url, local_repo_dir)
|
|
68
64
|
else:
|
|
69
65
|
# shallow clone (no commit history or branches)
|
|
70
|
-
git.Repo.clone_from(repo_url, local_repo_dir, multi_options=['--depth=1'])
|
|
71
|
-
try:
|
|
72
|
-
shutil.rmtree(os.path.join(local_repo_dir, '.git'))
|
|
73
|
-
except FileNotFoundError:
|
|
74
|
-
pass
|
|
66
|
+
repo = git.Repo.clone_from(repo_url, local_repo_dir, multi_options=['--depth=1'])
|
|
75
67
|
except git.GitCommandError as e:
|
|
76
68
|
logger.error(e)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
69
|
+
sys.exit('Error: failed cloning repo')
|
|
70
|
+
finally:
|
|
71
|
+
try:
|
|
72
|
+
# release resources
|
|
73
|
+
repo.close()
|
|
74
|
+
except UnboundLocalError:
|
|
75
|
+
# this occurs if we catch a signal while cloning
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def clone_and_archive_repo(repo_url, local_repo_dir, archive_format, include_history, is_gist=False):
|
|
81
|
+
def remove_readonly(func, path, _):
|
|
82
|
+
# This is necessary so rmtree() doesn't fail if there are any readonly
|
|
83
|
+
# dirs/files when trying to delete. This seems to happen after cloning on
|
|
84
|
+
# Windows. When any error occurs during deletion, we change the permissions
|
|
85
|
+
# and and reattempt removal.
|
|
86
|
+
#
|
|
87
|
+
# give read permissions
|
|
88
|
+
os.chmod(path, stat.S_IREAD)
|
|
89
|
+
# give write permissions
|
|
90
|
+
os.chmod(path, stat.S_IWRITE)
|
|
91
|
+
# try again
|
|
92
|
+
func(path)
|
|
93
|
+
try:
|
|
94
|
+
# delete repo if it already exists
|
|
95
|
+
shutil.rmtree(local_repo_dir, onexc=remove_readonly)
|
|
96
|
+
except FileNotFoundError:
|
|
97
|
+
pass
|
|
98
|
+
repo_name = urllib.parse.urlparse(repo_url).path.lstrip('/')
|
|
99
|
+
logger.info(f'cloning repo: {repo_name} to {local_repo_dir}')
|
|
100
|
+
start = default_timer()
|
|
101
|
+
clone(repo_url, local_repo_dir, include_history)
|
|
102
|
+
if not include_history:
|
|
103
|
+
# delete the .git directory if we are not saving history
|
|
104
|
+
try:
|
|
105
|
+
git_dir = os.path.join(local_repo_dir, '.git')
|
|
106
|
+
shutil.rmtree(git_dir, onexc=remove_readonly)
|
|
107
|
+
except FileNotFoundError:
|
|
108
|
+
pass
|
|
80
109
|
archive_path = archive(local_repo_dir, archive_format, is_gist)
|
|
81
110
|
size_kb = os.path.getsize(archive_path) / 1024
|
|
82
111
|
logger.info(f'archive size: {size_kb:.2f} KB')
|
|
83
112
|
logger.info('deleting repo')
|
|
84
113
|
try:
|
|
85
|
-
|
|
114
|
+
# delete repo after archive is created
|
|
115
|
+
shutil.rmtree(local_repo_dir, onexc=remove_readonly)
|
|
86
116
|
except FileNotFoundError:
|
|
87
117
|
pass
|
|
88
|
-
|
|
118
|
+
elapsed = default_timer() - start
|
|
119
|
+
base_name = os.path.basename(local_repo_dir)
|
|
120
|
+
logger.info(f'successfully backed up "{base_name}" repo in {elapsed:.3f} secs\n')
|
|
89
121
|
|
|
90
122
|
|
|
91
123
|
def get_user(username, prompt_token):
|
|
92
124
|
if prompt_token:
|
|
93
125
|
token = getpass.getpass('Token:')
|
|
94
126
|
if not token:
|
|
95
|
-
|
|
96
|
-
exit(1)
|
|
127
|
+
sys.exit('Error: auth token cannot be empty')
|
|
97
128
|
auth = github.Auth.Token(token)
|
|
98
129
|
gh = github.Github(auth=auth)
|
|
99
130
|
else:
|
|
@@ -108,10 +139,9 @@ def get_user(username, prompt_token):
|
|
|
108
139
|
user = gh.get_user(username)
|
|
109
140
|
except github.GithubException as e:
|
|
110
141
|
if e.data['status'] == '404':
|
|
111
|
-
|
|
142
|
+
sys.exit(f'Error: user "{username}" not found on GitHub')
|
|
112
143
|
else:
|
|
113
144
|
raise e
|
|
114
|
-
exit(1)
|
|
115
145
|
repos = user.get_repos()
|
|
116
146
|
else:
|
|
117
147
|
# you need to be authenticated and then call the API
|
|
@@ -120,28 +150,27 @@ def get_user(username, prompt_token):
|
|
|
120
150
|
repos = user.get_repos(affiliation='owner')
|
|
121
151
|
try:
|
|
122
152
|
# this just makes an API request so we can exit if unauthorized
|
|
123
|
-
repos.totalCount
|
|
153
|
+
_ = repos.totalCount
|
|
124
154
|
except github.GithubException as e:
|
|
125
155
|
if e.data['status'] == '401':
|
|
126
|
-
|
|
156
|
+
sys.exit(f'Error: invalid auth token for user "{username}" on GitHub')
|
|
127
157
|
else:
|
|
128
158
|
raise e
|
|
129
|
-
exit(1)
|
|
130
159
|
gists = user.get_gists()
|
|
131
160
|
return user, repos, gists, token
|
|
132
161
|
|
|
133
162
|
|
|
134
|
-
def run(username, base_dir, archive_format, include_gists, include_history,
|
|
163
|
+
def run(username, base_dir, archive_format, include_gists, include_history, list_only, prompt_token):
|
|
135
164
|
working_dir = os.path.join(base_dir, 'backups')
|
|
136
165
|
user, repos, gists, token = get_user(username, prompt_token)
|
|
137
166
|
num_repos = repos.totalCount
|
|
138
|
-
if not
|
|
167
|
+
if not list_only:
|
|
139
168
|
logger.info(f'creating archives in: {working_dir}\n')
|
|
140
169
|
logger.info(f'found {num_repos} repos for user "{username}"\n')
|
|
141
170
|
for repo in repos:
|
|
142
171
|
local_repo_dir = os.path.join(working_dir, repo.name)
|
|
143
172
|
url = add_creds(repo.clone_url, username, token)
|
|
144
|
-
if
|
|
173
|
+
if list_only:
|
|
145
174
|
logger.info(url)
|
|
146
175
|
else:
|
|
147
176
|
clone_and_archive_repo(
|
|
@@ -157,7 +186,7 @@ def run(username, base_dir, archive_format, include_gists, include_history, list
|
|
|
157
186
|
for gist in gists:
|
|
158
187
|
local_repo_dir = os.path.join(working_dir, gist.id)
|
|
159
188
|
url = add_creds(gist.git_pull_url, username, token)
|
|
160
|
-
if
|
|
189
|
+
if list_only:
|
|
161
190
|
logger.info(url)
|
|
162
191
|
else:
|
|
163
192
|
clone_and_archive_repo(
|
|
@@ -170,6 +199,8 @@ def run(username, base_dir, archive_format, include_gists, include_history, list
|
|
|
170
199
|
|
|
171
200
|
|
|
172
201
|
def main():
|
|
202
|
+
if sys.version_info < (3, 12):
|
|
203
|
+
sys.exit('Sorry, this program requires Python 3.12+')
|
|
173
204
|
parser = argparse.ArgumentParser()
|
|
174
205
|
parser.add_argument(
|
|
175
206
|
'username',
|
|
@@ -210,15 +241,18 @@ def main():
|
|
|
210
241
|
help='prompt for auth token'
|
|
211
242
|
)
|
|
212
243
|
args = parser.parse_args()
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
244
|
+
try:
|
|
245
|
+
run(
|
|
246
|
+
username=args.username,
|
|
247
|
+
base_dir=args.dir,
|
|
248
|
+
archive_format=args.format,
|
|
249
|
+
include_gists=args.gists,
|
|
250
|
+
include_history=args.history,
|
|
251
|
+
list_only=args.list,
|
|
252
|
+
prompt_token=args.token
|
|
253
|
+
)
|
|
254
|
+
except KeyboardInterrupt:
|
|
255
|
+
sys.exit('\nexiting program ...')
|
|
222
256
|
|
|
223
257
|
|
|
224
258
|
if __name__ == '__main__':
|
|
@@ -4,23 +4,20 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "githubtakeout"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "Archive public Git Repos and Gists from GitHub"
|
|
9
9
|
license = "MIT"
|
|
10
10
|
license-files = ["LICENSE"]
|
|
11
11
|
authors = [{name = "Corey Goldberg"}]
|
|
12
12
|
maintainers = [{name = "Corey Goldberg"}]
|
|
13
13
|
readme = "README.md"
|
|
14
|
-
requires-python = ">=3.
|
|
14
|
+
requires-python = ">= 3.12"
|
|
15
15
|
keywords = ["git", "github", "archive", "backup", "export", "takeout"]
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Environment :: Console",
|
|
18
18
|
"Intended Audience :: Developers",
|
|
19
19
|
"Programming Language :: Python",
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
21
|
-
"Programming Language :: Python :: 3.9",
|
|
22
|
-
"Programming Language :: Python :: 3.10",
|
|
23
|
-
"Programming Language :: Python :: 3.11",
|
|
24
21
|
"Programming Language :: Python :: 3.12",
|
|
25
22
|
"Programming Language :: Python :: 3.13"
|
|
26
23
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|