Conversation
Scrape NSO's monthly Apache index listings with requests instead of sunpy Fido/parfive. Month listings are cached per process, missing year directories (Jan 1 rollover) are treated as empty rather than a 404 crash, search windows are normalized to UTC, downloads go through a pid-unique .part file with a Content-Length completeness check, and save_directory is created if missing.
Reviewer's GuideReplaces the SunPy/Fido-based ADAPT downloader with a direct HTTP/Apache-index implementation, updates tests to match the new API and behavior (including caching, error handling, truncation checks, and month-spanning logic), and bumps several pre-commit hook versions. File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/suntoday/downloaders/adapt.py" line_range="80-81" />
<code_context>
- raise DataNotReadyError(msg)
- result = Fido.search(a.Time(start, end), *_ADAPT_QUERY)
- if len(result) == 0 or len(result[0]) == 0:
+ start = start.astimezone(UTC)
+ end = end.astimezone(UTC)
+ rows = []
+ month = start.date().replace(day=1)
</code_context>
<issue_to_address>
**issue (bug_risk):** Handle naive datetimes more defensively when normalizing to UTC
`datetime.astimezone` will raise `ValueError` for naive datetimes. If callers have been passing naive values (treating them as UTC), this becomes a new runtime failure. Either explicitly assume UTC for naive inputs (e.g. `if start.tzinfo is None: start = start.replace(tzinfo=UTC)`; same for `end`) or enforce that only timezone-aware datetimes are allowed. That way the behavior and failure mode are explicit and intentional.
</issue_to_address>
### Comment 2
<location path="src/suntoday/downloaders/adapt.py" line_range="179-180" />
<code_context>
+ partial.unlink(missing_ok=True)
+ msg = f"Failed to download {url} to {partial}: {error}"
+ raise OSError(msg) from error
+ actual_size = partial.stat().st_size
+ expected_size = int(response.headers.get("Content-Length", 0))
+ if expected_size and actual_size != expected_size:
+ partial.unlink(missing_ok=True)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Guard against non-integer or malformed Content-Length headers
`int(response.headers.get("Content-Length", 0))` will raise `ValueError` if the header is present but not a valid integer (e.g., malformed or oddly formatted). Consider parsing this in a small helper or `try/except` and defaulting to `0` on failure so non-compliant headers disable the size check instead of crashing the download path.
</issue_to_address>
### Comment 3
<location path="src/suntoday/downloaders/tests/test_adapt.py" line_range="29-41" />
<code_context>
assert now - timedelta(days=7) < latest < now
+def test_fetch_adapt_fits_downloads_and_caches(mocker, tmp_path) -> None:
+ url = "https://example.test/adapt.fts.gz"
+ mocker.patch.object(adapt, "_nearest_adapt_row", return_value=(datetime(2026, 7, 20, 12, tzinfo=UTC), url))
+ get = mocker.patch.object(adapt.requests, "get", return_value=_download_response(mocker, b"data", 4))
+ save_directory = tmp_path / "not-yet-created"
+
+ file = fetch_adapt_fits(datetime(2026, 7, 20, 12, tzinfo=UTC), save_directory=save_directory)
+
+ assert file == save_directory / "adapt.fts.gz"
+ assert file.read_bytes() == b"data"
+ # A second call reuses the existing file without touching the network.
+ assert fetch_adapt_fits(datetime(2026, 7, 20, 12, tzinfo=UTC), save_directory=save_directory) == file
+ assert get.call_count == 1
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Assert that temporary `.part` files are removed after a successful download.
Currently, the failure and truncation tests verify that no `.part` file is left behind, but this success-path test only checks the final file contents and cache behavior. Please also assert that no `adapt.fts.gz.<pid>.part` file remains in `save_directory` after a successful download, to guard against future regressions that might leave partial files in the cache.
```suggestion
def test_fetch_adapt_fits_downloads_and_caches(mocker, tmp_path) -> None:
url = "https://example.test/adapt.fts.gz"
mocker.patch.object(adapt, "_nearest_adapt_row", return_value=(datetime(2026, 7, 20, 12, tzinfo=UTC), url))
get = mocker.patch.object(adapt.requests, "get", return_value=_download_response(mocker, b"data", 4))
save_directory = tmp_path / "not-yet-created"
file = fetch_adapt_fits(datetime(2026, 7, 20, 12, tzinfo=UTC), save_directory=save_directory)
assert file == save_directory / "adapt.fts.gz"
assert file.read_bytes() == b"data"
# A second call reuses the existing file without touching the network.
assert fetch_adapt_fits(datetime(2026, 7, 20, 12, tzinfo=UTC), save_directory=save_directory) == file
assert get.call_count == 1
# Ensure that no temporary partial files remain after a successful download.
assert not list(save_directory.glob("adapt.fts.gz.*.part"))
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| def test_fetch_adapt_fits_downloads_and_caches(mocker, tmp_path) -> None: | ||
| url = "https://example.test/adapt.fts.gz" | ||
| mocker.patch.object(adapt, "_nearest_adapt_row", return_value=(datetime(2026, 7, 20, 12, tzinfo=UTC), url)) | ||
| get = mocker.patch.object(adapt.requests, "get", return_value=_download_response(mocker, b"data", 4)) | ||
| save_directory = tmp_path / "not-yet-created" | ||
|
|
||
| file = fetch_adapt_fits(datetime(2026, 7, 20, 12, tzinfo=UTC), save_directory=save_directory) | ||
|
|
||
| assert file == save_directory / "adapt.fts.gz" | ||
| assert file.read_bytes() == b"data" | ||
| # A second call reuses the existing file without touching the network. | ||
| assert fetch_adapt_fits(datetime(2026, 7, 20, 12, tzinfo=UTC), save_directory=save_directory) == file | ||
| assert get.call_count == 1 |
There was a problem hiding this comment.
suggestion (testing): Assert that temporary .part files are removed after a successful download.
Currently, the failure and truncation tests verify that no .part file is left behind, but this success-path test only checks the final file contents and cache behavior. Please also assert that no adapt.fts.gz.<pid>.part file remains in save_directory after a successful download, to guard against future regressions that might leave partial files in the cache.
| def test_fetch_adapt_fits_downloads_and_caches(mocker, tmp_path) -> None: | |
| url = "https://example.test/adapt.fts.gz" | |
| mocker.patch.object(adapt, "_nearest_adapt_row", return_value=(datetime(2026, 7, 20, 12, tzinfo=UTC), url)) | |
| get = mocker.patch.object(adapt.requests, "get", return_value=_download_response(mocker, b"data", 4)) | |
| save_directory = tmp_path / "not-yet-created" | |
| file = fetch_adapt_fits(datetime(2026, 7, 20, 12, tzinfo=UTC), save_directory=save_directory) | |
| assert file == save_directory / "adapt.fts.gz" | |
| assert file.read_bytes() == b"data" | |
| # A second call reuses the existing file without touching the network. | |
| assert fetch_adapt_fits(datetime(2026, 7, 20, 12, tzinfo=UTC), save_directory=save_directory) == file | |
| assert get.call_count == 1 | |
| def test_fetch_adapt_fits_downloads_and_caches(mocker, tmp_path) -> None: | |
| url = "https://example.test/adapt.fts.gz" | |
| mocker.patch.object(adapt, "_nearest_adapt_row", return_value=(datetime(2026, 7, 20, 12, tzinfo=UTC), url)) | |
| get = mocker.patch.object(adapt.requests, "get", return_value=_download_response(mocker, b"data", 4)) | |
| save_directory = tmp_path / "not-yet-created" | |
| file = fetch_adapt_fits(datetime(2026, 7, 20, 12, tzinfo=UTC), save_directory=save_directory) | |
| assert file == save_directory / "adapt.fts.gz" | |
| assert file.read_bytes() == b"data" | |
| # A second call reuses the existing file without touching the network. | |
| assert fetch_adapt_fits(datetime(2026, 7, 20, 12, tzinfo=UTC), save_directory=save_directory) == file | |
| assert get.call_count == 1 | |
| # Ensure that no temporary partial files remain after a successful download. | |
| assert not list(save_directory.glob("adapt.fts.gz.*.part")) |
Summary by Sourcery
Replace the SunPy-based ADAPT downloader with a direct HTTP implementation and strengthen ADAPT map selection and download safeguards while updating pre-commit tooling versions.
Enhancements:
Build:
Tests: