github-backup 0.63.0__tar.gz → 0.64.1__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.
Files changed (39) hide show
  1. {github_backup-0.63.0 → github_backup-0.64.1}/CHANGES.rst +86 -1
  2. {github_backup-0.63.0/github_backup.egg-info → github_backup-0.64.1}/PKG-INFO +1 -1
  3. github_backup-0.64.1/github_backup/__init__.py +1 -0
  4. {github_backup-0.63.0 → github_backup-0.64.1}/github_backup/cli.py +1 -1
  5. {github_backup-0.63.0 → github_backup-0.64.1}/github_backup/github_backup.py +54 -30
  6. {github_backup-0.63.0 → github_backup-0.64.1/github_backup.egg-info}/PKG-INFO +1 -1
  7. {github_backup-0.63.0 → github_backup-0.64.1}/github_backup.egg-info/SOURCES.txt +1 -0
  8. {github_backup-0.63.0 → github_backup-0.64.1}/release-requirements.txt +2 -2
  9. github_backup-0.64.1/tests/test_logging_subprocess.py +113 -0
  10. github_backup-0.63.0/github_backup/__init__.py +0 -1
  11. {github_backup-0.63.0 → github_backup-0.64.1}/LICENSE.txt +0 -0
  12. {github_backup-0.63.0 → github_backup-0.64.1}/MANIFEST.in +0 -0
  13. {github_backup-0.63.0 → github_backup-0.64.1}/README +0 -0
  14. {github_backup-0.63.0 → github_backup-0.64.1}/README.rst +0 -0
  15. {github_backup-0.63.0 → github_backup-0.64.1}/github_backup/__main__.py +0 -0
  16. {github_backup-0.63.0 → github_backup-0.64.1}/github_backup/graphql_queries.py +0 -0
  17. {github_backup-0.63.0 → github_backup-0.64.1}/github_backup.egg-info/dependency_links.txt +0 -0
  18. {github_backup-0.63.0 → github_backup-0.64.1}/github_backup.egg-info/entry_points.txt +0 -0
  19. {github_backup-0.63.0 → github_backup-0.64.1}/github_backup.egg-info/top_level.txt +0 -0
  20. {github_backup-0.63.0 → github_backup-0.64.1}/github_backup.egg-info/zip-safe +0 -0
  21. {github_backup-0.63.0 → github_backup-0.64.1}/requirements.txt +0 -0
  22. {github_backup-0.63.0 → github_backup-0.64.1}/setup.cfg +0 -0
  23. {github_backup-0.63.0 → github_backup-0.64.1}/setup.py +0 -0
  24. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_all_starred.py +0 -0
  25. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_attachments.py +0 -0
  26. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_auth.py +0 -0
  27. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_case_sensitivity.py +0 -0
  28. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_disabled_issues_pulls.py +0 -0
  29. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_discussions.py +0 -0
  30. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_http_451.py +0 -0
  31. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_incremental_per_repository.py +0 -0
  32. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_json_dump_if_changed.py +0 -0
  33. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_pagination.py +0 -0
  34. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_pull_incremental_pagination.py +0 -0
  35. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_pull_reviews.py +0 -0
  36. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_releases.py +0 -0
  37. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_retrieve_data.py +0 -0
  38. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_skip_assets_on.py +0 -0
  39. {github_backup-0.63.0 → github_backup-0.64.1}/tests/test_starred_skip_size_over.py +0 -0
@@ -1,9 +1,94 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
- 0.63.0 (2026-06-17)
4
+ 0.64.1 (2026-07-17)
5
5
  -------------------
6
6
  ------------------------
7
+ - Fix exception when DMCA'd/empty gists were backed up. [Duncan Ogilvie]
8
+
9
+
10
+ 0.64.0 (2026-07-07)
11
+ -------------------
12
+
13
+ Fix
14
+ ~~~
15
+ - Drain subprocess pipes with reader threads to prevent Windows deadlock
16
+ (#519) [Rodos]
17
+
18
+ On Windows the child's stdout/stderr pipes were never drained, so any
19
+ git operation producing more output than the pipe buffer (~8KB) hung
20
+ the backup at the first sizeable clone or fetch. Reader threads now
21
+ drain both pipes on all platforms, replacing the select() loop.
22
+
23
+ - Windows no longer hangs and now gets subprocess logging
24
+ - Log lines no longer carry a stray \r on Windows, and a final line
25
+ without a trailing newline is no longer truncated
26
+ - CPU no longer pinned at 100% during git operations on Windows
27
+
28
+ Adds tests covering the deadlock, log levels, and line handling.
29
+
30
+ Fixes #519
31
+
32
+ Other
33
+ ~~~~~
34
+ - Applied diff issuecomment-4900281663. [Peter Gabaldon]
35
+ - Fix template URL for watched repositories backup. [Peter Gabaldon]
36
+ - Chore(deps): bump setuptools in the python-packages group.
37
+ [dependabot[bot]]
38
+
39
+ Bumps the python-packages group with 1 update: [setuptools](https://github.com/pypa/setuptools).
40
+
41
+
42
+ Updates `setuptools` from 82.0.1 to 83.0.0
43
+ - [Release notes](https://github.com/pypa/setuptools/releases)
44
+ - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
45
+ - [Commits](https://github.com/pypa/setuptools/compare/v82.0.1...v83.0.0)
46
+
47
+ ---
48
+ updated-dependencies:
49
+ - dependency-name: setuptools
50
+ dependency-version: 83.0.0
51
+ dependency-type: direct:production
52
+ update-type: version-update:semver-major
53
+ dependency-group: python-packages
54
+ ...
55
+ - Chore(deps): bump actions/checkout from 6 to 7. [dependabot[bot]]
56
+
57
+ Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
58
+ - [Release notes](https://github.com/actions/checkout/releases)
59
+ - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
60
+ - [Commits](https://github.com/actions/checkout/compare/v6...v7)
61
+
62
+ ---
63
+ updated-dependencies:
64
+ - dependency-name: actions/checkout
65
+ dependency-version: '7'
66
+ dependency-type: direct:production
67
+ update-type: version-update:semver-major
68
+ ...
69
+ - Chore(deps): bump pytest in the python-packages group.
70
+ [dependabot[bot]]
71
+
72
+ Bumps the python-packages group with 1 update: [pytest](https://github.com/pytest-dev/pytest).
73
+
74
+
75
+ Updates `pytest` from 9.1.0 to 9.1.1
76
+ - [Release notes](https://github.com/pytest-dev/pytest/releases)
77
+ - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
78
+ - [Commits](https://github.com/pytest-dev/pytest/compare/9.1.0...9.1.1)
79
+
80
+ ---
81
+ updated-dependencies:
82
+ - dependency-name: pytest
83
+ dependency-version: 9.1.1
84
+ dependency-type: direct:production
85
+ update-type: version-update:semver-patch
86
+ dependency-group: python-packages
87
+ ...
88
+
89
+
90
+ 0.63.0 (2026-06-17)
91
+ -------------------
7
92
  - Skip repositories with pull requests disabled (HTTP 404) instead of
8
93
  crashing (#511) [Rodos]
9
94
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-backup
3
- Version: 0.63.0
3
+ Version: 0.64.1
4
4
  Summary: backup a github user or organization
5
5
  Home-page: http://github.com/josegonzalez/python-github-backup
6
6
  Author: Jose Diaz-Gonzalez
@@ -0,0 +1 @@
1
+ __version__ = "0.64.1"
@@ -81,7 +81,7 @@ def main():
81
81
  repositories = retrieve_repositories(args, authenticated_user)
82
82
  repositories = filter_repositories(args, repositories)
83
83
  backup_repositories(args, output_directory, repositories)
84
- backup_account(args, output_directory)
84
+ backup_account(args, output_directory, authenticated_user)
85
85
 
86
86
 
87
87
  if __name__ == "__main__":
@@ -12,11 +12,11 @@ import os
12
12
  import platform
13
13
  import random
14
14
  import re
15
- import select
16
15
  import socket
17
16
  import ssl
18
17
  import subprocess
19
18
  import sys
19
+ import threading
20
20
  import time
21
21
  from collections.abc import Generator
22
22
  from datetime import datetime
@@ -91,32 +91,37 @@ def logging_subprocess(
91
91
  child = subprocess.Popen(
92
92
  popenargs, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs
93
93
  )
94
- if sys.platform == "win32":
95
- logger.info(
96
- "Windows operating system detected - no subprocess logging will be returned"
97
- )
98
-
99
- log_level = {child.stdout: stdout_log_level, child.stderr: stderr_log_level}
100
-
101
- def check_io():
102
- if sys.platform == "win32":
103
- return
104
- ready_to_read = select.select([child.stdout, child.stderr], [], [], 1000)[0]
105
- for io in ready_to_read:
106
- line = io.readline()
107
- if not logger:
108
- continue
109
- if not (io == child.stderr and not line):
110
- logger.log(log_level[io], line[:-1])
111
94
 
112
- # keep checking stdout/stderr until the child exits
113
- while child.poll() is None:
114
- check_io()
115
-
116
- check_io() # check again to catch anything after the process exits
95
+ def log_output(pipe, log_level):
96
+ # Drain the pipe from a thread so the child never blocks on a full
97
+ # pipe buffer (issue #519), logging each line as it arrives.
98
+ with pipe:
99
+ for line in iter(pipe.readline, b""):
100
+ try:
101
+ logger.log(log_level, line.rstrip(b"\r\n"))
102
+ except Exception:
103
+ # Keep draining even if logging fails, or the child
104
+ # blocks on a full pipe buffer again
105
+ pass
106
+
107
+ threads = [
108
+ threading.Thread(
109
+ target=log_output, args=(child.stdout, stdout_log_level), daemon=True
110
+ ),
111
+ threading.Thread(
112
+ target=log_output, args=(child.stderr, stderr_log_level), daemon=True
113
+ ),
114
+ ]
115
+ for thread in threads:
116
+ thread.start()
117
117
 
118
118
  rc = child.wait()
119
119
 
120
+ # Timeout in case a grandchild inherited the pipe handles and keeps
121
+ # them open past the child's exit, which would delay EOF indefinitely
122
+ for thread in threads:
123
+ thread.join(timeout=60)
124
+
120
125
  if rc != 0:
121
126
  print("{} returned {}:".format(popenargs[0], rc), file=sys.stderr)
122
127
  print("\t", " ".join(popenargs), file=sys.stderr)
@@ -2104,7 +2109,10 @@ def backup_repositories(args, output_directory, repositories):
2104
2109
  )
2105
2110
 
2106
2111
  if repository.get("is_gist"):
2107
- # dump gist information to a file as well
2112
+ # dump gist information to a file as well; the clone may have
2113
+ # been skipped (e.g. DMCA-blocked or empty gist), so make sure
2114
+ # the directory exists
2115
+ mkdir_p(repo_cwd)
2108
2116
  output_file = "{0}/gist.json".format(repo_cwd)
2109
2117
  with codecs.open(output_file, "w", encoding="utf-8") as f:
2110
2118
  json_dump(repository, f)
@@ -3063,8 +3071,10 @@ def fetch_repository(
3063
3071
  logging_subprocess(git_command, cwd=local_dir)
3064
3072
 
3065
3073
 
3066
- def backup_account(args, output_directory):
3074
+ def backup_account(args, output_directory, authenticated_user=None):
3067
3075
  account_cwd = os.path.join(output_directory, "account")
3076
+ if authenticated_user is None:
3077
+ authenticated_user = {"login": None}
3068
3078
 
3069
3079
  if args.include_starred or args.include_everything:
3070
3080
  output_file = "{0}/starred.json".format(account_cwd)
@@ -3074,11 +3084,25 @@ def backup_account(args, output_directory):
3074
3084
  _backup_data(args, "starred repositories", template, output_file, account_cwd)
3075
3085
 
3076
3086
  if args.include_watched or args.include_everything:
3077
- output_file = "{0}/watched.json".format(account_cwd)
3078
- template = "https://{0}/users/{1}/subscriptions".format(
3079
- get_github_api_host(args), args.user
3080
- )
3081
- _backup_data(args, "watched repositories", template, output_file, account_cwd)
3087
+ # GitHub deprecated /users/{username}/subscriptions; watched
3088
+ # repositories are only available for the authenticated user via
3089
+ # /user/subscriptions.
3090
+ if (
3091
+ not authenticated_user.get("login")
3092
+ or args.user.lower() != authenticated_user["login"].lower()
3093
+ ):
3094
+ logger.warning(
3095
+ "Cannot retrieve watched repositories for '%s'. GitHub only allows access to the authenticated user's watched repositories.",
3096
+ args.user,
3097
+ )
3098
+ else:
3099
+ output_file = "{0}/watched.json".format(account_cwd)
3100
+ template = "https://{0}/user/subscriptions".format(
3101
+ get_github_api_host(args)
3102
+ )
3103
+ _backup_data(
3104
+ args, "watched repositories", template, output_file, account_cwd
3105
+ )
3082
3106
 
3083
3107
  if args.include_followers or args.include_everything:
3084
3108
  output_file = "{0}/followers.json".format(account_cwd)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-backup
3
- Version: 0.63.0
3
+ Version: 0.64.1
4
4
  Summary: backup a github user or organization
5
5
  Home-page: http://github.com/josegonzalez/python-github-backup
6
6
  Author: Jose Diaz-Gonzalez
@@ -26,6 +26,7 @@ tests/test_discussions.py
26
26
  tests/test_http_451.py
27
27
  tests/test_incremental_per_repository.py
28
28
  tests/test_json_dump_if_changed.py
29
+ tests/test_logging_subprocess.py
29
30
  tests/test_pagination.py
30
31
  tests/test_pull_incremental_pagination.py
31
32
  tests/test_pull_reviews.py
@@ -4,12 +4,12 @@ black==26.5.1
4
4
  flake8==7.3.0
5
5
 
6
6
  # Testing
7
- pytest==9.1.0
7
+ pytest==9.1.1
8
8
 
9
9
  # Release & Publishing
10
10
  twine==6.2.0
11
11
  gitchangelog==3.0.4
12
- setuptools==82.0.1
12
+ setuptools==83.0.0
13
13
 
14
14
  # Documentation
15
15
  restructuredtext-lint==2.0.2
@@ -0,0 +1,113 @@
1
+ """Tests for logging_subprocess pipe handling (issue #519)."""
2
+
3
+ import logging
4
+ import sys
5
+ import threading
6
+
7
+ import pytest
8
+
9
+ from github_backup import github_backup
10
+
11
+
12
+ class TestLoggingSubprocess:
13
+ """Test suite for logging_subprocess deadlock and logging behavior."""
14
+
15
+ def test_large_stderr_output_does_not_deadlock(self):
16
+ """Child output larger than the OS pipe buffer must not hang.
17
+
18
+ Regression test for issue #519: on Windows the pipes were never
19
+ drained, so the child blocked once its output exceeded the pipe
20
+ buffer (~8KB) and the parent spun forever on poll().
21
+ """
22
+ # Write 256KB to stderr, far past any platform's pipe buffer
23
+ child_code = (
24
+ "import sys\n"
25
+ "for _ in range(3200):\n"
26
+ " sys.stderr.write('x' * 79 + '\\n')\n"
27
+ )
28
+ result = {}
29
+
30
+ def run():
31
+ result["rc"] = github_backup.logging_subprocess(
32
+ [sys.executable, "-c", child_code]
33
+ )
34
+
35
+ thread = threading.Thread(target=run, daemon=True)
36
+ thread.start()
37
+ thread.join(timeout=30)
38
+
39
+ assert not thread.is_alive(), "logging_subprocess deadlocked on large output"
40
+ assert result["rc"] == 0
41
+
42
+ def test_stdout_logged_at_debug_stderr_at_error(self, caplog):
43
+ """stdout lines log at DEBUG and stderr lines at ERROR."""
44
+ child_code = (
45
+ "import sys\n"
46
+ "print('to stdout')\n"
47
+ "print('to stderr', file=sys.stderr)\n"
48
+ )
49
+
50
+ with caplog.at_level(logging.DEBUG, logger="github_backup.github_backup"):
51
+ rc = github_backup.logging_subprocess([sys.executable, "-c", child_code])
52
+
53
+ assert rc == 0
54
+ records = [
55
+ (r.levelno, r.getMessage())
56
+ for r in caplog.records
57
+ if r.name == "github_backup.github_backup"
58
+ ]
59
+ assert (logging.DEBUG, str(b"to stdout")) in records
60
+ assert (logging.ERROR, str(b"to stderr")) in records
61
+
62
+ def test_trailing_newlines_stripped(self, caplog):
63
+ """Logged lines have trailing \\r\\n stripped, including Windows CRLF."""
64
+ child_code = (
65
+ "import sys\n"
66
+ "sys.stdout.buffer.write(b'crlf line\\r\\n')\n"
67
+ "sys.stdout.buffer.write(b'lf line\\n')\n"
68
+ )
69
+
70
+ with caplog.at_level(logging.DEBUG, logger="github_backup.github_backup"):
71
+ rc = github_backup.logging_subprocess([sys.executable, "-c", child_code])
72
+
73
+ assert rc == 0
74
+ messages = [
75
+ r.getMessage()
76
+ for r in caplog.records
77
+ if r.name == "github_backup.github_backup"
78
+ ]
79
+ assert str(b"crlf line") in messages
80
+ assert str(b"lf line") in messages
81
+
82
+ def test_final_line_without_newline_not_truncated(self, caplog):
83
+ """A final line with no trailing newline keeps its last character.
84
+
85
+ The old code stripped the newline with line[:-1], which chopped the
86
+ last character off any line that did not end with a newline.
87
+ """
88
+ child_code = "import sys\nsys.stdout.write('no newline')\n"
89
+
90
+ with caplog.at_level(logging.DEBUG, logger="github_backup.github_backup"):
91
+ rc = github_backup.logging_subprocess([sys.executable, "-c", child_code])
92
+
93
+ assert rc == 0
94
+ messages = [
95
+ r.getMessage()
96
+ for r in caplog.records
97
+ if r.name == "github_backup.github_backup"
98
+ ]
99
+ assert str(b"no newline") in messages
100
+
101
+ def test_returns_child_exit_code(self, capsys):
102
+ """Non-zero child exit codes are returned and summarized on stderr."""
103
+ rc = github_backup.logging_subprocess(
104
+ [sys.executable, "-c", "import sys; sys.exit(3)"]
105
+ )
106
+
107
+ assert rc == 3
108
+ captured = capsys.readouterr()
109
+ assert "returned 3" in captured.err
110
+
111
+
112
+ if __name__ == "__main__":
113
+ pytest.main([__file__, "-v"])
@@ -1 +0,0 @@
1
- __version__ = "0.63.0"
File without changes
File without changes
File without changes