blaxel 0.64.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.
- blaxel/__init__.py +8 -0
- blaxel/agents/__init__.py +5 -0
- blaxel/agents/chain.py +153 -0
- blaxel/agents/chat.py +286 -0
- blaxel/agents/decorator.py +208 -0
- blaxel/agents/thread.py +24 -0
- blaxel/agents/voice/openai.py +255 -0
- blaxel/agents/voice/utils.py +25 -0
- blaxel/api/__init__.py +1 -0
- blaxel/api/agents/__init__.py +0 -0
- blaxel/api/agents/create_agent.py +155 -0
- blaxel/api/agents/delete_agent.py +146 -0
- blaxel/api/agents/get_agent.py +146 -0
- blaxel/api/agents/get_agent_logs.py +151 -0
- blaxel/api/agents/get_agent_metrics.py +150 -0
- blaxel/api/agents/get_agent_trace_ids.py +201 -0
- blaxel/api/agents/list_agent_revisions.py +155 -0
- blaxel/api/agents/list_agents.py +127 -0
- blaxel/api/agents/update_agent.py +168 -0
- blaxel/api/configurations/__init__.py +0 -0
- blaxel/api/configurations/get_configuration.py +122 -0
- blaxel/api/default/__init__.py +0 -0
- blaxel/api/default/get_trace.py +150 -0
- blaxel/api/default/get_trace_ids.py +218 -0
- blaxel/api/default/get_trace_logs.py +186 -0
- blaxel/api/default/list_mcp_hub_definitions.py +127 -0
- blaxel/api/functions/__init__.py +0 -0
- blaxel/api/functions/create_function.py +155 -0
- blaxel/api/functions/delete_function.py +146 -0
- blaxel/api/functions/get_function.py +146 -0
- blaxel/api/functions/get_function_logs.py +151 -0
- blaxel/api/functions/get_function_metrics.py +150 -0
- blaxel/api/functions/get_function_trace_ids.py +201 -0
- blaxel/api/functions/list_function_revisions.py +158 -0
- blaxel/api/functions/list_functions.py +131 -0
- blaxel/api/functions/update_function.py +168 -0
- blaxel/api/integrations/__init__.py +0 -0
- blaxel/api/integrations/create_integration_connection.py +167 -0
- blaxel/api/integrations/delete_integration_connection.py +158 -0
- blaxel/api/integrations/get_integration.py +97 -0
- blaxel/api/integrations/get_integration_connection.py +158 -0
- blaxel/api/integrations/get_integration_connection_model.py +104 -0
- blaxel/api/integrations/get_integration_connection_model_endpoint_configurations.py +97 -0
- blaxel/api/integrations/list_integration_connection_models.py +97 -0
- blaxel/api/integrations/list_integration_connections.py +139 -0
- blaxel/api/integrations/update_integration_connection.py +180 -0
- blaxel/api/invitations/__init__.py +0 -0
- blaxel/api/invitations/list_all_pending_invitations.py +142 -0
- blaxel/api/knowledgebases/__init__.py +0 -0
- blaxel/api/knowledgebases/create_knowledgebase.py +163 -0
- blaxel/api/knowledgebases/delete_knowledgebase.py +154 -0
- blaxel/api/knowledgebases/get_knowledgebase.py +154 -0
- blaxel/api/knowledgebases/list_knowledgebase_revisions.py +158 -0
- blaxel/api/knowledgebases/list_knowledgebases.py +139 -0
- blaxel/api/knowledgebases/update_knowledgebase.py +176 -0
- blaxel/api/locations/__init__.py +0 -0
- blaxel/api/locations/list_locations.py +139 -0
- blaxel/api/metrics/__init__.py +0 -0
- blaxel/api/metrics/get_metrics.py +130 -0
- blaxel/api/models/__init__.py +0 -0
- blaxel/api/models/create_model.py +163 -0
- blaxel/api/models/delete_model.py +154 -0
- blaxel/api/models/get_model.py +154 -0
- blaxel/api/models/get_model_logs.py +155 -0
- blaxel/api/models/get_model_metrics.py +158 -0
- blaxel/api/models/get_model_trace_ids.py +201 -0
- blaxel/api/models/list_model_revisions.py +158 -0
- blaxel/api/models/list_models.py +135 -0
- blaxel/api/models/update_model.py +176 -0
- blaxel/api/policies/__init__.py +0 -0
- blaxel/api/policies/create_policy.py +167 -0
- blaxel/api/policies/delete_policy.py +154 -0
- blaxel/api/policies/get_policy.py +154 -0
- blaxel/api/policies/list_policies.py +139 -0
- blaxel/api/policies/update_policy.py +180 -0
- blaxel/api/privateclusters/__init__.py +0 -0
- blaxel/api/privateclusters/create_private_cluster.py +132 -0
- blaxel/api/privateclusters/delete_private_cluster.py +156 -0
- blaxel/api/privateclusters/get_private_cluster.py +159 -0
- blaxel/api/privateclusters/get_private_cluster_health.py +97 -0
- blaxel/api/privateclusters/list_private_clusters.py +140 -0
- blaxel/api/privateclusters/update_private_cluster.py +156 -0
- blaxel/api/privateclusters/update_private_cluster_health.py +97 -0
- blaxel/api/service_accounts/__init__.py +0 -0
- blaxel/api/service_accounts/create_api_key_for_service_account.py +177 -0
- blaxel/api/service_accounts/create_workspace_service_account.py +170 -0
- blaxel/api/service_accounts/delete_api_key_for_service_account.py +104 -0
- blaxel/api/service_accounts/delete_workspace_service_account.py +160 -0
- blaxel/api/service_accounts/get_workspace_service_accounts.py +141 -0
- blaxel/api/service_accounts/list_api_keys_for_service_account.py +163 -0
- blaxel/api/service_accounts/update_workspace_service_account.py +183 -0
- blaxel/api/store/__init__.py +0 -0
- blaxel/api/store/get_store_agent.py +146 -0
- blaxel/api/store/get_store_function.py +146 -0
- blaxel/api/store/list_store_agents.py +131 -0
- blaxel/api/store/list_store_functions.py +131 -0
- blaxel/api/workspaces/__init__.py +0 -0
- blaxel/api/workspaces/accept_workspace_invitation.py +161 -0
- blaxel/api/workspaces/create_worspace.py +163 -0
- blaxel/api/workspaces/decline_workspace_invitation.py +158 -0
- blaxel/api/workspaces/delete_workspace.py +154 -0
- blaxel/api/workspaces/get_workspace.py +154 -0
- blaxel/api/workspaces/invite_workspace_user.py +174 -0
- blaxel/api/workspaces/leave_workspace.py +161 -0
- blaxel/api/workspaces/list_workspace_users.py +139 -0
- blaxel/api/workspaces/list_workspaces.py +139 -0
- blaxel/api/workspaces/remove_workspace_user.py +101 -0
- blaxel/api/workspaces/update_workspace.py +176 -0
- blaxel/api/workspaces/update_workspace_user_role.py +187 -0
- blaxel/authentication/__init__.py +45 -0
- blaxel/authentication/apikey.py +50 -0
- blaxel/authentication/authentication.py +176 -0
- blaxel/authentication/clientcredentials.py +103 -0
- blaxel/authentication/credentials.py +295 -0
- blaxel/authentication/device_mode.py +197 -0
- blaxel/client.py +281 -0
- blaxel/common/__init__.py +17 -0
- blaxel/common/error.py +27 -0
- blaxel/common/instrumentation.py +317 -0
- blaxel/common/logger.py +60 -0
- blaxel/common/secrets.py +39 -0
- blaxel/common/settings.py +150 -0
- blaxel/common/slugify.py +18 -0
- blaxel/common/utils.py +34 -0
- blaxel/deploy/__init__.py +8 -0
- blaxel/deploy/deploy.py +316 -0
- blaxel/deploy/format.py +46 -0
- blaxel/deploy/parser.py +192 -0
- blaxel/errors.py +16 -0
- blaxel/functions/__init__.py +7 -0
- blaxel/functions/common.py +228 -0
- blaxel/functions/decorator.py +64 -0
- blaxel/functions/local/local.py +48 -0
- blaxel/functions/mcp/client.py +96 -0
- blaxel/functions/mcp/mcp.py +168 -0
- blaxel/functions/mcp/utils.py +56 -0
- blaxel/functions/remote/remote.py +183 -0
- blaxel/models/__init__.py +233 -0
- blaxel/models/acl.py +133 -0
- blaxel/models/agent.py +126 -0
- blaxel/models/agent_chain.py +88 -0
- blaxel/models/agent_spec.py +346 -0
- blaxel/models/api_key.py +142 -0
- blaxel/models/configuration.py +85 -0
- blaxel/models/continent.py +70 -0
- blaxel/models/core_event.py +97 -0
- blaxel/models/core_spec.py +249 -0
- blaxel/models/core_spec_configurations.py +77 -0
- blaxel/models/country.py +70 -0
- blaxel/models/create_api_key_for_service_account_body.py +69 -0
- blaxel/models/create_workspace_service_account_body.py +71 -0
- blaxel/models/create_workspace_service_account_response_200.py +105 -0
- blaxel/models/delete_workspace_service_account_response_200.py +96 -0
- blaxel/models/entrypoint.py +96 -0
- blaxel/models/entrypoint_env.py +45 -0
- blaxel/models/flavor.py +70 -0
- blaxel/models/form.py +120 -0
- blaxel/models/form_config.py +45 -0
- blaxel/models/form_oauthomitempty.py +45 -0
- blaxel/models/form_secrets.py +45 -0
- blaxel/models/function.py +126 -0
- blaxel/models/function_kit.py +97 -0
- blaxel/models/function_spec.py +310 -0
- blaxel/models/get_trace_ids_response_200.py +45 -0
- blaxel/models/get_trace_logs_response_200.py +45 -0
- blaxel/models/get_trace_response_200.py +45 -0
- blaxel/models/get_workspace_service_accounts_response_200_item.py +96 -0
- blaxel/models/histogram_bucket.py +79 -0
- blaxel/models/histogram_stats.py +88 -0
- blaxel/models/integration_connection.py +96 -0
- blaxel/models/integration_connection_spec.py +114 -0
- blaxel/models/integration_connection_spec_config.py +45 -0
- blaxel/models/integration_connection_spec_secret.py +45 -0
- blaxel/models/integration_model.py +162 -0
- blaxel/models/integration_repository.py +88 -0
- blaxel/models/invite_workspace_user_body.py +60 -0
- blaxel/models/knowledgebase.py +126 -0
- blaxel/models/knowledgebase_spec.py +163 -0
- blaxel/models/knowledgebase_spec_options.py +45 -0
- blaxel/models/last_n_requests_metric.py +79 -0
- blaxel/models/latency_metric.py +144 -0
- blaxel/models/location_response.py +113 -0
- blaxel/models/mcp_definition.py +188 -0
- blaxel/models/mcp_definition_entrypoint.py +45 -0
- blaxel/models/mcp_definition_form.py +45 -0
- blaxel/models/metadata.py +139 -0
- blaxel/models/metadata_labels.py +45 -0
- blaxel/models/metric.py +79 -0
- blaxel/models/metrics.py +169 -0
- blaxel/models/metrics_models.py +45 -0
- blaxel/models/metrics_request_total_per_code.py +45 -0
- blaxel/models/metrics_rps_per_code.py +45 -0
- blaxel/models/model.py +126 -0
- blaxel/models/model_private_cluster.py +79 -0
- blaxel/models/model_spec.py +249 -0
- blaxel/models/o_auth.py +72 -0
- blaxel/models/owner_fields.py +70 -0
- blaxel/models/pending_invitation.py +124 -0
- blaxel/models/pending_invitation_accept.py +85 -0
- blaxel/models/pending_invitation_render.py +147 -0
- blaxel/models/pending_invitation_render_invited_by.py +88 -0
- blaxel/models/pending_invitation_render_workspace.py +70 -0
- blaxel/models/pending_invitation_workspace_details.py +72 -0
- blaxel/models/pod_template_spec.py +45 -0
- blaxel/models/policy.py +96 -0
- blaxel/models/policy_location.py +70 -0
- blaxel/models/policy_max_tokens.py +106 -0
- blaxel/models/policy_spec.py +151 -0
- blaxel/models/private_cluster.py +183 -0
- blaxel/models/private_location.py +61 -0
- blaxel/models/repository.py +70 -0
- blaxel/models/request_duration_over_time_metric.py +97 -0
- blaxel/models/request_duration_over_time_metrics.py +80 -0
- blaxel/models/request_total_by_origin_metric.py +115 -0
- blaxel/models/request_total_by_origin_metric_request_total_by_origin.py +45 -0
- blaxel/models/request_total_by_origin_metric_request_total_by_origin_and_code.py +45 -0
- blaxel/models/request_total_metric.py +123 -0
- blaxel/models/request_total_metric_request_total_per_code.py +45 -0
- blaxel/models/request_total_metric_rps_per_code.py +45 -0
- blaxel/models/resource_log.py +79 -0
- blaxel/models/resource_metrics.py +270 -0
- blaxel/models/resource_metrics_request_total_per_code.py +45 -0
- blaxel/models/resource_metrics_rps_per_code.py +45 -0
- blaxel/models/revision_configuration.py +97 -0
- blaxel/models/revision_metadata.py +124 -0
- blaxel/models/runtime.py +196 -0
- blaxel/models/runtime_startup_probe.py +45 -0
- blaxel/models/serverless_config.py +80 -0
- blaxel/models/spec_configuration.py +70 -0
- blaxel/models/store_agent.py +178 -0
- blaxel/models/store_agent_labels.py +45 -0
- blaxel/models/store_configuration.py +151 -0
- blaxel/models/store_configuration_option.py +79 -0
- blaxel/models/store_function.py +211 -0
- blaxel/models/store_function_kit.py +97 -0
- blaxel/models/store_function_labels.py +45 -0
- blaxel/models/store_function_parameter.py +88 -0
- blaxel/models/time_fields.py +70 -0
- blaxel/models/token_rate_metric.py +88 -0
- blaxel/models/token_rate_metrics.py +120 -0
- blaxel/models/token_total_metric.py +106 -0
- blaxel/models/trace_ids_response.py +45 -0
- blaxel/models/update_workspace_service_account_body.py +69 -0
- blaxel/models/update_workspace_service_account_response_200.py +96 -0
- blaxel/models/update_workspace_user_role_body.py +60 -0
- blaxel/models/websocket_channel.py +88 -0
- blaxel/models/workspace.py +148 -0
- blaxel/models/workspace_labels.py +45 -0
- blaxel/models/workspace_user.py +115 -0
- blaxel/py.typed +1 -0
- blaxel/run.py +108 -0
- blaxel/serve/app.py +131 -0
- blaxel/serve/middlewares/__init__.py +10 -0
- blaxel/serve/middlewares/accesslog.py +32 -0
- blaxel/serve/middlewares/processtime.py +28 -0
- blaxel/types.py +46 -0
- blaxel-0.64.0.dist-info/METADATA +96 -0
- blaxel-0.64.0.dist-info/RECORD +261 -0
- blaxel-0.64.0.dist-info/WHEEL +4 -0
- blaxel-0.64.0.dist-info/entry_points.txt +2 -0
- blaxel-0.64.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,103 @@
|
|
1
|
+
"""
|
2
|
+
This module provides the ClientCredentials class, which handles client credentials-based
|
3
|
+
authentication for Blaxel. It manages token refreshing and authentication flows using
|
4
|
+
client credentials and refresh tokens.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from dataclasses import dataclass
|
8
|
+
from datetime import datetime, timedelta
|
9
|
+
from typing import Generator, Optional
|
10
|
+
|
11
|
+
import requests
|
12
|
+
from httpx import Auth, Request, Response
|
13
|
+
|
14
|
+
from blaxel.aimon.settings import get_settings
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class DeviceLoginFinalizeResponse:
|
19
|
+
access_token: str
|
20
|
+
expires_in: int
|
21
|
+
refresh_token: str
|
22
|
+
token_type: str
|
23
|
+
|
24
|
+
|
25
|
+
class ClientCredentials(Auth):
|
26
|
+
"""
|
27
|
+
A provider that authenticates requests using client credentials.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(self, credentials, workspace_name: str, base_url: str):
|
31
|
+
"""
|
32
|
+
Initializes the ClientCredentials provider with the given credentials, workspace name, and base URL.
|
33
|
+
|
34
|
+
Parameters:
|
35
|
+
credentials: Credentials containing access and refresh tokens.
|
36
|
+
workspace_name (str): The name of the workspace.
|
37
|
+
base_url (str): The base URL for authentication.
|
38
|
+
"""
|
39
|
+
self.credentials = credentials
|
40
|
+
self.expires_at = None
|
41
|
+
self.workspace_name = workspace_name
|
42
|
+
self.base_url = base_url
|
43
|
+
|
44
|
+
def get_headers(self):
|
45
|
+
"""
|
46
|
+
Retrieves the authentication headers after ensuring tokens are valid.
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
dict: A dictionary of headers with Bearer token and workspace.
|
50
|
+
|
51
|
+
Raises:
|
52
|
+
Exception: If token refresh fails.
|
53
|
+
"""
|
54
|
+
err = self.get_token()
|
55
|
+
if err:
|
56
|
+
raise err
|
57
|
+
return {
|
58
|
+
"X-Blaxel-Authorization": f"Bearer {self.credentials.access_token}",
|
59
|
+
"X-Blaxel-Workspace": self.workspace_name,
|
60
|
+
}
|
61
|
+
|
62
|
+
def get_token(self) -> Optional[Exception]:
|
63
|
+
"""
|
64
|
+
Checks if the access token needs to be refreshed and performs the refresh if necessary.
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
Optional[Exception]: An exception if refreshing fails, otherwise None.
|
68
|
+
"""
|
69
|
+
settings = get_settings()
|
70
|
+
if self.need_token():
|
71
|
+
headers = {"Authorization": f"Basic {self.credentials.client_credentials}", "Content-Type": "application/json"}
|
72
|
+
body = {"grant_type": "client_credentials"}
|
73
|
+
response = requests.post(f"{settings.base_url}/oauth/token", headers=headers, json=body)
|
74
|
+
response.raise_for_status()
|
75
|
+
creds = response.json()
|
76
|
+
self.credentials.access_token = creds["access_token"]
|
77
|
+
self.credentials.refresh_token = creds["refresh_token"]
|
78
|
+
self.credentials.expires_in = creds["expires_in"]
|
79
|
+
self.expires_at = datetime.now() + timedelta(seconds=self.credentials.expires_in)
|
80
|
+
return None
|
81
|
+
|
82
|
+
def need_token(self):
|
83
|
+
if not self.expires_at:
|
84
|
+
return True
|
85
|
+
return datetime.now() > self.expires_at - timedelta(minutes=10)
|
86
|
+
|
87
|
+
def auth_flow(self, request: Request) -> Generator[Request, Response, None]:
|
88
|
+
"""
|
89
|
+
Processes the authentication flow by ensuring tokens are valid and adding necessary headers.
|
90
|
+
|
91
|
+
Parameters:
|
92
|
+
request (Request): The HTTP request to authenticate.
|
93
|
+
|
94
|
+
Yields:
|
95
|
+
Request: The authenticated request.
|
96
|
+
|
97
|
+
Raises:
|
98
|
+
Exception: If token refresh fails.
|
99
|
+
"""
|
100
|
+
self.get_token()
|
101
|
+
request.headers["X-Blaxel-Authorization"] = f"Bearer {self.credentials.access_token}"
|
102
|
+
request.headers["X-Blaxel-Workspace"] = self.workspace_name
|
103
|
+
yield request
|
@@ -0,0 +1,295 @@
|
|
1
|
+
"""
|
2
|
+
This module provides classes and functions for managing credentials and workspace configurations.
|
3
|
+
It includes functionalities to load, save, and manage authentication credentials, as well as to handle
|
4
|
+
workspace contexts and configurations.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from dataclasses import dataclass
|
8
|
+
from logging import getLogger
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import List
|
11
|
+
|
12
|
+
import yaml
|
13
|
+
|
14
|
+
from blaxel.aimon.settings import Settings
|
15
|
+
|
16
|
+
logger = getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
@dataclass
|
20
|
+
class Credentials:
|
21
|
+
"""
|
22
|
+
A dataclass representing user credentials for authentication.
|
23
|
+
|
24
|
+
Attributes:
|
25
|
+
apiKey (str): The API key.
|
26
|
+
access_token (str): The access token.
|
27
|
+
refresh_token (str): The refresh token.
|
28
|
+
expires_in (int): Token expiration time in seconds.
|
29
|
+
device_code (str): The device code for device authentication.
|
30
|
+
client_credentials (str): The client credentials for authentication.
|
31
|
+
"""
|
32
|
+
apiKey: str = ""
|
33
|
+
access_token: str = ""
|
34
|
+
refresh_token: str = ""
|
35
|
+
expires_in: int = 0
|
36
|
+
device_code: str = ""
|
37
|
+
client_credentials: str = ""
|
38
|
+
|
39
|
+
|
40
|
+
@dataclass
|
41
|
+
class WorkspaceConfig:
|
42
|
+
"""
|
43
|
+
A dataclass representing the configuration for a workspace.
|
44
|
+
|
45
|
+
Attributes:
|
46
|
+
name (str): The name of the workspace.
|
47
|
+
credentials (Credentials): The credentials associated with the workspace.
|
48
|
+
"""
|
49
|
+
name: str
|
50
|
+
credentials: Credentials
|
51
|
+
|
52
|
+
|
53
|
+
@dataclass
|
54
|
+
class ContextConfig:
|
55
|
+
"""
|
56
|
+
A dataclass representing the current context configuration.
|
57
|
+
|
58
|
+
Attributes:
|
59
|
+
workspace (str): The name of the current workspace.
|
60
|
+
"""
|
61
|
+
workspace: str = ""
|
62
|
+
|
63
|
+
|
64
|
+
@dataclass
|
65
|
+
class Config:
|
66
|
+
"""
|
67
|
+
A dataclass representing the overall configuration, including workspaces and context.
|
68
|
+
|
69
|
+
Attributes:
|
70
|
+
workspaces (List[WorkspaceConfig]): A list of workspace configurations.
|
71
|
+
context (ContextConfig): The current context configuration.
|
72
|
+
"""
|
73
|
+
workspaces: List[WorkspaceConfig] = None
|
74
|
+
context: ContextConfig = None
|
75
|
+
|
76
|
+
def __post_init__(self):
|
77
|
+
"""
|
78
|
+
Post-initialization to ensure workspaces and context are initialized.
|
79
|
+
"""
|
80
|
+
if self.workspaces is None:
|
81
|
+
self.workspaces = []
|
82
|
+
if self.context is None:
|
83
|
+
self.context = ContextConfig()
|
84
|
+
|
85
|
+
def to_json(self) -> dict:
|
86
|
+
"""
|
87
|
+
Converts the Config dataclass to a JSON-compatible dictionary.
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
dict: The JSON representation of the configuration.
|
91
|
+
"""
|
92
|
+
return {
|
93
|
+
"workspaces": [
|
94
|
+
{
|
95
|
+
"name": ws.name,
|
96
|
+
"credentials": {
|
97
|
+
"apiKey": ws.credentials.apiKey,
|
98
|
+
"access_token": ws.credentials.access_token,
|
99
|
+
"refresh_token": ws.credentials.refresh_token,
|
100
|
+
"expires_in": ws.credentials.expires_in,
|
101
|
+
"device_code": ws.credentials.device_code,
|
102
|
+
"client_credentials": ws.credentials.client_credentials,
|
103
|
+
},
|
104
|
+
}
|
105
|
+
for ws in self.workspaces
|
106
|
+
],
|
107
|
+
"context": {
|
108
|
+
"workspace": self.context.workspace,
|
109
|
+
},
|
110
|
+
}
|
111
|
+
|
112
|
+
|
113
|
+
def load_config() -> Config:
|
114
|
+
"""
|
115
|
+
Loads the configuration from the user's home directory.
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
Config: The loaded configuration.
|
119
|
+
"""
|
120
|
+
config = Config()
|
121
|
+
home_dir = Path.home()
|
122
|
+
if home_dir:
|
123
|
+
config_path = home_dir / ".blaxel" / "config.yaml"
|
124
|
+
if config_path.exists():
|
125
|
+
try:
|
126
|
+
with open(config_path) as f:
|
127
|
+
data = yaml.safe_load(f)
|
128
|
+
if data:
|
129
|
+
workspaces = []
|
130
|
+
for ws in data.get("workspaces", []):
|
131
|
+
creds = Credentials(**ws.get("credentials", {}))
|
132
|
+
workspaces.append(WorkspaceConfig(name=ws["name"], credentials=creds))
|
133
|
+
config.workspaces = workspaces
|
134
|
+
if "context" in data:
|
135
|
+
config.context = ContextConfig(workspace=data["context"].get("workspace", ""))
|
136
|
+
except yaml.YAMLError:
|
137
|
+
# Invalid YAML, use empty config
|
138
|
+
pass
|
139
|
+
return config
|
140
|
+
|
141
|
+
|
142
|
+
def save_config(config: Config):
|
143
|
+
"""
|
144
|
+
Saves the provided configuration to the user's home directory.
|
145
|
+
|
146
|
+
Parameters:
|
147
|
+
config (Config): The configuration to save.
|
148
|
+
|
149
|
+
Raises:
|
150
|
+
RuntimeError: If the home directory cannot be determined.
|
151
|
+
"""
|
152
|
+
home_dir = Path.home()
|
153
|
+
if not home_dir:
|
154
|
+
raise RuntimeError("Could not determine home directory")
|
155
|
+
|
156
|
+
config_dir = home_dir / ".blaxel"
|
157
|
+
config_file = config_dir / "config.yaml"
|
158
|
+
|
159
|
+
config_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
160
|
+
with open(config_file, "w", encoding="utf-8") as f:
|
161
|
+
yaml.dump(config.to_json(), f)
|
162
|
+
|
163
|
+
|
164
|
+
def list_workspaces() -> List[str]:
|
165
|
+
"""
|
166
|
+
Lists all available workspace names from the configuration.
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
List[str]: A list of workspace names.
|
170
|
+
"""
|
171
|
+
config = load_config()
|
172
|
+
return [workspace.name for workspace in config.workspaces]
|
173
|
+
|
174
|
+
|
175
|
+
def current_context() -> ContextConfig:
|
176
|
+
"""
|
177
|
+
Retrieves the current context configuration.
|
178
|
+
|
179
|
+
Returns:
|
180
|
+
ContextConfig: The current context configuration.
|
181
|
+
"""
|
182
|
+
config = load_config()
|
183
|
+
return config.context
|
184
|
+
|
185
|
+
|
186
|
+
def set_current_workspace(workspace_name: str):
|
187
|
+
"""
|
188
|
+
Sets the current workspace in the configuration.
|
189
|
+
|
190
|
+
Parameters:
|
191
|
+
workspace_name (str): The name of the workspace to set as current.
|
192
|
+
"""
|
193
|
+
config = load_config()
|
194
|
+
config.context.workspace = workspace_name
|
195
|
+
save_config(config)
|
196
|
+
|
197
|
+
|
198
|
+
def load_credentials(workspace_name: str) -> Credentials:
|
199
|
+
"""
|
200
|
+
Loads credentials for the specified workspace.
|
201
|
+
|
202
|
+
Parameters:
|
203
|
+
workspace_name (str): The name of the workspace whose credentials are to be loaded.
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
Credentials: The credentials associated with the workspace. Returns empty credentials if not found.
|
207
|
+
"""
|
208
|
+
config = load_config()
|
209
|
+
for workspace in config.workspaces:
|
210
|
+
if workspace.name == workspace_name:
|
211
|
+
return workspace.credentials
|
212
|
+
return Credentials()
|
213
|
+
|
214
|
+
|
215
|
+
def load_credentials_from_settings(settings: Settings) -> Credentials:
|
216
|
+
"""
|
217
|
+
Loads credentials from the provided settings.
|
218
|
+
|
219
|
+
Parameters:
|
220
|
+
settings (Settings): The settings containing authentication information.
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
Credentials: The loaded credentials from settings.
|
224
|
+
"""
|
225
|
+
return Credentials(
|
226
|
+
apiKey=settings.authentication.apiKey,
|
227
|
+
client_credentials=settings.authentication.client.credentials,
|
228
|
+
)
|
229
|
+
|
230
|
+
|
231
|
+
def create_home_dir_if_missing():
|
232
|
+
"""
|
233
|
+
Creates the Blaxel home directory if it does not exist.
|
234
|
+
|
235
|
+
Logs a warning if credentials already exist or an error if directory creation fails.
|
236
|
+
"""
|
237
|
+
home_dir = Path.home()
|
238
|
+
if not home_dir:
|
239
|
+
logger.error("Error getting home directory")
|
240
|
+
return
|
241
|
+
|
242
|
+
credentials_dir = home_dir / ".blaxel"
|
243
|
+
credentials_file = credentials_dir / "credentials.json"
|
244
|
+
|
245
|
+
if credentials_file.exists():
|
246
|
+
logger.warning("You are already logged in. Enter a new API key to overwrite it.")
|
247
|
+
else:
|
248
|
+
try:
|
249
|
+
credentials_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
250
|
+
except Exception as e:
|
251
|
+
logger.error(f"Error creating credentials directory: {e}")
|
252
|
+
|
253
|
+
|
254
|
+
def save_credentials(workspace_name: str, credentials: Credentials):
|
255
|
+
"""
|
256
|
+
Saves the provided credentials for the specified workspace.
|
257
|
+
|
258
|
+
Parameters:
|
259
|
+
workspace_name (str): The name of the workspace.
|
260
|
+
credentials (Credentials): The credentials to save.
|
261
|
+
"""
|
262
|
+
create_home_dir_if_missing()
|
263
|
+
if not credentials.access_token and not credentials.apiKey:
|
264
|
+
logger.info("No credentials to save, error")
|
265
|
+
return
|
266
|
+
|
267
|
+
config = load_config()
|
268
|
+
found = False
|
269
|
+
|
270
|
+
for i, workspace in enumerate(config.workspaces):
|
271
|
+
if workspace.name == workspace_name:
|
272
|
+
config.workspaces[i].credentials = credentials
|
273
|
+
found = True
|
274
|
+
break
|
275
|
+
|
276
|
+
if not found:
|
277
|
+
config.workspaces.append(WorkspaceConfig(name=workspace_name, credentials=credentials))
|
278
|
+
|
279
|
+
save_config(config)
|
280
|
+
|
281
|
+
|
282
|
+
def clear_credentials(workspace_name: str):
|
283
|
+
"""
|
284
|
+
Clears the credentials for the specified workspace.
|
285
|
+
|
286
|
+
Parameters:
|
287
|
+
workspace_name (str): The name of the workspace whose credentials are to be cleared.
|
288
|
+
"""
|
289
|
+
config = load_config()
|
290
|
+
config.workspaces = [ws for ws in config.workspaces if ws.name != workspace_name]
|
291
|
+
|
292
|
+
if config.context.workspace == workspace_name:
|
293
|
+
config.context.workspace = ""
|
294
|
+
|
295
|
+
save_config(config)
|
@@ -0,0 +1,197 @@
|
|
1
|
+
"""
|
2
|
+
This module provides classes for handling device-based authentication,
|
3
|
+
including device login processes and bearer token management. It facilitates token refreshing
|
4
|
+
and authentication flows using device codes and bearer tokens.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import base64
|
8
|
+
import json
|
9
|
+
from dataclasses import dataclass
|
10
|
+
from datetime import datetime, timedelta
|
11
|
+
from typing import Dict, Generator, Optional
|
12
|
+
|
13
|
+
from httpx import Auth, Request, Response, post
|
14
|
+
|
15
|
+
|
16
|
+
@dataclass
|
17
|
+
class DeviceLogin:
|
18
|
+
"""
|
19
|
+
A dataclass representing a device login request.
|
20
|
+
|
21
|
+
Attributes:
|
22
|
+
client_id (str): The client ID for the device.
|
23
|
+
scope (str): The scope of the authentication.
|
24
|
+
"""
|
25
|
+
client_id: str
|
26
|
+
scope: str
|
27
|
+
|
28
|
+
|
29
|
+
@dataclass
|
30
|
+
class DeviceLoginResponse:
|
31
|
+
"""
|
32
|
+
A dataclass representing the response from a device login request.
|
33
|
+
|
34
|
+
Attributes:
|
35
|
+
client_id (str): The client ID associated with the device login.
|
36
|
+
device_code (str): The device code for authentication.
|
37
|
+
user_code (str): The user code for completing authentication.
|
38
|
+
expires_in (int): Time in seconds until the device code expires.
|
39
|
+
interval (int): Polling interval in seconds.
|
40
|
+
verification_uri (str): URI for user to verify device login.
|
41
|
+
verification_uri_complete (str): Complete URI including the user code for verification.
|
42
|
+
"""
|
43
|
+
client_id: str
|
44
|
+
device_code: str
|
45
|
+
user_code: str
|
46
|
+
expires_in: int
|
47
|
+
interval: int
|
48
|
+
verification_uri: str
|
49
|
+
verification_uri_complete: str
|
50
|
+
|
51
|
+
|
52
|
+
@dataclass
|
53
|
+
class DeviceLoginFinalizeRequest:
|
54
|
+
"""
|
55
|
+
A dataclass representing a device login finalize request.
|
56
|
+
|
57
|
+
Attributes:
|
58
|
+
grant_type (str): The type of grant being requested.
|
59
|
+
client_id (str): The client ID for finalizing the device login.
|
60
|
+
device_code (str): The device code to finalize login.
|
61
|
+
"""
|
62
|
+
grant_type: str
|
63
|
+
client_id: str
|
64
|
+
device_code: str
|
65
|
+
|
66
|
+
|
67
|
+
@dataclass
|
68
|
+
class DeviceLoginFinalizeResponse:
|
69
|
+
access_token: str
|
70
|
+
expires_in: int
|
71
|
+
refresh_token: str
|
72
|
+
token_type: str
|
73
|
+
|
74
|
+
|
75
|
+
class BearerToken(Auth):
|
76
|
+
"""
|
77
|
+
A provider that authenticates requests using a Bearer token.
|
78
|
+
"""
|
79
|
+
|
80
|
+
def __init__(self, credentials, workspace_name: str, base_url: str):
|
81
|
+
"""
|
82
|
+
Initializes the BearerToken provider with the given credentials, workspace name, and base URL.
|
83
|
+
|
84
|
+
Parameters:
|
85
|
+
credentials: Credentials containing the Bearer token and refresh token.
|
86
|
+
workspace_name (str): The name of the workspace.
|
87
|
+
base_url (str): The base URL for authentication.
|
88
|
+
"""
|
89
|
+
self.credentials = credentials
|
90
|
+
self.workspace_name = workspace_name
|
91
|
+
self.base_url = base_url
|
92
|
+
|
93
|
+
def get_headers(self) -> Dict[str, str]:
|
94
|
+
"""
|
95
|
+
Retrieves the authentication headers containing the Bearer token and workspace information.
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
Dict[str, str]: A dictionary of headers with Bearer token and workspace.
|
99
|
+
|
100
|
+
Raises:
|
101
|
+
Exception: If token refresh fails.
|
102
|
+
"""
|
103
|
+
err = self.refresh_if_needed()
|
104
|
+
if err:
|
105
|
+
raise err
|
106
|
+
return {
|
107
|
+
"X-Blaxel-Authorization": f"Bearer {self.credentials.access_token}",
|
108
|
+
"X-Blaxel-Workspace": self.workspace_name,
|
109
|
+
}
|
110
|
+
|
111
|
+
def refresh_if_needed(self) -> Optional[Exception]:
|
112
|
+
"""
|
113
|
+
Checks if the Bearer token needs to be refreshed and performs the refresh if necessary.
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
Optional[Exception]: An exception if refreshing fails, otherwise None.
|
117
|
+
"""
|
118
|
+
# Need to refresh token if expires in less than 10 minutes
|
119
|
+
parts = self.credentials.access_token.split(".")
|
120
|
+
if len(parts) != 3:
|
121
|
+
return Exception("Invalid JWT token format")
|
122
|
+
|
123
|
+
try:
|
124
|
+
claims_bytes = base64.urlsafe_b64decode(parts[1] + "=" * (-len(parts[1]) % 4))
|
125
|
+
claims = json.loads(claims_bytes)
|
126
|
+
except Exception as e:
|
127
|
+
return Exception(f"Failed to decode/parse JWT claims: {str(e)}")
|
128
|
+
exp_time = datetime.fromtimestamp(claims["exp"])
|
129
|
+
current_time = datetime.now()
|
130
|
+
# Refresh if token expires in less than 10 minutes
|
131
|
+
if current_time + timedelta(minutes=10) > exp_time:
|
132
|
+
return self.do_refresh()
|
133
|
+
|
134
|
+
return None
|
135
|
+
|
136
|
+
def auth_flow(self, request: Request) -> Generator[Request, Response, None]:
|
137
|
+
"""
|
138
|
+
Processes the authentication flow by ensuring the Bearer token is valid and adding necessary headers.
|
139
|
+
|
140
|
+
Parameters:
|
141
|
+
request (Request): The HTTP request to authenticate.
|
142
|
+
|
143
|
+
Yields:
|
144
|
+
Request: The authenticated request.
|
145
|
+
|
146
|
+
Raises:
|
147
|
+
Exception: If token refresh fails.
|
148
|
+
"""
|
149
|
+
err = self.refresh_if_needed()
|
150
|
+
if err:
|
151
|
+
return err
|
152
|
+
|
153
|
+
request.headers["X-Blaxel-Authorization"] = f"Bearer {self.credentials.access_token}"
|
154
|
+
request.headers["X-Blaxel-Workspace"] = self.workspace_name
|
155
|
+
yield request
|
156
|
+
|
157
|
+
def do_refresh(self) -> Optional[Exception]:
|
158
|
+
"""
|
159
|
+
Performs the token refresh using the refresh token.
|
160
|
+
|
161
|
+
Returns:
|
162
|
+
Optional[Exception]: An exception if refreshing fails, otherwise None.
|
163
|
+
"""
|
164
|
+
if not self.credentials.refresh_token:
|
165
|
+
return Exception("No refresh token to refresh")
|
166
|
+
|
167
|
+
url = f"{self.base_url}/oauth/token"
|
168
|
+
refresh_data = {
|
169
|
+
"grant_type": "refresh_token",
|
170
|
+
"refresh_token": self.credentials.refresh_token,
|
171
|
+
"device_code": self.credentials.device_code,
|
172
|
+
"client_id": "blaxel",
|
173
|
+
}
|
174
|
+
|
175
|
+
try:
|
176
|
+
response = post(url, json=refresh_data, headers={"Content-Type": "application/json"})
|
177
|
+
response.raise_for_status()
|
178
|
+
finalize_response = DeviceLoginFinalizeResponse(**response.json())
|
179
|
+
|
180
|
+
if not finalize_response.refresh_token:
|
181
|
+
finalize_response.refresh_token = self.credentials.refresh_token
|
182
|
+
|
183
|
+
from .credentials import Credentials, save_credentials
|
184
|
+
|
185
|
+
creds = Credentials(
|
186
|
+
access_token=finalize_response.access_token,
|
187
|
+
refresh_token=finalize_response.refresh_token,
|
188
|
+
expires_in=finalize_response.expires_in,
|
189
|
+
device_code=self.credentials.device_code,
|
190
|
+
)
|
191
|
+
|
192
|
+
self.credentials = creds
|
193
|
+
save_credentials(self.workspace_name, creds)
|
194
|
+
return None
|
195
|
+
|
196
|
+
except Exception as e:
|
197
|
+
return Exception(f"Failed to refresh token: {str(e)}")
|