arcade-core 3.3.5__py3-none-any.whl → 4.0.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.
- arcade_core/auth.py +9 -0
- arcade_core/auth_tokens.py +110 -0
- arcade_core/config_model.py +105 -20
- arcade_core/constants.py +5 -0
- arcade_core/network/__init__.py +1 -0
- arcade_core/network/org_transport.py +99 -0
- arcade_core/usage/identity.py +52 -18
- {arcade_core-3.3.5.dist-info → arcade_core-4.0.0.dist-info}/METADATA +2 -1
- {arcade_core-3.3.5.dist-info → arcade_core-4.0.0.dist-info}/RECORD +10 -7
- {arcade_core-3.3.5.dist-info → arcade_core-4.0.0.dist-info}/WHEEL +1 -1
arcade_core/auth.py
CHANGED
|
@@ -78,6 +78,15 @@ class Dropbox(OAuth2):
|
|
|
78
78
|
super().__init__(id=id, scopes=scopes)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
class Figma(OAuth2):
|
|
82
|
+
"""Marks a tool as requiring Figma authorization."""
|
|
83
|
+
|
|
84
|
+
provider_id: str = "figma"
|
|
85
|
+
|
|
86
|
+
def __init__(self, *, id: Optional[str] = None, scopes: Optional[list[str]] = None): # noqa: A002
|
|
87
|
+
super().__init__(id=id, scopes=scopes)
|
|
88
|
+
|
|
89
|
+
|
|
81
90
|
class GitHub(OAuth2):
|
|
82
91
|
"""Marks a tool as requiring GitHub App authorization."""
|
|
83
92
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared access-token utilities used by both the CLI and MCP server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from arcade_core.config_model import Config
|
|
13
|
+
from arcade_core.constants import PROD_COORDINATOR_HOST
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CLIConfig(BaseModel):
|
|
17
|
+
"""OAuth configuration returned by the Coordinator."""
|
|
18
|
+
|
|
19
|
+
client_id: str
|
|
20
|
+
authorization_endpoint: str
|
|
21
|
+
token_endpoint: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TokenResponse(BaseModel):
|
|
25
|
+
"""OAuth token response."""
|
|
26
|
+
|
|
27
|
+
access_token: str
|
|
28
|
+
refresh_token: str
|
|
29
|
+
expires_in: int
|
|
30
|
+
token_type: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def fetch_cli_config(coordinator_url: str) -> CLIConfig:
|
|
34
|
+
"""Fetch OAuth configuration from the Coordinator."""
|
|
35
|
+
url = f"{coordinator_url}/api/v1/auth/cli_config"
|
|
36
|
+
response = httpx.get(url, timeout=30)
|
|
37
|
+
response.raise_for_status()
|
|
38
|
+
return CLIConfig.model_validate(response.json())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def refresh_access_token(
|
|
42
|
+
cli_config: CLIConfig,
|
|
43
|
+
refresh_token: str,
|
|
44
|
+
) -> TokenResponse:
|
|
45
|
+
"""Refresh the access token using authlib-compatible token endpoint."""
|
|
46
|
+
response = httpx.post(
|
|
47
|
+
cli_config.token_endpoint,
|
|
48
|
+
data={
|
|
49
|
+
"grant_type": "refresh_token",
|
|
50
|
+
"refresh_token": refresh_token,
|
|
51
|
+
"client_id": cli_config.client_id,
|
|
52
|
+
},
|
|
53
|
+
timeout=30,
|
|
54
|
+
)
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
token = response.json()
|
|
57
|
+
return TokenResponse(
|
|
58
|
+
access_token=token["access_token"],
|
|
59
|
+
refresh_token=token.get("refresh_token", refresh_token),
|
|
60
|
+
expires_in=token["expires_in"],
|
|
61
|
+
token_type=token["token_type"],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_valid_access_token(coordinator_url: str | None = None) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Get a valid access token, refreshing if necessary.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Valid access token
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValueError: If not logged in or token refresh fails
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
config = Config.load_from_file()
|
|
77
|
+
except FileNotFoundError:
|
|
78
|
+
raise ValueError("Not logged in. Please run 'arcade login' first.")
|
|
79
|
+
|
|
80
|
+
resolved_coordinator_url = (
|
|
81
|
+
coordinator_url
|
|
82
|
+
or (config.coordinator_url if config.coordinator_url else None)
|
|
83
|
+
or f"https://{PROD_COORDINATOR_HOST}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if not config.auth:
|
|
87
|
+
raise ValueError("Not logged in. Please run 'arcade login' first.")
|
|
88
|
+
|
|
89
|
+
# Check if token needs refresh
|
|
90
|
+
if config.is_token_expired():
|
|
91
|
+
cli_config = fetch_cli_config(resolved_coordinator_url)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
new_tokens = refresh_access_token(cli_config, config.auth.refresh_token)
|
|
95
|
+
except httpx.HTTPError as e:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Failed to refresh token: {e}. Please run 'arcade login' to re-authenticate."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Update stored credentials
|
|
101
|
+
expires_at = datetime.now() + timedelta(seconds=new_tokens.expires_in)
|
|
102
|
+
config.coordinator_url = resolved_coordinator_url
|
|
103
|
+
config.auth.access_token = new_tokens.access_token
|
|
104
|
+
config.auth.refresh_token = new_tokens.refresh_token
|
|
105
|
+
config.auth.expires_at = expires_at
|
|
106
|
+
config.save_to_file()
|
|
107
|
+
|
|
108
|
+
return new_tokens.access_token
|
|
109
|
+
|
|
110
|
+
return config.auth.access_token
|
arcade_core/config_model.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from datetime import datetime, timedelta
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
@@ -10,18 +11,22 @@ class BaseConfig(BaseModel):
|
|
|
10
11
|
model_config = ConfigDict(extra="ignore")
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
class
|
|
14
|
+
class AuthConfig(BaseConfig):
|
|
14
15
|
"""
|
|
15
|
-
|
|
16
|
+
OAuth authentication configuration.
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
access_token: str
|
|
19
20
|
"""
|
|
20
|
-
|
|
21
|
+
OAuth access token (JWT).
|
|
21
22
|
"""
|
|
22
|
-
|
|
23
|
+
refresh_token: str
|
|
23
24
|
"""
|
|
24
|
-
|
|
25
|
+
OAuth refresh token for obtaining new access tokens.
|
|
26
|
+
"""
|
|
27
|
+
expires_at: datetime
|
|
28
|
+
"""
|
|
29
|
+
When the access token expires.
|
|
25
30
|
"""
|
|
26
31
|
|
|
27
32
|
|
|
@@ -36,15 +41,51 @@ class UserConfig(BaseConfig):
|
|
|
36
41
|
"""
|
|
37
42
|
|
|
38
43
|
|
|
44
|
+
class ContextConfig(BaseConfig):
|
|
45
|
+
"""
|
|
46
|
+
Active organization and project context.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
org_id: str
|
|
50
|
+
"""
|
|
51
|
+
Active organization ID.
|
|
52
|
+
"""
|
|
53
|
+
org_name: str
|
|
54
|
+
"""
|
|
55
|
+
Active organization name.
|
|
56
|
+
"""
|
|
57
|
+
project_id: str
|
|
58
|
+
"""
|
|
59
|
+
Active project ID.
|
|
60
|
+
"""
|
|
61
|
+
project_name: str
|
|
62
|
+
"""
|
|
63
|
+
Active project name.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
39
67
|
class Config(BaseConfig):
|
|
40
68
|
"""
|
|
41
|
-
Configuration for Arcade.
|
|
69
|
+
Configuration for Arcade CLI.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
coordinator_url: str | None = None
|
|
73
|
+
"""
|
|
74
|
+
Base URL of the Arcade Coordinator used for authentication flows.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
auth: AuthConfig | None = None
|
|
78
|
+
"""
|
|
79
|
+
OAuth authentication configuration.
|
|
42
80
|
"""
|
|
43
81
|
|
|
44
|
-
|
|
82
|
+
# Active org/project context
|
|
83
|
+
context: ContextConfig | None = None
|
|
45
84
|
"""
|
|
46
|
-
|
|
85
|
+
Active organization and project context.
|
|
47
86
|
"""
|
|
87
|
+
|
|
88
|
+
# User info
|
|
48
89
|
user: UserConfig | None = None
|
|
49
90
|
"""
|
|
50
91
|
Arcade user configuration.
|
|
@@ -53,6 +94,48 @@ class Config(BaseConfig):
|
|
|
53
94
|
def __init__(self, **data: Any):
|
|
54
95
|
super().__init__(**data)
|
|
55
96
|
|
|
97
|
+
def is_authenticated(self) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Check if the user is authenticated (has valid auth config).
|
|
100
|
+
"""
|
|
101
|
+
return self.auth is not None
|
|
102
|
+
|
|
103
|
+
def is_token_expired(self) -> bool:
|
|
104
|
+
"""
|
|
105
|
+
Check if the access token is expired or will expire within 5 minutes.
|
|
106
|
+
"""
|
|
107
|
+
if not self.auth:
|
|
108
|
+
return True
|
|
109
|
+
# Consider expired if less than 5 minutes remaining
|
|
110
|
+
buffer_seconds = 300
|
|
111
|
+
return datetime.now() >= self.auth.expires_at.replace(tzinfo=None) - timedelta(
|
|
112
|
+
seconds=buffer_seconds
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def get_access_token(self) -> str | None:
|
|
116
|
+
"""
|
|
117
|
+
Get the current access token if available.
|
|
118
|
+
"""
|
|
119
|
+
if self.auth:
|
|
120
|
+
return self.auth.access_token
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
def get_active_org_id(self) -> str | None:
|
|
124
|
+
"""
|
|
125
|
+
Get the active organization ID.
|
|
126
|
+
"""
|
|
127
|
+
if self.context:
|
|
128
|
+
return self.context.org_id
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def get_active_project_id(self) -> str | None:
|
|
132
|
+
"""
|
|
133
|
+
Get the active project ID.
|
|
134
|
+
"""
|
|
135
|
+
if self.context:
|
|
136
|
+
return self.context.project_id
|
|
137
|
+
return None
|
|
138
|
+
|
|
56
139
|
@classmethod
|
|
57
140
|
def get_config_dir_path(cls) -> Path:
|
|
58
141
|
"""
|
|
@@ -82,16 +165,11 @@ class Config(BaseConfig):
|
|
|
82
165
|
"""
|
|
83
166
|
Load the configuration from the YAML file in the configuration directory.
|
|
84
167
|
|
|
85
|
-
If no configuration file exists, this method will create a new one with default values.
|
|
86
|
-
The default configuration includes:
|
|
87
|
-
- An empty API configuration
|
|
88
|
-
- A default Engine configuration (host: "api.arcade.dev", port: None, tls: True)
|
|
89
|
-
- No user configuration
|
|
90
|
-
|
|
91
168
|
Returns:
|
|
92
|
-
Config: The loaded
|
|
169
|
+
Config: The loaded configuration.
|
|
93
170
|
|
|
94
171
|
Raises:
|
|
172
|
+
FileNotFoundError: If no configuration file exists.
|
|
95
173
|
ValueError: If the existing configuration file is invalid.
|
|
96
174
|
"""
|
|
97
175
|
cls.ensure_config_dir_exists()
|
|
@@ -108,13 +186,13 @@ class Config(BaseConfig):
|
|
|
108
186
|
|
|
109
187
|
if config_data is None:
|
|
110
188
|
raise ValueError(
|
|
111
|
-
"Invalid credentials.yaml file. Please ensure it is a valid YAML file or"
|
|
189
|
+
"Invalid credentials.yaml file. Please ensure it is a valid YAML file or "
|
|
112
190
|
"run `arcade logout`, then `arcade login` to start from a clean slate."
|
|
113
191
|
)
|
|
114
192
|
|
|
115
193
|
if "cloud" not in config_data:
|
|
116
194
|
raise ValueError(
|
|
117
|
-
"Invalid credentials.yaml file. Expected a 'cloud' key."
|
|
195
|
+
"Invalid credentials.yaml file. Expected a 'cloud' key. "
|
|
118
196
|
"Run `arcade logout`, then `arcade login` to start from a clean slate."
|
|
119
197
|
)
|
|
120
198
|
|
|
@@ -123,7 +201,6 @@ class Config(BaseConfig):
|
|
|
123
201
|
except ValidationError as e:
|
|
124
202
|
# Get only the errors with {type:missing} and combine them
|
|
125
203
|
# into a nicely-formatted string message.
|
|
126
|
-
# Any other errors without {type:missing} should just be str()ed
|
|
127
204
|
missing_field_errors = [
|
|
128
205
|
".".join(map(str, error["loc"]))
|
|
129
206
|
for error in e.errors()
|
|
@@ -145,7 +222,15 @@ class Config(BaseConfig):
|
|
|
145
222
|
def save_to_file(self) -> None:
|
|
146
223
|
"""
|
|
147
224
|
Save the configuration to the YAML file in the configuration directory.
|
|
225
|
+
|
|
226
|
+
Sets file permissions to 600 (owner read/write only) for security.
|
|
148
227
|
"""
|
|
149
228
|
Config.ensure_config_dir_exists()
|
|
150
229
|
config_file_path = Config.get_config_file_path()
|
|
151
|
-
|
|
230
|
+
|
|
231
|
+
# Convert to dict, excluding None values for cleaner output
|
|
232
|
+
data = {"cloud": self.model_dump(exclude_none=True, mode="json")}
|
|
233
|
+
config_file_path.write_text(yaml.dump(data, default_flow_style=False))
|
|
234
|
+
|
|
235
|
+
# Set restrictive permissions (owner read/write only)
|
|
236
|
+
config_file_path.chmod(0o600)
|
arcade_core/constants.py
CHANGED
|
@@ -4,3 +4,8 @@ import os
|
|
|
4
4
|
ARCADE_CONFIG_PATH = os.path.join(os.path.expanduser(os.getenv("ARCADE_WORK_DIR", "~")), ".arcade")
|
|
5
5
|
# The path to the file containing the user's Arcade-related credentials (e.g., ARCADE_API_KEY).
|
|
6
6
|
CREDENTIALS_FILE_PATH = os.path.join(ARCADE_CONFIG_PATH, "credentials.yaml")
|
|
7
|
+
|
|
8
|
+
# Host defaults used by both the CLI and MCP server
|
|
9
|
+
PROD_COORDINATOR_HOST = "cloud.arcade.dev"
|
|
10
|
+
PROD_ENGINE_HOST = "api.arcade.dev"
|
|
11
|
+
LOCALHOST = "localhost"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Network utilities shared across Arcade components.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared HTTP transport helpers for org-scoped Arcade API access.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _rewrite_request_path(request: httpx.Request, org_id: str, project_id: str) -> httpx.Request:
|
|
11
|
+
"""Return a request with its path rewritten to include org/project scope."""
|
|
12
|
+
path = request.url.path
|
|
13
|
+
if path.startswith("/v1/") and "/v1/orgs/" not in path:
|
|
14
|
+
scoped_path = path.replace("/v1/", f"/v1/orgs/{org_id}/projects/{project_id}/", 1)
|
|
15
|
+
scoped_url = request.url.copy_with(path=scoped_path)
|
|
16
|
+
return httpx.Request(
|
|
17
|
+
method=request.method,
|
|
18
|
+
url=scoped_url,
|
|
19
|
+
headers=request.headers,
|
|
20
|
+
content=request.content,
|
|
21
|
+
extensions=request.extensions,
|
|
22
|
+
)
|
|
23
|
+
return request
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OrgScopedTransport(httpx.BaseTransport):
|
|
27
|
+
"""Sync transport that rewrites requests to include org/project scope."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
wrapped_transport: httpx.BaseTransport,
|
|
32
|
+
org_id: str,
|
|
33
|
+
project_id: str,
|
|
34
|
+
) -> None:
|
|
35
|
+
self.wrapped = wrapped_transport
|
|
36
|
+
self.org_id = org_id
|
|
37
|
+
self.project_id = project_id
|
|
38
|
+
|
|
39
|
+
def handle_request(self, request: httpx.Request) -> httpx.Response:
|
|
40
|
+
scoped_request = _rewrite_request_path(request, self.org_id, self.project_id)
|
|
41
|
+
return self.wrapped.handle_request(scoped_request)
|
|
42
|
+
|
|
43
|
+
def close(self) -> None:
|
|
44
|
+
self.wrapped.close()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AsyncOrgScopedTransport(httpx.AsyncBaseTransport):
|
|
48
|
+
"""Async transport that rewrites requests to include org/project scope."""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
wrapped_transport: httpx.AsyncBaseTransport,
|
|
53
|
+
org_id: str,
|
|
54
|
+
project_id: str,
|
|
55
|
+
) -> None:
|
|
56
|
+
self.wrapped = wrapped_transport
|
|
57
|
+
self.org_id = org_id
|
|
58
|
+
self.project_id = project_id
|
|
59
|
+
|
|
60
|
+
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
|
|
61
|
+
scoped_request = _rewrite_request_path(request, self.org_id, self.project_id)
|
|
62
|
+
return await self.wrapped.handle_async_request(scoped_request)
|
|
63
|
+
|
|
64
|
+
async def aclose(self) -> None:
|
|
65
|
+
await self.wrapped.aclose()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def build_org_scoped_http_client(
|
|
69
|
+
org_id: str,
|
|
70
|
+
project_id: str,
|
|
71
|
+
*,
|
|
72
|
+
base_transport: httpx.BaseTransport | None = None,
|
|
73
|
+
client_kwargs: dict | None = None,
|
|
74
|
+
) -> httpx.Client:
|
|
75
|
+
"""
|
|
76
|
+
Build a sync httpx.Client that rewrites /v1 requests with org/project scope.
|
|
77
|
+
"""
|
|
78
|
+
client_kwargs = client_kwargs or {}
|
|
79
|
+
transport = OrgScopedTransport(
|
|
80
|
+
base_transport or httpx.HTTPTransport(), org_id=org_id, project_id=project_id
|
|
81
|
+
)
|
|
82
|
+
return httpx.Client(transport=transport, **client_kwargs)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def build_org_scoped_async_http_client(
|
|
86
|
+
org_id: str,
|
|
87
|
+
project_id: str,
|
|
88
|
+
*,
|
|
89
|
+
base_transport: httpx.AsyncBaseTransport | None = None,
|
|
90
|
+
client_kwargs: dict | None = None,
|
|
91
|
+
) -> httpx.AsyncClient:
|
|
92
|
+
"""
|
|
93
|
+
Build an async httpx.AsyncClient that rewrites /v1 requests with org/project scope.
|
|
94
|
+
"""
|
|
95
|
+
client_kwargs = client_kwargs or {}
|
|
96
|
+
transport = AsyncOrgScopedTransport(
|
|
97
|
+
base_transport or httpx.AsyncHTTPTransport(), org_id=org_id, project_id=project_id
|
|
98
|
+
)
|
|
99
|
+
return httpx.AsyncClient(transport=transport, **client_kwargs)
|
arcade_core/usage/identity.py
CHANGED
|
@@ -124,34 +124,68 @@ class UsageIdentity:
|
|
|
124
124
|
return str(data[KEY_ANON_ID])
|
|
125
125
|
|
|
126
126
|
def get_principal_id(self) -> str | None:
|
|
127
|
-
"""Fetch principal_id from Arcade Cloud API.
|
|
127
|
+
"""Fetch principal_id (account_id) from Arcade Cloud API.
|
|
128
|
+
|
|
129
|
+
Prefers any linked principal already stored in usage.json. If not
|
|
130
|
+
found, attempts to fetch via OAuth access token and falls
|
|
131
|
+
back to legacy API key validation if present.
|
|
128
132
|
|
|
129
133
|
Returns:
|
|
130
134
|
str | None: Principal ID if authenticated and API call succeeds, None otherwise
|
|
131
135
|
"""
|
|
136
|
+
# Prefer already-linked principal_id in usage.json
|
|
137
|
+
data = self.load_or_create()
|
|
138
|
+
linked_principal_id = data.get(KEY_LINKED_PRINCIPAL_ID)
|
|
139
|
+
if linked_principal_id:
|
|
140
|
+
return str(linked_principal_id)
|
|
141
|
+
|
|
132
142
|
if not os.path.exists(CREDENTIALS_FILE_PATH):
|
|
133
143
|
return None
|
|
134
144
|
|
|
135
145
|
try:
|
|
136
146
|
with open(CREDENTIALS_FILE_PATH) as f:
|
|
137
|
-
config = yaml.safe_load(f)
|
|
138
|
-
|
|
139
|
-
cloud_config = config.get("cloud", {})
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
config = yaml.safe_load(f) or {}
|
|
148
|
+
|
|
149
|
+
cloud_config = config.get("cloud", {}) if isinstance(config, dict) else {}
|
|
150
|
+
|
|
151
|
+
# Determine coordinator/authority URL for auth calls
|
|
152
|
+
coordinator_url = cloud_config.get("coordinator_url") or "https://cloud.arcade.dev"
|
|
153
|
+
whoami_url = f"{coordinator_url}/api/v1/auth/whoami"
|
|
154
|
+
validate_url = f"{coordinator_url}/api/v1/auth/validate"
|
|
155
|
+
|
|
156
|
+
# OAuth credentials: use access_token to call /whoami
|
|
157
|
+
auth_config = cloud_config.get("auth", {}) if isinstance(cloud_config, dict) else {}
|
|
158
|
+
access_token = auth_config.get("access_token")
|
|
159
|
+
if access_token:
|
|
160
|
+
response = httpx.get(
|
|
161
|
+
whoami_url,
|
|
162
|
+
headers={
|
|
163
|
+
"accept": "application/json",
|
|
164
|
+
"Authorization": f"Bearer {access_token}",
|
|
165
|
+
},
|
|
166
|
+
timeout=TIMEOUT_ARCADE_API,
|
|
167
|
+
)
|
|
168
|
+
if response.status_code == 200:
|
|
169
|
+
data = response.json().get("data", {})
|
|
170
|
+
principal_id = data.get("account_id") or data.get("principal_id")
|
|
171
|
+
if principal_id:
|
|
172
|
+
return str(principal_id)
|
|
173
|
+
|
|
174
|
+
# Legacy API key credentials (deprecated)
|
|
175
|
+
api_key = (
|
|
176
|
+
cloud_config.get("api", {}).get("key") if isinstance(cloud_config, dict) else None
|
|
149
177
|
)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
178
|
+
if api_key:
|
|
179
|
+
response = httpx.get(
|
|
180
|
+
validate_url,
|
|
181
|
+
headers={"accept": "application/json", "Authorization": f"Bearer {api_key}"},
|
|
182
|
+
timeout=TIMEOUT_ARCADE_API,
|
|
183
|
+
)
|
|
184
|
+
if response.status_code == 200:
|
|
185
|
+
data = response.json()
|
|
186
|
+
principal_id = data.get("data", {}).get("principal_id")
|
|
187
|
+
if principal_id:
|
|
188
|
+
return str(principal_id)
|
|
155
189
|
|
|
156
190
|
except Exception: # noqa: S110
|
|
157
191
|
# Silent failure - don't disrupt CLI
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arcade-core
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: Arcade Core - Core library for Arcade platform
|
|
5
5
|
Author-email: Arcade <dev@arcade.dev>
|
|
6
6
|
License: MIT
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: httpx>=0.27.0
|
|
16
17
|
Requires-Dist: loguru>=0.7.0
|
|
17
18
|
Requires-Dist: packaging>=24.1
|
|
18
19
|
Requires-Dist: pydantic>=2.7.0
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
arcade_core/__init__.py,sha256=1heu3AROAjpistehPzY2H-2nkj_IjQEh-vVlVOCRF1E,88
|
|
2
2
|
arcade_core/annotations.py,sha256=Nst6aejLWXlpTu7GwzWETu1gQCG1XVAUR_qcFbNvyRc,198
|
|
3
|
-
arcade_core/auth.py,sha256=
|
|
3
|
+
arcade_core/auth.py,sha256=flz3UNso5VXiKvrwygapiyQdBmI1Ca6JzFOO2caUSQQ,6145
|
|
4
|
+
arcade_core/auth_tokens.py,sha256=4VAd634lgGigNHezoy2dBpLztliG4Qd2GH8ZBdZVDNo,3169
|
|
4
5
|
arcade_core/catalog.py,sha256=AA13L-ZG4O0S5rB1Fjc-wpLaa6im-dwIBx7GsE2ZW80,43527
|
|
5
6
|
arcade_core/config.py,sha256=e98XQAkYySGW9T_yrJg54BB8Wuq06GPVHp7xqe2d1vU,572
|
|
6
|
-
arcade_core/config_model.py,sha256=
|
|
7
|
-
arcade_core/constants.py,sha256=
|
|
7
|
+
arcade_core/config_model.py,sha256=EA6XQWj-oJu_e4j0kuVT1K5fbwRvwv-lUYMrctcW2Ws,6504
|
|
8
|
+
arcade_core/constants.py,sha256=E3s22aGDhYDM_vkxdo7PZf8F3pznqM-NqQQzgIjhhD8,531
|
|
8
9
|
arcade_core/context.py,sha256=J2MgbVznhJC2qarHq3dTL72W4NGYOM1pjXdI_YwgkA4,3316
|
|
9
10
|
arcade_core/discovery.py,sha256=PluKGhNtJ7RYjJuPDMB8LCNinQLKzlqoAtc3dwKb6IA,8397
|
|
10
11
|
arcade_core/errors.py,sha256=fsi7m6TQQSsdSNHl4rBoSN_YH3ZV910gjvBFqB207f4,13326
|
|
@@ -17,12 +18,14 @@ arcade_core/toolkit.py,sha256=lLlOL6fA6Lmo-dtLTMMcPCzKDf9YQObwxG1LdVADv3E,14431
|
|
|
17
18
|
arcade_core/utils.py,sha256=_3bM-yfIDFmMVqt-NFYp2Lx1QcNWp7xytGjUQzPs2LY,3255
|
|
18
19
|
arcade_core/version.py,sha256=CpXi3jGlx23RvRyU7iytOMZrnspdWw4yofS8lpP1AJU,18
|
|
19
20
|
arcade_core/converters/openai.py,sha256=4efdgTkvdwT44VGStBhdUmzCnoP5dysceIqPVVPG-vk,7408
|
|
21
|
+
arcade_core/network/__init__.py,sha256=8GEcg6QAr63U8U1n4bnf7cL-F7FrUrc9otHM2J9ODwk,53
|
|
22
|
+
arcade_core/network/org_transport.py,sha256=DZdIYdldtYZ6gtmWA1C8dXmsNuOv7tePhlpBuAvkhRQ,3222
|
|
20
23
|
arcade_core/usage/__init__.py,sha256=SUR5mqF-bjdbl-P-OOHN6OFAjXZu4agXyPhr7xdVXCw,234
|
|
21
24
|
arcade_core/usage/__main__.py,sha256=rSJkE1G9hlV3HRRA6EJE5Lmy3wKyan7rAxBXHX9A1cI,1577
|
|
22
25
|
arcade_core/usage/constants.py,sha256=1FQIhkFFMZUhU-H4A7GvMb7KQ3qLFrNAZb2-LEvSF3k,1052
|
|
23
|
-
arcade_core/usage/identity.py,sha256=
|
|
26
|
+
arcade_core/usage/identity.py,sha256=egclRR26jGP1vVvoOoaaZdcS4AtSTZ8fLHpBq1HRgHw,8452
|
|
24
27
|
arcade_core/usage/usage_service.py,sha256=xzWWSEktm58liiNYugBHRactSru8V5foriHcsoH0j1A,3407
|
|
25
28
|
arcade_core/usage/utils.py,sha256=FqBOmlhwT68cbnpI5Vx9ZW6vLRYPVg4FJ0GaMEp8qEM,398
|
|
26
|
-
arcade_core-
|
|
27
|
-
arcade_core-
|
|
28
|
-
arcade_core-
|
|
29
|
+
arcade_core-4.0.0.dist-info/METADATA,sha256=6JLUpZN5EFPFt6LqXK3dHYMo7xgrWr_8wh_M7ahWBq0,2412
|
|
30
|
+
arcade_core-4.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
31
|
+
arcade_core-4.0.0.dist-info/RECORD,,
|