github-backup 0.65.0__tar.gz → 0.66.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 (42) hide show
  1. {github_backup-0.65.0 → github_backup-0.66.0}/CHANGES.rst +148 -1
  2. {github_backup-0.65.0/github_backup.egg-info → github_backup-0.66.0}/PKG-INFO +3 -1
  3. {github_backup-0.65.0 → github_backup-0.66.0}/README +2 -0
  4. {github_backup-0.65.0 → github_backup-0.66.0}/README.rst +2 -0
  5. github_backup-0.66.0/github_backup/__init__.py +1 -0
  6. {github_backup-0.65.0 → github_backup-0.66.0}/github_backup/github_backup.py +35 -16
  7. {github_backup-0.65.0 → github_backup-0.66.0/github_backup.egg-info}/PKG-INFO +3 -1
  8. {github_backup-0.65.0 → github_backup-0.66.0}/github_backup.egg-info/SOURCES.txt +1 -0
  9. {github_backup-0.65.0 → github_backup-0.66.0}/release-requirements.txt +4 -4
  10. github_backup-0.66.0/tests/test_app_repositories.py +71 -0
  11. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_issue_timeline.py +31 -6
  12. github_backup-0.65.0/github_backup/__init__.py +0 -1
  13. {github_backup-0.65.0 → github_backup-0.66.0}/LICENSE.txt +0 -0
  14. {github_backup-0.65.0 → github_backup-0.66.0}/MANIFEST.in +0 -0
  15. {github_backup-0.65.0 → github_backup-0.66.0}/github_backup/__main__.py +0 -0
  16. {github_backup-0.65.0 → github_backup-0.66.0}/github_backup/cli.py +0 -0
  17. {github_backup-0.65.0 → github_backup-0.66.0}/github_backup/graphql_queries.py +0 -0
  18. {github_backup-0.65.0 → github_backup-0.66.0}/github_backup.egg-info/dependency_links.txt +0 -0
  19. {github_backup-0.65.0 → github_backup-0.66.0}/github_backup.egg-info/entry_points.txt +0 -0
  20. {github_backup-0.65.0 → github_backup-0.66.0}/github_backup.egg-info/top_level.txt +0 -0
  21. {github_backup-0.65.0 → github_backup-0.66.0}/github_backup.egg-info/zip-safe +0 -0
  22. {github_backup-0.65.0 → github_backup-0.66.0}/requirements.txt +0 -0
  23. {github_backup-0.65.0 → github_backup-0.66.0}/setup.cfg +0 -0
  24. {github_backup-0.65.0 → github_backup-0.66.0}/setup.py +0 -0
  25. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_all_starred.py +0 -0
  26. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_attachments.py +0 -0
  27. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_auth.py +0 -0
  28. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_case_sensitivity.py +0 -0
  29. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_disabled_issues_pulls.py +0 -0
  30. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_discussions.py +0 -0
  31. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_gists.py +0 -0
  32. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_http_451.py +0 -0
  33. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_incremental_per_repository.py +0 -0
  34. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_json_dump_if_changed.py +0 -0
  35. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_logging_subprocess.py +0 -0
  36. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_pagination.py +0 -0
  37. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_pull_incremental_pagination.py +0 -0
  38. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_pull_reviews.py +0 -0
  39. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_releases.py +0 -0
  40. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_retrieve_data.py +0 -0
  41. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_skip_assets_on.py +0 -0
  42. {github_backup-0.65.0 → github_backup-0.66.0}/tests/test_starred_skip_size_over.py +0 -0
@@ -1,9 +1,156 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
- 0.65.0 (2026-07-27)
4
+ 0.66.0 (2026-10-28)
5
5
  -------------------
6
6
  ------------------------
7
+ - List a user's private repositories when authenticated as a GitHub App.
8
+ [AC Capehart, Claude Opus 5.5]
9
+
10
+ With --as-app there is no authenticated user, so a user-account backup
11
+ fell through to /users/{user}/repos, which only returns public
12
+ repositories. --private silently backed up nothing private.
13
+
14
+ An installation token can list everything the installation can access
15
+ via /installation/repositories, private repositories included. Org
16
+ backups keep using /orgs/{org}/repos, and -R still fetches the single
17
+ repository directly.
18
+ - Chore(deps): bump setuptools. [dependabot[bot]]
19
+
20
+ Bumps the python-packages group with 1 update: [setuptools](https://github.com/pypa/setuptools).
21
+
22
+
23
+ Updates `setuptools` from 84.2.0 to 84.2.1
24
+ - [Release notes](https://github.com/pypa/setuptools/releases)
25
+ - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
26
+ - [Commits](https://github.com/pypa/setuptools/compare/v84.2.0...v84.2.1)
27
+
28
+ ---
29
+ updated-dependencies:
30
+ - dependency-name: setuptools
31
+ dependency-version: 84.2.1
32
+ dependency-type: direct:production
33
+ update-type: version-update:semver-patch
34
+ dependency-group: python-packages
35
+ ...
36
+ - Chore(deps): bump setuptools. [dependabot[bot]]
37
+
38
+ Bumps the python-packages group with 1 update: [setuptools](https://github.com/pypa/setuptools).
39
+
40
+
41
+ Updates `setuptools` from 84.0.0 to 84.2.0
42
+ - [Release notes](https://github.com/pypa/setuptools/releases)
43
+ - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
44
+ - [Commits](https://github.com/pypa/setuptools/compare/v84.0.0...v84.2.0)
45
+
46
+ ---
47
+ updated-dependencies:
48
+ - dependency-name: setuptools
49
+ dependency-version: 84.2.0
50
+ dependency-type: direct:production
51
+ update-type: version-update:semver-minor
52
+ dependency-group: python-packages
53
+ ...
54
+ - Chore(deps): bump black. [dependabot[bot]]
55
+
56
+ Bumps the python-packages group with 1 update: [black](https://github.com/psf/black).
57
+
58
+
59
+ Updates `black` from 26.10.0 to 26.10.1
60
+ - [Release notes](https://github.com/psf/black/releases)
61
+ - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
62
+ - [Commits](https://github.com/psf/black/compare/26.10.0...26.10.1)
63
+
64
+ ---
65
+ updated-dependencies:
66
+ - dependency-name: black
67
+ dependency-version: 26.10.1
68
+ dependency-type: direct:production
69
+ update-type: version-update:semver-patch
70
+ dependency-group: python-packages
71
+ ...
72
+ - Chore(deps): bump black. [dependabot[bot]]
73
+
74
+ Bumps the python-packages group with 1 update: [black](https://github.com/psf/black).
75
+
76
+
77
+ Updates `black` from 26.5.1 to 26.10.0
78
+ - [Release notes](https://github.com/psf/black/releases)
79
+ - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
80
+ - [Commits](https://github.com/psf/black/compare/26.5.1...26.10.0)
81
+
82
+ ---
83
+ updated-dependencies:
84
+ - dependency-name: black
85
+ dependency-version: 26.10.0
86
+ dependency-type: direct:production
87
+ update-type: version-update:semver-minor
88
+ dependency-group: python-packages
89
+ ...
90
+ - Chore(deps): bump flake8 in the python-packages group.
91
+ [dependabot[bot]]
92
+
93
+ Bumps the python-packages group with 1 update: [flake8](https://github.com/pycqa/flake8).
94
+
95
+
96
+ Updates `flake8` from 7.3.0 to 7.4.1
97
+ - [Commits](https://github.com/pycqa/flake8/compare/7.3.0...7.4.1)
98
+
99
+ ---
100
+ updated-dependencies:
101
+ - dependency-name: flake8
102
+ dependency-version: 7.4.1
103
+ dependency-type: direct:production
104
+ update-type: version-update:semver-minor
105
+ dependency-group: python-packages
106
+ ...
107
+ - Chore(deps): bump setuptools in the python-packages group.
108
+ [dependabot[bot]]
109
+
110
+ Bumps the python-packages group with 1 update: [setuptools](https://github.com/pypa/setuptools).
111
+
112
+
113
+ Updates `setuptools` from 83.0.0 to 84.0.0
114
+ - [Release notes](https://github.com/pypa/setuptools/releases)
115
+ - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
116
+ - [Commits](https://github.com/pypa/setuptools/compare/v83.0.0...v84.0.0)
117
+
118
+ ---
119
+ updated-dependencies:
120
+ - dependency-name: setuptools
121
+ dependency-version: 84.0.0
122
+ dependency-type: direct:production
123
+ update-type: version-update:semver-major
124
+ dependency-group: python-packages
125
+ ...
126
+
127
+
128
+ 0.65.1 (2026-08-01)
129
+ -------------------
130
+ - Fix timeline not being properly incremental. [Duncan Ogilvie]
131
+ - Chore(deps): bump twine from 6.2.0 to 7.0.0 in the python-packages
132
+ group. [dependabot[bot]]
133
+
134
+ Bumps the python-packages group with 1 update: [twine](https://github.com/pypa/twine).
135
+
136
+
137
+ Updates `twine` from 6.2.0 to 7.0.0
138
+ - [Release notes](https://github.com/pypa/twine/releases)
139
+ - [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst)
140
+ - [Commits](https://github.com/pypa/twine/compare/6.2.0...7.0.0)
141
+
142
+ ---
143
+ updated-dependencies:
144
+ - dependency-name: twine
145
+ dependency-version: 7.0.0
146
+ dependency-type: direct:production
147
+ update-type: version-update:semver-major
148
+ dependency-group: python-packages
149
+ ...
150
+
151
+
152
+ 0.65.0 (2026-07-27)
153
+ -------------------
7
154
  - Refresh issue timelines when cross-references change (#168) [Rodos]
8
155
 
9
156
  Cross-references do not change an issue's updated_at, so an incremental
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-backup
3
- Version: 0.65.0
3
+ Version: 0.66.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
@@ -229,6 +229,8 @@ GitHub Apps
229
229
 
230
230
  GitHub Apps are ideal for organization backups in CI/CD. Tokens are scoped to specific repositories and expire after 1 hour.
231
231
 
232
+ For a user account (no ``--organization``), ``--as-app`` lists the repositories the installation can access, so private repositories are included.
233
+
232
234
  **One-time setup:**
233
235
 
234
236
  1. Create a GitHub App at *Settings -> Developer Settings -> GitHub Apps -> New GitHub App*
@@ -199,6 +199,8 @@ GitHub Apps
199
199
 
200
200
  GitHub Apps are ideal for organization backups in CI/CD. Tokens are scoped to specific repositories and expire after 1 hour.
201
201
 
202
+ For a user account (no ``--organization``), ``--as-app`` lists the repositories the installation can access, so private repositories are included.
203
+
202
204
  **One-time setup:**
203
205
 
204
206
  1. Create a GitHub App at *Settings -> Developer Settings -> GitHub Apps -> New GitHub App*
@@ -199,6 +199,8 @@ GitHub Apps
199
199
 
200
200
  GitHub Apps are ideal for organization backups in CI/CD. Tokens are scoped to specific repositories and expire after 1 hour.
201
201
 
202
+ For a user account (no ``--organization``), ``--as-app`` lists the repositories the installation can access, so private repositories are included.
203
+
202
204
  **One-time setup:**
203
205
 
204
206
  1. Create a GitHub App at *Settings -> Developer Settings -> GitHub Apps -> New GitHub App*
@@ -0,0 +1 @@
1
+ __version__ = "0.66.0"
@@ -1838,6 +1838,11 @@ def retrieve_repositories(args, authenticated_user):
1838
1838
  if args.user == authenticated_user["login"]:
1839
1839
  # we must use the /user/repos API to be able to access private repos
1840
1840
  template = "https://{0}/user/repos".format(get_github_api_host(args))
1841
+ elif args.as_app and not args.organization:
1842
+ # /users/{user}/repos is public-only; the installation sees private repos too
1843
+ template = "https://{0}/installation/repositories".format(
1844
+ get_github_api_host(args)
1845
+ )
1841
1846
  else:
1842
1847
  if args.private and not args.organization:
1843
1848
  logger.warning(
@@ -1868,6 +1873,10 @@ def retrieve_repositories(args, authenticated_user):
1868
1873
  logger.warning(f"Legal notice: {e.legal_url}")
1869
1874
  return []
1870
1875
 
1876
+ if template.endswith("/installation/repositories"):
1877
+ # Each page is {"total_count": ..., "repositories": [...]}
1878
+ repos = [repo for page in repos for repo in page["repositories"]]
1879
+
1871
1880
  return retrieve_additional_repositories(args, authenticated_user, repos)
1872
1881
 
1873
1882
 
@@ -2757,19 +2766,24 @@ def backup_issues(args, repo_cwd, repository, repos_template):
2757
2766
  stale_timelines = find_stale_timeline_issues(
2758
2767
  args, issue_cwd, repository, include_pulls=not should_include_pulls
2759
2768
  )
2760
- for number in sorted(stale_timelines - set(issues)):
2761
- try:
2762
- issues[number] = retrieve_data(
2763
- args, "{0}/{1}".format(_issue_template, number), paginated=False
2764
- )[0]
2765
- except (HTTPError, RepositoryUnavailableError, IndexError) as e:
2766
- # Deleted or transferred between the sweep and this fetch. The
2767
- # refresh is opportunistic, so skip rather than fail the run.
2768
- logger.warning(
2769
- "Unable to refresh cross-references for issue {0}: {1}".format(
2770
- number, e
2771
- )
2772
- )
2769
+
2770
+ # Issues selected only by the cross-reference sweep have not otherwise
2771
+ # changed. Reuse their complete stored payload and refresh only timeline_data.
2772
+ # Fetching the issue, comments, events and attachments again turns the first
2773
+ # --issue-timeline run into a near-full backup and can discard optional data
2774
+ # when the corresponding flags are absent.
2775
+ timeline_only_issues = stale_timelines - set(issues)
2776
+ for number in sorted(timeline_only_issues):
2777
+ existing = read_json_file_if_exists(
2778
+ "{0}/{1}.json".format(issue_cwd, number)
2779
+ )
2780
+ if existing is None:
2781
+ logger.warning(
2782
+ "Unable to refresh cross-references for issue {0}: "
2783
+ "stored issue data is unreadable".format(number)
2784
+ )
2785
+ continue
2786
+ issues[number] = existing
2773
2787
 
2774
2788
  if issues_skipped:
2775
2789
  issues_skipped_message = " (skipped {0} pull requests)".format(issues_skipped)
@@ -2784,6 +2798,7 @@ def backup_issues(args, repo_cwd, repository, repos_template):
2784
2798
  timeline_template = _issue_template + "/{0}/timeline"
2785
2799
  for number, issue in list(issues.items()):
2786
2800
  issue_file = "{0}/{1}.json".format(issue_cwd, number)
2801
+ timeline_only_refresh = number in timeline_only_issues
2787
2802
  if (
2788
2803
  args.incremental_by_files
2789
2804
  and os.path.isfile(issue_file)
@@ -2799,10 +2814,14 @@ def backup_issues(args, repo_cwd, repository, repos_template):
2799
2814
  )
2800
2815
  continue
2801
2816
 
2802
- if args.include_issue_comments or args.include_everything:
2817
+ if not timeline_only_refresh and (
2818
+ args.include_issue_comments or args.include_everything
2819
+ ):
2803
2820
  template = comments_template.format(number)
2804
2821
  issues[number]["comment_data"] = retrieve_data(args, template)
2805
- if args.include_issue_events or args.include_everything:
2822
+ if not timeline_only_refresh and (
2823
+ args.include_issue_events or args.include_everything
2824
+ ):
2806
2825
  template = events_template.format(number)
2807
2826
  issues[number]["event_data"] = retrieve_data(args, template)
2808
2827
  if include_timeline:
@@ -2815,7 +2834,7 @@ def backup_issues(args, repo_cwd, repository, repos_template):
2815
2834
  for item in retrieve_data(args, template)
2816
2835
  if item.get("event") != "commented"
2817
2836
  ]
2818
- if args.include_attachments:
2837
+ if args.include_attachments and not timeline_only_refresh:
2819
2838
  download_attachments(
2820
2839
  args, issue_cwd, issues[number], number, repository, item_type="issue"
2821
2840
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-backup
3
- Version: 0.65.0
3
+ Version: 0.66.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
@@ -229,6 +229,8 @@ GitHub Apps
229
229
 
230
230
  GitHub Apps are ideal for organization backups in CI/CD. Tokens are scoped to specific repositories and expire after 1 hour.
231
231
 
232
+ For a user account (no ``--organization``), ``--as-app`` lists the repositories the installation can access, so private repositories are included.
233
+
232
234
  **One-time setup:**
233
235
 
234
236
  1. Create a GitHub App at *Settings -> Developer Settings -> GitHub Apps -> New GitHub App*
@@ -18,6 +18,7 @@ github_backup.egg-info/entry_points.txt
18
18
  github_backup.egg-info/top_level.txt
19
19
  github_backup.egg-info/zip-safe
20
20
  tests/test_all_starred.py
21
+ tests/test_app_repositories.py
21
22
  tests/test_attachments.py
22
23
  tests/test_auth.py
23
24
  tests/test_case_sensitivity.py
@@ -1,15 +1,15 @@
1
1
  # Linting & Formatting
2
2
  autopep8==2.3.2
3
- black==26.5.1
4
- flake8==7.3.0
3
+ black==26.10.1
4
+ flake8==7.4.1
5
5
 
6
6
  # Testing
7
7
  pytest==9.1.1
8
8
 
9
9
  # Release & Publishing
10
- twine==6.2.0
10
+ twine==7.0.0
11
11
  gitchangelog==3.0.4
12
- setuptools==83.0.0
12
+ setuptools==84.2.1
13
13
 
14
14
  # Documentation
15
15
  restructuredtext-lint==2.0.2
@@ -0,0 +1,71 @@
1
+ """Tests for listing a user's repositories with a GitHub App installation token."""
2
+
3
+ from unittest.mock import patch
4
+
5
+ import pytest
6
+
7
+ from github_backup import github_backup
8
+
9
+ NO_USER = {"login": None}
10
+
11
+
12
+ def page(*names):
13
+ return {
14
+ "total_count": 3,
15
+ "repositories": [{"name": name, "private": True} for name in names],
16
+ }
17
+
18
+
19
+ @pytest.fixture
20
+ def mock_retrieve_data():
21
+ with patch("github_backup.github_backup.retrieve_data") as mock:
22
+ yield mock
23
+
24
+
25
+ def test_app_lists_user_repositories_from_installation(create_args, mock_retrieve_data):
26
+ args = create_args(as_app=True, include_repository=True)
27
+ mock_retrieve_data.return_value = [page("a", "b"), page("c")]
28
+
29
+ repos = github_backup.retrieve_repositories(args, NO_USER)
30
+
31
+ assert mock_retrieve_data.call_args.args[1] == (
32
+ "https://api.github.com/installation/repositories"
33
+ )
34
+ assert [r["name"] for r in repos] == ["a", "b", "c"]
35
+
36
+
37
+ def test_app_installation_listing_uses_ghes_host(create_args, mock_retrieve_data):
38
+ args = create_args(
39
+ as_app=True, include_repository=True, github_host="ghe.example.com"
40
+ )
41
+ mock_retrieve_data.return_value = [page("a")]
42
+
43
+ github_backup.retrieve_repositories(args, NO_USER)
44
+
45
+ assert mock_retrieve_data.call_args.args[1] == (
46
+ "https://ghe.example.com/api/v3/installation/repositories"
47
+ )
48
+
49
+
50
+ def test_app_organization_still_lists_org_repositories(create_args, mock_retrieve_data):
51
+ args = create_args(as_app=True, include_repository=True, organization=True)
52
+ mock_retrieve_data.return_value = [{"name": "a"}]
53
+
54
+ repos = github_backup.retrieve_repositories(args, NO_USER)
55
+
56
+ assert mock_retrieve_data.call_args.args[1] == (
57
+ "https://api.github.com/orgs/testuser/repos"
58
+ )
59
+ assert repos == [{"name": "a"}]
60
+
61
+
62
+ def test_app_single_repository_is_fetched_directly(create_args, mock_retrieve_data):
63
+ args = create_args(as_app=True, repository="dotfiles")
64
+ mock_retrieve_data.return_value = [{"name": "dotfiles"}]
65
+
66
+ repos = github_backup.retrieve_repositories(args, NO_USER)
67
+
68
+ assert mock_retrieve_data.call_args.args[1] == (
69
+ "https://api.github.com/repos/testuser/dotfiles"
70
+ )
71
+ assert repos == [{"name": "dotfiles"}]
@@ -274,12 +274,24 @@ def test_graphql_failure_warns_and_continues(
274
274
  def test_stale_issue_backed_up_though_since_excludes_it(
275
275
  create_args, tmp_path, monkeypatch
276
276
  ):
277
- """The whole point: an issue since cannot see is still refreshed."""
278
- _stored(tmp_path, 7, [])
277
+ """A sweep-only issue refreshes just its timeline and preserves its data."""
278
+ issue_cwd = tmp_path / "issues"
279
+ issue_cwd.mkdir()
280
+ stored = _issue(
281
+ 7,
282
+ body="stored body",
283
+ comment_data=[{"id": 1, "body": "stored comment"}],
284
+ event_data=[{"id": 2, "event": "referenced"}],
285
+ timeline_data=[],
286
+ )
287
+ (issue_cwd / "7.json").write_text(json.dumps(stored))
279
288
  args = _sweep_args(
280
289
  create_args,
281
290
  include_issues=True,
291
+ include_issue_comments=True,
292
+ include_issue_events=True,
282
293
  include_issue_timeline=True,
294
+ include_attachments=True,
283
295
  since="2026-07-01T00:00:00Z",
284
296
  )
285
297
  timeline = [_xref("2026-07-20T00:00:00Z")]
@@ -287,22 +299,35 @@ def test_stale_issue_backed_up_though_since_excludes_it(
287
299
  monkeypatch.setattr(
288
300
  github_backup,
289
301
  "retrieve_data",
290
- _fake_retrieve(
291
- {"/issues": [], "/issues/7": [_issue(7)], "/7/timeline": timeline}, calls
292
- ),
302
+ _fake_retrieve({"/issues": [], "/7/timeline": timeline}, calls),
293
303
  )
294
304
  monkeypatch.setattr(
295
305
  github_backup,
296
306
  "retrieve_graphql_data",
297
307
  _graphql_state({7: (1, "2026-07-20T00:00:00Z")}),
298
308
  )
309
+ attachment_calls = []
310
+ monkeypatch.setattr(
311
+ github_backup,
312
+ "download_attachments",
313
+ lambda *args, **kwargs: attachment_calls.append((args, kwargs)),
314
+ )
299
315
 
300
316
  github_backup.backup_issues(
301
317
  args, str(tmp_path), {"full_name": "owner/repo"}, "https://api.github.com/repos"
302
318
  )
303
319
 
304
- saved = json.loads((tmp_path / "issues" / "7.json").read_text())
320
+ saved = json.loads((issue_cwd / "7.json").read_text())
305
321
  assert saved["timeline_data"] == timeline
322
+ assert saved["body"] == "stored body"
323
+ assert saved["comment_data"] == stored["comment_data"]
324
+ assert saved["event_data"] == stored["event_data"]
325
+ assert calls == [
326
+ "https://api.github.com/repos/owner/repo/issues",
327
+ "https://api.github.com/repos/owner/repo/issues",
328
+ "https://api.github.com/repos/owner/repo/issues/7/timeline",
329
+ ]
330
+ assert attachment_calls == []
306
331
 
307
332
 
308
333
  def test_sweep_follows_pagination_cursors(create_args, tmp_path, monkeypatch):
@@ -1 +0,0 @@
1
- __version__ = "0.65.0"
File without changes
File without changes