githubtakeout 0.1.16__tar.gz → 0.1.17__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.16
3
+ Version: 0.1.17
4
4
  Summary: Backup and archive Git Repos and Gists from GitHub
5
5
  Author: Corey Goldberg
6
6
  Maintainer: Corey Goldberg
@@ -101,24 +101,27 @@ the `--token` argument.
101
101
  ## CLI Options:
102
102
 
103
103
  ```
104
- usage: githubtakeout [-h] [--dir DIR] [--pattern PATTERN] [--format {tar,zip,none}]
105
- [--gists] [--history] [--keep] [--list] [--token]
104
+ $ githubtakeout --help
105
+ usage: githubtakeout [-h] [--dir DIR] [--pattern PATTERN] [--skip_pattern PATTERN]
106
+ [--format {tar,zip,none}] [--gists] [--history]
107
+ [--skip_forks] [--keep] [--list] [--token]
106
108
  username
107
109
 
108
110
  positional arguments:
109
- username github username
111
+ username github username
110
112
 
111
113
  options:
112
- -h, --help show this help message and exit
113
- --dir DIR output directory (default: .)
114
- --pattern PATTERN regex matching repo names to include
115
- --skip_pattern PATTERN regex matching repo names to skip
116
- --format {tar,zip,none} archive format (default: zip)
117
- --gists include gists
118
- --history include commit history and branches (.git directory)
119
- --keep keep repos after archiving
120
- --list list repos only
121
- --token prompt for auth token
114
+ -h, --help show this help message and exit
115
+ --dir DIR output directory (default: .)
116
+ --pattern PATTERN regex matching repo names to include
117
+ --skip_pattern PATTERN regex matching repo names to skip
118
+ --format {tar,zip,none} archive format (default: zip)
119
+ --gists include gists
120
+ --history include commit history and branches (.git directory)
121
+ --skip_forks skip repos that are forks
122
+ --keep keep repos after archiving
123
+ --list list repos only
124
+ --token prompt for auth token
122
125
  ```
123
126
 
124
127
  ## Usage Examples:
@@ -74,24 +74,27 @@ the `--token` argument.
74
74
  ## CLI Options:
75
75
 
76
76
  ```
77
- usage: githubtakeout [-h] [--dir DIR] [--pattern PATTERN] [--format {tar,zip,none}]
78
- [--gists] [--history] [--keep] [--list] [--token]
77
+ $ githubtakeout --help
78
+ usage: githubtakeout [-h] [--dir DIR] [--pattern PATTERN] [--skip_pattern PATTERN]
79
+ [--format {tar,zip,none}] [--gists] [--history]
80
+ [--skip_forks] [--keep] [--list] [--token]
79
81
  username
80
82
 
81
83
  positional arguments:
82
- username github username
84
+ username github username
83
85
 
84
86
  options:
85
- -h, --help show this help message and exit
86
- --dir DIR output directory (default: .)
87
- --pattern PATTERN regex matching repo names to include
88
- --skip_pattern PATTERN regex matching repo names to skip
89
- --format {tar,zip,none} archive format (default: zip)
90
- --gists include gists
91
- --history include commit history and branches (.git directory)
92
- --keep keep repos after archiving
93
- --list list repos only
94
- --token prompt for auth token
87
+ -h, --help show this help message and exit
88
+ --dir DIR output directory (default: .)
89
+ --pattern PATTERN regex matching repo names to include
90
+ --skip_pattern PATTERN regex matching repo names to skip
91
+ --format {tar,zip,none} archive format (default: zip)
92
+ --gists include gists
93
+ --history include commit history and branches (.git directory)
94
+ --skip_forks skip repos that are forks
95
+ --keep keep repos after archiving
96
+ --list list repos only
97
+ --token prompt for auth token
95
98
  ```
96
99
 
97
100
  ## Usage Examples:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "githubtakeout"
7
- version = "0.1.16"
7
+ version = "0.1.17"
8
8
  description = "Backup and archive Git Repos and Gists from GitHub"
9
9
  license = "MIT"
10
10
  license-files = ["LICENSE"]
@@ -62,13 +62,13 @@ addopts = "-vv" # extra verbose
62
62
  tmp_path_retention_policy = "none"
63
63
 
64
64
  [tool.black]
65
- line-length = 120
65
+ line-length = 88
66
66
  target-version = [
67
67
  "py312", "py313", "py314"
68
68
  ]
69
69
 
70
70
  [tool.ruff]
71
- line-length = 120
71
+ line-length = 88
72
72
  respect-gitignore = true
73
73
  target-version = "py312"
74
74
 
@@ -84,10 +84,10 @@ fixable = ["ALL"]
84
84
 
85
85
  [tool.ruff.format]
86
86
  docstring-code-format = true
87
- docstring-code-line-length = 120
87
+ docstring-code-line-length = 88
88
88
 
89
89
  [tool.refurb]
90
- python_version = "3.9"
90
+ python_version = "3.12"
91
91
  enable_all = true
92
92
  disable = [
93
93
  "FURB104", "FURB107", "FURB147", "FURB173", "FURB183", "FURB184"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: githubtakeout
3
- Version: 0.1.16
3
+ Version: 0.1.17
4
4
  Summary: Backup and archive Git Repos and Gists from GitHub
5
5
  Author: Corey Goldberg
6
6
  Maintainer: Corey Goldberg
@@ -101,24 +101,27 @@ the `--token` argument.
101
101
  ## CLI Options:
102
102
 
103
103
  ```
104
- usage: githubtakeout [-h] [--dir DIR] [--pattern PATTERN] [--format {tar,zip,none}]
105
- [--gists] [--history] [--keep] [--list] [--token]
104
+ $ githubtakeout --help
105
+ usage: githubtakeout [-h] [--dir DIR] [--pattern PATTERN] [--skip_pattern PATTERN]
106
+ [--format {tar,zip,none}] [--gists] [--history]
107
+ [--skip_forks] [--keep] [--list] [--token]
106
108
  username
107
109
 
108
110
  positional arguments:
109
- username github username
111
+ username github username
110
112
 
111
113
  options:
112
- -h, --help show this help message and exit
113
- --dir DIR output directory (default: .)
114
- --pattern PATTERN regex matching repo names to include
115
- --skip_pattern PATTERN regex matching repo names to skip
116
- --format {tar,zip,none} archive format (default: zip)
117
- --gists include gists
118
- --history include commit history and branches (.git directory)
119
- --keep keep repos after archiving
120
- --list list repos only
121
- --token prompt for auth token
114
+ -h, --help show this help message and exit
115
+ --dir DIR output directory (default: .)
116
+ --pattern PATTERN regex matching repo names to include
117
+ --skip_pattern PATTERN regex matching repo names to skip
118
+ --format {tar,zip,none} archive format (default: zip)
119
+ --gists include gists
120
+ --history include commit history and branches (.git directory)
121
+ --skip_forks skip repos that are forks
122
+ --keep keep repos after archiving
123
+ --list list repos only
124
+ --token prompt for auth token
122
125
  ```
123
126
 
124
127
  ## Usage Examples:
@@ -90,7 +90,7 @@ def clone(repo_url, local_repo_dir, include_history):
90
90
  repo = git.Repo.clone_from(
91
91
  url=repo_url,
92
92
  to_path=local_repo_dir,
93
- multi_options=["--single-branch", "--depth=1"],
93
+ multi_options=["--depth=1"],
94
94
  progress=GitProgress(),
95
95
  )
96
96
  except git.GitCommandError as e:
@@ -123,7 +123,9 @@ def pull(local_repo_dir):
123
123
  pass
124
124
 
125
125
 
126
- def get_and_archive_repo(repo_url, local_repo_dir, archive_format, include_history, keep, is_gist=False):
126
+ def get_and_archive_repo(
127
+ repo_url, local_repo_dir, archive_format, include_history, keep, is_gist=False
128
+ ):
127
129
  def remove_readonly(func, path, _):
128
130
  # This is necessary so rmtree() doesn't fail if there are any readonly
129
131
  # dirs/files when trying to delete. This seems to happen after cloning on
@@ -206,10 +208,19 @@ def get_repos(username, token, include_gists):
206
208
  if include_gists:
207
209
  gists = user.get_gists()
208
210
  else:
209
- gists = None
211
+ gists = []
210
212
  return repos, gists
211
213
 
212
214
 
215
+ def filter_repos(repos, pattern, skip_pattern, skip_forks):
216
+ repos = (repo for repo in repos if re.match(pattern, repo.name))
217
+ if skip_pattern:
218
+ repos = (repo for repo in repos if not re.match(skip_pattern, repo.name))
219
+ if skip_forks:
220
+ repos = (repo for repo in repos if not repo.fork)
221
+ return repos
222
+
223
+
213
224
  def get_token(prompt_for_token):
214
225
  if prompt_for_token:
215
226
  token = getpass.getpass("Token:")
@@ -230,49 +241,52 @@ def run(
230
241
  archive_format,
231
242
  include_gists,
232
243
  include_history,
244
+ skip_forks,
233
245
  keep,
234
246
  list_only,
235
247
  prompt_for_token,
236
248
  ):
237
249
  working_dir = os.path.join(base_dir, "backups")
238
250
  token = get_token(prompt_for_token)
239
- repos, gists = get_repos(username, token, include_gists)
240
- num_repos = len(
241
- [
242
- repo
243
- for repo in repos
244
- if re.match(pattern, repo.name) and not (re.match(skip_pattern, repo.name) if skip_pattern else False)
245
- ]
246
- )
251
+
252
+ all_repos, gists = get_repos(username, token, include_gists)
253
+ repos = filter_repos(all_repos, pattern, skip_pattern, skip_forks)
254
+ num_repos = sum(1 for repo in repos)
255
+ num_gists = sum(1 for gist in gists)
256
+
257
+ # we have to fetch the repos again because we exhaust the generator
258
+ # when counting repos above
259
+ all_repos, gists = get_repos(username, token, include_gists)
260
+ repos = filter_repos(all_repos, pattern, skip_pattern, skip_forks)
247
261
  if not list_only:
248
262
  logger.info(f"creating archives in: {working_dir}\n")
249
263
  logger.info(f"found {num_repos} repos for user '{username}':\n")
250
264
  for repo in repos:
251
- if re.match(pattern, repo.name) and not (re.match(skip_pattern, repo.name) if skip_pattern else False):
252
- local_repo_dir = os.path.join(working_dir, repo.name)
253
- url = add_creds(repo.clone_url, username, token)
254
- if list_only:
255
- logger.info(f"{username}/{repo.name}")
256
- else:
257
- get_and_archive_repo(url, local_repo_dir, archive_format, include_history, keep)
258
- if gists is not None:
259
- num_gists = len(
260
- [
261
- gist
262
- for gist in gists
263
- if re.match(pattern, gist.id) and not (re.match(skip_pattern, gist.id) if skip_pattern else False)
264
- ]
265
- )
265
+ local_repo_dir = os.path.join(working_dir, repo.name)
266
+ url = add_creds(repo.clone_url, username, token)
267
+ if list_only:
268
+ logger.info(f"{username}/{repo.name}")
269
+ else:
270
+ get_and_archive_repo(
271
+ url, local_repo_dir, archive_format, include_history, keep
272
+ )
273
+ if include_gists:
266
274
  logger.info("")
267
275
  logger.info(f"found {num_gists} gists for user '{username}':\n")
268
276
  for gist in gists:
269
- if re.match(pattern, gist.id) and not (re.match(skip_pattern, gist.id) if skip_pattern else False):
270
- local_repo_dir = os.path.join(working_dir, gist.id)
271
- url = add_creds(gist.git_pull_url, username, token)
272
- if list_only:
273
- logger.info(f"{username}/{gist.id}\n - {gist.description}")
274
- else:
275
- get_and_archive_repo(url, local_repo_dir, archive_format, include_history, keep, is_gist=True)
277
+ local_repo_dir = os.path.join(working_dir, gist.id)
278
+ url = add_creds(gist.git_pull_url, username, token)
279
+ if list_only:
280
+ logger.info(f"{username}/{gist.id}\n - {gist.description}")
281
+ else:
282
+ get_and_archive_repo(
283
+ url,
284
+ local_repo_dir,
285
+ archive_format,
286
+ include_history,
287
+ keep,
288
+ is_gist=True,
289
+ )
276
290
 
277
291
 
278
292
  def main():
@@ -305,16 +319,30 @@ def main():
305
319
  default="zip",
306
320
  help="archive format (default: %(default)s)",
307
321
  )
308
- parser.add_argument("--gists", action="store_true", default=False, help="include gists")
322
+ parser.add_argument(
323
+ "--gists", action="store_true", default=False, help="include gists"
324
+ )
309
325
  parser.add_argument(
310
326
  "--history",
311
327
  action="store_true",
312
328
  default=False,
313
329
  help="include commit history and branches (.git directory)",
314
330
  )
315
- parser.add_argument("--keep", action="store_true", default=False, help="keep repos after archiving")
316
- parser.add_argument("--list", action="store_true", default=False, help="list repos only")
317
- parser.add_argument("--token", action="store_true", default=False, help="prompt for auth token")
331
+ parser.add_argument(
332
+ "--skip_forks",
333
+ action="store_true",
334
+ default=False,
335
+ help="skip repos that are forks",
336
+ )
337
+ parser.add_argument(
338
+ "--keep", action="store_true", default=False, help="keep repos after archiving"
339
+ )
340
+ parser.add_argument(
341
+ "--list", action="store_true", default=False, help="list repos only"
342
+ )
343
+ parser.add_argument(
344
+ "--token", action="store_true", default=False, help="prompt for auth token"
345
+ )
318
346
  args = parser.parse_args()
319
347
  try:
320
348
  run(
@@ -325,6 +353,7 @@ def main():
325
353
  archive_format=args.format,
326
354
  include_gists=args.gists,
327
355
  include_history=args.history,
356
+ skip_forks=args.skip_forks,
328
357
  keep=args.keep,
329
358
  list_only=args.list,
330
359
  prompt_for_token=args.token,
@@ -20,7 +20,9 @@ class GitProgress(git.RemoteProgress):
20
20
  "RESOLVING",
21
21
  "WRITING",
22
22
  ]
23
- OP_CODE_MAP = {getattr(git.RemoteProgress, _op_code): _op_code for _op_code in OP_CODES}
23
+ OP_CODE_MAP = {
24
+ getattr(git.RemoteProgress, _op_code): _op_code for _op_code in OP_CODES
25
+ }
24
26
 
25
27
  def __init__(self):
26
28
  super().__init__()
@@ -15,10 +15,15 @@ def require_token():
15
15
  load_dotenv()
16
16
  required = "GITHUB_TOKEN"
17
17
  if not os.getenv(required):
18
- pytest.skip(reason=f"{required} environment variable or .env file is required to avoid rate limiting")
18
+ pytest.skip(
19
+ reason=f"{required} environment variable or .env file is "
20
+ "required to avoid rate limiting"
21
+ )
19
22
 
20
23
 
21
- @pytest.mark.skipif(os.getenv("GITHUB_TOKEN"), reason="GITHUB_TOKEN required to avoid rate limiting")
24
+ @pytest.mark.skipif(
25
+ os.getenv("GITHUB_TOKEN"), reason="GITHUB_TOKEN required to avoid rate limiting"
26
+ )
22
27
  def test_run_list_1_match(tmp_path, caplog):
23
28
  caplog.set_level("INFO")
24
29
  repo = "githubtakeout"
@@ -30,13 +35,14 @@ def test_run_list_1_match(tmp_path, caplog):
30
35
  archive_format="none",
31
36
  include_gists=False,
32
37
  include_history=False,
38
+ skip_forks=False,
33
39
  keep=False,
34
40
  list_only=True,
35
41
  prompt_for_token=False,
36
42
  )
37
43
  assert "creating archives" not in caplog.text
38
44
  assert f"found 1 repos for user '{USER}'" in caplog.text
39
- assert "gists" not in caplog.text
45
+ assert not re.search(r"found \d* gists", caplog.text)
40
46
  assert f"{USER}/{repo}" in caplog.text
41
47
  assert not Path(tmp_path / "backups" / repo).exists()
42
48
 
@@ -52,13 +58,14 @@ def test_run_list_0_match(tmp_path, caplog):
52
58
  archive_format="none",
53
59
  include_gists=False,
54
60
  include_history=False,
61
+ skip_forks=False,
55
62
  keep=False,
56
63
  list_only=True,
57
64
  prompt_for_token=False,
58
65
  )
59
66
  assert "creating archives" not in caplog.text
60
- assert f"found 0 repos for user '{USER}'" in caplog.text
61
- assert "gists" not in caplog.text
67
+ assert "found 0 repos" in caplog.text
68
+ assert not re.search(r"found \d* gists", caplog.text)
62
69
  assert not Path(tmp_path / "backups" / repo).exists()
63
70
 
64
71
 
@@ -73,13 +80,14 @@ def test_run_list_0_match_with_gists(tmp_path, caplog):
73
80
  archive_format="none",
74
81
  include_gists=True,
75
82
  include_history=False,
83
+ skip_forks=False,
76
84
  keep=False,
77
85
  list_only=True,
78
86
  prompt_for_token=False,
79
87
  )
80
88
  assert "creating archives" not in caplog.text
81
- assert f"found 0 repos for user '{USER}'" in caplog.text
82
- assert f"found 0 gists for user '{USER}'" in caplog.text
89
+ assert "found 0 repos" in caplog.text
90
+ assert re.search(r"found \d* gists", caplog.text)
83
91
  assert not Path(tmp_path / "backups" / repo).exists()
84
92
 
85
93
 
@@ -89,16 +97,17 @@ def test_run_list_skip_all(tmp_path, caplog):
89
97
  username=USER,
90
98
  base_dir=tmp_path,
91
99
  pattern=".*",
92
- skip_pattern=".",
100
+ skip_pattern=".*",
93
101
  archive_format="none",
94
102
  include_gists=False,
95
103
  include_history=False,
104
+ skip_forks=False,
96
105
  keep=False,
97
106
  list_only=True,
98
107
  prompt_for_token=False,
99
108
  )
100
109
  assert "creating archives" not in caplog.text
101
- assert f"found 0 repos for user '{USER}'" in caplog.text
110
+ assert "found 0 repos" in caplog.text
102
111
 
103
112
 
104
113
  @pytest.mark.parametrize("archive_format", ["zip", "tar"])
@@ -114,6 +123,7 @@ def test_run_archive_1_match(archive_format, tmp_path, caplog):
114
123
  archive_format=archive_format,
115
124
  include_gists=False,
116
125
  include_history=False,
126
+ skip_forks=False,
117
127
  keep=False,
118
128
  list_only=False,
119
129
  prompt_for_token=False,
@@ -146,6 +156,7 @@ def test_run_no_archive_1_match_with_history(tmp_path, caplog):
146
156
  archive_format="none",
147
157
  include_gists=False,
148
158
  include_history=True,
159
+ skip_forks=False,
149
160
  keep=False,
150
161
  list_only=False,
151
162
  prompt_for_token=False,
@@ -176,6 +187,7 @@ def test_run_no_archive_1_match_without_history(tmp_path, caplog):
176
187
  archive_format="none",
177
188
  include_gists=False,
178
189
  include_history=False,
190
+ skip_forks=False,
179
191
  keep=False, # this doesn't matter when archive_format is "none"
180
192
  list_only=False,
181
193
  prompt_for_token=False,
@@ -206,6 +218,7 @@ def test_run_archive_1_match_with_history_pull_with_keep(tmp_path, caplog):
206
218
  archive_format="none",
207
219
  include_gists=False,
208
220
  include_history=True,
221
+ skip_forks=False,
209
222
  keep=True,
210
223
  list_only=False,
211
224
  prompt_for_token=False,
@@ -229,6 +242,7 @@ def test_run_archive_1_match_with_history_pull_with_keep(tmp_path, caplog):
229
242
  archive_format="zip",
230
243
  include_gists=False,
231
244
  include_history=True,
245
+ skip_forks=False,
232
246
  keep=True,
233
247
  list_only=False,
234
248
  prompt_for_token=False,
@@ -236,7 +250,9 @@ def test_run_archive_1_match_with_history_pull_with_keep(tmp_path, caplog):
236
250
  assert f"found 1 repos for user '{USER}'" in caplog.text
237
251
  assert "gists" not in caplog.text
238
252
  assert not re.search(f"cloning repo: {USER}/{repo}.git to: .*backups", caplog.text)
239
- assert re.search(f"pulling changes from repo: {USER}/{repo}.git to: .*backups", caplog.text)
253
+ assert re.search(
254
+ f"pulling changes from repo: {USER}/{repo}.git to: .*backups", caplog.text
255
+ )
240
256
  assert "creating archive:" in caplog.text
241
257
  assert "archive size:" in caplog.text
242
258
  assert "deleting repo" not in caplog.text
File without changes
File without changes