github-backup 0.66.1__tar.gz → 0.67.0__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.
- {github_backup-0.66.1 → github_backup-0.67.0}/CHANGES.rst +18 -1
- {github_backup-0.66.1/github_backup.egg-info → github_backup-0.67.0}/PKG-INFO +3 -4
- {github_backup-0.66.1 → github_backup-0.67.0}/README +1 -1
- {github_backup-0.66.1 → github_backup-0.67.0}/README.rst +1 -1
- github_backup-0.67.0/github_backup/__init__.py +1 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/github_backup/github_backup.py +11 -5
- {github_backup-0.66.1 → github_backup-0.67.0/github_backup.egg-info}/PKG-INFO +3 -4
- {github_backup-0.66.1 → github_backup-0.67.0}/github_backup.egg-info/SOURCES.txt +1 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/setup.py +1 -2
- github_backup-0.67.0/tests/test_incremental_by_files.py +130 -0
- github_backup-0.66.1/github_backup/__init__.py +0 -1
- {github_backup-0.66.1 → github_backup-0.67.0}/LICENSE.txt +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/MANIFEST.in +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/github_backup/__main__.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/github_backup/cli.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/github_backup/graphql_queries.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/github_backup.egg-info/dependency_links.txt +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/github_backup.egg-info/entry_points.txt +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/github_backup.egg-info/top_level.txt +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/github_backup.egg-info/zip-safe +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/release-requirements.txt +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/requirements.txt +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/setup.cfg +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_all_starred.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_app_repositories.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_attachments.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_auth.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_case_sensitivity.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_disabled_issues_pulls.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_discussions.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_gists.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_hooks.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_http_451.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_incremental_per_repository.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_issue_timeline.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_json_dump_if_changed.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_logging_subprocess.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_pagination.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_pull_incremental_pagination.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_pull_reviews.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_releases.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_retrieve_data.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_skip_assets_on.py +0 -0
- {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_starred_skip_size_over.py +0 -0
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
-
0.
|
|
4
|
+
0.67.0 (2026-11-04)
|
|
5
5
|
-------------------
|
|
6
6
|
------------------------
|
|
7
|
+
- Drop support for Python 3.10 (end of life) [Rodos]
|
|
8
|
+
|
|
9
|
+
Python 3.10 reached end of life on 2026-10-01 and no longer receives
|
|
10
|
+
security fixes. Require Python 3.11 or higher, and remove 3.10 from the
|
|
11
|
+
package classifiers and the CI test and lint matrices.
|
|
12
|
+
- Fix --incremental-by-files skipping updated items east of UTC. [Rodos]
|
|
13
|
+
|
|
14
|
+
The modification time of a saved issue, pull request or discussion was
|
|
15
|
+
formatted in local time but labelled UTC, then compared with GitHub's UTC
|
|
16
|
+
updated_at. East of UTC, items updated within the offset after they were saved
|
|
17
|
+
were skipped and their changes missed; west of UTC, unchanged items were
|
|
18
|
+
fetched again. The checkpoint time used when a repository has no updated_at or
|
|
19
|
+
pushed_at had the same local-time mistake.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
0.66.1 (2026-11-03)
|
|
23
|
+
-------------------
|
|
7
24
|
- Skip hooks for repositories without admin access instead of logging a
|
|
8
25
|
404 error (#532) [Rodos]
|
|
9
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.67.0
|
|
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
|
|
@@ -9,12 +9,11 @@ License: MIT
|
|
|
9
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
10
10
|
Classifier: Topic :: System :: Archiving :: Backup
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
-
Requires-Python: >=3.
|
|
16
|
+
Requires-Python: >=3.11
|
|
18
17
|
Description-Content-Type: text/x-rst
|
|
19
18
|
License-File: LICENSE.txt
|
|
20
19
|
Dynamic: author
|
|
@@ -39,7 +38,7 @@ The package can be used to backup an *entire* `Github <https://github.com/>`_ or
|
|
|
39
38
|
Requirements
|
|
40
39
|
============
|
|
41
40
|
|
|
42
|
-
- Python 3.
|
|
41
|
+
- Python 3.11 or higher
|
|
43
42
|
- GIT 1.9+
|
|
44
43
|
|
|
45
44
|
Installation
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.67.0"
|
|
@@ -20,7 +20,7 @@ import threading
|
|
|
20
20
|
import time
|
|
21
21
|
import traceback
|
|
22
22
|
from collections.abc import Generator
|
|
23
|
-
from datetime import datetime
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
24
|
from http.client import IncompleteRead
|
|
25
25
|
from urllib.error import HTTPError, URLError
|
|
26
26
|
from urllib.parse import urlencode, urlparse
|
|
@@ -2004,7 +2004,7 @@ def get_repository_checkpoint_time(repository):
|
|
|
2004
2004
|
if timestamps:
|
|
2005
2005
|
return max(timestamps)
|
|
2006
2006
|
|
|
2007
|
-
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.
|
|
2007
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
2008
2008
|
|
|
2009
2009
|
|
|
2010
2010
|
def resource_backup_exists(resource_cwd):
|
|
@@ -2525,7 +2525,9 @@ def backup_discussions(args, repo_cwd, repository):
|
|
|
2525
2525
|
|
|
2526
2526
|
if args.incremental_by_files and os.path.isfile(discussion_file):
|
|
2527
2527
|
modified = os.path.getmtime(discussion_file)
|
|
2528
|
-
modified = datetime.fromtimestamp(modified).strftime(
|
|
2528
|
+
modified = datetime.fromtimestamp(modified, timezone.utc).strftime(
|
|
2529
|
+
"%Y-%m-%dT%H:%M:%SZ"
|
|
2530
|
+
)
|
|
2529
2531
|
if modified > summary["updatedAt"]:
|
|
2530
2532
|
logger.info(
|
|
2531
2533
|
"Skipping discussion {0} because it wasn't modified since last backup".format(
|
|
@@ -2805,7 +2807,9 @@ def backup_issues(args, repo_cwd, repository, repos_template):
|
|
|
2805
2807
|
and number not in stale_timelines
|
|
2806
2808
|
):
|
|
2807
2809
|
modified = os.path.getmtime(issue_file)
|
|
2808
|
-
modified = datetime.fromtimestamp(modified).strftime(
|
|
2810
|
+
modified = datetime.fromtimestamp(modified, timezone.utc).strftime(
|
|
2811
|
+
"%Y-%m-%dT%H:%M:%SZ"
|
|
2812
|
+
)
|
|
2809
2813
|
if modified > issue["updated_at"]:
|
|
2810
2814
|
logger.info(
|
|
2811
2815
|
"Skipping issue {0} because it wasn't modified since last backup".format(
|
|
@@ -3006,7 +3010,9 @@ def backup_pulls(args, repo_cwd, repository, repos_template):
|
|
|
3006
3010
|
|
|
3007
3011
|
if args.incremental_by_files and os.path.isfile(pull_file):
|
|
3008
3012
|
modified = os.path.getmtime(pull_file)
|
|
3009
|
-
modified = datetime.fromtimestamp(modified).strftime(
|
|
3013
|
+
modified = datetime.fromtimestamp(modified, timezone.utc).strftime(
|
|
3014
|
+
"%Y-%m-%dT%H:%M:%SZ"
|
|
3015
|
+
)
|
|
3010
3016
|
if modified > pull["updated_at"] and not needs_review_backfill:
|
|
3011
3017
|
logger.info(
|
|
3012
3018
|
"Skipping pull request {0} because it wasn't modified since last backup".format(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.67.0
|
|
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
|
|
@@ -9,12 +9,11 @@ License: MIT
|
|
|
9
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
10
10
|
Classifier: Topic :: System :: Archiving :: Backup
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
-
Requires-Python: >=3.
|
|
16
|
+
Requires-Python: >=3.11
|
|
18
17
|
Description-Content-Type: text/x-rst
|
|
19
18
|
License-File: LICENSE.txt
|
|
20
19
|
Dynamic: author
|
|
@@ -39,7 +38,7 @@ The package can be used to backup an *entire* `Github <https://github.com/>`_ or
|
|
|
39
38
|
Requirements
|
|
40
39
|
============
|
|
41
40
|
|
|
42
|
-
- Python 3.
|
|
41
|
+
- Python 3.11 or higher
|
|
43
42
|
- GIT 1.9+
|
|
44
43
|
|
|
45
44
|
Installation
|
|
@@ -44,7 +44,6 @@ setup(
|
|
|
44
44
|
"Development Status :: 5 - Production/Stable",
|
|
45
45
|
"Topic :: System :: Archiving :: Backup",
|
|
46
46
|
"License :: OSI Approved :: MIT License",
|
|
47
|
-
"Programming Language :: Python :: 3.10",
|
|
48
47
|
"Programming Language :: Python :: 3.11",
|
|
49
48
|
"Programming Language :: Python :: 3.12",
|
|
50
49
|
"Programming Language :: Python :: 3.13",
|
|
@@ -54,6 +53,6 @@ setup(
|
|
|
54
53
|
long_description=open_file("README.rst").read(),
|
|
55
54
|
long_description_content_type="text/x-rst",
|
|
56
55
|
install_requires=open_file("requirements.txt").readlines(),
|
|
57
|
-
python_requires=">=3.
|
|
56
|
+
python_requires=">=3.11",
|
|
58
57
|
zip_safe=True,
|
|
59
58
|
)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Tests for --incremental-by-files timestamp comparison.
|
|
2
|
+
|
|
3
|
+
A saved file is skipped when its modification time is newer than the item's
|
|
4
|
+
``updated_at``. GitHub's timestamps are UTC, so the file's modification time
|
|
5
|
+
must be compared in UTC too, whatever the local time zone is.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import calendar
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from github_backup import github_backup
|
|
16
|
+
|
|
17
|
+
UPDATED_AT = "2026-07-01T12:00:00Z"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def east_of_utc(monkeypatch):
|
|
22
|
+
"""Run the test with the local time zone at UTC+10 (no daylight saving)."""
|
|
23
|
+
monkeypatch.setenv("TZ", "Etc/GMT-10")
|
|
24
|
+
time.tzset()
|
|
25
|
+
yield
|
|
26
|
+
monkeypatch.undo()
|
|
27
|
+
time.tzset()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _stored(path, number, written_at):
|
|
31
|
+
"""Save an item file whose modification time is ``written_at`` (UTC)."""
|
|
32
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
item_file = path / "{0}.json".format(number)
|
|
34
|
+
item_file.write_text(json.dumps({"number": number, "title": "stored"}))
|
|
35
|
+
mtime = calendar.timegm(time.strptime(written_at, "%Y-%m-%dT%H:%M:%SZ"))
|
|
36
|
+
os.utime(item_file, (mtime, mtime))
|
|
37
|
+
return item_file
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _fake_retrieve(items):
|
|
41
|
+
def _retrieve(args, template, query_args=None, **kwargs):
|
|
42
|
+
return items if template.endswith(("/issues", "/pulls")) else []
|
|
43
|
+
|
|
44
|
+
return _retrieve
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.parametrize(
|
|
48
|
+
"written_at, refetched",
|
|
49
|
+
[
|
|
50
|
+
("2026-07-01T10:00:00Z", True), # updated 2 hours after it was saved
|
|
51
|
+
("2026-07-01T14:00:00Z", False), # saved 2 hours after the update
|
|
52
|
+
],
|
|
53
|
+
)
|
|
54
|
+
def test_issue_skip_compares_in_utc(
|
|
55
|
+
create_args, tmp_path, monkeypatch, east_of_utc, written_at, refetched
|
|
56
|
+
):
|
|
57
|
+
issue_file = _stored(tmp_path / "issues", 1, written_at)
|
|
58
|
+
issue = {"number": 1, "title": "updated", "updated_at": UPDATED_AT}
|
|
59
|
+
monkeypatch.setattr(github_backup, "retrieve_data", _fake_retrieve([issue]))
|
|
60
|
+
args = create_args(include_issues=True, incremental_by_files=True, since=None)
|
|
61
|
+
|
|
62
|
+
github_backup.backup_issues(
|
|
63
|
+
args, str(tmp_path), {"full_name": "owner/repo"}, "https://api.github.com/repos"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
title = json.loads(issue_file.read_text())["title"]
|
|
67
|
+
assert title == ("updated" if refetched else "stored")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.parametrize(
|
|
71
|
+
"written_at, refetched",
|
|
72
|
+
[
|
|
73
|
+
("2026-07-01T10:00:00Z", True),
|
|
74
|
+
("2026-07-01T14:00:00Z", False),
|
|
75
|
+
],
|
|
76
|
+
)
|
|
77
|
+
def test_pull_skip_compares_in_utc(
|
|
78
|
+
create_args, tmp_path, monkeypatch, east_of_utc, written_at, refetched
|
|
79
|
+
):
|
|
80
|
+
pull_file = _stored(tmp_path / "pulls", 1, written_at)
|
|
81
|
+
pull = {"number": 1, "title": "updated", "updated_at": UPDATED_AT}
|
|
82
|
+
monkeypatch.setattr(github_backup, "retrieve_data", _fake_retrieve([pull]))
|
|
83
|
+
args = create_args(include_pulls=True, incremental_by_files=True, since=None)
|
|
84
|
+
|
|
85
|
+
github_backup.backup_pulls(
|
|
86
|
+
args, str(tmp_path), {"full_name": "owner/repo"}, "https://api.github.com/repos"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
title = json.loads(pull_file.read_text())["title"]
|
|
90
|
+
assert title == ("updated" if refetched else "stored")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.mark.parametrize(
|
|
94
|
+
"written_at, refetched",
|
|
95
|
+
[
|
|
96
|
+
("2026-07-01T10:00:00Z", True),
|
|
97
|
+
("2026-07-01T14:00:00Z", False),
|
|
98
|
+
],
|
|
99
|
+
)
|
|
100
|
+
def test_discussion_skip_compares_in_utc(
|
|
101
|
+
create_args, tmp_path, monkeypatch, east_of_utc, written_at, refetched
|
|
102
|
+
):
|
|
103
|
+
discussion_file = _stored(tmp_path / "discussions", 1, written_at)
|
|
104
|
+
summary = {"number": 1, "updatedAt": UPDATED_AT}
|
|
105
|
+
monkeypatch.setattr(
|
|
106
|
+
github_backup,
|
|
107
|
+
"retrieve_discussion_summaries",
|
|
108
|
+
lambda args, repository, since=None: ([summary], UPDATED_AT, True, 1),
|
|
109
|
+
)
|
|
110
|
+
monkeypatch.setattr(
|
|
111
|
+
github_backup,
|
|
112
|
+
"retrieve_discussion",
|
|
113
|
+
lambda args, repository, number: {"number": number, "title": "updated"},
|
|
114
|
+
)
|
|
115
|
+
args = create_args(
|
|
116
|
+
include_discussions=True, incremental_by_files=True, token_classic="ghp_test"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
github_backup.backup_discussions(args, str(tmp_path), {"full_name": "owner/repo"})
|
|
120
|
+
|
|
121
|
+
title = json.loads(discussion_file.read_text())["title"]
|
|
122
|
+
assert title == ("updated" if refetched else "stored")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_checkpoint_fallback_time_is_utc(east_of_utc):
|
|
126
|
+
before = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
127
|
+
checkpoint = github_backup.get_repository_checkpoint_time({})
|
|
128
|
+
after = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
129
|
+
|
|
130
|
+
assert before <= checkpoint <= after
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.66.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|