arc-tsdb-client 0.1.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.
- arc_client/__init__.py +38 -0
- arc_client/async_client.py +179 -0
- arc_client/auth/__init__.py +9 -0
- arc_client/auth/async_manager.py +174 -0
- arc_client/auth/manager.py +186 -0
- arc_client/client.py +179 -0
- arc_client/config.py +28 -0
- arc_client/dataframe/__init__.py +1 -0
- arc_client/exceptions.py +61 -0
- arc_client/http/__init__.py +11 -0
- arc_client/http/async_http.py +136 -0
- arc_client/http/base.py +90 -0
- arc_client/http/sync_http.py +136 -0
- arc_client/ingestion/__init__.py +47 -0
- arc_client/ingestion/async_buffered.py +163 -0
- arc_client/ingestion/async_writer.py +209 -0
- arc_client/ingestion/buffered.py +219 -0
- arc_client/ingestion/compression.py +67 -0
- arc_client/ingestion/line_protocol.py +261 -0
- arc_client/ingestion/msgpack.py +349 -0
- arc_client/ingestion/writer.py +326 -0
- arc_client/management/__init__.py +17 -0
- arc_client/management/async_continuous_query.py +191 -0
- arc_client/management/async_delete.py +60 -0
- arc_client/management/async_retention.py +153 -0
- arc_client/management/continuous_query.py +191 -0
- arc_client/management/delete.py +63 -0
- arc_client/management/retention.py +153 -0
- arc_client/models/__init__.py +43 -0
- arc_client/models/auth.py +96 -0
- arc_client/models/common.py +22 -0
- arc_client/models/continuous_query.py +64 -0
- arc_client/models/delete.py +49 -0
- arc_client/models/query.py +67 -0
- arc_client/models/retention.py +51 -0
- arc_client/py.typed +0 -0
- arc_client/query/__init__.py +9 -0
- arc_client/query/async_executor.py +297 -0
- arc_client/query/executor.py +336 -0
- arc_tsdb_client-0.1.0.dist-info/METADATA +410 -0
- arc_tsdb_client-0.1.0.dist-info/RECORD +42 -0
- arc_tsdb_client-0.1.0.dist-info/WHEEL +4 -0
arc_client/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Arc Python SDK - High-performance client for Arc time-series database."""
|
|
2
|
+
|
|
3
|
+
from arc_client.async_client import AsyncArcClient
|
|
4
|
+
from arc_client.client import ArcClient
|
|
5
|
+
from arc_client.config import ClientConfig
|
|
6
|
+
from arc_client.exceptions import (
|
|
7
|
+
ArcAuthenticationError,
|
|
8
|
+
ArcConnectionError,
|
|
9
|
+
ArcError,
|
|
10
|
+
ArcIngestionError,
|
|
11
|
+
ArcNotFoundError,
|
|
12
|
+
ArcQueryError,
|
|
13
|
+
ArcRateLimitError,
|
|
14
|
+
ArcServerError,
|
|
15
|
+
ArcValidationError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
# Clients
|
|
22
|
+
"ArcClient",
|
|
23
|
+
"AsyncArcClient",
|
|
24
|
+
# Config
|
|
25
|
+
"ClientConfig",
|
|
26
|
+
# Exceptions
|
|
27
|
+
"ArcError",
|
|
28
|
+
"ArcConnectionError",
|
|
29
|
+
"ArcAuthenticationError",
|
|
30
|
+
"ArcQueryError",
|
|
31
|
+
"ArcIngestionError",
|
|
32
|
+
"ArcValidationError",
|
|
33
|
+
"ArcNotFoundError",
|
|
34
|
+
"ArcRateLimitError",
|
|
35
|
+
"ArcServerError",
|
|
36
|
+
# Version
|
|
37
|
+
"__version__",
|
|
38
|
+
]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Asynchronous Arc client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from arc_client.config import ClientConfig
|
|
8
|
+
from arc_client.http.async_http import AsyncHTTPClient
|
|
9
|
+
from arc_client.models.common import HealthResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncArcClient:
|
|
13
|
+
"""Asynchronous client for Arc time-series database.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
>>> async with AsyncArcClient(host="localhost", token="my-token") as client:
|
|
17
|
+
... health = await client.health()
|
|
18
|
+
... print(health.status)
|
|
19
|
+
...
|
|
20
|
+
... await client.write.write_columnar(
|
|
21
|
+
... measurement="cpu",
|
|
22
|
+
... columns={
|
|
23
|
+
... "time": [1633024800000000, 1633024801000000],
|
|
24
|
+
... "host": ["server01", "server01"],
|
|
25
|
+
... "usage": [45.2, 47.8],
|
|
26
|
+
... },
|
|
27
|
+
... )
|
|
28
|
+
...
|
|
29
|
+
... df = await client.query.query_pandas("SELECT * FROM cpu LIMIT 100")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
host: str = "localhost",
|
|
35
|
+
port: int = 8000,
|
|
36
|
+
token: Optional[str] = None,
|
|
37
|
+
database: str = "default",
|
|
38
|
+
timeout: float = 30.0,
|
|
39
|
+
compression: bool = True,
|
|
40
|
+
ssl: bool = False,
|
|
41
|
+
verify_ssl: bool = True,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Initialize the async Arc client.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
host: Arc server hostname.
|
|
47
|
+
port: Arc server port.
|
|
48
|
+
token: API token for authentication.
|
|
49
|
+
database: Default database name.
|
|
50
|
+
timeout: Request timeout in seconds.
|
|
51
|
+
compression: Enable gzip compression for writes.
|
|
52
|
+
ssl: Use HTTPS.
|
|
53
|
+
verify_ssl: Verify SSL certificates.
|
|
54
|
+
"""
|
|
55
|
+
self._config = ClientConfig(
|
|
56
|
+
host=host,
|
|
57
|
+
port=port,
|
|
58
|
+
token=token,
|
|
59
|
+
database=database,
|
|
60
|
+
timeout=timeout,
|
|
61
|
+
compression=compression,
|
|
62
|
+
ssl=ssl,
|
|
63
|
+
verify_ssl=verify_ssl,
|
|
64
|
+
)
|
|
65
|
+
self._http: Optional[AsyncHTTPClient] = None
|
|
66
|
+
|
|
67
|
+
# Lazy-initialized sub-clients
|
|
68
|
+
self._write: Any = None
|
|
69
|
+
self._query: Any = None
|
|
70
|
+
self._auth: Any = None
|
|
71
|
+
self._retention: Any = None
|
|
72
|
+
self._continuous_queries: Any = None
|
|
73
|
+
self._delete: Any = None
|
|
74
|
+
|
|
75
|
+
def _get_http(self) -> AsyncHTTPClient:
|
|
76
|
+
"""Get or create the HTTP client."""
|
|
77
|
+
if self._http is None:
|
|
78
|
+
self._http = AsyncHTTPClient(self._config)
|
|
79
|
+
return self._http
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def config(self) -> ClientConfig:
|
|
83
|
+
"""Get the client configuration."""
|
|
84
|
+
return self._config
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def write(self) -> Any:
|
|
88
|
+
"""Get the async write client for data ingestion."""
|
|
89
|
+
if self._write is None:
|
|
90
|
+
from arc_client.ingestion.async_writer import AsyncWriteClient
|
|
91
|
+
|
|
92
|
+
self._write = AsyncWriteClient(self._get_http(), self._config)
|
|
93
|
+
return self._write
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def query(self) -> Any:
|
|
97
|
+
"""Get the async query client for SQL queries."""
|
|
98
|
+
if self._query is None:
|
|
99
|
+
from arc_client.query.async_executor import AsyncQueryClient
|
|
100
|
+
|
|
101
|
+
self._query = AsyncQueryClient(self._get_http(), self._config)
|
|
102
|
+
return self._query
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def auth(self) -> Any:
|
|
106
|
+
"""Get the async auth client for token management."""
|
|
107
|
+
if self._auth is None:
|
|
108
|
+
from arc_client.auth.async_manager import AsyncAuthClient
|
|
109
|
+
|
|
110
|
+
self._auth = AsyncAuthClient(self._get_http(), self._config)
|
|
111
|
+
return self._auth
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def retention(self) -> Any:
|
|
115
|
+
"""Get the async retention client for policy management."""
|
|
116
|
+
if self._retention is None:
|
|
117
|
+
from arc_client.management.async_retention import AsyncRetentionClient
|
|
118
|
+
|
|
119
|
+
self._retention = AsyncRetentionClient(self._get_http(), self._config)
|
|
120
|
+
return self._retention
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def continuous_queries(self) -> Any:
|
|
124
|
+
"""Get the async continuous queries client."""
|
|
125
|
+
if self._continuous_queries is None:
|
|
126
|
+
from arc_client.management.async_continuous_query import AsyncContinuousQueryClient
|
|
127
|
+
|
|
128
|
+
self._continuous_queries = AsyncContinuousQueryClient(self._get_http(), self._config)
|
|
129
|
+
return self._continuous_queries
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def delete(self) -> Any:
|
|
133
|
+
"""Get the async delete client for data deletion."""
|
|
134
|
+
if self._delete is None:
|
|
135
|
+
from arc_client.management.async_delete import AsyncDeleteClient
|
|
136
|
+
|
|
137
|
+
self._delete = AsyncDeleteClient(self._get_http(), self._config)
|
|
138
|
+
return self._delete
|
|
139
|
+
|
|
140
|
+
async def health(self) -> HealthResponse:
|
|
141
|
+
"""Check server health.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
HealthResponse with server status.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ArcConnectionError: If connection to server fails.
|
|
148
|
+
"""
|
|
149
|
+
response = await self._get_http().get("/health")
|
|
150
|
+
return HealthResponse.model_validate(response.json())
|
|
151
|
+
|
|
152
|
+
async def ready(self) -> bool:
|
|
153
|
+
"""Check if server is ready to accept requests.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
True if server is ready, False otherwise.
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
response = await self._get_http().get("/ready")
|
|
160
|
+
return response.status_code == 200
|
|
161
|
+
except Exception:
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
async def close(self) -> None:
|
|
165
|
+
"""Close the client and release resources."""
|
|
166
|
+
if self._http is not None:
|
|
167
|
+
await self._http.close()
|
|
168
|
+
self._http = None
|
|
169
|
+
|
|
170
|
+
async def __aenter__(self) -> AsyncArcClient:
|
|
171
|
+
"""Enter async context manager."""
|
|
172
|
+
return self
|
|
173
|
+
|
|
174
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
175
|
+
"""Exit async context manager."""
|
|
176
|
+
await self.close()
|
|
177
|
+
|
|
178
|
+
def __repr__(self) -> str:
|
|
179
|
+
return f"AsyncArcClient(host={self._config.host!r}, port={self._config.port})"
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Asynchronous auth client for Arc."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
|
+
|
|
7
|
+
from arc_client.config import ClientConfig
|
|
8
|
+
from arc_client.exceptions import ArcAuthenticationError, ArcNotFoundError
|
|
9
|
+
from arc_client.http.async_http import AsyncHTTPClient
|
|
10
|
+
from arc_client.models.auth import (
|
|
11
|
+
CreateTokenResponse,
|
|
12
|
+
RotateTokenResponse,
|
|
13
|
+
TokenInfo,
|
|
14
|
+
TokenListResponse,
|
|
15
|
+
VerifyResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AsyncAuthClient:
|
|
20
|
+
"""Asynchronous client for authentication and token management."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, http: AsyncHTTPClient, config: ClientConfig) -> None:
|
|
23
|
+
self._http = http
|
|
24
|
+
self._config = config
|
|
25
|
+
|
|
26
|
+
async def verify(self) -> VerifyResponse:
|
|
27
|
+
"""Verify the current token."""
|
|
28
|
+
if not self._config.token:
|
|
29
|
+
return VerifyResponse(valid=False, error="No token configured")
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
response = await self._http.get("/api/v1/auth/verify")
|
|
33
|
+
data = response.json()
|
|
34
|
+
return VerifyResponse(**data)
|
|
35
|
+
except ArcAuthenticationError:
|
|
36
|
+
return VerifyResponse(valid=False, error="Invalid or expired token")
|
|
37
|
+
except Exception as e:
|
|
38
|
+
raise ArcAuthenticationError(f"Token verification failed: {e}") from e
|
|
39
|
+
|
|
40
|
+
async def create_token(
|
|
41
|
+
self,
|
|
42
|
+
name: str,
|
|
43
|
+
description: Optional[str] = None,
|
|
44
|
+
permissions: Optional[list[str]] = None,
|
|
45
|
+
expires_in: Optional[str] = None,
|
|
46
|
+
) -> CreateTokenResponse:
|
|
47
|
+
"""Create a new API token. Requires admin permissions."""
|
|
48
|
+
payload: dict[str, Any] = {"name": name}
|
|
49
|
+
if description:
|
|
50
|
+
payload["description"] = description
|
|
51
|
+
if permissions:
|
|
52
|
+
payload["permissions"] = permissions
|
|
53
|
+
if expires_in:
|
|
54
|
+
payload["expires_in"] = expires_in
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
response = await self._http.post("/api/v1/auth/tokens", json=payload)
|
|
58
|
+
data = response.json()
|
|
59
|
+
return CreateTokenResponse(**data)
|
|
60
|
+
except ArcAuthenticationError:
|
|
61
|
+
raise
|
|
62
|
+
except Exception as e:
|
|
63
|
+
raise ArcAuthenticationError(f"Failed to create token: {e}") from e
|
|
64
|
+
|
|
65
|
+
async def list_tokens(self) -> List[TokenInfo]:
|
|
66
|
+
"""List all tokens. Requires admin permissions."""
|
|
67
|
+
try:
|
|
68
|
+
response = await self._http.get("/api/v1/auth/tokens")
|
|
69
|
+
data = response.json()
|
|
70
|
+
result = TokenListResponse(**data)
|
|
71
|
+
if not result.success:
|
|
72
|
+
raise ArcAuthenticationError(result.error or "Failed to list tokens")
|
|
73
|
+
return result.tokens
|
|
74
|
+
except ArcAuthenticationError:
|
|
75
|
+
raise
|
|
76
|
+
except Exception as e:
|
|
77
|
+
raise ArcAuthenticationError(f"Failed to list tokens: {e}") from e
|
|
78
|
+
|
|
79
|
+
async def get_token(self, token_id: int) -> TokenInfo:
|
|
80
|
+
"""Get token details by ID. Requires admin permissions."""
|
|
81
|
+
try:
|
|
82
|
+
response = await self._http.get(f"/api/v1/auth/tokens/{token_id}")
|
|
83
|
+
data = response.json()
|
|
84
|
+
if not data.get("success", True):
|
|
85
|
+
error = data.get("error", "Unknown error")
|
|
86
|
+
if "not found" in error.lower():
|
|
87
|
+
raise ArcNotFoundError(f"Token {token_id} not found")
|
|
88
|
+
raise ArcAuthenticationError(error)
|
|
89
|
+
return TokenInfo(**data["token"])
|
|
90
|
+
except (ArcNotFoundError, ArcAuthenticationError):
|
|
91
|
+
raise
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise ArcAuthenticationError(f"Failed to get token: {e}") from e
|
|
94
|
+
|
|
95
|
+
async def update_token(
|
|
96
|
+
self,
|
|
97
|
+
token_id: int,
|
|
98
|
+
name: Optional[str] = None,
|
|
99
|
+
description: Optional[str] = None,
|
|
100
|
+
permissions: Optional[list[str]] = None,
|
|
101
|
+
expires_in: Optional[str] = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Update token properties. Requires admin permissions."""
|
|
104
|
+
payload: dict[str, Any] = {}
|
|
105
|
+
if name is not None:
|
|
106
|
+
payload["name"] = name
|
|
107
|
+
if description is not None:
|
|
108
|
+
payload["description"] = description
|
|
109
|
+
if permissions is not None:
|
|
110
|
+
payload["permissions"] = permissions
|
|
111
|
+
if expires_in is not None:
|
|
112
|
+
payload["expires_in"] = expires_in
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
response = await self._http.patch(f"/api/v1/auth/tokens/{token_id}", json=payload)
|
|
116
|
+
data = response.json()
|
|
117
|
+
if not data.get("success", True):
|
|
118
|
+
error = data.get("error", "Unknown error")
|
|
119
|
+
if "not found" in error.lower():
|
|
120
|
+
raise ArcNotFoundError(f"Token {token_id} not found")
|
|
121
|
+
raise ArcAuthenticationError(error)
|
|
122
|
+
except (ArcNotFoundError, ArcAuthenticationError):
|
|
123
|
+
raise
|
|
124
|
+
except Exception as e:
|
|
125
|
+
raise ArcAuthenticationError(f"Failed to update token: {e}") from e
|
|
126
|
+
|
|
127
|
+
async def delete_token(self, token_id: int) -> None:
|
|
128
|
+
"""Delete a token. Requires admin permissions."""
|
|
129
|
+
try:
|
|
130
|
+
response = await self._http.delete(f"/api/v1/auth/tokens/{token_id}")
|
|
131
|
+
data = response.json()
|
|
132
|
+
if not data.get("success", True):
|
|
133
|
+
error = data.get("error", "Unknown error")
|
|
134
|
+
if "not found" in error.lower():
|
|
135
|
+
raise ArcNotFoundError(f"Token {token_id} not found")
|
|
136
|
+
raise ArcAuthenticationError(error)
|
|
137
|
+
except (ArcNotFoundError, ArcAuthenticationError):
|
|
138
|
+
raise
|
|
139
|
+
except Exception as e:
|
|
140
|
+
raise ArcAuthenticationError(f"Failed to delete token: {e}") from e
|
|
141
|
+
|
|
142
|
+
async def rotate_token(self, token_id: int) -> str:
|
|
143
|
+
"""Rotate a token and return the new value. Requires admin permissions."""
|
|
144
|
+
try:
|
|
145
|
+
response = await self._http.post(f"/api/v1/auth/tokens/{token_id}/rotate")
|
|
146
|
+
data = response.json()
|
|
147
|
+
result = RotateTokenResponse(**data)
|
|
148
|
+
if not result.success:
|
|
149
|
+
error = result.error or "Unknown error"
|
|
150
|
+
if "not found" in error.lower():
|
|
151
|
+
raise ArcNotFoundError(f"Token {token_id} not found")
|
|
152
|
+
raise ArcAuthenticationError(error)
|
|
153
|
+
if not result.new_token:
|
|
154
|
+
raise ArcAuthenticationError("No new token returned")
|
|
155
|
+
return result.new_token
|
|
156
|
+
except (ArcNotFoundError, ArcAuthenticationError):
|
|
157
|
+
raise
|
|
158
|
+
except Exception as e:
|
|
159
|
+
raise ArcAuthenticationError(f"Failed to rotate token: {e}") from e
|
|
160
|
+
|
|
161
|
+
async def revoke_token(self, token_id: int) -> None:
|
|
162
|
+
"""Revoke a token. Requires admin permissions."""
|
|
163
|
+
try:
|
|
164
|
+
response = await self._http.post(f"/api/v1/auth/tokens/{token_id}/revoke")
|
|
165
|
+
data = response.json()
|
|
166
|
+
if not data.get("success", True):
|
|
167
|
+
error = data.get("error", "Unknown error")
|
|
168
|
+
if "not found" in error.lower():
|
|
169
|
+
raise ArcNotFoundError(f"Token {token_id} not found")
|
|
170
|
+
raise ArcAuthenticationError(error)
|
|
171
|
+
except (ArcNotFoundError, ArcAuthenticationError):
|
|
172
|
+
raise
|
|
173
|
+
except Exception as e:
|
|
174
|
+
raise ArcAuthenticationError(f"Failed to revoke token: {e}") from e
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Synchronous auth client for Arc."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
|
+
|
|
7
|
+
from arc_client.config import ClientConfig
|
|
8
|
+
from arc_client.exceptions import ArcAuthenticationError, ArcNotFoundError
|
|
9
|
+
from arc_client.http.sync_http import SyncHTTPClient
|
|
10
|
+
from arc_client.models.auth import (
|
|
11
|
+
CreateTokenResponse,
|
|
12
|
+
RotateTokenResponse,
|
|
13
|
+
TokenInfo,
|
|
14
|
+
TokenListResponse,
|
|
15
|
+
VerifyResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AuthClient:
|
|
20
|
+
"""Synchronous client for authentication and token management.
|
|
21
|
+
|
|
22
|
+
This client provides methods for verifying tokens and managing API tokens
|
|
23
|
+
(create, list, update, delete, rotate, revoke).
|
|
24
|
+
|
|
25
|
+
Note: Token management operations require admin permissions.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> with ArcClient(host="localhost", token="admin-token") as client:
|
|
29
|
+
... result = client.auth.verify()
|
|
30
|
+
... if result.valid:
|
|
31
|
+
... print(f"Token valid with permissions: {result.permissions}")
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, http: SyncHTTPClient, config: ClientConfig) -> None:
|
|
35
|
+
self._http = http
|
|
36
|
+
self._config = config
|
|
37
|
+
|
|
38
|
+
def verify(self) -> VerifyResponse:
|
|
39
|
+
"""Verify the current token."""
|
|
40
|
+
if not self._config.token:
|
|
41
|
+
return VerifyResponse(valid=False, error="No token configured")
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
response = self._http.get("/api/v1/auth/verify")
|
|
45
|
+
data = response.json()
|
|
46
|
+
return VerifyResponse(**data)
|
|
47
|
+
except ArcAuthenticationError:
|
|
48
|
+
return VerifyResponse(valid=False, error="Invalid or expired token")
|
|
49
|
+
except Exception as e:
|
|
50
|
+
raise ArcAuthenticationError(f"Token verification failed: {e}") from e
|
|
51
|
+
|
|
52
|
+
def create_token(
|
|
53
|
+
self,
|
|
54
|
+
name: str,
|
|
55
|
+
description: Optional[str] = None,
|
|
56
|
+
permissions: Optional[list[str]] = None,
|
|
57
|
+
expires_in: Optional[str] = None,
|
|
58
|
+
) -> CreateTokenResponse:
|
|
59
|
+
"""Create a new API token. Requires admin permissions."""
|
|
60
|
+
payload: dict[str, Any] = {"name": name}
|
|
61
|
+
if description:
|
|
62
|
+
payload["description"] = description
|
|
63
|
+
if permissions:
|
|
64
|
+
payload["permissions"] = permissions
|
|
65
|
+
if expires_in:
|
|
66
|
+
payload["expires_in"] = expires_in
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
response = self._http.post("/api/v1/auth/tokens", json=payload)
|
|
70
|
+
data = response.json()
|
|
71
|
+
return CreateTokenResponse(**data)
|
|
72
|
+
except ArcAuthenticationError:
|
|
73
|
+
raise
|
|
74
|
+
except Exception as e:
|
|
75
|
+
raise ArcAuthenticationError(f"Failed to create token: {e}") from e
|
|
76
|
+
|
|
77
|
+
def list_tokens(self) -> List[TokenInfo]:
|
|
78
|
+
"""List all tokens. Requires admin permissions."""
|
|
79
|
+
try:
|
|
80
|
+
response = self._http.get("/api/v1/auth/tokens")
|
|
81
|
+
data = response.json()
|
|
82
|
+
result = TokenListResponse(**data)
|
|
83
|
+
if not result.success:
|
|
84
|
+
raise ArcAuthenticationError(result.error or "Failed to list tokens")
|
|
85
|
+
return result.tokens
|
|
86
|
+
except ArcAuthenticationError:
|
|
87
|
+
raise
|
|
88
|
+
except Exception as e:
|
|
89
|
+
raise ArcAuthenticationError(f"Failed to list tokens: {e}") from e
|
|
90
|
+
|
|
91
|
+
def get_token(self, token_id: int) -> TokenInfo:
|
|
92
|
+
"""Get token details by ID. Requires admin permissions."""
|
|
93
|
+
try:
|
|
94
|
+
response = self._http.get(f"/api/v1/auth/tokens/{token_id}")
|
|
95
|
+
data = response.json()
|
|
96
|
+
if not data.get("success", True):
|
|
97
|
+
error = data.get("error", "Unknown error")
|
|
98
|
+
if "not found" in error.lower():
|
|
99
|
+
raise ArcNotFoundError(f"Token {token_id} not found")
|
|
100
|
+
raise ArcAuthenticationError(error)
|
|
101
|
+
return TokenInfo(**data["token"])
|
|
102
|
+
except (ArcNotFoundError, ArcAuthenticationError):
|
|
103
|
+
raise
|
|
104
|
+
except Exception as e:
|
|
105
|
+
raise ArcAuthenticationError(f"Failed to get token: {e}") from e
|
|
106
|
+
|
|
107
|
+
def update_token(
|
|
108
|
+
self,
|
|
109
|
+
token_id: int,
|
|
110
|
+
name: Optional[str] = None,
|
|
111
|
+
description: Optional[str] = None,
|
|
112
|
+
permissions: Optional[list[str]] = None,
|
|
113
|
+
expires_in: Optional[str] = None,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Update token properties. Requires admin permissions."""
|
|
116
|
+
payload: dict[str, Any] = {}
|
|
117
|
+
if name is not None:
|
|
118
|
+
payload["name"] = name
|
|
119
|
+
if description is not None:
|
|
120
|
+
payload["description"] = description
|
|
121
|
+
if permissions is not None:
|
|
122
|
+
payload["permissions"] = permissions
|
|
123
|
+
if expires_in is not None:
|
|
124
|
+
payload["expires_in"] = expires_in
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
response = self._http.patch(f"/api/v1/auth/tokens/{token_id}", json=payload)
|
|
128
|
+
data = response.json()
|
|
129
|
+
if not data.get("success", True):
|
|
130
|
+
error = data.get("error", "Unknown error")
|
|
131
|
+
if "not found" in error.lower():
|
|
132
|
+
raise ArcNotFoundError(f"Token {token_id} not found")
|
|
133
|
+
raise ArcAuthenticationError(error)
|
|
134
|
+
except (ArcNotFoundError, ArcAuthenticationError):
|
|
135
|
+
raise
|
|
136
|
+
except Exception as e:
|
|
137
|
+
raise ArcAuthenticationError(f"Failed to update token: {e}") from e
|
|
138
|
+
|
|
139
|
+
def delete_token(self, token_id: int) -> None:
|
|
140
|
+
"""Delete a token. Requires admin permissions."""
|
|
141
|
+
try:
|
|
142
|
+
response = self._http.delete(f"/api/v1/auth/tokens/{token_id}")
|
|
143
|
+
data = response.json()
|
|
144
|
+
if not data.get("success", True):
|
|
145
|
+
error = data.get("error", "Unknown error")
|
|
146
|
+
if "not found" in error.lower():
|
|
147
|
+
raise ArcNotFoundError(f"Token {token_id} not found")
|
|
148
|
+
raise ArcAuthenticationError(error)
|
|
149
|
+
except (ArcNotFoundError, ArcAuthenticationError):
|
|
150
|
+
raise
|
|
151
|
+
except Exception as e:
|
|
152
|
+
raise ArcAuthenticationError(f"Failed to delete token: {e}") from e
|
|
153
|
+
|
|
154
|
+
def rotate_token(self, token_id: int) -> str:
|
|
155
|
+
"""Rotate a token and return the new value. Requires admin permissions."""
|
|
156
|
+
try:
|
|
157
|
+
response = self._http.post(f"/api/v1/auth/tokens/{token_id}/rotate")
|
|
158
|
+
data = response.json()
|
|
159
|
+
result = RotateTokenResponse(**data)
|
|
160
|
+
if not result.success:
|
|
161
|
+
error = result.error or "Unknown error"
|
|
162
|
+
if "not found" in error.lower():
|
|
163
|
+
raise ArcNotFoundError(f"Token {token_id} not found")
|
|
164
|
+
raise ArcAuthenticationError(error)
|
|
165
|
+
if not result.new_token:
|
|
166
|
+
raise ArcAuthenticationError("No new token returned")
|
|
167
|
+
return result.new_token
|
|
168
|
+
except (ArcNotFoundError, ArcAuthenticationError):
|
|
169
|
+
raise
|
|
170
|
+
except Exception as e:
|
|
171
|
+
raise ArcAuthenticationError(f"Failed to rotate token: {e}") from e
|
|
172
|
+
|
|
173
|
+
def revoke_token(self, token_id: int) -> None:
|
|
174
|
+
"""Revoke a token. Requires admin permissions."""
|
|
175
|
+
try:
|
|
176
|
+
response = self._http.post(f"/api/v1/auth/tokens/{token_id}/revoke")
|
|
177
|
+
data = response.json()
|
|
178
|
+
if not data.get("success", True):
|
|
179
|
+
error = data.get("error", "Unknown error")
|
|
180
|
+
if "not found" in error.lower():
|
|
181
|
+
raise ArcNotFoundError(f"Token {token_id} not found")
|
|
182
|
+
raise ArcAuthenticationError(error)
|
|
183
|
+
except (ArcNotFoundError, ArcAuthenticationError):
|
|
184
|
+
raise
|
|
185
|
+
except Exception as e:
|
|
186
|
+
raise ArcAuthenticationError(f"Failed to revoke token: {e}") from e
|