adss 1.0__py3-none-any.whl → 1.2__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.
- adss/__init__.py +24 -0
- adss/auth.py +121 -0
- adss/client.py +671 -0
- adss/endpoints/__init__.py +14 -0
- adss/endpoints/admin.py +433 -0
- adss/endpoints/images.py +898 -0
- adss/endpoints/metadata.py +216 -0
- adss/endpoints/queries.py +498 -0
- adss/endpoints/users.py +311 -0
- adss/exceptions.py +57 -0
- adss/models/__init__.py +13 -0
- adss/models/metadata.py +138 -0
- adss/models/query.py +134 -0
- adss/models/user.py +123 -0
- adss/utils.py +107 -0
- {adss-1.0.dist-info → adss-1.2.dist-info}/METADATA +1 -1
- adss-1.2.dist-info/RECORD +30 -0
- {adss-1.0.dist-info → adss-1.2.dist-info}/WHEEL +1 -1
- adss-1.0.dist-info/RECORD +0 -16
- {adss-1.0.dist-info → adss-1.2.dist-info}/LICENSE +0 -0
- {adss-1.0.dist-info → adss-1.2.dist-info}/top_level.txt +0 -0
adss/endpoints/users.py
ADDED
@@ -0,0 +1,311 @@
|
|
1
|
+
"""
|
2
|
+
User management functionality for the Astronomy TAP Client.
|
3
|
+
"""
|
4
|
+
import requests
|
5
|
+
from typing import Dict, List, Optional, Any
|
6
|
+
|
7
|
+
from adss.exceptions import (
|
8
|
+
AuthenticationError, ResourceNotFoundError, PermissionDeniedError,
|
9
|
+
ValidationError
|
10
|
+
)
|
11
|
+
from adss.utils import handle_response_errors
|
12
|
+
from adss.models.user import User, Role
|
13
|
+
|
14
|
+
|
15
|
+
class UsersEndpoint:
|
16
|
+
"""
|
17
|
+
Handles user management operations.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self, base_url: str, auth_manager):
|
21
|
+
"""
|
22
|
+
Initialize the Users endpoint.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
base_url: The base URL of the API server
|
26
|
+
auth_manager: Authentication manager providing auth headers
|
27
|
+
"""
|
28
|
+
self.base_url = base_url.rstrip('/')
|
29
|
+
self.auth_manager = auth_manager
|
30
|
+
|
31
|
+
def register(self, username: str, email: str, password: str, full_name: Optional[str] = None, **kwargs) -> User:
|
32
|
+
"""
|
33
|
+
Register a new user account.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
username: Desired username
|
37
|
+
email: User's email address
|
38
|
+
password: User's password (will be validated for strength)
|
39
|
+
full_name: Optional full name
|
40
|
+
**kwargs: Additional keyword arguments to pass to the request (e.g., verify=False)
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
The newly created User object
|
44
|
+
|
45
|
+
Raises:
|
46
|
+
ValidationError: If input validation fails (e.g., password too weak)
|
47
|
+
"""
|
48
|
+
data = {
|
49
|
+
"username": username,
|
50
|
+
"email": email,
|
51
|
+
"password": password
|
52
|
+
}
|
53
|
+
|
54
|
+
if full_name:
|
55
|
+
data["full_name"] = full_name
|
56
|
+
|
57
|
+
try:
|
58
|
+
response = self.auth_manager.request(
|
59
|
+
method="POST",
|
60
|
+
url="/adss/v1/users",
|
61
|
+
json=data,
|
62
|
+
**kwargs
|
63
|
+
)
|
64
|
+
handle_response_errors(response)
|
65
|
+
|
66
|
+
user_data = response.json()
|
67
|
+
return User.from_dict(user_data)
|
68
|
+
|
69
|
+
except Exception as e:
|
70
|
+
if hasattr(e, 'response') and e.response.status_code == 400:
|
71
|
+
try:
|
72
|
+
error_data = e.response.json()
|
73
|
+
raise ValidationError(
|
74
|
+
f"User registration failed: {error_data.get('detail', str(e))}",
|
75
|
+
error_data.get('errors')
|
76
|
+
)
|
77
|
+
except (ValueError, AttributeError):
|
78
|
+
pass
|
79
|
+
raise
|
80
|
+
|
81
|
+
def get_me(self, **kwargs) -> User:
|
82
|
+
"""
|
83
|
+
Get information about the currently authenticated user.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
**kwargs: Additional keyword arguments to pass to the request (e.g., verify=False)
|
87
|
+
|
88
|
+
Returns:
|
89
|
+
The current User object
|
90
|
+
|
91
|
+
Raises:
|
92
|
+
AuthenticationError: If not authenticated
|
93
|
+
"""
|
94
|
+
try:
|
95
|
+
response = self.auth_manager.request(
|
96
|
+
method="GET",
|
97
|
+
url="/adss/v1/users/me",
|
98
|
+
auth_required=True,
|
99
|
+
**kwargs
|
100
|
+
)
|
101
|
+
handle_response_errors(response)
|
102
|
+
|
103
|
+
user_data = response.json()
|
104
|
+
return User.from_dict(user_data)
|
105
|
+
|
106
|
+
except Exception as e:
|
107
|
+
if hasattr(e, 'response') and e.response.status_code == 401:
|
108
|
+
raise AuthenticationError("Authentication required")
|
109
|
+
raise
|
110
|
+
|
111
|
+
def update_profile(self,
|
112
|
+
email: Optional[str] = None,
|
113
|
+
full_name: Optional[str] = None,
|
114
|
+
**kwargs) -> User:
|
115
|
+
"""
|
116
|
+
Update the current user's profile information.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
email: New email address (optional)
|
120
|
+
full_name: New full name (optional)
|
121
|
+
**kwargs: Additional keyword arguments to pass to the request (e.g., verify=False)
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
The updated User object
|
125
|
+
|
126
|
+
Raises:
|
127
|
+
AuthenticationError: If not authenticated
|
128
|
+
ValidationError: If input validation fails
|
129
|
+
"""
|
130
|
+
data = {}
|
131
|
+
if email is not None:
|
132
|
+
data["email"] = email
|
133
|
+
if full_name is not None:
|
134
|
+
data["full_name"] = full_name
|
135
|
+
|
136
|
+
if not data:
|
137
|
+
# Nothing to update
|
138
|
+
return self.get_me(**kwargs)
|
139
|
+
|
140
|
+
try:
|
141
|
+
response = self.auth_manager.request(
|
142
|
+
method="PATCH",
|
143
|
+
url="/adss/v1/users/me",
|
144
|
+
json=data,
|
145
|
+
auth_required=True,
|
146
|
+
**kwargs
|
147
|
+
)
|
148
|
+
handle_response_errors(response)
|
149
|
+
|
150
|
+
user_data = response.json()
|
151
|
+
return User.from_dict(user_data)
|
152
|
+
|
153
|
+
except Exception as e:
|
154
|
+
if hasattr(e, 'response') and e.response.status_code == 400:
|
155
|
+
try:
|
156
|
+
error_data = e.response.json()
|
157
|
+
raise ValidationError(
|
158
|
+
f"Profile update failed: {error_data.get('detail', str(e))}",
|
159
|
+
error_data.get('errors')
|
160
|
+
)
|
161
|
+
except (ValueError, AttributeError):
|
162
|
+
pass
|
163
|
+
elif hasattr(e, 'response') and e.response.status_code == 401:
|
164
|
+
raise AuthenticationError("Authentication required")
|
165
|
+
raise
|
166
|
+
|
167
|
+
def get_users(self, skip: int = 0, limit: int = 100, **kwargs) -> List[User]:
|
168
|
+
"""
|
169
|
+
Get a list of users (staff only).
|
170
|
+
|
171
|
+
Args:
|
172
|
+
skip: Number of users to skip (for pagination)
|
173
|
+
limit: Maximum number of users to return
|
174
|
+
**kwargs: Additional keyword arguments to pass to the request (e.g., verify=False)
|
175
|
+
|
176
|
+
Returns:
|
177
|
+
List of User objects
|
178
|
+
|
179
|
+
Raises:
|
180
|
+
AuthenticationError: If not authenticated
|
181
|
+
PermissionDeniedError: If not a staff user
|
182
|
+
"""
|
183
|
+
params = {"skip": skip, "limit": limit}
|
184
|
+
|
185
|
+
try:
|
186
|
+
response = self.auth_manager.request(
|
187
|
+
method="GET",
|
188
|
+
url="/adss/v1/users",
|
189
|
+
params=params,
|
190
|
+
auth_required=True,
|
191
|
+
**kwargs
|
192
|
+
)
|
193
|
+
handle_response_errors(response)
|
194
|
+
|
195
|
+
users_data = response.json()
|
196
|
+
return [User.from_dict(user_data) for user_data in users_data]
|
197
|
+
|
198
|
+
except Exception as e:
|
199
|
+
if hasattr(e, 'response'):
|
200
|
+
if e.response.status_code == 401:
|
201
|
+
raise AuthenticationError("Authentication required")
|
202
|
+
elif e.response.status_code == 403:
|
203
|
+
raise PermissionDeniedError("Staff access required")
|
204
|
+
raise
|
205
|
+
|
206
|
+
def get_user(self, user_id: str, **kwargs) -> User:
|
207
|
+
"""
|
208
|
+
Get information about a specific user (staff only).
|
209
|
+
|
210
|
+
Args:
|
211
|
+
user_id: ID of the user to get
|
212
|
+
**kwargs: Additional keyword arguments to pass to the request (e.g., verify=False)
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
User object
|
216
|
+
|
217
|
+
Raises:
|
218
|
+
AuthenticationError: If not authenticated
|
219
|
+
PermissionDeniedError: If not a staff user
|
220
|
+
ResourceNotFoundError: If the user is not found
|
221
|
+
"""
|
222
|
+
try:
|
223
|
+
response = self.auth_manager.request(
|
224
|
+
method="GET",
|
225
|
+
url=f"/adss/v1/users/{user_id}",
|
226
|
+
auth_required=True,
|
227
|
+
**kwargs
|
228
|
+
)
|
229
|
+
handle_response_errors(response)
|
230
|
+
|
231
|
+
user_data = response.json()
|
232
|
+
return User.from_dict(user_data)
|
233
|
+
|
234
|
+
except Exception as e:
|
235
|
+
if hasattr(e, 'response'):
|
236
|
+
if e.response.status_code == 401:
|
237
|
+
raise AuthenticationError("Authentication required")
|
238
|
+
elif e.response.status_code == 403:
|
239
|
+
raise PermissionDeniedError("Staff access required")
|
240
|
+
elif e.response.status_code == 404:
|
241
|
+
raise ResourceNotFoundError(f"User not found: {user_id}")
|
242
|
+
raise
|
243
|
+
|
244
|
+
def update_user(self,
|
245
|
+
user_id: str,
|
246
|
+
email: Optional[str] = None,
|
247
|
+
full_name: Optional[str] = None,
|
248
|
+
is_active: Optional[bool] = None,
|
249
|
+
**kwargs) -> User:
|
250
|
+
"""
|
251
|
+
Update a user's information (staff only).
|
252
|
+
|
253
|
+
Args:
|
254
|
+
user_id: ID of the user to update
|
255
|
+
email: New email address (optional)
|
256
|
+
full_name: New full name (optional)
|
257
|
+
is_active: New active status (optional)
|
258
|
+
**kwargs: Additional keyword arguments to pass to the request (e.g., verify=False)
|
259
|
+
|
260
|
+
Returns:
|
261
|
+
The updated User object
|
262
|
+
|
263
|
+
Raises:
|
264
|
+
AuthenticationError: If not authenticated
|
265
|
+
PermissionDeniedError: If not a staff user
|
266
|
+
ResourceNotFoundError: If the user is not found
|
267
|
+
ValidationError: If input validation fails
|
268
|
+
"""
|
269
|
+
data = {}
|
270
|
+
if email is not None:
|
271
|
+
data["email"] = email
|
272
|
+
if full_name is not None:
|
273
|
+
data["full_name"] = full_name
|
274
|
+
if is_active is not None:
|
275
|
+
data["is_active"] = is_active
|
276
|
+
|
277
|
+
if not data:
|
278
|
+
# Nothing to update
|
279
|
+
return self.get_user(user_id, **kwargs)
|
280
|
+
|
281
|
+
try:
|
282
|
+
response = self.auth_manager.request(
|
283
|
+
method="PATCH",
|
284
|
+
url=f"/adss/v1/users/{user_id}",
|
285
|
+
json=data,
|
286
|
+
auth_required=True,
|
287
|
+
**kwargs
|
288
|
+
)
|
289
|
+
handle_response_errors(response)
|
290
|
+
|
291
|
+
user_data = response.json()
|
292
|
+
return User.from_dict(user_data)
|
293
|
+
|
294
|
+
except Exception as e:
|
295
|
+
if hasattr(e, 'response'):
|
296
|
+
if e.response.status_code == 401:
|
297
|
+
raise AuthenticationError("Authentication required")
|
298
|
+
elif e.response.status_code == 403:
|
299
|
+
raise PermissionDeniedError("Staff access required")
|
300
|
+
elif e.response.status_code == 404:
|
301
|
+
raise ResourceNotFoundError(f"User not found: {user_id}")
|
302
|
+
elif e.response.status_code == 400:
|
303
|
+
try:
|
304
|
+
error_data = e.response.json()
|
305
|
+
raise ValidationError(
|
306
|
+
f"User update failed: {error_data.get('detail', str(e))}",
|
307
|
+
error_data.get('errors')
|
308
|
+
)
|
309
|
+
except (ValueError, AttributeError):
|
310
|
+
pass
|
311
|
+
raise
|
adss/exceptions.py
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
"""
|
2
|
+
Custom exceptions for the Astronomy TAP Client.
|
3
|
+
"""
|
4
|
+
|
5
|
+
class ADSSClientError(Exception):
|
6
|
+
"""Base exception for all TAP client errors."""
|
7
|
+
|
8
|
+
def __init__(self, message, response=None):
|
9
|
+
self.message = message
|
10
|
+
self.response = response
|
11
|
+
super().__init__(self.message)
|
12
|
+
|
13
|
+
|
14
|
+
class AuthenticationError(ADSSClientError):
|
15
|
+
"""Exception raised for authentication failures."""
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
class PermissionDeniedError(ADSSClientError):
|
20
|
+
"""Exception raised when the user doesn't have sufficient permissions."""
|
21
|
+
pass
|
22
|
+
|
23
|
+
|
24
|
+
class ResourceNotFoundError(ADSSClientError):
|
25
|
+
"""Exception raised when a requested resource is not found."""
|
26
|
+
pass
|
27
|
+
|
28
|
+
|
29
|
+
class QueryExecutionError(ADSSClientError):
|
30
|
+
"""Exception raised when a query fails to execute."""
|
31
|
+
|
32
|
+
def __init__(self, message, query=None, response=None):
|
33
|
+
self.query = query
|
34
|
+
super().__init__(message, response)
|
35
|
+
|
36
|
+
|
37
|
+
class ValidationError(ADSSClientError):
|
38
|
+
"""Exception raised when input validation fails."""
|
39
|
+
|
40
|
+
def __init__(self, message, errors=None, response=None):
|
41
|
+
self.errors = errors or {}
|
42
|
+
super().__init__(message, response)
|
43
|
+
|
44
|
+
|
45
|
+
class ConnectionError(ADSSClientError):
|
46
|
+
"""Exception raised when connection to the API server fails."""
|
47
|
+
pass
|
48
|
+
|
49
|
+
|
50
|
+
class TimeoutError(ADSSClientError):
|
51
|
+
"""Exception raised when a request times out."""
|
52
|
+
pass
|
53
|
+
|
54
|
+
|
55
|
+
class ServerError(ADSSClientError):
|
56
|
+
"""Exception raised when the server returns a 5xx error."""
|
57
|
+
pass
|
adss/models/__init__.py
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
"""
|
2
|
+
Data models for the Astronomy TAP Client.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .user import User, Role, SchemaPermission, TablePermission, RolePermissions
|
6
|
+
from .query import Query, QueryResult
|
7
|
+
from .metadata import Column, Table, Schema, DatabaseMetadata
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
'User', 'Role', 'SchemaPermission', 'TablePermission', 'RolePermissions',
|
11
|
+
'Query', 'QueryResult',
|
12
|
+
'Column', 'Table', 'Schema', 'DatabaseMetadata'
|
13
|
+
]
|
adss/models/metadata.py
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
"""
|
2
|
+
Database metadata models for the Astronomy TAP Client.
|
3
|
+
"""
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
from typing import List, Dict, Any, Optional
|
6
|
+
|
7
|
+
|
8
|
+
@dataclass
|
9
|
+
class Column:
|
10
|
+
"""
|
11
|
+
Represents a database column and its metadata.
|
12
|
+
"""
|
13
|
+
name: str
|
14
|
+
data_type: str
|
15
|
+
is_nullable: bool
|
16
|
+
|
17
|
+
@classmethod
|
18
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'Column':
|
19
|
+
"""Create a Column object from a dictionary."""
|
20
|
+
return cls(
|
21
|
+
name=data.get('name'),
|
22
|
+
data_type=data.get('data_type'),
|
23
|
+
is_nullable=data.get('is_nullable', False)
|
24
|
+
)
|
25
|
+
|
26
|
+
|
27
|
+
@dataclass
|
28
|
+
class Table:
|
29
|
+
"""
|
30
|
+
Represents a database table and its columns.
|
31
|
+
"""
|
32
|
+
name: str
|
33
|
+
columns: List[Column] = field(default_factory=list)
|
34
|
+
|
35
|
+
@classmethod
|
36
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'Table':
|
37
|
+
"""Create a Table object from a dictionary."""
|
38
|
+
columns = [
|
39
|
+
Column.from_dict(col_data)
|
40
|
+
for col_data in data.get('columns', [])
|
41
|
+
]
|
42
|
+
|
43
|
+
return cls(
|
44
|
+
name=data.get('name'),
|
45
|
+
columns=columns
|
46
|
+
)
|
47
|
+
|
48
|
+
def get_column(self, name: str) -> Optional[Column]:
|
49
|
+
"""Get a column by name."""
|
50
|
+
for column in self.columns:
|
51
|
+
if column.name == name:
|
52
|
+
return column
|
53
|
+
return None
|
54
|
+
|
55
|
+
def has_column(self, name: str) -> bool:
|
56
|
+
"""Check if the table has a column with the given name."""
|
57
|
+
return any(col.name == name for col in self.columns)
|
58
|
+
|
59
|
+
def column_names(self) -> List[str]:
|
60
|
+
"""Get a list of all column names."""
|
61
|
+
return [col.name for col in self.columns]
|
62
|
+
|
63
|
+
|
64
|
+
@dataclass
|
65
|
+
class Schema:
|
66
|
+
"""
|
67
|
+
Represents a database schema and its tables.
|
68
|
+
"""
|
69
|
+
name: str
|
70
|
+
tables: List[Table] = field(default_factory=list)
|
71
|
+
|
72
|
+
@classmethod
|
73
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'Schema':
|
74
|
+
"""Create a Schema object from a dictionary."""
|
75
|
+
tables = [
|
76
|
+
Table.from_dict(table_data)
|
77
|
+
for table_data in data.get('tables', [])
|
78
|
+
]
|
79
|
+
|
80
|
+
return cls(
|
81
|
+
name=data.get('name'),
|
82
|
+
tables=tables
|
83
|
+
)
|
84
|
+
|
85
|
+
def get_table(self, name: str) -> Optional[Table]:
|
86
|
+
"""Get a table by name."""
|
87
|
+
for table in self.tables:
|
88
|
+
if table.name == name:
|
89
|
+
return table
|
90
|
+
return None
|
91
|
+
|
92
|
+
def has_table(self, name: str) -> bool:
|
93
|
+
"""Check if the schema has a table with the given name."""
|
94
|
+
return any(table.name == name for table in self.tables)
|
95
|
+
|
96
|
+
def table_names(self) -> List[str]:
|
97
|
+
"""Get a list of all table names in the schema."""
|
98
|
+
return [table.name for table in self.tables]
|
99
|
+
|
100
|
+
|
101
|
+
@dataclass
|
102
|
+
class DatabaseMetadata:
|
103
|
+
"""
|
104
|
+
Represents database metadata, including schemas and their tables.
|
105
|
+
"""
|
106
|
+
schemas: List[Schema] = field(default_factory=list)
|
107
|
+
|
108
|
+
@classmethod
|
109
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'DatabaseMetadata':
|
110
|
+
"""Create a DatabaseMetadata object from a dictionary."""
|
111
|
+
schemas = [
|
112
|
+
Schema.from_dict(schema_data)
|
113
|
+
for schema_data in data.get('schemas', [])
|
114
|
+
]
|
115
|
+
|
116
|
+
return cls(schemas=schemas)
|
117
|
+
|
118
|
+
def get_schema(self, name: str) -> Optional[Schema]:
|
119
|
+
"""Get a schema by name."""
|
120
|
+
for schema in self.schemas:
|
121
|
+
if schema.name == name:
|
122
|
+
return schema
|
123
|
+
return None
|
124
|
+
|
125
|
+
def has_schema(self, name: str) -> bool:
|
126
|
+
"""Check if the database has a schema with the given name."""
|
127
|
+
return any(schema.name == name for schema in self.schemas)
|
128
|
+
|
129
|
+
def schema_names(self) -> List[str]:
|
130
|
+
"""Get a list of all schema names."""
|
131
|
+
return [schema.name for schema in self.schemas]
|
132
|
+
|
133
|
+
def get_table(self, schema_name: str, table_name: str) -> Optional[Table]:
|
134
|
+
"""Get a table by schema and table name."""
|
135
|
+
schema = self.get_schema(schema_name)
|
136
|
+
if schema:
|
137
|
+
return schema.get_table(table_name)
|
138
|
+
return None
|
adss/models/query.py
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
"""
|
2
|
+
Query-related data models for the Astronomy TAP Client.
|
3
|
+
"""
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from typing import Dict, Optional, Any, List
|
6
|
+
from datetime import datetime
|
7
|
+
import pandas as pd
|
8
|
+
|
9
|
+
from adss.utils import parse_datetime
|
10
|
+
|
11
|
+
|
12
|
+
@dataclass
|
13
|
+
class Query:
|
14
|
+
"""
|
15
|
+
Represents a database query and its metadata.
|
16
|
+
"""
|
17
|
+
id: str
|
18
|
+
query_text: str
|
19
|
+
status: str # 'PENDING', 'QUEUED', 'RUNNING', 'COMPLETED', 'ERROR'
|
20
|
+
created_at: datetime
|
21
|
+
mode: str = 'adql' # 'adql' or 'sql'
|
22
|
+
user_id: Optional[str] = None
|
23
|
+
completed_at: Optional[datetime] = None
|
24
|
+
result_url: Optional[str] = None
|
25
|
+
error: Optional[str] = None
|
26
|
+
execution_time_ms: Optional[int] = None
|
27
|
+
row_count: Optional[int] = None
|
28
|
+
position_in_queue: Optional[int] = None
|
29
|
+
expires_at: Optional[datetime] = None
|
30
|
+
query_metadata: Optional[Dict[str, Any]] = None
|
31
|
+
|
32
|
+
@classmethod
|
33
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'Query':
|
34
|
+
"""Create a Query object from a dictionary."""
|
35
|
+
query_id = data.get('id')
|
36
|
+
query_text = data.get('query_text')
|
37
|
+
status = data.get('status')
|
38
|
+
mode = data.get('mode', 'adql')
|
39
|
+
user_id = data.get('user_id')
|
40
|
+
|
41
|
+
created_at = parse_datetime(data.get('created_at'))
|
42
|
+
completed_at = parse_datetime(data.get('completed_at'))
|
43
|
+
expires_at = parse_datetime(data.get('expires_at'))
|
44
|
+
|
45
|
+
result_url = data.get('result_url')
|
46
|
+
error = data.get('error')
|
47
|
+
execution_time_ms = data.get('execution_time_ms')
|
48
|
+
row_count = data.get('row_count')
|
49
|
+
position_in_queue = data.get('position_in_queue')
|
50
|
+
query_metadata = data.get('query_metadata')
|
51
|
+
|
52
|
+
return cls(
|
53
|
+
id=query_id,
|
54
|
+
query_text=query_text,
|
55
|
+
status=status,
|
56
|
+
mode=mode,
|
57
|
+
user_id=user_id,
|
58
|
+
created_at=created_at,
|
59
|
+
completed_at=completed_at,
|
60
|
+
result_url=result_url,
|
61
|
+
error=error,
|
62
|
+
execution_time_ms=execution_time_ms,
|
63
|
+
row_count=row_count,
|
64
|
+
position_in_queue=position_in_queue,
|
65
|
+
expires_at=expires_at,
|
66
|
+
query_metadata=query_metadata
|
67
|
+
)
|
68
|
+
|
69
|
+
@property
|
70
|
+
def is_complete(self) -> bool:
|
71
|
+
"""Check if the query has completed (successfully or with error)."""
|
72
|
+
return self.status in ['COMPLETED', 'ERROR']
|
73
|
+
|
74
|
+
@property
|
75
|
+
def is_running(self) -> bool:
|
76
|
+
"""Check if the query is currently running."""
|
77
|
+
return self.status == 'RUNNING'
|
78
|
+
|
79
|
+
@property
|
80
|
+
def is_queued(self) -> bool:
|
81
|
+
"""Check if the query is queued."""
|
82
|
+
return self.status == 'QUEUED'
|
83
|
+
|
84
|
+
@property
|
85
|
+
def is_successful(self) -> bool:
|
86
|
+
"""Check if the query completed successfully."""
|
87
|
+
return self.status == 'COMPLETED'
|
88
|
+
|
89
|
+
@property
|
90
|
+
def is_failed(self) -> bool:
|
91
|
+
"""Check if the query failed."""
|
92
|
+
return self.status == 'ERROR'
|
93
|
+
|
94
|
+
|
95
|
+
@dataclass
|
96
|
+
class QueryResult:
|
97
|
+
"""
|
98
|
+
Represents the result of a query, including the data and metadata.
|
99
|
+
"""
|
100
|
+
query: Query
|
101
|
+
data: pd.DataFrame
|
102
|
+
execution_time_ms: Optional[int] = None
|
103
|
+
row_count: Optional[int] = None
|
104
|
+
column_count: Optional[int] = None
|
105
|
+
|
106
|
+
def to_csv(self, path: str, **kwargs) -> None:
|
107
|
+
"""Save the query result to a CSV file."""
|
108
|
+
self.data.to_csv(path, **kwargs)
|
109
|
+
|
110
|
+
def to_parquet(self, path: str, **kwargs) -> None:
|
111
|
+
"""Save the query result to a Parquet file."""
|
112
|
+
self.data.to_parquet(path, **kwargs)
|
113
|
+
|
114
|
+
def to_json(self, path: str = None, **kwargs) -> Optional[str]:
|
115
|
+
"""
|
116
|
+
Convert the query result to JSON.
|
117
|
+
If path is provided, saves to file, otherwise returns a JSON string.
|
118
|
+
"""
|
119
|
+
if path:
|
120
|
+
self.data.to_json(path, **kwargs)
|
121
|
+
return None
|
122
|
+
return self.data.to_json(**kwargs)
|
123
|
+
|
124
|
+
def head(self, n: int = 5) -> pd.DataFrame:
|
125
|
+
"""Return the first n rows of the result."""
|
126
|
+
return self.data.head(n)
|
127
|
+
|
128
|
+
def tail(self, n: int = 5) -> pd.DataFrame:
|
129
|
+
"""Return the last n rows of the result."""
|
130
|
+
return self.data.tail(n)
|
131
|
+
|
132
|
+
def describe(self) -> pd.DataFrame:
|
133
|
+
"""Return summary statistics of the result."""
|
134
|
+
return self.data.describe()
|