aa-fleetfinder 2.7.2__py3-none-any.whl → 3.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aa-fleetfinder might be problematic. Click here for more details.
- {aa_fleetfinder-2.7.2.dist-info → aa_fleetfinder-3.0.0b1.dist-info}/METADATA +5 -14
- {aa_fleetfinder-2.7.2.dist-info → aa_fleetfinder-3.0.0b1.dist-info}/RECORD +32 -32
- fleetfinder/__init__.py +5 -3
- fleetfinder/apps.py +5 -4
- fleetfinder/locale/cs_CZ/LC_MESSAGES/django.po +21 -22
- fleetfinder/locale/de/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/de/LC_MESSAGES/django.po +27 -24
- fleetfinder/locale/django.pot +22 -23
- fleetfinder/locale/es/LC_MESSAGES/django.po +25 -22
- fleetfinder/locale/fr_FR/LC_MESSAGES/django.po +26 -22
- fleetfinder/locale/it_IT/LC_MESSAGES/django.po +21 -22
- fleetfinder/locale/ja/LC_MESSAGES/django.po +25 -22
- fleetfinder/locale/ko_KR/LC_MESSAGES/django.po +25 -22
- fleetfinder/locale/nl_NL/LC_MESSAGES/django.po +21 -22
- fleetfinder/locale/pl_PL/LC_MESSAGES/django.po +21 -22
- fleetfinder/locale/ru/LC_MESSAGES/django.po +25 -22
- fleetfinder/locale/sk/LC_MESSAGES/django.po +21 -22
- fleetfinder/locale/uk/LC_MESSAGES/django.po +26 -22
- fleetfinder/locale/zh_Hans/LC_MESSAGES/django.po +25 -22
- fleetfinder/providers.py +22 -4
- fleetfinder/tasks.py +279 -110
- fleetfinder/tests/__init__.py +39 -1
- fleetfinder/tests/test_access.py +2 -2
- fleetfinder/tests/test_auth_hooks.py +2 -2
- fleetfinder/tests/test_settings.py +3 -2
- fleetfinder/tests/test_tasks.py +1010 -34
- fleetfinder/tests/test_templatetags.py +2 -4
- fleetfinder/tests/test_user_agent.py +62 -14
- fleetfinder/tests/test_views.py +700 -52
- fleetfinder/views.py +102 -55
- {aa_fleetfinder-2.7.2.dist-info → aa_fleetfinder-3.0.0b1.dist-info}/WHEEL +0 -0
- {aa_fleetfinder-2.7.2.dist-info → aa_fleetfinder-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
fleetfinder/tasks.py
CHANGED
|
@@ -3,11 +3,12 @@ Tasks
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
# Standard Library
|
|
6
|
+
from collections.abc import Iterable
|
|
6
7
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
8
|
from datetime import timedelta
|
|
8
9
|
|
|
9
10
|
# Third Party
|
|
10
|
-
from
|
|
11
|
+
from aiopenapi3 import ContentTypeError
|
|
11
12
|
from celery import shared_task
|
|
12
13
|
|
|
13
14
|
# Django
|
|
@@ -16,10 +17,10 @@ from django.utils import timezone
|
|
|
16
17
|
# Alliance Auth
|
|
17
18
|
from allianceauth.services.hooks import get_extension_logger
|
|
18
19
|
from allianceauth.services.tasks import QueueOnce
|
|
20
|
+
from esi.exceptions import HTTPClientError
|
|
19
21
|
from esi.models import Token
|
|
20
22
|
|
|
21
23
|
# Alliance Auth (External Libs)
|
|
22
|
-
from app_utils.esi import fetch_esi_status
|
|
23
24
|
from app_utils.logging import LoggerAddTag
|
|
24
25
|
|
|
25
26
|
# AA Fleet Finder
|
|
@@ -44,39 +45,67 @@ TASK_DEFAULT_KWARGS = {"time_limit": TASK_TIME_LIMIT, "max_retries": ESI_MAX_RET
|
|
|
44
45
|
|
|
45
46
|
class FleetViewAggregate: # pylint: disable=too-few-public-methods
|
|
46
47
|
"""
|
|
47
|
-
|
|
48
|
+
A helper class to encapsulate fleet data and its aggregate information.
|
|
49
|
+
|
|
50
|
+
This class is used to store and return the fleet view along with its aggregated data.
|
|
48
51
|
"""
|
|
49
52
|
|
|
50
|
-
def __init__(self, fleet, aggregate):
|
|
53
|
+
def __init__(self, fleet: list, aggregate: dict) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Initialize the FleetViewAggregate object.
|
|
56
|
+
|
|
57
|
+
:param fleet: A list of fleet members or fleet-related data.
|
|
58
|
+
:type fleet: list
|
|
59
|
+
:param aggregate: A dictionary containing aggregated data about the fleet.
|
|
60
|
+
:type aggregate: dict
|
|
61
|
+
"""
|
|
62
|
+
|
|
51
63
|
self.fleet = fleet
|
|
52
64
|
self.aggregate = aggregate
|
|
53
65
|
|
|
54
66
|
|
|
55
67
|
@shared_task
|
|
56
|
-
def _send_invitation(
|
|
68
|
+
def _send_invitation(
|
|
69
|
+
character_id: int, fleet_commander_token: Token, fleet_id: int
|
|
70
|
+
) -> None:
|
|
57
71
|
"""
|
|
58
|
-
|
|
72
|
+
Sends a fleet invitation to a character in the EVE Online client.
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
This task uses the ESI API to open the fleet invite window for a specific character,
|
|
75
|
+
assigning them the role of "squad_member" in the specified fleet.
|
|
76
|
+
|
|
77
|
+
:param character_id: The ID of the character to invite to the fleet.
|
|
78
|
+
:type character_id: int
|
|
79
|
+
:param fleet_commander_token: The ESI token of the fleet commander, used for authentication.
|
|
80
|
+
:type fleet_commander_token: str
|
|
81
|
+
:param fleet_id: The ID of the fleet to which the character is being invited.
|
|
82
|
+
:type fleet_id: int
|
|
83
|
+
:return: None
|
|
84
|
+
:rtype: None
|
|
63
85
|
"""
|
|
64
86
|
|
|
87
|
+
# Define the invitation payload with the character ID and role
|
|
65
88
|
invitation = {"character_id": character_id, "role": "squad_member"}
|
|
66
89
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
token=fleet_commander_token
|
|
70
|
-
|
|
71
|
-
).result()
|
|
90
|
+
# Send the invitation using the ESI API
|
|
91
|
+
esi.client.Fleets.PostFleetsFleetIdMembers(
|
|
92
|
+
fleet_id=fleet_id, token=fleet_commander_token, body=invitation
|
|
93
|
+
).result(force_refresh=True)
|
|
72
94
|
|
|
73
95
|
|
|
74
96
|
def _close_esi_fleet(fleet: Fleet, reason: str) -> None:
|
|
75
97
|
"""
|
|
76
|
-
|
|
98
|
+
Close a registered fleet and log the reason for closure.
|
|
99
|
+
|
|
100
|
+
This function deletes the specified fleet from the database and logs
|
|
101
|
+
the closure event with the provided reason.
|
|
77
102
|
|
|
78
|
-
:param fleet:
|
|
79
|
-
:
|
|
103
|
+
:param fleet: The fleet object to be closed.
|
|
104
|
+
:type fleet: Fleet
|
|
105
|
+
:param reason: The reason for closing the fleet.
|
|
106
|
+
:type reason: str
|
|
107
|
+
:return: None
|
|
108
|
+
:rtype: None
|
|
80
109
|
"""
|
|
81
110
|
|
|
82
111
|
logger.info(
|
|
@@ -91,14 +120,19 @@ def _close_esi_fleet(fleet: Fleet, reason: str) -> None:
|
|
|
91
120
|
|
|
92
121
|
def _esi_fleet_error_handling(fleet: Fleet, error_key: str) -> None:
|
|
93
122
|
"""
|
|
94
|
-
ESI
|
|
123
|
+
Handle errors related to ESI (EVE Swagger Interface) fleet operations.
|
|
95
124
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
:
|
|
125
|
+
This function manages error handling for a fleet by checking the error count
|
|
126
|
+
and the time of the last error. If the error count exceeds the maximum allowed
|
|
127
|
+
within the grace period, the fleet is closed. Otherwise, the error count is updated
|
|
128
|
+
and logged.
|
|
129
|
+
|
|
130
|
+
:param fleet: The fleet object associated with the error.
|
|
131
|
+
:type fleet: Fleet
|
|
132
|
+
:param error_key: A key representing the specific error encountered.
|
|
133
|
+
:type error_key: str
|
|
134
|
+
:return: None
|
|
135
|
+
:rtype: None
|
|
102
136
|
"""
|
|
103
137
|
|
|
104
138
|
time_now = timezone.now()
|
|
@@ -114,6 +148,7 @@ def _esi_fleet_error_handling(fleet: Fleet, error_key: str) -> None:
|
|
|
114
148
|
|
|
115
149
|
return
|
|
116
150
|
|
|
151
|
+
# Increment the error count or reset it if the error is new or outside the grace period
|
|
117
152
|
error_count = (
|
|
118
153
|
fleet.esi_error_count + 1
|
|
119
154
|
if fleet.last_esi_error == error_key
|
|
@@ -122,11 +157,13 @@ def _esi_fleet_error_handling(fleet: Fleet, error_key: str) -> None:
|
|
|
122
157
|
else 1
|
|
123
158
|
)
|
|
124
159
|
|
|
160
|
+
# Log the error details
|
|
125
161
|
logger.info(
|
|
126
162
|
f'Fleet "{fleet.name}" of {fleet.fleet_commander} (ESI ID: {fleet.fleet_id}) » '
|
|
127
163
|
f'Error: "{error_key.label}" ({error_count} of {ESI_MAX_ERROR_COUNT}).'
|
|
128
164
|
)
|
|
129
165
|
|
|
166
|
+
# Update the fleet object with the new error details
|
|
130
167
|
fleet.esi_error_count = error_count
|
|
131
168
|
fleet.last_esi_error = error_key
|
|
132
169
|
fleet.last_esi_error_time = time_now
|
|
@@ -134,22 +171,36 @@ def _esi_fleet_error_handling(fleet: Fleet, error_key: str) -> None:
|
|
|
134
171
|
|
|
135
172
|
|
|
136
173
|
@shared_task
|
|
137
|
-
def _get_fleet_aggregate(fleet_infos):
|
|
174
|
+
def _get_fleet_aggregate(fleet_infos: list) -> dict:
|
|
138
175
|
"""
|
|
139
|
-
|
|
176
|
+
Calculate the composition of a fleet based on ship types.
|
|
140
177
|
|
|
141
|
-
|
|
142
|
-
|
|
178
|
+
This function processes a list of fleet members and counts the occurrences
|
|
179
|
+
of each ship type. The result is a dictionary where the keys are ship type names
|
|
180
|
+
and the values are the counts of those ship types.
|
|
181
|
+
|
|
182
|
+
:param fleet_infos: A list of dictionaries containing fleet member information.
|
|
183
|
+
Each dictionary is expected to have a "ship_type_name" key.
|
|
184
|
+
:type fleet_infos: list
|
|
185
|
+
:return: A dictionary with ship type names as keys and their counts as values.
|
|
186
|
+
:rtype: dict
|
|
143
187
|
"""
|
|
144
188
|
|
|
145
189
|
counts = {}
|
|
146
190
|
|
|
191
|
+
logger.debug(f"Fleet infos for aggregation: {fleet_infos}")
|
|
192
|
+
|
|
147
193
|
for member in fleet_infos:
|
|
194
|
+
logger.debug(f"Processing member for aggregation: {member}")
|
|
195
|
+
|
|
196
|
+
# Extract the ship type name from the member information
|
|
148
197
|
type_ = member.get("ship_type_name")
|
|
149
198
|
|
|
199
|
+
# Check if the ship type name is valid and normalize it
|
|
150
200
|
if type_ and isinstance(type_, str) and type_.strip():
|
|
151
201
|
type_ = type_.strip() # Normalize ship type name
|
|
152
202
|
|
|
203
|
+
# Increment the count for the ship type or initialize it
|
|
153
204
|
if type_ in counts:
|
|
154
205
|
counts[type_] += 1
|
|
155
206
|
else:
|
|
@@ -158,14 +209,18 @@ def _get_fleet_aggregate(fleet_infos):
|
|
|
158
209
|
return counts
|
|
159
210
|
|
|
160
211
|
|
|
161
|
-
def _check_for_esi_fleet(fleet: Fleet):
|
|
212
|
+
def _check_for_esi_fleet(fleet: Fleet) -> dict | bool:
|
|
162
213
|
"""
|
|
163
|
-
Check
|
|
214
|
+
Check if a fleet exists and retrieve its ESI (EVE Swagger Interface) data.
|
|
164
215
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
216
|
+
This function verifies the existence of a fleet by checking the required ESI scopes
|
|
217
|
+
and retrieving the fleet data using the fleet commander's token. If the fleet is not found
|
|
218
|
+
or an error occurs, appropriate error handling is performed.
|
|
219
|
+
|
|
220
|
+
:param fleet: The fleet object to check.
|
|
221
|
+
:type fleet: Fleet
|
|
222
|
+
:return: A dictionary containing the fleet data and the ESI token if successful, or False otherwise.
|
|
223
|
+
:rtype: dict | bool
|
|
169
224
|
"""
|
|
170
225
|
|
|
171
226
|
required_scopes = ["esi-fleets.read_fleet.v1"]
|
|
@@ -175,15 +230,28 @@ def _check_for_esi_fleet(fleet: Fleet):
|
|
|
175
230
|
fleet_commander_id = fleet.fleet_commander.character_id
|
|
176
231
|
esi_token = Token.get_token(fleet_commander_id, required_scopes)
|
|
177
232
|
|
|
178
|
-
fleet_from_esi = esi.client.Fleets.
|
|
179
|
-
character_id=fleet_commander_id,
|
|
180
|
-
|
|
181
|
-
).result()
|
|
233
|
+
fleet_from_esi = esi.client.Fleets.GetCharactersCharacterIdFleet(
|
|
234
|
+
character_id=fleet_commander_id, token=esi_token
|
|
235
|
+
).result(force_refresh=True)
|
|
182
236
|
|
|
183
237
|
return {"fleet": fleet_from_esi, "token": esi_token}
|
|
184
|
-
except
|
|
185
|
-
|
|
238
|
+
except ContentTypeError:
|
|
239
|
+
logger.debug(
|
|
240
|
+
f'ESI returned gibberish for fleet "{fleet.name}" of {fleet.fleet_commander} '
|
|
241
|
+
f"(ESI ID: {fleet.fleet_id}), skipping update."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return False
|
|
245
|
+
except HTTPClientError as ex:
|
|
246
|
+
# Handle the case where the fleet is not found
|
|
247
|
+
if ex.status_code == 404:
|
|
248
|
+
_esi_fleet_error_handling(
|
|
249
|
+
error_key=Fleet.EsiError.NOT_IN_FLEET, fleet=fleet
|
|
250
|
+
)
|
|
251
|
+
else: # 400, 401, 402, 403 ?
|
|
252
|
+
_esi_fleet_error_handling(error_key=Fleet.EsiError.NO_FLEET, fleet=fleet)
|
|
186
253
|
except Exception: # pylint: disable=broad-exception-caught
|
|
254
|
+
# Handle any other errors that occur
|
|
187
255
|
_esi_fleet_error_handling(error_key=Fleet.EsiError.NO_FLEET, fleet=fleet)
|
|
188
256
|
|
|
189
257
|
return False
|
|
@@ -191,63 +259,79 @@ def _check_for_esi_fleet(fleet: Fleet):
|
|
|
191
259
|
|
|
192
260
|
def _process_fleet(fleet: Fleet) -> None:
|
|
193
261
|
"""
|
|
194
|
-
|
|
262
|
+
Process a fleet and handle its state based on ESI (EVE Swagger Interface) data.
|
|
263
|
+
|
|
264
|
+
This function retrieves the fleet's ESI data, verifies its consistency, and performs
|
|
265
|
+
appropriate error handling if discrepancies are found. It also checks if the current
|
|
266
|
+
user is the fleet boss.
|
|
195
267
|
|
|
196
|
-
:param fleet:
|
|
268
|
+
:param fleet: The fleet object to process.
|
|
197
269
|
:type fleet: Fleet
|
|
198
270
|
:return: None
|
|
199
271
|
:rtype: None
|
|
200
272
|
"""
|
|
201
273
|
|
|
274
|
+
# Log the start of fleet processing
|
|
202
275
|
logger.info(
|
|
203
276
|
f'Processing information for fleet "{fleet.name}" '
|
|
204
277
|
f"of {fleet.fleet_commander} (ESI ID: {fleet.fleet_id})"
|
|
205
278
|
)
|
|
206
279
|
|
|
207
|
-
# Check if
|
|
280
|
+
# Check if the fleet exists in ESI
|
|
208
281
|
esi_fleet = _check_for_esi_fleet(fleet=fleet)
|
|
209
282
|
|
|
283
|
+
# Exit if the fleet does not exist
|
|
210
284
|
if not esi_fleet:
|
|
211
285
|
return
|
|
212
286
|
|
|
213
|
-
#
|
|
214
|
-
if fleet.fleet_id != esi_fleet["fleet"]
|
|
287
|
+
# Handle the case where fleet IDs do not match, indicating the fleet commander changed fleets
|
|
288
|
+
if fleet.fleet_id != esi_fleet["fleet"].fleet_id:
|
|
215
289
|
_esi_fleet_error_handling(
|
|
216
290
|
fleet=fleet, error_key=Fleet.EsiError.FC_CHANGED_FLEET
|
|
217
291
|
)
|
|
218
292
|
return
|
|
219
293
|
|
|
220
|
-
#
|
|
294
|
+
# Verify if the current user is the fleet boss
|
|
221
295
|
try:
|
|
222
|
-
_ = esi.client.Fleets.
|
|
223
|
-
fleet_id=fleet.fleet_id,
|
|
224
|
-
|
|
225
|
-
).result()
|
|
296
|
+
_ = esi.client.Fleets.GetFleetsFleetIdMembers(
|
|
297
|
+
fleet_id=fleet.fleet_id, token=esi_fleet["token"]
|
|
298
|
+
).result(force_refresh=True)
|
|
226
299
|
except Exception: # pylint: disable=broad-exception-caught
|
|
300
|
+
# Handle the case where the user is not the fleet boss
|
|
227
301
|
_esi_fleet_error_handling(fleet=fleet, error_key=Fleet.EsiError.NOT_FLEETBOSS)
|
|
228
302
|
|
|
229
303
|
|
|
230
304
|
@shared_task
|
|
231
305
|
def send_fleet_invitation(fleet_id: int, character_ids: list) -> None:
|
|
232
306
|
"""
|
|
233
|
-
Send fleet invitations to characters through ESI
|
|
307
|
+
Send fleet invitations to characters through ESI.
|
|
308
|
+
|
|
234
309
|
This task sends fleet invitations to a list of character IDs using the ESI API.
|
|
310
|
+
It retrieves the fleet and the fleet commander's token, then processes the invitations
|
|
311
|
+
concurrently using a thread pool.
|
|
235
312
|
|
|
236
|
-
:param fleet_id: The ID of the fleet to which invitations are sent
|
|
313
|
+
:param fleet_id: The ID of the fleet to which invitations are sent.
|
|
237
314
|
:type fleet_id: int
|
|
238
|
-
:param character_ids: List of character IDs to invite to the fleet
|
|
315
|
+
:param character_ids: List of character IDs to invite to the fleet.
|
|
239
316
|
:type character_ids: list[int]
|
|
240
317
|
:return: None
|
|
241
318
|
:rtype: None
|
|
242
319
|
"""
|
|
243
320
|
|
|
321
|
+
# Define the required ESI scopes for sending fleet invitations
|
|
244
322
|
required_scopes = ["esi-fleets.write_fleet.v1"]
|
|
323
|
+
|
|
324
|
+
# Retrieve the fleet object using the provided fleet ID
|
|
245
325
|
fleet = Fleet.objects.get(fleet_id=fleet_id)
|
|
326
|
+
|
|
327
|
+
# Retrieve the fleet commander's token for authentication
|
|
246
328
|
fleet_commander_token = Token.get_token(
|
|
247
329
|
character_id=fleet.fleet_commander.character_id, scopes=required_scopes
|
|
248
330
|
)
|
|
249
331
|
|
|
332
|
+
# Use a thread pool to send invitations concurrently
|
|
250
333
|
with ThreadPoolExecutor(max_workers=50) as ex:
|
|
334
|
+
# Create a list of futures for sending invitations
|
|
251
335
|
futures = [
|
|
252
336
|
ex.submit(
|
|
253
337
|
_send_invitation,
|
|
@@ -258,6 +342,7 @@ def send_fleet_invitation(fleet_id: int, character_ids: list) -> None:
|
|
|
258
342
|
for character_id in character_ids
|
|
259
343
|
]
|
|
260
344
|
|
|
345
|
+
# Wait for all futures to complete and raise any exceptions that occurred
|
|
261
346
|
for future in as_completed(futures):
|
|
262
347
|
future.result() # This will raise any exceptions that occurred
|
|
263
348
|
|
|
@@ -265,76 +350,157 @@ def send_fleet_invitation(fleet_id: int, character_ids: list) -> None:
|
|
|
265
350
|
@shared_task(**{**TASK_DEFAULT_KWARGS}, **{"base": QueueOnce})
|
|
266
351
|
def check_fleet_adverts() -> None:
|
|
267
352
|
"""
|
|
268
|
-
Check all registered fleets and process them
|
|
353
|
+
Check all registered fleets and process them.
|
|
354
|
+
|
|
355
|
+
This task retrieves all registered fleets from the database and processes each fleet
|
|
356
|
+
individually. It first checks if there are any fleets to process and logs the count.
|
|
357
|
+
If no fleets are found, it logs a message and exits. Before processing, it ensures
|
|
358
|
+
that the ESI (EVE Swagger Interface) service is available. If ESI is offline or
|
|
359
|
+
above the error limit, the task aborts. Each fleet is then processed using the
|
|
360
|
+
`_process_fleet` function.
|
|
269
361
|
|
|
270
362
|
:return: None
|
|
271
363
|
:rtype: None
|
|
272
364
|
"""
|
|
273
365
|
|
|
366
|
+
# Retrieve all registered fleets from the database
|
|
274
367
|
fleets = Fleet.objects.all()
|
|
275
368
|
|
|
369
|
+
# Check if there are any fleets to process
|
|
276
370
|
if not fleets.exists():
|
|
277
371
|
logger.info("No registered fleets found. Nothing to do...")
|
|
278
|
-
|
|
279
372
|
return
|
|
280
373
|
|
|
374
|
+
# Log the number of fleets to be processed
|
|
281
375
|
logger.info(f"Processing {fleets.count()} registered fleets...")
|
|
282
376
|
|
|
283
|
-
#
|
|
284
|
-
if not fetch_esi_status().is_ok:
|
|
285
|
-
logger.warning("ESI doesn't seem to be available at this time. Aborting.")
|
|
286
|
-
|
|
287
|
-
return
|
|
288
|
-
|
|
377
|
+
# Process each fleet individually
|
|
289
378
|
for fleet in fleets:
|
|
290
379
|
_process_fleet(fleet=fleet)
|
|
291
380
|
|
|
292
381
|
|
|
382
|
+
def _fetch_chunk(ids: list) -> list:
|
|
383
|
+
"""
|
|
384
|
+
Fetch names for a list of IDs using the ESI API.
|
|
385
|
+
|
|
386
|
+
This function sends a request to the ESI API to retrieve names for a given list of IDs.
|
|
387
|
+
If the request fails and the list contains more than one ID, the list is split into two
|
|
388
|
+
halves, and the function is called recursively for each half. If the list contains only
|
|
389
|
+
one ID, the ID is dropped, and a warning is logged.
|
|
390
|
+
|
|
391
|
+
:param ids: A list of IDs to fetch names for.
|
|
392
|
+
:type ids: list
|
|
393
|
+
:return: A list of results containing the names corresponding to the provided IDs.
|
|
394
|
+
:rtype: list
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
result = esi.client.Universe.PostUniverseNames(body=ids).result(
|
|
399
|
+
force_refresh=True
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
logger.debug(f"Fetched {len(result)} names for {len(ids)} IDs.")
|
|
403
|
+
logger.debug(f"Result: {result}")
|
|
404
|
+
|
|
405
|
+
return result
|
|
406
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
407
|
+
if len(ids) == 1:
|
|
408
|
+
logger.warning(f"Dropping ID {ids[0]}: failed to fetch name.")
|
|
409
|
+
|
|
410
|
+
return []
|
|
411
|
+
|
|
412
|
+
mid = len(ids) // 2
|
|
413
|
+
|
|
414
|
+
return _fetch_chunk(ids[:mid]) + _fetch_chunk(ids[mid:])
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _make_name_lookup(ids_to_name: Iterable) -> dict:
|
|
418
|
+
"""
|
|
419
|
+
Create a lookup dictionary mapping IDs to names.
|
|
420
|
+
|
|
421
|
+
Build a mapping of id -> name from a sequence that may contain either
|
|
422
|
+
dicts like {'id': ..., 'name': ...} or objects with .id and .name attributes.
|
|
423
|
+
|
|
424
|
+
:param ids_to_name:
|
|
425
|
+
:type ids_to_name:
|
|
426
|
+
:return:
|
|
427
|
+
:rtype:
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
lookup = {}
|
|
431
|
+
|
|
432
|
+
if not ids_to_name:
|
|
433
|
+
return lookup
|
|
434
|
+
|
|
435
|
+
for item in ids_to_name:
|
|
436
|
+
if item is None:
|
|
437
|
+
continue
|
|
438
|
+
|
|
439
|
+
if isinstance(item, dict):
|
|
440
|
+
id_ = item.get("id")
|
|
441
|
+
name = item.get("name")
|
|
442
|
+
else:
|
|
443
|
+
id_ = getattr(item, "id", None)
|
|
444
|
+
name = getattr(item, "name", None)
|
|
445
|
+
|
|
446
|
+
if id_ is not None and name is not None:
|
|
447
|
+
lookup[id_] = name
|
|
448
|
+
|
|
449
|
+
return lookup
|
|
450
|
+
|
|
451
|
+
|
|
293
452
|
@shared_task
|
|
294
|
-
def get_fleet_composition(
|
|
295
|
-
fleet_id: int,
|
|
296
|
-
) -> FleetViewAggregate | None:
|
|
453
|
+
def get_fleet_composition(fleet_id: int) -> FleetViewAggregate | None:
|
|
297
454
|
"""
|
|
298
|
-
|
|
299
|
-
This task retrieves the composition of a fleet using its ESI ID.
|
|
455
|
+
Retrieve the composition of a fleet by its ESI ID.
|
|
300
456
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
457
|
+
This task fetches the fleet composition, including detailed information about its members,
|
|
458
|
+
using the EVE Swagger Interface (ESI). It processes the fleet data, retrieves names for
|
|
459
|
+
associated IDs, and aggregates the fleet composition based on ship types.
|
|
460
|
+
|
|
461
|
+
:param fleet_id: The ESI ID of the fleet to retrieve.
|
|
462
|
+
:type fleet_id: int
|
|
463
|
+
:return: A FleetViewAggregate object containing fleet members and aggregate data, or None if an error occurs.
|
|
304
464
|
:rtype: FleetViewAggregate | None
|
|
305
465
|
"""
|
|
306
466
|
|
|
307
467
|
try:
|
|
468
|
+
# Retrieve the fleet object from the database
|
|
308
469
|
fleet = Fleet.objects.get(fleet_id=fleet_id)
|
|
309
470
|
except Fleet.DoesNotExist as exc:
|
|
471
|
+
# Log and raise an error if the fleet does not exist
|
|
310
472
|
logger.error(f"Fleet with ID {fleet_id} not found")
|
|
311
473
|
|
|
312
474
|
raise Fleet.DoesNotExist(f"Fleet with ID {fleet_id} not found.") from exc
|
|
313
475
|
|
|
476
|
+
# Log the start of fleet composition retrieval
|
|
314
477
|
logger.info(
|
|
315
478
|
f'Getting fleet composition for fleet "{fleet.name}" '
|
|
316
479
|
f"of {fleet.fleet_commander.character_name} (ESI ID: {fleet_id})"
|
|
317
480
|
)
|
|
318
481
|
|
|
319
|
-
required_scopes = ["esi-fleets.read_fleet.v1"]
|
|
320
|
-
|
|
321
482
|
try:
|
|
483
|
+
# Retrieve the fleet commander's token for authentication
|
|
322
484
|
token = Token.get_token(
|
|
323
|
-
character_id=fleet.fleet_commander.character_id,
|
|
485
|
+
character_id=fleet.fleet_commander.character_id,
|
|
486
|
+
scopes=["esi-fleets.read_fleet.v1"],
|
|
324
487
|
)
|
|
325
488
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
489
|
+
# Fetch fleet member information from the ESI API
|
|
490
|
+
fleet_infos = esi.client.Fleets.GetFleetsFleetIdMembers(
|
|
491
|
+
fleet_id=fleet_id, token=token
|
|
492
|
+
).result(force_refresh=True)
|
|
493
|
+
|
|
494
|
+
logger.debug(f"Fleet infos: {fleet_infos}")
|
|
329
495
|
|
|
330
|
-
#
|
|
496
|
+
# Extract all unique IDs (character, solar system, and ship type) for name resolution
|
|
331
497
|
all_ids = {
|
|
332
498
|
item_id
|
|
333
499
|
for member in fleet_infos
|
|
334
500
|
for item_id in [
|
|
335
|
-
member
|
|
336
|
-
member
|
|
337
|
-
member
|
|
501
|
+
member.character_id,
|
|
502
|
+
member.solar_system_id,
|
|
503
|
+
member.ship_type_id,
|
|
338
504
|
]
|
|
339
505
|
}
|
|
340
506
|
|
|
@@ -342,44 +508,47 @@ def get_fleet_composition( # pylint: disable=too-many-locals
|
|
|
342
508
|
f"Found {len(all_ids)} unique IDs to fetch names for in fleet {fleet_id}"
|
|
343
509
|
)
|
|
344
510
|
|
|
345
|
-
# Process IDs in chunks
|
|
346
|
-
# ESI has a limit of 1000 IDs per request, so we will chunk the requests,
|
|
347
|
-
# even though there is a theoretical limit of 768 unique IDs per fleet,
|
|
348
|
-
# so we never should hit the ESI limit.
|
|
349
|
-
# But to be on the safe side, we will chunk the requests in case CCP decides
|
|
350
|
-
# to change the fleet limit in the future, we will use a chunk size of 1000,
|
|
351
|
-
# which is the maximum allowed by ESI for the `post_universe_names` endpoint.
|
|
511
|
+
# Process IDs in chunks to avoid exceeding ESI limits
|
|
352
512
|
chunk_size = 1000
|
|
353
|
-
ids_to_name = []
|
|
354
513
|
all_ids_list = list(all_ids)
|
|
514
|
+
ids_to_name = []
|
|
355
515
|
|
|
356
|
-
for
|
|
357
|
-
chunk = all_ids_list[
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
516
|
+
for start in range(0, len(all_ids_list), chunk_size):
|
|
517
|
+
chunk = all_ids_list[start : start + chunk_size]
|
|
518
|
+
results = _fetch_chunk(chunk)
|
|
519
|
+
ids_to_name.extend(results)
|
|
520
|
+
|
|
521
|
+
logger.debug(f"Fetched names for {len(ids_to_name)} IDs.")
|
|
522
|
+
|
|
523
|
+
# Create a lookup dictionary for resolving names
|
|
524
|
+
name_lookup = _make_name_lookup(ids_to_name)
|
|
525
|
+
|
|
526
|
+
logger.debug(f"Name lookup: {name_lookup}")
|
|
527
|
+
|
|
528
|
+
# Add detailed information to each fleet member
|
|
529
|
+
member_in_fleet = [
|
|
530
|
+
{
|
|
531
|
+
**member.dict(),
|
|
532
|
+
"takes_fleet_warp": member.takes_fleet_warp,
|
|
533
|
+
"character_name": name_lookup[member.character_id],
|
|
534
|
+
"solar_system_name": name_lookup[member.solar_system_id],
|
|
535
|
+
"ship_type_name": name_lookup[member.ship_type_id],
|
|
536
|
+
"is_fleet_boss": member.character_id
|
|
537
|
+
== fleet.fleet_commander.character_id,
|
|
538
|
+
}
|
|
539
|
+
for member in fleet_infos
|
|
540
|
+
]
|
|
377
541
|
|
|
378
|
-
|
|
542
|
+
logger.debug(f"Member in fleet after processing: {member_in_fleet}")
|
|
379
543
|
|
|
380
|
-
|
|
544
|
+
# Return the fleet composition and aggregate data
|
|
545
|
+
return FleetViewAggregate(
|
|
546
|
+
fleet=member_in_fleet,
|
|
547
|
+
aggregate=_get_fleet_aggregate(fleet_infos=member_in_fleet),
|
|
548
|
+
)
|
|
381
549
|
|
|
382
550
|
except Exception as e: # pylint: disable=broad-exception-caught
|
|
551
|
+
# Log and raise an error if fleet composition retrieval fails
|
|
383
552
|
logger.error(f"Failed to get fleet composition for fleet {fleet_id}: {e}")
|
|
384
553
|
|
|
385
554
|
raise RuntimeError(
|
fleetfinder/tests/__init__.py
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Initializing our tests
|
|
3
3
|
"""
|
|
4
|
+
|
|
5
|
+
# Standard Library
|
|
6
|
+
import socket
|
|
7
|
+
|
|
8
|
+
# Django
|
|
9
|
+
from django.test import TestCase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SocketAccessError(Exception):
|
|
13
|
+
"""Error raised when a test script accesses the network"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseTestCase(TestCase):
|
|
17
|
+
"""Variation of Django's TestCase class that prevents any network use.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
|
|
21
|
+
.. code-block:: python
|
|
22
|
+
|
|
23
|
+
class TestMyStuff(BaseTestCase):
|
|
24
|
+
def test_should_do_what_i_need(self): ...
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def setUpClass(cls):
|
|
30
|
+
cls.socket_original = socket.socket
|
|
31
|
+
socket.socket = cls.guard
|
|
32
|
+
return super().setUpClass()
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def tearDownClass(cls):
|
|
36
|
+
socket.socket = cls.socket_original
|
|
37
|
+
return super().tearDownClass()
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def guard(*args, **kwargs):
|
|
41
|
+
raise SocketAccessError("Attempted to access network")
|
fleetfinder/tests/test_access.py
CHANGED
|
@@ -7,14 +7,14 @@ from http import HTTPStatus
|
|
|
7
7
|
|
|
8
8
|
# Django
|
|
9
9
|
from django.contrib.auth.models import Group
|
|
10
|
-
from django.test import TestCase
|
|
11
10
|
from django.urls import reverse
|
|
12
11
|
|
|
13
12
|
# AA Fleet Finder
|
|
13
|
+
from fleetfinder.tests import BaseTestCase
|
|
14
14
|
from fleetfinder.tests.utils import create_fake_user
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class TestAccess(
|
|
17
|
+
class TestAccess(BaseTestCase):
|
|
18
18
|
"""
|
|
19
19
|
Testing module access
|
|
20
20
|
"""
|