refactor(native-ingest): extract native_ingest_common + BaseNativeDownloader (#899 DRY 1/3)
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 17s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m20s

DRY pass commit 1 (process #594). Consolidate the helpers + download plumbing
the Patreon and SubscribeStar adapters had duplicated (SubscribeStar was
importing patreon privates — wrong owner). New backend/app/services/
native_ingest_common.py is the neutral home for:
- make_session (was _load_session ×2), retry_after_seconds + 429 constants,
  sanitize_segment, basename_from_url, post_dir_name, MediaOutcome /
  PostRecordOutcome.
- BaseNativeDownloader: the shared streaming GET (transient-retry + Range-resume)
  and validation/quarantine. Patreon + SubscribeStar downloaders now subclass it;
  each keeps only what differs (Patreon's Mux/yt-dlp video branch + detail-fetch
  enrichment; SubscribeStar nothing extra). Behavior preserved exactly; the
  divergence-bug risk (a fix to one _fetch_to_file not reaching the other) is gone.
- Folds in #899 L2: a quarantine now log.warning's path+reason (was counted only).

post_dir_name merges both date handlers (accepts trailing-Z and pre-parsed ISO).
Tests repointed to the single source at every consumer (rule 93 / §8b parity):
patreon_client/downloader, subscribestar_native. Exception-trio consolidation +
base _failure_result (2/3) and the remaining ingest_core logging (3/3) follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 11:32:28 -04:00
parent 817a002c2b
commit 7ac5c7e522
8 changed files with 402 additions and 504 deletions
+7 -7
View File
@@ -12,13 +12,13 @@ import pytest
import requests
from backend.app.services import patreon_client as pc_mod
from backend.app.services.native_ingest_common import retry_after_seconds
from backend.app.services.patreon_client import (
MediaItem,
PatreonAPIError,
PatreonAuthError,
PatreonClient,
PatreonDriftError,
_retry_after_seconds,
parse_cursor_from_url,
)
@@ -288,19 +288,19 @@ def test_verify_auth_inconclusive_on_network(monkeypatch):
def test_retry_after_seconds_honors_numeric_header():
assert _retry_after_seconds(_FakeResp(429, headers={"Retry-After": "7"}), 1) == 7.0
assert retry_after_seconds(_FakeResp(429, headers={"Retry-After": "7"}), 1) == 7.0
def test_retry_after_seconds_exponential_without_header():
resp = _FakeResp(429)
assert _retry_after_seconds(resp, 1) == 2.0
assert _retry_after_seconds(resp, 2) == 4.0
assert _retry_after_seconds(resp, 3) == 8.0
assert retry_after_seconds(resp, 1) == 2.0
assert retry_after_seconds(resp, 2) == 4.0
assert retry_after_seconds(resp, 3) == 8.0
def test_retry_after_seconds_caps():
assert _retry_after_seconds(_FakeResp(429), 10) == 30.0
assert _retry_after_seconds(_FakeResp(429, headers={"Retry-After": "999"}), 1) == 30.0
assert retry_after_seconds(_FakeResp(429), 10) == 30.0
assert retry_after_seconds(_FakeResp(429, headers={"Retry-After": "999"}), 1) == 30.0
def _client_with_sequence(monkeypatch, responses):