diff --git a/allianceauth/corputils/__init__.py b/allianceauth/corputils/__init__.py index e69de29b..4a281e3c 100644 --- a/allianceauth/corputils/__init__.py +++ b/allianceauth/corputils/__init__.py @@ -0,0 +1 @@ +__esi_compatibility_date__ = "2025-12-16" diff --git a/allianceauth/corputils/models.py b/allianceauth/corputils/models.py index 6ee3dbcf..ebcef4d4 100644 --- a/allianceauth/corputils/models.py +++ b/allianceauth/corputils/models.py @@ -1,31 +1,23 @@ import logging -import os from typing import ClassVar -from bravado.exception import HTTPForbidden - from django.db import models from esi.errors import TokenError +from esi.exceptions import HTTPClientError from esi.models import Token from allianceauth.authentication.models import CharacterOwnership, UserProfile from allianceauth.corputils.managers import CorpStatsManager -from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo +from allianceauth.corputils.providers import ( + get_characters_character_id, get_corporations_corporation_id_members, + post_universe_names, +) +from allianceauth.eveonline.models import ( + EveAllianceInfo, EveCharacter, EveCorporationInfo, +) from allianceauth.notifications import notify -SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json') -""" -Swagger spec operations: - -Character -get_characters_character_id -get_corporations_corporation_id_members -Universe -post_universe_names -""" - - logger = logging.getLogger(__name__) @@ -33,8 +25,9 @@ class CorpStats(models.Model): token = models.ForeignKey(Token, on_delete=models.CASCADE) corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE) last_update = models.DateTimeField(auto_now=True) + members: models.QuerySet["CorpMember"] - objects: ClassVar[CorpStatsManager] = CorpStatsManager() + objects: ClassVar[CorpStatsManager] = CorpStatsManager() # pyright: ignore[reportIncompatibleVariableOverride] class Meta: permissions = ( @@ -42,24 +35,22 @@ class CorpStats(models.Model): ('view_alliance_corpstats', 'Can view corp stats of members of their alliance.'), ('view_state_corpstats', 'Can view corp stats of members of their auth state.'), ) - verbose_name = "corp stats" - verbose_name_plural = "corp stats" + verbose_name = "Corp Stat" + verbose_name_plural = "Corp Stats" def __str__(self) -> str: return f"{self.__class__.__name__} for {self.corp}" def update(self) -> None: try: - c = self.token.get_esi_client(spec_file=SWAGGER_SPEC_PATH) - assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()['corporation_id'] == int(self.corp.corporation_id) - member_ids = c.Corporation.get_corporations_corporation_id_members( - corporation_id=self.corp.corporation_id).result() + assert get_characters_character_id(self.token.character_id).corporation_id == int(self.corp.corporation_id) + member_ids = get_corporations_corporation_id_members(corporation_id=self.corp.corporation_id, token=self.token) # requesting too many ids per call results in a HTTP400 # the swagger spec doesn't have a maxItems count # manual testing says we can do over 350, but let's not risk it member_id_chunks = [member_ids[i:i + 255] for i in range(0, len(member_ids), 255)] - member_name_chunks = [c.Universe.post_universe_names(ids=id_chunk).result() for id_chunk in member_id_chunks] + member_name_chunks = [post_universe_names(ids=id_chunk).result() for id_chunk in member_id_chunks] member_list = {} for name_chunk in member_name_chunks: member_list.update({m['id']: m['name'] for m in name_chunk}) @@ -83,11 +74,12 @@ class CorpStats(models.Model): message="Your token has expired or is no longer valid. Please add a new one to create a new CorpStats.", level="error") self.delete() - except HTTPForbidden as e: - logger.warning(f"{self} failed to update: {e}") - if self.token.user: - notify(self.token.user, f"{self} failed to update with your ESI token.", message=f"{e.status_code}: {e.message}", level="error") - self.delete() + except HTTPClientError as e: + if e.status_code == 403: + logger.warning(f"{self} failed to update: {e}") + if self.token.user: + notify(self.token.user, f"{self} failed to update with your ESI token.", message=f"{e.status_code}", level="error") + self.delete() except AssertionError: logger.warning(f"{self} token character no longer in corp.") if self.token.user: @@ -97,7 +89,7 @@ class CorpStats(models.Model): self.delete() @property - def member_count(self): + def member_count(self) -> int: return self.members.count() @property @@ -109,15 +101,15 @@ class CorpStats(models.Model): return len(self.registered_members) @property - def registered_members(self): + def registered_members(self) -> models.QuerySet["CorpMember"]: return self.members.filter(pk__in=[m.pk for m in self.members.all() if m.registered]) @property - def unregistered_member_count(self): + def unregistered_member_count(self) -> int: return self.member_count - self.registered_member_count @property - def unregistered_members(self): + def unregistered_members(self) -> models.QuerySet["CorpMember"]: return self.members.filter(pk__in=[m.pk for m in self.members.all() if not m.registered]) @property @@ -125,13 +117,13 @@ class CorpStats(models.Model): return len(self.mains) @property - def mains(self): + def mains(self) -> models.QuerySet["CorpMember"]: return self.members.filter(pk__in=[m.pk for m in self.members.all() if m.main_character and int(m.main_character.character_id) == int(m.character_id)]) - def visible_to(self, user): + def visible_to(self, user) -> bool: return CorpStats.objects.filter(pk=self.pk).visible_to(user).exists() - def can_update(self, user): + def can_update(self, user) -> bool: return self.token.user == user or self.visible_to(user) def corp_logo(self, size=128) -> str: @@ -150,6 +142,9 @@ class CorpMember(models.Model): corpstats = models.ForeignKey(CorpStats, on_delete=models.CASCADE, related_name='members') class Meta: + permissions = () + verbose_name = "Corp Member" + verbose_name_plural = "Corp Members" # not making character_id unique in case a character moves between two corps while only one updates unique_together = ('corpstats', 'character_id') ordering = ['character_name'] @@ -165,14 +160,14 @@ class CorpMember(models.Model): return None @property - def main_character(self): + def main_character(self) -> EveCharacter | None: try: return self.character.character_ownership.user.profile.main_character except (CharacterOwnership.DoesNotExist, UserProfile.DoesNotExist, AttributeError): return None @property - def alts(self): + def alts(self) -> list["EveCharacter"]: if self.main_character: return [co.character for co in self.main_character.character_ownership.user.character_ownerships.all()] else: diff --git a/allianceauth/corputils/providers.py b/allianceauth/corputils/providers.py new file mode 100644 index 00000000..85efd70f --- /dev/null +++ b/allianceauth/corputils/providers.py @@ -0,0 +1,47 @@ +from typing import TYPE_CHECKING + +from esi.models import Token +from esi.openapi_clients import ESIClientProvider +from esi.stubs import PostUniverseNamesOperation + +from .. import __title__, __url__, __version__ +from . import __esi_compatibility_date__ + +if TYPE_CHECKING: + from esi.stubs import ( + CharactersDetail, CorporationsCorporationIdMembersGet, + PostUniverseNamesOperationBody, + ) + +esi = ESIClientProvider( + ua_appname=__title__, + ua_version=__version__, + ua_url=__url__, + compatibility_date=__esi_compatibility_date__, + operations=[ + "GetCharactersCharacterId", + "GetCorporationsCorporationIdMembers" + "PostUniverseNames", + ]) + +""" +Swagger spec operations: + +Character +get_characters_character_id +get_corporations_corporation_id_members +Universe +post_universe_names +""" + + +def get_characters_character_id(character_id: int) -> CharactersDetail: + return esi.client.Character.GetCharactersCharacterId(character_id).result() + + +def get_corporations_corporation_id_members(corporation_id: int, token: Token) -> CorporationsCorporationIdMembersGet: + return esi.client.Corporation.GetCorporationsCorporationIdMembers(corporation_id=corporation_id, token=token).result() + + +def post_universe_names(ids: PostUniverseNamesOperationBody) -> PostUniverseNamesOperation: + return esi.client.Universe.PostUniverseNames(body=ids).result() diff --git a/allianceauth/corputils/tests.py b/allianceauth/corputils/tests.py index 176541b3..a63c3b42 100644 --- a/allianceauth/corputils/tests.py +++ b/allianceauth/corputils/tests.py @@ -1,7 +1,5 @@ from unittest import mock -from bravado.exception import HTTPForbidden - from django.contrib.auth.models import Permission from django.test import TestCase @@ -9,7 +7,9 @@ from esi.errors import TokenError from esi.models import Token from allianceauth.authentication.models import CharacterOwnership -from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo +from allianceauth.eveonline.models import ( + EveAllianceInfo, EveCharacter, EveCorporationInfo, +) from allianceauth.tests.auth_utils import AuthUtils from .models import CorpMember, CorpStats diff --git a/allianceauth/corputils/views.py b/allianceauth/corputils/views.py index d0e62690..768c2345 100644 --- a/allianceauth/corputils/views.py +++ b/allianceauth/corputils/views.py @@ -1,9 +1,9 @@ import os -from bravado.exception import HTTPError - from django.contrib import messages -from django.contrib.auth.decorators import login_required, permission_required, user_passes_test +from django.contrib.auth.decorators import ( + login_required, permission_required, user_passes_test, +) from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import IntegrityError from django.shortcuts import get_object_or_404, redirect, render