python-eveonline 0.1.0__tar.gz → 0.2.1__tar.gz

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.
Files changed (23) hide show
  1. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/PKG-INFO +1 -1
  2. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/pyproject.toml +1 -1
  3. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/src/eveonline/__init__.py +1 -1
  4. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/src/eveonline/client.py +121 -0
  5. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/src/eveonline/const.py +8 -0
  6. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/src/eveonline/models.py +59 -0
  7. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/src/python_eveonline.egg-info/PKG-INFO +1 -1
  8. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/tests/test_client_authenticated.py +273 -0
  9. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/LICENSE +0 -0
  10. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/README.md +0 -0
  11. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/setup.cfg +0 -0
  12. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/src/eveonline/auth.py +0 -0
  13. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/src/eveonline/exceptions.py +0 -0
  14. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/src/eveonline/py.typed +0 -0
  15. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/src/python_eveonline.egg-info/SOURCES.txt +0 -0
  16. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/src/python_eveonline.egg-info/dependency_links.txt +0 -0
  17. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/src/python_eveonline.egg-info/requires.txt +0 -0
  18. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/src/python_eveonline.egg-info/top_level.txt +0 -0
  19. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/tests/test_auth.py +0 -0
  20. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/tests/test_client_public.py +0 -0
  21. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/tests/test_const.py +0 -0
  22. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/tests/test_exceptions.py +0 -0
  23. {python_eveonline-0.1.0 → python_eveonline-0.2.1}/tests/test_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-eveonline
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Async Python client for the Eve Online ESI API
5
5
  Author: Ronald van der Meer
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-eveonline"
7
- version = "0.1.0"
7
+ version = "0.2.1"
8
8
  description = "Async Python client for the Eve Online ESI API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -9,7 +9,7 @@ from .exceptions import (
9
9
  EveOnlineRateLimitError,
10
10
  )
11
11
 
12
- __version__ = "0.1.0"
12
+ __version__ = "0.2.1"
13
13
 
14
14
  __all__ = [
15
15
  "AbstractAuth",
@@ -22,7 +22,12 @@ from .models import (
22
22
  CharacterPortrait,
23
23
  CharacterPublicInfo,
24
24
  CharacterShip,
25
+ CharacterSkillsSummary,
25
26
  CorporationPublicInfo,
27
+ IndustryJob,
28
+ JumpFatigue,
29
+ MailLabelsSummary,
30
+ MarketOrder,
26
31
  ServerStatus,
27
32
  SkillQueueEntry,
28
33
  UniverseName,
@@ -362,3 +367,119 @@ class EveOnlineClient:
362
367
  )
363
368
  for entry in data
364
369
  ]
370
+
371
+ async def async_get_skills(self, character_id: int) -> CharacterSkillsSummary:
372
+ """Get a character's total and unallocated skill points.
373
+
374
+ Requires scope: ``esi-skills.read_skills.v1``
375
+
376
+ Args:
377
+ character_id: The Eve Online character ID.
378
+
379
+ Returns:
380
+ CharacterSkillsSummary with total SP and unallocated SP.
381
+ """
382
+ data = await self._request("GET", f"characters/{character_id}/skills/", authenticated=True)
383
+ return CharacterSkillsSummary(
384
+ total_sp=data["total_sp"],
385
+ unallocated_sp=data.get("unallocated_sp", 0),
386
+ )
387
+
388
+ async def async_get_mail_labels(self, character_id: int) -> MailLabelsSummary:
389
+ """Get a character's mail labels with unread counts.
390
+
391
+ Requires scope: ``esi-mail.read_mail.v1``
392
+
393
+ Args:
394
+ character_id: The Eve Online character ID.
395
+
396
+ Returns:
397
+ MailLabelsSummary with total unread count.
398
+ """
399
+ data = await self._request("GET", f"characters/{character_id}/mail/labels/", authenticated=True)
400
+ return MailLabelsSummary(
401
+ total_unread_count=data.get("total_unread_count", 0),
402
+ )
403
+
404
+ async def async_get_industry_jobs(self, character_id: int, *, include_completed: bool = False) -> list[IndustryJob]:
405
+ """Get a character's industry jobs.
406
+
407
+ Requires scope: ``esi-industry.read_character_jobs.v1``
408
+
409
+ Args:
410
+ character_id: The Eve Online character ID.
411
+ include_completed: Whether to include completed jobs.
412
+
413
+ Returns:
414
+ List of IndustryJob entries.
415
+ """
416
+ params: dict[str, str] = {}
417
+ if include_completed:
418
+ params["include_completed"] = "true"
419
+ data = await self._request(
420
+ "GET", f"characters/{character_id}/industry/jobs/", authenticated=True, params=params
421
+ )
422
+ return [
423
+ IndustryJob(
424
+ job_id=entry["job_id"],
425
+ activity_id=entry["activity_id"],
426
+ status=entry["status"],
427
+ start_date=datetime.fromisoformat(entry["start_date"].replace("Z", "+00:00")),
428
+ end_date=datetime.fromisoformat(entry["end_date"].replace("Z", "+00:00")),
429
+ blueprint_type_id=entry["blueprint_type_id"],
430
+ output_location_id=entry["output_location_id"],
431
+ runs=entry["runs"],
432
+ product_type_id=entry.get("product_type_id"),
433
+ facility_id=entry.get("facility_id"),
434
+ cost=entry.get("cost"),
435
+ )
436
+ for entry in data
437
+ ]
438
+
439
+ async def async_get_market_orders(self, character_id: int) -> list[MarketOrder]:
440
+ """Get a character's open market orders.
441
+
442
+ Requires scope: ``esi-markets.read_character_orders.v1``
443
+
444
+ Args:
445
+ character_id: The Eve Online character ID.
446
+
447
+ Returns:
448
+ List of MarketOrder entries.
449
+ """
450
+ data = await self._request("GET", f"characters/{character_id}/orders/", authenticated=True)
451
+ return [
452
+ MarketOrder(
453
+ order_id=entry["order_id"],
454
+ type_id=entry["type_id"],
455
+ is_buy_order=entry.get("is_buy_order", False),
456
+ price=entry["price"],
457
+ volume_remain=entry["volume_remain"],
458
+ volume_total=entry["volume_total"],
459
+ location_id=entry["location_id"],
460
+ region_id=entry["region_id"],
461
+ issued=datetime.fromisoformat(entry["issued"].replace("Z", "+00:00")),
462
+ duration=entry["duration"],
463
+ range=entry["range"],
464
+ min_volume=entry.get("min_volume"),
465
+ )
466
+ for entry in data
467
+ ]
468
+
469
+ async def async_get_jump_fatigue(self, character_id: int) -> JumpFatigue:
470
+ """Get a character's jump fatigue information.
471
+
472
+ Requires scope: ``esi-characters.read_fatigue.v1``
473
+
474
+ Args:
475
+ character_id: The Eve Online character ID.
476
+
477
+ Returns:
478
+ JumpFatigue with expiry date and last jump info.
479
+ """
480
+ data = await self._request("GET", f"characters/{character_id}/fatigue/", authenticated=True)
481
+ return JumpFatigue(
482
+ jump_fatigue_expire_date=self._parse_datetime(data.get("jump_fatigue_expire_date")),
483
+ last_jump_date=self._parse_datetime(data.get("last_jump_date")),
484
+ last_update_date=self._parse_datetime(data.get("last_update_date")),
485
+ )
@@ -22,6 +22,10 @@ SCOPE_READ_KILLMAILS: Final = "esi-killmails.read_killmails.v1"
22
22
  SCOPE_READ_CLONES: Final = "esi-clones.read_clones.v1"
23
23
  SCOPE_READ_IMPLANTS: Final = "esi-clones.read_implants.v1"
24
24
  SCOPE_READ_NOTIFICATIONS: Final = "esi-characters.read_notifications.v1"
25
+ SCOPE_READ_FATIGUE: Final = "esi-characters.read_fatigue.v1"
26
+ SCOPE_READ_MAIL: Final = "esi-mail.read_mail.v1"
27
+ SCOPE_READ_INDUSTRY_JOBS: Final = "esi-industry.read_character_jobs.v1"
28
+ SCOPE_READ_MARKET_ORDERS: Final = "esi-markets.read_character_orders.v1"
25
29
 
26
30
  # Default scopes for a typical Home Assistant integration
27
31
  DEFAULT_SCOPES: Final = [
@@ -31,4 +35,8 @@ DEFAULT_SCOPES: Final = [
31
35
  SCOPE_READ_WALLET,
32
36
  SCOPE_READ_SKILLS,
33
37
  SCOPE_READ_SKILLQUEUE,
38
+ SCOPE_READ_FATIGUE,
39
+ SCOPE_READ_MAIL,
40
+ SCOPE_READ_INDUSTRY_JOBS,
41
+ SCOPE_READ_MARKET_ORDERS,
34
42
  ]
@@ -117,3 +117,62 @@ class UniverseName:
117
117
  id: int
118
118
  name: str
119
119
  category: str
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class CharacterSkillsSummary:
124
+ """Character skills summary (requires auth)."""
125
+
126
+ total_sp: int
127
+ unallocated_sp: int
128
+
129
+
130
+ @dataclass(frozen=True)
131
+ class MailLabelsSummary:
132
+ """Mail labels with unread count (requires auth)."""
133
+
134
+ total_unread_count: int
135
+
136
+
137
+ @dataclass(frozen=True)
138
+ class IndustryJob:
139
+ """An active industry job (requires auth)."""
140
+
141
+ job_id: int
142
+ activity_id: int
143
+ status: str
144
+ start_date: datetime
145
+ end_date: datetime
146
+ blueprint_type_id: int
147
+ output_location_id: int
148
+ runs: int
149
+ product_type_id: int | None = None
150
+ facility_id: int | None = None
151
+ cost: float | None = None
152
+
153
+
154
+ @dataclass(frozen=True)
155
+ class MarketOrder:
156
+ """A character's market order (requires auth)."""
157
+
158
+ order_id: int
159
+ type_id: int
160
+ is_buy_order: bool
161
+ price: float
162
+ volume_remain: int
163
+ volume_total: int
164
+ location_id: int
165
+ region_id: int
166
+ issued: datetime
167
+ duration: int
168
+ range: str
169
+ min_volume: int | None = None
170
+
171
+
172
+ @dataclass(frozen=True)
173
+ class JumpFatigue:
174
+ """Character jump fatigue information (requires auth)."""
175
+
176
+ jump_fatigue_expire_date: datetime | None = None
177
+ last_jump_date: datetime | None = None
178
+ last_update_date: datetime | None = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-eveonline
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Async Python client for the Eve Online ESI API
5
5
  Author: Ronald van der Meer
6
6
  License-Expression: MIT
@@ -15,6 +15,11 @@ from eveonline.models import (
15
15
  CharacterLocation,
16
16
  CharacterOnlineStatus,
17
17
  CharacterShip,
18
+ CharacterSkillsSummary,
19
+ IndustryJob,
20
+ JumpFatigue,
21
+ MailLabelsSummary,
22
+ MarketOrder,
18
23
  SkillQueueEntry,
19
24
  WalletBalance,
20
25
  )
@@ -70,6 +75,46 @@ class TestAuthRequired:
70
75
  with pytest.raises(EveOnlineAuthenticationError):
71
76
  await client.async_get_skill_queue(CHARACTER_ID)
72
77
 
78
+ @pytest.mark.asyncio
79
+ async def test_skills_without_auth_raises(self):
80
+ """Calling skills endpoint without auth raises error."""
81
+ async with aiohttp.ClientSession() as session:
82
+ client = EveOnlineClient(session=session)
83
+ with pytest.raises(EveOnlineAuthenticationError):
84
+ await client.async_get_skills(CHARACTER_ID)
85
+
86
+ @pytest.mark.asyncio
87
+ async def test_mail_labels_without_auth_raises(self):
88
+ """Calling mail labels endpoint without auth raises error."""
89
+ async with aiohttp.ClientSession() as session:
90
+ client = EveOnlineClient(session=session)
91
+ with pytest.raises(EveOnlineAuthenticationError):
92
+ await client.async_get_mail_labels(CHARACTER_ID)
93
+
94
+ @pytest.mark.asyncio
95
+ async def test_industry_jobs_without_auth_raises(self):
96
+ """Calling industry jobs endpoint without auth raises error."""
97
+ async with aiohttp.ClientSession() as session:
98
+ client = EveOnlineClient(session=session)
99
+ with pytest.raises(EveOnlineAuthenticationError):
100
+ await client.async_get_industry_jobs(CHARACTER_ID)
101
+
102
+ @pytest.mark.asyncio
103
+ async def test_market_orders_without_auth_raises(self):
104
+ """Calling market orders endpoint without auth raises error."""
105
+ async with aiohttp.ClientSession() as session:
106
+ client = EveOnlineClient(session=session)
107
+ with pytest.raises(EveOnlineAuthenticationError):
108
+ await client.async_get_market_orders(CHARACTER_ID)
109
+
110
+ @pytest.mark.asyncio
111
+ async def test_jump_fatigue_without_auth_raises(self):
112
+ """Calling jump fatigue endpoint without auth raises error."""
113
+ async with aiohttp.ClientSession() as session:
114
+ client = EveOnlineClient(session=session)
115
+ with pytest.raises(EveOnlineAuthenticationError):
116
+ await client.async_get_jump_fatigue(CHARACTER_ID)
117
+
73
118
 
74
119
  class TestCharacterOnline:
75
120
  """Test GET /characters/{character_id}/online/ endpoint."""
@@ -334,3 +379,231 @@ class TestSkillQueue:
334
379
  assert queue[0].start_date is None
335
380
  assert queue[0].finish_date is None
336
381
  assert queue[0].training_start_sp is None
382
+
383
+
384
+ class TestSkills:
385
+ """Test GET /characters/{character_id}/skills/ endpoint."""
386
+
387
+ @pytest.mark.asyncio
388
+ async def test_get_skills_success(self, character_skills_data):
389
+ """Successful skills summary fetch."""
390
+ with aioresponses() as mocked:
391
+ mocked.get(
392
+ f"{ESI_BASE_URL}/characters/{CHARACTER_ID}/skills/?datasource=tranquility",
393
+ payload=character_skills_data,
394
+ )
395
+ async with aiohttp.ClientSession() as session:
396
+ auth = MockAuth(session)
397
+ client = EveOnlineClient(auth=auth)
398
+ skills = await client.async_get_skills(CHARACTER_ID)
399
+
400
+ assert isinstance(skills, CharacterSkillsSummary)
401
+ assert skills.total_sp == 48500000
402
+ assert skills.unallocated_sp == 150000
403
+
404
+ @pytest.mark.asyncio
405
+ async def test_get_skills_no_unallocated(self):
406
+ """Skills when unallocated_sp is missing defaults to 0."""
407
+ data = {"total_sp": 10000000, "skills": []}
408
+ with aioresponses() as mocked:
409
+ mocked.get(
410
+ f"{ESI_BASE_URL}/characters/{CHARACTER_ID}/skills/?datasource=tranquility",
411
+ payload=data,
412
+ )
413
+ async with aiohttp.ClientSession() as session:
414
+ auth = MockAuth(session)
415
+ client = EveOnlineClient(auth=auth)
416
+ skills = await client.async_get_skills(CHARACTER_ID)
417
+
418
+ assert skills.total_sp == 10000000
419
+ assert skills.unallocated_sp == 0
420
+
421
+
422
+ class TestMailLabels:
423
+ """Test GET /characters/{character_id}/mail/labels/ endpoint."""
424
+
425
+ @pytest.mark.asyncio
426
+ async def test_get_mail_labels_success(self, mail_labels_data):
427
+ """Successful mail labels fetch."""
428
+ with aioresponses() as mocked:
429
+ mocked.get(
430
+ f"{ESI_BASE_URL}/characters/{CHARACTER_ID}/mail/labels/?datasource=tranquility",
431
+ payload=mail_labels_data,
432
+ )
433
+ async with aiohttp.ClientSession() as session:
434
+ auth = MockAuth(session)
435
+ client = EveOnlineClient(auth=auth)
436
+ labels = await client.async_get_mail_labels(CHARACTER_ID)
437
+
438
+ assert isinstance(labels, MailLabelsSummary)
439
+ assert labels.total_unread_count == 7
440
+
441
+ @pytest.mark.asyncio
442
+ async def test_get_mail_labels_no_unread(self):
443
+ """Mail labels when no unread mail."""
444
+ data = {"total_unread_count": 0, "labels": []}
445
+ with aioresponses() as mocked:
446
+ mocked.get(
447
+ f"{ESI_BASE_URL}/characters/{CHARACTER_ID}/mail/labels/?datasource=tranquility",
448
+ payload=data,
449
+ )
450
+ async with aiohttp.ClientSession() as session:
451
+ auth = MockAuth(session)
452
+ client = EveOnlineClient(auth=auth)
453
+ labels = await client.async_get_mail_labels(CHARACTER_ID)
454
+
455
+ assert labels.total_unread_count == 0
456
+
457
+
458
+ class TestIndustryJobs:
459
+ """Test GET /characters/{character_id}/industry/jobs/ endpoint."""
460
+
461
+ @pytest.mark.asyncio
462
+ async def test_get_industry_jobs_success(self, industry_jobs_data):
463
+ """Successful industry jobs fetch."""
464
+ with aioresponses() as mocked:
465
+ mocked.get(
466
+ f"{ESI_BASE_URL}/characters/{CHARACTER_ID}/industry/jobs/?datasource=tranquility",
467
+ payload=industry_jobs_data,
468
+ )
469
+ async with aiohttp.ClientSession() as session:
470
+ auth = MockAuth(session)
471
+ client = EveOnlineClient(auth=auth)
472
+ jobs = await client.async_get_industry_jobs(CHARACTER_ID)
473
+
474
+ assert len(jobs) == 2
475
+ assert all(isinstance(j, IndustryJob) for j in jobs)
476
+
477
+ first = jobs[0]
478
+ assert first.job_id == 12345
479
+ assert first.activity_id == 1
480
+ assert first.status == "active"
481
+ assert first.blueprint_type_id == 1137
482
+ assert first.runs == 10
483
+ assert first.cost == 1500.50
484
+ assert first.start_date == datetime(2026, 3, 25, 10, 0, tzinfo=UTC)
485
+ assert first.end_date == datetime(2026, 3, 27, 10, 0, tzinfo=UTC)
486
+
487
+ second = jobs[1]
488
+ assert second.product_type_id is None
489
+ assert second.cost is None
490
+
491
+ @pytest.mark.asyncio
492
+ async def test_get_industry_jobs_include_completed(self):
493
+ """Industry jobs with include_completed parameter."""
494
+ with aioresponses() as mocked:
495
+ mocked.get(
496
+ f"{ESI_BASE_URL}/characters/{CHARACTER_ID}/industry/jobs/"
497
+ "?datasource=tranquility&include_completed=true",
498
+ payload=[],
499
+ )
500
+ async with aiohttp.ClientSession() as session:
501
+ auth = MockAuth(session)
502
+ client = EveOnlineClient(auth=auth)
503
+ jobs = await client.async_get_industry_jobs(CHARACTER_ID, include_completed=True)
504
+
505
+ assert jobs == []
506
+
507
+ @pytest.mark.asyncio
508
+ async def test_get_industry_jobs_empty(self):
509
+ """No active industry jobs returns empty list."""
510
+ with aioresponses() as mocked:
511
+ mocked.get(
512
+ f"{ESI_BASE_URL}/characters/{CHARACTER_ID}/industry/jobs/?datasource=tranquility",
513
+ payload=[],
514
+ )
515
+ async with aiohttp.ClientSession() as session:
516
+ auth = MockAuth(session)
517
+ client = EveOnlineClient(auth=auth)
518
+ jobs = await client.async_get_industry_jobs(CHARACTER_ID)
519
+
520
+ assert jobs == []
521
+
522
+
523
+ class TestMarketOrders:
524
+ """Test GET /characters/{character_id}/orders/ endpoint."""
525
+
526
+ @pytest.mark.asyncio
527
+ async def test_get_market_orders_success(self, market_orders_data):
528
+ """Successful market orders fetch."""
529
+ with aioresponses() as mocked:
530
+ mocked.get(
531
+ f"{ESI_BASE_URL}/characters/{CHARACTER_ID}/orders/?datasource=tranquility",
532
+ payload=market_orders_data,
533
+ )
534
+ async with aiohttp.ClientSession() as session:
535
+ auth = MockAuth(session)
536
+ client = EveOnlineClient(auth=auth)
537
+ orders = await client.async_get_market_orders(CHARACTER_ID)
538
+
539
+ assert len(orders) == 2
540
+ assert all(isinstance(o, MarketOrder) for o in orders)
541
+
542
+ sell = orders[0]
543
+ assert sell.order_id == 9876543
544
+ assert sell.is_buy_order is False
545
+ assert sell.price == 5.50
546
+ assert sell.volume_remain == 100000
547
+ assert sell.volume_total == 500000
548
+ assert sell.duration == 90
549
+ assert sell.range == "region"
550
+ assert sell.issued == datetime(2026, 3, 20, 12, 0, tzinfo=UTC)
551
+
552
+ buy = orders[1]
553
+ assert buy.is_buy_order is True
554
+ assert buy.price == 10.00
555
+ assert buy.min_volume is None
556
+
557
+ @pytest.mark.asyncio
558
+ async def test_get_market_orders_empty(self):
559
+ """No active market orders returns empty list."""
560
+ with aioresponses() as mocked:
561
+ mocked.get(
562
+ f"{ESI_BASE_URL}/characters/{CHARACTER_ID}/orders/?datasource=tranquility",
563
+ payload=[],
564
+ )
565
+ async with aiohttp.ClientSession() as session:
566
+ auth = MockAuth(session)
567
+ client = EveOnlineClient(auth=auth)
568
+ orders = await client.async_get_market_orders(CHARACTER_ID)
569
+
570
+ assert orders == []
571
+
572
+
573
+ class TestJumpFatigue:
574
+ """Test GET /characters/{character_id}/fatigue/ endpoint."""
575
+
576
+ @pytest.mark.asyncio
577
+ async def test_get_jump_fatigue_success(self, jump_fatigue_data):
578
+ """Successful jump fatigue fetch."""
579
+ with aioresponses() as mocked:
580
+ mocked.get(
581
+ f"{ESI_BASE_URL}/characters/{CHARACTER_ID}/fatigue/?datasource=tranquility",
582
+ payload=jump_fatigue_data,
583
+ )
584
+ async with aiohttp.ClientSession() as session:
585
+ auth = MockAuth(session)
586
+ client = EveOnlineClient(auth=auth)
587
+ fatigue = await client.async_get_jump_fatigue(CHARACTER_ID)
588
+
589
+ assert isinstance(fatigue, JumpFatigue)
590
+ assert fatigue.jump_fatigue_expire_date == datetime(2026, 3, 27, 15, 30, tzinfo=UTC)
591
+ assert fatigue.last_jump_date == datetime(2026, 3, 26, 12, 0, tzinfo=UTC)
592
+ assert fatigue.last_update_date == datetime(2026, 3, 26, 12, 0, tzinfo=UTC)
593
+
594
+ @pytest.mark.asyncio
595
+ async def test_get_jump_fatigue_no_fatigue(self):
596
+ """Character with no jump fatigue (all fields empty)."""
597
+ with aioresponses() as mocked:
598
+ mocked.get(
599
+ f"{ESI_BASE_URL}/characters/{CHARACTER_ID}/fatigue/?datasource=tranquility",
600
+ payload={},
601
+ )
602
+ async with aiohttp.ClientSession() as session:
603
+ auth = MockAuth(session)
604
+ client = EveOnlineClient(auth=auth)
605
+ fatigue = await client.async_get_jump_fatigue(CHARACTER_ID)
606
+
607
+ assert fatigue.jump_fatigue_expire_date is None
608
+ assert fatigue.last_jump_date is None
609
+ assert fatigue.last_update_date is None