Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ Section Order:

<!-- Your changes go here -->

### Changed

- ESI error handling improved to catch and `HTTPError`, `ESIErrorLimitException` and
`ESIBucketLimitException` which can be thrown by `django-esi` when ESI is
unavailable or rate limited.

## [5.2.0] - 2026-10-04

> [!IMPORTANT]
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ ParsedConfigFiles := $(ConfigFile) $(wildcard $(ConfigFileOverride))

# Extract all config values from $(ConfigFile) and export them as Makefile variables
ifneq ($(wildcard $(ConfigFile)),)
TMPFILE := $(shell mkdir -p $(dir $(ConfigFile)) && mktemp $(dir $(ConfigFile))/make_vars.XXXXXX)
TMPFILE := $(shell mktemp "$${TMPDIR:-/tmp}/make_vars.XXXXXX")
$(shell /usr/bin/env python3 .make/parse_config.py --mode=make $(ParsedConfigFiles) > $(TMPFILE))
include $(TMPFILE)
$(shell rm -f $(TMPFILE))
Expand Down
14 changes: 12 additions & 2 deletions sovtimer/providers/esi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@

# Alliance Auth
from allianceauth.services.hooks import get_extension_logger
from esi.exceptions import HTTPClientError, HTTPNotModified
from esi.exceptions import (
ESIBucketLimitException,
ESIErrorLimitException,
HTTPClientError,
HTTPNotModified,
HTTPServerError,
)
from esi.openapi_clients import ESIClientProvider, EsiOperation

# AA Sovereignty Timer
Expand Down Expand Up @@ -127,9 +133,13 @@ def result( # pylint: disable=too-many-arguments, too-many-positional-arguments
)

esi_result = None
except (HTTPClientError, RequestError) as exc:
except (HTTPClientError, HTTPServerError, RequestError) as exc:
logger.error(msg=f"Error while fetching data from ESI: {str(exc)}")

esi_result = None
except (ESIErrorLimitException, ESIBucketLimitException) as exc:
logger.warning(msg=f"ESI rate limit hit, request not sent: {str(exc)}")

esi_result = None

# If caller requested the raw response, return a tuple (result, response)
Expand Down
4 changes: 2 additions & 2 deletions sovtimer/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def update_sov_campaigns(use_etags: bool = True, force_refresh: bool = False) ->
use_etags=use_etags, force_refresh=force_refresh
)

# Exit early if no campaigns are returned
# Exit early if no campaigns result is returned
if campaigns_from_esi is None:
return

Expand Down Expand Up @@ -195,7 +195,7 @@ def update_sov_structures( # pylint: disable=too-many-locals
use_etags=use_etags, force_refresh=force_refresh
)

# Exit early if no structures are returned
# Exit early if no structures result is returned
if structures_from_esi is None:
return

Expand Down
46 changes: 24 additions & 22 deletions sovtimer/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from sovtimer.models import Alliance, Campaign, SovereigntyStructure, logger
from sovtimer.tests import BaseTestCase

MODULE = "sovtimer.models"


class TypeCheckingImportBehavior(BaseTestCase):
"""
Expand Down Expand Up @@ -41,7 +43,7 @@ def test_imports_type_checking_symbols_when_flag_is_true(self):
sys.modules["esi.stubs"].SovereigntyCampaignsGetItem = object()

with patch.object(typing, "TYPE_CHECKING", True):
models = importlib.reload(importlib.import_module("sovtimer.models"))
models = importlib.reload(importlib.import_module(MODULE))

self.assertIn("Response", models.__dict__)
self.assertIn("SovereigntyCampaignsGetItem", models.__dict__)
Expand All @@ -67,7 +69,7 @@ def test_imports_type_checking_symbols_when_flag_is_true(self):
else:
sys.modules["esi"] = orig_esi

importlib.reload(importlib.import_module("sovtimer.models"))
importlib.reload(importlib.import_module(MODULE))

def test_does_not_import_type_checking_symbols_when_flag_is_false(self):
"""
Expand All @@ -90,7 +92,7 @@ def test_does_not_import_type_checking_symbols_when_flag_is_false(self):
sys.modules["esi.stubs"].SovereigntyCampaignsGetItem = object()

with patch.object(typing, "TYPE_CHECKING", False):
models = importlib.reload(importlib.import_module("sovtimer.models"))
models = importlib.reload(importlib.import_module(MODULE))

self.assertNotIn("Response", models.__dict__)
self.assertNotIn("SovereigntyCampaignsGetItem", models.__dict__)
Expand All @@ -111,17 +113,17 @@ def test_does_not_import_type_checking_symbols_when_flag_is_false(self):
else:
sys.modules["esi"] = orig_esi

importlib.reload(importlib.import_module("sovtimer.models"))
importlib.reload(importlib.import_module(MODULE))


class TestAlliance(BaseTestCase):
"""
Test cases for the Alliance model
"""

@patch("sovtimer.models.ESIHandler.get_alliances_alliance_id")
@patch("sovtimer.models.Alliance.objects.bulk_create")
@patch("sovtimer.models.Alliance.objects.filter")
@patch(MODULE + ".ESIHandler.get_alliances_alliance_id")
@patch(MODULE + ".Alliance.objects.bulk_create")
@patch(MODULE + ".Alliance.objects.filter")
def test_returns_all_alliances_when_all_ids_exist(
self, mock_filter, mock_bulk_create, mock_get_alliance
):
Expand Down Expand Up @@ -158,9 +160,9 @@ def test_returns_all_alliances_when_all_ids_exist(
mock_bulk_create.assert_not_called()
mock_get_alliance.assert_not_called()

@patch("sovtimer.models.ESIHandler.get_alliances_alliance_id")
@patch("sovtimer.models.Alliance.objects.bulk_create")
@patch("sovtimer.models.Alliance.objects.filter")
@patch(MODULE + ".ESIHandler.get_alliances_alliance_id")
@patch(MODULE + ".Alliance.objects.bulk_create")
@patch(MODULE + ".Alliance.objects.filter")
def test_creates_new_alliances_when_some_ids_do_not_exist(
self, mock_filter, mock_bulk_create, mock_get_alliance
):
Expand Down Expand Up @@ -209,9 +211,9 @@ def test_creates_new_alliances_when_some_ids_do_not_exist(
mock_get_alliance.assert_any_call(alliance_id=3, force_refresh=False)
mock_bulk_create.assert_called_once()

@patch("sovtimer.models.ESIHandler.get_alliances_alliance_id")
@patch("sovtimer.models.Alliance.objects.bulk_create")
@patch("sovtimer.models.Alliance.objects.filter")
@patch(MODULE + ".ESIHandler.get_alliances_alliance_id")
@patch(MODULE + ".Alliance.objects.bulk_create")
@patch(MODULE + ".Alliance.objects.filter")
def test_skips_creation_when_esi_returns_no_data(
self, mock_filter, mock_bulk_create, mock_get_alliance
):
Expand Down Expand Up @@ -249,7 +251,7 @@ class TestSovereigntyStructure(BaseTestCase):
Test cases for the SovereigntyStructure model
"""

@patch("sovtimer.models.ESIHandler.get_sovereignty_systems")
@patch(MODULE + ".ESIHandler.get_sovereignty_systems")
def test_returns_none_when_esihandler_returns_none(self, mock_get):
"""
Test that none is returned when ESI returns None
Expand All @@ -269,7 +271,7 @@ def test_returns_none_when_esihandler_returns_none(self, mock_get):
self.assertIsNone(result)
mock_get.assert_called_once_with(use_etags=False, force_refresh=False)

@patch("sovtimer.models.ESIHandler.get_sovereignty_systems")
@patch(MODULE + ".ESIHandler.get_sovereignty_systems")
def test_returns_parsed_structure_list_when_esihandler_returns_data(self, mock_get):
"""
Test that data is returned when ESI returns parsed data
Expand Down Expand Up @@ -328,7 +330,7 @@ def test_returns_parsed_structure_list_when_esihandler_returns_data(self, mock_g
self.assertEqual(structure["development"]["activity_defense_multiplier"], 0.5)
mock_get.assert_called_once_with(use_etags=True, force_refresh=True)

@patch("sovtimer.models.ESIHandler.get_sovereignty_systems")
@patch(MODULE + ".ESIHandler.get_sovereignty_systems")
def test_skips_systems_without_alliance_and_continues_processing_others(
self, mock_get
):
Expand Down Expand Up @@ -395,7 +397,7 @@ def root(self):
self.assertEqual(result[0]["solar_system_id"], 4001)
mock_get.assert_called_once_with(use_etags=True, force_refresh=False)

@patch("sovtimer.models.ESIHandler.get_sovereignty_systems")
@patch(MODULE + ".ESIHandler.get_sovereignty_systems")
def test_skips_system_when_development_raises_attribute_error(self, mock_get):
"""
Test skips system when system development is raises AttributeError and continues processing others
Expand Down Expand Up @@ -481,7 +483,7 @@ def strategic_level(self):
self.assertEqual(len(result), 0)
mock_get.assert_called_once_with(use_etags=True, force_refresh=False)

@patch("sovtimer.models.ESIHandler.get_sovereignty_systems")
@patch(MODULE + ".ESIHandler.get_sovereignty_systems")
def test_handles_system_with_no_alliance_field_gracefully(self, mock_get):
"""
Test handles system with no alliance field gracefully
Expand Down Expand Up @@ -515,7 +517,7 @@ class TestCampaign(BaseTestCase):
Test cases for the Campaign model
"""

@patch("sovtimer.models.ESIHandler.get_sovereignty_campaigns")
@patch(MODULE + ".ESIHandler.get_sovereignty_campaigns")
def test_returns_campaigns_when_esi_returns_data(self, mock_get_campaigns):
"""
Test that campaigns are returned when ESI returns data
Expand Down Expand Up @@ -545,7 +547,7 @@ def test_returns_campaigns_when_esi_returns_data(self, mock_get_campaigns):
use_etags=True, force_refresh=False, return_response=False
)

@patch("sovtimer.models.ESIHandler.get_sovereignty_campaigns")
@patch(MODULE + ".ESIHandler.get_sovereignty_campaigns")
def test_returns_empty_list_when_esi_returns_no_data(self, mock_get_campaigns):
"""
Test that an empty list is returned when ESI returns no data for campaigns
Expand All @@ -565,7 +567,7 @@ def test_returns_empty_list_when_esi_returns_no_data(self, mock_get_campaigns):
use_etags=True, force_refresh=False, return_response=False
)

@patch("sovtimer.models.ESIHandler.get_sovereignty_campaigns")
@patch(MODULE + ".ESIHandler.get_sovereignty_campaigns")
def test_logs_info_when_esi_returns_no_data(self, mock_get_campaigns):
"""
Test that an info log is generated when ESI returns no data for campaigns
Expand All @@ -583,7 +585,7 @@ def test_logs_info_when_esi_returns_no_data(self, mock_get_campaigns):

self.assertIn("No campaign changes found, nothing to update.", log.output[0])

@patch("sovtimer.models.ESIHandler.get_sovereignty_campaigns")
@patch(MODULE + ".ESIHandler.get_sovereignty_campaigns")
def test_logs_debug_when_esi_returns_data(self, mock_get_campaigns):
"""
Test that a debug log is generated when ESI returns data for campaigns
Expand Down
Loading
Loading