python-eveonline 0.2.2__tar.gz → 0.3.0__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.
- python_eveonline-0.3.0/PKG-INFO +84 -0
- python_eveonline-0.3.0/README.md +52 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/pyproject.toml +13 -1
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/src/eveonline/client.py +291 -18
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/src/eveonline/const.py +3 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/src/eveonline/models.py +152 -0
- python_eveonline-0.3.0/src/python_eveonline.egg-info/PKG-INFO +84 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/src/python_eveonline.egg-info/SOURCES.txt +1 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/tests/test_client_authenticated.py +406 -0
- python_eveonline-0.3.0/tests/test_etag_caching.py +525 -0
- python_eveonline-0.2.2/PKG-INFO +0 -144
- python_eveonline-0.2.2/README.md +0 -112
- python_eveonline-0.2.2/src/python_eveonline.egg-info/PKG-INFO +0 -144
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/LICENSE +0 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/setup.cfg +0 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/src/eveonline/__init__.py +0 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/src/eveonline/auth.py +0 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/src/eveonline/exceptions.py +0 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/src/eveonline/py.typed +0 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/src/python_eveonline.egg-info/dependency_links.txt +0 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/src/python_eveonline.egg-info/requires.txt +0 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/src/python_eveonline.egg-info/top_level.txt +0 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/tests/test_auth.py +0 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/tests/test_client_public.py +0 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/tests/test_const.py +0 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/tests/test_exceptions.py +0 -0
- {python_eveonline-0.2.2 → python_eveonline-0.3.0}/tests/test_models.py +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-eveonline
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Async Python client for the Eve Online ESI API
|
|
5
|
+
Author: Ronald van der Meer
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ronaldvdmeer/python-eveonline
|
|
8
|
+
Project-URL: Repository, https://github.com/ronaldvdmeer/python-eveonline
|
|
9
|
+
Project-URL: Issues, https://github.com/ronaldvdmeer/python-eveonline/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Games/Entertainment
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Classifier: Framework :: AsyncIO
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
27
|
+
Requires-Dist: aioresponses>=0.7; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
29
|
+
Requires-Dist: pylint>=3.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.3; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# python-eveonline
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/python-eveonline/)
|
|
36
|
+
[](https://pypi.org/project/python-eveonline/)
|
|
37
|
+
[](https://github.com/ronaldvdmeer/python-eveonline/blob/main/LICENSE)
|
|
38
|
+
[](https://github.com/ronaldvdmeer/python-eveonline/releases)
|
|
39
|
+
|
|
40
|
+
Async Python client library for the [Eve Online ESI API](https://esi.evetech.net/ui/).
|
|
41
|
+
|
|
42
|
+
Built for use with [Home Assistant](https://www.home-assistant.io/) but can be used standalone in any async Python project.
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
- **Fully async** — built on [aiohttp](https://docs.aiohttp.org/)
|
|
47
|
+
- **Typed models** — all API responses are frozen dataclasses with full type annotations
|
|
48
|
+
- **15 endpoints** — public (server, character, corporation, universe) and authenticated (wallet, skills, location, industry, market, mail, fatigue)
|
|
49
|
+
- **Abstract auth** — implement `AbstractAuth` to plug in any OAuth2 token source
|
|
50
|
+
- **Type-safe** — PEP 561 compatible (`py.typed`), strict mypy configuration
|
|
51
|
+
- **Tested** — 100% test coverage
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install python-eveonline
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import asyncio
|
|
63
|
+
import aiohttp
|
|
64
|
+
from eveonline import EveOnlineClient
|
|
65
|
+
|
|
66
|
+
async def main():
|
|
67
|
+
async with aiohttp.ClientSession() as session:
|
|
68
|
+
client = EveOnlineClient(session=session)
|
|
69
|
+
status = await client.async_get_server_status()
|
|
70
|
+
print(f"{status.players} players online (v{status.server_version})")
|
|
71
|
+
|
|
72
|
+
asyncio.run(main())
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
- [**Quickstart**](docs/quickstart.md) — public and authenticated endpoint examples
|
|
78
|
+
- [**Authentication**](docs/authentication.md) — implementing `AbstractAuth`, required OAuth scopes
|
|
79
|
+
- [**Endpoints**](docs/endpoints.md) — full reference with field tables for all 15 methods
|
|
80
|
+
- [**Error Handling**](docs/error-handling.md) — exception hierarchy, rate limiting, ESI cache times
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# python-eveonline
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/python-eveonline/)
|
|
4
|
+
[](https://pypi.org/project/python-eveonline/)
|
|
5
|
+
[](https://github.com/ronaldvdmeer/python-eveonline/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/ronaldvdmeer/python-eveonline/releases)
|
|
7
|
+
|
|
8
|
+
Async Python client library for the [Eve Online ESI API](https://esi.evetech.net/ui/).
|
|
9
|
+
|
|
10
|
+
Built for use with [Home Assistant](https://www.home-assistant.io/) but can be used standalone in any async Python project.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Fully async** — built on [aiohttp](https://docs.aiohttp.org/)
|
|
15
|
+
- **Typed models** — all API responses are frozen dataclasses with full type annotations
|
|
16
|
+
- **15 endpoints** — public (server, character, corporation, universe) and authenticated (wallet, skills, location, industry, market, mail, fatigue)
|
|
17
|
+
- **Abstract auth** — implement `AbstractAuth` to plug in any OAuth2 token source
|
|
18
|
+
- **Type-safe** — PEP 561 compatible (`py.typed`), strict mypy configuration
|
|
19
|
+
- **Tested** — 100% test coverage
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install python-eveonline
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import asyncio
|
|
31
|
+
import aiohttp
|
|
32
|
+
from eveonline import EveOnlineClient
|
|
33
|
+
|
|
34
|
+
async def main():
|
|
35
|
+
async with aiohttp.ClientSession() as session:
|
|
36
|
+
client = EveOnlineClient(session=session)
|
|
37
|
+
status = await client.async_get_server_status()
|
|
38
|
+
print(f"{status.players} players online (v{status.server_version})")
|
|
39
|
+
|
|
40
|
+
asyncio.run(main())
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Documentation
|
|
44
|
+
|
|
45
|
+
- [**Quickstart**](docs/quickstart.md) — public and authenticated endpoint examples
|
|
46
|
+
- [**Authentication**](docs/authentication.md) — implementing `AbstractAuth`, required OAuth scopes
|
|
47
|
+
- [**Endpoints**](docs/endpoints.md) — full reference with field tables for all 15 methods
|
|
48
|
+
- [**Error Handling**](docs/error-handling.md) — exception hierarchy, rate limiting, ESI cache times
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
[MIT](LICENSE)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-eveonline"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Async Python client for the Eve Online ESI API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -58,6 +58,10 @@ addopts = [
|
|
|
58
58
|
"--cov-report=term-missing",
|
|
59
59
|
"--cov-fail-under=90",
|
|
60
60
|
"-v",
|
|
61
|
+
"-m", "not integration",
|
|
62
|
+
]
|
|
63
|
+
markers = [
|
|
64
|
+
"integration: live ESI API tests — require network access; authenticated tests also require ESI_TOKEN env var",
|
|
61
65
|
]
|
|
62
66
|
|
|
63
67
|
# -- coverage ----------------------------------------------------------------
|
|
@@ -160,6 +164,13 @@ ignore = [
|
|
|
160
164
|
"src/eveonline/const.py" = [
|
|
161
165
|
"S105", # SSO_TOKEN_URL is a URL, not a password
|
|
162
166
|
]
|
|
167
|
+
"scripts/**/*.py" = [
|
|
168
|
+
"D", # No docstring requirements in scripts
|
|
169
|
+
"T20", # Allow print in scripts (CLI output)
|
|
170
|
+
"ANN", # No annotation requirements in scripts
|
|
171
|
+
"S105", # URL constants that look like passwords
|
|
172
|
+
"S310", # URL open is expected in these scripts
|
|
173
|
+
]
|
|
163
174
|
|
|
164
175
|
[tool.ruff.lint.pydocstyle]
|
|
165
176
|
convention = "google"
|
|
@@ -181,3 +192,4 @@ disable = [
|
|
|
181
192
|
|
|
182
193
|
[tool.pylint.design]
|
|
183
194
|
max-args = 10
|
|
195
|
+
max-public-methods = 30
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from datetime import datetime
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any, overload
|
|
7
7
|
|
|
8
8
|
from aiohttp import ClientSession
|
|
9
9
|
|
|
@@ -17,21 +17,29 @@ from .exceptions import (
|
|
|
17
17
|
EveOnlineRateLimitError,
|
|
18
18
|
)
|
|
19
19
|
from .models import (
|
|
20
|
+
CalendarEvent,
|
|
21
|
+
CharacterClones,
|
|
22
|
+
CharacterContact,
|
|
20
23
|
CharacterLocation,
|
|
24
|
+
CharacterNotification,
|
|
21
25
|
CharacterOnlineStatus,
|
|
22
26
|
CharacterPortrait,
|
|
23
27
|
CharacterPublicInfo,
|
|
24
28
|
CharacterShip,
|
|
25
29
|
CharacterSkillsSummary,
|
|
30
|
+
CloneHomeLocation,
|
|
26
31
|
CorporationPublicInfo,
|
|
27
32
|
IndustryJob,
|
|
33
|
+
JumpClone,
|
|
28
34
|
JumpFatigue,
|
|
35
|
+
LoyaltyPoints,
|
|
29
36
|
MailLabelsSummary,
|
|
30
37
|
MarketOrder,
|
|
31
38
|
ServerStatus,
|
|
32
39
|
SkillQueueEntry,
|
|
33
40
|
UniverseName,
|
|
34
41
|
WalletBalance,
|
|
42
|
+
WalletJournalEntry,
|
|
35
43
|
)
|
|
36
44
|
|
|
37
45
|
|
|
@@ -79,6 +87,8 @@ class EveOnlineClient:
|
|
|
79
87
|
"""
|
|
80
88
|
self._auth = auth
|
|
81
89
|
self._host = host
|
|
90
|
+
# ETag cache: maps cache_key -> (etag, cached_response_data)
|
|
91
|
+
self._etag_cache: dict[str, tuple[str, Any]] = {}
|
|
82
92
|
|
|
83
93
|
if auth is not None:
|
|
84
94
|
self._session = auth.websession
|
|
@@ -88,13 +98,81 @@ class EveOnlineClient:
|
|
|
88
98
|
msg = "Either 'session' or 'auth' must be provided"
|
|
89
99
|
raise EveOnlineError(msg)
|
|
90
100
|
|
|
101
|
+
def clear_etag_cache(self) -> None:
|
|
102
|
+
"""Clear the ETag cache, forcing fresh responses on the next requests."""
|
|
103
|
+
self._etag_cache.clear()
|
|
104
|
+
|
|
91
105
|
# -------------------------------------------------------------------------
|
|
92
106
|
# Internal helpers
|
|
93
107
|
# -------------------------------------------------------------------------
|
|
94
108
|
|
|
109
|
+
def _etag_key(self, path: str, params: dict[str, Any], authenticated: bool) -> str:
|
|
110
|
+
"""Build a deterministic cache key for an ESI endpoint.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
path: API path relative to the ESI base URL.
|
|
114
|
+
params: Query parameters (must already contain ``datasource``).
|
|
115
|
+
authenticated: Whether the request uses OAuth.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
A string key unique to this endpoint + parameter combination.
|
|
119
|
+
"""
|
|
120
|
+
sorted_params = "&".join(f"{k}={v}" for k, v in sorted(params.items()))
|
|
121
|
+
auth_prefix = "auth" if authenticated else "pub"
|
|
122
|
+
return f"{auth_prefix}:{path}?{sorted_params}"
|
|
123
|
+
|
|
124
|
+
def _build_etag_headers(self, method: str, cache_key: str) -> dict[str, str]:
|
|
125
|
+
"""Return an ``If-None-Match`` header dict if a cached ETag exists.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
method: HTTP method (only ``"GET"`` uses ETags).
|
|
129
|
+
cache_key: Cache key produced by :meth:`_etag_key`.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
A headers dict with ``If-None-Match`` set, or an empty dict.
|
|
133
|
+
"""
|
|
134
|
+
if method == "GET" and cache_key in self._etag_cache:
|
|
135
|
+
return {"If-None-Match": self._etag_cache[cache_key][0]}
|
|
136
|
+
return {}
|
|
137
|
+
|
|
138
|
+
def _store_etag(self, method: str, cache_key: str, response: Any, data: Any) -> None:
|
|
139
|
+
"""Cache the ETag from a successful GET response.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
method: HTTP method (only ``"GET"`` responses are cached).
|
|
143
|
+
cache_key: Cache key produced by :meth:`_etag_key`.
|
|
144
|
+
response: The aiohttp response object.
|
|
145
|
+
data: Parsed JSON data to cache alongside the ETag.
|
|
146
|
+
"""
|
|
147
|
+
etag = response.headers.get("ETag")
|
|
148
|
+
if method == "GET" and etag:
|
|
149
|
+
self._etag_cache[cache_key] = (etag, data)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _parse_retry_after(response: Any) -> int | None:
|
|
153
|
+
"""Extract the Retry-After delay in seconds from a rate-limit response.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
response: The aiohttp response object.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
The delay in seconds, or ``None`` if the header is absent or
|
|
160
|
+
cannot be parsed as an integer.
|
|
161
|
+
"""
|
|
162
|
+
if (retry_after := response.headers.get("Retry-After")) is None:
|
|
163
|
+
return None
|
|
164
|
+
try:
|
|
165
|
+
return int(retry_after)
|
|
166
|
+
except ValueError:
|
|
167
|
+
return None
|
|
168
|
+
|
|
95
169
|
async def _request(self, method: str, path: str, *, authenticated: bool = False, **kwargs: Any) -> Any:
|
|
96
170
|
"""Make a request to the ESI API.
|
|
97
171
|
|
|
172
|
+
GET requests use ETag caching: a cached ``ETag`` is sent as
|
|
173
|
+
``If-None-Match``; a ``304 Not Modified`` response returns the
|
|
174
|
+
previously cached data without consuming bandwidth.
|
|
175
|
+
|
|
98
176
|
Args:
|
|
99
177
|
method: HTTP method.
|
|
100
178
|
path: API path relative to ESI base URL.
|
|
@@ -114,18 +192,21 @@ class EveOnlineClient:
|
|
|
114
192
|
"""
|
|
115
193
|
params: dict[str, Any] = dict(kwargs.pop("params", {}) or {})
|
|
116
194
|
params.setdefault("datasource", ESI_DATASOURCE)
|
|
195
|
+
cache_key = self._etag_key(path, params, authenticated)
|
|
196
|
+
headers = {**dict(kwargs.pop("headers", {}) or {}), **self._build_etag_headers(method, cache_key)}
|
|
117
197
|
|
|
118
198
|
try:
|
|
119
199
|
if authenticated:
|
|
120
200
|
if self._auth is None:
|
|
121
201
|
msg = "Authentication required but no auth provider configured"
|
|
122
202
|
raise EveOnlineAuthenticationError(msg)
|
|
123
|
-
response = await self._auth.request(method, path, params=params, **kwargs)
|
|
203
|
+
response = await self._auth.request(method, path, params=params, headers=headers, **kwargs)
|
|
124
204
|
else:
|
|
125
205
|
response = await self._session.request(
|
|
126
206
|
method,
|
|
127
207
|
f"{self._host}/{path}",
|
|
128
208
|
params=params,
|
|
209
|
+
headers=headers,
|
|
129
210
|
**kwargs,
|
|
130
211
|
)
|
|
131
212
|
except EveOnlineError:
|
|
@@ -139,26 +220,46 @@ class EveOnlineClient:
|
|
|
139
220
|
msg = f"Authentication failed ({response.status}): {text}"
|
|
140
221
|
raise EveOnlineAuthenticationError(msg)
|
|
141
222
|
|
|
223
|
+
if response.status == 304:
|
|
224
|
+
# Not Modified — return the data we cached earlier if it still exists.
|
|
225
|
+
response.release()
|
|
226
|
+
if (cached := self._etag_cache.get(cache_key)) is None:
|
|
227
|
+
msg = (
|
|
228
|
+
f"Received 304 Not Modified from ESI, but no matching ETag "
|
|
229
|
+
f"cache entry exists for key {cache_key!r}."
|
|
230
|
+
)
|
|
231
|
+
raise EveOnlineError(msg)
|
|
232
|
+
return cached[1]
|
|
233
|
+
|
|
142
234
|
if response.status == 404:
|
|
235
|
+
response.release()
|
|
143
236
|
msg = f"Resource not found: {path}"
|
|
144
237
|
raise EveOnlineNotFoundError(msg)
|
|
145
238
|
|
|
146
239
|
if response.status in (420, 429):
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if retry_after is not None:
|
|
150
|
-
try:
|
|
151
|
-
retry_after_seconds = int(retry_after)
|
|
152
|
-
except ValueError:
|
|
153
|
-
retry_after_seconds = None
|
|
154
|
-
raise EveOnlineRateLimitError(retry_after=retry_after_seconds)
|
|
240
|
+
response.release()
|
|
241
|
+
raise EveOnlineRateLimitError(retry_after=self._parse_retry_after(response))
|
|
155
242
|
|
|
156
243
|
if response.status >= 400:
|
|
157
244
|
text = await response.text()
|
|
158
245
|
msg = f"ESI API error ({response.status}): {text}"
|
|
159
246
|
raise EveOnlineError(msg)
|
|
160
247
|
|
|
161
|
-
|
|
248
|
+
data = await response.json()
|
|
249
|
+
self._store_etag(method, cache_key, response, data)
|
|
250
|
+
return data
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
@overload
|
|
254
|
+
def _parse_datetime(value: str) -> datetime: ...
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
@overload
|
|
258
|
+
def _parse_datetime(value: None) -> None: ...
|
|
259
|
+
|
|
260
|
+
@staticmethod
|
|
261
|
+
@overload
|
|
262
|
+
def _parse_datetime(value: str | None) -> datetime | None: ...
|
|
162
263
|
|
|
163
264
|
@staticmethod
|
|
164
265
|
def _parse_datetime(value: str | None) -> datetime | None:
|
|
@@ -169,8 +270,8 @@ class EveOnlineClient:
|
|
|
169
270
|
``None``.
|
|
170
271
|
|
|
171
272
|
Returns:
|
|
172
|
-
A
|
|
173
|
-
*value* is ``None``.
|
|
273
|
+
A :class:`~datetime.datetime` when *value* is a string, or
|
|
274
|
+
``None`` when *value* is ``None``.
|
|
174
275
|
"""
|
|
175
276
|
if value is None:
|
|
176
277
|
return None
|
|
@@ -190,7 +291,7 @@ class EveOnlineClient:
|
|
|
190
291
|
return ServerStatus(
|
|
191
292
|
players=data["players"],
|
|
192
293
|
server_version=data["server_version"],
|
|
193
|
-
start_time=
|
|
294
|
+
start_time=self._parse_datetime(data["start_time"]),
|
|
194
295
|
vip=data.get("vip"),
|
|
195
296
|
)
|
|
196
297
|
|
|
@@ -208,7 +309,7 @@ class EveOnlineClient:
|
|
|
208
309
|
character_id=character_id,
|
|
209
310
|
name=data["name"],
|
|
210
311
|
corporation_id=data["corporation_id"],
|
|
211
|
-
birthday=
|
|
312
|
+
birthday=self._parse_datetime(data["birthday"]),
|
|
212
313
|
gender=data["gender"],
|
|
213
314
|
race_id=data["race_id"],
|
|
214
315
|
bloodline_id=data["bloodline_id"],
|
|
@@ -438,8 +539,8 @@ class EveOnlineClient:
|
|
|
438
539
|
job_id=entry["job_id"],
|
|
439
540
|
activity_id=entry["activity_id"],
|
|
440
541
|
status=entry["status"],
|
|
441
|
-
start_date=
|
|
442
|
-
end_date=
|
|
542
|
+
start_date=self._parse_datetime(entry["start_date"]),
|
|
543
|
+
end_date=self._parse_datetime(entry["end_date"]),
|
|
443
544
|
blueprint_type_id=entry["blueprint_type_id"],
|
|
444
545
|
output_location_id=entry["output_location_id"],
|
|
445
546
|
runs=entry["runs"],
|
|
@@ -472,7 +573,7 @@ class EveOnlineClient:
|
|
|
472
573
|
volume_total=entry["volume_total"],
|
|
473
574
|
location_id=entry["location_id"],
|
|
474
575
|
region_id=entry["region_id"],
|
|
475
|
-
issued=
|
|
576
|
+
issued=self._parse_datetime(entry["issued"]),
|
|
476
577
|
duration=entry["duration"],
|
|
477
578
|
range=entry["range"],
|
|
478
579
|
min_volume=entry.get("min_volume"),
|
|
@@ -497,3 +598,175 @@ class EveOnlineClient:
|
|
|
497
598
|
last_jump_date=self._parse_datetime(data.get("last_jump_date")),
|
|
498
599
|
last_update_date=self._parse_datetime(data.get("last_update_date")),
|
|
499
600
|
)
|
|
601
|
+
|
|
602
|
+
async def async_get_notifications(self, character_id: int) -> list[CharacterNotification]:
|
|
603
|
+
"""Get a character's recent notifications.
|
|
604
|
+
|
|
605
|
+
Requires scope: ``esi-characters.read_notifications.v1``
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
character_id: The Eve Online character ID.
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
List of CharacterNotification entries, newest first.
|
|
612
|
+
"""
|
|
613
|
+
data = await self._request("GET", f"characters/{character_id}/notifications/", authenticated=True)
|
|
614
|
+
return [
|
|
615
|
+
CharacterNotification(
|
|
616
|
+
notification_id=entry["notification_id"],
|
|
617
|
+
sender_id=entry["sender_id"],
|
|
618
|
+
sender_type=entry["sender_type"],
|
|
619
|
+
type=entry["type"],
|
|
620
|
+
timestamp=self._parse_datetime(entry["timestamp"]),
|
|
621
|
+
is_read=entry.get("is_read"),
|
|
622
|
+
text=entry.get("text"),
|
|
623
|
+
)
|
|
624
|
+
for entry in data
|
|
625
|
+
]
|
|
626
|
+
|
|
627
|
+
async def async_get_clones(self, character_id: int) -> CharacterClones:
|
|
628
|
+
"""Get a character's clone information.
|
|
629
|
+
|
|
630
|
+
Requires scope: ``esi-clones.read_clones.v1``
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
character_id: The Eve Online character ID.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
CharacterClones with home location and jump clones.
|
|
637
|
+
"""
|
|
638
|
+
data = await self._request("GET", f"characters/{character_id}/clones/", authenticated=True)
|
|
639
|
+
|
|
640
|
+
home_loc = data.get("home_location")
|
|
641
|
+
home_location: CloneHomeLocation | None = None
|
|
642
|
+
if home_loc:
|
|
643
|
+
home_location = CloneHomeLocation(
|
|
644
|
+
location_id=home_loc["location_id"],
|
|
645
|
+
location_type=home_loc["location_type"],
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
jump_clones = tuple(
|
|
649
|
+
JumpClone(
|
|
650
|
+
jump_clone_id=jc["jump_clone_id"],
|
|
651
|
+
location_id=jc["location_id"],
|
|
652
|
+
location_type=jc["location_type"],
|
|
653
|
+
implants=tuple(jc.get("implants", [])),
|
|
654
|
+
name=jc.get("name"),
|
|
655
|
+
)
|
|
656
|
+
for jc in data.get("jump_clones", [])
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
return CharacterClones(
|
|
660
|
+
home_location=home_location,
|
|
661
|
+
jump_clones=jump_clones,
|
|
662
|
+
last_clone_jump_date=self._parse_datetime(data.get("last_clone_jump_date")),
|
|
663
|
+
last_station_change_date=self._parse_datetime(data.get("last_station_change_date")),
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
async def async_get_implants(self, character_id: int) -> tuple[int, ...]:
|
|
667
|
+
"""Get the type IDs of a character's active implants.
|
|
668
|
+
|
|
669
|
+
Requires scope: ``esi-clones.read_implants.v1``
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
character_id: The Eve Online character ID.
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
Tuple of implant type IDs.
|
|
676
|
+
"""
|
|
677
|
+
data = await self._request("GET", f"characters/{character_id}/implants/", authenticated=True)
|
|
678
|
+
return tuple(data)
|
|
679
|
+
|
|
680
|
+
async def async_get_wallet_journal(self, character_id: int) -> list[WalletJournalEntry]:
|
|
681
|
+
"""Get a character's wallet journal (recent transactions).
|
|
682
|
+
|
|
683
|
+
Requires scope: ``esi-wallet.read_character_wallet.v1``
|
|
684
|
+
|
|
685
|
+
Args:
|
|
686
|
+
character_id: The Eve Online character ID.
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
List of WalletJournalEntry entries, newest first.
|
|
690
|
+
"""
|
|
691
|
+
data = await self._request("GET", f"characters/{character_id}/wallet/journal/", authenticated=True)
|
|
692
|
+
return [
|
|
693
|
+
WalletJournalEntry(
|
|
694
|
+
id=entry["id"],
|
|
695
|
+
date=self._parse_datetime(entry["date"]),
|
|
696
|
+
ref_type=entry["ref_type"],
|
|
697
|
+
description=entry.get("description", ""),
|
|
698
|
+
amount=entry.get("amount"),
|
|
699
|
+
balance=entry.get("balance"),
|
|
700
|
+
first_party_id=entry.get("first_party_id"),
|
|
701
|
+
second_party_id=entry.get("second_party_id"),
|
|
702
|
+
reason=entry.get("reason"),
|
|
703
|
+
)
|
|
704
|
+
for entry in data
|
|
705
|
+
]
|
|
706
|
+
|
|
707
|
+
async def async_get_contacts(self, character_id: int) -> list[CharacterContact]:
|
|
708
|
+
"""Get a character's contacts.
|
|
709
|
+
|
|
710
|
+
Requires scope: ``esi-characters.read_contacts.v1``
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
character_id: The Eve Online character ID.
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
List of CharacterContact entries.
|
|
717
|
+
"""
|
|
718
|
+
data = await self._request("GET", f"characters/{character_id}/contacts/", authenticated=True)
|
|
719
|
+
return [
|
|
720
|
+
CharacterContact(
|
|
721
|
+
contact_id=entry["contact_id"],
|
|
722
|
+
contact_type=entry["contact_type"],
|
|
723
|
+
standing=entry["standing"],
|
|
724
|
+
is_blocked=entry.get("is_blocked"),
|
|
725
|
+
is_watched=entry.get("is_watched"),
|
|
726
|
+
label_ids=tuple(entry["label_ids"]) if entry.get("label_ids") else None,
|
|
727
|
+
)
|
|
728
|
+
for entry in data
|
|
729
|
+
]
|
|
730
|
+
|
|
731
|
+
async def async_get_calendar(self, character_id: int) -> list[CalendarEvent]:
|
|
732
|
+
"""Get a character's upcoming calendar events.
|
|
733
|
+
|
|
734
|
+
Requires scope: ``esi-calendar.read_calendar_events.v1``
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
character_id: The Eve Online character ID.
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
List of CalendarEvent entries.
|
|
741
|
+
"""
|
|
742
|
+
data = await self._request("GET", f"characters/{character_id}/calendar/", authenticated=True)
|
|
743
|
+
return [
|
|
744
|
+
CalendarEvent(
|
|
745
|
+
event_id=entry["event_id"],
|
|
746
|
+
event_date=self._parse_datetime(entry["event_date"]),
|
|
747
|
+
title=entry["title"],
|
|
748
|
+
importance=entry.get("importance"),
|
|
749
|
+
event_response=entry.get("event_response"),
|
|
750
|
+
)
|
|
751
|
+
for entry in data
|
|
752
|
+
]
|
|
753
|
+
|
|
754
|
+
async def async_get_loyalty_points(self, character_id: int) -> list[LoyaltyPoints]:
|
|
755
|
+
"""Get a character's loyalty points per corporation.
|
|
756
|
+
|
|
757
|
+
Requires scope: ``esi-characters.read_loyalty.v1``
|
|
758
|
+
|
|
759
|
+
Args:
|
|
760
|
+
character_id: The Eve Online character ID.
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
List of LoyaltyPoints entries.
|
|
764
|
+
"""
|
|
765
|
+
data = await self._request("GET", f"characters/{character_id}/loyalty/points/", authenticated=True)
|
|
766
|
+
return [
|
|
767
|
+
LoyaltyPoints(
|
|
768
|
+
corporation_id=entry["corporation_id"],
|
|
769
|
+
loyalty_points=entry["loyalty_points"],
|
|
770
|
+
)
|
|
771
|
+
for entry in data
|
|
772
|
+
]
|
|
@@ -28,6 +28,9 @@ SCOPE_READ_FATIGUE: Final = "esi-characters.read_fatigue.v1"
|
|
|
28
28
|
SCOPE_READ_MAIL: Final = "esi-mail.read_mail.v1"
|
|
29
29
|
SCOPE_READ_INDUSTRY_JOBS: Final = "esi-industry.read_character_jobs.v1"
|
|
30
30
|
SCOPE_READ_MARKET_ORDERS: Final = "esi-markets.read_character_orders.v1"
|
|
31
|
+
SCOPE_READ_CONTACTS: Final = "esi-characters.read_contacts.v1"
|
|
32
|
+
SCOPE_READ_CALENDAR: Final = "esi-calendar.read_calendar_events.v1"
|
|
33
|
+
SCOPE_READ_LOYALTY: Final = "esi-characters.read_loyalty.v1"
|
|
31
34
|
|
|
32
35
|
# Default scopes for a typical Home Assistant integration
|
|
33
36
|
DEFAULT_SCOPES: Final = (
|