dataquery-sdk 0.0.7__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.
- dataquery/__init__.py +215 -0
- dataquery/auth.py +380 -0
- dataquery/auto_download.py +448 -0
- dataquery/cli.py +244 -0
- dataquery/client.py +2825 -0
- dataquery/config.py +571 -0
- dataquery/connection_pool.py +538 -0
- dataquery/dataquery.py +2467 -0
- dataquery/exceptions.py +169 -0
- dataquery/logging_config.py +415 -0
- dataquery/models.py +1303 -0
- dataquery/py.typed +2 -0
- dataquery/rate_limiter.py +520 -0
- dataquery/retry.py +406 -0
- dataquery/utils.py +525 -0
- dataquery_sdk-0.0.7.dist-info/METADATA +996 -0
- dataquery_sdk-0.0.7.dist-info/RECORD +21 -0
- dataquery_sdk-0.0.7.dist-info/WHEEL +5 -0
- dataquery_sdk-0.0.7.dist-info/entry_points.txt +2 -0
- dataquery_sdk-0.0.7.dist-info/licenses/LICENSE +201 -0
- dataquery_sdk-0.0.7.dist-info/top_level.txt +1 -0
dataquery/__init__.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DATAQUERY SDK - Python SDK for DATAQUERY Data API
|
|
3
|
+
|
|
4
|
+
A high-performance Python SDK for the DATAQUERY Data API, providing seamless access
|
|
5
|
+
to economic data files with advanced features like querying, downloading, availability
|
|
6
|
+
checking, rate limiting, retry logic, connection pool monitoring, and comprehensive logging.
|
|
7
|
+
|
|
8
|
+
Quick Start:
|
|
9
|
+
>>> from dataquery import DataQuery
|
|
10
|
+
>>> async with DataQuery() as dq:
|
|
11
|
+
... groups = await dq.list_groups_async()
|
|
12
|
+
... print(f"Found {len(groups)} groups")
|
|
13
|
+
|
|
14
|
+
For more information, visit: https://github.com/dataquery/dataquery-sdk
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__version__ = "0.0.7"
|
|
18
|
+
__author__ = "DATAQUERY SDK Team"
|
|
19
|
+
__email__ = "support@dataquery.com"
|
|
20
|
+
__license__ = "MIT"
|
|
21
|
+
__url__ = "https://github.com/dataquery/dataquery-sdk-python"
|
|
22
|
+
|
|
23
|
+
# Authentication imports
|
|
24
|
+
from .auth import (
|
|
25
|
+
OAuthManager,
|
|
26
|
+
TokenManager,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Auto-download imports
|
|
30
|
+
from .auto_download import AutoDownloadManager
|
|
31
|
+
|
|
32
|
+
# Client imports
|
|
33
|
+
from .client import DataQueryClient
|
|
34
|
+
|
|
35
|
+
# Configuration imports
|
|
36
|
+
from .config import EnvConfig
|
|
37
|
+
|
|
38
|
+
# Connection pool imports
|
|
39
|
+
from .connection_pool import (
|
|
40
|
+
ConnectionPoolConfig,
|
|
41
|
+
ConnectionPoolMonitor,
|
|
42
|
+
ConnectionPoolStats,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Core imports
|
|
46
|
+
from .dataquery import DataQuery, setup_logging
|
|
47
|
+
from .exceptions import (
|
|
48
|
+
AuthenticationError,
|
|
49
|
+
AvailabilityError,
|
|
50
|
+
ConfigurationError,
|
|
51
|
+
DataQueryError,
|
|
52
|
+
DateRangeError,
|
|
53
|
+
DownloadError,
|
|
54
|
+
FileNotFoundError,
|
|
55
|
+
FileTypeError,
|
|
56
|
+
GroupNotFoundError,
|
|
57
|
+
NetworkError,
|
|
58
|
+
NotFoundError,
|
|
59
|
+
RateLimitError,
|
|
60
|
+
ValidationError,
|
|
61
|
+
WorkflowError,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Logging imports
|
|
65
|
+
from .logging_config import (
|
|
66
|
+
LogFormat,
|
|
67
|
+
LoggingConfig,
|
|
68
|
+
LoggingManager,
|
|
69
|
+
LogLevel,
|
|
70
|
+
)
|
|
71
|
+
from .models import (
|
|
72
|
+
AvailabilityInfo,
|
|
73
|
+
AvailableFilesResponse,
|
|
74
|
+
ClientConfig,
|
|
75
|
+
DateRange,
|
|
76
|
+
DownloadOptions,
|
|
77
|
+
DownloadProgress,
|
|
78
|
+
DownloadResult,
|
|
79
|
+
DownloadStatus,
|
|
80
|
+
FileInfo,
|
|
81
|
+
FileList,
|
|
82
|
+
Group,
|
|
83
|
+
GroupList,
|
|
84
|
+
OAuthToken,
|
|
85
|
+
TokenRequest,
|
|
86
|
+
TokenResponse,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Rate limiting imports
|
|
90
|
+
from .rate_limiter import (
|
|
91
|
+
EnhancedTokenBucketRateLimiter,
|
|
92
|
+
QueuePriority,
|
|
93
|
+
RateLimitConfig,
|
|
94
|
+
RateLimitContext,
|
|
95
|
+
TokenBucketRateLimiter,
|
|
96
|
+
create_rate_limiter,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Retry imports
|
|
100
|
+
from .retry import (
|
|
101
|
+
CircuitBreaker,
|
|
102
|
+
CircuitState,
|
|
103
|
+
RetryConfig,
|
|
104
|
+
RetryManager,
|
|
105
|
+
RetryStrategy,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Common utility helpers
|
|
109
|
+
# Utility imports
|
|
110
|
+
from .utils import (
|
|
111
|
+
create_env_template,
|
|
112
|
+
ensure_directory,
|
|
113
|
+
format_duration,
|
|
114
|
+
format_file_size,
|
|
115
|
+
get_download_paths,
|
|
116
|
+
get_env_value,
|
|
117
|
+
load_env_file,
|
|
118
|
+
save_config_to_env,
|
|
119
|
+
set_env_value,
|
|
120
|
+
validate_env_config,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Type aliases for convenience
|
|
124
|
+
__all__ = [
|
|
125
|
+
# Core
|
|
126
|
+
"DataQuery",
|
|
127
|
+
"setup_logging",
|
|
128
|
+
# Models
|
|
129
|
+
"ClientConfig",
|
|
130
|
+
"Group",
|
|
131
|
+
"FileInfo",
|
|
132
|
+
"FileList",
|
|
133
|
+
"AvailabilityInfo",
|
|
134
|
+
"DownloadResult",
|
|
135
|
+
"DownloadStatus",
|
|
136
|
+
"DownloadOptions",
|
|
137
|
+
"DownloadProgress",
|
|
138
|
+
"OAuthToken",
|
|
139
|
+
"TokenRequest",
|
|
140
|
+
"TokenResponse",
|
|
141
|
+
"DateRange",
|
|
142
|
+
"GroupList",
|
|
143
|
+
"AvailableFilesResponse",
|
|
144
|
+
# Exceptions
|
|
145
|
+
"DataQueryError",
|
|
146
|
+
"AuthenticationError",
|
|
147
|
+
"ValidationError",
|
|
148
|
+
"NotFoundError",
|
|
149
|
+
"RateLimitError",
|
|
150
|
+
"NetworkError",
|
|
151
|
+
"ConfigurationError",
|
|
152
|
+
"DownloadError",
|
|
153
|
+
"AvailabilityError",
|
|
154
|
+
"GroupNotFoundError",
|
|
155
|
+
"FileNotFoundError",
|
|
156
|
+
"DateRangeError",
|
|
157
|
+
"FileTypeError",
|
|
158
|
+
"WorkflowError",
|
|
159
|
+
# Client
|
|
160
|
+
"DataQueryClient",
|
|
161
|
+
"format_file_size",
|
|
162
|
+
"format_duration",
|
|
163
|
+
"AutoDownloadManager",
|
|
164
|
+
# Configuration
|
|
165
|
+
"EnvConfig",
|
|
166
|
+
# Utilities
|
|
167
|
+
"create_env_template",
|
|
168
|
+
"save_config_to_env",
|
|
169
|
+
"load_env_file",
|
|
170
|
+
"get_env_value",
|
|
171
|
+
"set_env_value",
|
|
172
|
+
"validate_env_config",
|
|
173
|
+
"ensure_directory",
|
|
174
|
+
"get_download_paths",
|
|
175
|
+
# Rate Limiting
|
|
176
|
+
"TokenBucketRateLimiter",
|
|
177
|
+
"EnhancedTokenBucketRateLimiter",
|
|
178
|
+
"RateLimitConfig",
|
|
179
|
+
"RateLimitContext",
|
|
180
|
+
"QueuePriority",
|
|
181
|
+
"create_rate_limiter",
|
|
182
|
+
# Retry
|
|
183
|
+
"RetryManager",
|
|
184
|
+
"RetryConfig",
|
|
185
|
+
"RetryStrategy",
|
|
186
|
+
"CircuitBreaker",
|
|
187
|
+
"CircuitState",
|
|
188
|
+
# Connection Pool
|
|
189
|
+
"ConnectionPoolMonitor",
|
|
190
|
+
"ConnectionPoolConfig",
|
|
191
|
+
"ConnectionPoolStats",
|
|
192
|
+
# Logging
|
|
193
|
+
"LoggingManager",
|
|
194
|
+
"LoggingConfig",
|
|
195
|
+
"LogLevel",
|
|
196
|
+
"LogFormat",
|
|
197
|
+
# Authentication
|
|
198
|
+
"OAuthManager",
|
|
199
|
+
"TokenManager",
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
# Version info
|
|
203
|
+
__version_info__ = tuple(int(x) for x in __version__.split("."))
|
|
204
|
+
|
|
205
|
+
# Package metadata
|
|
206
|
+
__package_info__ = {
|
|
207
|
+
"name": "dataquery-sdk",
|
|
208
|
+
"version": __version__,
|
|
209
|
+
"author": __author__,
|
|
210
|
+
"email": __email__,
|
|
211
|
+
"license": __license__,
|
|
212
|
+
"url": __url__,
|
|
213
|
+
"description": "Python SDK for DATAQUERY Data API - Query, download, and check availability of economic data files",
|
|
214
|
+
"python_requires": ">=3.11",
|
|
215
|
+
}
|
dataquery/auth.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication module for the DATAQUERY SDK.
|
|
3
|
+
|
|
4
|
+
Provides OAuth token management and Bearer token authentication.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
import aiohttp
|
|
13
|
+
import structlog
|
|
14
|
+
|
|
15
|
+
from .exceptions import AuthenticationError, ConfigurationError
|
|
16
|
+
from .models import ClientConfig, OAuthToken, TokenRequest, TokenResponse
|
|
17
|
+
|
|
18
|
+
logger = structlog.get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TokenManager:
|
|
22
|
+
"""Manages OAuth tokens and Bearer token authentication."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config: ClientConfig):
|
|
25
|
+
self.config = config
|
|
26
|
+
self.current_token: Optional[OAuthToken] = None
|
|
27
|
+
self.token_file: Optional[Path] = None
|
|
28
|
+
self._setup_token_storage()
|
|
29
|
+
|
|
30
|
+
def _setup_token_storage(self):
|
|
31
|
+
"""Setup token storage file."""
|
|
32
|
+
# Prefer explicit token_storage_dir only when token storage is enabled,
|
|
33
|
+
# else fallback to download_dir/.tokens if download_dir is provided.
|
|
34
|
+
base_dir: Optional[Path] = None
|
|
35
|
+
token_storage_enabled = bool(
|
|
36
|
+
getattr(self.config, "token_storage_enabled", False)
|
|
37
|
+
)
|
|
38
|
+
token_storage_dir = getattr(self.config, "token_storage_dir", None)
|
|
39
|
+
if token_storage_enabled and token_storage_dir:
|
|
40
|
+
base_dir = Path(token_storage_dir)
|
|
41
|
+
elif getattr(self.config, "download_dir", None):
|
|
42
|
+
# Use hidden .tokens folder under download_dir only if download_dir is configured
|
|
43
|
+
try:
|
|
44
|
+
if str(self.config.download_dir).strip():
|
|
45
|
+
base_dir = Path(self.config.download_dir) / ".tokens"
|
|
46
|
+
except Exception:
|
|
47
|
+
base_dir = None
|
|
48
|
+
|
|
49
|
+
if base_dir:
|
|
50
|
+
base_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
self.token_file = base_dir / "oauth_token.json"
|
|
52
|
+
else:
|
|
53
|
+
self.token_file = None
|
|
54
|
+
|
|
55
|
+
async def get_valid_token(self) -> Optional[str]:
|
|
56
|
+
"""
|
|
57
|
+
Get a valid access token for API requests.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Bearer token string or None if no valid token available
|
|
61
|
+
"""
|
|
62
|
+
# Check if we have a static bearer token
|
|
63
|
+
if self.config.has_bearer_token:
|
|
64
|
+
return f"Bearer {self.config.bearer_token}"
|
|
65
|
+
|
|
66
|
+
# Check if OAuth is enabled and credentials are available
|
|
67
|
+
if not self.config.has_oauth_credentials:
|
|
68
|
+
logger.warning("No OAuth credentials or bearer token configured")
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
# Load existing token
|
|
72
|
+
if not self.current_token:
|
|
73
|
+
await self._load_token()
|
|
74
|
+
|
|
75
|
+
# Check if current token is valid
|
|
76
|
+
if self.current_token and not self.current_token.is_expired:
|
|
77
|
+
# Check if token is expiring soon
|
|
78
|
+
if self.current_token.is_expiring_soon(self.config.token_refresh_threshold):
|
|
79
|
+
logger.info("Token expiring soon, refreshing...")
|
|
80
|
+
await self._refresh_token()
|
|
81
|
+
|
|
82
|
+
return self.current_token.to_authorization_header()
|
|
83
|
+
|
|
84
|
+
# Token is expired or invalid, get new token
|
|
85
|
+
logger.info("Getting new OAuth token...")
|
|
86
|
+
await self._get_new_token()
|
|
87
|
+
|
|
88
|
+
if self.current_token:
|
|
89
|
+
return self.current_token.to_authorization_header()
|
|
90
|
+
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
async def _get_new_token(self) -> Optional[OAuthToken]:
|
|
94
|
+
"""Get a new OAuth token from the server."""
|
|
95
|
+
if not self.config.oauth_token_url:
|
|
96
|
+
raise ConfigurationError("OAuth token URL not configured")
|
|
97
|
+
|
|
98
|
+
# Validate required fields
|
|
99
|
+
if not self.config.client_id or not self.config.client_secret:
|
|
100
|
+
raise ConfigurationError(
|
|
101
|
+
"client_id and client_secret are required for OAuth"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Create token request
|
|
105
|
+
token_request = TokenRequest(
|
|
106
|
+
grant_type=self.config.grant_type,
|
|
107
|
+
client_id=self.config.client_id,
|
|
108
|
+
client_secret=self.config.client_secret,
|
|
109
|
+
# scope removed
|
|
110
|
+
aud=getattr(self.config, "aud", None),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Make token request
|
|
115
|
+
async with aiohttp.ClientSession() as session:
|
|
116
|
+
async with session.post(
|
|
117
|
+
self.config.oauth_token_url,
|
|
118
|
+
data=token_request.to_dict(),
|
|
119
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
120
|
+
timeout=aiohttp.ClientTimeout(
|
|
121
|
+
total=self.config.timeout,
|
|
122
|
+
connect=min(300.0, self.config.timeout * 0.5),
|
|
123
|
+
sock_read=min(300.0, self.config.timeout * 0.5),
|
|
124
|
+
),
|
|
125
|
+
) as response:
|
|
126
|
+
if response.status == 200:
|
|
127
|
+
data = await response.json()
|
|
128
|
+
token_response = TokenResponse(**data)
|
|
129
|
+
self.current_token = token_response.to_oauth_token()
|
|
130
|
+
|
|
131
|
+
# Save token
|
|
132
|
+
await self._save_token()
|
|
133
|
+
|
|
134
|
+
logger.info(
|
|
135
|
+
"OAuth token obtained successfully",
|
|
136
|
+
expires_in=self.current_token.expires_in,
|
|
137
|
+
)
|
|
138
|
+
return self.current_token
|
|
139
|
+
else:
|
|
140
|
+
error_data = await response.text()
|
|
141
|
+
logger.error(
|
|
142
|
+
"Failed to get OAuth token",
|
|
143
|
+
status=response.status,
|
|
144
|
+
error=error_data,
|
|
145
|
+
)
|
|
146
|
+
raise AuthenticationError(
|
|
147
|
+
f"OAuth token request failed: {response.status}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error("Error getting OAuth token", error=str(e))
|
|
152
|
+
raise AuthenticationError(f"Failed to get OAuth token: {e}")
|
|
153
|
+
|
|
154
|
+
async def _refresh_token(self) -> Optional[OAuthToken]:
|
|
155
|
+
"""Refresh the current OAuth token."""
|
|
156
|
+
if not self.current_token or not self.current_token.refresh_token:
|
|
157
|
+
# No refresh token available, get new token
|
|
158
|
+
return await self._get_new_token()
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
# Create refresh request
|
|
162
|
+
refresh_data = {
|
|
163
|
+
"grant_type": "refresh_token",
|
|
164
|
+
"refresh_token": self.current_token.refresh_token,
|
|
165
|
+
"client_id": self.config.client_id,
|
|
166
|
+
"client_secret": self.config.client_secret,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Validate OAuth token URL
|
|
170
|
+
if not self.config.oauth_token_url:
|
|
171
|
+
raise ConfigurationError("OAuth token URL not configured")
|
|
172
|
+
|
|
173
|
+
# Make refresh request
|
|
174
|
+
async with aiohttp.ClientSession() as session:
|
|
175
|
+
async with session.post(
|
|
176
|
+
self.config.oauth_token_url,
|
|
177
|
+
data=refresh_data,
|
|
178
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
179
|
+
timeout=aiohttp.ClientTimeout(
|
|
180
|
+
total=self.config.timeout,
|
|
181
|
+
connect=min(300.0, self.config.timeout * 0.5),
|
|
182
|
+
sock_read=min(300.0, self.config.timeout * 0.5),
|
|
183
|
+
),
|
|
184
|
+
) as response:
|
|
185
|
+
if response.status == 200:
|
|
186
|
+
data = await response.json()
|
|
187
|
+
token_response = TokenResponse(**data)
|
|
188
|
+
self.current_token = token_response.to_oauth_token()
|
|
189
|
+
|
|
190
|
+
# Save token
|
|
191
|
+
await self._save_token()
|
|
192
|
+
|
|
193
|
+
logger.info(
|
|
194
|
+
"OAuth token refreshed successfully",
|
|
195
|
+
expires_in=self.current_token.expires_in,
|
|
196
|
+
)
|
|
197
|
+
return self.current_token
|
|
198
|
+
else:
|
|
199
|
+
error_data = await response.text()
|
|
200
|
+
logger.error(
|
|
201
|
+
"Failed to refresh OAuth token",
|
|
202
|
+
status=response.status,
|
|
203
|
+
error=error_data,
|
|
204
|
+
)
|
|
205
|
+
# Fall back to getting new token
|
|
206
|
+
return await self._get_new_token()
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.error("Error refreshing OAuth token", error=str(e))
|
|
210
|
+
# Fall back to getting new token
|
|
211
|
+
return await self._get_new_token()
|
|
212
|
+
|
|
213
|
+
async def _load_token(self) -> Optional[OAuthToken]:
|
|
214
|
+
"""Load token from storage."""
|
|
215
|
+
if not self.token_file or not self.token_file.exists():
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
with open(self.token_file, "r") as f:
|
|
220
|
+
token_data = json.load(f)
|
|
221
|
+
|
|
222
|
+
# Convert timestamp back to datetime
|
|
223
|
+
if "issued_at" in token_data:
|
|
224
|
+
token_data["issued_at"] = datetime.fromisoformat(
|
|
225
|
+
token_data["issued_at"]
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
self.current_token = OAuthToken(**token_data)
|
|
229
|
+
|
|
230
|
+
# Check if token is still valid
|
|
231
|
+
if self.current_token.is_expired:
|
|
232
|
+
logger.info("Stored token is expired")
|
|
233
|
+
self.current_token = None
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
logger.info(
|
|
237
|
+
"Token loaded from storage", expires_at=self.current_token.expires_at
|
|
238
|
+
)
|
|
239
|
+
return self.current_token
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.warning("Failed to load token from storage", error=str(e))
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
async def _save_token(self) -> None:
|
|
246
|
+
"""Save token to storage."""
|
|
247
|
+
if not self.token_file or not self.current_token:
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
token_data = self.current_token.model_dump()
|
|
252
|
+
# Convert datetime to ISO string for JSON serialization
|
|
253
|
+
if "issued_at" in token_data and token_data["issued_at"] is not None:
|
|
254
|
+
token_data["issued_at"] = token_data["issued_at"].isoformat()
|
|
255
|
+
|
|
256
|
+
# Ensure directory exists
|
|
257
|
+
self.token_file.parent.mkdir(parents=True, exist_ok=True)
|
|
258
|
+
|
|
259
|
+
# Write to temporary file first, then rename for atomic operation
|
|
260
|
+
temp_file = self.token_file.with_suffix(".tmp")
|
|
261
|
+
with open(temp_file, "w") as f:
|
|
262
|
+
# Set restrictive permissions on the temp file (owner read/write only)
|
|
263
|
+
import os
|
|
264
|
+
|
|
265
|
+
os.chmod(temp_file, 0o600)
|
|
266
|
+
json.dump(token_data, f, indent=2)
|
|
267
|
+
|
|
268
|
+
# Atomic rename
|
|
269
|
+
temp_file.replace(self.token_file)
|
|
270
|
+
|
|
271
|
+
logger.debug("Token saved to storage with secure permissions")
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.warning("Failed to save token to storage", error=str(e))
|
|
275
|
+
# Clean up temp file if it exists
|
|
276
|
+
temp_file = self.token_file.with_suffix(".tmp")
|
|
277
|
+
if temp_file.exists():
|
|
278
|
+
try:
|
|
279
|
+
temp_file.unlink()
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
def clear_token(self) -> None:
|
|
284
|
+
"""Clear the current token."""
|
|
285
|
+
self.current_token = None
|
|
286
|
+
if self.token_file and self.token_file.exists():
|
|
287
|
+
try:
|
|
288
|
+
self.token_file.unlink()
|
|
289
|
+
logger.info("Token file removed")
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.warning("Failed to remove token file", error=str(e))
|
|
292
|
+
|
|
293
|
+
def get_token_info(self) -> Dict[str, Any]:
|
|
294
|
+
"""Get information about the current token."""
|
|
295
|
+
if not self.current_token:
|
|
296
|
+
return {"status": "no_token"}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
"status": self.current_token.status.value,
|
|
300
|
+
"token_type": self.current_token.token_type,
|
|
301
|
+
"issued_at": (
|
|
302
|
+
self.current_token.issued_at.isoformat()
|
|
303
|
+
if self.current_token.issued_at
|
|
304
|
+
else None
|
|
305
|
+
),
|
|
306
|
+
"expires_at": (
|
|
307
|
+
self.current_token.expires_at.isoformat()
|
|
308
|
+
if self.current_token.expires_at
|
|
309
|
+
else None
|
|
310
|
+
),
|
|
311
|
+
"is_expired": self.current_token.is_expired,
|
|
312
|
+
"has_refresh_token": self.current_token.refresh_token is not None,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class OAuthManager:
|
|
317
|
+
"""High-level OAuth management for the DATAQUERY SDK."""
|
|
318
|
+
|
|
319
|
+
def __init__(self, config: ClientConfig):
|
|
320
|
+
self.config = config
|
|
321
|
+
self.token_manager = TokenManager(config)
|
|
322
|
+
|
|
323
|
+
async def authenticate(self) -> str:
|
|
324
|
+
"""
|
|
325
|
+
Authenticate and get a valid Bearer token.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Bearer token string
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
AuthenticationError: If authentication fails
|
|
332
|
+
"""
|
|
333
|
+
token = await self.token_manager.get_valid_token()
|
|
334
|
+
if not token:
|
|
335
|
+
raise AuthenticationError("Failed to obtain valid authentication token")
|
|
336
|
+
return token
|
|
337
|
+
|
|
338
|
+
async def get_headers(self) -> Dict[str, str]:
|
|
339
|
+
"""
|
|
340
|
+
Get headers with authentication token.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Dictionary with Authorization header
|
|
344
|
+
"""
|
|
345
|
+
token = await self.authenticate()
|
|
346
|
+
return {"Authorization": token}
|
|
347
|
+
|
|
348
|
+
def is_authenticated(self) -> bool:
|
|
349
|
+
"""Check if authentication is configured."""
|
|
350
|
+
return self.config.has_bearer_token or self.config.has_oauth_credentials
|
|
351
|
+
|
|
352
|
+
def get_auth_info(self) -> Dict[str, Any]:
|
|
353
|
+
"""Get authentication configuration information."""
|
|
354
|
+
return {
|
|
355
|
+
"oauth_enabled": self.config.oauth_enabled,
|
|
356
|
+
"has_oauth_credentials": self.config.has_oauth_credentials,
|
|
357
|
+
"has_bearer_token": self.config.has_bearer_token,
|
|
358
|
+
"oauth_token_url": self.config.oauth_token_url,
|
|
359
|
+
"grant_type": self.config.grant_type,
|
|
360
|
+
"token_info": self.token_manager.get_token_info(),
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
def clear_authentication(self) -> None:
|
|
364
|
+
"""Clear all authentication data."""
|
|
365
|
+
self.token_manager.clear_token()
|
|
366
|
+
logger.info("Authentication data cleared")
|
|
367
|
+
|
|
368
|
+
async def test_authentication(self) -> bool:
|
|
369
|
+
"""
|
|
370
|
+
Test if authentication is working.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
True if authentication is successful
|
|
374
|
+
"""
|
|
375
|
+
try:
|
|
376
|
+
await self.authenticate()
|
|
377
|
+
return True
|
|
378
|
+
except Exception as e:
|
|
379
|
+
logger.error("Authentication test failed", error=str(e))
|
|
380
|
+
return False
|