githubtakeout 0.1.17__tar.gz → 0.1.19__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.17
3
+ Version: 0.1.19
4
4
  Summary: Backup and archive Git Repos and Gists from GitHub
5
5
  Author: Corey Goldberg
6
6
  Maintainer: Corey Goldberg
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "githubtakeout"
7
- version = "0.1.17"
7
+ version = "0.1.19"
8
8
  description = "Backup and archive Git Repos and Gists from GitHub"
9
9
  license = "MIT"
10
10
  license-files = ["LICENSE"]
@@ -73,14 +73,15 @@ respect-gitignore = true
73
73
  target-version = "py312"
74
74
 
75
75
  [tool.ruff.lint]
76
+ fixable = ["ALL"]
76
77
  select = [
77
- "D", "E", "F", "I", "PT", "RUF", "TID", "W"
78
+ "D", "E", "F", "I", "N", "FURB", "PT",
79
+ "RUF", "TID", "UP", "W"
78
80
  ]
79
81
  extend-ignore = [
80
- "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203",
81
- "D205", "D211", "D213", "D400", "D401", "RUF012"
82
+ "D100", "D101", "D102", "D103", "D105",
83
+ "D107", "D203", "D213", "RUF012"
82
84
  ]
83
- fixable = ["ALL"]
84
85
 
85
86
  [tool.ruff.format]
86
87
  docstring-code-format = true
@@ -89,6 +90,4 @@ docstring-code-line-length = 88
89
90
  [tool.refurb]
90
91
  python_version = "3.12"
91
92
  enable_all = true
92
- disable = [
93
- "FURB104", "FURB107", "FURB147", "FURB173", "FURB183", "FURB184"
94
- ]
93
+ disable = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: githubtakeout
3
- Version: 0.1.17
3
+ Version: 0.1.19
4
4
  Summary: Backup and archive Git Repos and Gists from GitHub
5
5
  Author: Corey Goldberg
6
6
  Maintainer: Corey Goldberg
@@ -15,6 +15,7 @@ import sys
15
15
  import tarfile
16
16
  import urllib
17
17
  import zipfile
18
+ from contextlib import suppress
18
19
  from pathlib import Path
19
20
  from timeit import default_timer
20
21
 
@@ -51,27 +52,27 @@ def add_creds(url, username, token):
51
52
  return new_url
52
53
 
53
54
 
54
- def archive(local_repo_dir, archive_format="zip", is_gist=False):
55
+ def archive(local_repo_dir, archive_format="zip", archive_basename=None):
55
56
  if archive_format not in ARCHIVE_FORMATS:
56
57
  raise ValueError(f"{archive_format} is not a valid archive format")
57
58
  if archive_format == "none":
58
59
  return None
59
- base_name = os.path.basename(local_repo_dir)
60
+ basename = local_repo_dir.stem
60
61
  extension = "tar.gz" if archive_format == "tar" else archive_format
61
- archive_name = f"{base_name}.{extension}"
62
- if is_gist:
63
- archive_name = f"gist-{archive_name}"
64
- parent_dir = os.path.dirname(local_repo_dir)
65
- archive_path = os.path.join(parent_dir, archive_name)
62
+ if archive_basename is None:
63
+ archive_name = f"{basename}.{extension}"
64
+ else:
65
+ archive_name = f"{archive_basename}.{extension}"
66
+ archive_path = local_repo_dir.parent / archive_name
66
67
  logger.info(f"creating archive: {archive_path}")
67
68
  if archive_format == "tar":
68
69
  with tarfile.open(archive_path, "w:gz") as tar_archive:
69
- tar_archive.add(local_repo_dir, arcname=base_name)
70
+ tar_archive.add(local_repo_dir, arcname=basename)
70
71
  elif archive_format == "zip":
71
72
  with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as zip_archive:
72
73
  repo_path = Path(local_repo_dir)
73
74
  for entry in repo_path.rglob("*"):
74
- path = os.path.join(base_name, entry.relative_to(repo_path))
75
+ path = basename / entry.relative_to(repo_path)
75
76
  zip_archive.write(entry, arcname=path)
76
77
  return archive_path
77
78
 
@@ -97,12 +98,10 @@ def clone(repo_url, local_repo_dir, include_history):
97
98
  logger.error(e)
98
99
  sys.exit("error: failed cloning repo")
99
100
  finally:
100
- try:
101
+ # `UnboundLocalError` can occur if we catch a signal while cloning
102
+ with suppress(UnboundLocalError):
101
103
  # release resources
102
104
  repo.close()
103
- except UnboundLocalError:
104
- # this occurs if we catch a signal while cloning
105
- pass
106
105
 
107
106
 
108
107
  def pull(local_repo_dir):
@@ -115,16 +114,14 @@ def pull(local_repo_dir):
115
114
  logger.error(e)
116
115
  sys.exit("error: failed pulling changes in repo")
117
116
  finally:
118
- try:
117
+ # `UnboundLocalError` can occur if we catch a signal while pulling
118
+ with suppress(UnboundLocalError):
119
119
  # release resources
120
120
  repo.close()
121
- except UnboundLocalError:
122
- # this occurs if we catch a signal while pulling
123
- pass
124
121
 
125
122
 
126
123
  def get_and_archive_repo(
127
- repo_url, local_repo_dir, archive_format, include_history, keep, is_gist=False
124
+ repo_url, local_repo_dir, archive_format, include_history, keep, description=None
128
125
  ):
129
126
  def remove_readonly(func, path, _):
130
127
  # This is necessary so rmtree() doesn't fail if there are any readonly
@@ -145,10 +142,8 @@ def get_and_archive_repo(
145
142
  needs_clone = False
146
143
  else:
147
144
  needs_clone = True
148
- try:
145
+ with suppress(FileNotFoundError):
149
146
  shutil.rmtree(local_repo_dir, onexc=remove_readonly)
150
- except FileNotFoundError:
151
- pass
152
147
  start = default_timer()
153
148
  if needs_clone:
154
149
  logger.info(f"cloning repo: {repo_name} to: {local_repo_dir}")
@@ -158,25 +153,30 @@ def get_and_archive_repo(
158
153
  pull(local_repo_dir)
159
154
  if not include_history:
160
155
  # delete the .git directory if we are not saving history
161
- try:
162
- git_dir = Path(local_repo_dir, ".git")
156
+ git_dir = Path(local_repo_dir, ".git")
157
+ with suppress(FileNotFoundError):
163
158
  shutil.rmtree(git_dir, onexc=remove_readonly)
164
- except FileNotFoundError:
165
- pass
166
- archive_path = archive(local_repo_dir, archive_format, is_gist)
159
+ if description:
160
+ # clean unsafe chars and truncate description to create a useable file name
161
+ clean_name = re.sub(r"[/\\?%*:|\"<>\x7F\x00-\x1F]", "-", description)[:255]
162
+ archive_basename = f"gist - {clean_name}"
163
+ else:
164
+ archive_basename = None
165
+ archive_path = archive(
166
+ local_repo_dir, archive_format, archive_basename=archive_basename
167
+ )
167
168
  if archive_path:
168
- size = convert_size(os.path.getsize(archive_path))
169
+ size = convert_size(archive_path.stat().st_size)
169
170
  logger.info(f"archive size: {size}")
170
171
  if not keep:
172
+ # delete repo after archive is created
171
173
  logger.info("deleting repo")
172
- try:
173
- # delete repo after archive is created
174
+ with suppress(FileNotFoundError):
174
175
  shutil.rmtree(local_repo_dir, onexc=remove_readonly)
175
- except FileNotFoundError:
176
- pass
177
176
  elapsed = default_timer() - start
178
- base_name = os.path.basename(local_repo_dir)
179
- logger.info(f"successfully backed up '{base_name}' repo in {elapsed:.3f} secs\n")
177
+ logger.info(
178
+ f"successfully backed up '{local_repo_dir.stem}' repo in {elapsed:.3f} secs\n"
179
+ )
180
180
 
181
181
 
182
182
  def get_repos(username, token, include_gists):
@@ -246,7 +246,7 @@ def run(
246
246
  list_only,
247
247
  prompt_for_token,
248
248
  ):
249
- working_dir = os.path.join(base_dir, "backups")
249
+ working_dir = base_dir / "backups"
250
250
  token = get_token(prompt_for_token)
251
251
 
252
252
  all_repos, gists = get_repos(username, token, include_gists)
@@ -262,7 +262,7 @@ def run(
262
262
  logger.info(f"creating archives in: {working_dir}\n")
263
263
  logger.info(f"found {num_repos} repos for user '{username}':\n")
264
264
  for repo in repos:
265
- local_repo_dir = os.path.join(working_dir, repo.name)
265
+ local_repo_dir = working_dir / repo.name
266
266
  url = add_creds(repo.clone_url, username, token)
267
267
  if list_only:
268
268
  logger.info(f"{username}/{repo.name}")
@@ -274,7 +274,7 @@ def run(
274
274
  logger.info("")
275
275
  logger.info(f"found {num_gists} gists for user '{username}':\n")
276
276
  for gist in gists:
277
- local_repo_dir = os.path.join(working_dir, gist.id)
277
+ local_repo_dir = working_dir / gist.id
278
278
  url = add_creds(gist.git_pull_url, username, token)
279
279
  if list_only:
280
280
  logger.info(f"{username}/{gist.id}\n - {gist.description}")
@@ -285,14 +285,11 @@ def run(
285
285
  archive_format,
286
286
  include_history,
287
287
  keep,
288
- is_gist=True,
288
+ description=gist.description,
289
289
  )
290
290
 
291
291
 
292
292
  def main():
293
- if sys.version_info < (3, 12):
294
- sys.exit("sorry, this program requires Python 3.12+")
295
-
296
293
  def formatter(prog):
297
294
  return argparse.HelpFormatter(prog, max_help_position=30)
298
295
 
@@ -300,7 +297,7 @@ def main():
300
297
  parser.add_argument("username", help="github username")
301
298
  parser.add_argument(
302
299
  "--dir",
303
- default=os.getcwd(),
300
+ default=Path.cwd(),
304
301
  help="output directory (default: .)",
305
302
  )
306
303
  parser.add_argument(
@@ -347,7 +344,7 @@ def main():
347
344
  try:
348
345
  run(
349
346
  username=args.username,
350
- base_dir=args.dir,
347
+ base_dir=Path(args.dir),
351
348
  pattern=args.pattern,
352
349
  skip_pattern=args.skip_pattern,
353
350
  archive_format=args.format,
File without changes
File without changes
File without changes