githubtakeout 0.1.4__tar.gz → 0.1.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: githubtakeout
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Archive public Git Repos and Gists from GitHub
5
5
  Author: Corey Goldberg
6
6
  Maintainer: Corey Goldberg
@@ -20,6 +20,7 @@ Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: GitPython==3.1.44
22
22
  Requires-Dist: PyGithub==2.6.1
23
+ Requires-Dist: rich==14.0.0
23
24
  Dynamic: license-file
24
25
 
25
26
  # githubtakeout
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "githubtakeout"
7
- version = "0.1.4"
7
+ version = "0.1.5"
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.12"
14
+ requires-python = ">=3.12"
15
15
  keywords = ["git", "github", "archive", "backup", "export", "takeout"]
16
16
  classifiers = [
17
17
  "Environment :: Console",
@@ -19,11 +19,12 @@ classifiers = [
19
19
  "Programming Language :: Python",
20
20
  "Programming Language :: Python :: 3",
21
21
  "Programming Language :: Python :: 3.12",
22
- "Programming Language :: Python :: 3.13"
22
+ "Programming Language :: Python :: 3.13",
23
23
  ]
24
24
  dependencies = [
25
25
  "GitPython==3.1.44",
26
- "PyGithub==2.6.1"
26
+ "PyGithub==2.6.1",
27
+ "rich==14.0.0",
27
28
  ]
28
29
 
29
30
  [project.scripts]
@@ -33,3 +34,23 @@ githubtakeout = "githubtakeout:main"
33
34
  homepage = "https://github.com/cgoldberg/githubtakeout"
34
35
  source = "https://github.com/cgoldberg/githubtakeout"
35
36
  download = "https://pypi.org/project/githubtakeout"
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
40
+
41
+ [tool.black]
42
+ line-length = 88
43
+ target-version = ["py312"]
44
+
45
+ [tool.ruff]
46
+ line-length = 88
47
+ respect-gitignore = true
48
+ target-version = "py312"
49
+
50
+ [tool.ruff.lint]
51
+ extend-select = ["E4", "E7", "E9", "F", "I", "E501", "RUF022"]
52
+ fixable = ["ALL"]
53
+
54
+ [tool.ruff.format]
55
+ docstring-code-format = true
56
+ docstring-code-line-length = 88
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: githubtakeout
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Archive public Git Repos and Gists from GitHub
5
5
  Author: Corey Goldberg
6
6
  Maintainer: Corey Goldberg
@@ -20,6 +20,7 @@ Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: GitPython==3.1.44
22
22
  Requires-Dist: PyGithub==2.6.1
23
+ Requires-Dist: rich==14.0.0
23
24
  Dynamic: license-file
24
25
 
25
26
  # githubtakeout
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/githubtakeout.egg-info/PKG-INFO
5
+ src/githubtakeout.egg-info/SOURCES.txt
6
+ src/githubtakeout.egg-info/dependency_links.txt
7
+ src/githubtakeout.egg-info/entry_points.txt
8
+ src/githubtakeout.egg-info/requires.txt
9
+ src/githubtakeout.egg-info/top_level.txt
@@ -1,2 +1,3 @@
1
1
  GitPython==3.1.44
2
2
  PyGithub==2.6.1
3
+ rich==14.0.0
@@ -1,10 +0,0 @@
1
- LICENSE
2
- README.md
3
- githubtakeout.py
4
- pyproject.toml
5
- githubtakeout.egg-info/PKG-INFO
6
- githubtakeout.egg-info/SOURCES.txt
7
- githubtakeout.egg-info/dependency_links.txt
8
- githubtakeout.egg-info/entry_points.txt
9
- githubtakeout.egg-info/requires.txt
10
- githubtakeout.egg-info/top_level.txt
@@ -1 +0,0 @@
1
- githubtakeout
@@ -1,259 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- import argparse
4
- import getpass
5
- import logging
6
- import os
7
- import shutil
8
- import stat
9
- import sys
10
- import tarfile
11
- import urllib
12
- import zipfile
13
-
14
- from pathlib import Path
15
- from timeit import default_timer
16
-
17
- import git
18
- import github
19
-
20
-
21
- logging.basicConfig(level=logging.INFO, format='%(message)s')
22
- logger = logging.getLogger(__name__)
23
-
24
-
25
- def add_creds(url, username, token):
26
- if token is None:
27
- new_url = url
28
- else:
29
- username = urllib.parse.quote(username)
30
- url_parts = urllib.parse.urlparse(url)
31
- netloc = f'{username}:{token}@{url_parts.netloc}'
32
- new_url = urllib.parse.urlunparse(url_parts._replace(netloc=netloc))
33
- return new_url
34
-
35
-
36
- def archive(local_repo_dir, archive_format='zip', is_gist=False):
37
- if archive_format not in ('tar', 'zip'):
38
- raise ValueError(f'{archive_format} is not a valid archive format')
39
- base_name = os.path.basename(local_repo_dir)
40
- extension = 'tar.gz' if archive_format == 'tar' else archive_format
41
- archive_name = f'{base_name}.{extension}'
42
- if is_gist:
43
- archive_name = f'gist-{archive_name}'
44
- parent_dir = os.path.dirname(local_repo_dir)
45
- archive_path = os.path.join(parent_dir, archive_name)
46
- logger.info(f'creating archive: {archive_path}')
47
- if archive_format == 'tar':
48
- with tarfile.open(archive_path, 'w:gz') as tar_archive:
49
- tar_archive.add(local_repo_dir, arcname=base_name)
50
- elif archive_format == 'zip':
51
- with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zip_archive:
52
- repo_path = Path(local_repo_dir)
53
- for entry in repo_path.rglob('*'):
54
- path = os.path.join(base_name, entry.relative_to(repo_path))
55
- zip_archive.write(entry, arcname=path)
56
- return archive_path
57
-
58
-
59
- def clone(repo_url, local_repo_dir, include_history):
60
- try:
61
- if include_history:
62
- # full clone
63
- repo = git.Repo.clone_from(repo_url, local_repo_dir)
64
- else:
65
- # shallow clone (no commit history or branches)
66
- repo = git.Repo.clone_from(repo_url, local_repo_dir, multi_options=['--depth=1'])
67
- except git.GitCommandError as e:
68
- logger.error(e)
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
109
- archive_path = archive(local_repo_dir, archive_format, is_gist)
110
- size_kb = os.path.getsize(archive_path) / 1024
111
- logger.info(f'archive size: {size_kb:.2f} KB')
112
- logger.info('deleting repo')
113
- try:
114
- # delete repo after archive is created
115
- shutil.rmtree(local_repo_dir, onexc=remove_readonly)
116
- except FileNotFoundError:
117
- pass
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')
121
-
122
-
123
- def get_user(username, prompt_token):
124
- if prompt_token:
125
- token = getpass.getpass('Token:')
126
- if not token:
127
- sys.exit('Error: auth token cannot be empty')
128
- auth = github.Auth.Token(token)
129
- gh = github.Github(auth=auth)
130
- else:
131
- token = os.getenv('GITHUB_TOKEN')
132
- if token is not None:
133
- auth = github.Auth.Token(token)
134
- gh = github.Github(auth=auth)
135
- else:
136
- gh = github.Github()
137
- if token is None:
138
- try:
139
- user = gh.get_user(username)
140
- except github.GithubException as e:
141
- if e.data['status'] == '404':
142
- sys.exit(f'Error: user "{username}" not found on GitHub')
143
- else:
144
- raise e
145
- repos = user.get_repos()
146
- else:
147
- # you need to be authenticated and then call the API
148
- # with no username to get private repos
149
- user = gh.get_user()
150
- repos = user.get_repos(affiliation='owner')
151
- try:
152
- # this just makes an API request so we can exit if unauthorized
153
- _ = repos.totalCount
154
- except github.GithubException as e:
155
- if e.data['status'] == '401':
156
- sys.exit(f'Error: invalid auth token for user "{username}" on GitHub')
157
- else:
158
- raise e
159
- gists = user.get_gists()
160
- return user, repos, gists, token
161
-
162
-
163
- def run(username, base_dir, archive_format, include_gists, include_history, list_only, prompt_token):
164
- working_dir = os.path.join(base_dir, 'backups')
165
- user, repos, gists, token = get_user(username, prompt_token)
166
- num_repos = repos.totalCount
167
- if not list_only:
168
- logger.info(f'creating archives in: {working_dir}\n')
169
- logger.info(f'found {num_repos} repos for user "{username}"\n')
170
- for repo in repos:
171
- local_repo_dir = os.path.join(working_dir, repo.name)
172
- url = add_creds(repo.clone_url, username, token)
173
- if list_only:
174
- logger.info(url)
175
- else:
176
- clone_and_archive_repo(
177
- url,
178
- local_repo_dir,
179
- archive_format,
180
- include_history
181
- )
182
- if include_gists:
183
- num_gists = gists.totalCount
184
- logger.info('')
185
- logger.info(f'found {num_gists} gists for user "{username}"\n')
186
- for gist in gists:
187
- local_repo_dir = os.path.join(working_dir, gist.id)
188
- url = add_creds(gist.git_pull_url, username, token)
189
- if list_only:
190
- logger.info(url)
191
- else:
192
- clone_and_archive_repo(
193
- url,
194
- local_repo_dir,
195
- archive_format,
196
- include_history,
197
- is_gist=True
198
- )
199
-
200
-
201
- def main():
202
- if sys.version_info < (3, 12):
203
- sys.exit('sorry, this program requires Python 3.12+')
204
- parser = argparse.ArgumentParser()
205
- parser.add_argument(
206
- 'username',
207
- help='github username'
208
- )
209
- parser.add_argument(
210
- '--dir',
211
- default=os.getcwd(),
212
- help='output directory'
213
- )
214
- parser.add_argument(
215
- '--format',
216
- default='zip',
217
- help='archive format (tar, zip)'
218
- )
219
- parser.add_argument(
220
- '--gists',
221
- action='store_true',
222
- default=False,
223
- help='include gists'
224
- )
225
- parser.add_argument(
226
- '--history',
227
- action='store_true',
228
- default=False,
229
- help='include commit history and branches (.git directory)'
230
- )
231
- parser.add_argument(
232
- '--list',
233
- action='store_true',
234
- default=False,
235
- help='list repos only'
236
- )
237
- parser.add_argument(
238
- '--token',
239
- action='store_true',
240
- default=False,
241
- help='prompt for auth token'
242
- )
243
- args = parser.parse_args()
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 ...')
256
-
257
-
258
- if __name__ == '__main__':
259
- main()
File without changes
File without changes
File without changes