Skip to content

chore: bump pre-commit hooks - #48

Merged
nabobalis merged 3 commits into
mainfrom
pfss
Aug 7, 2026
Merged

nabobalis merged 3 commits into
mainfrom
pfss

Conversation

@nabobalis

@nabobalis nabobalis commented Aug 7, 2026 •

Copy link
Copy Markdown
Member

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:

  • Refactor the ADAPT downloader to use direct HTTP requests against NSO directory listings instead of the SunPy Fido client.
  • Introduce monthly ADAPT map caching with robust time-window handling across timezones, months, and missing year directories.
  • Add download integrity checks, atomic file replacement, and clearer error reporting for ADAPT FITS downloads.

Build:

  • Update ruff, prettier, and typos pre-commit hook revisions to newer versions.

Tests:

  • Extend ADAPT downloader tests to cover caching behavior, error handling, truncated downloads, cross-month searches, and timezone-normalized search windows.

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.
@sourcery-ai

sourcery-ai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Replaces 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

Change Details Files
Refactor ADAPT data discovery from SunPy/Fido queries to direct HTTP scraping of NSO Apache directory listings with per-month caching and time-window handling in UTC.
  • Introduce _ADAPT_ROOT, regex-based filename parsing, and _AdaptRow tuples to represent (time, url) rows.
  • Replace the old Fido-based _adapt_rows implementation and cache with _month_rows and per-month cache keyed by date, including handling of missing year directories (HTTP 404) and multi-month windows.
  • Ensure start/end times are converted to UTC, iterate month by month collecting rows, and use itemgetter-based _row_time plus updated _latest_adapt_row/_nearest_adapt_row signatures to work with tuples.
src/suntoday/downloaders/adapt.py
src/suntoday/downloaders/tests/test_adapt.py
Replace parfive/Fido-based file downloads with a streaming requests-based downloader that supports caching, atomic writes via PID-suffixed partial files, and content-length validation.
  • Change fetch_adapt_fits to derive the URL from _nearest_adapt_row and download via requests with a configurable timeout, streaming into a PID-unique .part file and atomically replacing the final destination.
  • Add logic to skip the download if the target file already exists and to validate against the Content-Length header, treating mismatches as truncated downloads and cleaning up partial files.
  • Update tests to mock requests.get, validate caching behavior (no extra GET on repeated calls), error propagation when downloads fail, truncated-download handling, and empty-directory behavior.
  • Introduce helper _download_response in tests using BytesIO to simulate streaming responses with headers.
src/suntoday/downloaders/adapt.py
src/suntoday/downloaders/tests/test_adapt.py
Strengthen and expand tests for ADAPT row selection and window/caching behavior under various edge cases, including month boundaries, missing years, and non-UTC inputs.
  • Update nearest-row tests to operate on (datetime, url) tuples instead of QueryResponseRow objects and to rely on the new _adapt_rows/_month_rows behavior.
  • Add tests that assert only one HTTP listing fetch is performed and verify the exact query pattern, timeout usage, and reuse of cached listings across multiple calls within the same month.
  • Add tests covering cross-year windows where the next year's directory is missing (HTTP 404) and non-UTC time windows that must be normalized to UTC, including checks on which months get scraped and resulting row times.
src/suntoday/downloaders/tests/test_adapt.py
Update pre-commit tooling versions to pick up newer linting/formatting behavior.
  • Bump ruff-pre-commit from v0.15.22 to v0.16.1.
  • Bump pre-commit-prettier from v3.9.5 to v3.9.6.
  • Bump typos mirror from v1.48.0 to v1.49.0.
.pre-commit-config.yaml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/suntoday/downloaders/adapt.py Outdated
Comment thread src/suntoday/downloaders/adapt.py Outdated
Comment on lines +29 to +41
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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"))

@nabobalis
nabobalis merged commit 33b78f1 into main Aug 7, 2026
5 of 6 checks passed
@nabobalis
nabobalis deleted the pfss branch August 7, 2026 01:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant