fix: surface Perenual API errors instead of silently returning no results
Timeout and rate-limit failures now show a warning message to the user. Timeout increased to 10s. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d8d7899a26
commit
40e3419160
4 changed files with 50 additions and 16 deletions
|
|
@ -4,17 +4,32 @@ from django.conf import settings
|
|||
_BASE = 'https://perenual.com/api'
|
||||
|
||||
|
||||
class PerenualError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def search_species(query):
|
||||
"""Query Perenual species-list endpoint. Returns list of result dicts or [] on any error."""
|
||||
"""Query Perenual species-list endpoint.
|
||||
|
||||
Returns list of result dicts on success.
|
||||
Raises PerenualError if the API is unreachable or rate-limited.
|
||||
Returns [] if no API key is configured.
|
||||
"""
|
||||
if not settings.PERENUAL_API_KEY:
|
||||
return []
|
||||
try:
|
||||
resp = requests.get(
|
||||
f'{_BASE}/species-list',
|
||||
params={'q': query, 'key': settings.PERENUAL_API_KEY},
|
||||
timeout=5,
|
||||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json().get('data', [])
|
||||
except requests.Timeout:
|
||||
raise PerenualError('Perenual API timed out — try again in a moment.')
|
||||
except requests.HTTPError as e:
|
||||
if e.response is not None and e.response.status_code == 429:
|
||||
raise PerenualError('Perenual API rate limit reached — try again later.')
|
||||
raise PerenualError(f'Perenual API error ({e.response.status_code if e.response else "?"}).')
|
||||
except Exception:
|
||||
return []
|
||||
raise PerenualError('Could not reach Perenual API.')
|
||||
|
|
|
|||
|
|
@ -24,5 +24,9 @@
|
|||
</form>
|
||||
</div>
|
||||
{% empty %}
|
||||
{% if q %}<p class="list-group-item text-muted">No results for "{{ q }}"</p>{% endif %}
|
||||
{% if error %}
|
||||
<p class="list-group-item text-warning">⚠️ {{ error }}</p>
|
||||
{% elif q %}
|
||||
<p class="list-group-item text-muted">No results for "{{ q }}"</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import pytest
|
||||
import requests as req_lib
|
||||
from unittest.mock import patch, MagicMock
|
||||
from plants.services.perenual import search_species
|
||||
from plants.services.perenual import search_species, PerenualError
|
||||
|
||||
|
||||
def test_search_returns_empty_without_api_key(settings):
|
||||
|
|
@ -24,17 +25,25 @@ def test_search_returns_data(settings):
|
|||
assert results[0]['common_name'] == 'Rose'
|
||||
|
||||
|
||||
def test_search_returns_empty_on_timeout(settings):
|
||||
def test_search_raises_on_timeout(settings):
|
||||
settings.PERENUAL_API_KEY = 'test-key'
|
||||
with patch('plants.services.perenual.requests.get', side_effect=Exception('timeout')):
|
||||
results = search_species('rose')
|
||||
assert results == []
|
||||
with patch('plants.services.perenual.requests.get', side_effect=req_lib.Timeout):
|
||||
with pytest.raises(PerenualError, match='timed out'):
|
||||
search_species('rose')
|
||||
|
||||
|
||||
def test_search_returns_empty_on_bad_status(settings):
|
||||
def test_search_raises_on_rate_limit(settings):
|
||||
settings.PERENUAL_API_KEY = 'test-key'
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.raise_for_status.side_effect = Exception('404')
|
||||
mock_resp.status_code = 429
|
||||
mock_resp.raise_for_status.side_effect = req_lib.HTTPError(response=mock_resp)
|
||||
with patch('plants.services.perenual.requests.get', return_value=mock_resp):
|
||||
results = search_species('rose')
|
||||
assert results == []
|
||||
with pytest.raises(PerenualError, match='rate limit'):
|
||||
search_species('rose')
|
||||
|
||||
|
||||
def test_search_raises_on_other_error(settings):
|
||||
settings.PERENUAL_API_KEY = 'test-key'
|
||||
with patch('plants.services.perenual.requests.get', side_effect=Exception('network error')):
|
||||
with pytest.raises(PerenualError):
|
||||
search_species('rose')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from django.shortcuts import render, redirect
|
||||
from django.urls import reverse
|
||||
from plants.models import Species
|
||||
from plants.services.perenual import search_species
|
||||
from plants.services.perenual import search_species, PerenualError
|
||||
|
||||
_MONTH_MAP = {
|
||||
'January': 1, 'February': 2, 'March': 3, 'April': 4,
|
||||
|
|
@ -12,9 +12,15 @@ _MONTH_MAP = {
|
|||
|
||||
def species_search(request):
|
||||
q = request.GET.get('q', '').strip()
|
||||
results = search_species(q) if len(q) >= 2 else []
|
||||
results = []
|
||||
error = None
|
||||
if len(q) >= 2:
|
||||
try:
|
||||
results = search_species(q)
|
||||
except PerenualError as e:
|
||||
error = str(e)
|
||||
return render(request, 'plants/partials/species_results.html', {
|
||||
'results': results, 'q': q,
|
||||
'results': results, 'q': q, 'error': error,
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue