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.
Files changed (42) hide show
  1. arc_client/__init__.py +38 -0
  2. arc_client/async_client.py +179 -0
  3. arc_client/auth/__init__.py +9 -0
  4. arc_client/auth/async_manager.py +174 -0
  5. arc_client/auth/manager.py +186 -0
  6. arc_client/client.py +179 -0
  7. arc_client/config.py +28 -0
  8. arc_client/dataframe/__init__.py +1 -0
  9. arc_client/exceptions.py +61 -0
  10. arc_client/http/__init__.py +11 -0
  11. arc_client/http/async_http.py +136 -0
  12. arc_client/http/base.py +90 -0
  13. arc_client/http/sync_http.py +136 -0
  14. arc_client/ingestion/__init__.py +47 -0
  15. arc_client/ingestion/async_buffered.py +163 -0
  16. arc_client/ingestion/async_writer.py +209 -0
  17. arc_client/ingestion/buffered.py +219 -0
  18. arc_client/ingestion/compression.py +67 -0
  19. arc_client/ingestion/line_protocol.py +261 -0
  20. arc_client/ingestion/msgpack.py +349 -0
  21. arc_client/ingestion/writer.py +326 -0
  22. arc_client/management/__init__.py +17 -0
  23. arc_client/management/async_continuous_query.py +191 -0
  24. arc_client/management/async_delete.py +60 -0
  25. arc_client/management/async_retention.py +153 -0
  26. arc_client/management/continuous_query.py +191 -0
  27. arc_client/management/delete.py +63 -0
  28. arc_client/management/retention.py +153 -0
  29. arc_client/models/__init__.py +43 -0
  30. arc_client/models/auth.py +96 -0
  31. arc_client/models/common.py +22 -0
  32. arc_client/models/continuous_query.py +64 -0
  33. arc_client/models/delete.py +49 -0
  34. arc_client/models/query.py +67 -0
  35. arc_client/models/retention.py +51 -0
  36. arc_client/py.typed +0 -0
  37. arc_client/query/__init__.py +9 -0
  38. arc_client/query/async_executor.py +297 -0
  39. arc_client/query/executor.py +336 -0
  40. arc_tsdb_client-0.1.0.dist-info/METADATA +410 -0
  41. arc_tsdb_client-0.1.0.dist-info/RECORD +42 -0
  42. 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,9 @@
1
+ """Authentication module for Arc client."""
2
+
3
+ from arc_client.auth.async_manager import AsyncAuthClient
4
+ from arc_client.auth.manager import AuthClient
5
+
6
+ __all__ = [
7
+ "AsyncAuthClient",
8
+ "AuthClient",
9
+ ]
@@ -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