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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: githubtakeout
3
- Version: 0.1.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.9
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.9+
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.9+
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.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.9
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.9+
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 tar:
47
- tar.add(local_repo_dir, arcname=base_name)
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 zip:
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
- zip.write(entry, arcname=path)
55
+ zip_archive.write(entry, arcname=path)
54
56
  return archive_path
55
57
 
56
58
 
57
- def clone_and_archive_repo(repo_url, local_repo_dir, archive_format, include_history, is_gist=False):
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
- git.Repo.clone_from(repo_url, local_repo_dir)
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
- return
78
- elapsed = default_timer() - start
79
- logger.info(f'elapsed time: {elapsed:.3f} secs')
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
- shutil.rmtree(local_repo_dir)
114
+ # delete repo after archive is created
115
+ shutil.rmtree(local_repo_dir, onexc=remove_readonly)
86
116
  except FileNotFoundError:
87
117
  pass
88
- logger.info('')
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
- print(f'Error: auth token cannot be empty')
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
- print(f'Error: user "{username}" not found on GitHub')
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
- print(f'Error: invalid auth token for user "{username}" on GitHub')
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, list, prompt_token):
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 list:
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 list:
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 list:
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
- run(
214
- username=args.username,
215
- base_dir=args.dir,
216
- archive_format=args.format,
217
- include_gists=args.gists,
218
- include_history=args.history,
219
- list=args.list,
220
- prompt_token=args.token
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.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.9"
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