adss 0.1__py3-none-any.whl → 1.1__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/adss_manager.py +53 -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/executors/async_query.py +4 -3
- adss/executors/sync_query.py +9 -3
- 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/table.py +295 -0
- adss/utils/__init__.py +0 -0
- adss/utils/format_table.py +115 -0
- adss/utils.py +107 -0
- adss-1.1.dist-info/LICENSE +11 -0
- {adss-0.1.dist-info → adss-1.1.dist-info}/METADATA +2 -2
- adss-1.1.dist-info/RECORD +30 -0
- {adss-0.1.dist-info → adss-1.1.dist-info}/WHEEL +1 -1
- adss-0.1.dist-info/RECORD +0 -11
- {adss-0.1.dist-info → adss-1.1.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 ..exceptions import (
|
8
|
+
AuthenticationError, ResourceNotFoundError, PermissionDeniedError,
|
9
|
+
ValidationError
|
10
|
+
)
|
11
|
+
from ..utils import handle_response_errors
|
12
|
+
from ..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/executors/async_query.py
CHANGED
@@ -2,7 +2,9 @@ from astropy.io.votable import from_table, writeto
|
|
2
2
|
from xml.dom import minidom
|
3
3
|
from astropy.table import Table
|
4
4
|
|
5
|
+
|
5
6
|
from adss.variables import BASEURL
|
7
|
+
from adss.utils.format_table import format_result_table
|
6
8
|
import requests
|
7
9
|
import os
|
8
10
|
|
@@ -32,7 +34,7 @@ def execute_async(query, table_upload=None, refresh_rate=5):
|
|
32
34
|
"query": query,
|
33
35
|
"format": 'csv'
|
34
36
|
}
|
35
|
-
|
37
|
+
|
36
38
|
if str(type(table_upload)) != "<class 'NoneType'>":
|
37
39
|
if 'astropy.table' in str(type(table_upload)):
|
38
40
|
if len(table_upload) > 6000:
|
@@ -76,7 +78,6 @@ def execute_async(query, table_upload=None, refresh_rate=5):
|
|
76
78
|
writeto(table_upload, IObytes)
|
77
79
|
IObytes.seek(0)
|
78
80
|
|
79
|
-
|
80
81
|
else:
|
81
82
|
return 'Table type not supported'
|
82
83
|
|
@@ -109,7 +110,7 @@ def execute_async(query, table_upload=None, refresh_rate=5):
|
|
109
110
|
|
110
111
|
res = requests.get(link)
|
111
112
|
|
112
|
-
return Table.read(io.BytesIO(res.content), format="csv")
|
113
|
+
return format_result_table(Table.read(io.BytesIO(res.content), format="csv"))
|
113
114
|
|
114
115
|
if process == 'ERROR':
|
115
116
|
item = xmldoc.getElementsByTagName('message')[0]
|
adss/executors/sync_query.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
from adss.variables import BASEURL
|
2
|
+
from adss.utils.format_table import format_result_table
|
2
3
|
|
3
4
|
from astropy.table import Table
|
4
5
|
from xml.dom import minidom
|
@@ -6,9 +7,11 @@ import requests
|
|
6
7
|
import os
|
7
8
|
import io
|
8
9
|
|
10
|
+
from requests.exceptions import Timeout
|
11
|
+
|
9
12
|
sync_url = os.path.join(BASEURL, "sync")
|
10
13
|
|
11
|
-
def execute_sync(query):
|
14
|
+
def execute_sync(query, timeout=20):
|
12
15
|
data = {
|
13
16
|
"request": "doQuery",
|
14
17
|
"version": "1.0",
|
@@ -19,7 +22,10 @@ def execute_sync(query):
|
|
19
22
|
}
|
20
23
|
|
21
24
|
# Make request to TAP server
|
22
|
-
|
25
|
+
try:
|
26
|
+
res = requests.post(sync_url, data=data, timeout=20)
|
27
|
+
except Timeout:
|
28
|
+
raise Exception("Request to TAP server timed out, for large queries use async query")
|
23
29
|
|
24
30
|
# Handle errors from TAP response
|
25
31
|
if res.status_code != 200:
|
@@ -31,6 +37,6 @@ def execute_sync(query):
|
|
31
37
|
raise Exception(f"ADQL Query Error: {error_message}")
|
32
38
|
|
33
39
|
# Convert CSV response to Astropy Table
|
34
|
-
return Table.read(io.BytesIO(res.content), format="csv")
|
40
|
+
return format_result_table(Table.read(io.BytesIO(res.content), format="csv"))
|
35
41
|
|
36
42
|
|
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
|