mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-24 13:40:21 +01:00
fix(prowlarr): prevent silent loss of indexer seed limits on transient Prowlarr API failures (#1110)
# fix(prowlarr): prevent silent loss of indexer seed limits Related to #795, though not a fix for that specific (closed) report — see note below. ## Problem With "Use Prowlarr seed preferences" enabled, ~5–10% of torrent grabs are added to the download client without their configured share limits and seed indefinitely (∞ ETA in qBittorrent). Seed limits are resolved once at search time. `get_indexer_seed_settings()` builds on `get_indexers()`, which swallows all API errors and returns `[]`. A transient failure of the `/api/v1/indexer` call therefore produces an empty settings dict that is indistinguishable from "no limits configured", while the search itself (separate HTTP calls) still succeeds. Every result from that search is cached without `configuredSeedTimeMinutes`; grabbing one sends the torrent to the client with no limits. Compounding factors: `cache_release()` is last-write-wins by GUID, so one degraded search can strip enrichment from a previously good cache entry; and the retry fields persisted at queue time snapshot the same missing values, so retries reproduce the failure. ## Changes - **`api.py`** — `get_indexers()` / `get_enabled_indexers_detailed()` gain a keyword-only `raise_on_error` (default `False`, existing behavior unchanged). `get_indexer_seed_settings()` uses it, so fetch failures now propagate and an empty dict strictly means "nothing configured". - **`source.py`** — searches fetch settings via `_fetch_indexer_seed_settings()`, which maintains a module-level last-known-good copy (merged on each success) and falls back to it with a warning when the fetch fails. After one successful fetch, results can no longer be cached un-enriched. - **`handler.py`** — grab-time safety net in `_resolve_download()`: if seed preferences are enabled, the release is a torrent, and no configured limits are present in the cached result, the handler re-resolves the limits from Prowlarr for that indexer (`restrict_to=[indexerId]`) before adding to the client. If limits still can't be resolved, a warning is logged so the condition is visible instead of silent. - **Tests** — regression coverage: last-known-good fallback (success updates cache, failure falls back, failure with no history returns empty, fallback copy is mutation-safe) and grab-time fallback (used when enrichment is missing, tolerates Prowlarr being down, skipped when enrichment is present). Existing test stubs for `get_enabled_indexers_detailed` updated to accept the new kwarg. ## Testing - `uv run pytest tests/prowlarr/test_handler.py tests/prowlarr/test_source.py tests/prowlarr/test_integration_handler.py` — 93 passed, 2 skipped (Python 3.14.4) - Full `tests/prowlarr` run has 11 pre-existing failures on this environment (Windows path-separator assertions in the qBittorrent/NZBGet/SABnzbd/Transmission client tests, e.g. `/downloads/x` vs `\downloads\x`); confirmed these also fail on unpatched `main` and are unrelated to this change - `uv run ruff check` / `ruff format --check` — clean on touched files - `uv run basedpyright` — 0 errors on touched files No behavior change when `PROWLARR_USE_SEED_PREFERENCES` is disabled; the fallback path only activates when the preference is on and enrichment is missing for a torrent. --- **Note on #795:** this PR references #795 for background context on the seed-limits feature, but it does not fix that report — #795 was about seed limits not being converted/applied at all (a units mismatch), and was already fixed by #946 / #959. This PR fixes a separate, still-present bug: `get_indexers()` silently swallowing transient API errors, which intermittently drops seed limits even when the feature is otherwise working correctly.
This commit is contained in:
@@ -158,24 +158,35 @@ class ProwlarrClient:
|
||||
logger.info("Prowlarr connection successful: version %s", version)
|
||||
return True, f"Connected to Prowlarr {version}"
|
||||
|
||||
def get_indexers(self) -> list[dict[str, Any]]:
|
||||
"""Get all configured indexers."""
|
||||
def get_indexers(self, *, raise_on_error: bool = False) -> list[dict[str, Any]]:
|
||||
"""Get all configured indexers.
|
||||
|
||||
Args:
|
||||
raise_on_error: When True, propagate API failures instead of
|
||||
returning an empty list. Callers that must distinguish
|
||||
"no indexers" from "the request failed" should set this.
|
||||
|
||||
"""
|
||||
try:
|
||||
return _normalize_json_object_list(
|
||||
self._request("GET", "/api/v1/indexer"),
|
||||
context="Prowlarr indexer list",
|
||||
)
|
||||
except _PROWLARR_CLIENT_ERRORS:
|
||||
if raise_on_error:
|
||||
raise
|
||||
logger.exception("Failed to get indexers")
|
||||
return []
|
||||
|
||||
def get_enabled_indexers_detailed(self) -> list[dict[str, Any]]:
|
||||
def get_enabled_indexers_detailed(
|
||||
self, *, raise_on_error: bool = False
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get enabled indexers, including implementation metadata.
|
||||
|
||||
Note: Prowlarr indexer "name" is user-configurable; prefer
|
||||
"implementation"/"implementationName" for stable identification.
|
||||
"""
|
||||
indexers = self.get_indexers()
|
||||
indexers = self.get_indexers(raise_on_error=raise_on_error)
|
||||
return [idx for idx in indexers if idx.get("enable", False)]
|
||||
|
||||
def get_enriched_indexer_ids(self, *, restrict_to: list[int] | None = None) -> list[int]:
|
||||
@@ -214,10 +225,17 @@ class ProwlarrClient:
|
||||
|
||||
Prowlarr exposes seedTime in minutes, which is also the unit expected by
|
||||
torrent clients.
|
||||
|
||||
Raises:
|
||||
requests.exceptions.RequestException (and other client errors) when
|
||||
the indexer list cannot be fetched. An empty dict strictly means
|
||||
"no share limits are configured", never "the request failed" -
|
||||
callers rely on this to avoid silently dropping seed limits.
|
||||
|
||||
"""
|
||||
settings_by_indexer: dict[int, IndexerSeedSettings] = {}
|
||||
|
||||
for idx in self.get_enabled_indexers_detailed():
|
||||
for idx in self.get_enabled_indexers_detailed(raise_on_error=True):
|
||||
idx_id_int = coerce_int_like(idx.get("id"))
|
||||
if idx_id_int is None:
|
||||
continue
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import requests
|
||||
|
||||
from shelfmark.core.config import config
|
||||
from shelfmark.core.logger import setup_logger
|
||||
from shelfmark.core.request_helpers import normalize_optional_text
|
||||
from shelfmark.core.utils import normalize_http_url
|
||||
from shelfmark.download.clients import (
|
||||
DownloadClient,
|
||||
get_client,
|
||||
@@ -24,6 +27,7 @@ from shelfmark.download.clients.base_handler import (
|
||||
ExternalClientHandler,
|
||||
)
|
||||
from shelfmark.release_sources import register_handler
|
||||
from shelfmark.release_sources.prowlarr.api import IndexerSeedSettings, ProwlarrClient
|
||||
from shelfmark.release_sources.prowlarr.cache import get_release, remove_release
|
||||
from shelfmark.release_sources.prowlarr.utils import (
|
||||
coerce_int_like,
|
||||
@@ -37,6 +41,16 @@ if TYPE_CHECKING:
|
||||
from shelfmark.core.models import DownloadTask
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
# Errors that ProwlarrClient can raise when fetching indexer settings.
|
||||
_SEED_SETTINGS_FALLBACK_ERRORS = (
|
||||
requests.exceptions.RequestException,
|
||||
OSError,
|
||||
RuntimeError,
|
||||
TypeError,
|
||||
ValueError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ProwlarrHandler",
|
||||
"POLL_INTERVAL",
|
||||
@@ -62,6 +76,47 @@ def _coerce_positive_minutes(raw_minutes: object) -> int | None:
|
||||
class ProwlarrHandler(ExternalClientHandler):
|
||||
"""Handler for Prowlarr downloads via configured torrent or usenet client."""
|
||||
|
||||
@staticmethod
|
||||
def _build_prowlarr_client() -> ProwlarrClient | None:
|
||||
"""Build a ProwlarrClient from config, or None if not configured."""
|
||||
raw_url = config.get("PROWLARR_URL", "")
|
||||
raw_api_key = config.get("PROWLARR_API_KEY", "")
|
||||
url = normalize_optional_text(raw_url) if isinstance(raw_url, str) else None
|
||||
api_key = normalize_optional_text(raw_api_key) if isinstance(raw_api_key, str) else None
|
||||
if not url or not api_key:
|
||||
return None
|
||||
normalized_url = normalize_http_url(url)
|
||||
if not normalized_url:
|
||||
return None
|
||||
return ProwlarrClient(normalized_url, api_key)
|
||||
|
||||
def _fetch_seed_settings_fallback(self, raw_indexer_id: object) -> IndexerSeedSettings | None:
|
||||
"""Fetch share limits for one indexer directly from Prowlarr.
|
||||
|
||||
Used when the cached release is missing its search-time seed-limit
|
||||
enrichment so that transient failures during search cannot cause a
|
||||
torrent to be added without its configured share limits.
|
||||
"""
|
||||
indexer_id = coerce_int_like(raw_indexer_id)
|
||||
if indexer_id is None:
|
||||
return None
|
||||
|
||||
client = self._build_prowlarr_client()
|
||||
if client is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
settings = client.get_indexer_seed_settings(restrict_to=[indexer_id])
|
||||
except _SEED_SETTINGS_FALLBACK_ERRORS:
|
||||
logger.warning(
|
||||
"Grab-time seed settings fallback failed for indexerId=%s",
|
||||
indexer_id,
|
||||
exc_info=True,
|
||||
)
|
||||
return None
|
||||
|
||||
return settings.get(indexer_id)
|
||||
|
||||
def _get_client(self, protocol: str) -> DownloadClient | None:
|
||||
"""Compatibility shim so module-level patching still works in tests."""
|
||||
return get_client(protocol)
|
||||
@@ -171,6 +226,28 @@ class ProwlarrHandler(ExternalClientHandler):
|
||||
seeding_time_limit = _coerce_positive_minutes(raw_configured_seed_time)
|
||||
ratio_limit = float(raw_configured_ratio) if raw_configured_ratio is not None else None
|
||||
|
||||
# Fallback: search-time enrichment can be missing when the indexer
|
||||
# settings fetch transiently failed during the search (#795).
|
||||
# Re-resolve the limits from Prowlarr at grab time so torrents are
|
||||
# never sent to the client without their configured share limits.
|
||||
if seeding_time_limit is None and ratio_limit is None and protocol == "torrent":
|
||||
fallback = self._fetch_seed_settings_fallback(prowlarr_result.get("indexerId"))
|
||||
if fallback:
|
||||
seeding_time_limit = _coerce_positive_minutes(
|
||||
fallback.get("seeding_time_limit_minutes")
|
||||
)
|
||||
raw_ratio = fallback.get("ratio_limit")
|
||||
ratio_limit = float(raw_ratio) if raw_ratio is not None else None
|
||||
|
||||
if seeding_time_limit is None and ratio_limit is None and protocol == "torrent":
|
||||
logger.warning(
|
||||
"Prowlarr seed preferences are enabled but no share limits "
|
||||
"could be resolved for release '%s' (indexerId=%s); the "
|
||||
"torrent will use the client's global limits",
|
||||
release_name,
|
||||
prowlarr_result.get("indexerId"),
|
||||
)
|
||||
|
||||
return DownloadRequest(
|
||||
url=download_url,
|
||||
protocol=protocol,
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
import re
|
||||
import time
|
||||
from threading import Lock
|
||||
from typing import TYPE_CHECKING, ClassVar, NoReturn
|
||||
|
||||
import requests
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from shelfmark.core.search_plan import ReleaseSearchPlan
|
||||
from shelfmark.metadata_providers import BookMetadata
|
||||
@@ -41,6 +44,11 @@ _SIZE_UNIT_BASE = 1024
|
||||
_TWO_FORMATS = 2
|
||||
_PROWLARR_SOURCE_ERRORS = (AttributeError, OSError, RuntimeError, TypeError, ValueError)
|
||||
|
||||
# Errors that can surface from ProwlarrClient.get_indexer_seed_settings(). The
|
||||
# client raises requests exceptions (subclasses of OSError via IOError lineage
|
||||
# is not guaranteed), so include RequestException explicitly.
|
||||
_PROWLARR_SEED_SETTINGS_ERRORS = (*_PROWLARR_SOURCE_ERRORS, requests.exceptions.RequestException)
|
||||
|
||||
|
||||
def _raise_timeout_error(message: str) -> NoReturn:
|
||||
raise TimeoutError(message)
|
||||
@@ -443,6 +451,36 @@ def _prowlarr_result_to_release(
|
||||
)
|
||||
|
||||
|
||||
# Last successfully fetched per-indexer share limits. Used as a fallback when
|
||||
# a transient Prowlarr API failure prevents fetching fresh settings during a
|
||||
# search, so results are never silently cached without seed limits (#795).
|
||||
_seed_settings_lock = Lock()
|
||||
_last_known_seed_settings: dict[int, IndexerSeedSettings] = {}
|
||||
|
||||
|
||||
def _fetch_indexer_seed_settings(
|
||||
client: ProwlarrClient,
|
||||
indexer_ids: list[int] | None,
|
||||
) -> dict[int, IndexerSeedSettings]:
|
||||
"""Fetch per-indexer share limits, falling back to last-known-good on failure."""
|
||||
try:
|
||||
fetched = client.get_indexer_seed_settings(restrict_to=indexer_ids)
|
||||
except _PROWLARR_SEED_SETTINGS_ERRORS:
|
||||
with _seed_settings_lock:
|
||||
fallback = dict(_last_known_seed_settings)
|
||||
logger.warning(
|
||||
"Failed to fetch Prowlarr indexer seed settings; "
|
||||
"falling back to last known settings for %s indexer(s)",
|
||||
len(fallback),
|
||||
exc_info=True,
|
||||
)
|
||||
return fallback
|
||||
|
||||
with _seed_settings_lock:
|
||||
_last_known_seed_settings.update(fetched)
|
||||
return fetched
|
||||
|
||||
|
||||
def _apply_indexer_seed_settings(
|
||||
result: dict,
|
||||
indexer_seed_settings: dict[int, IndexerSeedSettings],
|
||||
@@ -783,7 +821,7 @@ class ProwlarrSource(ReleaseSource):
|
||||
enriched_indexer_ids = client.get_enriched_indexer_ids(restrict_to=indexer_ids)
|
||||
enriched_indexer_ids_set = set(enriched_indexer_ids)
|
||||
indexer_seed_settings = (
|
||||
client.get_indexer_seed_settings(restrict_to=indexer_ids)
|
||||
_fetch_indexer_seed_settings(client, indexer_ids)
|
||||
if config.get("PROWLARR_USE_SEED_PREFERENCES", False)
|
||||
else {}
|
||||
)
|
||||
|
||||
@@ -316,6 +316,124 @@ class TestProwlarrHandlerSeedCriteria:
|
||||
assert request.seeding_time_limit is None
|
||||
assert request.ratio_limit is None
|
||||
|
||||
def test_resolve_download_falls_back_to_prowlarr_when_enrichment_missing(self):
|
||||
"""Regression test for #795: when search-time enrichment is missing,
|
||||
share limits are re-resolved from Prowlarr at grab time."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_indexer_seed_settings.return_value = {
|
||||
5: {"seeding_time_limit_minutes": 4320, "ratio_limit": 1.0}
|
||||
}
|
||||
|
||||
def config_get(key, default=None):
|
||||
return True if key == "PROWLARR_USE_SEED_PREFERENCES" else default
|
||||
|
||||
with (
|
||||
patch(
|
||||
"shelfmark.release_sources.prowlarr.handler.get_release",
|
||||
return_value={
|
||||
"protocol": "torrent",
|
||||
"title": "Test Release",
|
||||
"magnetUrl": "magnet:?xt=urn:btih:abc123",
|
||||
"indexerId": 5,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"shelfmark.release_sources.prowlarr.handler.config.get",
|
||||
side_effect=config_get,
|
||||
),
|
||||
patch.object(
|
||||
ProwlarrHandler,
|
||||
"_build_prowlarr_client",
|
||||
return_value=mock_client,
|
||||
),
|
||||
):
|
||||
handler = ProwlarrHandler()
|
||||
task = DownloadTask(
|
||||
task_id="seed-time-fallback",
|
||||
source="prowlarr",
|
||||
title="Test Book",
|
||||
)
|
||||
|
||||
request = handler._resolve_download(task, lambda *_: None)
|
||||
|
||||
assert request is not None
|
||||
assert request.seeding_time_limit == 4320
|
||||
assert request.ratio_limit == 1.0
|
||||
mock_client.get_indexer_seed_settings.assert_called_once_with(restrict_to=[5])
|
||||
|
||||
def test_resolve_download_fallback_failure_leaves_limits_unset(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_indexer_seed_settings.side_effect = RuntimeError("prowlarr down")
|
||||
|
||||
def config_get(key, default=None):
|
||||
return True if key == "PROWLARR_USE_SEED_PREFERENCES" else default
|
||||
|
||||
with (
|
||||
patch(
|
||||
"shelfmark.release_sources.prowlarr.handler.get_release",
|
||||
return_value={
|
||||
"protocol": "torrent",
|
||||
"title": "Test Release",
|
||||
"magnetUrl": "magnet:?xt=urn:btih:abc123",
|
||||
"indexerId": 5,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"shelfmark.release_sources.prowlarr.handler.config.get",
|
||||
side_effect=config_get,
|
||||
),
|
||||
patch.object(
|
||||
ProwlarrHandler,
|
||||
"_build_prowlarr_client",
|
||||
return_value=mock_client,
|
||||
),
|
||||
):
|
||||
handler = ProwlarrHandler()
|
||||
task = DownloadTask(
|
||||
task_id="seed-time-fallback-failure",
|
||||
source="prowlarr",
|
||||
title="Test Book",
|
||||
)
|
||||
|
||||
request = handler._resolve_download(task, lambda *_: None)
|
||||
|
||||
assert request is not None
|
||||
assert request.seeding_time_limit is None
|
||||
assert request.ratio_limit is None
|
||||
|
||||
def test_resolve_download_skips_fallback_when_enrichment_present(self):
|
||||
with (
|
||||
patch(
|
||||
"shelfmark.release_sources.prowlarr.handler.get_release",
|
||||
return_value={
|
||||
"protocol": "torrent",
|
||||
"title": "Test Release",
|
||||
"magnetUrl": "magnet:?xt=urn:btih:abc123",
|
||||
"indexerId": 5,
|
||||
"configuredSeedTimeMinutes": 7200,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"shelfmark.release_sources.prowlarr.handler.config.get",
|
||||
side_effect=lambda key, default=None: (
|
||||
True if key == "PROWLARR_USE_SEED_PREFERENCES" else default
|
||||
),
|
||||
),
|
||||
patch.object(ProwlarrHandler, "_build_prowlarr_client") as mock_builder,
|
||||
):
|
||||
handler = ProwlarrHandler()
|
||||
task = DownloadTask(
|
||||
task_id="seed-time-no-fallback",
|
||||
source="prowlarr",
|
||||
title="Test Book",
|
||||
)
|
||||
|
||||
request = handler._resolve_download(task, lambda *_: None)
|
||||
|
||||
assert request is not None
|
||||
assert request.seeding_time_limit == 7200
|
||||
mock_builder.assert_not_called()
|
||||
|
||||
def test_download_passes_seed_limits_to_client(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.name = "qbittorrent"
|
||||
|
||||
@@ -13,6 +13,8 @@ from shelfmark.release_sources.prowlarr.source import (
|
||||
ProwlarrSource,
|
||||
_detect_content_type_from_categories,
|
||||
_extract_format,
|
||||
_fetch_indexer_seed_settings,
|
||||
_last_known_seed_settings,
|
||||
_parse_size,
|
||||
)
|
||||
from shelfmark.release_sources.prowlarr.utils import get_protocol_display, sanitize_download_url
|
||||
@@ -234,7 +236,7 @@ class FakeTorznabClient:
|
||||
self.search_results = search_results or []
|
||||
self.seed_settings = seed_settings or {}
|
||||
|
||||
def get_enabled_indexers_detailed(self):
|
||||
def get_enabled_indexers_detailed(self, *, raise_on_error=False):
|
||||
return [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -278,7 +280,7 @@ class TestProwlarrIndexerSeedSettings:
|
||||
monkeypatch.setattr(
|
||||
client,
|
||||
"get_enabled_indexers_detailed",
|
||||
lambda: [
|
||||
lambda *, raise_on_error=False: [
|
||||
{
|
||||
"id": 13,
|
||||
"protocol": "torrent",
|
||||
@@ -640,7 +642,7 @@ class TestProwlarrLocalizedQueries:
|
||||
source = ProwlarrSource()
|
||||
|
||||
class FailingClient:
|
||||
def get_enabled_indexers_detailed(self):
|
||||
def get_enabled_indexers_detailed(self, *, raise_on_error=False):
|
||||
raise RuntimeError("indexers unavailable")
|
||||
|
||||
monkeypatch.setattr(source, "_get_client", lambda: FailingClient())
|
||||
@@ -655,7 +657,7 @@ class TestProwlarrLocalizedQueries:
|
||||
source = ProwlarrSource()
|
||||
|
||||
class FailingClient:
|
||||
def get_enabled_indexers_detailed(self):
|
||||
def get_enabled_indexers_detailed(self, *, raise_on_error=False):
|
||||
raise RuntimeError("indexers unavailable")
|
||||
|
||||
assert source._resolve_indexer_ids_from_names(FailingClient(), ["Alpha"]) is None
|
||||
@@ -664,7 +666,52 @@ class TestProwlarrLocalizedQueries:
|
||||
source = ProwlarrSource()
|
||||
|
||||
class FailingClient:
|
||||
def get_enabled_indexers_detailed(self):
|
||||
def get_enabled_indexers_detailed(self, *, raise_on_error=False):
|
||||
raise RuntimeError("indexers unavailable")
|
||||
|
||||
assert source._get_search_indexer_ids(FailingClient(), None, [7000]) == []
|
||||
|
||||
|
||||
class TestFetchIndexerSeedSettingsFallback:
|
||||
"""Regression tests for #795: transient Prowlarr API failures must not
|
||||
silently strip per-indexer share limits from search results."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_last_known_seed_settings(self):
|
||||
_last_known_seed_settings.clear()
|
||||
yield
|
||||
_last_known_seed_settings.clear()
|
||||
|
||||
def test_success_updates_last_known_good(self):
|
||||
class Client:
|
||||
def get_indexer_seed_settings(self, restrict_to=None):
|
||||
del restrict_to
|
||||
return {13: {"seeding_time_limit_minutes": 4320, "ratio_limit": 1.0}}
|
||||
|
||||
fetched = _fetch_indexer_seed_settings(Client(), None)
|
||||
|
||||
assert fetched == {13: {"seeding_time_limit_minutes": 4320, "ratio_limit": 1.0}}
|
||||
assert _last_known_seed_settings == fetched
|
||||
|
||||
def test_failure_falls_back_to_last_known_good(self):
|
||||
_last_known_seed_settings[13] = {"seeding_time_limit_minutes": 4320}
|
||||
|
||||
class FailingClient:
|
||||
def get_indexer_seed_settings(self, restrict_to=None):
|
||||
del restrict_to
|
||||
raise RuntimeError("indexers unavailable")
|
||||
|
||||
fetched = _fetch_indexer_seed_settings(FailingClient(), None)
|
||||
|
||||
assert fetched == {13: {"seeding_time_limit_minutes": 4320}}
|
||||
# The fallback must be a copy so callers cannot mutate the cache.
|
||||
fetched[99] = {"ratio_limit": 2.0}
|
||||
assert 99 not in _last_known_seed_settings
|
||||
|
||||
def test_failure_with_no_history_returns_empty(self):
|
||||
class FailingClient:
|
||||
def get_indexer_seed_settings(self, restrict_to=None):
|
||||
del restrict_to
|
||||
raise RuntimeError("indexers unavailable")
|
||||
|
||||
assert _fetch_indexer_seed_settings(FailingClient(), None) == {}
|
||||
|
||||
Reference in New Issue
Block a user