diff --git a/plants/services/perenual.py b/plants/services/perenual.py index ce94494..97b3f4e 100644 --- a/plants/services/perenual.py +++ b/plants/services/perenual.py @@ -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.') diff --git a/plants/templates/plants/partials/species_results.html b/plants/templates/plants/partials/species_results.html index 7ec09ab..8dfd6ec 100644 --- a/plants/templates/plants/partials/species_results.html +++ b/plants/templates/plants/partials/species_results.html @@ -24,5 +24,9 @@ {% empty %} -{% if q %}
No results for "{{ q }}"
{% endif %} +{% if error %} +⚠️ {{ error }}
+{% elif q %} +No results for "{{ q }}"
+{% endif %} {% endfor %} diff --git a/plants/tests/test_perenual.py b/plants/tests/test_perenual.py index 81219bc..41d4b54 100644 --- a/plants/tests/test_perenual.py +++ b/plants/tests/test_perenual.py @@ -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') diff --git a/plants/views/species.py b/plants/views/species.py index 9a5cfdd..a0fa634 100644 --- a/plants/views/species.py +++ b/plants/views/species.py @@ -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, })