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.
Files changed (44) hide show
  1. {github_backup-0.66.1 → github_backup-0.67.0}/CHANGES.rst +18 -1
  2. {github_backup-0.66.1/github_backup.egg-info → github_backup-0.67.0}/PKG-INFO +3 -4
  3. {github_backup-0.66.1 → github_backup-0.67.0}/README +1 -1
  4. {github_backup-0.66.1 → github_backup-0.67.0}/README.rst +1 -1
  5. github_backup-0.67.0/github_backup/__init__.py +1 -0
  6. {github_backup-0.66.1 → github_backup-0.67.0}/github_backup/github_backup.py +11 -5
  7. {github_backup-0.66.1 → github_backup-0.67.0/github_backup.egg-info}/PKG-INFO +3 -4
  8. {github_backup-0.66.1 → github_backup-0.67.0}/github_backup.egg-info/SOURCES.txt +1 -0
  9. {github_backup-0.66.1 → github_backup-0.67.0}/setup.py +1 -2
  10. github_backup-0.67.0/tests/test_incremental_by_files.py +130 -0
  11. github_backup-0.66.1/github_backup/__init__.py +0 -1
  12. {github_backup-0.66.1 → github_backup-0.67.0}/LICENSE.txt +0 -0
  13. {github_backup-0.66.1 → github_backup-0.67.0}/MANIFEST.in +0 -0
  14. {github_backup-0.66.1 → github_backup-0.67.0}/github_backup/__main__.py +0 -0
  15. {github_backup-0.66.1 → github_backup-0.67.0}/github_backup/cli.py +0 -0
  16. {github_backup-0.66.1 → github_backup-0.67.0}/github_backup/graphql_queries.py +0 -0
  17. {github_backup-0.66.1 → github_backup-0.67.0}/github_backup.egg-info/dependency_links.txt +0 -0
  18. {github_backup-0.66.1 → github_backup-0.67.0}/github_backup.egg-info/entry_points.txt +0 -0
  19. {github_backup-0.66.1 → github_backup-0.67.0}/github_backup.egg-info/top_level.txt +0 -0
  20. {github_backup-0.66.1 → github_backup-0.67.0}/github_backup.egg-info/zip-safe +0 -0
  21. {github_backup-0.66.1 → github_backup-0.67.0}/release-requirements.txt +0 -0
  22. {github_backup-0.66.1 → github_backup-0.67.0}/requirements.txt +0 -0
  23. {github_backup-0.66.1 → github_backup-0.67.0}/setup.cfg +0 -0
  24. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_all_starred.py +0 -0
  25. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_app_repositories.py +0 -0
  26. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_attachments.py +0 -0
  27. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_auth.py +0 -0
  28. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_case_sensitivity.py +0 -0
  29. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_disabled_issues_pulls.py +0 -0
  30. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_discussions.py +0 -0
  31. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_gists.py +0 -0
  32. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_hooks.py +0 -0
  33. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_http_451.py +0 -0
  34. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_incremental_per_repository.py +0 -0
  35. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_issue_timeline.py +0 -0
  36. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_json_dump_if_changed.py +0 -0
  37. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_logging_subprocess.py +0 -0
  38. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_pagination.py +0 -0
  39. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_pull_incremental_pagination.py +0 -0
  40. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_pull_reviews.py +0 -0
  41. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_releases.py +0 -0
  42. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_retrieve_data.py +0 -0
  43. {github_backup-0.66.1 → github_backup-0.67.0}/tests/test_skip_assets_on.py +0 -0
  44. {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.66.1 (2026-11-03)
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.66.1
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.10
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.10 or higher
41
+ - Python 3.11 or higher
43
42
  - GIT 1.9+
44
43
 
45
44
  Installation
@@ -9,7 +9,7 @@ The package can be used to backup an *entire* `Github <https://github.com/>`_ or
9
9
  Requirements
10
10
  ============
11
11
 
12
- - Python 3.10 or higher
12
+ - Python 3.11 or higher
13
13
  - GIT 1.9+
14
14
 
15
15
  Installation
@@ -9,7 +9,7 @@ The package can be used to backup an *entire* `Github <https://github.com/>`_ or
9
9
  Requirements
10
10
  ============
11
11
 
12
- - Python 3.10 or higher
12
+ - Python 3.11 or higher
13
13
  - GIT 1.9+
14
14
 
15
15
  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.localtime())
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("%Y-%m-%dT%H:%M:%SZ")
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("%Y-%m-%dT%H:%M:%SZ")
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("%Y-%m-%dT%H:%M:%SZ")
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.66.1
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.10
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.10 or higher
41
+ - Python 3.11 or higher
43
42
  - GIT 1.9+
44
43
 
45
44
  Installation
@@ -27,6 +27,7 @@ tests/test_discussions.py
27
27
  tests/test_gists.py
28
28
  tests/test_hooks.py
29
29
  tests/test_http_451.py
30
+ tests/test_incremental_by_files.py
30
31
  tests/test_incremental_per_repository.py
31
32
  tests/test_issue_timeline.py
32
33
  tests/test_json_dump_if_changed.py
@@ -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.10",
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