github-backup 0.51.0__tar.gz → 0.51.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.
- {github_backup-0.51.0 → github_backup-0.51.1}/CHANGES.rst +89 -1
- {github_backup-0.51.0/github_backup.egg-info → github_backup-0.51.1}/PKG-INFO +1 -1
- github_backup-0.51.1/github_backup/__init__.py +1 -0
- {github_backup-0.51.0 → github_backup-0.51.1}/github_backup/github_backup.py +2 -11
- {github_backup-0.51.0 → github_backup-0.51.1/github_backup.egg-info}/PKG-INFO +1 -1
- {github_backup-0.51.0 → github_backup-0.51.1}/github_backup.egg-info/SOURCES.txt +2 -1
- {github_backup-0.51.0 → github_backup-0.51.1}/release-requirements.txt +4 -3
- github_backup-0.51.1/tests/test_attachments.py +353 -0
- github_backup-0.51.0/github_backup/__init__.py +0 -1
- {github_backup-0.51.0 → github_backup-0.51.1}/LICENSE.txt +0 -0
- {github_backup-0.51.0 → github_backup-0.51.1}/MANIFEST.in +0 -0
- {github_backup-0.51.0 → github_backup-0.51.1}/README +0 -0
- {github_backup-0.51.0 → github_backup-0.51.1}/README.rst +0 -0
- {github_backup-0.51.0 → github_backup-0.51.1}/bin/github-backup +0 -0
- {github_backup-0.51.0 → github_backup-0.51.1}/github_backup.egg-info/dependency_links.txt +0 -0
- {github_backup-0.51.0 → github_backup-0.51.1}/github_backup.egg-info/top_level.txt +0 -0
- {github_backup-0.51.0 → github_backup-0.51.1}/github_backup.egg-info/zip-safe +0 -0
- {github_backup-0.51.0 → github_backup-0.51.1}/requirements.txt +0 -0
- {github_backup-0.51.0 → github_backup-0.51.1}/setup.cfg +0 -0
- {github_backup-0.51.0 → github_backup-0.51.1}/setup.py +0 -0
|
@@ -1,10 +1,98 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
-
0.51.
|
|
4
|
+
0.51.1 (2025-11-16)
|
|
5
5
|
-------------------
|
|
6
6
|
------------------------
|
|
7
7
|
|
|
8
|
+
Fix
|
|
9
|
+
~~~
|
|
10
|
+
- Prevent duplicate attachment downloads. [Rodos]
|
|
11
|
+
|
|
12
|
+
Fixes bug where attachments were downloaded multiple times with
|
|
13
|
+
incremented filenames (file.mov, file_1.mov, file_2.mov) when
|
|
14
|
+
running backups without --skip-existing flag.
|
|
15
|
+
|
|
16
|
+
I should not have used the --skip-existing flag for attachments,
|
|
17
|
+
it did not do what I thought it did.
|
|
18
|
+
|
|
19
|
+
The correct approach is to always use the manifest to guide what
|
|
20
|
+
has already been downloaded and what now needs to be done.
|
|
21
|
+
|
|
22
|
+
Other
|
|
23
|
+
~~~~~
|
|
24
|
+
- Chore(deps): bump certifi in the python-packages group.
|
|
25
|
+
[dependabot[bot]]
|
|
26
|
+
|
|
27
|
+
Bumps the python-packages group with 1 update: [certifi](https://github.com/certifi/python-certifi).
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
Updates `certifi` from 2025.10.5 to 2025.11.12
|
|
31
|
+
- [Commits](https://github.com/certifi/python-certifi/compare/2025.10.05...2025.11.12)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
updated-dependencies:
|
|
35
|
+
- dependency-name: certifi
|
|
36
|
+
dependency-version: 2025.11.12
|
|
37
|
+
dependency-type: direct:production
|
|
38
|
+
update-type: version-update:semver-minor
|
|
39
|
+
dependency-group: python-packages
|
|
40
|
+
...
|
|
41
|
+
- Test: Add pytest infrastructure and attachment tests. [Rodos]
|
|
42
|
+
|
|
43
|
+
In making my last fix to attachments, I found it challenging not
|
|
44
|
+
having tests to ensure there was no regression.
|
|
45
|
+
|
|
46
|
+
Added pytest with minimal setup and isolated configuration. Created
|
|
47
|
+
a separate test workflow to keep tests isolated from linting.
|
|
48
|
+
|
|
49
|
+
Tests cover the key elements of the attachment logic:
|
|
50
|
+
- URL extraction from issue bodies
|
|
51
|
+
- Filename extraction from different URL types
|
|
52
|
+
- Filename collision resolution
|
|
53
|
+
- Manifest duplicate prevention
|
|
54
|
+
- Chore(deps): bump black in the python-packages group.
|
|
55
|
+
[dependabot[bot]]
|
|
56
|
+
|
|
57
|
+
Bumps the python-packages group with 1 update: [black](https://github.com/psf/black).
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
Updates `black` from 25.9.0 to 25.11.0
|
|
61
|
+
- [Release notes](https://github.com/psf/black/releases)
|
|
62
|
+
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
|
|
63
|
+
- [Commits](https://github.com/psf/black/compare/25.9.0...25.11.0)
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
updated-dependencies:
|
|
67
|
+
- dependency-name: black
|
|
68
|
+
dependency-version: 25.11.0
|
|
69
|
+
dependency-type: direct:production
|
|
70
|
+
update-type: version-update:semver-minor
|
|
71
|
+
dependency-group: python-packages
|
|
72
|
+
...
|
|
73
|
+
- Chore(deps): bump docutils in the python-packages group.
|
|
74
|
+
[dependabot[bot]]
|
|
75
|
+
|
|
76
|
+
Bumps the python-packages group with 1 update: [docutils](https://github.com/rtfd/recommonmark).
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
Updates `docutils` from 0.22.2 to 0.22.3
|
|
80
|
+
- [Changelog](https://github.com/readthedocs/recommonmark/blob/master/CHANGELOG.md)
|
|
81
|
+
- [Commits](https://github.com/rtfd/recommonmark/commits)
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
updated-dependencies:
|
|
85
|
+
- dependency-name: docutils
|
|
86
|
+
dependency-version: 0.22.3
|
|
87
|
+
dependency-type: direct:production
|
|
88
|
+
update-type: version-update:semver-patch
|
|
89
|
+
dependency-group: python-packages
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
0.51.0 (2025-11-06)
|
|
94
|
+
-------------------
|
|
95
|
+
|
|
8
96
|
Fix
|
|
9
97
|
~~~
|
|
10
98
|
- Remove Python 3.8 and 3.9 from CI matrix. [Rodos]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.51.1"
|
|
@@ -919,12 +919,6 @@ def download_attachment_file(url, path, auth, as_app=False, fine=False):
|
|
|
919
919
|
"error": None,
|
|
920
920
|
}
|
|
921
921
|
|
|
922
|
-
if os.path.exists(path):
|
|
923
|
-
metadata["success"] = True
|
|
924
|
-
metadata["http_status"] = 200 # Assume success if already exists
|
|
925
|
-
metadata["size_bytes"] = os.path.getsize(path)
|
|
926
|
-
return metadata
|
|
927
|
-
|
|
928
922
|
# Create simple request (no API query params)
|
|
929
923
|
request = Request(url)
|
|
930
924
|
request.add_header("Accept", "application/octet-stream")
|
|
@@ -1337,10 +1331,10 @@ def download_attachments(
|
|
|
1337
1331
|
attachments_dir = os.path.join(item_cwd, "attachments", str(number))
|
|
1338
1332
|
manifest_path = os.path.join(attachments_dir, "manifest.json")
|
|
1339
1333
|
|
|
1340
|
-
# Load existing manifest
|
|
1334
|
+
# Load existing manifest to prevent duplicate downloads
|
|
1341
1335
|
existing_urls = set()
|
|
1342
1336
|
existing_metadata = []
|
|
1343
|
-
if
|
|
1337
|
+
if os.path.exists(manifest_path):
|
|
1344
1338
|
try:
|
|
1345
1339
|
with open(manifest_path, "r") as f:
|
|
1346
1340
|
existing_manifest = json.load(f)
|
|
@@ -1395,9 +1389,6 @@ def download_attachments(
|
|
|
1395
1389
|
filename = get_attachment_filename(url)
|
|
1396
1390
|
filepath = os.path.join(attachments_dir, filename)
|
|
1397
1391
|
|
|
1398
|
-
# Check for collision BEFORE downloading
|
|
1399
|
-
filepath = resolve_filename_collision(filepath)
|
|
1400
|
-
|
|
1401
1392
|
# Download and get metadata
|
|
1402
1393
|
metadata = download_attachment_file(
|
|
1403
1394
|
url,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
autopep8==2.3.2
|
|
2
|
-
black==25.
|
|
2
|
+
black==25.11.0
|
|
3
3
|
bleach==6.3.0
|
|
4
|
-
certifi==2025.
|
|
4
|
+
certifi==2025.11.12
|
|
5
5
|
charset-normalizer==3.4.4
|
|
6
6
|
click==8.3.0
|
|
7
7
|
colorama==0.4.6
|
|
8
|
-
docutils==0.22.
|
|
8
|
+
docutils==0.22.3
|
|
9
9
|
flake8==7.3.0
|
|
10
10
|
gitchangelog==3.0.4
|
|
11
|
+
pytest==8.3.3
|
|
11
12
|
idna==3.11
|
|
12
13
|
importlib-metadata==8.7.0
|
|
13
14
|
jaraco.classes==3.4.0
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Behavioral tests for attachment functionality."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import Mock
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from github_backup import github_backup
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def attachment_test_setup(tmp_path):
|
|
16
|
+
"""Fixture providing setup and helper for attachment download tests."""
|
|
17
|
+
from unittest.mock import patch
|
|
18
|
+
|
|
19
|
+
issue_cwd = tmp_path / "issues"
|
|
20
|
+
issue_cwd.mkdir()
|
|
21
|
+
|
|
22
|
+
# Mock args
|
|
23
|
+
args = Mock()
|
|
24
|
+
args.as_app = False
|
|
25
|
+
args.token_fine = None
|
|
26
|
+
args.token_classic = None
|
|
27
|
+
args.username = None
|
|
28
|
+
args.password = None
|
|
29
|
+
args.osx_keychain_item_name = None
|
|
30
|
+
args.osx_keychain_item_account = None
|
|
31
|
+
args.user = "testuser"
|
|
32
|
+
args.repository = "testrepo"
|
|
33
|
+
|
|
34
|
+
repository = {"full_name": "testuser/testrepo"}
|
|
35
|
+
|
|
36
|
+
def call_download(issue_data, issue_number=123):
|
|
37
|
+
"""Call download_attachments with mocked HTTP downloads.
|
|
38
|
+
|
|
39
|
+
Returns list of URLs that were actually downloaded.
|
|
40
|
+
"""
|
|
41
|
+
downloaded_urls = []
|
|
42
|
+
|
|
43
|
+
def mock_download(url, path, auth, as_app, fine):
|
|
44
|
+
downloaded_urls.append(url)
|
|
45
|
+
return {
|
|
46
|
+
"success": True,
|
|
47
|
+
"saved_as": os.path.basename(path),
|
|
48
|
+
"url": url,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
with patch(
|
|
52
|
+
"github_backup.github_backup.download_attachment_file",
|
|
53
|
+
side_effect=mock_download,
|
|
54
|
+
):
|
|
55
|
+
github_backup.download_attachments(
|
|
56
|
+
args, str(issue_cwd), issue_data, issue_number, repository
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return downloaded_urls
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
"issue_cwd": str(issue_cwd),
|
|
63
|
+
"args": args,
|
|
64
|
+
"repository": repository,
|
|
65
|
+
"call_download": call_download,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestURLExtraction:
|
|
70
|
+
"""Test URL extraction with realistic issue content."""
|
|
71
|
+
|
|
72
|
+
def test_mixed_urls(self):
|
|
73
|
+
issue_data = {
|
|
74
|
+
"body": """
|
|
75
|
+
## Bug Report
|
|
76
|
+
|
|
77
|
+
When uploading files, I see this error. Here's a screenshot:
|
|
78
|
+
https://github.com/user-attachments/assets/abc123def456
|
|
79
|
+
|
|
80
|
+
The logs show: https://github.com/user-attachments/files/789/error-log.txt
|
|
81
|
+
|
|
82
|
+
This is similar to https://github.com/someorg/somerepo/issues/42 but different.
|
|
83
|
+
|
|
84
|
+
You can also see the video at https://user-images.githubusercontent.com/12345/video-demo.mov
|
|
85
|
+
|
|
86
|
+
Here's how to reproduce:
|
|
87
|
+
```bash
|
|
88
|
+
# Don't extract this example URL:
|
|
89
|
+
curl https://github.com/user-attachments/assets/example999
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
More info at https://docs.example.com/guide
|
|
93
|
+
|
|
94
|
+
Also see this inline code `https://github.com/user-attachments/files/111/inline.pdf` should not extract.
|
|
95
|
+
|
|
96
|
+
Final attachment: https://github.com/user-attachments/files/222/report.pdf.
|
|
97
|
+
""",
|
|
98
|
+
"comment_data": [
|
|
99
|
+
{
|
|
100
|
+
"body": "Here's another attachment: https://private-user-images.githubusercontent.com/98765/secret.png?jwt=token123"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"body": """
|
|
104
|
+
Example code:
|
|
105
|
+
```python
|
|
106
|
+
url = "https://github.com/user-attachments/assets/code-example"
|
|
107
|
+
```
|
|
108
|
+
But this is real: https://github.com/user-attachments/files/333/actual.zip
|
|
109
|
+
"""
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Extract URLs
|
|
115
|
+
urls = github_backup.extract_attachment_urls(issue_data)
|
|
116
|
+
|
|
117
|
+
expected_urls = [
|
|
118
|
+
"https://github.com/user-attachments/assets/abc123def456",
|
|
119
|
+
"https://github.com/user-attachments/files/789/error-log.txt",
|
|
120
|
+
"https://user-images.githubusercontent.com/12345/video-demo.mov",
|
|
121
|
+
"https://github.com/user-attachments/files/222/report.pdf",
|
|
122
|
+
"https://private-user-images.githubusercontent.com/98765/secret.png?jwt=token123",
|
|
123
|
+
"https://github.com/user-attachments/files/333/actual.zip",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
assert set(urls) == set(expected_urls)
|
|
127
|
+
|
|
128
|
+
def test_trailing_punctuation_stripped(self):
|
|
129
|
+
"""URLs with trailing punctuation should have punctuation stripped."""
|
|
130
|
+
issue_data = {
|
|
131
|
+
"body": """
|
|
132
|
+
See this file: https://github.com/user-attachments/files/1/doc.pdf.
|
|
133
|
+
And this one (https://github.com/user-attachments/files/2/image.png).
|
|
134
|
+
Check it out! https://github.com/user-attachments/files/3/data.csv!
|
|
135
|
+
"""
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
urls = github_backup.extract_attachment_urls(issue_data)
|
|
139
|
+
|
|
140
|
+
expected = [
|
|
141
|
+
"https://github.com/user-attachments/files/1/doc.pdf",
|
|
142
|
+
"https://github.com/user-attachments/files/2/image.png",
|
|
143
|
+
"https://github.com/user-attachments/files/3/data.csv",
|
|
144
|
+
]
|
|
145
|
+
assert set(urls) == set(expected)
|
|
146
|
+
|
|
147
|
+
def test_deduplication_across_body_and_comments(self):
|
|
148
|
+
"""Same URL in body and comments should only appear once."""
|
|
149
|
+
duplicate_url = "https://github.com/user-attachments/assets/abc123"
|
|
150
|
+
|
|
151
|
+
issue_data = {
|
|
152
|
+
"body": f"First mention: {duplicate_url}",
|
|
153
|
+
"comment_data": [
|
|
154
|
+
{"body": f"Second mention: {duplicate_url}"},
|
|
155
|
+
{"body": f"Third mention: {duplicate_url}"},
|
|
156
|
+
],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
urls = github_backup.extract_attachment_urls(issue_data)
|
|
160
|
+
|
|
161
|
+
assert set(urls) == {duplicate_url}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TestFilenameExtraction:
|
|
165
|
+
"""Test filename extraction from different URL types."""
|
|
166
|
+
|
|
167
|
+
def test_modern_assets_url(self):
|
|
168
|
+
"""Modern assets URL returns UUID."""
|
|
169
|
+
url = "https://github.com/user-attachments/assets/abc123def456"
|
|
170
|
+
filename = github_backup.get_attachment_filename(url)
|
|
171
|
+
assert filename == "abc123def456"
|
|
172
|
+
|
|
173
|
+
def test_modern_files_url(self):
|
|
174
|
+
"""Modern files URL returns filename."""
|
|
175
|
+
url = "https://github.com/user-attachments/files/12345/report.pdf"
|
|
176
|
+
filename = github_backup.get_attachment_filename(url)
|
|
177
|
+
assert filename == "report.pdf"
|
|
178
|
+
|
|
179
|
+
def test_legacy_cdn_url(self):
|
|
180
|
+
"""Legacy CDN URL returns filename with extension."""
|
|
181
|
+
url = "https://user-images.githubusercontent.com/123456/abc-def.png"
|
|
182
|
+
filename = github_backup.get_attachment_filename(url)
|
|
183
|
+
assert filename == "abc-def.png"
|
|
184
|
+
|
|
185
|
+
def test_private_cdn_url(self):
|
|
186
|
+
"""Private CDN URL returns filename."""
|
|
187
|
+
url = "https://private-user-images.githubusercontent.com/98765/secret.png?jwt=token123"
|
|
188
|
+
filename = github_backup.get_attachment_filename(url)
|
|
189
|
+
assert filename == "secret.png"
|
|
190
|
+
|
|
191
|
+
def test_repo_files_url(self):
|
|
192
|
+
"""Repo-scoped files URL returns filename."""
|
|
193
|
+
url = "https://github.com/owner/repo/files/789/document.txt"
|
|
194
|
+
filename = github_backup.get_attachment_filename(url)
|
|
195
|
+
assert filename == "document.txt"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class TestFilenameCollision:
|
|
199
|
+
"""Test filename collision resolution."""
|
|
200
|
+
|
|
201
|
+
def test_collision_behavior(self):
|
|
202
|
+
"""Test filename collision resolution with real files."""
|
|
203
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
204
|
+
# No collision - file doesn't exist
|
|
205
|
+
result = github_backup.resolve_filename_collision(
|
|
206
|
+
os.path.join(tmpdir, "report.pdf")
|
|
207
|
+
)
|
|
208
|
+
assert result == os.path.join(tmpdir, "report.pdf")
|
|
209
|
+
|
|
210
|
+
# Create the file, now collision exists
|
|
211
|
+
Path(os.path.join(tmpdir, "report.pdf")).touch()
|
|
212
|
+
result = github_backup.resolve_filename_collision(
|
|
213
|
+
os.path.join(tmpdir, "report.pdf")
|
|
214
|
+
)
|
|
215
|
+
assert result == os.path.join(tmpdir, "report_1.pdf")
|
|
216
|
+
|
|
217
|
+
# Create report_1.pdf too
|
|
218
|
+
Path(os.path.join(tmpdir, "report_1.pdf")).touch()
|
|
219
|
+
result = github_backup.resolve_filename_collision(
|
|
220
|
+
os.path.join(tmpdir, "report.pdf")
|
|
221
|
+
)
|
|
222
|
+
assert result == os.path.join(tmpdir, "report_2.pdf")
|
|
223
|
+
|
|
224
|
+
def test_manifest_reserved(self):
|
|
225
|
+
"""manifest.json is always treated as reserved."""
|
|
226
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
227
|
+
# Even if manifest.json doesn't exist, should get manifest_1.json
|
|
228
|
+
result = github_backup.resolve_filename_collision(
|
|
229
|
+
os.path.join(tmpdir, "manifest.json")
|
|
230
|
+
)
|
|
231
|
+
assert result == os.path.join(tmpdir, "manifest_1.json")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class TestManifestDuplicatePrevention:
|
|
235
|
+
"""Test that manifest prevents duplicate downloads (the bug fix)."""
|
|
236
|
+
|
|
237
|
+
def test_manifest_filters_existing_urls(self, attachment_test_setup):
|
|
238
|
+
"""URLs in manifest are not re-downloaded."""
|
|
239
|
+
setup = attachment_test_setup
|
|
240
|
+
|
|
241
|
+
# Create manifest with existing URLs
|
|
242
|
+
attachments_dir = os.path.join(setup["issue_cwd"], "attachments", "123")
|
|
243
|
+
os.makedirs(attachments_dir)
|
|
244
|
+
manifest_path = os.path.join(attachments_dir, "manifest.json")
|
|
245
|
+
|
|
246
|
+
manifest = {
|
|
247
|
+
"attachments": [
|
|
248
|
+
{
|
|
249
|
+
"url": "https://github.com/user-attachments/assets/old1",
|
|
250
|
+
"success": True,
|
|
251
|
+
"saved_as": "old1.pdf",
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"url": "https://github.com/user-attachments/assets/old2",
|
|
255
|
+
"success": True,
|
|
256
|
+
"saved_as": "old2.pdf",
|
|
257
|
+
},
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
with open(manifest_path, "w") as f:
|
|
261
|
+
json.dump(manifest, f)
|
|
262
|
+
|
|
263
|
+
# Issue data with 2 old URLs and 1 new URL
|
|
264
|
+
issue_data = {
|
|
265
|
+
"body": """
|
|
266
|
+
Old: https://github.com/user-attachments/assets/old1
|
|
267
|
+
Old: https://github.com/user-attachments/assets/old2
|
|
268
|
+
New: https://github.com/user-attachments/assets/new1
|
|
269
|
+
"""
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
downloaded_urls = setup["call_download"](issue_data)
|
|
273
|
+
|
|
274
|
+
# Should only download the NEW URL (old ones filtered by manifest)
|
|
275
|
+
assert len(downloaded_urls) == 1
|
|
276
|
+
assert downloaded_urls[0] == "https://github.com/user-attachments/assets/new1"
|
|
277
|
+
|
|
278
|
+
def test_no_manifest_downloads_all(self, attachment_test_setup):
|
|
279
|
+
"""Without manifest, all URLs should be downloaded."""
|
|
280
|
+
setup = attachment_test_setup
|
|
281
|
+
|
|
282
|
+
# Issue data with 2 URLs
|
|
283
|
+
issue_data = {
|
|
284
|
+
"body": """
|
|
285
|
+
https://github.com/user-attachments/assets/url1
|
|
286
|
+
https://github.com/user-attachments/assets/url2
|
|
287
|
+
"""
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
downloaded_urls = setup["call_download"](issue_data)
|
|
291
|
+
|
|
292
|
+
# Should download ALL URLs (no manifest to filter)
|
|
293
|
+
assert len(downloaded_urls) == 2
|
|
294
|
+
assert set(downloaded_urls) == {
|
|
295
|
+
"https://github.com/user-attachments/assets/url1",
|
|
296
|
+
"https://github.com/user-attachments/assets/url2",
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
def test_manifest_skips_permanent_failures(self, attachment_test_setup):
|
|
300
|
+
"""Manifest skips permanent failures (404, 410) but retries transient (503)."""
|
|
301
|
+
setup = attachment_test_setup
|
|
302
|
+
|
|
303
|
+
# Create manifest with different failure types
|
|
304
|
+
attachments_dir = os.path.join(setup["issue_cwd"], "attachments", "123")
|
|
305
|
+
os.makedirs(attachments_dir)
|
|
306
|
+
manifest_path = os.path.join(attachments_dir, "manifest.json")
|
|
307
|
+
|
|
308
|
+
manifest = {
|
|
309
|
+
"attachments": [
|
|
310
|
+
{
|
|
311
|
+
"url": "https://github.com/user-attachments/assets/success",
|
|
312
|
+
"success": True,
|
|
313
|
+
"saved_as": "success.pdf",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"url": "https://github.com/user-attachments/assets/notfound",
|
|
317
|
+
"success": False,
|
|
318
|
+
"http_status": 404,
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"url": "https://github.com/user-attachments/assets/gone",
|
|
322
|
+
"success": False,
|
|
323
|
+
"http_status": 410,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"url": "https://github.com/user-attachments/assets/unavailable",
|
|
327
|
+
"success": False,
|
|
328
|
+
"http_status": 503,
|
|
329
|
+
},
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
with open(manifest_path, "w") as f:
|
|
333
|
+
json.dump(manifest, f)
|
|
334
|
+
|
|
335
|
+
# Issue data has all 4 URLs
|
|
336
|
+
issue_data = {
|
|
337
|
+
"body": """
|
|
338
|
+
https://github.com/user-attachments/assets/success
|
|
339
|
+
https://github.com/user-attachments/assets/notfound
|
|
340
|
+
https://github.com/user-attachments/assets/gone
|
|
341
|
+
https://github.com/user-attachments/assets/unavailable
|
|
342
|
+
"""
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
downloaded_urls = setup["call_download"](issue_data)
|
|
346
|
+
|
|
347
|
+
# Should only retry 503 (transient failure)
|
|
348
|
+
# Success, 404, and 410 should be skipped
|
|
349
|
+
assert len(downloaded_urls) == 1
|
|
350
|
+
assert (
|
|
351
|
+
downloaded_urls[0]
|
|
352
|
+
== "https://github.com/user-attachments/assets/unavailable"
|
|
353
|
+
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.51.0"
|
|
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
|