aa-fleetfinder 2.7.1__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.

Files changed (35) hide show
  1. {aa_fleetfinder-2.7.1.dist-info → aa_fleetfinder-3.0.0b1.dist-info}/METADATA +5 -14
  2. {aa_fleetfinder-2.7.1.dist-info → aa_fleetfinder-3.0.0b1.dist-info}/RECORD +35 -35
  3. fleetfinder/__init__.py +5 -3
  4. fleetfinder/apps.py +5 -4
  5. fleetfinder/locale/cs_CZ/LC_MESSAGES/django.po +21 -22
  6. fleetfinder/locale/de/LC_MESSAGES/django.mo +0 -0
  7. fleetfinder/locale/de/LC_MESSAGES/django.po +27 -24
  8. fleetfinder/locale/django.pot +22 -23
  9. fleetfinder/locale/es/LC_MESSAGES/django.po +25 -22
  10. fleetfinder/locale/fr_FR/LC_MESSAGES/django.mo +0 -0
  11. fleetfinder/locale/fr_FR/LC_MESSAGES/django.po +67 -59
  12. fleetfinder/locale/it_IT/LC_MESSAGES/django.po +21 -22
  13. fleetfinder/locale/ja/LC_MESSAGES/django.po +25 -22
  14. fleetfinder/locale/ko_KR/LC_MESSAGES/django.po +25 -22
  15. fleetfinder/locale/nl_NL/LC_MESSAGES/django.po +21 -22
  16. fleetfinder/locale/pl_PL/LC_MESSAGES/django.po +21 -22
  17. fleetfinder/locale/ru/LC_MESSAGES/django.po +25 -22
  18. fleetfinder/locale/sk/LC_MESSAGES/django.po +21 -22
  19. fleetfinder/locale/uk/LC_MESSAGES/django.mo +0 -0
  20. fleetfinder/locale/uk/LC_MESSAGES/django.po +50 -56
  21. fleetfinder/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  22. fleetfinder/locale/zh_Hans/LC_MESSAGES/django.po +28 -25
  23. fleetfinder/providers.py +22 -4
  24. fleetfinder/tasks.py +279 -110
  25. fleetfinder/tests/__init__.py +39 -1
  26. fleetfinder/tests/test_access.py +2 -2
  27. fleetfinder/tests/test_auth_hooks.py +2 -2
  28. fleetfinder/tests/test_settings.py +3 -2
  29. fleetfinder/tests/test_tasks.py +1010 -34
  30. fleetfinder/tests/test_templatetags.py +2 -4
  31. fleetfinder/tests/test_user_agent.py +62 -14
  32. fleetfinder/tests/test_views.py +700 -52
  33. fleetfinder/views.py +102 -55
  34. {aa_fleetfinder-2.7.1.dist-info → aa_fleetfinder-3.0.0b1.dist-info}/WHEEL +0 -0
  35. {aa_fleetfinder-2.7.1.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 bravado.exception import HTTPNotFound
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
- Helper class
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(character_id, fleet_commander_token, fleet_id):
68
+ def _send_invitation(
69
+ character_id: int, fleet_commander_token: Token, fleet_id: int
70
+ ) -> None:
57
71
  """
58
- Open the fleet invite window in the eve client
72
+ Sends a fleet invitation to a character in the EVE Online client.
59
73
 
60
- :param character_id:
61
- :param fleet_commander_token:
62
- :param fleet_id:
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
- esi.client.Fleets.post_fleets_fleet_id_members(
68
- fleet_id=fleet_id,
69
- token=fleet_commander_token.valid_access_token(),
70
- invitation=invitation,
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
- Closing registered fleet
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
- :param reason:
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 error handling
123
+ Handle errors related to ESI (EVE Swagger Interface) fleet operations.
95
124
 
96
- :param fleet:
97
- :type fleet:
98
- :param error_key:
99
- :type error_key:
100
- :return:
101
- :rtype:
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
- Getting numbers for fleet composition
176
+ Calculate the composition of a fleet based on ship types.
140
177
 
141
- :param fleet_infos:
142
- :return:
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 for required ESI scopes
214
+ Check if a fleet exists and retrieve its ESI (EVE Swagger Interface) data.
164
215
 
165
- :param fleet:
166
- :type fleet:
167
- :return:
168
- :rtype:
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.get_characters_character_id_fleet(
179
- character_id=fleet_commander_id,
180
- token=esi_token.valid_access_token(),
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 HTTPNotFound:
185
- _esi_fleet_error_handling(error_key=Fleet.EsiError.NOT_IN_FLEET, fleet=fleet)
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
- Processing a fleet
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: Fleet object to process
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 there is a fleet
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
- # Fleet IDs don't match, FC changed fleets
214
- if fleet.fleet_id != esi_fleet["fleet"]["fleet_id"]:
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
- # Check if we deal with the fleet boss here
294
+ # Verify if the current user is the fleet boss
221
295
  try:
222
- _ = esi.client.Fleets.get_fleets_fleet_id_members(
223
- fleet_id=fleet.fleet_id,
224
- token=esi_fleet["token"].valid_access_token(),
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
- # Abort if ESI seems to be offline or above the error limit
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( # pylint: disable=too-many-locals
295
- fleet_id: int,
296
- ) -> FleetViewAggregate | None:
453
+ def get_fleet_composition(fleet_id: int) -> FleetViewAggregate | None:
297
454
  """
298
- Get the composition of a fleet by its ID
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
- :param fleet_id: The ESI ID of the fleet to retrieve
302
- :type fleet_id: int
303
- :return: FleetViewAggregate containing fleet members and aggregate data
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, scopes=required_scopes
485
+ character_id=fleet.fleet_commander.character_id,
486
+ scopes=["esi-fleets.read_fleet.v1"],
324
487
  )
325
488
 
326
- fleet_infos = esi.client.Fleets.get_fleets_fleet_id_members(
327
- fleet_id=fleet_id, token=token.valid_access_token()
328
- ).result()
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
- # Get all unique IDs and fetch names in one call
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["character_id"],
336
- member["solar_system_id"],
337
- member["ship_type_id"],
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 of 1000 to avoid ESI limits.
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 i in range(0, len(all_ids_list), chunk_size):
357
- chunk = all_ids_list[i : i + chunk_size]
358
- chunk_result = esi.client.Universe.post_universe_names(ids=chunk).result()
359
-
360
- ids_to_name.extend(chunk_result)
361
-
362
- # Create a lookup dictionary for names
363
- name_lookup = {item["id"]: item["name"] for item in ids_to_name}
364
-
365
- # Add additional information to each fleet member
366
- for member in fleet_infos:
367
- is_fleet_boss = member["character_id"] == fleet.fleet_commander.character_id
368
-
369
- member.update(
370
- {
371
- "character_name": name_lookup[member["character_id"]],
372
- "solar_system_name": name_lookup[member["solar_system_id"]],
373
- "ship_type_name": name_lookup[member["ship_type_id"]],
374
- "is_fleet_boss": is_fleet_boss,
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
- aggregate = _get_fleet_aggregate(fleet_infos=fleet_infos)
542
+ logger.debug(f"Member in fleet after processing: {member_in_fleet}")
379
543
 
380
- return FleetViewAggregate(fleet=fleet_infos, aggregate=aggregate)
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(
@@ -1,3 +1,41 @@
1
1
  """
2
- Initialize the tests
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")
@@ -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(TestCase):
17
+ class TestAccess(BaseTestCase):
18
18
  """
19
19
  Testing module access
20
20
  """