blizzardapi3 3.0.0__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.
- blizzardapi3/__init__.py +13 -0
- blizzardapi3/api/__init__.py +5 -0
- blizzardapi3/api/wow.py +76 -0
- blizzardapi3/blizzard_api.py +53 -0
- blizzardapi3/core/__init__.py +17 -0
- blizzardapi3/core/auth.py +177 -0
- blizzardapi3/core/client.py +144 -0
- blizzardapi3/core/context.py +23 -0
- blizzardapi3/core/executor.py +249 -0
- blizzardapi3/core/factory.py +377 -0
- blizzardapi3/core/registry.py +203 -0
- blizzardapi3/exceptions/__init__.py +28 -0
- blizzardapi3/exceptions/auth.py +64 -0
- blizzardapi3/exceptions/base.py +33 -0
- blizzardapi3/exceptions/request.py +89 -0
- blizzardapi3/exceptions/validation.py +85 -0
- blizzardapi3/types.py +83 -0
- blizzardapi3-3.0.0.dist-info/METADATA +426 -0
- blizzardapi3-3.0.0.dist-info/RECORD +21 -0
- blizzardapi3-3.0.0.dist-info/WHEEL +5 -0
- blizzardapi3-3.0.0.dist-info/top_level.txt +1 -0
blizzardapi3/__init__.py
ADDED
blizzardapi3/api/wow.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""World of Warcraft API facade."""
|
|
2
|
+
|
|
3
|
+
from ..core import BaseClient, EndpointRegistry, MethodFactory, RequestExecutor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WowGameDataAPI:
|
|
7
|
+
"""WoW Game Data API with dynamically generated methods."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, client: BaseClient, executor: RequestExecutor, registry: EndpointRegistry):
|
|
10
|
+
"""Initialize WoW Game Data API.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
client: Base client for session management
|
|
14
|
+
executor: Request executor
|
|
15
|
+
registry: Endpoint registry
|
|
16
|
+
"""
|
|
17
|
+
self.client = client
|
|
18
|
+
self.executor = executor
|
|
19
|
+
self.registry = registry
|
|
20
|
+
|
|
21
|
+
# Generate and attach all methods
|
|
22
|
+
factory = MethodFactory(executor, registry)
|
|
23
|
+
methods = factory.generate_all_methods("wow", "game_data")
|
|
24
|
+
|
|
25
|
+
for method_name, (sync_method, async_method) in methods.items():
|
|
26
|
+
# Bind methods to this instance
|
|
27
|
+
setattr(self, method_name, sync_method.__get__(self, type(self)))
|
|
28
|
+
setattr(self, f"{method_name}_async", async_method.__get__(self, type(self)))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WowProfileAPI:
|
|
32
|
+
"""WoW Profile API with dynamically generated methods."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, client: BaseClient, executor: RequestExecutor, registry: EndpointRegistry):
|
|
35
|
+
"""Initialize WoW Profile API.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
client: Base client for session management
|
|
39
|
+
executor: Request executor
|
|
40
|
+
registry: Endpoint registry
|
|
41
|
+
"""
|
|
42
|
+
self.client = client
|
|
43
|
+
self.executor = executor
|
|
44
|
+
self.registry = registry
|
|
45
|
+
|
|
46
|
+
# Generate and attach all methods
|
|
47
|
+
factory = MethodFactory(executor, registry)
|
|
48
|
+
methods = factory.generate_all_methods("wow", "profile")
|
|
49
|
+
|
|
50
|
+
for method_name, (sync_method, async_method) in methods.items():
|
|
51
|
+
# Bind methods to this instance
|
|
52
|
+
setattr(self, method_name, sync_method.__get__(self, type(self)))
|
|
53
|
+
setattr(self, f"{method_name}_async", async_method.__get__(self, type(self)))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class WowAPI:
|
|
57
|
+
"""World of Warcraft API facade.
|
|
58
|
+
|
|
59
|
+
Provides access to:
|
|
60
|
+
- game_data: WoW Game Data API (achievements, items, etc.)
|
|
61
|
+
- profile: WoW Profile API (characters, guilds, etc.)
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, client: BaseClient, registry: EndpointRegistry):
|
|
65
|
+
"""Initialize WoW API.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
client: Base client for session management
|
|
69
|
+
registry: Endpoint registry
|
|
70
|
+
"""
|
|
71
|
+
self.client = client
|
|
72
|
+
executor = RequestExecutor(client.token_manager)
|
|
73
|
+
|
|
74
|
+
# Initialize sub-APIs
|
|
75
|
+
self.game_data = WowGameDataAPI(client, executor, registry)
|
|
76
|
+
self.profile = WowProfileAPI(client, executor, registry)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Main BlizzardAPI client."""
|
|
2
|
+
|
|
3
|
+
from .api import WowAPI
|
|
4
|
+
from .core import BaseClient, EndpointRegistry
|
|
5
|
+
from .types import Locale, Region
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BlizzardAPI(BaseClient):
|
|
9
|
+
"""Main Blizzard API client.
|
|
10
|
+
|
|
11
|
+
Provides access to all Blizzard game APIs with proper session management,
|
|
12
|
+
authentication, and error handling.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
Synchronous usage:
|
|
16
|
+
with BlizzardAPI(client_id, client_secret) as api:
|
|
17
|
+
data = api.wow.game_data.get_achievement(
|
|
18
|
+
region="us",
|
|
19
|
+
locale="en_US",
|
|
20
|
+
achievement_id=6
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
Asynchronous usage:
|
|
24
|
+
async with BlizzardAPI(client_id, client_secret) as api:
|
|
25
|
+
data = await api.wow.game_data.get_achievement_async(
|
|
26
|
+
region="us",
|
|
27
|
+
locale="en_US",
|
|
28
|
+
achievement_id=6
|
|
29
|
+
)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
client_id: str,
|
|
35
|
+
client_secret: str,
|
|
36
|
+
region: Region | str = Region.US,
|
|
37
|
+
locale: Locale | str | None = None,
|
|
38
|
+
):
|
|
39
|
+
"""Initialize Blizzard API client.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
client_id: Blizzard API client ID
|
|
43
|
+
client_secret: Blizzard API client secret
|
|
44
|
+
region: Default region (defaults to US)
|
|
45
|
+
locale: Default locale (defaults to region's default)
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(client_id, client_secret, region, locale)
|
|
48
|
+
|
|
49
|
+
# Initialize endpoint registry
|
|
50
|
+
self.registry = EndpointRegistry()
|
|
51
|
+
|
|
52
|
+
# Initialize game APIs
|
|
53
|
+
self.wow = WowAPI(self, self.registry)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Core framework components."""
|
|
2
|
+
|
|
3
|
+
from .auth import TokenManager
|
|
4
|
+
from .client import BaseClient
|
|
5
|
+
from .context import RequestContext
|
|
6
|
+
from .executor import RequestExecutor
|
|
7
|
+
from .factory import MethodFactory
|
|
8
|
+
from .registry import EndpointRegistry
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BaseClient",
|
|
12
|
+
"TokenManager",
|
|
13
|
+
"RequestContext",
|
|
14
|
+
"RequestExecutor",
|
|
15
|
+
"MethodFactory",
|
|
16
|
+
"EndpointRegistry",
|
|
17
|
+
]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""OAuth token management."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from ..exceptions import TokenError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TokenManager:
|
|
12
|
+
"""Manages OAuth token lifecycle with automatic refresh.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
TOKEN_BUFFER_SECONDS: Refresh tokens 5 minutes before expiry
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
TOKEN_BUFFER_SECONDS = 300 # 5 minutes
|
|
19
|
+
|
|
20
|
+
def __init__(self, client_id: str, client_secret: str):
|
|
21
|
+
"""Initialize token manager.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
client_id: Blizzard API client ID
|
|
25
|
+
client_secret: Blizzard API client secret
|
|
26
|
+
"""
|
|
27
|
+
self.client_id = client_id
|
|
28
|
+
self.client_secret = client_secret
|
|
29
|
+
|
|
30
|
+
self._token: str | None = None
|
|
31
|
+
self._token_type: str | None = None
|
|
32
|
+
self._expires_at: float | None = None
|
|
33
|
+
|
|
34
|
+
def is_token_valid(self) -> bool:
|
|
35
|
+
"""Check if current token is valid.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
True if token exists and hasn't expired (with buffer)
|
|
39
|
+
"""
|
|
40
|
+
if not self._token or not self._expires_at:
|
|
41
|
+
return False
|
|
42
|
+
return time.time() < (self._expires_at - self.TOKEN_BUFFER_SECONDS)
|
|
43
|
+
|
|
44
|
+
def get_token(self, region: str, session: requests.Session) -> str:
|
|
45
|
+
"""Get valid access token (synchronous).
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
region: API region for OAuth endpoint
|
|
49
|
+
session: requests Session to use
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Valid access token
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
TokenError: If token fetch fails
|
|
56
|
+
"""
|
|
57
|
+
if self.is_token_valid():
|
|
58
|
+
return self._token
|
|
59
|
+
|
|
60
|
+
return self._fetch_token(region, session)
|
|
61
|
+
|
|
62
|
+
async def get_token_async(self, region: str, session: aiohttp.ClientSession) -> str:
|
|
63
|
+
"""Get valid access token (asynchronous).
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
region: API region for OAuth endpoint
|
|
67
|
+
session: aiohttp ClientSession to use
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Valid access token
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
TokenError: If token fetch fails
|
|
74
|
+
"""
|
|
75
|
+
if self.is_token_valid():
|
|
76
|
+
return self._token
|
|
77
|
+
|
|
78
|
+
return await self._fetch_token_async(region, session)
|
|
79
|
+
|
|
80
|
+
def _fetch_token(self, region: str, session: requests.Session) -> str:
|
|
81
|
+
"""Fetch new access token (synchronous).
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
region: API region for OAuth endpoint
|
|
85
|
+
session: requests Session to use
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
New access token
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
TokenError: If token request fails
|
|
92
|
+
"""
|
|
93
|
+
url = self._get_oauth_url(region)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
response = session.post(
|
|
97
|
+
url, auth=(self.client_id, self.client_secret), data={"grant_type": "client_credentials"}, timeout=10
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if response.status_code != 200:
|
|
101
|
+
raise TokenError(
|
|
102
|
+
f"Failed to obtain token: {response.status_code}",
|
|
103
|
+
status_code=response.status_code,
|
|
104
|
+
request_url=url,
|
|
105
|
+
response_data=response.json() if response.text else None,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
data = response.json()
|
|
109
|
+
self._token = data["access_token"]
|
|
110
|
+
self._token_type = data["token_type"]
|
|
111
|
+
self._expires_at = time.time() + data["expires_in"]
|
|
112
|
+
|
|
113
|
+
return self._token
|
|
114
|
+
|
|
115
|
+
except requests.RequestException as e:
|
|
116
|
+
raise TokenError(f"Token request failed: {str(e)}", request_url=url)
|
|
117
|
+
|
|
118
|
+
async def _fetch_token_async(self, region: str, session: aiohttp.ClientSession) -> str:
|
|
119
|
+
"""Fetch new access token (asynchronous).
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
region: API region for OAuth endpoint
|
|
123
|
+
session: aiohttp ClientSession to use
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
New access token
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
TokenError: If token request fails
|
|
130
|
+
"""
|
|
131
|
+
url = self._get_oauth_url(region)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
async with session.post(
|
|
135
|
+
url,
|
|
136
|
+
auth=aiohttp.BasicAuth(self.client_id, self.client_secret),
|
|
137
|
+
data={"grant_type": "client_credentials"},
|
|
138
|
+
timeout=aiohttp.ClientTimeout(total=10),
|
|
139
|
+
) as response:
|
|
140
|
+
|
|
141
|
+
if response.status != 200:
|
|
142
|
+
response_data = await response.json() if response.content_type == "application/json" else None
|
|
143
|
+
raise TokenError(
|
|
144
|
+
f"Failed to obtain token: {response.status}",
|
|
145
|
+
status_code=response.status,
|
|
146
|
+
request_url=url,
|
|
147
|
+
response_data=response_data,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
data = await response.json()
|
|
151
|
+
self._token = data["access_token"]
|
|
152
|
+
self._token_type = data["token_type"]
|
|
153
|
+
self._expires_at = time.time() + data["expires_in"]
|
|
154
|
+
|
|
155
|
+
return self._token
|
|
156
|
+
|
|
157
|
+
except aiohttp.ClientError as e:
|
|
158
|
+
raise TokenError(f"Token request failed: {str(e)}", request_url=url)
|
|
159
|
+
|
|
160
|
+
def invalidate(self) -> None:
|
|
161
|
+
"""Invalidate current token, forcing a refresh on next request."""
|
|
162
|
+
self._token = None
|
|
163
|
+
self._expires_at = None
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def _get_oauth_url(region: str) -> str:
|
|
167
|
+
"""Get OAuth URL for region.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
region: API region
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
OAuth token endpoint URL
|
|
174
|
+
"""
|
|
175
|
+
if region == "cn":
|
|
176
|
+
return "https://oauth.battlenet.com.cn/token"
|
|
177
|
+
return f"https://{region}.battle.net/oauth/token"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Base API client with session management."""
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from ..types import Locale, Region, get_default_locale
|
|
7
|
+
from .auth import TokenManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseClient:
|
|
11
|
+
"""Base API client with proper session management.
|
|
12
|
+
|
|
13
|
+
Manages HTTP sessions for both sync and async requests,
|
|
14
|
+
with automatic cleanup via context managers.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
Synchronous usage:
|
|
18
|
+
with BaseClient(client_id, client_secret) as client:
|
|
19
|
+
# Use client
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
Asynchronous usage:
|
|
23
|
+
async with BaseClient(client_id, client_secret) as client:
|
|
24
|
+
# Use client
|
|
25
|
+
pass
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
client_id: str,
|
|
31
|
+
client_secret: str,
|
|
32
|
+
region: Region | str = Region.US,
|
|
33
|
+
locale: Locale | str | None = None,
|
|
34
|
+
):
|
|
35
|
+
"""Initialize API client.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
client_id: Blizzard API client ID
|
|
39
|
+
client_secret: Blizzard API client secret
|
|
40
|
+
region: Default region (defaults to US)
|
|
41
|
+
locale: Default locale (defaults to region's default locale)
|
|
42
|
+
"""
|
|
43
|
+
self.client_id = client_id
|
|
44
|
+
self.client_secret = client_secret
|
|
45
|
+
|
|
46
|
+
# Convert region to enum if string
|
|
47
|
+
if isinstance(region, str):
|
|
48
|
+
self.default_region = Region(region)
|
|
49
|
+
else:
|
|
50
|
+
self.default_region = region
|
|
51
|
+
|
|
52
|
+
# Set default locale
|
|
53
|
+
if locale is None:
|
|
54
|
+
self.default_locale = get_default_locale(self.default_region)
|
|
55
|
+
elif isinstance(locale, str):
|
|
56
|
+
self.default_locale = Locale(locale)
|
|
57
|
+
else:
|
|
58
|
+
self.default_locale = locale
|
|
59
|
+
|
|
60
|
+
# Session management
|
|
61
|
+
self._sync_session: requests.Session | None = None
|
|
62
|
+
self._async_session: aiohttp.ClientSession | None = None
|
|
63
|
+
|
|
64
|
+
# Token manager (shared between sync and async)
|
|
65
|
+
self.token_manager = TokenManager(client_id, client_secret)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def sync_session(self) -> requests.Session:
|
|
69
|
+
"""Get or create synchronous session.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Active requests.Session instance
|
|
73
|
+
"""
|
|
74
|
+
if self._sync_session is None:
|
|
75
|
+
self._sync_session = requests.Session()
|
|
76
|
+
return self._sync_session
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def async_session(self) -> aiohttp.ClientSession:
|
|
80
|
+
"""Get or create asynchronous session.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Active aiohttp.ClientSession instance
|
|
84
|
+
"""
|
|
85
|
+
if self._async_session is None or self._async_session.closed:
|
|
86
|
+
self._async_session = aiohttp.ClientSession()
|
|
87
|
+
return self._async_session
|
|
88
|
+
|
|
89
|
+
def close(self) -> None:
|
|
90
|
+
"""Close synchronous session."""
|
|
91
|
+
if self._sync_session is not None:
|
|
92
|
+
self._sync_session.close()
|
|
93
|
+
self._sync_session = None
|
|
94
|
+
|
|
95
|
+
async def aclose(self) -> None:
|
|
96
|
+
"""Close asynchronous session."""
|
|
97
|
+
if self._async_session is not None and not self._async_session.closed:
|
|
98
|
+
await self._async_session.close()
|
|
99
|
+
self._async_session = None
|
|
100
|
+
|
|
101
|
+
# Context manager support (sync)
|
|
102
|
+
|
|
103
|
+
def __enter__(self):
|
|
104
|
+
"""Enter synchronous context manager.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Self for context manager usage
|
|
108
|
+
"""
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
112
|
+
"""Exit synchronous context manager.
|
|
113
|
+
|
|
114
|
+
Closes sync session automatically.
|
|
115
|
+
"""
|
|
116
|
+
self.close()
|
|
117
|
+
|
|
118
|
+
# Async context manager support
|
|
119
|
+
|
|
120
|
+
async def __aenter__(self):
|
|
121
|
+
"""Enter asynchronous context manager.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Self for async context manager usage
|
|
125
|
+
"""
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
129
|
+
"""Exit asynchronous context manager.
|
|
130
|
+
|
|
131
|
+
Closes async session automatically.
|
|
132
|
+
"""
|
|
133
|
+
await self.aclose()
|
|
134
|
+
|
|
135
|
+
def __del__(self):
|
|
136
|
+
"""Cleanup on deletion.
|
|
137
|
+
|
|
138
|
+
Attempts to close sessions if they're still open.
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
if self._sync_session is not None:
|
|
142
|
+
self._sync_session.close()
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Request context for API calls."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class RequestContext:
|
|
9
|
+
"""Context information for an API request.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
region: API region (e.g., "us", "eu")
|
|
13
|
+
path: API endpoint path
|
|
14
|
+
query_params: Query string parameters
|
|
15
|
+
access_token: User-provided OAuth access token (optional)
|
|
16
|
+
auth_type: Type of authentication ("client_credentials" or "oauth")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
region: str
|
|
20
|
+
path: str
|
|
21
|
+
query_params: dict[str, Any]
|
|
22
|
+
access_token: str | None = None
|
|
23
|
+
auth_type: str = "client_credentials"
|